diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:45:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 12:45:49 +0000 |
commit | 0ff39c83d38ce538a9f5dba53eca0fa9cb16d9e6 (patch) | |
tree | 84c735df2e97350a721273e9dd425729d43cc8a2 | |
parent | Initial commit. (diff) | |
download | icingaweb2-module-pdfexport-0ff39c83d38ce538a9f5dba53eca0fa9cb16d9e6.tar.xz icingaweb2-module-pdfexport-0ff39c83d38ce538a9f5dba53eca0fa9cb16d9e6.zip |
Adding upstream version 0.10.2+dfsg1.upstream/0.10.2+dfsg1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
209 files changed, 23317 insertions, 0 deletions
diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 0000000..feb3c1c --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,46 @@ +<!--- Provide a general summary of the issue in the Title above --> + +<!-- Formatting tips: + +GitHub supports Markdown: https://guides.github.com/features/mastering-markdown/ +Multi-line code blocks either with three back ticks, or four space indent. + +``` +Stacktrace ... +<line> +<line> +``` +--> + +## Expected Behavior +<!--- If you're describing a bug, tell us what should happen --> +<!--- If you're suggesting a change/improvement, tell us how it should work --> + +## Current Behavior +<!--- If describing a bug, tell us what happens instead of the expected behavior --> +<!--- If suggesting a change/improvement, explain the difference from current behavior --> + +## Possible Solution +<!--- Not obligatory, but suggest a fix/reason for the bug, --> +<!--- or ideas how to implement: the addition or change --> + +## Steps to Reproduce (for bugs) +<!--- Provide a link to a live example, or an unambiguous set of steps to --> +<!--- reproduce this bug. Include configuration, logs, etc. to reproduce, if relevant --> +1. +2. +3. +4. + +## Context +<!--- How has this issue affected you? What are you trying to accomplish? --> +<!--- Providing context helps us come up with a solution that is most useful in the real world --> + +## Your Environment +<!--- Include as many relevant details about the environment you experienced the problem in --> +* Module version (System - About): +* Icinga Web 2 version and modules (System - About): +* Icinga 2 version (`icinga2 --version`): +* Operating System and version: +* Webserver, PHP versions: + diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..10e5f43 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,43 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +## Describe the bug + +A clear and concise description of what the bug is. + +## To Reproduce + +Steps to reproduce the behavior: + +1. +2. +3. +4. + +## Expected behavior + +A clear and concise description of what you expected to happen. + +## Screenshots + +If applicable, add screenshots to help explain your problem. + +## Your Environment + +* Module version: +* Dependent module versions: +* Icinga Web 2 version and modules (System - About): +* Chrome/Chromium version (`google-chrome --version`): +* Web browser and version: +* PHP version used (`php --version`): +* Server operating system and version: + +## Additional context + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..a7621fd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: '' +assignees: '' + +--- + +## Is your feature request related to a problem? Please describe. +A clear and concise description of what the problem is. Ex. I'm always using this feature but am missing [...] + +## Describe the solution you'd like +A clear and concise description of what you want to happen. + +## Describe alternatives you've considered +A clear and concise description of any alternative solutions or features you've considered. + +## Additional context +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/L10n-update.yml b/.github/workflows/L10n-update.yml new file mode 100644 index 0000000..9dce59a --- /dev/null +++ b/.github/workflows/L10n-update.yml @@ -0,0 +1,20 @@ +name: L10n Update + +on: + push: + branches: + - master + +jobs: + trigger-update: + name: L10n Update Trigger + runs-on: ubuntu-latest + + steps: + - name: Repository dispatch + uses: peter-evans/repository-dispatch@v1 + with: + token: ${{ secrets.ICINGABOT_TOKEN }} + repository: Icinga/L10n + event-type: update + client-payload: '{"origin": "${{ github.repository }}", "commit": "${{ github.sha }}"}' diff --git a/.github/workflows/php.yml b/.github/workflows/php.yml new file mode 100644 index 0000000..8321fa1 --- /dev/null +++ b/.github/workflows/php.yml @@ -0,0 +1,50 @@ +name: PHP Tests + +on: + push: + branches: + - master + - release/* + pull_request: + branches: + - master + +jobs: + lint: + name: Static analysis for php ${{ matrix.php }} on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + + strategy: + fail-fast: false + matrix: + php: ['7.2', '7.3', '7.4', '8.0', '8.1'] + os: ['ubuntu-latest'] + + steps: + - name: Checkout code base + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: phpcs + + # The cleanup is required because otherwise the tests fail on PHP 8+. + # Some of our deps are not officially compatible with that but will + # pass the lint tests. Removing composer.json and composer.lock first + # ensures that the test dep setup succeeds. Not removing vendor, but + # only renaming it ensures the files are scanned still. + - name: Cleanup + run: rm composer.* && mv vendor real-vendor + + - name: Setup dependencies + run: composer require -n --no-progress overtrue/phplint + + - name: PHP Lint + if: success() || matrix.allow_failure + run: ./vendor/bin/phplint -n --exclude={^vendor/.*} -- . + + - name: PHP CodeSniffer + if: success() || matrix.allow_failure + run: phpcs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1f03d4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Exclude all hidden files +.* + +# Except those related to Git +!.git* diff --git a/.phpcs.xml b/.phpcs.xml new file mode 100644 index 0000000..2a93e4b --- /dev/null +++ b/.phpcs.xml @@ -0,0 +1,26 @@ +<?xml version="1.0"?> +<ruleset name="PHP_CodeSniffer"> + <description>Sniff our code a while</description> + + <file>./</file> + + <exclude-pattern>vendor/*</exclude-pattern> + + <arg name="report-width" value="auto"/> + <arg name="report-full"/> + <arg name="report-gitblame"/> + <arg name="report-summary"/> + <arg name="encoding" value="UTF-8"/> + <arg name="extensions" value="php"/> + + <rule ref="PSR12"> + <exclude name="PSR12.Properties.ConstantVisibility.NotFound"/> + </rule> + + <rule ref="Generic.Files.LineLength"> + <properties> + <property name="lineLimit" value="120"/> + <property name="absoluteLineLimit" value="0"/> + </properties> + </rule> +</ruleset> @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/> + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + {description} + Copyright (C) {year} {fullname} + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + {signature of Ty Coon}, 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a64884 --- /dev/null +++ b/README.md @@ -0,0 +1,12 @@ +# Icinga PDF Export + +[![PHP Support](https://img.shields.io/badge/php-%3E%3D%207.2-777BB4?logo=PHP)](https://php.net/) +![Build Status](https://github.com/icinga/icingaweb2-module-pdfexport/workflows/PHP%20Tests/badge.svg?branch=master) +[![Github Tag](https://img.shields.io/github/tag/Icinga/icingaweb2-module-pdfexport.svg)](https://github.com/Icinga/icingaweb2-module-pdfexport) + +PDF export functionality for Icinga Web 2 using Google Chrome/Chromium for rendering. + +## Documentation + +* [Installation](doc/02-Installation.md) +* [Troubleshooting](doc/70-Troubleshooting.md) diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php new file mode 100644 index 0000000..7b5130b --- /dev/null +++ b/application/controllers/ConfigController.php @@ -0,0 +1,30 @@ +<?php + +/* Icinga PDF Export | (c) 2019 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Pdfexport\Controllers; + +use Icinga\Application\Config; +use Icinga\Module\Pdfexport\Forms\ChromeBinaryForm; +use Icinga\Web\Controller; + +class ConfigController extends Controller +{ + public function init() + { + $this->assertPermission('config/modules'); + + parent::init(); + } + + public function chromeAction() + { + $form = (new ChromeBinaryForm()) + ->setIniConfig(Config::module('pdfexport')); + + $form->handleRequest(); + + $this->view->tabs = $this->Module()->getConfigTabs()->activate('chrome'); + $this->view->form = $form; + } +} diff --git a/application/forms/ChromeBinaryForm.php b/application/forms/ChromeBinaryForm.php new file mode 100644 index 0000000..c72d933 --- /dev/null +++ b/application/forms/ChromeBinaryForm.php @@ -0,0 +1,94 @@ +<?php + +/* Icinga PDF Export | (c) 2019 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Pdfexport\Forms; + +use Exception; +use Icinga\Forms\ConfigForm; +use Icinga\Module\Pdfexport\HeadlessChrome; +use Zend_Validate_Callback; + +class ChromeBinaryForm extends ConfigForm +{ + public function init() + { + $this->setName('pdfexport_binary'); + $this->setSubmitLabel($this->translate('Save Changes')); + } + + public function createElements(array $formData) + { + $this->addElement('text', 'chrome_binary', [ + 'label' => $this->translate('Local Binary'), + 'placeholder' => '/usr/bin/google-chrome', + 'validators' => [new Zend_Validate_Callback(function ($value) { + $chrome = (new HeadlessChrome()) + ->setBinary($value); + + try { + $version = $chrome->getVersion(); + } catch (Exception $e) { + $this->getElement('chrome_binary')->addError($e->getMessage()); + return true; + } + + if ($version < 59) { + $this->getElement('chrome_binary')->addError(sprintf( + $this->translate( + 'Chrome/Chromium supporting headless mode required' + . ' which is provided since version 59. Version detected: %s' + ), + $version + )); + } + + return true; + })] + ]); + + $this->addElement('checkbox', 'chrome_force_temp_storage', [ + 'label' => $this->translate('Force local temp storage') + ]); + + $this->addElement('text', 'chrome_host', [ + 'label' => $this->translate('Remote Host'), + 'validators' => [new Zend_Validate_Callback(function ($value) { + if ($value === null) { + return true; + } + + $port = $this->getValue('chrome_port') ?: 9222; + + $chrome = (new HeadlessChrome()) + ->setRemote($value, $port); + + try { + $version = $chrome->getVersion(); + } catch (Exception $e) { + $this->getElement('chrome_host')->addError($e->getMessage()); + return true; + } + + if ($version < 59) { + $this->getElement('chrome_host')->addError(sprintf( + $this->translate( + 'Chrome/Chromium supporting headless mode required' + . ' which is provided since version 59. Version detected: %s' + ), + $version + )); + } + + return true; + })] + ]); + + $this->addElement('number', 'chrome_port', [ + 'label' => $this->translate('Remote Port'), + 'placeholder' => 9222, + 'min' => 1, + 'max' => 65535 + ]); + } +} diff --git a/application/views/scripts/config/chrome.phtml b/application/views/scripts/config/chrome.phtml new file mode 100644 index 0000000..46daf07 --- /dev/null +++ b/application/views/scripts/config/chrome.phtml @@ -0,0 +1,6 @@ +<div class="controls"> + <?= /** @var \Icinga\Web\Widget\Tabs $tabs */ $tabs ?> +</div> +<div class="content"> + <?= /** @var \Icinga\Module\Pdfexport\Forms\ChromeBinaryForm $form */ $form ?> +</div> diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..c56521a --- /dev/null +++ b/composer.json @@ -0,0 +1,6 @@ +{ + "require": { + "textalk/websocket": "^1.5", + "iio/libmergepdf": "^4.0" + } +} diff --git a/composer.lock b/composer.lock new file mode 100644 index 0000000..788a21a --- /dev/null +++ b/composer.lock @@ -0,0 +1,301 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "410e13f411197a49c927ab5e9a9d1c4f", + "packages": [ + { + "name": "iio/libmergepdf", + "version": "4.0.4", + "source": { + "type": "git", + "url": "https://github.com/hanneskod/libmergepdf.git", + "reference": "6613b978c08d00d559796ab510614243e4dd5dfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hanneskod/libmergepdf/zipball/6613b978c08d00d559796ab510614243e4dd5dfb", + "reference": "6613b978c08d00d559796ab510614243e4dd5dfb", + "shasum": "" + }, + "require": { + "php": "^7.1||^8.0", + "setasign/fpdi": "^2", + "tecnickcom/tcpdf": "^6.2.22" + }, + "conflict": { + "rafikhaceb/tcpdi": "*", + "setasign/fpdf": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7|^8", + "smalot/pdfparser": "~0.13" + }, + "type": "library", + "autoload": { + "psr-4": { + "iio\\libmergepdf\\": "src/" + }, + "classmap": [ + "tcpdi/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Hannes Forsgård", + "email": "hannes.forsgard@fripost.org" + } + ], + "description": "Library for merging multiple PDFs", + "homepage": "https://github.com/hanneskod/libmergepdf", + "keywords": [ + "merge", + "pdf" + ], + "time": "2020-12-07T12:18:49+00:00" + }, + { + "name": "psr/log", + "version": "1.1.4", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2021-05-03T11:20:27+00:00" + }, + { + "name": "setasign/fpdi", + "version": "v2.3.6", + "source": { + "type": "git", + "url": "https://github.com/Setasign/FPDI.git", + "reference": "6231e315f73e4f62d72b73f3d6d78ff0eed93c31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/6231e315f73e4f62d72b73f3d6d78ff0eed93c31", + "reference": "6231e315f73e4f62d72b73f3d6d78ff0eed93c31", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "conflict": { + "setasign/tfpdf": "<1.31" + }, + "require-dev": { + "phpunit/phpunit": "~5.7", + "setasign/fpdf": "~1.8", + "setasign/tfpdf": "1.31", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "~6.2" + }, + "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." + }, + "type": "library", + "autoload": { + "psr-4": { + "setasign\\Fpdi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Slabon", + "email": "jan.slabon@setasign.com", + "homepage": "https://www.setasign.com" + }, + { + "name": "Maximilian Kresse", + "email": "maximilian.kresse@setasign.com", + "homepage": "https://www.setasign.com" + } + ], + "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", + "homepage": "https://www.setasign.com/fpdi", + "keywords": [ + "fpdf", + "fpdi", + "pdf" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ], + "time": "2021-02-11T11:37:01+00:00" + }, + { + "name": "tecnickcom/tcpdf", + "version": "6.4.2", + "source": { + "type": "git", + "url": "https://github.com/tecnickcom/TCPDF.git", + "reference": "172540dcbfdf8dc983bc2fe78feff48ff7ec1c76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/172540dcbfdf8dc983bc2fe78feff48ff7ec1c76", + "reference": "172540dcbfdf8dc983bc2fe78feff48ff7ec1c76", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "config", + "include", + "tcpdf.php", + "tcpdf_parser.php", + "tcpdf_import.php", + "tcpdf_barcodes_1d.php", + "tcpdf_barcodes_2d.php", + "include/tcpdf_colors.php", + "include/tcpdf_filters.php", + "include/tcpdf_font_data.php", + "include/tcpdf_fonts.php", + "include/tcpdf_images.php", + "include/tcpdf_static.php", + "include/barcodes/datamatrix.php", + "include/barcodes/pdf417.php", + "include/barcodes/qrcode.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-only" + ], + "authors": [ + { + "name": "Nicola Asuni", + "email": "info@tecnick.com", + "role": "lead" + } + ], + "description": "TCPDF is a PHP class for generating PDF documents and barcodes.", + "homepage": "http://www.tcpdf.org/", + "keywords": [ + "PDFD32000-2008", + "TCPDF", + "barcodes", + "datamatrix", + "pdf", + "pdf417", + "qrcode" + ], + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tcpdf%20project", + "type": "custom" + } + ], + "time": "2021-07-20T14:43:20+00:00" + }, + { + "name": "textalk/websocket", + "version": "1.5.5", + "source": { + "type": "git", + "url": "https://github.com/Textalk/websocket-php.git", + "reference": "846542f82658132cd36acb7a7e8ce0f03960c295" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/846542f82658132cd36acb7a7e8ce0f03960c295", + "reference": "846542f82658132cd36acb7a7e8ce0f03960c295", + "shasum": "" + }, + "require": { + "php": "^7.2 | ^8.0", + "psr/log": "^1 | ^2 | ^3" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^8.0|^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "WebSocket\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen", + "email": "soren@abicart.se" + } + ], + "description": "WebSocket client and server", + "time": "2021-08-07T10:21:40+00:00" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [], + "plugin-api-version": "1.1.0" +} diff --git a/configuration.php b/configuration.php new file mode 100644 index 0000000..f9ebcbe --- /dev/null +++ b/configuration.php @@ -0,0 +1,11 @@ +<?php + +/* Icinga PDF Export | (c) 2019 Icinga GmbH | GPLv2 */ + +/** @var \Icinga\Application\Modules\Module $this */ + +$this->provideConfigTab('chrome', array( + 'title' => $this->translate('Configure the Chrome/Chromium connection'), + 'label' => $this->translate('Chrome'), + 'url' => 'config/chrome' +)); diff --git a/doc/02-Installation.md b/doc/02-Installation.md new file mode 100644 index 0000000..82a63a9 --- /dev/null +++ b/doc/02-Installation.md @@ -0,0 +1,86 @@ +# Installation <a id="installation"></a> + +## Requirements <a id="installation-requirements"></a> + +* PHP (>= 7.2) +* Icinga Web 2 (>= 2.9) +* Icinga Web 2 libraries: + * [Icinga PHP Library (ipl)](https://github.com/Icinga/icinga-php-library) (>= 0.8) + * [Icinga PHP Thirdparty](https://github.com/Icinga/icinga-php-thirdparty) (>= 0.10) + +## Google Chrome/Chromium Setup <a id="installation-chrome-setup"></a> + +The module needs Google Chrome or Chromium supporting headless mode. + +### RHEL/CentOS <a id="installation-chrome-setup-rhel"></a> + +Add the Chrome repository from Google to yum, next to EPEL. + +``` +yum -y install epel-release + +cat >/etc/yum.repos.d/google-chrome-stable.repo <<EOF +[google-chrome-stable] +name=google-chrome-stable +baseurl=http://dl.google.com/linux/chrome/rpm/stable/\$basearch +enabled=1 +gpgcheck=1 +gpgkey=https://dl-ssl.google.com/linux/linux_signing_key.pub +EOF + +yum makecache +``` + +Install Chrome and additional dependencies (optional). + +``` +yum install google-chrome-stable +yum install mesa-libOSMesa mesa-libOSMesa-devel gnu-free-sans-fonts ipa-gothic-fonts ipa-pgothic-fonts +``` + +### Debian/Ubuntu <a id="installation-chrome-setup-rhel"></a> + +Add the Chrome repository from Google to apt. + +``` +apt-get -y install apt-transport-https gnupg wget + +wget -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - + +echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list + +apt-get update +``` + +Install Chrome. + +``` +apt-get install google-chrome-stable +``` + +## Module Installation <a id="installation-module"></a> + +1. Install it [like any other module](https://icinga.com/docs/icinga-web-2/latest/doc/08-Modules/#installation). +Use `pdfexport` as name. + +2. You might need to set the absolute path to the Google Chrome / Chromium +binary, depending on your system. This can be done in +`Configuration -> Modules -> pdfexport -> Chrome` + +This concludes the installation. PDF exports now use Google Chrome/Chromium for rendering. + +### Using a Remote Chrome/Chromium + +As an alternative to a local installation of Chrome/Chromium it is also possible +to launch and utilize a remote instance. + +Just install it as described above on a different machine and configure its connection +details in `Configuration -> Modules -> pdfexport -> Chrome`. + +To start a remote instance of Chrome/Chromium use the following commandline options: + +> google-chrome --remote-debugging-address=0.0.0.0 --remote-debugging-port=9222 --headless --keep-alive-for-test --disable-gpu --disable-dev-shm-usage --no-sandbox --bwsi --no-first-run --user-data-dir=/tmp --homedir=/tmp + +Note that the browser does accept any and all connection attempts without any authentication. +Keep that in mind and let it listen on a public IP (or even on 0.0.0.0) only during tests or +with a proper firewall in place. diff --git a/doc/70-Troubleshooting.md b/doc/70-Troubleshooting.md new file mode 100644 index 0000000..0cb7fa3 --- /dev/null +++ b/doc/70-Troubleshooting.md @@ -0,0 +1,16 @@ +# Troubleshooting <a id="troubleshooting"></a> + +## PDF Export <a id="troubleshooting-pdf-export"></a> + +If the PDF export fails, ensure that Chrome headless works fine. +You can test that on the CLI like this: + +``` +google-chrome --version +``` + +If you have a local installation, you could also try to force temporary local +storage. (Available in the module's configuration) This will store the content +to print on disk, instead of transferring it directly to the browser. Note that +for this to work, the browser needs to be able to access the temporary files of +the webserver's process user. diff --git a/library/Pdfexport/HeadlessChrome.php b/library/Pdfexport/HeadlessChrome.php new file mode 100644 index 0000000..0612987 --- /dev/null +++ b/library/Pdfexport/HeadlessChrome.php @@ -0,0 +1,701 @@ +<?php + +/* Icinga PDF Export | (c) 2018 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Pdfexport; + +use Exception; +use Icinga\Application\Logger; +use Icinga\Application\Platform; +use Icinga\File\Storage\StorageInterface; +use Icinga\File\Storage\TemporaryLocalFileStorage; +use ipl\Html\HtmlString; +use LogicException; +use React\ChildProcess\Process; +use React\EventLoop\Factory; +use React\EventLoop\TimerInterface; +use WebSocket\Client; +use WebSocket\ConnectionException; + +class HeadlessChrome +{ + /** + * Line of stderr output identifying the websocket url + * + * First matching group is the used port and the second one the browser id. + */ + const DEBUG_ADDR_PATTERN = '/DevTools listening on ws:\/\/((?>\d+\.?){4}:\d+)\/devtools\/browser\/([\w-]+)/'; + + /** @var string */ + const WAIT_FOR_NETWORK = 'wait-for-network'; + + /** @var string Javascript Promise to wait for layout initialization */ + const WAIT_FOR_LAYOUT = <<<JS +new Promise((fulfill, reject) => { + let timeoutId = setTimeout(() => reject('fail'), 10000); + + if (document.documentElement.dataset.layoutReady === 'yes') { + clearTimeout(timeoutId); + fulfill(null); + return; + } + + document.addEventListener('layout-ready', e => { + clearTimeout(timeoutId); + fulfill(e.detail); + }, { + once: true + }); +}) +JS; + + /** @var string Path to the Chrome binary */ + protected $binary; + + /** @var array Host and port to the remote Chrome */ + protected $remote; + + /** + * The document to print + * + * @var PrintableHtmlDocument + */ + protected $document; + + /** @var string Target Url */ + protected $url; + + /** @var StorageInterface */ + protected $fileStorage; + + /** @var array */ + private $interceptedRequests = []; + + /** @var array */ + private $interceptedEvents = []; + + /** + * Get the path to the Chrome binary + * + * @return string + */ + public function getBinary() + { + return $this->binary; + } + + /** + * Set the path to the Chrome binary + * + * @param string $binary + * + * @return $this + */ + public function setBinary($binary) + { + $this->binary = $binary; + + return $this; + } + + /** + * Get host and port combination of the remote chrome + * + * @return array + */ + public function getRemote() + { + return $this->remote; + } + + /** + * Set host and port combination of a remote chrome + * + * @param string $host + * @param int $port + * + * @return $this + */ + public function setRemote($host, $port) + { + $this->remote = [$host, $port]; + + return $this; + } + + /** + * Get the target Url + * + * @return string + */ + public function getUrl() + { + return $this->url; + } + + /** + * Set the target Url + * + * @param string $url + * + * @return $this + */ + public function setUrl($url) + { + $this->url = $url; + + return $this; + } + + /** + * Get the file storage + * + * @return StorageInterface + */ + public function getFileStorage() + { + if ($this->fileStorage === null) { + $this->fileStorage = new TemporaryLocalFileStorage(); + } + + return $this->fileStorage; + } + + /** + * Set the file storage + * + * @param StorageInterface $fileStorage + * + * @return $this + */ + public function setFileStorage($fileStorage) + { + $this->fileStorage = $fileStorage; + + return $this; + } + + /** + * Render the given argument name-value pairs as shell-escaped string + * + * @param array $arguments + * + * @return string + */ + public static function renderArgumentList(array $arguments) + { + $list = []; + + foreach ($arguments as $name => $value) { + if ($value !== null) { + $value = escapeshellarg($value); + + if (! is_int($name)) { + if (substr($name, -1) === '=') { + $glue = ''; + } else { + $glue = ' '; + } + + $list[] = escapeshellarg($name) . $glue . $value; + } else { + $list[] = $value; + } + } else { + $list[] = escapeshellarg($name); + } + } + + return implode(' ', $list); + } + + /** + * Use the given HTML as input + * + * @param string|PrintableHtmlDocument $html + * @param bool $asFile + * @return $this + */ + public function fromHtml($html, $asFile = false) + { + if ($html instanceof PrintableHtmlDocument) { + $this->document = $html; + } else { + $this->document = (new PrintableHtmlDocument()) + ->setContent(HtmlString::create($html)); + } + + if ($asFile) { + $path = uniqid('icingaweb2-pdfexport-') . '.html'; + $storage = $this->getFileStorage(); + + $storage->create($path, $this->document->render()); + + $path = $storage->resolvePath($path, true); + + $this->setUrl("file://$path"); + } + + return $this; + } + + /** + * Export to PDF + * + * @return string + * @throws Exception + */ + public function toPdf() + { + switch (true) { + case $this->remote !== null: + try { + $result = $this->jsonVersion($this->remote[0], $this->remote[1]); + $parts = explode('/', $result['webSocketDebuggerUrl']); + $pdf = $this->printToPDF(join(':', $this->remote), end($parts), isset($this->document) + ? $this->document->getPrintParameters() + : []); + break; + } catch (Exception $e) { + if ($this->binary === null) { + throw $e; + } else { + Logger::warning( + 'Failed to connect to remote chrome: %s:%d (%s)', + $this->remote[0], + $this->remote[1], + $e + ); + } + } + + // Fallback to the local binary if a remote chrome is unavailable + case $this->binary !== null: + $browserHome = $this->getFileStorage()->resolvePath('HOME'); + $commandLine = join(' ', [ + escapeshellarg($this->getBinary()), + static::renderArgumentList([ + '--bwsi', + '--headless', + '--disable-gpu', + '--no-sandbox', + '--no-first-run', + '--disable-dev-shm-usage', + '--remote-debugging-port=0', + '--homedir=' => $browserHome, + '--user-data-dir=' => $browserHome + ]) + ]); + + if (Platform::isLinux()) { + Logger::debug('Starting browser process: HOME=%s exec %s', $browserHome, $commandLine); + $chrome = new Process('exec ' . $commandLine, null, ['HOME' => $browserHome]); + } else { + Logger::debug('Starting browser process: %s', $commandLine); + $chrome = new Process($commandLine); + } + + $loop = Factory::create(); + + $killer = $loop->addTimer(10, function (TimerInterface $timer) use ($chrome) { + $chrome->terminate(6); // SIGABRT + Logger::error( + 'Terminated browser process after %d seconds elapsed without the expected output', + $timer->getInterval() + ); + }); + + $chrome->start($loop); + + $pdf = null; + $chrome->stderr->on('data', function ($chunk) use (&$pdf, $chrome, $loop, $killer) { + Logger::debug('Caught browser output: %s', $chunk); + + if (preg_match(self::DEBUG_ADDR_PATTERN, trim($chunk), $matches)) { + $loop->cancelTimer($killer); + + try { + $pdf = $this->printToPDF($matches[1], $matches[2], isset($this->document) + ? $this->document->getPrintParameters() + : []); + } catch (Exception $e) { + Logger::error('Failed to print PDF. An error occurred: %s', $e); + } + + $chrome->terminate(); + } + }); + + $chrome->on('exit', function ($exitCode, $termSignal) use ($loop, $killer) { + $loop->cancelTimer($killer); + + Logger::debug('Browser terminated by signal %d and exited with code %d', $termSignal, $exitCode); + }); + + $loop->run(); + } + + if (empty($pdf)) { + throw new Exception( + 'Received empty response or none at all from browser.' + . ' Please check the logs for further details.' + ); + } + + return $pdf; + } + + /** + * Export to PDF and save as file on disk + * + * @return string The path to the file on disk + */ + public function savePdf() + { + $path = uniqid('icingaweb2-pdfexport-') . '.pdf'; + + $storage = $this->getFileStorage(); + $storage->create($path, ''); + + $path = $storage->resolvePath($path, true); + file_put_contents($path, $this->toPdf()); + + return $path; + } + + private function printToPDF($socket, $browserId, array $parameters) + { + $browser = new Client(sprintf('ws://%s/devtools/browser/%s', $socket, $browserId)); + + // Open new tab, get its id + $result = $this->communicate($browser, 'Target.createTarget', [ + 'url' => 'about:blank' + ]); + if (isset($result['targetId'])) { + $targetId = $result['targetId']; + } else { + throw new Exception('Expected target id. Got instead: ' . json_encode($result)); + } + + $page = new Client(sprintf('ws://%s/devtools/page/%s', $socket, $targetId), ['timeout' => 300]); + + // enable various events + $this->communicate($page, 'Log.enable'); + $this->communicate($page, 'Network.enable'); + $this->communicate($page, 'Page.enable'); + + try { + $this->communicate($page, 'Console.enable'); + } catch (Exception $_) { + // Deprecated, might fail + } + + if (($url = $this->getUrl()) !== null) { + // Navigate to target + $result = $this->communicate($page, 'Page.navigate', [ + 'url' => $url + ]); + if (isset($result['frameId'])) { + $frameId = $result['frameId']; + } else { + throw new Exception('Expected navigation frame. Got instead: ' . json_encode($result)); + } + + // wait for page to fully load + $this->waitFor($page, 'Page.frameStoppedLoading', ['frameId' => $frameId]); + } elseif (isset($this->document)) { + // If there's no url to load transfer the document's content directly + $this->communicate($page, 'Page.setDocumentContent', [ + 'frameId' => $targetId, + 'html' => $this->document->render() + ]); + + // wait for page to fully load + $this->waitFor($page, 'Page.loadEventFired'); + } else { + throw new LogicException('Nothing to print'); + } + + // Wait for network activity to finish + $this->waitFor($page, self::WAIT_FOR_NETWORK); + + // Wait for layout to initialize + if (isset($this->document)) { + // Ensure layout scripts work in the same environment as the pdf printing itself + $this->communicate($page, 'Emulation.setEmulatedMedia', ['media' => 'print']); + + $this->communicate($page, 'Runtime.evaluate', [ + 'timeout' => 1000, + 'expression' => 'setTimeout(() => new Layout().apply(), 0)' + ]); + + $promisedResult = $this->communicate($page, 'Runtime.evaluate', [ + 'awaitPromise' => true, + 'returnByValue' => true, + 'timeout' => 1000, // Failsafe, doesn't apply to `await` it seems + 'expression' => static::WAIT_FOR_LAYOUT + ]); + if (isset($promisedResult['exceptionDetails'])) { + if (isset($promisedResult['exceptionDetails']['exception']['description'])) { + Logger::error( + 'PDF layout failed to initialize: %s', + $promisedResult['exceptionDetails']['exception']['description'] + ); + } else { + Logger::warning('PDF layout failed to initialize. Pages might look skewed.'); + } + } + + // Reset media emulation, this may prevent the real media from coming into effect? + $this->communicate($page, 'Emulation.setEmulatedMedia', ['media' => '']); + } + + // print pdf + $result = $this->communicate($page, 'Page.printToPDF', array_merge( + $parameters, + ['transferMode' => 'ReturnAsBase64', 'printBackground' => true] + )); + if (isset($result['data']) && !empty($result['data'])) { + $pdf = base64_decode($result['data']); + } else { + throw new Exception('Expected base64 data. Got instead: ' . json_encode($result)); + } + + // close tab + $result = $this->communicate($browser, 'Target.closeTarget', [ + 'targetId' => $targetId + ]); + if (! isset($result['success'])) { + throw new Exception('Expected close confirmation. Got instead: ' . json_encode($result)); + } + + try { + $browser->close(); + } catch (ConnectionException $e) { + // For some reason, the browser doesn't send a response + Logger::debug(sprintf('Failed to close browser connection: ' . $e->getMessage())); + } + + return $pdf; + } + + private function renderApiCall($method, $options = null) + { + $data = [ + 'id' => time(), + 'method' => $method, + 'params' => $options ?: [] + ]; + + return json_encode($data, JSON_FORCE_OBJECT); + } + + private function parseApiResponse($payload) + { + $data = json_decode($payload, true); + if (isset($data['method']) || isset($data['result'])) { + return $data; + } elseif (isset($data['error'])) { + throw new Exception(sprintf( + 'Error response (%s): %s', + $data['error']['code'], + $data['error']['message'] + )); + } else { + throw new Exception(sprintf('Unknown response received: %s', $payload)); + } + } + + private function registerEvent($method, $params) + { + if (Logger::getInstance()->getLevel() === Logger::DEBUG) { + $shortenValues = function ($params) use (&$shortenValues) { + foreach ($params as &$value) { + if (is_array($value)) { + $value = $shortenValues($value); + } elseif (is_string($value)) { + $shortened = substr($value, 0, 256); + if ($shortened !== $value) { + $value = $shortened . '...'; + } + } + } + + return $params; + }; + $shortenedParams = $shortenValues($params); + + Logger::debug( + 'Received CDP event: %s(%s)', + $method, + join(',', array_map(function ($param) use ($shortenedParams) { + return $param . '=' . json_encode($shortenedParams[$param]); + }, array_keys($shortenedParams))) + ); + } + + if ($method === 'Network.requestWillBeSent') { + $this->interceptedRequests[$params['requestId']] = $params; + } elseif ($method === 'Network.loadingFinished') { + unset($this->interceptedRequests[$params['requestId']]); + } elseif ($method === 'Network.loadingFailed') { + $requestData = $this->interceptedRequests[$params['requestId']]; + unset($this->interceptedRequests[$params['requestId']]); + + Logger::error( + 'Headless Chrome was unable to complete a request to "%s". Error: %s', + $requestData['request']['url'], + $params['errorText'] + ); + } else { + $this->interceptedEvents[] = ['method' => $method, 'params' => $params]; + } + } + + private function communicate(Client $ws, $method, $params = null) + { + Logger::debug('Transmitting CDP call: %s(%s)', $method, $params ? join(',', array_keys($params)) : ''); + $ws->send($this->renderApiCall($method, $params)); + + do { + $response = $this->parseApiResponse($ws->receive()); + $gotEvent = isset($response['method']); + + if ($gotEvent) { + $this->registerEvent($response['method'], $response['params']); + } + } while ($gotEvent); + + Logger::debug('Received CDP result: %s', empty($response['result']) + ? 'none' + : join(',', array_keys($response['result']))); + + return $response['result']; + } + + private function waitFor(Client $ws, $eventName, array $expectedParams = null) + { + if ($eventName !== self::WAIT_FOR_NETWORK) { + Logger::debug( + 'Awaiting CDP event: %s(%s)', + $eventName, + $expectedParams ? join(',', array_keys($expectedParams)) : '' + ); + } elseif (empty($this->interceptedRequests)) { + return null; + } + + $wait = true; + $interceptedPos = -1; + + do { + if (isset($this->interceptedEvents[++$interceptedPos])) { + $response = $this->interceptedEvents[$interceptedPos]; + $intercepted = true; + } else { + $response = $this->parseApiResponse($ws->receive()); + $intercepted = false; + } + + if (isset($response['method'])) { + $method = $response['method']; + $params = $response['params']; + + if (! $intercepted) { + $this->registerEvent($method, $params); + } + + if ($eventName === self::WAIT_FOR_NETWORK) { + $wait = ! empty($this->interceptedRequests); + } elseif ($method === $eventName) { + if ($expectedParams !== null) { + $diff = array_intersect_assoc($params, $expectedParams); + $wait = empty($diff); + } else { + $wait = false; + } + } + + if (! $wait && $intercepted) { + unset($this->interceptedEvents[$interceptedPos]); + } + } + } while ($wait); + + return $params; + } + + /** + * Get the major version number of Chrome or false on failure + * + * @return int|false + * + * @throws Exception + */ + public function getVersion() + { + switch (true) { + case $this->remote !== null: + try { + $result = $this->jsonVersion($this->remote[0], $this->remote[1]); + $version = $result['Browser']; + break; + } catch (Exception $e) { + if ($this->binary === null) { + throw $e; + } else { + Logger::warning( + 'Failed to connect to remote chrome: %s:%d (%s)', + $this->remote[0], + $this->remote[1], + $e + ); + } + } + + // Fallback to the local binary if a remote chrome is unavailable + case $this->binary !== null: + $command = new ShellCommand( + escapeshellarg($this->getBinary()) . ' ' . static::renderArgumentList(['--version']), + false + ); + + $output = $command->execute(); + + if ($command->getExitCode() !== 0) { + throw new \Exception($output->stderr); + } + + $version = $output->stdout; + break; + default: + throw new LogicException('Set a binary or remote first'); + } + + if (preg_match('/(\d+)\.[\d.]+/', $version, $match)) { + return (int) $match[1]; + } + + return false; + } + + /** + * Fetch result from the /json/version API endpoint + * + * @param string $host + * @param int $port + * + * @return bool|array + */ + protected function jsonVersion($host, $port) + { + $client = new \GuzzleHttp\Client(); + $response = $client->request('GET', sprintf('http://%s:%s/json/version', $host, $port)); + + if ($response->getStatusCode() !== 200) { + return false; + } + + return json_decode($response->getBody(), true); + } +} diff --git a/library/Pdfexport/PrintStyleSheet.php b/library/Pdfexport/PrintStyleSheet.php new file mode 100644 index 0000000..71a235a --- /dev/null +++ b/library/Pdfexport/PrintStyleSheet.php @@ -0,0 +1,25 @@ +<?php + +/* Icinga PDF Export | (c) 2021 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Pdfexport; + +use Icinga\Application\Icinga; +use Icinga\Web\StyleSheet; + +class PrintStyleSheet extends StyleSheet +{ + protected function collect() + { + parent::collect(); + + $this->lessCompiler->setTheme(join(DIRECTORY_SEPARATOR, [ + Icinga::app()->getModuleManager()->getModule('pdfexport')->getCssDir(), + 'print.less' + ])); + + if (method_exists($this->lessCompiler, 'setThemeMode')) { + $this->lessCompiler->setThemeMode($this->pubPath . '/css/modes/none.less'); + } + } +} diff --git a/library/Pdfexport/PrintableHtmlDocument.php b/library/Pdfexport/PrintableHtmlDocument.php new file mode 100644 index 0000000..c1807e5 --- /dev/null +++ b/library/Pdfexport/PrintableHtmlDocument.php @@ -0,0 +1,542 @@ +<?php + +/* Icinga PDF Export | (c) 2019 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Pdfexport; + +use Icinga\Application\Icinga; +use Icinga\Web\UrlParams; +use ipl\Html\Attributes; +use ipl\Html\BaseHtmlElement; +use ipl\Html\HtmlDocument; +use ipl\Html\HtmlElement; +use ipl\Html\HtmlString; +use ipl\Html\Text; +use ipl\Html\ValidHtml; + +class PrintableHtmlDocument extends BaseHtmlElement +{ + /** @var string */ + const DEFAULT_HEADER_FOOTER_STYLE = <<<'CSS' +@font-face { + font-family: "Helvetica Neue", "Helvetica", "Arial", sans-serif; +} + +header, footer { + width: 100%; + height: 21px; + font-size: 7px; + display: flex; + justify-content: space-between; + margin-left: 0.75cm; + margin-right: 0.75cm; +} + +header > *:not(:last-child), +footer > *:not(:last-child) { + margin-right: 10px; +} + +span { + line-height: 7px; + white-space: nowrap; +} + +p { + margin: 0; + line-height: 7px; + word-break: break-word; + text-overflow: ellipsis; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 3; + -webkit-box-orient: vertical; +} +CSS; + + /** @var string Document title */ + protected $title; + + /** + * Paper orientation + * + * Defaults to false. + * + * @var bool + */ + protected $landscape; + + /** + * Print background graphics + * + * Defaults to false. + * + * @var bool + */ + protected $printBackground; + + /** + * Scale of the webpage rendering + * + * Defaults to 1. + * + * @var float + */ + protected $scale; + + /** + * Paper width in inches + * + * Defaults to 8.5 inches. + * + * @var float + */ + protected $paperWidth; + + /** + * Paper height in inches + * + * Defaults to 11 inches. + * + * @var float + */ + protected $paperHeight; + + /** + * Top margin in inches + * + * Defaults to 1cm (~0.4 inches). + * + * @var float + */ + protected $marginTop; + + /** + * Bottom margin in inches + * + * Defaults to 1cm (~0.4 inches). + * + * @var float + */ + protected $marginBottom; + + /** + * Left margin in inches + * + * Defaults to 1cm (~0.4 inches). + * + * @var float + */ + protected $marginLeft; + + /** + * Right margin in inches + * + * Defaults to 1cm (~0.4 inches). + * + * @var float + */ + protected $marginRight; + + /** + * Paper ranges to print, e.g., '1-5, 8, 11-13' + * + * Defaults to the empty string, which means print all pages + * + * @var string + */ + protected $pageRanges; + + /** + * Page height in pixels + * + * Minus the default vertical margins, this is 1035. + * If the vertical margins are zero, it's 1160. + * Whether there's a header or footer doesn't matter in any case. + * + * @var int + */ + protected $pagePixelHeight = 1035; + + /** + * HTML template for the print header + * + * Should be valid HTML markup with following classes used to inject printing values into them: + * * date: formatted print date + * * title: document title + * * url: document location + * * pageNumber: current page number + * * totalPages: total pages in the document + * + * For example, `<span class=title></span>` would generate span containing the title. + * + * Note that the header cannot exceed a height of 21px regardless of the margin's height or document's scale. + * With the default style, this height is separated by three lines, each accommodating 7px. + * Use `span`'s for single line text and `p`'s for multiline text. + * + * @var ValidHtml + */ + protected $headerTemplate; + + /** + * HTML template for the print footer + * + * Should be valid HTML markup with following classes used to inject printing values into them: + * * date: formatted print date + * * title: document title + * * url: document location + * * pageNumber: current page number + * * totalPages: total pages in the document + * + * For example, `<span class=title></span>` would generate span containing the title. + * + * Note that the footer cannot exceed a height of 21px regardless of the margin's height or document's scale. + * With the default style, this height is separated by three lines, each accommodating 7px. + * Use `span`'s for single line text and `p`'s for multiline text. + * + * @var ValidHtml + */ + protected $footerTemplate; + + /** + * HTML for the cover page + * + * @var ValidHtml + */ + protected $coverPage; + + /** + * Whether or not to prefer page size as defined by css + * + * Defaults to false, in which case the content will be scaled to fit the paper size. + * + * @var bool + */ + protected $preferCSSPageSize; + + protected $tag = 'body'; + + /** + * Get the document title + * + * @return string + */ + public function getTitle() + { + return $this->title; + } + + /** + * Set the document title + * + * @param string $title + * + * @return $this + */ + public function setTitle($title) + { + $this->title = $title; + + return $this; + } + + /** + * Set page header + * + * @param ValidHtml $header + * + * @return $this + */ + public function setHeader(ValidHtml $header) + { + $this->headerTemplate = $header; + + return $this; + } + + /** + * Set page footer + * + * @param ValidHtml $footer + * + * @return $this + */ + public function setFooter(ValidHtml $footer) + { + $this->footerTemplate = $footer; + + return $this; + } + + /** + * Get the cover page + * + * @return ValidHtml|null + */ + public function getCoverPage() + { + return $this->coverPage; + } + + /** + * Set cover page + * + * @param ValidHtml $coverPage + * + * @return $this + */ + public function setCoverPage(ValidHtml $coverPage) + { + $this->coverPage = $coverPage; + + return $this; + } + + /** + * Remove page margins + * + * @return $this + */ + public function removeMargins() + { + $this->marginBottom = 0; + $this->marginLeft = 0; + $this->marginRight = 0; + $this->marginTop = 0; + + return $this; + } + + /** + * Finalize document to be printed + */ + protected function assemble() + { + $this->setWrapper(new HtmlElement( + 'html', + null, + new HtmlElement( + 'head', + null, + new HtmlElement( + 'title', + null, + Text::create($this->title) + ), + $this->createStylesheet(), + $this->createLayoutScript() + ) + )); + + $this->getAttributes()->registerAttributeCallback('data-content-height', function () { + return $this->pagePixelHeight; + }); + $this->getAttributes()->registerAttributeCallback('style', function () { + return sprintf('width: %sin;', $this->paperWidth ?: 8.5); + }); + } + + /** + * Get the parameters for Page.printToPDF + * + * @return array + */ + public function getPrintParameters() + { + $parameters = []; + + if (isset($this->landscape)) { + $parameters['landscape'] = $this->landscape; + } + + if (isset($this->printBackground)) { + $parameters['printBackground'] = $this->printBackground; + } + + if (isset($this->scale)) { + $parameters['scale'] = $this->scale; + } + + if (isset($this->paperWidth)) { + $parameters['paperWidth'] = $this->paperWidth; + } + + if (isset($this->paperHeight)) { + $parameters['paperHeight'] = $this->paperHeight; + } + + if (isset($this->marginTop)) { + $parameters['marginTop'] = $this->marginTop; + } + + if (isset($this->marginBottom)) { + $parameters['marginBottom'] = $this->marginBottom; + } + + if (isset($this->marginLeft)) { + $parameters['marginLeft'] = $this->marginLeft; + } + + if (isset($this->marginRight)) { + $parameters['marginRight'] = $this->marginRight; + } + + if (isset($this->pageRanges)) { + $parameters['pageRanges'] = $this->pageRanges; + } + + if (isset($this->headerTemplate)) { + $parameters['headerTemplate'] = $this->createHeader()->render(); + $parameters['displayHeaderFooter'] = true; + + if (! isset($parameters['marginTop'])) { + $parameters['marginTop'] = 0.6; + } + } else { + $parameters['headerTemplate'] = ' '; // An empty string is ignored + } + + if (isset($this->footerTemplate)) { + $parameters['footerTemplate'] = $this->createFooter()->render(); + $parameters['displayHeaderFooter'] = true; + + if (! isset($parameters['marginBottom'])) { + $parameters['marginBottom'] = 0.6; + } + } else { + $parameters['footerTemplate'] = ' '; // An empty string is ignored + } + + if (isset($this->preferCSSPageSize)) { + $parameters['preferCSSPageSize'] = $this->preferCSSPageSize; + } + + return $parameters; + } + + /** + * Create CSS stylesheet + * + * @return ValidHtml + */ + protected function createStylesheet(): ValidHtml + { + $app = Icinga::app(); + + $css = preg_replace_callback( + '~(?<=url\()[\'"]?([^(\'"]*)[\'"]?(?=\))~', + function ($matches) use ($app) { + if (substr($matches[1], 0, 3) !== '../') { + return $matches[1]; + } + + $path = substr($matches[1], 3); + if (substr($path, 0, 4) === 'lib/') { + $assetPath = substr($path, 4); + + $library = null; + foreach ($app->getLibraries() as $candidate) { + if (substr($assetPath, 0, strlen($candidate->getName())) === $candidate->getName()) { + $library = $candidate; + $assetPath = ltrim(substr($assetPath, strlen($candidate->getName())), '/'); + break; + } + } + + if ($library === null) { + return $matches[1]; + } + + $path = $library->getStaticAssetPath() . DIRECTORY_SEPARATOR . $assetPath; + } elseif (substr($matches[1], 0, 14) === '../static/img?') { + list($_, $query) = explode('?', $matches[1], 2); + $params = UrlParams::fromQueryString($query); + if (! $app->getModuleManager()->hasEnabled($params->get('module_name'))) { + return $matches[1]; + } + + $module = $app->getModuleManager()->getModule($params->get('module_name')); + $imgRoot = $module->getBaseDir() . '/public/img/'; + $path = realpath($imgRoot . $params->get('file')); + } else { + $path = $app->getBootstrapDirectory() . '/' . $path; + } + + if (! $path || ! file_exists($path) || ! is_file($path)) { + return $matches[1]; + } + + $mimeType = @mime_content_type($path); + if ($mimeType === false) { + return $matches[1]; + } + + $fileContent = @file_get_contents($path); + if ($fileContent === false) { + return $matches[1]; + } + + return "'data:$mimeType; base64, " . base64_encode($fileContent) . "'"; + }, + (new PrintStyleSheet())->render(true) + ); + + return new HtmlElement('style', null, HtmlString::create($css)); + } + + /** + * Create layout javascript + * + * @return ValidHtml + */ + protected function createLayoutScript(): ValidHtml + { + $module = Icinga::app()->getModuleManager()->getModule('pdfexport'); + if (! method_exists($module, 'getJsDir')) { + $jsPath = join(DIRECTORY_SEPARATOR, [$module->getBaseDir(), 'public', 'js']); + } else { + $jsPath = $module->getJsDir(); + } + + $layoutJS = file_get_contents($jsPath . '/layout.js') . "\n\n\n"; + $layoutJS .= file_get_contents($jsPath . '/layout-plugins/page-breaker.js') . "\n\n\n"; + + return new HtmlElement( + 'script', + Attributes::create(['type' => 'application/javascript']), + HtmlString::create($layoutJS) + ); + } + + /** + * Create document header + * + * @return ValidHtml + */ + protected function createHeader(): ValidHtml + { + return (new HtmlDocument()) + ->addHtml( + new HtmlElement('style', null, HtmlString::create( + static::DEFAULT_HEADER_FOOTER_STYLE + )), + new HtmlElement('header', null, $this->headerTemplate) + ); + } + + /** + * Create document footer + * + * @return ValidHtml + */ + protected function createFooter(): ValidHtml + { + return (new HtmlDocument()) + ->addHtml( + new HtmlElement('style', null, HtmlString::create( + static::DEFAULT_HEADER_FOOTER_STYLE + )), + new HtmlElement('footer', null, $this->footerTemplate) + ); + } +} diff --git a/library/Pdfexport/ProvidedHook/Pdfexport.php b/library/Pdfexport/ProvidedHook/Pdfexport.php new file mode 100644 index 0000000..113eff0 --- /dev/null +++ b/library/Pdfexport/ProvidedHook/Pdfexport.php @@ -0,0 +1,151 @@ +<?php + +/* Icinga PDF Export | (c) 2018 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Pdfexport\ProvidedHook; + +use Exception; +use Icinga\Application\Config; +use Icinga\Application\Hook; +use Icinga\Application\Hook\PdfexportHook; +use Icinga\Application\Icinga; +use Icinga\Module\Pdfexport\HeadlessChrome; +use Icinga\Module\Pdfexport\PrintableHtmlDocument; +use iio\libmergepdf\Driver\TcpdiDriver; +use iio\libmergepdf\Merger; + +class Pdfexport extends PdfexportHook +{ + public static function first() + { + $pdfexport = null; + + if (Hook::has('Pdfexport')) { + $pdfexport = Hook::first('Pdfexport'); + + if (! $pdfexport->isSupported()) { + throw new Exception( + sprintf("Can't export: %s does not support exporting PDFs", get_class($pdfexport)) + ); + } + } + + if (! $pdfexport) { + throw new Exception("Can't export: No module found which provides PDF export"); + } + + return $pdfexport; + } + + public static function getBinary() + { + return Config::module('pdfexport')->get('chrome', 'binary', '/usr/bin/google-chrome'); + } + + public static function getForceTempStorage() + { + return (bool) Config::module('pdfexport')->get('chrome', 'force_temp_storage', '0'); + } + + public static function getHost() + { + return Config::module('pdfexport')->get('chrome', 'host'); + } + + public static function getPort() + { + return Config::module('pdfexport')->get('chrome', 'port', 9222); + } + + public function isSupported() + { + try { + return $this->chrome()->getVersion() >= 59; + } catch (Exception $e) { + return false; + } + } + + public function htmlToPdf($html) + { + // Keep reference to the chrome object because it is using temp files which are automatically removed when + // the object is destructed + $chrome = $this->chrome(); + + $pdf = $chrome->fromHtml($html, static::getForceTempStorage())->toPdf(); + + if ($html instanceof PrintableHtmlDocument && ($coverPage = $html->getCoverPage()) !== null) { + $coverPagePdf = $chrome + ->fromHtml( + (new PrintableHtmlDocument()) + ->add($coverPage) + ->addAttributes($html->getAttributes()) + ->removeMargins(), + static::getForceTempStorage() + ) + ->toPdf(); + + $merger = new Merger(new TcpdiDriver()); + $merger->addRaw($coverPagePdf); + $merger->addRaw($pdf); + + $pdf = $merger->merge(); + } + + return $pdf; + } + + public function streamPdfFromHtml($html, $filename) + { + $filename = basename($filename, '.pdf') . '.pdf'; + + // Keep reference to the chrome object because it is using temp files which are automatically removed when + // the object is destructed + $chrome = $this->chrome(); + + $pdf = $chrome->fromHtml($html, static::getForceTempStorage())->toPdf(); + + if ($html instanceof PrintableHtmlDocument && ($coverPage = $html->getCoverPage()) !== null) { + $coverPagePdf = $chrome + ->fromHtml( + (new PrintableHtmlDocument()) + ->add($coverPage) + ->addAttributes($html->getAttributes()) + ->removeMargins(), + static::getForceTempStorage() + ) + ->toPdf(); + + $merger = new Merger(new TcpdiDriver()); + $merger->addRaw($coverPagePdf); + $merger->addRaw($pdf); + + $pdf = $merger->merge(); + } + + Icinga::app()->getResponse() + ->setHeader('Content-Type', 'application/pdf', true) + ->setHeader('Content-Disposition', "inline; filename=\"$filename\"", true) + ->setBody($pdf) + ->sendResponse(); + + exit; + } + + /** + * Create an instance of HeadlessChrome from configuration + * + * @return HeadlessChrome + */ + protected function chrome() + { + $chrome = new HeadlessChrome(); + $chrome->setBinary(static::getBinary()); + + if (($host = static::getHost()) !== null) { + $chrome->setRemote($host, static::getPort()); + } + + return $chrome; + } +} diff --git a/library/Pdfexport/ShellCommand.php b/library/Pdfexport/ShellCommand.php new file mode 100644 index 0000000..c669773 --- /dev/null +++ b/library/Pdfexport/ShellCommand.php @@ -0,0 +1,148 @@ +<?php + +/* Icinga PDF Export | (c) 2018 Icinga GmbH | GPLv2 */ + +namespace Icinga\Module\Pdfexport; + +class ShellCommand +{ + /** @var string Command to execute */ + protected $command; + + /** @var int Exit code of the command */ + protected $exitCode; + + /** @var resource Process resource */ + protected $resource; + + /** + * Create a new command + * + * @param string $command The command to execute + * @param bool $escape Whether to escape the command + */ + public function __construct($command, $escape = true) + { + $command = (string) $command; + + $this->command = $escape ? escapeshellcmd($command) : $command; + } + + /** + * Get the exit code of the command + * + * @return int + */ + public function getExitCode() + { + return $this->exitCode; + } + + /** + * Get the status of the command + * + * @return object + */ + public function getStatus() + { + $status = (object) proc_get_status($this->resource); + if ($status->running === false && $this->exitCode === null) { + // The exit code is only valid the first time proc_get_status is + // called in terms of running false, hence we capture it + $this->exitCode = $status->exitcode; + } + + return $status; + } + + /** + * Execute the command + * + * @return object + * + * @throws \Exception + */ + public function execute() + { + if ($this->resource !== null) { + throw new \Exception('Command already started'); + } + + $descriptors = [ + ['pipe', 'r'], // stdin + ['pipe', 'w'], // stdout + ['pipe', 'w'] // stderr + ]; + + $this->resource = proc_open( + $this->command, + $descriptors, + $pipes + ); + + if (! is_resource($this->resource)) { + throw new \Exception(sprintf( + "Can't fork '%s'", + $this->command + )); + } + + $namedpipes = (object) [ + 'stdin' => &$pipes[0], + 'stdout' => &$pipes[1], + 'stderr' => &$pipes[2] + ]; + + fclose($namedpipes->stdin); + + $read = [$namedpipes->stderr, $namedpipes->stdout]; + $origRead = $read; + $write = null; // stdin not handled + $except = null; + $stdout = ''; + $stderr = ''; + + stream_set_blocking($namedpipes->stdout, 0); // non-blocking + stream_set_blocking($namedpipes->stderr, 0); + + while (stream_select($read, $write, $except, 0, 20000) !== false) { + foreach ($read as $pipe) { + if ($pipe === $namedpipes->stdout) { + $stdout .= stream_get_contents($pipe); + } + + if ($pipe === $namedpipes->stderr) { + $stderr .= stream_get_contents($pipe); + } + } + + foreach ($origRead as $i => $str) { + if (feof($str)) { + unset($origRead[$i]); + } + } + + if (empty($origRead)) { + break; + } + + // Reset pipes + $read = $origRead; + } + + fclose($namedpipes->stderr); + fclose($namedpipes->stdout); + + $exitCode = proc_close($this->resource); + if ($this->exitCode === null) { + $this->exitCode = $exitCode; + } + + $this->resource = null; + + return (object) [ + 'stdout' => $stdout, + 'stderr' => $stderr + ]; + } +} diff --git a/module.info b/module.info new file mode 100644 index 0000000..65c0a74 --- /dev/null +++ b/module.info @@ -0,0 +1,5 @@ +Module: PDF Export +Version: 0.10.2 +Requires: + Libraries: icinga-php-library (>=0.8.0), icinga-php-thirdparty (>=0.10.0) +Description: PDF Export via Google Chrome/Chromium diff --git a/public/css/print.less b/public/css/print.less new file mode 100644 index 0000000..00feac3 --- /dev/null +++ b/public/css/print.less @@ -0,0 +1,29 @@ +// icingaweb2 + +@gray: #7F7F7F; +@gray-semilight: #A9A9A9; +@gray-light: #C9C9C9; +@gray-lighter: #EEEEEE; +@gray-lightest: #F7F7F7; +@icinga-blue: #0095BF; +@low-sat-blue: #dae3e6; +@low-sat-blue-dark: #becbcf; +@body-bg-color: #fff; +@text-color: @black; +@text-color-light: @gray; +@tr-active-color: @body-bg-color; +@tr-hover-color: @body-bg-color; + +// ipl-web +@default-bg: @body-bg-color; + +@base-gray: @gray; +@base-gray-light: @gray-light; +@base-gray-lighter: @gray-lighter; +@base-disabled: @disabled-gray; +@base-primary-color: @icinga-blue; +@base-primary-bg: @icinga-blue; + +@default-text-color: @text-color; +@default-text-color-light: @text-color-light; +@default-text-color-inverted: @text-color-inverted; diff --git a/public/js/layout-plugins/page-breaker.js b/public/js/layout-plugins/page-breaker.js new file mode 100644 index 0000000..bdf04ec --- /dev/null +++ b/public/js/layout-plugins/page-breaker.js @@ -0,0 +1,44 @@ +/* Icinga PDF Export | (c) 2021 Icinga GmbH | GPLv2 */ + +"use strict"; + +(() => { + Layout.registerPlugin('page-breaker', () => { + let pageBreaksFor = document.querySelector('[data-pdfexport-page-breaks-at]'); + if (! pageBreaksFor) { + return; + } + + let pageBreaksAt = pageBreaksFor.dataset.pdfexportPageBreaksAt; + if (! pageBreaksAt) { + return; + } + + let contentHeight = document.body.dataset.contentHeight; + let items = Array.from(pageBreaksFor.querySelectorAll(':scope > ' + pageBreaksAt)); + + let remainingHeight = contentHeight; + items.forEach((item, i) => { + let requiredHeight; + if (i < items.length - 1) { + requiredHeight = items[i + 1].getBoundingClientRect().top - item.getBoundingClientRect().top; + } else { + requiredHeight = item.parentElement.getBoundingClientRect().bottom - item.getBoundingClientRect().top; + } + + if (remainingHeight < requiredHeight) { + if (!! item.previousElementSibling) { + item.previousElementSibling.style.pageBreakAfter = 'always'; + item.previousElementSibling.classList.add('page-break-follows'); + } else { + item.style.pageBreakAfter = 'always'; + item.classList.add('page-break-follows'); + } + + remainingHeight = contentHeight; + } + + remainingHeight -= requiredHeight; + }); + }); +})(); diff --git a/public/js/layout.js b/public/js/layout.js new file mode 100644 index 0000000..0ea5d64 --- /dev/null +++ b/public/js/layout.js @@ -0,0 +1,32 @@ +/* Icinga PDF Export | (c) 2021 Icinga GmbH | GPLv2 */ + +"use strict"; + +class Layout +{ + static #plugins = []; + + static registerPlugin(name, plugin) { + this.#plugins.push([name, plugin]); + } + + apply() { + for (let [name, plugin] of Layout.#plugins) { + try { + plugin(); + } catch (error) { + console.error('Layout plugin ' + name + ' failed to run: ' + error); + } + } + + this.finish(); + } + + finish() { + document.documentElement.dataset.layoutReady = 'yes'; + document.dispatchEvent(new CustomEvent('layout-ready', { + cancelable: false, + bubbles: false + })); + } +} @@ -0,0 +1,9 @@ +<?php + +/* Icinga PDF Export | (c) 2018 Icinga GmbH | GPLv2 */ + +/** @var \Icinga\Application\Modules\Module $this */ + +$this->provideHook('Pdfexport'); + +require_once 'vendor/autoload.php'; diff --git a/vendor/autoload.php b/vendor/autoload.php new file mode 100644 index 0000000..5081d7d --- /dev/null +++ b/vendor/autoload.php @@ -0,0 +1,7 @@ +<?php + +// autoload.php @generated by Composer + +require_once __DIR__ . '/composer/autoload_real.php'; + +return ComposerAutoloaderInitff8ca5c94912b5ce3ac82b5c9f2b4776::getLoader(); diff --git a/vendor/composer/ClassLoader.php b/vendor/composer/ClassLoader.php new file mode 100644 index 0000000..03b9bb9 --- /dev/null +++ b/vendor/composer/ClassLoader.php @@ -0,0 +1,445 @@ +<?php + +/* + * This file is part of Composer. + * + * (c) Nils Adermann <naderman@naderman.de> + * Jordi Boggiano <j.boggiano@seld.be> + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Autoload; + +/** + * ClassLoader implements a PSR-0, PSR-4 and classmap class loader. + * + * $loader = new \Composer\Autoload\ClassLoader(); + * + * // register classes with namespaces + * $loader->add('Symfony\Component', __DIR__.'/component'); + * $loader->add('Symfony', __DIR__.'/framework'); + * + * // activate the autoloader + * $loader->register(); + * + * // to enable searching the include path (eg. for PEAR packages) + * $loader->setUseIncludePath(true); + * + * In this example, if you try to use a class in the Symfony\Component + * namespace or one of its children (Symfony\Component\Console for instance), + * the autoloader will first look for the class under the component/ + * directory, and it will then fallback to the framework/ directory if not + * found before giving up. + * + * This class is loosely based on the Symfony UniversalClassLoader. + * + * @author Fabien Potencier <fabien@symfony.com> + * @author Jordi Boggiano <j.boggiano@seld.be> + * @see http://www.php-fig.org/psr/psr-0/ + * @see http://www.php-fig.org/psr/psr-4/ + */ +class ClassLoader +{ + // PSR-4 + private $prefixLengthsPsr4 = array(); + private $prefixDirsPsr4 = array(); + private $fallbackDirsPsr4 = array(); + + // PSR-0 + private $prefixesPsr0 = array(); + private $fallbackDirsPsr0 = array(); + + private $useIncludePath = false; + private $classMap = array(); + private $classMapAuthoritative = false; + private $missingClasses = array(); + private $apcuPrefix; + + public function getPrefixes() + { + if (!empty($this->prefixesPsr0)) { + return call_user_func_array('array_merge', array_values($this->prefixesPsr0)); + } + + return array(); + } + + public function getPrefixesPsr4() + { + return $this->prefixDirsPsr4; + } + + public function getFallbackDirs() + { + return $this->fallbackDirsPsr0; + } + + public function getFallbackDirsPsr4() + { + return $this->fallbackDirsPsr4; + } + + public function getClassMap() + { + return $this->classMap; + } + + /** + * @param array $classMap Class to filename map + */ + public function addClassMap(array $classMap) + { + if ($this->classMap) { + $this->classMap = array_merge($this->classMap, $classMap); + } else { + $this->classMap = $classMap; + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, either + * appending or prepending to the ones previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 root directories + * @param bool $prepend Whether to prepend the directories + */ + public function add($prefix, $paths, $prepend = false) + { + if (!$prefix) { + if ($prepend) { + $this->fallbackDirsPsr0 = array_merge( + (array) $paths, + $this->fallbackDirsPsr0 + ); + } else { + $this->fallbackDirsPsr0 = array_merge( + $this->fallbackDirsPsr0, + (array) $paths + ); + } + + return; + } + + $first = $prefix[0]; + if (!isset($this->prefixesPsr0[$first][$prefix])) { + $this->prefixesPsr0[$first][$prefix] = (array) $paths; + + return; + } + if ($prepend) { + $this->prefixesPsr0[$first][$prefix] = array_merge( + (array) $paths, + $this->prefixesPsr0[$first][$prefix] + ); + } else { + $this->prefixesPsr0[$first][$prefix] = array_merge( + $this->prefixesPsr0[$first][$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, either + * appending or prepending to the ones previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * @param bool $prepend Whether to prepend the directories + * + * @throws \InvalidArgumentException + */ + public function addPsr4($prefix, $paths, $prepend = false) + { + if (!$prefix) { + // Register directories for the root namespace. + if ($prepend) { + $this->fallbackDirsPsr4 = array_merge( + (array) $paths, + $this->fallbackDirsPsr4 + ); + } else { + $this->fallbackDirsPsr4 = array_merge( + $this->fallbackDirsPsr4, + (array) $paths + ); + } + } elseif (!isset($this->prefixDirsPsr4[$prefix])) { + // Register directories for a new namespace. + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } elseif ($prepend) { + // Prepend directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + (array) $paths, + $this->prefixDirsPsr4[$prefix] + ); + } else { + // Append directories for an already registered namespace. + $this->prefixDirsPsr4[$prefix] = array_merge( + $this->prefixDirsPsr4[$prefix], + (array) $paths + ); + } + } + + /** + * Registers a set of PSR-0 directories for a given prefix, + * replacing any others previously set for this prefix. + * + * @param string $prefix The prefix + * @param array|string $paths The PSR-0 base directories + */ + public function set($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr0 = (array) $paths; + } else { + $this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths; + } + } + + /** + * Registers a set of PSR-4 directories for a given namespace, + * replacing any others previously set for this namespace. + * + * @param string $prefix The prefix/namespace, with trailing '\\' + * @param array|string $paths The PSR-4 base directories + * + * @throws \InvalidArgumentException + */ + public function setPsr4($prefix, $paths) + { + if (!$prefix) { + $this->fallbackDirsPsr4 = (array) $paths; + } else { + $length = strlen($prefix); + if ('\\' !== $prefix[$length - 1]) { + throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator."); + } + $this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length; + $this->prefixDirsPsr4[$prefix] = (array) $paths; + } + } + + /** + * Turns on searching the include path for class files. + * + * @param bool $useIncludePath + */ + public function setUseIncludePath($useIncludePath) + { + $this->useIncludePath = $useIncludePath; + } + + /** + * Can be used to check if the autoloader uses the include path to check + * for classes. + * + * @return bool + */ + public function getUseIncludePath() + { + return $this->useIncludePath; + } + + /** + * Turns off searching the prefix and fallback directories for classes + * that have not been registered with the class map. + * + * @param bool $classMapAuthoritative + */ + public function setClassMapAuthoritative($classMapAuthoritative) + { + $this->classMapAuthoritative = $classMapAuthoritative; + } + + /** + * Should class lookup fail if not found in the current class map? + * + * @return bool + */ + public function isClassMapAuthoritative() + { + return $this->classMapAuthoritative; + } + + /** + * APCu prefix to use to cache found/not-found classes, if the extension is enabled. + * + * @param string|null $apcuPrefix + */ + public function setApcuPrefix($apcuPrefix) + { + $this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null; + } + + /** + * The APCu prefix in use, or null if APCu caching is not enabled. + * + * @return string|null + */ + public function getApcuPrefix() + { + return $this->apcuPrefix; + } + + /** + * Registers this instance as an autoloader. + * + * @param bool $prepend Whether to prepend the autoloader or not + */ + public function register($prepend = false) + { + spl_autoload_register(array($this, 'loadClass'), true, $prepend); + } + + /** + * Unregisters this instance as an autoloader. + */ + public function unregister() + { + spl_autoload_unregister(array($this, 'loadClass')); + } + + /** + * Loads the given class or interface. + * + * @param string $class The name of the class + * @return bool|null True if loaded, null otherwise + */ + public function loadClass($class) + { + if ($file = $this->findFile($class)) { + includeFile($file); + + return true; + } + } + + /** + * Finds the path to the file where the class is defined. + * + * @param string $class The name of the class + * + * @return string|false The path if found, false otherwise + */ + public function findFile($class) + { + // class map lookup + if (isset($this->classMap[$class])) { + return $this->classMap[$class]; + } + if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) { + return false; + } + if (null !== $this->apcuPrefix) { + $file = apcu_fetch($this->apcuPrefix.$class, $hit); + if ($hit) { + return $file; + } + } + + $file = $this->findFileWithExtension($class, '.php'); + + // Search for Hack files if we are running on HHVM + if (false === $file && defined('HHVM_VERSION')) { + $file = $this->findFileWithExtension($class, '.hh'); + } + + if (null !== $this->apcuPrefix) { + apcu_add($this->apcuPrefix.$class, $file); + } + + if (false === $file) { + // Remember that this class does not exist. + $this->missingClasses[$class] = true; + } + + return $file; + } + + private function findFileWithExtension($class, $ext) + { + // PSR-4 lookup + $logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext; + + $first = $class[0]; + if (isset($this->prefixLengthsPsr4[$first])) { + $subPath = $class; + while (false !== $lastPos = strrpos($subPath, '\\')) { + $subPath = substr($subPath, 0, $lastPos); + $search = $subPath . '\\'; + if (isset($this->prefixDirsPsr4[$search])) { + $pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1); + foreach ($this->prefixDirsPsr4[$search] as $dir) { + if (file_exists($file = $dir . $pathEnd)) { + return $file; + } + } + } + } + } + + // PSR-4 fallback dirs + foreach ($this->fallbackDirsPsr4 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) { + return $file; + } + } + + // PSR-0 lookup + if (false !== $pos = strrpos($class, '\\')) { + // namespaced class name + $logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1) + . strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR); + } else { + // PEAR-like class name + $logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext; + } + + if (isset($this->prefixesPsr0[$first])) { + foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) { + if (0 === strpos($class, $prefix)) { + foreach ($dirs as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + } + } + } + + // PSR-0 fallback dirs + foreach ($this->fallbackDirsPsr0 as $dir) { + if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) { + return $file; + } + } + + // PSR-0 include paths. + if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) { + return $file; + } + + return false; + } +} + +/** + * Scope isolated include. + * + * Prevents access to $this/self from included files. + */ +function includeFile($file) +{ + include $file; +} diff --git a/vendor/composer/LICENSE b/vendor/composer/LICENSE new file mode 100644 index 0000000..f27399a --- /dev/null +++ b/vendor/composer/LICENSE @@ -0,0 +1,21 @@ + +Copyright (c) Nils Adermann, Jordi Boggiano + +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/composer/autoload_classmap.php b/vendor/composer/autoload_classmap.php new file mode 100644 index 0000000..20a0548 --- /dev/null +++ b/vendor/composer/autoload_classmap.php @@ -0,0 +1,27 @@ +<?php + +// autoload_classmap.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'Datamatrix' => $vendorDir . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php', + 'FPDF' => $vendorDir . '/iio/libmergepdf/tcpdi/tcpdi.php', + 'FPDF_TPL' => $vendorDir . '/iio/libmergepdf/tcpdi/fpdf_tpl.php', + 'PDF417' => $vendorDir . '/tecnickcom/tcpdf/include/barcodes/pdf417.php', + 'QRcode' => $vendorDir . '/tecnickcom/tcpdf/include/barcodes/qrcode.php', + 'TCPDF' => $vendorDir . '/tecnickcom/tcpdf/tcpdf.php', + 'TCPDF2DBarcode' => $vendorDir . '/tecnickcom/tcpdf/tcpdf_barcodes_2d.php', + 'TCPDFBarcode' => $vendorDir . '/tecnickcom/tcpdf/tcpdf_barcodes_1d.php', + 'TCPDF_COLORS' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_colors.php', + 'TCPDF_FILTERS' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_filters.php', + 'TCPDF_FONTS' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_fonts.php', + 'TCPDF_FONT_DATA' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_font_data.php', + 'TCPDF_IMAGES' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_images.php', + 'TCPDF_IMPORT' => $vendorDir . '/tecnickcom/tcpdf/tcpdf_import.php', + 'TCPDF_PARSER' => $vendorDir . '/tecnickcom/tcpdf/tcpdf_parser.php', + 'TCPDF_STATIC' => $vendorDir . '/tecnickcom/tcpdf/include/tcpdf_static.php', + 'TCPDI' => $vendorDir . '/iio/libmergepdf/tcpdi/tcpdi.php', + 'tcpdi_parser' => $vendorDir . '/iio/libmergepdf/tcpdi/tcpdi_parser.php', +); diff --git a/vendor/composer/autoload_namespaces.php b/vendor/composer/autoload_namespaces.php new file mode 100644 index 0000000..b7fc012 --- /dev/null +++ b/vendor/composer/autoload_namespaces.php @@ -0,0 +1,9 @@ +<?php + +// autoload_namespaces.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( +); diff --git a/vendor/composer/autoload_psr4.php b/vendor/composer/autoload_psr4.php new file mode 100644 index 0000000..c91a123 --- /dev/null +++ b/vendor/composer/autoload_psr4.php @@ -0,0 +1,13 @@ +<?php + +// autoload_psr4.php @generated by Composer + +$vendorDir = dirname(dirname(__FILE__)); +$baseDir = dirname($vendorDir); + +return array( + 'setasign\\Fpdi\\' => array($vendorDir . '/setasign/fpdi/src'), + 'iio\\libmergepdf\\' => array($vendorDir . '/iio/libmergepdf/src'), + 'WebSocket\\' => array($vendorDir . '/textalk/websocket/lib'), + 'Psr\\Log\\' => array($vendorDir . '/psr/log/Psr/Log'), +); diff --git a/vendor/composer/autoload_real.php b/vendor/composer/autoload_real.php new file mode 100644 index 0000000..52be6f5 --- /dev/null +++ b/vendor/composer/autoload_real.php @@ -0,0 +1,55 @@ +<?php + +// autoload_real.php @generated by Composer + +class ComposerAutoloaderInitff8ca5c94912b5ce3ac82b5c9f2b4776 +{ + private static $loader; + + public static function loadClassLoader($class) + { + if ('Composer\Autoload\ClassLoader' === $class) { + require __DIR__ . '/ClassLoader.php'; + } + } + + /** + * @return \Composer\Autoload\ClassLoader + */ + public static function getLoader() + { + if (null !== self::$loader) { + return self::$loader; + } + + spl_autoload_register(array('ComposerAutoloaderInitff8ca5c94912b5ce3ac82b5c9f2b4776', 'loadClassLoader'), true, true); + self::$loader = $loader = new \Composer\Autoload\ClassLoader(); + spl_autoload_unregister(array('ComposerAutoloaderInitff8ca5c94912b5ce3ac82b5c9f2b4776', 'loadClassLoader')); + + $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded()); + if ($useStaticLoader) { + require_once __DIR__ . '/autoload_static.php'; + + call_user_func(\Composer\Autoload\ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776::getInitializer($loader)); + } else { + $map = require __DIR__ . '/autoload_namespaces.php'; + foreach ($map as $namespace => $path) { + $loader->set($namespace, $path); + } + + $map = require __DIR__ . '/autoload_psr4.php'; + foreach ($map as $namespace => $path) { + $loader->setPsr4($namespace, $path); + } + + $classMap = require __DIR__ . '/autoload_classmap.php'; + if ($classMap) { + $loader->addClassMap($classMap); + } + } + + $loader->register(true); + + return $loader; + } +} diff --git a/vendor/composer/autoload_static.php b/vendor/composer/autoload_static.php new file mode 100644 index 0000000..5467679 --- /dev/null +++ b/vendor/composer/autoload_static.php @@ -0,0 +1,77 @@ +<?php + +// autoload_static.php @generated by Composer + +namespace Composer\Autoload; + +class ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776 +{ + public static $prefixLengthsPsr4 = array ( + 's' => + array ( + 'setasign\\Fpdi\\' => 14, + ), + 'i' => + array ( + 'iio\\libmergepdf\\' => 16, + ), + 'W' => + array ( + 'WebSocket\\' => 10, + ), + 'P' => + array ( + 'Psr\\Log\\' => 8, + ), + ); + + public static $prefixDirsPsr4 = array ( + 'setasign\\Fpdi\\' => + array ( + 0 => __DIR__ . '/..' . '/setasign/fpdi/src', + ), + 'iio\\libmergepdf\\' => + array ( + 0 => __DIR__ . '/..' . '/iio/libmergepdf/src', + ), + 'WebSocket\\' => + array ( + 0 => __DIR__ . '/..' . '/textalk/websocket/lib', + ), + 'Psr\\Log\\' => + array ( + 0 => __DIR__ . '/..' . '/psr/log/Psr/Log', + ), + ); + + public static $classMap = array ( + 'Datamatrix' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/barcodes/datamatrix.php', + 'FPDF' => __DIR__ . '/..' . '/iio/libmergepdf/tcpdi/tcpdi.php', + 'FPDF_TPL' => __DIR__ . '/..' . '/iio/libmergepdf/tcpdi/fpdf_tpl.php', + 'PDF417' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/barcodes/pdf417.php', + 'QRcode' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/barcodes/qrcode.php', + 'TCPDF' => __DIR__ . '/..' . '/tecnickcom/tcpdf/tcpdf.php', + 'TCPDF2DBarcode' => __DIR__ . '/..' . '/tecnickcom/tcpdf/tcpdf_barcodes_2d.php', + 'TCPDFBarcode' => __DIR__ . '/..' . '/tecnickcom/tcpdf/tcpdf_barcodes_1d.php', + 'TCPDF_COLORS' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_colors.php', + 'TCPDF_FILTERS' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_filters.php', + 'TCPDF_FONTS' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_fonts.php', + 'TCPDF_FONT_DATA' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_font_data.php', + 'TCPDF_IMAGES' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_images.php', + 'TCPDF_IMPORT' => __DIR__ . '/..' . '/tecnickcom/tcpdf/tcpdf_import.php', + 'TCPDF_PARSER' => __DIR__ . '/..' . '/tecnickcom/tcpdf/tcpdf_parser.php', + 'TCPDF_STATIC' => __DIR__ . '/..' . '/tecnickcom/tcpdf/include/tcpdf_static.php', + 'TCPDI' => __DIR__ . '/..' . '/iio/libmergepdf/tcpdi/tcpdi.php', + 'tcpdi_parser' => __DIR__ . '/..' . '/iio/libmergepdf/tcpdi/tcpdi_parser.php', + ); + + public static function getInitializer(ClassLoader $loader) + { + return \Closure::bind(function () use ($loader) { + $loader->prefixLengthsPsr4 = ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776::$prefixLengthsPsr4; + $loader->prefixDirsPsr4 = ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776::$prefixDirsPsr4; + $loader->classMap = ComposerStaticInitff8ca5c94912b5ce3ac82b5c9f2b4776::$classMap; + + }, null, ClassLoader::class); + } +} diff --git a/vendor/composer/installed.json b/vendor/composer/installed.json new file mode 100644 index 0000000..48f51c7 --- /dev/null +++ b/vendor/composer/installed.json @@ -0,0 +1,294 @@ +[ + { + "name": "iio/libmergepdf", + "version": "4.0.4", + "version_normalized": "4.0.4.0", + "source": { + "type": "git", + "url": "https://github.com/hanneskod/libmergepdf.git", + "reference": "6613b978c08d00d559796ab510614243e4dd5dfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/hanneskod/libmergepdf/zipball/6613b978c08d00d559796ab510614243e4dd5dfb", + "reference": "6613b978c08d00d559796ab510614243e4dd5dfb", + "shasum": "" + }, + "require": { + "php": "^7.1||^8.0", + "setasign/fpdi": "^2", + "tecnickcom/tcpdf": "^6.2.22" + }, + "conflict": { + "rafikhaceb/tcpdi": "*", + "setasign/fpdf": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7|^8", + "smalot/pdfparser": "~0.13" + }, + "time": "2020-12-07T12:18:49+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "iio\\libmergepdf\\": "src/" + }, + "classmap": [ + "tcpdi/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "WTFPL" + ], + "authors": [ + { + "name": "Hannes Forsgård", + "email": "hannes.forsgard@fripost.org" + } + ], + "description": "Library for merging multiple PDFs", + "homepage": "https://github.com/hanneskod/libmergepdf", + "keywords": [ + "merge", + "pdf" + ] + }, + { + "name": "psr/log", + "version": "1.1.4", + "version_normalized": "1.1.4.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/d49695b909c3b7628b6289db5479a1c204601f11", + "reference": "d49695b909c3b7628b6289db5479a1c204601f11", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2021-05-03T11:20:27+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "homepage": "https://github.com/php-fig/log", + "keywords": [ + "log", + "psr", + "psr-3" + ] + }, + { + "name": "setasign/fpdi", + "version": "v2.3.6", + "version_normalized": "2.3.6.0", + "source": { + "type": "git", + "url": "https://github.com/Setasign/FPDI.git", + "reference": "6231e315f73e4f62d72b73f3d6d78ff0eed93c31" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Setasign/FPDI/zipball/6231e315f73e4f62d72b73f3d6d78ff0eed93c31", + "reference": "6231e315f73e4f62d72b73f3d6d78ff0eed93c31", + "shasum": "" + }, + "require": { + "ext-zlib": "*", + "php": "^5.6 || ^7.0 || ^8.0" + }, + "conflict": { + "setasign/tfpdf": "<1.31" + }, + "require-dev": { + "phpunit/phpunit": "~5.7", + "setasign/fpdf": "~1.8", + "setasign/tfpdf": "1.31", + "squizlabs/php_codesniffer": "^3.5", + "tecnickcom/tcpdf": "~6.2" + }, + "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": "2021-02-11T11:37:01+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "setasign\\Fpdi\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jan Slabon", + "email": "jan.slabon@setasign.com", + "homepage": "https://www.setasign.com" + }, + { + "name": "Maximilian Kresse", + "email": "maximilian.kresse@setasign.com", + "homepage": "https://www.setasign.com" + } + ], + "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", + "homepage": "https://www.setasign.com/fpdi", + "keywords": [ + "fpdf", + "fpdi", + "pdf" + ], + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/setasign/fpdi", + "type": "tidelift" + } + ] + }, + { + "name": "tecnickcom/tcpdf", + "version": "6.4.2", + "version_normalized": "6.4.2.0", + "source": { + "type": "git", + "url": "https://github.com/tecnickcom/TCPDF.git", + "reference": "172540dcbfdf8dc983bc2fe78feff48ff7ec1c76" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/tecnickcom/TCPDF/zipball/172540dcbfdf8dc983bc2fe78feff48ff7ec1c76", + "reference": "172540dcbfdf8dc983bc2fe78feff48ff7ec1c76", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "time": "2021-07-20T14:43:20+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "classmap": [ + "config", + "include", + "tcpdf.php", + "tcpdf_parser.php", + "tcpdf_import.php", + "tcpdf_barcodes_1d.php", + "tcpdf_barcodes_2d.php", + "include/tcpdf_colors.php", + "include/tcpdf_filters.php", + "include/tcpdf_font_data.php", + "include/tcpdf_fonts.php", + "include/tcpdf_images.php", + "include/tcpdf_static.php", + "include/barcodes/datamatrix.php", + "include/barcodes/pdf417.php", + "include/barcodes/qrcode.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-only" + ], + "authors": [ + { + "name": "Nicola Asuni", + "email": "info@tecnick.com", + "role": "lead" + } + ], + "description": "TCPDF is a PHP class for generating PDF documents and barcodes.", + "homepage": "http://www.tcpdf.org/", + "keywords": [ + "PDFD32000-2008", + "TCPDF", + "barcodes", + "datamatrix", + "pdf", + "pdf417", + "qrcode" + ], + "funding": [ + { + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_donations¤cy_code=GBP&business=paypal@tecnick.com&item_name=donation%20for%20tcpdf%20project", + "type": "custom" + } + ] + }, + { + "name": "textalk/websocket", + "version": "1.5.5", + "version_normalized": "1.5.5.0", + "source": { + "type": "git", + "url": "https://github.com/Textalk/websocket-php.git", + "reference": "846542f82658132cd36acb7a7e8ce0f03960c295" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Textalk/websocket-php/zipball/846542f82658132cd36acb7a7e8ce0f03960c295", + "reference": "846542f82658132cd36acb7a7e8ce0f03960c295", + "shasum": "" + }, + "require": { + "php": "^7.2 | ^8.0", + "psr/log": "^1 | ^2 | ^3" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.0", + "phpunit/phpunit": "^8.0|^9.0", + "squizlabs/php_codesniffer": "^3.5" + }, + "time": "2021-08-07T10:21:40+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "WebSocket\\": "lib" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "ISC" + ], + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen", + "email": "soren@abicart.se" + } + ], + "description": "WebSocket client and server" + } +] diff --git a/vendor/iio/libmergepdf/.github/ISSUE_TEMPLATE/bug_report.md b/vendor/iio/libmergepdf/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..e6a8753 --- /dev/null +++ b/vendor/iio/libmergepdf/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: '' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Include files** +Please include the files that you are trying to merge so that the error can be reproduced. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Please complete the following information:** + - PHP-version + - Version of library [e.g. 22] + - Driver used + +**Additional context** +Add any other context about the problem here. + +> Please note that this library is only a wrapper around other pdf editing tools. +> As such we are only able to respond to problems regaring the actual +> merging process. Issues concering deeper pdf parsing problems should +> be directed elsewhere. diff --git a/vendor/iio/libmergepdf/composer.json b/vendor/iio/libmergepdf/composer.json new file mode 100644 index 0000000..195d0aa --- /dev/null +++ b/vendor/iio/libmergepdf/composer.json @@ -0,0 +1,35 @@ +{ + "name": "iio/libmergepdf", + "description": "Library for merging multiple PDFs", + "keywords": ["pdf", "merge"], + "homepage": "https://github.com/hanneskod/libmergepdf", + "type": "library", + "license": "WTFPL", + "authors": [ + { + "name": "Hannes Forsgård", + "email": "hannes.forsgard@fripost.org" + } + ], + "autoload": { + "psr-4": { + "iio\\libmergepdf\\": "src/" + }, + "classmap": [ + "tcpdi/" + ] + }, + "require": { + "php": "^7.1||^8.0", + "tecnickcom/tcpdf": "^6.2.22", + "setasign/fpdi": "^2" + }, + "conflict": { + "setasign/fpdf": "*", + "rafikhaceb/tcpdi": "*" + }, + "require-dev": { + "phpunit/phpunit": "^7|^8", + "smalot/pdfparser": "~0.13" + } +} diff --git a/vendor/iio/libmergepdf/src/Driver/DefaultDriver.php b/vendor/iio/libmergepdf/src/Driver/DefaultDriver.php new file mode 100644 index 0000000..9ad6f73 --- /dev/null +++ b/vendor/iio/libmergepdf/src/Driver/DefaultDriver.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types = 1); + +namespace iio\libmergepdf\Driver; + +use iio\libmergepdf\Source\SourceInterface; + +final class DefaultDriver implements DriverInterface +{ + /** + * @var DriverInterface + */ + private $wrapped; + + public function __construct(DriverInterface $wrapped = null) + { + $this->wrapped = $wrapped ?: new Fpdi2Driver; + } + + public function merge(SourceInterface ...$sources): string + { + return $this->wrapped->merge(...$sources); + } +} diff --git a/vendor/iio/libmergepdf/src/Driver/DriverInterface.php b/vendor/iio/libmergepdf/src/Driver/DriverInterface.php new file mode 100644 index 0000000..39cca1e --- /dev/null +++ b/vendor/iio/libmergepdf/src/Driver/DriverInterface.php @@ -0,0 +1,13 @@ +<?php + +namespace iio\libmergepdf\Driver; + +use iio\libmergepdf\Source\SourceInterface; + +interface DriverInterface +{ + /** + * Merge multiple sources + */ + public function merge(SourceInterface ...$sources): string; +} diff --git a/vendor/iio/libmergepdf/src/Driver/Fpdi2Driver.php b/vendor/iio/libmergepdf/src/Driver/Fpdi2Driver.php new file mode 100644 index 0000000..e56d8b1 --- /dev/null +++ b/vendor/iio/libmergepdf/src/Driver/Fpdi2Driver.php @@ -0,0 +1,64 @@ +<?php + +declare(strict_types = 1); + +namespace iio\libmergepdf\Driver; + +use iio\libmergepdf\Exception; +use iio\libmergepdf\Source\SourceInterface; +use setasign\Fpdi\Fpdi as FpdiFpdf; +use setasign\Fpdi\Tcpdf\Fpdi as FpdiTcpdf; +use setasign\Fpdi\PdfParser\StreamReader; + +final class Fpdi2Driver implements DriverInterface +{ + /** + * @var FpdiFpdf|FpdiTcpdf + */ + private $fpdi; + + /** + * @param FpdiFpdf|FpdiTcpdf $fpdi + */ + public function __construct($fpdi = null) + { + // Tcpdf generates warnings due to argument ordering with php 8 + // suppressing errors is a dirty hack until tcpdf is patched + $this->fpdi = $fpdi ?: @new FpdiTcpdf; + + if (!($this->fpdi instanceof FpdiFpdf) && !($this->fpdi instanceof FpdiTcpdf)) { + throw new \InvalidArgumentException('Constructor argument must be an FPDI instance.'); + } + } + + public function merge(SourceInterface ...$sources): string + { + $sourceName = ''; + + try { + $fpdi = clone $this->fpdi; + + foreach ($sources as $source) { + $sourceName = $source->getName(); + $pageCount = $fpdi->setSourceFile(StreamReader::createByString($source->getContents())); + $pageNumbers = $source->getPages()->getPageNumbers() ?: range(1, $pageCount); + + foreach ($pageNumbers as $pageNr) { + $template = $fpdi->importPage($pageNr); + $size = $fpdi->getTemplateSize($template); + $fpdi->SetPrintHeader(false); + $fpdi->SetPrintFooter(false); + $fpdi->AddPage( + $size['width'] > $size['height'] ? 'L' : 'P', + [$size['width'], $size['height']] + ); + $fpdi->useTemplate($template); + } + } + + return $fpdi->Output('', 'S'); + } catch (\Exception $e) { + throw new Exception("'{$e->getMessage()}' in '$sourceName'", 0, $e); + } + } +} diff --git a/vendor/iio/libmergepdf/src/Driver/TcpdiDriver.php b/vendor/iio/libmergepdf/src/Driver/TcpdiDriver.php new file mode 100644 index 0000000..fa0ddc8 --- /dev/null +++ b/vendor/iio/libmergepdf/src/Driver/TcpdiDriver.php @@ -0,0 +1,52 @@ +<?php + +declare(strict_types = 1); + +namespace iio\libmergepdf\Driver; + +use iio\libmergepdf\Exception; +use iio\libmergepdf\Source\SourceInterface; + +final class TcpdiDriver implements DriverInterface +{ + /** + * @var \TCPDI + */ + private $tcpdi; + + public function __construct(\TCPDI $tcpdi = null) + { + $this->tcpdi = $tcpdi ?: new \TCPDI; + } + + public function merge(SourceInterface ...$sources): string + { + $sourceName = ''; + + try { + $tcpdi = clone $this->tcpdi; + + foreach ($sources as $source) { + $sourceName = $source->getName(); + $pageCount = $tcpdi->setSourceData($source->getContents()); + $pageNumbers = $source->getPages()->getPageNumbers() ?: range(1, $pageCount); + + foreach ($pageNumbers as $pageNr) { + $template = $tcpdi->importPage($pageNr); + $size = $tcpdi->getTemplateSize($template); + $tcpdi->SetPrintHeader(false); + $tcpdi->SetPrintFooter(false); + $tcpdi->AddPage( + $size['w'] > $size['h'] ? 'L' : 'P', + [$size['w'], $size['h']] + ); + $tcpdi->useTemplate($template); + } + } + + return $tcpdi->Output('', 'S'); + } catch (\Exception $e) { + throw new Exception("'{$e->getMessage()}' in '$sourceName'", 0, $e); + } + } +} diff --git a/vendor/iio/libmergepdf/src/Exception.php b/vendor/iio/libmergepdf/src/Exception.php new file mode 100644 index 0000000..69b3208 --- /dev/null +++ b/vendor/iio/libmergepdf/src/Exception.php @@ -0,0 +1,7 @@ +<?php + +namespace iio\libmergepdf; + +final class Exception extends \Exception +{ +} diff --git a/vendor/iio/libmergepdf/src/Merger.php b/vendor/iio/libmergepdf/src/Merger.php new file mode 100644 index 0000000..57725c9 --- /dev/null +++ b/vendor/iio/libmergepdf/src/Merger.php @@ -0,0 +1,79 @@ +<?php + +declare(strict_types = 1); + +namespace iio\libmergepdf; + +use iio\libmergepdf\Driver\DriverInterface; +use iio\libmergepdf\Driver\DefaultDriver; +use iio\libmergepdf\Source\SourceInterface; +use iio\libmergepdf\Source\FileSource; +use iio\libmergepdf\Source\RawSource; + +/** + * Merge existing pdfs into one + * + * Note that your PDFs are merged in the order that you add them + */ +final class Merger +{ + /** + * @var SourceInterface[] List of pdf sources to merge + */ + private $sources = []; + + /** + * @var DriverInterface + */ + private $driver; + + public function __construct(DriverInterface $driver = null) + { + $this->driver = $driver ?: new DefaultDriver; + } + + /** + * Add raw PDF from string + */ + public function addRaw(string $content, PagesInterface $pages = null): void + { + $this->sources[] = new RawSource($content, $pages); + } + + /** + * Add PDF from file + */ + public function addFile(string $filename, PagesInterface $pages = null): void + { + $this->sources[] = new FileSource($filename, $pages); + } + + /** + * Add files using iterator + * + * @param iterable<string> $iterator Set of filenames to add + * @param PagesInterface $pages Optional pages constraint used for every added pdf + */ + public function addIterator(iterable $iterator, PagesInterface $pages = null): void + { + foreach ($iterator as $filename) { + $this->addFile($filename, $pages); + } + } + + /** + * Merges loaded PDFs + */ + public function merge(): string + { + return $this->driver->merge(...$this->sources); + } + + /** + * Reset internal state + */ + public function reset(): void + { + $this->sources = []; + } +} diff --git a/vendor/iio/libmergepdf/src/Pages.php b/vendor/iio/libmergepdf/src/Pages.php new file mode 100644 index 0000000..7675315 --- /dev/null +++ b/vendor/iio/libmergepdf/src/Pages.php @@ -0,0 +1,67 @@ +<?php + +declare(strict_types = 1); + +namespace iio\libmergepdf; + +/** + * Parse page numbers from string + */ +final class Pages implements PagesInterface +{ + /** + * @var int[] Added integer page numbers + */ + private $pages = []; + + /** + * Parse page numbers from expression string + * + * Pages should be formatted as 1,3,6 or 12-16 or combined. Note that pages + * are merged in the order that you provide them. If you put pages 12-14 + * before 1-5 then 12-14 will be placed first. + */ + public function __construct(string $expressionString = '') + { + $expressions = explode( + ',', + str_replace(' ', '', $expressionString) + ); + + foreach ($expressions as $expr) { + if (empty($expr)) { + continue; + } + if (ctype_digit($expr)) { + $this->addPage((int)$expr); + continue; + } + if (preg_match("/^(\d+)-(\d+)/", $expr, $matches)) { + $this->addRange((int)$matches[1], (int)$matches[2]); + continue; + } + throw new Exception("Invalid page number(s) for expression '$expr'"); + } + } + + /** + * Add a single page + */ + public function addPage(int $page): void + { + $this->pages[] = $page; + } + + /** + * Add a range of pages + */ + public function addRange(int $start, int $end): void + { + $this->pages = array_merge($this->pages, range($start, $end)); + } + + public function getPageNumbers(): array + { + return $this->pages; + } +} diff --git a/vendor/iio/libmergepdf/src/PagesInterface.php b/vendor/iio/libmergepdf/src/PagesInterface.php new file mode 100644 index 0000000..e85d3c8 --- /dev/null +++ b/vendor/iio/libmergepdf/src/PagesInterface.php @@ -0,0 +1,11 @@ +<?php + +namespace iio\libmergepdf; + +interface PagesInterface +{ + /** + * @return int[] + */ + public function getPageNumbers(): array; +} diff --git a/vendor/iio/libmergepdf/src/Source/FileSource.php b/vendor/iio/libmergepdf/src/Source/FileSource.php new file mode 100644 index 0000000..2802d80 --- /dev/null +++ b/vendor/iio/libmergepdf/src/Source/FileSource.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types = 1); + +namespace iio\libmergepdf\Source; + +use iio\libmergepdf\PagesInterface; +use iio\libmergepdf\Pages; +use iio\libmergepdf\Exception; + +/** + * Pdf source from file + */ +final class FileSource implements SourceInterface +{ + /** + * @var string + */ + private $filename; + + /** + * @var PagesInterface + */ + private $pages; + + public function __construct(string $filename, PagesInterface $pages = null) + { + if (!is_file($filename) || !is_readable($filename)) { + throw new Exception("Invalid file '$filename'"); + } + + $this->filename = $filename; + $this->pages = $pages ?: new Pages; + } + + public function getName(): string + { + return $this->filename; + } + + public function getContents(): string + { + return (string)file_get_contents($this->filename); + } + + public function getPages(): PagesInterface + { + return $this->pages; + } +} diff --git a/vendor/iio/libmergepdf/src/Source/RawSource.php b/vendor/iio/libmergepdf/src/Source/RawSource.php new file mode 100644 index 0000000..7966918 --- /dev/null +++ b/vendor/iio/libmergepdf/src/Source/RawSource.php @@ -0,0 +1,45 @@ +<?php + +declare(strict_types = 1); + +namespace iio\libmergepdf\Source; + +use iio\libmergepdf\PagesInterface; +use iio\libmergepdf\Pages; + +/** + * Pdf source from raw string + */ +final class RawSource implements SourceInterface +{ + /** + * @var string + */ + private $contents; + + /** + * @var PagesInterface + */ + private $pages; + + public function __construct(string $contents, PagesInterface $pages = null) + { + $this->contents = $contents; + $this->pages = $pages ?: new Pages; + } + + public function getName(): string + { + return "raw-content"; + } + + public function getContents(): string + { + return $this->contents; + } + + public function getPages(): PagesInterface + { + return $this->pages; + } +} diff --git a/vendor/iio/libmergepdf/src/Source/SourceInterface.php b/vendor/iio/libmergepdf/src/Source/SourceInterface.php new file mode 100644 index 0000000..4ce9c3c --- /dev/null +++ b/vendor/iio/libmergepdf/src/Source/SourceInterface.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types = 1); + +namespace iio\libmergepdf\Source; + +use iio\libmergepdf\PagesInterface; + +interface SourceInterface +{ + /** + * Get name of file or source + */ + public function getName(): string; + + /** + * Get pdf content + */ + public function getContents(): string; + + /** + * Get pages to fetch from source + */ + public function getPages(): PagesInterface; +} diff --git a/vendor/iio/libmergepdf/tcpdi/LICENSE b/vendor/iio/libmergepdf/tcpdi/LICENSE new file mode 100644 index 0000000..37ec93a --- /dev/null +++ b/vendor/iio/libmergepdf/tcpdi/LICENSE @@ -0,0 +1,191 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and +distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright +owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities +that control, are controlled by, or are under common control with that entity. +For the purposes of this definition, "control" means (i) the power, direct or +indirect, to cause the direction or management of such entity, whether by +contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the +outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising +permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including +but not limited to software source code, documentation source, and configuration +files. + +"Object" form shall mean any form resulting from mechanical transformation or +translation of a Source form, including but not limited to compiled object code, +generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made +available under the License, as indicated by a copyright notice that is included +in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that +is based on (or derived from) the Work and for which the editorial revisions, +annotations, elaborations, or other modifications represent, as a whole, an +original work of authorship. For the purposes of this License, Derivative Works +shall not include works that remain separable from, or merely link (or bind by +name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version +of the Work and any modifications or additions to that Work or Derivative Works +thereof, that is intentionally submitted to Licensor for inclusion in the Work +by the copyright owner or by an individual or Legal Entity authorized to submit +on behalf of the copyright owner. For the purposes of this definition, +"submitted" means any form of electronic, verbal, or written communication sent +to the Licensor or its representatives, including but not limited to +communication on electronic mailing lists, source code control systems, and +issue tracking systems that are managed by, or on behalf of, the Licensor for +the purpose of discussing and improving the Work, but excluding communication +that is conspicuously marked or otherwise designated in writing by the copyright +owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf +of whom a Contribution has been received by Licensor and subsequently +incorporated within the Work. + +2. Grant of Copyright License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable copyright license to reproduce, prepare Derivative Works of, +publicly display, publicly perform, sublicense, and distribute the Work and such +Derivative Works in Source or Object form. + +3. Grant of Patent License. + +Subject to the terms and conditions of this License, each Contributor hereby +grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, +irrevocable (except as stated in this section) patent license to make, have +made, use, offer to sell, sell, import, and otherwise transfer the Work, where +such license applies only to those patent claims licensable by such Contributor +that are necessarily infringed by their Contribution(s) alone or by combination +of their Contribution(s) with the Work to which such Contribution(s) was +submitted. If You institute patent litigation against any entity (including a +cross-claim or counterclaim in a lawsuit) alleging that the Work or a +Contribution incorporated within the Work constitutes direct or contributory +patent infringement, then any patent licenses granted to You under this License +for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. + +You may reproduce and distribute copies of the Work or Derivative Works thereof +in any medium, with or without modifications, and in Source or Object form, +provided that You meet the following conditions: + +You must give any other recipients of the Work or Derivative Works a copy of +this License; and +You must cause any modified files to carry prominent notices stating that You +changed the files; and +You must retain, in the Source form of any Derivative Works that You distribute, +all copyright, patent, trademark, and attribution notices from the Source form +of the Work, excluding those notices that do not pertain to any part of the +Derivative Works; and +If the Work includes a "NOTICE" text file as part of its distribution, then any +Derivative Works that You distribute must include a readable copy of the +attribution notices contained within such NOTICE file, excluding those notices +that do not pertain to any part of the Derivative Works, in at least one of the +following places: within a NOTICE text file distributed as part of the +Derivative Works; within the Source form or documentation, if provided along +with the Derivative Works; or, within a display generated by the Derivative +Works, if and wherever such third-party notices normally appear. The contents of +the NOTICE file are for informational purposes only and do not modify the +License. You may add Your own attribution notices within Derivative Works that +You distribute, alongside or as an addendum to the NOTICE text from the Work, +provided that such additional attribution notices cannot be construed as +modifying the License. +You may add Your own copyright statement to Your modifications and may provide +additional or different license terms and conditions for use, reproduction, or +distribution of Your modifications, or for any such Derivative Works as a whole, +provided Your use, reproduction, and distribution of the Work otherwise complies +with the conditions stated in this License. + +5. Submission of Contributions. + +Unless You explicitly state otherwise, any Contribution intentionally submitted +for inclusion in the Work by You to the Licensor shall be under the terms and +conditions of this License, without any additional terms or conditions. +Notwithstanding the above, nothing herein shall supersede or modify the terms of +any separate license agreement you may have executed with Licensor regarding +such Contributions. + +6. Trademarks. + +This License does not grant permission to use the trade names, trademarks, +service marks, or product names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. + +Unless required by applicable law or agreed to in writing, Licensor provides the +Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, +including, without limitation, any warranties or conditions of TITLE, +NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are +solely responsible for determining the appropriateness of using or +redistributing the Work and assume any risks associated with Your exercise of +permissions under this License. + +8. Limitation of Liability. + +In no event and under no legal theory, whether in tort (including negligence), +contract, or otherwise, unless required by applicable law (such as deliberate +and grossly negligent acts) or agreed to in writing, shall any Contributor be +liable to You for damages, including any direct, indirect, special, incidental, +or consequential damages of any character arising as a result of this License or +out of the use or inability to use the Work (including but not limited to +damages for loss of goodwill, work stoppage, computer failure or malfunction, or +any and all other commercial damages or losses), even if such Contributor has +been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. + +While redistributing the Work or Derivative Works thereof, You may choose to +offer, and charge a fee for, acceptance of support, warranty, indemnity, or +other liability obligations and/or rights consistent with this License. However, +in accepting such obligations, You may act only on Your own behalf and on Your +sole responsibility, not on behalf of any other Contributor, and only if You +agree to indemnify, defend, and hold each Contributor harmless for any liability +incurred by, or claims asserted against, such Contributor by reason of your +accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work + +To apply the Apache License to your work, attach the following boilerplate +notice, with the fields enclosed by brackets "[]" replaced with your own +identifying information. (Don't include the brackets!) The text should be +enclosed in the appropriate comment syntax for the file format. We also +recommend that a file or class name and description of purpose be included on +the same "printed page" as the copyright notice for easier identification within +third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/vendor/iio/libmergepdf/tcpdi/README.md b/vendor/iio/libmergepdf/tcpdi/README.md new file mode 100644 index 0000000..73aa22c --- /dev/null +++ b/vendor/iio/libmergepdf/tcpdi/README.md @@ -0,0 +1,65 @@ +TCPDI +===== + +Composer ready TCPDI with PDF annotations handling. + +Imported from https://github.com/RafikHaceb/tcpdi + +PDF importer for [TCPDF](http://www.tcpdf.org/), based on [FPDI](http://www.setasign.de/products/pdf-php-solutions/fpdi/). +Requires [pauln/tcpdi_parser](https://github.com/pauln/tcpdi_parser) and [FPDF_TPL](http://www.setasign.de/products/pdf-php-solutions/fpdi/downloads/) +which included in the repository. + +Usage +----- + +Usage is essentially the same as FPDI, except importing TCPDI rather than FPDI. It also has a "setSourceData()" function which accepts raw PDF data, for cases where the file does not reside on disk or is not readable by TCPDI. + +```php +// Create new PDF document. +$pdf = new TCPDI(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); + +// Add a page from a PDF by file path. +$pdf->AddPage(); +$pdf->setSourceFile('/path/to/file-to-import.pdf'); +$idx = $pdf->importPage(1); +$pdf->useTemplate($idx); + +$pdfdata = file_get_contents('/path/to/other-file.pdf'); // Simulate only having raw data available. +$pagecount = $pdf->setSourceData($pdfdata); +for ($i = 1; $i <= $pagecount; $i++) { + $tplidx = $pdf->importPage($i); + $pdf->AddPage(); + $pdf->useTemplate($tplidx); +} +``` + +As of version 1.1, TCPDI also includes additional functionality for handling PDF Annotations. As annotations are positioned relative to the bleed box rather than the crop box, you'll need to ensure that you're importing the full bleed box; a new function has also been introduced to set the page format (the various boxes, including the crop box) from the imported page, so that the imported page matches the original better. The following example demonstrates this: + +```php +// Create new PDF document. +$pdf = new TCPDI(PDF_PAGE_ORIENTATION, PDF_UNIT, PDF_PAGE_FORMAT, true, 'UTF-8', false); + +// Add a page from a PDF by file path. +$pdf->setSourceFile('/path/to/file-to-import.pdf'); + +// Import the bleed box (default is crop box) for page 1. +$tplidx = $pdf->importPage(1, '/BleedBox'); +$size = $pdf->getTemplatesize($tplidx); +$orientation = ($size['w'] > $size['h']) ? 'L' : 'P'; + +$pdf->AddPage($orientation); + +// Set page boxes from imported page 1. +$pdf->setPageFormatFromTemplatePage(1, $orientation); + +// Import the content for page 1. +$pdf->useTemplate($tplidx); + +// Import the annotations for page 1. +$pdf->importAnnotations(1); +``` + +TCPDI_PARSER +============ + +Parser for use with TCPDI, based on TCPDF_PARSER. Supports PDFs up to v1.7. diff --git a/vendor/iio/libmergepdf/tcpdi/fpdf_tpl.php b/vendor/iio/libmergepdf/tcpdi/fpdf_tpl.php new file mode 100644 index 0000000..0da7d7b --- /dev/null +++ b/vendor/iio/libmergepdf/tcpdi/fpdf_tpl.php @@ -0,0 +1,460 @@ +<?php +// +// FPDF_TPL - Version 1.2.3 +// +// Copyright 2004-2013 Setasign - Jan Slabon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +class FPDF_TPL extends FPDF { + /** + * Array of Tpl-Data + * @var array + */ + var $tpls = array(); + + /** + * Current Template-ID + * @var int + */ + var $tpl = 0; + + /** + * "In Template"-Flag + * @var boolean + */ + var $_intpl = false; + + /** + * Nameprefix of Templates used in Resources-Dictonary + * @var string A String defining the Prefix used as Template-Object-Names. Have to beginn with an / + */ + var $tplprefix = "/TPL"; + + /** + * Resources used By Templates and Pages + * @var array + */ + var $_res = array(); + + /** + * Last used Template data + * + * @var array + */ + var $lastUsedTemplateData = array(); + + /** + * Start a Template + * + * This method starts a template. You can give own coordinates to build an own sized + * Template. Pay attention, that the margins are adapted to the new templatesize. + * If you want to write outside the template, for example to build a clipped Template, + * you have to set the Margins and "Cursor"-Position manual after beginTemplate-Call. + * + * If no parameter is given, the template uses the current page-size. + * The Method returns an ID of the current Template. This ID is used later for using this template. + * Warning: A created Template is used in PDF at all events. Still if you don't use it after creation! + * + * @param int $x The x-coordinate given in user-unit + * @param int $y The y-coordinate given in user-unit + * @param int $w The width given in user-unit + * @param int $h The height given in user-unit + * @return int The ID of new created Template + */ + function beginTemplate($x = null, $y = null, $w = null, $h = null) { + if (is_subclass_of($this, 'TCPDF')) { + $this->Error('This method is only usable with FPDF. Use TCPDF methods startTemplate() instead.'); + return; + } + + if ($this->page <= 0) + $this->error("You have to add a page to fpdf first!"); + + if ($x == null) + $x = 0; + if ($y == null) + $y = 0; + if ($w == null) + $w = $this->w; + if ($h == null) + $h = $this->h; + + // Save settings + $this->tpl++; + $tpl =& $this->tpls[$this->tpl]; + $tpl = array( + 'o_x' => $this->x, + 'o_y' => $this->y, + 'o_AutoPageBreak' => $this->AutoPageBreak, + 'o_bMargin' => $this->bMargin, + 'o_tMargin' => $this->tMargin, + 'o_lMargin' => $this->lMargin, + 'o_rMargin' => $this->rMargin, + 'o_h' => $this->h, + 'o_w' => $this->w, + 'o_FontFamily' => $this->FontFamily, + 'o_FontStyle' => $this->FontStyle, + 'o_FontSizePt' => $this->FontSizePt, + 'o_FontSize' => $this->FontSize, + 'buffer' => '', + 'x' => $x, + 'y' => $y, + 'w' => $w, + 'h' => $h + ); + + $this->SetAutoPageBreak(false); + + // Define own high and width to calculate possitions correct + $this->h = $h; + $this->w = $w; + + $this->_intpl = true; + $this->SetXY($x + $this->lMargin, $y + $this->tMargin); + $this->SetRightMargin($this->w - $w + $this->rMargin); + + if ($this->CurrentFont) { + $fontkey = $this->FontFamily . $this->FontStyle; + $this->_res['tpl'][$this->tpl]['fonts'][$fontkey] =& $this->fonts[$fontkey]; + + $this->_out(sprintf('BT /F%d %.2f Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + + return $this->tpl; + } + + /** + * End Template + * + * This method ends a template and reset initiated variables on beginTemplate. + * + * @return mixed If a template is opened, the ID is returned. If not a false is returned. + */ + function endTemplate() { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::endTemplate'), $args); + } + + if ($this->_intpl) { + $this->_intpl = false; + $tpl =& $this->tpls[$this->tpl]; + $this->SetXY($tpl['o_x'], $tpl['o_y']); + $this->tMargin = $tpl['o_tMargin']; + $this->lMargin = $tpl['o_lMargin']; + $this->rMargin = $tpl['o_rMargin']; + $this->h = $tpl['o_h']; + $this->w = $tpl['o_w']; + $this->SetAutoPageBreak($tpl['o_AutoPageBreak'], $tpl['o_bMargin']); + + $this->FontFamily = $tpl['o_FontFamily']; + $this->FontStyle = $tpl['o_FontStyle']; + $this->FontSizePt = $tpl['o_FontSizePt']; + $this->FontSize = $tpl['o_FontSize']; + + $fontkey = $this->FontFamily . $this->FontStyle; + if ($fontkey) + $this->CurrentFont =& $this->fonts[$fontkey]; + + return $this->tpl; + } else { + return false; + } + } + + /** + * Use a Template in current Page or other Template + * + * You can use a template in a page or in another template. + * You can give the used template a new size like you use the Image()-method. + * All parameters are optional. The width or height is calculated automaticaly + * if one is given. If no parameter is given the origin size as defined in + * beginTemplate() is used. + * The calculated or used width and height are returned as an array. + * + * @param int $tplidx A valid template-Id + * @param int $_x The x-position + * @param int $_y The y-position + * @param int $_w The new width of the template + * @param int $_h The new height of the template + * @retrun array The height and width of the template + */ + function useTemplate($tplidx, $_x = null, $_y = null, $_w = 0, $_h = 0) { + if ($this->page <= 0) + $this->error('You have to add a page first!'); + + if (!isset($this->tpls[$tplidx])) + $this->error('Template does not exist!'); + + if ($this->_intpl) { + $this->_res['tpl'][$this->tpl]['tpls'][$tplidx] =& $this->tpls[$tplidx]; + } + + $tpl =& $this->tpls[$tplidx]; + $w = $tpl['w']; + $h = $tpl['h']; + + if ($_x == null) + $_x = 0; + if ($_y == null) + $_y = 0; + + $_x += $tpl['x']; + $_y += $tpl['y']; + + $wh = $this->getTemplateSize($tplidx, $_w, $_h); + $_w = $wh['w']; + $_h = $wh['h']; + + $tData = array( + 'x' => $this->x, + 'y' => $this->y, + 'w' => $_w, + 'h' => $_h, + 'scaleX' => ($_w / $w), + 'scaleY' => ($_h / $h), + 'tx' => $_x, + 'ty' => ($this->h - $_y - $_h), + 'lty' => ($this->h - $_y - $_h) - ($this->h - $h) * ($_h / $h) + ); + + $this->_out(sprintf('q %.4F 0 0 %.4F %.4F %.4F cm', $tData['scaleX'], $tData['scaleY'], $tData['tx'] * $this->k, $tData['ty'] * $this->k)); // Translate + $this->_out(sprintf('%s%d Do Q', $this->tplprefix, $tplidx)); + + $this->lastUsedTemplateData = $tData; + + return array('w' => $_w, 'h' => $_h); + } + + /** + * Get The calculated Size of a Template + * + * If one size is given, this method calculates the other one. + * + * @param int $tplidx A valid template-Id + * @param int $_w The width of the template + * @param int $_h The height of the template + * @return array The height and width of the template + */ + function getTemplateSize($tplidx, $_w = 0, $_h = 0) { + if (!isset($this->tpls[$tplidx])) + return false; + + $tpl =& $this->tpls[$tplidx]; + $w = $tpl['w']; + $h = $tpl['h']; + + if ($_w == 0 and $_h == 0) { + $_w = $w; + $_h = $h; + } + + if($_w == 0) + $_w = $_h * $w / $h; + if($_h == 0) + $_h = $_w * $h / $w; + + return array("w" => $_w, "h" => $_h); + } + + /** + * See FPDF/TCPDF-Documentation ;-) + */ + public function SetFont($family, $style = '', $size = 0, $fontfile='', $subset='default', $out=true) { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::SetFont'), $args); + } + + parent::SetFont($family, $style, $size); + + $fontkey = $this->FontFamily . $this->FontStyle; + + if ($this->_intpl) { + $this->_res['tpl'][$this->tpl]['fonts'][$fontkey] =& $this->fonts[$fontkey]; + } else { + $this->_res['page'][$this->page]['fonts'][$fontkey] =& $this->fonts[$fontkey]; + } + } + + /** + * See FPDF/TCPDF-Documentation ;-) + */ + function Image( + $file, $x = '', $y = '', $w = 0, $h = 0, $type = '', $link = '', $align = '', $resize = false, + $dpi = 300, $palign = '', $ismask = false, $imgmask = false, $border = 0, $fitbox = false, + $hidden = false, $fitonpage = false, $alt = false, $altimgs = array() + ) { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::Image'), $args); + } + + $ret = parent::Image($file, $x, $y, $w, $h, $type, $link); + if ($this->_intpl) { + $this->_res['tpl'][$this->tpl]['images'][$file] =& $this->images[$file]; + } else { + $this->_res['page'][$this->page]['images'][$file] =& $this->images[$file]; + } + + return $ret; + } + + /** + * See FPDF-Documentation ;-) + * + * AddPage is not available when you're "in" a template. + */ + function AddPage($orientation = '', $format = '', $keepmargins = false, $tocpage = false) { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::AddPage'), $args); + } + + if ($this->_intpl) + $this->Error('Adding pages in templates isn\'t possible!'); + + parent::AddPage($orientation, $format); + } + + /** + * Preserve adding Links in Templates ...won't work + */ + function Link($x, $y, $w, $h, $link, $spaces = 0) { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::Link'), $args); + } + + if ($this->_intpl) + $this->Error('Using links in templates aren\'t possible!'); + + parent::Link($x, $y, $w, $h, $link); + } + + function AddLink() { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::AddLink'), $args); + } + + if ($this->_intpl) + $this->Error('Adding links in templates aren\'t possible!'); + return parent::AddLink(); + } + + function SetLink($link, $y = 0, $page = -1) { + if (is_subclass_of($this, 'TCPDF')) { + $args = func_get_args(); + return call_user_func_array(array($this, 'TCPDF::SetLink'), $args); + } + + if ($this->_intpl) + $this->Error('Setting links in templates aren\'t possible!'); + parent::SetLink($link, $y, $page); + } + + /** + * Private Method that writes the form xobjects + */ + function _putformxobjects() { + $filter=($this->compress) ? '/Filter /FlateDecode ' : ''; + reset($this->tpls); + foreach($this->tpls AS $tplidx => $tpl) { + + $p=($this->compress) ? gzcompress($tpl['buffer']) : $tpl['buffer']; + $this->_newobj(); + $this->tpls[$tplidx]['n'] = $this->n; + $this->_out('<<'.$filter.'/Type /XObject'); + $this->_out('/Subtype /Form'); + $this->_out('/FormType 1'); + $this->_out(sprintf('/BBox [%.2F %.2F %.2F %.2F]', + // llx + $tpl['x'] * $this->k, + // lly + -$tpl['y'] * $this->k, + // urx + ($tpl['w'] + $tpl['x']) * $this->k, + // ury + ($tpl['h'] - $tpl['y']) * $this->k + )); + + if ($tpl['x'] != 0 || $tpl['y'] != 0) { + $this->_out(sprintf('/Matrix [1 0 0 1 %.5F %.5F]', + -$tpl['x'] * $this->k * 2, $tpl['y'] * $this->k * 2 + )); + } + + $this->_out('/Resources '); + + $this->_out('<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + if (isset($this->_res['tpl'][$tplidx]['fonts']) && count($this->_res['tpl'][$tplidx]['fonts'])) { + $this->_out('/Font <<'); + foreach($this->_res['tpl'][$tplidx]['fonts'] as $font) + $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); + $this->_out('>>'); + } + if(isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images']) || + isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) + { + $this->_out('/XObject <<'); + if (isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images'])) { + foreach($this->_res['tpl'][$tplidx]['images'] as $image) + $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R'); + } + if (isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) { + foreach($this->_res['tpl'][$tplidx]['tpls'] as $i => $tpl) + $this->_out($this->tplprefix . $i . ' ' . $tpl['n'] . ' 0 R'); + } + $this->_out('>>'); + } + $this->_out('>>'); + + $this->_out('/Length ' . strlen($p) . ' >>'); + $this->_putstream($p); + $this->_out('endobj'); + } + } + + /** + * Overwritten to add _putformxobjects() after _putimages() + * + */ + function _putimages() { + parent::_putimages(); + $this->_putformxobjects(); + } + + function _putxobjectdict() { + parent::_putxobjectdict(); + + if (count($this->tpls)) { + foreach($this->tpls as $tplidx => $tpl) { + $this->_out(sprintf('%s%d %d 0 R', $this->tplprefix, $tplidx, $tpl['n'])); + } + } + } + + /** + * Private Method + */ + function _out($s) { + if ($this->state == 2 && $this->_intpl) { + $this->tpls[$this->tpl]['buffer'] .= $s . "\n"; + } else { + parent::_out($s); + } + } +} diff --git a/vendor/iio/libmergepdf/tcpdi/tcpdi.php b/vendor/iio/libmergepdf/tcpdi/tcpdi.php new file mode 100644 index 0000000..f210d42 --- /dev/null +++ b/vendor/iio/libmergepdf/tcpdi/tcpdi.php @@ -0,0 +1,841 @@ +<?php +// +// TCPDI - Version 1.1 +// Based on FPDI - Version 1.4.4 +// +// Copyright 2004-2013 Setasign - Jan Slabon +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +// Dummy shim to allow unmodified use of fpdf_tpl +class FPDF extends TCPDF {} + +require_once('fpdf_tpl.php'); + +require_once('tcpdi_parser.php'); + + +class TCPDI extends FPDF_TPL { + /** + * Actual filename + * @var string + */ + public $current_filename; + + /** + * Parser-Objects + * @var array + */ + public $parsers = array(); + + /** + * Current parser + * @var object + */ + public $current_parser; + + /** + * object stack + * @var array + */ + protected $_obj_stack = array(); + + /** + * done object stack + * @var array + */ + protected $_don_obj_stack = array(); + + /** + * Current Object Id. + * @var integer + */ + protected $_current_obj_id; + + /** + * The name of the last imported page box + * @var string + */ + public $lastUsedPageBox; + + /** + * Cache for imported pages/template ids + * @var array + */ + protected $_importedPages = array(); + + /** + * Cache for imported page annotations + * @var array + */ + protected $_importedAnnots = array(); + + /** + * Number of TOC pages, used for annotation offset + * @var integer + */ + protected $_numTOCpages = 0; + + /** + * First TOC page, used for annotation offset + * @var integer + */ + protected $_TOCpagenum = 0; + + /** + * Set a source-file + * + * @param string $filename a valid filename + * @return int number of available pages + */ + public function setSourceFile($filename) { + $this->current_filename = $filename; + + if (!isset($this->parsers[$filename])) + $this->parsers[$filename] = $this->_getPdfParser($filename); + $this->current_parser =& $this->parsers[$filename]; + $this->setPDFVersion(max($this->getPDFVersion(), $this->current_parser->getPDFVersion())); + + return $this->parsers[$filename]->getPageCount(); + } + + /** + * Set a source-file PDF data + * + * @param string $pdfdata The PDF file content + * @return int number of available pages + */ + public function setSourceData($pdfdata) { + $filename = uniqid('tcpdi-'); + $this->current_filename = $filename; + + if (!isset($this->parsers[$filename])) + $this->parsers[$filename] = new tcpdi_parser($pdfdata, $filename); + $this->current_parser =& $this->parsers[$filename]; + $this->setPDFVersion(max($this->getPDFVersion(), $this->current_parser->getPDFVersion())); + + return $this->parsers[$filename]->getPageCount(); + } + + /** + * Returns a PDF parser object + * + * @param string $filename + * @return fpdi_pdf_parser + */ + protected function _getPdfParser($filename) { + $data = file_get_contents($filename); + return new tcpdi_parser($data, $filename); + } + + /** + * Get the current PDF version + * + * @return string + */ + public function getPDFVersion() { + return $this->PDFVersion; + } + + /** + * Set the PDF version + * + * @return string + */ + public function setPDFVersion($version = '1.3') { + $this->PDFVersion = $version; + } + + /** + * Import a page + * + * @param int $pageno pagenumber + * @return int Index of imported page - to use with fpdf_tpl::useTemplate() + */ + public function importPage($pageno, $boxName = '/CropBox') { + if ($this->_intpl) { + return $this->error('Please import the desired pages before creating a new template.'); + } + + $fn = $this->current_filename; + + // check if page already imported + $pageKey = $fn . '-' . ((int)$pageno) . $boxName; + if (isset($this->_importedPages[$pageKey])) + return $this->_importedPages[$pageKey]; + + $parser =& $this->parsers[$fn]; + $parser->setPageno($pageno); + + if (!in_array($boxName, $parser->availableBoxes)) + return $this->Error(sprintf('Unknown box: %s', $boxName)); + + $pageboxes = $parser->getPageBoxes($pageno, $this->k); + + /** + * MediaBox + * CropBox: Default -> MediaBox + * BleedBox: Default -> CropBox + * TrimBox: Default -> CropBox + * ArtBox: Default -> CropBox + */ + if (!isset($pageboxes[$boxName]) && ($boxName == '/BleedBox' || $boxName == '/TrimBox' || $boxName == '/ArtBox')) + $boxName = '/CropBox'; + if (!isset($pageboxes[$boxName]) && $boxName == '/CropBox') + $boxName = '/MediaBox'; + + if (!isset($pageboxes[$boxName])) + return false; + + $this->lastUsedPageBox = $boxName; + + $box = $pageboxes[$boxName]; + + $this->tpl++; + $this->tpls[$this->tpl] = array(); + $tpl =& $this->tpls[$this->tpl]; + $tpl['parser'] =& $parser; + $tpl['resources'] = $parser->getPageResources(); + $tpl['buffer'] = $parser->getContent(); + $tpl['box'] = $box; + + // To build an array that can be used by PDF_TPL::useTemplate() + $this->tpls[$this->tpl] = array_merge($this->tpls[$this->tpl], $box); + + // An imported page will start at 0,0 everytime. Translation will be set in _putformxobjects() + $tpl['x'] = 0; + $tpl['y'] = 0; + + // handle rotated pages + $rotation = $parser->getPageRotation($pageno); + $tpl['_rotationAngle'] = 0; + if (isset($rotation[1]) && ($angle = $rotation[1] % 360) != 0) { + $steps = $angle / 90; + + $_w = $tpl['w']; + $_h = $tpl['h']; + $tpl['w'] = $steps % 2 == 0 ? $_w : $_h; + $tpl['h'] = $steps % 2 == 0 ? $_h : $_w; + + if ($angle < 0) + $angle += 360; + + $tpl['_rotationAngle'] = $angle * -1; + } + + $this->_importedPages[$pageKey] = $this->tpl; + + return $this->tpl; + } + + public function setPageFormatFromTemplatePage($pageno, $orientation) { + $fn = $this->current_filename; + $parser =& $this->parsers[$fn]; + $parser->setPageno($pageno); + $boxes = $parser->getPageBoxes($pageno, $this->k); + foreach ($boxes as $name => $box) { + if ($name[0] == '/') { + $boxes[substr($name, 1)] = $box; + unset($boxes[$name]); + } + } + $this->setPageFormat($boxes, $orientation); + } + + /* Wrapper for AddPage() which tracks TOC pages to offset annotations later */ + public function AddPage($orientation='', $format='', $keepmargins=false, $tocpage=false) { + if ($this->inxobj) { + // we are inside an XObject template + return; + } + parent::AddPage($orientation, $format, $keepmargins, $tocpage); + if ($this->tocpage) { + $this->_numTOCpages++; + } + } + + /* Wrapper for AddTOC() which tracks TOC position to offset annotations later */ + public function AddTOC($page='', $numbersfont='', $filler='.', $toc_name='TOC', $style='', $color=array(0,0,0)) { + if (!TCPDF_STATIC::empty_string($page)) { + $this->_TOCpagenum = $page; + } else { + $this->_TOCpagenum = $this->page; + } + + parent::AddTOC($page, $numbersfont, $filler, $toc_name, $style, $color); + } + + public function importAnnotations($pageno) { + $fn = $this->current_filename; + $parser =& $this->parsers[$fn]; + $parser->setPageno($pageno); + $annots = $parser->getPageAnnotations(); + + if (is_array($annots) && $annots[0] == PDF_TYPE_OBJECT // We got an object + && is_array($annots[1]) && $annots[1][0] == PDF_TYPE_ARRAY // It's an array + && is_array($annots[1][1]) && count($annots[1][1] > 1) // It's not empty - there are annotations for this page + ) { + if (!isset($this->_obj_stack[$fn])) { + $this->_obj_stack[$fn] = array(); + } + + $this->_importedAnnots[$this->page] = array(); + foreach ($annots[1][1] as $annot) { + $this->importAnnotation($annot); + } + } + } + + public function importAnnotation($annotation) { + $fn = $this->current_filename; + $old_id = $annotation[1]; + $value = array(PDF_TYPE_OBJREF, $old_id, 0); + if (!isset($this->_don_obj_stack[$fn][$old_id])) { + $this->_newobj(false, true); + $this->_obj_stack[$fn][$old_id] = array($this->n, $value); + $this->_don_obj_stack[$fn][$old_id] = array($this->n, $value); + } + $objid = $this->_don_obj_stack[$fn][$old_id][0]; + $this->_importedAnnots[$this->page][] = $objid; + } + + /** + * Get references to page annotations. + * @param $n (int) page number + * @return string + * @protected + * @author Nicola Asuni + * @since 5.0.010 (2010-05-17) + */ + protected function _getannotsrefs($n) { + if (!empty($this->_numTOCpages) && $n >= $this->_TOCpagenum) { + // Offset page number to account for TOC being inserted before page containing annotations. + $n -= $this->_numTOCpages; + } + if (!(isset($this->_importedAnnots[$n]) OR isset($this->PageAnnots[$n]) OR ($this->sign AND isset($this->signature_data['cert_type'])))) { + return ''; + } + $out = ' /Annots ['; + if (isset($this->_importedAnnots[$n])) { + foreach ($this->_importedAnnots[$n] as $key => $val) { + $out .= ' '.$val.' 0 R'; + } + } + if (isset($this->PageAnnots[$n])) { + foreach ($this->PageAnnots[$n] as $key => $val) { + if (!in_array($val['n'], $this->radio_groups)) { + $out .= ' '.$val['n'].' 0 R'; + } + } + // add radiobutton groups + if (isset($this->radiobutton_groups[$n])) { + foreach ($this->radiobutton_groups[$n] as $key => $data) { + if (isset($data['n'])) { + $out .= ' '.$data['n'].' 0 R'; + } + } + } + } + if ($this->sign AND ($n == $this->signature_appearance['page']) AND isset($this->signature_data['cert_type'])) { + // set reference for signature object + $out .= ' '.$this->sig_obj_id.' 0 R'; + } + if (!empty($this->empty_signature_appearance)) { + foreach ($this->empty_signature_appearance as $esa) { + if ($esa['page'] == $n) { + // set reference for empty signature objects + $out .= ' '.$esa['objid'].' 0 R'; + } + } + } + $out .= ' ]'; + return $out; + } + + /** + * Returns the last used page box + * + * @return string + */ + public function getLastUsedPageBox() { + return $this->lastUsedPageBox; + } + + + public function useTemplate($tplidx, $_x = null, $_y = null, $_w = 0, $_h = 0, $adjustPageSize = false) { + if ($adjustPageSize == true && is_null($_x) && is_null($_y)) { + $size = $this->getTemplateSize($tplidx, $_w, $_h); + $orientation = $size['w'] > $size['h'] ? 'L' : 'P'; + $size = array($size['w'], $size['h']); + + $this->setPageFormat($size, $orientation); + } + + $this->_out('q 0 J 1 w 0 j 0 G 0 g'); // reset standard values + $s = parent::useTemplate($tplidx, $_x, $_y, $_w, $_h); + $this->_out('Q'); + + return $s; + } + + /** + * Private method, that rebuilds all needed objects of source files + */ + public function _putimportedobjects() { + if (is_array($this->parsers) && count($this->parsers) > 0) { + foreach($this->parsers AS $filename => $p) { + $this->current_parser =& $this->parsers[$filename]; + if (isset($this->_obj_stack[$filename]) && is_array($this->_obj_stack[$filename])) { + while(($n = key($this->_obj_stack[$filename])) !== null) { + $nObj = $this->current_parser->getObjectVal($this->_obj_stack[$filename][$n][1]); + + $this->_newobj($this->_obj_stack[$filename][$n][0]); + + if ($nObj[0] == PDF_TYPE_STREAM) { + $this->pdf_write_value($nObj); + } else { + $this->pdf_write_value($nObj[1]); + } + + $this->_out('endobj'); + $this->_obj_stack[$filename][$n] = null; // free memory + unset($this->_obj_stack[$filename][$n]); + reset($this->_obj_stack[$filename]); + } + } + + // We're done with this parser. Clean it up to free a bit of RAM. + $this->current_parser->cleanUp(); + unset($this->parsers[$filename]); + } + } + } + + + /** + * Private Method that writes the form xobjects + */ + public function _putformxobjects() { + $filter=($this->compress) ? '/Filter /FlateDecode ' : ''; + reset($this->tpls); + foreach($this->tpls AS $tplidx => $tpl) { + $p=($this->compress) ? gzcompress($tpl['buffer']) : $tpl['buffer']; + $this->_newobj(); + $cN = $this->n; // TCPDF/Protection: rem current "n" + + $this->tpls[$tplidx]['n'] = $this->n; + $this->_out('<<' . $filter . '/Type /XObject'); + $this->_out('/Subtype /Form'); + $this->_out('/FormType 1'); + + $this->_out(sprintf('/BBox [%.2F %.2F %.2F %.2F]', + (isset($tpl['box']['llx']) ? $tpl['box']['llx'] : $tpl['x']) * $this->k, + (isset($tpl['box']['lly']) ? $tpl['box']['lly'] : -$tpl['y']) * $this->k, + (isset($tpl['box']['urx']) ? $tpl['box']['urx'] : $tpl['w'] + $tpl['x']) * $this->k, + (isset($tpl['box']['ury']) ? $tpl['box']['ury'] : $tpl['h'] - $tpl['y']) * $this->k + )); + + $c = 1; + $s = 0; + $tx = 0; + $ty = 0; + + if (isset($tpl['box'])) { + $tx = -$tpl['box']['llx']; + $ty = -$tpl['box']['lly']; + + if ($tpl['_rotationAngle'] <> 0) { + $angle = $tpl['_rotationAngle'] * M_PI/180; + $c=cos($angle); + $s=sin($angle); + + switch($tpl['_rotationAngle']) { + case -90: + $tx = -$tpl['box']['lly']; + $ty = $tpl['box']['urx']; + break; + case -180: + $tx = $tpl['box']['urx']; + $ty = $tpl['box']['ury']; + break; + case -270: + $tx = $tpl['box']['ury']; + $ty = -$tpl['box']['llx']; + break; + } + } + } elseif ($tpl['x'] != 0 || $tpl['y'] != 0) { + $tx = -$tpl['x'] * 2; + $ty = $tpl['y'] * 2; + } + + $tx *= $this->k; + $ty *= $this->k; + + if ($c != 1 || $s != 0 || $tx != 0 || $ty != 0) { + $this->_out(sprintf('/Matrix [%.5F %.5F %.5F %.5F %.5F %.5F]', + $c, $s, -$s, $c, $tx, $ty + )); + } + + $this->_out('/Resources '); + + if (isset($tpl['resources'])) { + $this->current_parser =& $tpl['parser']; + $this->pdf_write_value($tpl['resources']); // "n" will be changed + } else { + $this->_out('<</ProcSet [/PDF /Text /ImageB /ImageC /ImageI]'); + if (isset($this->_res['tpl'][$tplidx]['fonts']) && count($this->_res['tpl'][$tplidx]['fonts'])) { + $this->_out('/Font <<'); + foreach($this->_res['tpl'][$tplidx]['fonts'] as $font) + $this->_out('/F' . $font['i'] . ' ' . $font['n'] . ' 0 R'); + $this->_out('>>'); + } + if(isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images']) || + isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) + { + $this->_out('/XObject <<'); + if (isset($this->_res['tpl'][$tplidx]['images']) && count($this->_res['tpl'][$tplidx]['images'])) { + foreach($this->_res['tpl'][$tplidx]['images'] as $image) + $this->_out('/I' . $image['i'] . ' ' . $image['n'] . ' 0 R'); + } + if (isset($this->_res['tpl'][$tplidx]['tpls']) && count($this->_res['tpl'][$tplidx]['tpls'])) { + foreach($this->_res['tpl'][$tplidx]['tpls'] as $i => $tpl) + $this->_out($this->tplprefix . $i . ' ' . $tpl['n'] . ' 0 R'); + } + $this->_out('>>'); + } + $this->_out('>>'); + } + + $this->_out('/Group <</Type/Group/S/Transparency>>'); + + $nN = $this->n; // TCPDF: rem new "n" + $this->n = $cN; // TCPDF: reset to current "n" + + $p = $this->_getrawstream($p); + $this->_out('/Length ' . strlen($p) . ' >>'); + $this->_out("stream\n" . $p . "\nendstream"); + + $this->_out('endobj'); + $this->n = $nN; // TCPDF: reset to new "n" + } + + $this->_putimportedobjects(); + } + + /** + * Rewritten to handle existing own defined objects + */ + protected function _newobj($obj_id = false, $onlynewobj = false) { + if (!$obj_id) { + $obj_id = ++$this->n; + } + + //Begin a new object + if (!$onlynewobj) { + $this->offsets[$obj_id] = $this->bufferlen; + $this->_out($obj_id . ' 0 obj'); + $this->_current_obj_id = $obj_id; // for later use with encryption + } + + return $obj_id; + } + + /** + * Writes a value + * Needed to rebuild the source document + * + * @param mixed $value A PDF-Value. Structure of values see cases in this method + */ + public function pdf_write_value(&$value) + { + switch ($value[0]) { + case PDF_TYPE_STRING: + if ($this->encrypted) { + $value[1] = $this->_unescape($value[1]); + $value[1] = $this->_encrypt_data($this->_current_obj_id, $value[1]); + $value[1] = TCPDF_STATIC::_escape($value[1]); + } + break; + + case PDF_TYPE_STREAM: + if ($this->encrypted) { + $value[2][1] = $this->_encrypt_data($this->_current_obj_id, $value[2][1]); + $value[1][1]['/Length'] = array( + PDF_TYPE_NUMERIC, + strlen($value[2][1]) + ); + } + break; + + case PDF_TYPE_HEX: + if ($this->encrypted) { + $value[1] = $this->hex2str($value[1]); + $value[1] = $this->_encrypt_data($this->_current_obj_id, $value[1]); + + // remake hexstring of encrypted string + $value[1] = $this->str2hex($value[1]); + } + break; + } + + switch ($value[0]) { + + case PDF_TYPE_TOKEN: + $this->_straightOut('/'.$value[1] . ' '); + break; + case PDF_TYPE_NUMERIC: + case PDF_TYPE_REAL: + if (is_float($value[1]) && $value[1] != 0) { + $this->_straightOut(rtrim(rtrim(sprintf('%F', $value[1]), '0'), '.') . ' '); + } else { + $this->_straightOut($value[1] . ' '); + } + break; + + case PDF_TYPE_ARRAY: + + // An array. Output the proper + // structure and move on. + + $this->_straightOut('['); + for ($i = 0; $i < count($value[1]); $i++) { + $this->pdf_write_value($value[1][$i]); + } + + $this->_out(']'); + break; + + case PDF_TYPE_DICTIONARY: + + // A dictionary. + $this->_straightOut('<<'); + + foreach ($value[1] as $k => $v) { + $this->_straightOut($k . ' '); + $this->pdf_write_value($v); + } + + $this->_straightOut('>>'); + break; + + case PDF_TYPE_OBJREF: + + // An indirect object reference + // Fill the object stack if needed + $cpfn =& $this->current_parser->uniqueid; + + if (!isset($this->_don_obj_stack[$cpfn][$value[1]])) { + $this->_newobj(false, true); + $this->_obj_stack[$cpfn][$value[1]] = array($this->n, $value); + $this->_don_obj_stack[$cpfn][$value[1]] = array($this->n, $value); // Value is maybee obsolete!!! + } + $objid = $this->_don_obj_stack[$cpfn][$value[1]][0]; + + $this->_out($objid . ' 0 R'); + break; + + case PDF_TYPE_STRING: + + // A string. + $this->_straightOut('(' . $value[1] . ')'); + + break; + + case PDF_TYPE_STREAM: + + // A stream. First, output the + // stream dictionary, then the + // stream data itself. + $this->pdf_write_value($value[1]); + $this->_out('stream'); + $this->_out($value[2][1]); + $this->_out('endstream'); + break; + + case PDF_TYPE_HEX: + $this->_straightOut('<' . $value[1] . '>'); + break; + + case PDF_TYPE_BOOLEAN: + $this->_straightOut($value[1] ? 'true ' : 'false '); + break; + + case PDF_TYPE_NULL: + // The null object. + + $this->_straightOut('null '); + break; + } + } + + /** + * Modified so not each call will add a newline to the output. + */ + protected function _straightOut($s) { + if ($this->state == 2) { + if ($this->inxobj) { + // we are inside an XObject template + $this->xobjects[$this->xobjid]['outdata'] .= $s; + } elseif ((!$this->InFooter) AND isset($this->footerlen[$this->page]) AND ($this->footerlen[$this->page] > 0)) { + // puts data before page footer + $pagebuff = $this->getPageBuffer($this->page); + $page = substr($pagebuff, 0, -$this->footerlen[$this->page]); + $footer = substr($pagebuff, -$this->footerlen[$this->page]); + $this->setPageBuffer($this->page, $page.$s.$footer); + // update footer position + $this->footerpos[$this->page] += strlen($s); + } else { + // set page data + $this->setPageBuffer($this->page, $s, true); + } + } elseif ($this->state > 0) { + // set general data + $this->setBuffer($s); + } + } + + /** + * rewritten to close opened parsers + * + */ + protected function _enddoc() { + parent::_enddoc(); + $this->_closeParsers(); + } + + /** + * close all files opened by parsers + */ + protected function _closeParsers() { + if ($this->state > 2 && count($this->parsers) > 0) { + $this->cleanUp(); + return true; + } + return false; + } + + /** + * Removes cylced references and closes the file handles of the parser objects + */ + public function cleanUp() { + foreach ($this->parsers as $k => $_){ + $this->parsers[$k]->cleanUp(); + $this->parsers[$k] = null; + unset($this->parsers[$k]); + } + } + + // Functions from here on are taken from FPDI's fpdi2tcpdf_bridge.php to remove dependence on it + protected function _putstream($s, $n=0) { + $this->_out($this->_getstream($s, $n)); + } + + protected function _getxobjectdict() { + $out = parent::_getxobjectdict(); + if (count($this->tpls)) { + foreach($this->tpls as $tplidx => $tpl) { + $out .= sprintf('%s%d %d 0 R', $this->tplprefix, $tplidx, $tpl['n']); + } + } + + return $out; + } + + /** + * Unescapes a PDF string + * + * @param string $s + * @return string + */ + protected function _unescape($s) { + $out = ''; + for ($count = 0, $n = strlen($s); $count < $n; $count++) { + if ($s[$count] != '\\' || $count == $n-1) { + $out .= $s[$count]; + } else { + switch ($s[++$count]) { + case ')': + case '(': + case '\\': + $out .= $s[$count]; + break; + case 'f': + $out .= chr(0x0C); + break; + case 'b': + $out .= chr(0x08); + break; + case 't': + $out .= chr(0x09); + break; + case 'r': + $out .= chr(0x0D); + break; + case 'n': + $out .= chr(0x0A); + break; + case "\r": + if ($count != $n-1 && $s[$count+1] == "\n") + $count++; + break; + case "\n": + break; + default: + // Octal-Values + if (ord($s[$count]) >= ord('0') && + ord($s[$count]) <= ord('9')) { + $oct = ''. $s[$count]; + + if (ord($s[$count+1]) >= ord('0') && + ord($s[$count+1]) <= ord('9')) { + $oct .= $s[++$count]; + + if (ord($s[$count+1]) >= ord('0') && + ord($s[$count+1]) <= ord('9')) { + $oct .= $s[++$count]; + } + } + + $out .= chr(octdec($oct)); + } else { + $out .= $s[$count]; + } + } + } + } + return $out; + } + + /** + * Hexadecimal to string + * + * @param string $hex + * @return string + */ + public function hex2str($hex) { + return pack('H*', str_replace(array("\r", "\n", ' '), '', $hex)); + } + + /** + * String to hexadecimal + * + * @param string $str + * @return string + */ + public function str2hex($str) { + return current(unpack('H*', $str)); + } +} diff --git a/vendor/iio/libmergepdf/tcpdi/tcpdi_parser.php b/vendor/iio/libmergepdf/tcpdi/tcpdi_parser.php new file mode 100644 index 0000000..2391934 --- /dev/null +++ b/vendor/iio/libmergepdf/tcpdi/tcpdi_parser.php @@ -0,0 +1,1450 @@ +<?php +//============================================================+ +// File name : tcpdi_parser.php +// Version : 1.1 +// Begin : 2013-09-25 +// Last Update : 2016-05-03 +// Author : Paul Nicholls - https://github.com/pauln +// License : GNU-LGPL v3 (http://www.gnu.org/copyleft/lesser.html) +// +// Based on : tcpdf_parser.php +// Version : 1.0.003 +// Begin : 2011-05-23 +// Last Update : 2013-03-17 +// Author : Nicola Asuni - Tecnick.com LTD - www.tecnick.com - info@tecnick.com +// License : GNU-LGPL v3 (http://www.gnu.org/copyleft/lesser.html) +// ------------------------------------------------------------------- +// Copyright (C) 2011-2013 Nicola Asuni - Tecnick.com LTD +// +// This file is for use with the TCPDF software library. +// +// tcpdi_parser is free software: you can redistribute it and/or modify it +// under the terms of the GNU Lesser General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// tcpdi_parser is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU Lesser General Public License for more details. +// +// You should have received a copy of the License +// along with tcpdi_parser. If not, see +// <http://www.tecnick.com/pagefiles/tcpdf/LICENSE.TXT>. +// +// See LICENSE file for more information. +// ------------------------------------------------------------------- +// +// Description : This is a PHP class for parsing PDF documents. +// +//============================================================+ + +/** + * @file + * This is a PHP class for parsing PDF documents.<br> + * @author Paul Nicholls + * @author Nicola Asuni + * @version 1.1 + */ + +if (!defined ('PDF_TYPE_NULL')) + define ('PDF_TYPE_NULL', 0); +if (!defined ('PDF_TYPE_NUMERIC')) + define ('PDF_TYPE_NUMERIC', 1); +if (!defined ('PDF_TYPE_TOKEN')) + define ('PDF_TYPE_TOKEN', 2); +if (!defined ('PDF_TYPE_HEX')) + define ('PDF_TYPE_HEX', 3); +if (!defined ('PDF_TYPE_STRING')) + define ('PDF_TYPE_STRING', 4); +if (!defined ('PDF_TYPE_DICTIONARY')) + define ('PDF_TYPE_DICTIONARY', 5); +if (!defined ('PDF_TYPE_ARRAY')) + define ('PDF_TYPE_ARRAY', 6); +if (!defined ('PDF_TYPE_OBJDEC')) + define ('PDF_TYPE_OBJDEC', 7); +if (!defined ('PDF_TYPE_OBJREF')) + define ('PDF_TYPE_OBJREF', 8); +if (!defined ('PDF_TYPE_OBJECT')) + define ('PDF_TYPE_OBJECT', 9); +if (!defined ('PDF_TYPE_STREAM')) + define ('PDF_TYPE_STREAM', 10); +if (!defined ('PDF_TYPE_BOOLEAN')) + define ('PDF_TYPE_BOOLEAN', 11); +if (!defined ('PDF_TYPE_REAL')) + define ('PDF_TYPE_REAL', 12); + +/** + * @class tcpdi_parser + * This is a PHP class for parsing PDF documents.<br> + * Based on TCPDF_PARSER, part of the TCPDF project by Nicola Asuni. + * @brief This is a PHP class for parsing PDF documents.. + * @version 1.1 + * @author Paul Nicholls - github.com/pauln + * @author Nicola Asuni - info@tecnick.com + */ +class tcpdi_parser { + /** + * Unique parser ID + * @public + */ + public $uniqueid = ''; + + /** + * Raw content of the PDF document. + * @private + */ + private $pdfdata = ''; + + /** + * XREF data. + * @protected + */ + protected $xref = array(); + + /** + * Object streams. + * @protected + */ + protected $objstreams = array(); + + /** + * Objects in objstreams. + * @protected + */ + protected $objstreamobjs = array(); + + /** + * List of seen XREF data locations. + * @protected + */ + protected $xref_seen_offsets = array(); + + /** + * Array of PDF objects. + * @protected + */ + protected $objects = array(); + + /** + * Array of object offsets. + * @private + */ + private $objoffsets = array(); + + /** + * Class object for decoding filters. + * @private + */ + private $FilterDecoders; + + /** + * Pages + * + * @private array + */ + private $pages; + + /** + * Page count + * @private integer + */ + private $page_count; + + /** + * actual page number + * @private integer + */ + private $pageno; + + /** + * PDF version of the loaded document + * @private string + */ + private $pdfVersion; + + /** + * Available BoxTypes + * + * @public array + */ + public $availableBoxes = array('/MediaBox', '/CropBox', '/BleedBox', '/TrimBox', '/ArtBox'); + +// ----------------------------------------------------------------------------- + + /** + * Parse a PDF document an return an array of objects. + * @param $data (string) PDF data to parse. + * @public + * @since 1.0.000 (2011-05-24) + */ + public function __construct($data, $uniqueid) { + if (empty($data)) { + $this->Error('Empty PDF data.'); + } + $this->uniqueid = $uniqueid; + $this->pdfdata = $data; + // get length + $pdflen = strlen($this->pdfdata); + // initialize class for decoding filters + $this->FilterDecoders = new TCPDF_FILTERS(); + // get xref and trailer data + $this->xref = $this->getXrefData(); + $this->findObjectOffsets(); + // parse all document objects + $this->objects = array(); + /*foreach ($this->xref['xref'] as $obj => $offset) { + if (!isset($this->objects[$obj]) AND ($offset > 0)) { + // decode only objects with positive offset + //$this->objects[$obj] = $this->getIndirectObject($obj, $offset, true); + } + }*/ + $this->getPDFVersion(); + $this->readPages(); + } + + /** + * Clean up when done, to free memory etc + */ + public function cleanUp() { + unset($this->pdfdata); + $this->pdfdata = ''; + unset($this->objstreams); + $this->objstreams = array(); + unset($this->objects); + $this->objects = array(); + unset($this->objstreamobjs); + $this->objstreamobjs = array(); + unset($this->xref); + $this->xref = array(); + unset($this->objoffsets); + $this->objoffsets = array(); + unset($this->pages); + $this->pages = array(); + } + + /** + * Return an array of parsed PDF document objects. + * @return (array) Array of parsed PDF document objects. + * @public + * @since 1.0.000 (2011-06-26) + */ + public function getParsedData() { + return array($this->xref, $this->objects, $this->pages); + } + + /** + * Get PDF-Version + * + * And reset the PDF Version used in FPDI if needed + * @public + */ + public function getPDFVersion() { + preg_match('/\d\.\d/', substr($this->pdfdata, 0, 16), $m); + if (isset($m[0])) + $this->pdfVersion = $m[0]; + return $this->pdfVersion; + } + + /** + * Read all /Page(es) + * + */ + function readPages() { + $params = $this->getObjectVal($this->xref['trailer'][1]['/Root']); + $objref = null; + if ($params && $params[1] && is_array($params[1][1])) { + foreach ($params[1][1] as $k=>$v) { + if ($k == '/Pages') { + $objref = $v; + break; + } + } + } + if ($objref == null || $objref[0] !== PDF_TYPE_OBJREF) { + // Offset not found. + return; + } + + $dict = $this->getObjectVal($objref); + if ($dict[0] == PDF_TYPE_OBJECT && $dict[1][0] == PDF_TYPE_DICTIONARY) { + // Dict wrapped in an object + $dict = $dict[1]; + } + + if ($dict[0] !== PDF_TYPE_DICTIONARY) { + return; + } + + $this->pages = array(); + if (isset($dict[1]['/Kids'])) { + $v = $dict[1]['/Kids']; + if ($v[0] == PDF_TYPE_ARRAY) { + foreach ($v[1] as $ref) { + $page = $this->getObjectVal($ref); + $this->readPage($page); + } + } + } + + $this->page_count = count($this->pages); + } + + /** + * Read a single /Page element, recursing through /Kids if necessary + * + */ + private function readPage($page) { + if (isset($page[1][1]['/Kids'])) { + // Nested pages! + foreach ($page[1][1]['/Kids'][1] as $subref) { + $subpage = $this->getObjectVal($subref); + $this->readPage($subpage); + } + } else { + $this->pages[] = $page; + } + } + + /** + * Get pagecount from sourcefile + * + * @return int + */ + function getPageCount() { + return $this->page_count; + } + + /** + * Get Cross-Reference (xref) table and trailer data from PDF document data. + * @param $offset (int) xref offset (if know). + * @param $xref (array) previous xref array (if any). + * @return Array containing xref and trailer data. + * @protected + * @since 1.0.000 (2011-05-24) + */ + protected function getXrefData($offset=0, $xref=array()) { + if ($offset == 0) { + // find last startxref + if (preg_match('/.*[\r\n]startxref[\s\r\n]+([0-9]+)[\s\r\n]+%%EOF/is', $this->pdfdata, $matches) == 0) { + $this->Error('Unable to find startxref'); + } + $startxref = $matches[1]; + } else { + if (preg_match('/([0-9]+[\s][0-9]+[\s]obj)/i', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE, $offset)) { + // Cross-Reference Stream object + $startxref = $offset; + } elseif (preg_match('/[\r\n]startxref[\s\r\n]+([0-9]+)[\s\r\n]+%%EOF/i', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE, $offset)) { + // startxref found + $startxref = $matches[1][0]; + } else { + $this->Error('Unable to find startxref'); + } + } + unset($matches); + + // DOMPDF gets the startxref wrong, giving us the linebreak before the xref starts. + $startxref += strspn($this->pdfdata, "\r\n", $startxref); + + // check xref position + if (strpos($this->pdfdata, 'xref', $startxref) == $startxref) { + // Cross-Reference + $xref = $this->decodeXref($startxref, $xref); + } else { + // Cross-Reference Stream + $xref = $this->decodeXrefStream($startxref, $xref); + } + if (empty($xref)) { + $this->Error('Unable to find xref'); + } + + return $xref; + } + + /** + * Decode the Cross-Reference section + * @param $startxref (int) Offset at which the xref section starts. + * @param $xref (array) Previous xref array (if any). + * @return Array containing xref and trailer data. + * @protected + * @since 1.0.000 (2011-06-20) + */ + protected function decodeXref($startxref, $xref=array()) { + $this->xref_seen_offsets[] = $startxref; + if (!isset($xref['xref_location'])) { + $xref['xref_location'] = $startxref; + $xref['max_object'] = 0; + } + // extract xref data (object indexes and offsets) + $xoffset = $startxref + 5; + // initialize object number + $obj_num = 0; + $offset = $xoffset; + while (preg_match('/^([0-9]+)[\s]([0-9]+)[\s]?([nf]?)/im', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE, $offset) > 0) { + $offset = (strlen($matches[0][0]) + $matches[0][1]); + if ($matches[3][0] == 'n') { + // create unique object index: [object number]_[generation number] + $gen_num = intval($matches[2][0]); + $index = $obj_num.'_'.$gen_num; + // check if object already exist + if (!isset($xref['xref'][$obj_num][$gen_num])) { + // store object offset position + $xref['xref'][$obj_num][$gen_num] = intval($matches[1][0]); + } + ++$obj_num; + $offset += 2; + } elseif ($matches[3][0] == 'f') { + ++$obj_num; + $offset += 2; + } else { + // object number (index) + $obj_num = intval($matches[1][0]); + } + } + unset($matches); + $xref['max_object'] = max($xref['max_object'], $obj_num); + // get trailer data + if (preg_match('/trailer[\s]*<<(.*)>>[\s\r\n]+(?:[%].*[\r\n]+)*startxref[\s\r\n]+/isU', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE, $xoffset) > 0) { + $trailer_data = $matches[1][0]; + if (!isset($xref['trailer']) OR empty($xref['trailer'])) { + // get only the last updated version + $xref['trailer'] = array(); + $xref['trailer'][0] = PDF_TYPE_DICTIONARY; + $xref['trailer'][1] = array(); + // parse trailer_data + if (preg_match('/Size[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) { + $xref['trailer'][1]['/Size'] = array(PDF_TYPE_NUMERIC, intval($matches[1])); + } + if (preg_match('/Root[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) { + $xref['trailer'][1]['/Root'] = array(PDF_TYPE_OBJREF, intval($matches[1]), intval($matches[2])); + } + if (preg_match('/Encrypt[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) { + $xref['trailer'][1]['/Encrypt'] = array(PDF_TYPE_OBJREF, intval($matches[1]), intval($matches[2])); + } + if (preg_match('/Info[\s]+([0-9]+)[\s]+([0-9]+)[\s]+R/i', $trailer_data, $matches) > 0) { + $xref['trailer'][1]['/Info'] = array(PDF_TYPE_OBJREF, intval($matches[1]), intval($matches[2])); + } + if (preg_match('/ID[\s]*[\[][\s]*[<]([^>]*)[>][\s]*[<]([^>]*)[>]/i', $trailer_data, $matches) > 0) { + $xref['trailer'][1]['/ID'] = array(PDF_TYPE_ARRAY, array()); + $xref['trailer'][1]['/ID'][1][0] = array(PDF_TYPE_HEX, $matches[1]); + $xref['trailer'][1]['/ID'][1][1] = array(PDF_TYPE_HEX, $matches[2]); + } + } + if (preg_match('/Prev[\s]+([0-9]+)/i', $trailer_data, $matches) > 0) { + // get previous xref + $prevoffset = intval($matches[1]); + if (!in_array($prevoffset, $this->xref_seen_offsets)) { + $this->xref_seen_offsets[] = $prevoffset; + $xref = $this->getXrefData($prevoffset, $xref); + } + } + unset($matches); + } else { + $this->Error('Unable to find trailer'); + } + return $xref; + } + + /** + * Decode the Cross-Reference Stream section + * @param $startxref (int) Offset at which the xref section starts. + * @param $xref (array) Previous xref array (if any). + * @return Array containing xref and trailer data. + * @protected + * @since 1.0.003 (2013-03-16) + */ + protected function decodeXrefStream($startxref, $xref=array()) { + // try to read Cross-Reference Stream + list($xrefobj, $unused) = $this->getRawObject($startxref); + $xrefcrs = $this->getIndirectObject($xrefobj[1], $startxref, true); + if (!isset($xref['xref_location'])) { + $xref['xref_location'] = $startxref; + $xref['max_object'] = 0; + } + if (!isset($xref['xref'])) { + $xref['xref'] = array(); + } + if (!isset($xref['trailer']) OR empty($xref['trailer'])) { + // get only the last updated version + $xref['trailer'] = array(); + $xref['trailer'][0] = PDF_TYPE_DICTIONARY; + $xref['trailer'][1] = array(); + $filltrailer = true; + } else { + $filltrailer = false; + } + $valid_crs = false; + $sarr = $xrefcrs[0][1]; + $keys = array_keys($sarr); + $columns = 1; // Default as per PDF 32000-1:2008. + $predictor = 1; // Default as per PDF 32000-1:2008. + foreach ($keys as $k=>$key) { + $v = $sarr[$key]; + if (($key == '/Type') AND ($v[0] == PDF_TYPE_TOKEN AND ($v[1] == 'XRef'))) { + $valid_crs = true; + } elseif (($key == '/Index') AND ($v[0] == PDF_TYPE_ARRAY AND count($v[1]) >= 2)) { + // first object number in the subsection + $index_first = intval($v[1][0][1]); + // number of entries in the subsection + $index_entries = intval($v[1][1][1]); + } elseif (($key == '/Prev') AND ($v[0] == PDF_TYPE_NUMERIC)) { + // get previous xref offset + $prevxref = intval($v[1]); + } elseif (($key == '/W') AND ($v[0] == PDF_TYPE_ARRAY)) { + // number of bytes (in the decoded stream) of the corresponding field + $wb = array(); + $wb[0] = intval($v[1][0][1]); + $wb[1] = intval($v[1][1][1]); + $wb[2] = intval($v[1][2][1]); + } elseif (($key == '/DecodeParms') AND ($v[0] == PDF_TYPE_DICTIONARY)) { + $decpar = $v[1]; + foreach ($decpar as $kdc => $vdc) { + if (($kdc == '/Columns') AND ($vdc[0] == PDF_TYPE_NUMERIC)) { + $columns = intval($vdc[1]); + } elseif (($kdc == '/Predictor') AND ($vdc[0] == PDF_TYPE_NUMERIC)) { + $predictor = intval($vdc[1]); + } + } + } elseif ($filltrailer) { + switch($key) { + case '/Size': + case '/Root': + case '/Info': + case '/ID': + $xref['trailer'][1][$key] = $v; + break; + default: + break; + } + } + } + // decode data + $obj_num = 0; + if ($valid_crs AND isset($xrefcrs[1][3][0])) { + // number of bytes in a row + $rowlen = ($columns + 1); + // convert the stream into an array of integers + $sdata = unpack('C*', $xrefcrs[1][3][0]); + // split the rows + $sdata = array_chunk($sdata, $rowlen); + // initialize decoded array + $ddata = array(); + // initialize first row with zeros + $prev_row = array_fill (0, $rowlen, 0); + // for each row apply PNG unpredictor + foreach ($sdata as $k => $row) { + // initialize new row + $ddata[$k] = array(); + // get PNG predictor value + if (empty($predictor)) { + $predictor = (10 + $row[0]); + } + // for each byte on the row + for ($i=1; $i<=$columns; ++$i) { + if (!isset($row[$i])) { + // No more data in this row - we're done here. + break; + } + // new index + $j = ($i - 1); + $row_up = $prev_row[$j]; + if ($i == 1) { + $row_left = 0; + $row_upleft = 0; + } else { + $row_left = $row[($i - 1)]; + $row_upleft = $prev_row[($j - 1)]; + } + switch ($predictor) { + case 1: // No prediction (equivalent to PNG None) + case 10: { // PNG prediction (on encoding, PNG None on all rows) + $ddata[$k][$j] = $row[$i]; + break; + } + case 11: { // PNG prediction (on encoding, PNG Sub on all rows) + $ddata[$k][$j] = (($row[$i] + $row_left) & 0xff); + break; + } + case 12: { // PNG prediction (on encoding, PNG Up on all rows) + $ddata[$k][$j] = (($row[$i] + $row_up) & 0xff); + break; + } + case 13: { // PNG prediction (on encoding, PNG Average on all rows) + $ddata[$k][$j] = (($row[$i] + (($row_left + $row_up) / 2)) & 0xff); + break; + } + case 14: { // PNG prediction (on encoding, PNG Paeth on all rows) + // initial estimate + $p = ($row_left + $row_up - $row_upleft); + // distances + $pa = abs($p - $row_left); + $pb = abs($p - $row_up); + $pc = abs($p - $row_upleft); + $pmin = min($pa, $pb, $pc); + // return minumum distance + switch ($pmin) { + case $pa: { + $ddata[$k][$j] = (($row[$i] + $row_left) & 0xff); + break; + } + case $pb: { + $ddata[$k][$j] = (($row[$i] + $row_up) & 0xff); + break; + } + case $pc: { + $ddata[$k][$j] = (($row[$i] + $row_upleft) & 0xff); + break; + } + } + break; + } + default: { // PNG prediction (on encoding, PNG optimum) + $this->Error("Unknown PNG predictor $predictor"); + break; + } + } + } + $prev_row = $ddata[$k]; + } // end for each row + // complete decoding + unset($sdata); + $sdata = array(); + // for every row + foreach ($ddata as $k => $row) { + // initialize new row + $sdata[$k] = array(0, 0, 0); + if ($wb[0] == 0) { + // default type field + $sdata[$k][0] = 1; + } + $i = 0; // count bytes on the row + // for every column + for ($c = 0; $c < 3; ++$c) { + // for every byte on the column + for ($b = 0; $b < $wb[$c]; ++$b) { + if (isset($row[$i])) { + $sdata[$k][$c] += ($row[$i] << (($wb[$c] - 1 - $b) * 8)); + } + ++$i; + } + } + } + unset($ddata); + // fill xref + if (isset($index_first)) { + $obj_num = $index_first; + } else { + $obj_num = 0; + } + foreach ($sdata as $k => $row) { + switch ($row[0]) { + case 0: { // (f) linked list of free objects + ++$obj_num; + break; + } + case 1: { // (n) objects that are in use but are not compressed + // create unique object index: [object number]_[generation number] + $index = $obj_num.'_'.$row[2]; + // check if object already exist + if (!isset($xref['xref'][$obj_num][$row[2]])) { + // store object offset position + $xref['xref'][$obj_num][$row[2]] = $row[1]; + } + ++$obj_num; + break; + } + case 2: { // compressed objects + // $row[1] = object number of the object stream in which this object is stored + // $row[2] = index of this object within the object stream + /*$index = $row[1].'_0_'.$row[2]; + $xref['xref'][$row[1]][0][$row[2]] = -1;*/ + break; + } + default: { // null objects + break; + } + } + } + } // end decoding data + $xref['max_object'] = max($xref['max_object'], $obj_num); + if (isset($prevxref)) { + // get previous xref + $xref = $this->getXrefData($prevxref, $xref); + } + return $xref; + } + + /** + * Get raw stream data + * @param $offset (int) Stream offset. + * @param $length (int) Stream length. + * @return string Steam content + * @protected + */ + protected function getRawStream($offset, $length) { + $offset += strspn($this->pdfdata, "\x00\x09\x0a\x0c\x0d\x20", $offset); + $offset += 6; // "stream" + $offset += strspn($this->pdfdata, "\x20", $offset); + $offset += strspn($this->pdfdata, "\r\n", $offset); + + $obj = array(); + $obj[] = PDF_TYPE_STREAM; + $obj[] = substr($this->pdfdata, $offset, $length); + + return array($obj, $offset+$length); + } + + /** + * Get object type, raw value and offset to next object + * @param $offset (int) Object offset. + * @return array containing object type, raw value and offset to next object + * @protected + * @since 1.0.000 (2011-06-20) + */ + protected function getRawObject($offset=0, $data=null) { + if ($data == null) { + $data =& $this->pdfdata; + } + $objtype = ''; // object type to be returned + $objval = ''; // object value to be returned + // skip initial white space chars: \x00 null (NUL), \x09 horizontal tab (HT), \x0A line feed (LF), \x0C form feed (FF), \x0D carriage return (CR), \x20 space (SP) + while (strspn($data[$offset], "\x00\x09\x0a\x0c\x0d\x20") == 1) { + $offset++; + } + // get first char + $char = $data[$offset]; + // get object type + switch ($char) { + case '%': { // \x25 PERCENT SIGN + // skip comment and search for next token + $next = strcspn($data, "\r\n", $offset); + if ($next > 0) { + $offset += $next; + list($obj, $unused) = $this->getRawObject($offset, $data); + return $obj; + } + break; + } + case '/': { // \x2F SOLIDUS + // name object + $objtype = PDF_TYPE_TOKEN; + ++$offset; + $length = strcspn($data, "\x00\x09\x0a\x0c\x0d\x20\x28\x29\x3c\x3e\x5b\x5d\x7b\x7d\x2f\x25", $offset); + $objval = substr($data, $offset, $length); + $offset += $length; + break; + } + case '(': // \x28 LEFT PARENTHESIS + case ')': { // \x29 RIGHT PARENTHESIS + // literal string object + $objtype = PDF_TYPE_STRING; + ++$offset; + $strpos = $offset; + if ($char == '(') { + $open_bracket = 1; + while ($open_bracket > 0) { + if (!isset($data[$strpos])) { + break; + } + $ch = $data[$strpos]; + switch ($ch) { + case '\\': { // REVERSE SOLIDUS (5Ch) (Backslash) + // skip next character + ++$strpos; + break; + } + case '(': { // LEFT PARENHESIS (28h) + ++$open_bracket; + break; + } + case ')': { // RIGHT PARENTHESIS (29h) + --$open_bracket; + break; + } + } + ++$strpos; + } + $objval = substr($data, $offset, ($strpos - $offset - 1)); + $offset = $strpos; + } + break; + } + case '[': // \x5B LEFT SQUARE BRACKET + case ']': { // \x5D RIGHT SQUARE BRACKET + // array object + $objtype = PDF_TYPE_ARRAY; + ++$offset; + if ($char == '[') { + // get array content + $objval = array(); + do { + // get element + list($element, $offset) = $this->getRawObject($offset, $data); + $objval[] = $element; + } while ($element[0] !== ']'); + // remove closing delimiter + array_pop($objval); + } else { + $objtype = ']'; + } + break; + } + case '<': // \x3C LESS-THAN SIGN + case '>': { // \x3E GREATER-THAN SIGN + if (isset($data[($offset + 1)]) AND ($data[($offset + 1)] == $char)) { + // dictionary object + $objtype = PDF_TYPE_DICTIONARY; + if ($char == '<') { + list ($objval, $offset) = $this->getDictValue($offset, $data); + } else { + $objtype = '>>'; + $offset += 2; + } + } else { + // hexadecimal string object + $objtype = PDF_TYPE_HEX; + ++$offset; + // The "Panose" entry in the FontDescriptor Style dict seems to have hex bytes separated by spaces. + if (($char == '<') AND (preg_match('/^([0-9A-Fa-f ]+)[>]/iU', substr($data, $offset), $matches) == 1)) { + $objval = $matches[1]; + $offset += strlen($matches[0]); + unset($matches); + } + } + break; + } + default: { + $frag = $data[$offset] . @$data[$offset+1] . @$data[$offset+2] . @$data[$offset+3]; + switch ($frag) { + case 'endo': + // indirect object + $objtype = 'endobj'; + $offset += 6; + break; + case 'stre': + // Streams should always be indirect objects, and thus processed by getRawStream(). + // If we get here, treat it as a null object as something has gone wrong. + case 'null': + // null object + $objtype = PDF_TYPE_NULL; + $offset += 4; + $objval = 'null'; + break; + case 'true': + // boolean true object + $objtype = PDF_TYPE_BOOLEAN; + $offset += 4; + $objval = true; + break; + case 'fals': + // boolean false object + $objtype = PDF_TYPE_BOOLEAN; + $offset += 5; + $objval = false; + break; + case 'ends': + // end stream object + $objtype = 'endstream'; + $offset += 9; + break; + default: + if (preg_match('/^([0-9]+)[\s]+([0-9]+)[\s]+([Robj]{1,3})/i', substr($data, $offset, 33), $matches) == 1) { + if ($matches[3] == 'R') { + // indirect object reference + $objtype = PDF_TYPE_OBJREF; + $offset += strlen($matches[0]); + $objval = array(intval($matches[1]), intval($matches[2])); + } elseif ($matches[3] == 'obj') { + // object start + $objtype = PDF_TYPE_OBJECT; + $objval = intval($matches[1]).'_'.intval($matches[2]); + $offset += strlen ($matches[0]); + } + } elseif (($numlen = strspn($data, '+-.0123456789', $offset)) > 0) { + // numeric object + $objval = substr($data, $offset, $numlen); + $objtype = (intval($objval) != $objval) ? PDF_TYPE_REAL : PDF_TYPE_NUMERIC; + $offset += $numlen; + } + unset($matches); + break; + } + break; + } + } + $obj = array(); + $obj[] = $objtype; + if ($objtype == PDF_TYPE_OBJREF && is_array($objval)) { + foreach ($objval as $val) { + $obj[] = $val; + } + } else { + $obj[] = $objval; + } + return array($obj, $offset); + } + private function getDictValue($offset, &$data) { + $objval = array(); + + // Extract dict from data. + $i=1; + $dict = ''; + $offset += 2; + do { + if ($data[$offset] == '>' && $data[$offset+1] == '>') { + $i--; + $dict .= '>>'; + $offset += 2; + } else if ($data[$offset] == '<' && $data[$offset+1] == '<') { + $i++; + $dict .= '<<'; + $offset += 2; + } else { + $dict .= $data[$offset]; + $offset++; + } + } while ($i>0); + + // Now that we have just the dict, parse it. + $dictoffset = 0; + do { + // Get dict element. + list($key, $eloffset) = $this->getRawObject($dictoffset, $dict); + if ($key[0] == '>>') { + break; + } + list($element, $dictoffset) = $this->getRawObject($eloffset, $dict); + $objval['/'.$key[1]] = $element; + unset($key); + unset($element); + } while (true); + + return array($objval, $offset); + } + + /** + * Get content of indirect object. + * @param $obj_ref (string) Object number and generation number separated by underscore character. + * @param $offset (int) Object offset. + * @param $decoding (boolean) If true decode streams. + * @return array containing object data. + * @protected + * @since 1.0.000 (2011-05-24) + */ + protected function getIndirectObject($obj_ref, $offset=0, $decoding=true) { + $obj = explode('_', $obj_ref); + if (($obj === false) OR (count($obj) != 2)) { + $this->Error('Invalid object reference: '.$obj); + return; + } + $objref = $obj[0].' '.$obj[1].' obj'; + + if (strpos($this->pdfdata, $objref, $offset) != $offset) { + // an indirect reference to an undefined object shall be considered a reference to the null object + return array('null', 'null', $offset); + } + // starting position of object content + $offset += strlen($objref); + // get array of object content + $objdata = array(); + $i = 0; // object main index + do { + if (($i > 0) AND (isset($objdata[($i - 1)][0])) AND ($objdata[($i - 1)][0] == PDF_TYPE_DICTIONARY) AND array_key_exists('/Length', $objdata[($i - 1)][1])) { + // Stream - get using /Length in stream's dict + $lengthobj = $objdata[($i-1)][1]['/Length']; + if ($lengthobj[0] === PDF_TYPE_OBJREF) { + $lengthobj = $this->getObjectVal($lengthobj); + if ($lengthobj[0] === PDF_TYPE_OBJECT) { + $lengthobj = $lengthobj[1]; + } + } + $streamlength = $lengthobj[1]; + list($element, $offset) = $this->getRawStream($offset, $streamlength); + } else { + // get element + list($element, $offset) = $this->getRawObject($offset); + } + // decode stream using stream's dictionary information + if ($decoding AND ($element[0] == PDF_TYPE_STREAM) AND (isset($objdata[($i - 1)][0])) AND ($objdata[($i - 1)][0] == PDF_TYPE_DICTIONARY)) { + $element[3] = $this->decodeStream($objdata[($i - 1)][1], $element[1]); + } + $objdata[$i] = $element; + ++$i; + } while ($element[0] != 'endobj'); + // remove closing delimiter + array_pop($objdata); + // return raw object content + return $objdata; + } + + /** + * Get the content of object, resolving indect object reference if necessary. + * @param $obj (string) Object value. + * @return array containing object data. + * @public + * @since 1.0.000 (2011-06-26) + */ + public function getObjectVal($obj) { + if ($obj[0] == PDF_TYPE_OBJREF) { + if (strpos($obj[1], '_') !== false) { + $key = explode('_', $obj[1]); + } else { + $key = array($obj[1], $obj[2]); + } + + $ret = array(0=>PDF_TYPE_OBJECT, 'obj'=>$key[0], 'gen'=>$key[1]); + + // reference to indirect object + $object = null; + if (isset($this->objects[$key[0]][$key[1]])) { + // this object has been already parsed + $object = $this->objects[$key[0]][$key[1]]; + } elseif (($offset = $this->findObjectOffset($key)) !== false) { + // parse new object + $this->objects[$key[0]][$key[1]] = $this->getIndirectObject($key[0].'_'.$key[1], $offset, false); + $object = $this->objects[$key[0]][$key[1]]; + } elseif (($key[1] == 0) && isset($this->objstreamobjs[$key[0]])) { + // Object is in an object stream + $streaminfo = $this->objstreamobjs[$key[0]]; + $objs = $streaminfo[0]; + if (!isset($this->objstreams[$objs[0]][$objs[1]])) { + // Fetch and decode object stream + $offset = $this->findObjectOffset($objs);; + $objstream = $this->getObjectVal(array(PDF_TYPE_OBJREF, $objs[0], $objs[1])); + $decoded = $this->decodeStream($objstream[1][1], $objstream[2][1]); + $this->objstreams[$objs[0]][$objs[1]] = $decoded[0]; // Store just the data, in case we need more from this objstream + // Free memory + unset($objstream); + unset($decoded); + } + $this->objects[$key[0]][$key[1]] = $this->getRawObject($streaminfo[1], $this->objstreams[$objs[0]][$objs[1]]); + $object = $this->objects[$key[0]][$key[1]]; + } + if (!is_null($object)) { + $ret[1] = $object[0]; + if (isset($object[1][0]) && $object[1][0] == PDF_TYPE_STREAM) { + $ret[0] = PDF_TYPE_STREAM; + $ret[2] = $object[1]; + } + return $ret; + } + } + return $obj; + } + + /** + * Extract object stream to find out what it contains. + * + */ + function extractObjectStream($key) { + $objref = array(PDF_TYPE_OBJREF, $key[0], $key[1]); + $obj = $this->getObjectVal($objref); + if ($obj[0] !== PDF_TYPE_STREAM || !isset($obj[1][1]['/First'][1])) { + // Not a valid object stream dictionary - skip it. + return; + } + $stream = $this->decodeStream($obj[1][1], $obj[2][1]);// Decode object stream, as we need the first bit + $first = intval($obj[1][1]['/First'][1]); + $ints = preg_split('/\s/', substr($stream[0], 0, $first)); // Get list of object / offset pairs + for ($j=1; $j<count($ints); $j++) { + if (($j % 2) == 1) { + $this->objstreamobjs[$ints[$j-1]] = array($key, $ints[$j]+$first); + } + } + + // Free memory - we may not need this at all. + unset($obj); + unset($stream); + } + + /** + * Find all object offsets. Saves having to scour the file multiple times. + * @private + */ + private function findObjectOffsets() { + $this->objoffsets = array(); + if (preg_match_all('/(*ANYCRLF)^[\s]*([0-9]+)[\s]+([0-9]+)[\s]+obj/im', $this->pdfdata, $matches, PREG_OFFSET_CAPTURE) >= 1) { + $i = 0; + $laststreamend = 0; + foreach($matches[0] as $match) { + $offset = $match[1] + strspn($match[0], "\x00\x09\x0a\x0c\x0d\x20"); + if ($offset < $laststreamend) { + // Contained within another stream, skip it. + continue; + } + $this->objoffsets[trim($match[0])] = $offset; + $dictoffset = $match[1] + strlen($match[0]); + $dictfrag = substr($this->pdfdata, $dictoffset, 256); + if (preg_match('|^\s+<<[^>]+/Length\s+(\d+)|', $dictfrag, $lengthmatch, PREG_OFFSET_CAPTURE) == 1) { + $laststreamend += intval($lengthmatch[1][0]); + } + if (preg_match('|^\s+<<[^>]+/ObjStm|', $dictfrag, $objstm) == 1) { + $this->extractObjectStream(array($matches[1][$i][0], $matches[2][$i][0])); + } + $i++; + } + } + unset($lengthmatch); + unset($dictfrag); + unset($matches); + } + + /** + * Get offset of an object. Checks xref first, then offsets found by scouring the file. + * @param $key (array) Object key to find (obj, gen). + * @return int Offset of the object in $this->pdfdata. + * @private + */ + private function findObjectOffset($key) { + $objref = $key[0].' '.$key[1].' obj'; + if (isset($this->xref['xref'][$key[0]][$key[1]])) { + $offset = $this->xref['xref'][$key[0]][$key[1]]; + if (strpos($this->pdfdata, $objref, $offset) === $offset) { + // Offset is in xref table and matches actual position in file + //echo "Offset in XREF is correct, returning<br>"; + return $this->xref['xref'][$key[0]][$key[1]]; + } + } + if (array_key_exists($objref, $this->objoffsets)) { + //echo "Offset found in internal reftable<br>"; + return $this->objoffsets[$objref]; + } + return false; + } + + /** + * Decode the specified stream. + * @param $sdic (array) Stream's dictionary array. + * @param $stream (string) Stream to decode. + * @return array containing decoded stream data and remaining filters. + * @protected + * @since 1.0.000 (2011-06-22) + */ + protected function decodeStream($sdic, $stream) { + // get stream lenght and filters + $slength = strlen($stream); + if ($slength <= 0) { + return array('', array()); + } + $filters = array(); + foreach ($sdic as $k => $v) { + if ($v[0] == PDF_TYPE_TOKEN) { + if (($k == '/Length') AND ($v[0] == PDF_TYPE_NUMERIC)) { + // get declared stream lenght + $declength = intval($v[1]); + if ($declength < $slength) { + $stream = substr($stream, 0, $declength); + $slength = $declength; + } + } elseif ($k == '/Filter') { + if ($v[0] == PDF_TYPE_TOKEN) { + // single filter + $filters[] = $v[1]; + } elseif ($v[0] == PDF_TYPE_ARRAY) { + // array of filters + foreach ($v[1] as $flt) { + if ($flt[0] == PDF_TYPE_TOKEN) { + $filters[] = $flt[1]; + } + } + } + } + } + } + // decode the stream + $remaining_filters = array(); + foreach ($filters as $filter) { + if (in_array($filter, $this->FilterDecoders->getAvailableFilters())) { + $stream = $this->FilterDecoders->decodeFilter($filter, $stream); + } else { + // add missing filter to array + $remaining_filters[] = $filter; + } + } + return array($stream, $remaining_filters); + } + + + /** + * Set pageno + * + * @param int $pageno Pagenumber to use + */ + public function setPageno($pageno) { + $pageno = ((int) $pageno) - 1; + + if ($pageno < 0 || $pageno >= $this->getPageCount()) { + $this->error("Pagenumber is wrong! (Requested $pageno, max ".$this->getPageCount().")"); + } + + $this->pageno = $pageno; + } + + /** + * Get page-resources from current page + * + * @return array + */ + public function getPageResources() { + return $this->_getPageResources($this->pages[$this->pageno]); + } + + /** + * Get page-resources from /Page + * + * @param array $obj Array of pdf-data + */ + private function _getPageResources ($obj) { // $obj = /Page + $obj = $this->getObjectVal($obj); + + // If the current object has a resources + // dictionary associated with it, we use + // it. Otherwise, we move back to its + // parent object. + if (isset ($obj[1][1]['/Resources'])) { + $res = $obj[1][1]['/Resources']; + if ($res[0] == PDF_TYPE_OBJECT) + return $res[1]; + return $res; + } else { + if (!isset ($obj[1][1]['/Parent'])) { + return false; + } else { + $res = $this->_getPageResources($obj[1][1]['/Parent']); + if ($res[0] == PDF_TYPE_OBJECT) + return $res[1]; + return $res; + } + } + } + + /** + * Get annotations from current page + * + * @return array + */ + public function getPageAnnotations() { + return $this->_getPageAnnotations($this->pages[$this->pageno]); + } + + /** + * Get annotations from /Page + * + * @param array $obj Array of pdf-data + */ + private function _getPageAnnotations ($obj) { // $obj = /Page + $obj = $this->getObjectVal($obj); + + // If the current object has an annotations + // dictionary associated with it, we use + // it. Otherwise, we move back to its + // parent object. + if (isset ($obj[1][1]['/Annots'])) { + $annots = $obj[1][1]['/Annots']; + } else { + if (!isset ($obj[1][1]['/Parent'])) { + return false; + } else { + $annots = $this->_getPageAnnotations($obj[1][1]['/Parent']); + } + } + + if ($annots[0] == PDF_TYPE_OBJREF) + return $this->getObjectVal($annots); + return $annots; + } + + + /** + * Get content of current page + * + * If more /Contents is an array, the streams are concated + * + * @return string + */ + public function getContent() { + $buffer = ''; + + if (isset($this->pages[$this->pageno][1][1]['/Contents'])) { + $contents = $this->_getPageContent($this->pages[$this->pageno][1][1]['/Contents']); + foreach($contents AS $tmp_content) { + $buffer .= $this->_rebuildContentStream($tmp_content) . ' '; + } + } + + return $buffer; + } + + + /** + * Resolve all content-objects + * + * @param array $content_ref + * @return array + */ + private function _getPageContent($content_ref) { + $contents = array(); + + if ($content_ref[0] == PDF_TYPE_OBJREF) { + $content = $this->getObjectVal($content_ref); + if ($content[1][0] == PDF_TYPE_ARRAY) { + $contents = $this->_getPageContent($content[1]); + } else { + $contents[] = $content; + } + } elseif ($content_ref[0] == PDF_TYPE_ARRAY) { + foreach ($content_ref[1] AS $tmp_content_ref) { + $contents = array_merge($contents,$this->_getPageContent($tmp_content_ref)); + } + } + + return $contents; + } + + + /** + * Rebuild content-streams + * + * @param array $obj + * @return string + */ + private function _rebuildContentStream($obj) { + $filters = array(); + + if (isset($obj[1][1]['/Filter'])) { + $_filter = $obj[1][1]['/Filter']; + + if ($_filter[0] == PDF_TYPE_OBJREF) { + $tmpFilter = $this->getObjectVal($_filter); + $_filter = $tmpFilter[1]; + } + + if ($_filter[0] == PDF_TYPE_TOKEN) { + $filters[] = $_filter; + } elseif ($_filter[0] == PDF_TYPE_ARRAY) { + $filters = $_filter[1]; + } + } + + $stream = $obj[2][1]; + + foreach ($filters AS $_filter) { + $stream = $this->FilterDecoders->decodeFilter($_filter[1], $stream); + } + + return $stream; + } + + + /** + * Get a Box from a page + * Arrayformat is same as used by fpdf_tpl + * + * @param array $page a /Page + * @param string $box_index Type of Box @see $availableBoxes + * @param float Scale factor from user space units to points + * @return array + */ + public function getPageBox($page, $box_index, $k) { + $page = $this->getObjectVal($page); + $box = null; + if (isset($page[1][1][$box_index])) + $box =& $page[1][1][$box_index]; + + if (!is_null($box) && $box[0] == PDF_TYPE_OBJREF) { + $tmp_box = $this->getObjectVal($box); + $box = $tmp_box[1]; + } + + if (!is_null($box) && $box[0] == PDF_TYPE_ARRAY) { + $b =& $box[1]; + return array('x' => $b[0][1] / $k, + 'y' => $b[1][1] / $k, + 'w' => abs($b[0][1] - $b[2][1]) / $k, + 'h' => abs($b[1][1] - $b[3][1]) / $k, + 'llx' => min($b[0][1], $b[2][1]) / $k, + 'lly' => min($b[1][1], $b[3][1]) / $k, + 'urx' => max($b[0][1], $b[2][1]) / $k, + 'ury' => max($b[1][1], $b[3][1]) / $k, + ); + } elseif (!isset ($page[1][1]['/Parent'])) { + return false; + } else { + return $this->getPageBox($this->getObjectVal($page[1][1]['/Parent']), $box_index, $k); + } + } + + /** + * Get all page boxes by page no + * + * @param int The page number + * @param float Scale factor from user space units to points + * @return array + */ + public function getPageBoxes($pageno, $k) { + return $this->_getPageBoxes($this->pages[$pageno - 1], $k); + } + + /** + * Get all boxes from /Page + * + * @param array a /Page + * @return array + */ + private function _getPageBoxes($page, $k) { + $boxes = array(); + + foreach($this->availableBoxes AS $box) { + if ($_box = $this->getPageBox($page, $box, $k)) { + $boxes[$box] = $_box; + } + } + + return $boxes; + } + + /** + * Get the page rotation by pageno + * + * @param integer $pageno + * @return array + */ + public function getPageRotation($pageno) { + return $this->_getPageRotation($this->pages[$pageno - 1]); + } + + private function _getPageRotation($obj) { // $obj = /Page + $obj = $this->getObjectVal($obj); + if (isset ($obj[1][1]['/Rotate'])) { + $res = $this->getObjectVal($obj[1][1]['/Rotate']); + if ($res[0] == PDF_TYPE_OBJECT) + return $res[1]; + return $res; + } else { + if (!isset ($obj[1][1]['/Parent'])) { + return false; + } else { + $res = (array)$this->_getPageRotation($obj[1][1]['/Parent']); + if ($res[0] == PDF_TYPE_OBJECT) + return $res[1]; + return $res; + } + } + } + + /** + * This method is automatically called in case of fatal error; it simply outputs the message and halts the execution. + * @param $msg (string) The error message + * @public + * @since 1.0.000 (2011-05-23) + */ + public function Error($msg) { + // exit program and print error + die("<strong>TCPDI_PARSER ERROR [{$this->uniqueid}]: </strong>".$msg); + } + +} // END OF TCPDF_PARSER CLASS + +//============================================================+ +// END OF FILE +//============================================================+ diff --git a/vendor/psr/log/LICENSE b/vendor/psr/log/LICENSE new file mode 100644 index 0000000..474c952 --- /dev/null +++ b/vendor/psr/log/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2012 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/log/Psr/Log/AbstractLogger.php b/vendor/psr/log/Psr/Log/AbstractLogger.php new file mode 100644 index 0000000..e02f9da --- /dev/null +++ b/vendor/psr/log/Psr/Log/AbstractLogger.php @@ -0,0 +1,128 @@ +<?php + +namespace Psr\Log; + +/** + * This is a simple Logger implementation that other Loggers can inherit from. + * + * It simply delegates all log-level-specific methods to the `log` method to + * reduce boilerplate code that a simple Logger that does the same thing with + * messages regardless of the error level has to implement. + */ +abstract class AbstractLogger implements LoggerInterface +{ + /** + * System is unusable. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function emergency($message, array $context = array()) + { + $this->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 mixed[] $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 mixed[] $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 mixed[] $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 mixed[] $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param mixed[] $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 mixed[] $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } +} diff --git a/vendor/psr/log/Psr/Log/InvalidArgumentException.php b/vendor/psr/log/Psr/Log/InvalidArgumentException.php new file mode 100644 index 0000000..67f852d --- /dev/null +++ b/vendor/psr/log/Psr/Log/InvalidArgumentException.php @@ -0,0 +1,7 @@ +<?php + +namespace Psr\Log; + +class InvalidArgumentException extends \InvalidArgumentException +{ +} diff --git a/vendor/psr/log/Psr/Log/LogLevel.php b/vendor/psr/log/Psr/Log/LogLevel.php new file mode 100644 index 0000000..9cebcac --- /dev/null +++ b/vendor/psr/log/Psr/Log/LogLevel.php @@ -0,0 +1,18 @@ +<?php + +namespace Psr\Log; + +/** + * Describes log levels. + */ +class LogLevel +{ + const EMERGENCY = 'emergency'; + const ALERT = 'alert'; + const CRITICAL = 'critical'; + const ERROR = 'error'; + const WARNING = 'warning'; + const NOTICE = 'notice'; + const INFO = 'info'; + const DEBUG = 'debug'; +} diff --git a/vendor/psr/log/Psr/Log/LoggerAwareInterface.php b/vendor/psr/log/Psr/Log/LoggerAwareInterface.php new file mode 100644 index 0000000..4d64f47 --- /dev/null +++ b/vendor/psr/log/Psr/Log/LoggerAwareInterface.php @@ -0,0 +1,18 @@ +<?php + +namespace Psr\Log; + +/** + * Describes a logger-aware instance. + */ +interface LoggerAwareInterface +{ + /** + * Sets a logger instance on the object. + * + * @param LoggerInterface $logger + * + * @return void + */ + public function setLogger(LoggerInterface $logger); +} diff --git a/vendor/psr/log/Psr/Log/LoggerAwareTrait.php b/vendor/psr/log/Psr/Log/LoggerAwareTrait.php new file mode 100644 index 0000000..82bf45c --- /dev/null +++ b/vendor/psr/log/Psr/Log/LoggerAwareTrait.php @@ -0,0 +1,26 @@ +<?php + +namespace Psr\Log; + +/** + * Basic Implementation of LoggerAwareInterface. + */ +trait LoggerAwareTrait +{ + /** + * The logger instance. + * + * @var LoggerInterface|null + */ + protected $logger; + + /** + * Sets a logger. + * + * @param LoggerInterface $logger + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } +} diff --git a/vendor/psr/log/Psr/Log/LoggerInterface.php b/vendor/psr/log/Psr/Log/LoggerInterface.php new file mode 100644 index 0000000..2206cfd --- /dev/null +++ b/vendor/psr/log/Psr/Log/LoggerInterface.php @@ -0,0 +1,125 @@ +<?php + +namespace Psr\Log; + +/** + * Describes a logger instance. + * + * The message MUST be a string or object implementing __toString(). + * + * The message MAY contain placeholders in the form: {foo} where foo + * will be replaced by the context data in key "foo". + * + * The context array can contain arbitrary data. The only assumption that + * can be made by implementors is that if an Exception instance is given + * to produce a stack trace, it MUST be in a key named "exception". + * + * See https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md + * for the full interface specification. + */ +interface LoggerInterface +{ + /** + * System is unusable. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function emergency($message, array $context = array()); + + /** + * 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 mixed[] $context + * + * @return void + */ + public function alert($message, array $context = array()); + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + 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 mixed[] $context + * + * @return void + */ + public function error($message, array $context = array()); + + /** + * 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 mixed[] $context + * + * @return void + */ + public function warning($message, array $context = array()); + + /** + * Normal but significant events. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function notice($message, array $context = array()); + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function info($message, array $context = array()); + + /** + * Detailed debug information. + * + * @param string $message + * @param mixed[] $context + * + * @return void + */ + public function debug($message, array $context = array()); + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param mixed[] $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, $message, array $context = array()); +} diff --git a/vendor/psr/log/Psr/Log/LoggerTrait.php b/vendor/psr/log/Psr/Log/LoggerTrait.php new file mode 100644 index 0000000..e392fef --- /dev/null +++ b/vendor/psr/log/Psr/Log/LoggerTrait.php @@ -0,0 +1,142 @@ +<?php + +namespace Psr\Log; + +/** + * This is a simple Logger trait that classes unable to extend AbstractLogger + * (because they extend another class, etc) can include. + * + * It simply delegates all log-level-specific methods to the `log` method to + * reduce boilerplate code that a simple Logger that does the same thing with + * messages regardless of the error level has to implement. + */ +trait LoggerTrait +{ + /** + * System is unusable. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function emergency($message, array $context = array()) + { + $this->log(LogLevel::EMERGENCY, $message, $context); + } + + /** + * Action must be taken immediately. + * + * Example: Entire website down, database unavailable, etc. This should + * trigger the SMS alerts and wake you up. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function alert($message, array $context = array()) + { + $this->log(LogLevel::ALERT, $message, $context); + } + + /** + * Critical conditions. + * + * Example: Application component unavailable, unexpected exception. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function critical($message, array $context = array()) + { + $this->log(LogLevel::CRITICAL, $message, $context); + } + + /** + * Runtime errors that do not require immediate action but should typically + * be logged and monitored. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function error($message, array $context = array()) + { + $this->log(LogLevel::ERROR, $message, $context); + } + + /** + * Exceptional occurrences that are not errors. + * + * Example: Use of deprecated APIs, poor use of an API, undesirable things + * that are not necessarily wrong. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function warning($message, array $context = array()) + { + $this->log(LogLevel::WARNING, $message, $context); + } + + /** + * Normal but significant events. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function notice($message, array $context = array()) + { + $this->log(LogLevel::NOTICE, $message, $context); + } + + /** + * Interesting events. + * + * Example: User logs in, SQL logs. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function info($message, array $context = array()) + { + $this->log(LogLevel::INFO, $message, $context); + } + + /** + * Detailed debug information. + * + * @param string $message + * @param array $context + * + * @return void + */ + public function debug($message, array $context = array()) + { + $this->log(LogLevel::DEBUG, $message, $context); + } + + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + abstract public function log($level, $message, array $context = array()); +} diff --git a/vendor/psr/log/Psr/Log/NullLogger.php b/vendor/psr/log/Psr/Log/NullLogger.php new file mode 100644 index 0000000..c8f7293 --- /dev/null +++ b/vendor/psr/log/Psr/Log/NullLogger.php @@ -0,0 +1,30 @@ +<?php + +namespace Psr\Log; + +/** + * This Logger can be used to avoid conditional log calls. + * + * Logging should always be optional, and if no logger is provided to your + * library creating a NullLogger instance to have something to throw logs at + * is a good way to avoid littering your code with `if ($this->logger) { }` + * blocks. + */ +class NullLogger extends AbstractLogger +{ + /** + * Logs with an arbitrary level. + * + * @param mixed $level + * @param string $message + * @param array $context + * + * @return void + * + * @throws \Psr\Log\InvalidArgumentException + */ + public function log($level, $message, array $context = array()) + { + // noop + } +} diff --git a/vendor/psr/log/Psr/Log/Test/DummyTest.php b/vendor/psr/log/Psr/Log/Test/DummyTest.php new file mode 100644 index 0000000..9638c11 --- /dev/null +++ b/vendor/psr/log/Psr/Log/Test/DummyTest.php @@ -0,0 +1,18 @@ +<?php + +namespace Psr\Log\Test; + +/** + * This class is internal and does not follow the BC promise. + * + * Do NOT use this class in any way. + * + * @internal + */ +class DummyTest +{ + public function __toString() + { + return 'DummyTest'; + } +} diff --git a/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php b/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php new file mode 100644 index 0000000..e1e5354 --- /dev/null +++ b/vendor/psr/log/Psr/Log/Test/LoggerInterfaceTest.php @@ -0,0 +1,138 @@ +<?php + +namespace Psr\Log\Test; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use PHPUnit\Framework\TestCase; + +/** + * Provides a base test class for ensuring compliance with the LoggerInterface. + * + * Implementors can extend the class and implement abstract methods to run this + * as part of their test suite. + */ +abstract class LoggerInterfaceTest extends TestCase +{ + /** + * @return LoggerInterface + */ + abstract public function getLogger(); + + /** + * This must return the log messages in order. + * + * The simple formatting of the messages is: "<LOG LEVEL> <MESSAGE>". + * + * 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 new file mode 100644 index 0000000..1be3230 --- /dev/null +++ b/vendor/psr/log/Psr/Log/Test/TestLogger.php @@ -0,0 +1,147 @@ +<?php + +namespace Psr\Log\Test; + +use Psr\Log\AbstractLogger; + +/** + * Used for testing purposes. + * + * It records all records and gives you access to them for verification. + * + * @method bool hasEmergency($record) + * @method bool hasAlert($record) + * @method bool hasCritical($record) + * @method bool hasError($record) + * @method bool hasWarning($record) + * @method bool hasNotice($record) + * @method bool hasInfo($record) + * @method bool hasDebug($record) + * + * @method bool hasEmergencyRecords() + * @method bool hasAlertRecords() + * @method bool hasCriticalRecords() + * @method bool hasErrorRecords() + * @method bool hasWarningRecords() + * @method bool hasNoticeRecords() + * @method bool hasInfoRecords() + * @method bool hasDebugRecords() + * + * @method bool hasEmergencyThatContains($message) + * @method bool hasAlertThatContains($message) + * @method bool hasCriticalThatContains($message) + * @method bool hasErrorThatContains($message) + * @method bool hasWarningThatContains($message) + * @method bool hasNoticeThatContains($message) + * @method bool hasInfoThatContains($message) + * @method bool hasDebugThatContains($message) + * + * @method bool hasEmergencyThatMatches($message) + * @method bool hasAlertThatMatches($message) + * @method bool hasCriticalThatMatches($message) + * @method bool hasErrorThatMatches($message) + * @method bool hasWarningThatMatches($message) + * @method bool hasNoticeThatMatches($message) + * @method bool hasInfoThatMatches($message) + * @method bool hasDebugThatMatches($message) + * + * @method bool hasEmergencyThatPasses($message) + * @method bool hasAlertThatPasses($message) + * @method bool hasCriticalThatPasses($message) + * @method bool hasErrorThatPasses($message) + * @method bool hasWarningThatPasses($message) + * @method bool hasNoticeThatPasses($message) + * @method bool hasInfoThatPasses($message) + * @method bool hasDebugThatPasses($message) + */ +class TestLogger extends AbstractLogger +{ + /** + * @var array + */ + public $records = []; + + public $recordsByLevel = []; + + /** + * @inheritdoc + */ + public function log($level, $message, array $context = []) + { + $record = [ + 'level' => $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/README.md b/vendor/psr/log/README.md new file mode 100644 index 0000000..a9f20c4 --- /dev/null +++ b/vendor/psr/log/README.md @@ -0,0 +1,58 @@ +PSR Log +======= + +This repository holds all interfaces/classes/traits related to +[PSR-3](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md). + +Note that this is not a logger of its own. It is merely an interface that +describes a logger. See the specification for more details. + +Installation +------------ + +```bash +composer require psr/log +``` + +Usage +----- + +If you need a logger, you can use the interface like this: + +```php +<?php + +use Psr\Log\LoggerInterface; + +class Foo +{ + private $logger; + + public function __construct(LoggerInterface $logger = null) + { + $this->logger = $logger; + } + + public function doSomething() + { + if ($this->logger) { + $this->logger->info('Doing work'); + } + + try { + $this->doSomethingElse(); + } catch (Exception $exception) { + $this->logger->error('Oh no!', array('exception' => $exception)); + } + + // do something useful + } +} +``` + +You can then pick one of the implementations of the interface to get a logger. + +If you want to implement the interface, you can require this package and +implement `Psr\Log\LoggerInterface` in your code. Please read the +[specification text](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-3-logger-interface.md) +for details. diff --git a/vendor/psr/log/composer.json b/vendor/psr/log/composer.json new file mode 100644 index 0000000..ca05695 --- /dev/null +++ b/vendor/psr/log/composer.json @@ -0,0 +1,26 @@ +{ + "name": "psr/log", + "description": "Common interface for logging libraries", + "keywords": ["psr", "psr-3", "log"], + "homepage": "https://github.com/php-fig/log", + "license": "MIT", + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "require": { + "php": ">=5.3.0" + }, + "autoload": { + "psr-4": { + "Psr\\Log\\": "Psr/Log/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + } +} diff --git a/vendor/setasign/fpdi/LICENSE.txt b/vendor/setasign/fpdi/LICENSE.txt new file mode 100644 index 0000000..45672fa --- /dev/null +++ b/vendor/setasign/fpdi/LICENSE.txt @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 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 +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.
\ No newline at end of file diff --git a/vendor/setasign/fpdi/README.md b/vendor/setasign/fpdi/README.md new file mode 100644 index 0000000..c503b7d --- /dev/null +++ b/vendor/setasign/fpdi/README.md @@ -0,0 +1,131 @@ +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) + +:heavy_exclamation_mark: This document refers to FPDI 2. Version 1 is deprecated and development is discontinued. :heavy_exclamation_mark: + +FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF +documents and use them as templates in [FPDF](http://www.fpdf.org), which was developed by Olivier Plathey. Apart +from a copy of [FPDF](http://www.fpdf.org), FPDI does not require any special PHP extensions. + +FPDI can also be used as an extension for [TCPDF](https://github.com/tecnickcom/TCPDF) or +[tFPDF](http://fpdf.org/en/script/script92.php), too. + +## Installation with [Composer](https://packagist.org/packages/setasign/fpdi) + +Because FPDI can be used with FPDF, TCPDF or tFPDF we haven't added a fixed dependency in the main +composer.json file. You need to add the dependency to the PDF generation library of your choice +yourself. + +To use FPDI with FPDF include following in your composer.json file: + +```json +{ + "require": { + "setasign/fpdf": "1.8.*", + "setasign/fpdi": "^2.0" + } +} +``` + +If you want to use TCPDF, you have to update your composer.json to: + +```json +{ + "require": { + "tecnickcom/tcpdf": "6.3.*", + "setasign/fpdi": "^2.0" + } +} +``` + +If you want to use tFPDF, you have to update your composer.json to: + +```json +{ + "require": { + "setasign/tfpdf": "1.31.*", + "setasign/fpdi": "^2.3" + } +} +``` + +## Manual Installation + +If you do not use composer, just require the autoload.php in the /src folder: + +```php +require_once('src/autoload.php'); +``` + +If you have a PSR-4 autoloader implemented, just register the src path as follows: +```php +$loader = new \Example\Psr4AutoloaderClass; +$loader->register(); +$loader->addNamespace('setasign\Fpdi', 'path/to/src/'); +``` + +## Changes to Version 1 + +Version 2 is a complete rewrite from scratch of FPDI which comes with: +- Namespaced code +- Clean and up-to-date code base and style +- PSR-4 compatible autoloading +- Performance improvements by up to 100% +- Less memory consumption +- Native support for reading PDFs from strings or stream-resources +- Support for documents with "invalid" data before their file-header +- Optimized page tree resolving +- Usage of individual exceptions +- Several test types (unit, functional and visual tests) + +We tried to keep the main methods and logical workflow the same as in version 1 but please +notice that there were incompatible changes which you should consider when updating to +version 2: +- You need to load the code using the `src/autoload.php` file instead of `classes/FPDI.php`. +- The classes and traits are namespaced now: `setasign\Fpdi` +- Page boundaries beginning with a slash, such as `/MediaBox`, are not supported anymore. Remove + the slash or use a constant of `PdfReader\PageBoundaries`. +- The parameters $x, $y, $width and $height of the `useTemplate()` or `getTemplateSize()` + method have more logical correct default values now. Passing `0` as width or height will + result in an `InvalidArgumentException` now. +- The return value of `getTemplateSize()` had changed to an array with more speaking keys + and reusability: Use `width` instead of `w` and `height` instead of `h`. +- If you want to use **FPDI with TCPDF** you need to refactor your code to use the class `Tcpdf\Fpdi` +(since 2.1; before it was `TcpdfFpdi`) instead of `FPDI`. + +## Example and Documentation + +A simple example, that imports a single page and places this onto a new created page: + +```php +<?php +use setasign\Fpdi\Fpdi; +// or for usage with TCPDF: +// use setasign\Fpdi\Tcpdf\Fpdi; + +// or for usage with tFPDF: +// use setasign\Fpdi\Tfpdf\Fpdi; + +// setup the autoload function +require_once('vendor/autoload.php'); + +// initiate FPDI +$pdf = new Fpdi(); +// add a page +$pdf->AddPage(); +// set the source file +$pdf->setSourceFile("Fantastic-Speaker.pdf"); +// import page 1 +$tplId = $pdf->importPage(1); +// use the imported page and place it at point 10,10 with a width of 100 mm +$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/). diff --git a/vendor/setasign/fpdi/SECURITY.md b/vendor/setasign/fpdi/SECURITY.md new file mode 100644 index 0000000..da9c516 --- /dev/null +++ b/vendor/setasign/fpdi/SECURITY.md @@ -0,0 +1,5 @@ +## Security contact information + +To report a security vulnerability, please use the +[Tidelift security contact](https://tidelift.com/security). +Tidelift will coordinate the fix and disclosure. diff --git a/vendor/setasign/fpdi/composer.json b/vendor/setasign/fpdi/composer.json new file mode 100644 index 0000000..da67012 --- /dev/null +++ b/vendor/setasign/fpdi/composer.json @@ -0,0 +1,51 @@ +{ + "name": "setasign/fpdi", + "homepage": "https://www.setasign.com/fpdi", + "description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.", + "type": "library", + "keywords": [ + "pdf", + "fpdi", + "fpdf" + ], + "license": "MIT", + "autoload": { + "psr-4": { + "setasign\\Fpdi\\": "src/" + } + }, + "require": { + "php": "^5.6 || ^7.0 || ^8.0", + "ext-zlib": "*" + }, + "conflict": { + "setasign/tfpdf": "<1.31" + }, + "authors": [ + { + "name": "Jan Slabon", + "email": "jan.slabon@setasign.com", + "homepage": "https://www.setasign.com" + }, + { + "name": "Maximilian Kresse", + "email": "maximilian.kresse@setasign.com", + "homepage": "https://www.setasign.com" + } + ], + "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." + }, + "require-dev": { + "phpunit/phpunit": "~5.7", + "setasign/fpdf": "~1.8", + "tecnickcom/tcpdf": "~6.2", + "setasign/tfpdf": "1.31", + "squizlabs/php_codesniffer": "^3.5" + }, + "autoload-dev": { + "psr-4": { + "setasign\\Fpdi\\": "tests/" + } + } +} diff --git a/vendor/setasign/fpdi/src/FpdfTpl.php b/vendor/setasign/fpdi/src/FpdfTpl.php new file mode 100644 index 0000000..4b93f53 --- /dev/null +++ b/vendor/setasign/fpdi/src/FpdfTpl.php @@ -0,0 +1,21 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi; + +/** + * Class FpdfTpl + * + * This class adds a templating feature to FPDF. + */ +class FpdfTpl extends \FPDF +{ + use FpdfTplTrait; +} diff --git a/vendor/setasign/fpdi/src/FpdfTplTrait.php b/vendor/setasign/fpdi/src/FpdfTplTrait.php new file mode 100644 index 0000000..d2da97c --- /dev/null +++ b/vendor/setasign/fpdi/src/FpdfTplTrait.php @@ -0,0 +1,470 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi; + +/** + * Trait FpdfTplTrait + * + * This class adds a templating feature to tFPDF. + */ +trait FpdfTplTrait +{ + /** + * Data of all created templates. + * + * @var array + */ + protected $templates = []; + + /** + * The template id for the currently created template. + * + * @var null|int + */ + protected $currentTemplateId; + + /** + * A counter for template ids. + * + * @var int + */ + protected $templateId = 0; + + /** + * Set the page format of the current page. + * + * @param array $size An array with two values defining the size. + * @param string $orientation "L" for landscape, "P" for portrait. + * @throws \BadMethodCallException + */ + public function setPageFormat($size, $orientation) + { + if ($this->currentTemplateId !== null) { + throw new \BadMethodCallException('The page format cannot be changed when writing to a template.'); + } + + if (!\in_array($orientation, ['P', 'L'], true)) { + throw new \InvalidArgumentException(\sprintf( + 'Invalid page orientation "%s"! Only "P" and "L" are allowed!', + $orientation + )); + } + + $size = $this->_getpagesize($size); + + if ( + $orientation != $this->CurOrientation + || $size[0] != $this->CurPageSize[0] + || $size[1] != $this->CurPageSize[1] + ) { + // New size or orientation + if ($orientation === 'P') { + $this->w = $size[0]; + $this->h = $size[1]; + } else { + $this->w = $size[1]; + $this->h = $size[0]; + } + $this->wPt = $this->w * $this->k; + $this->hPt = $this->h * $this->k; + $this->PageBreakTrigger = $this->h - $this->bMargin; + $this->CurOrientation = $orientation; + $this->CurPageSize = $size; + + $this->PageInfo[$this->page]['size'] = array($this->wPt, $this->hPt); + } + } + + /** + * Draws 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 array|float|int $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 FpdfTplTrait::getTemplateSize() + */ + public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) + { + if (!isset($this->templates[$tpl])) { + throw new \InvalidArgumentException('Template does not exist!'); + } + + if (\is_array($x)) { + unset($x['tpl']); + \extract($x, EXTR_IF_EXISTS); + /** @noinspection NotOptimalIfConditionsInspection */ + /** @noinspection PhpConditionAlreadyCheckedInspection */ + if (\is_array($x)) { + $x = 0; + } + } + + $template = $this->templates[$tpl]; + + $originalSize = $this->getTemplateSize($tpl); + $newSize = $this->getTemplateSize($tpl, $width, $height); + if ($adjustPageSize) { + $this->setPageFormat($newSize, $newSize['orientation']); + } + + $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, + $template['id'] + ) + ); + + return $newSize; + } + + /** + * Get the size of a 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) + { + if (!isset($this->templates[$tpl])) { + return false; + } + + if ($width === null && $height === null) { + $width = $this->templates[$tpl]['width']; + $height = $this->templates[$tpl]['height']; + } elseif ($width === null) { + $width = $height * $this->templates[$tpl]['width'] / $this->templates[$tpl]['height']; + } + + if ($height === null) { + $height = $width * $this->templates[$tpl]['height'] / $this->templates[$tpl]['width']; + } + + if ($height <= 0. || $width <= 0.) { + throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.'); + } + + return [ + 'width' => $width, + 'height' => $height, + 0 => $width, + 1 => $height, + 'orientation' => $width > $height ? 'L' : 'P' + ]; + } + + /** + * Begins a new template. + * + * @param float|int|null $width The width of the template. If null, the current page width is used. + * @param float|int|null $height The height of the template. If null, the current page height is used. + * @param bool $groupXObject Define the form XObject as a group XObject to support transparency (if used). + * @return int A template identifier. + */ + public function beginTemplate($width = null, $height = null, $groupXObject = false) + { + if ($width === null) { + $width = $this->w; + } + + if ($height === null) { + $height = $this->h; + } + + $templateId = $this->getNextTemplateId(); + + // initiate buffer with current state of FPDF + $buffer = "2 J\n" + . \sprintf('%.2F w', $this->LineWidth * $this->k) . "\n"; + + if ($this->FontFamily) { + $buffer .= \sprintf("BT /F%d %.2F Tf ET\n", $this->CurrentFont['i'], $this->FontSizePt); + } + + if ($this->DrawColor !== '0 G') { + $buffer .= $this->DrawColor . "\n"; + } + if ($this->FillColor !== '0 g') { + $buffer .= $this->FillColor . "\n"; + } + + if ($groupXObject && \version_compare('1.4', $this->PDFVersion, '>')) { + $this->PDFVersion = '1.4'; + } + + $this->templates[$templateId] = [ + 'objectNumber' => null, + 'id' => 'TPL' . $templateId, + 'buffer' => $buffer, + 'width' => $width, + 'height' => $height, + 'groupXObject' => $groupXObject, + 'state' => [ + 'x' => $this->x, + 'y' => $this->y, + 'AutoPageBreak' => $this->AutoPageBreak, + 'bMargin' => $this->bMargin, + 'tMargin' => $this->tMargin, + 'lMargin' => $this->lMargin, + 'rMargin' => $this->rMargin, + 'h' => $this->h, + 'w' => $this->w, + 'FontFamily' => $this->FontFamily, + 'FontStyle' => $this->FontStyle, + 'FontSizePt' => $this->FontSizePt, + 'FontSize' => $this->FontSize, + 'underline' => $this->underline, + 'TextColor' => $this->TextColor, + 'DrawColor' => $this->DrawColor, + 'FillColor' => $this->FillColor, + 'ColorFlag' => $this->ColorFlag + ] + ]; + + $this->SetAutoPageBreak(false); + $this->currentTemplateId = $templateId; + + $this->h = $height; + $this->w = $width; + + $this->SetXY($this->lMargin, $this->tMargin); + $this->SetRightMargin($this->w - $width + $this->rMargin); + + return $templateId; + } + + /** + * Ends a template. + * + * @return bool|int|null A template identifier. + */ + public function endTemplate() + { + if ($this->currentTemplateId === null) { + return false; + } + + $templateId = $this->currentTemplateId; + $template = $this->templates[$templateId]; + + $state = $template['state']; + $this->SetXY($state['x'], $state['y']); + $this->tMargin = $state['tMargin']; + $this->lMargin = $state['lMargin']; + $this->rMargin = $state['rMargin']; + $this->h = $state['h']; + $this->w = $state['w']; + $this->SetAutoPageBreak($state['AutoPageBreak'], $state['bMargin']); + + $this->FontFamily = $state['FontFamily']; + $this->FontStyle = $state['FontStyle']; + $this->FontSizePt = $state['FontSizePt']; + $this->FontSize = $state['FontSize']; + + $this->TextColor = $state['TextColor']; + $this->DrawColor = $state['DrawColor']; + $this->FillColor = $state['FillColor']; + $this->ColorFlag = $state['ColorFlag']; + + $this->underline = $state['underline']; + + $fontKey = $this->FontFamily . $this->FontStyle; + if ($fontKey) { + $this->CurrentFont =& $this->fonts[$fontKey]; + } else { + unset($this->CurrentFont); + } + + $this->currentTemplateId = null; + + return $templateId; + } + + /** + * Get the next template id. + * + * @return int + */ + protected function getNextTemplateId() + { + return $this->templateId++; + } + + /* overwritten FPDF methods: */ + + /** + * @inheritdoc + */ + public function AddPage($orientation = '', $size = '', $rotation = 0) + { + if ($this->currentTemplateId !== null) { + throw new \BadMethodCallException('Pages cannot be added when writing to a template.'); + } + parent::AddPage($orientation, $size, $rotation); + } + + /** + * @inheritdoc + */ + public function Link($x, $y, $w, $h, $link) + { + if ($this->currentTemplateId !== null) { + throw new \BadMethodCallException('Links cannot be set when writing to a template.'); + } + parent::Link($x, $y, $w, $h, $link); + } + + /** + * @inheritdoc + */ + public function SetLink($link, $y = 0, $page = -1) + { + if ($this->currentTemplateId !== null) { + throw new \BadMethodCallException('Links cannot be set when writing to a template.'); + } + return parent::SetLink($link, $y, $page); + } + + /** + * @inheritdoc + */ + public function SetDrawColor($r, $g = null, $b = null) + { + parent::SetDrawColor($r, $g, $b); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out($this->DrawColor); + } + } + + /** + * @inheritdoc + */ + public function SetFillColor($r, $g = null, $b = null) + { + parent::SetFillColor($r, $g, $b); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out($this->FillColor); + } + } + + /** + * @inheritdoc + */ + public function SetLineWidth($width) + { + parent::SetLineWidth($width); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out(\sprintf('%.2F w', $width * $this->k)); + } + } + + /** + * @inheritdoc + */ + public function SetFont($family, $style = '', $size = 0) + { + parent::SetFont($family, $style, $size); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out(\sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + } + + /** + * @inheritdoc + */ + public function SetFontSize($size) + { + parent::SetFontSize($size); + if ($this->page === 0 && $this->currentTemplateId !== null) { + $this->_out(sprintf('BT /F%d %.2F Tf ET', $this->CurrentFont['i'], $this->FontSizePt)); + } + } + + /** + * @inheritdoc + */ + protected function _putimages() + { + parent::_putimages(); + + foreach ($this->templates as $key => $template) { + $this->_newobj(); + $this->templates[$key]['objectNumber'] = $this->n; + + $this->_put('<</Type /XObject /Subtype /Form /FormType 1'); + $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) { + $buffer = \gzcompress($template['buffer']); + $this->_put('/Filter/FlateDecode'); + } else { + $buffer = $template['buffer']; + } + + $this->_put('/Length ' . \strlen($buffer)); + + if ($template['groupXObject']) { + $this->_put('/Group <</Type/Group/S/Transparency>>'); + } + + $this->_put('>>'); + $this->_putstream($buffer); + $this->_put('endobj'); + } + } + + /** + * @inheritdoc + */ + protected function _putxobjectdict() + { + foreach ($this->templates as $key => $template) { + $this->_put('/' . $template['id'] . ' ' . $template['objectNumber'] . ' 0 R'); + } + + parent::_putxobjectdict(); + } + + /** + * @inheritdoc + */ + public function _out($s) + { + if ($this->currentTemplateId !== null) { + $this->templates[$this->currentTemplateId]['buffer'] .= $s . "\n"; + } else { + parent::_out($s); + } + } +} diff --git a/vendor/setasign/fpdi/src/Fpdi.php b/vendor/setasign/fpdi/src/Fpdi.php new file mode 100644 index 0000000..157f87c --- /dev/null +++ b/vendor/setasign/fpdi/src/Fpdi.php @@ -0,0 +1,153 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi; + +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; +use setasign\Fpdi\PdfParser\PdfParserException; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; +use setasign\Fpdi\PdfParser\Type\PdfNull; + +/** + * Class Fpdi + * + * This class let you import pages of existing PDF documents into a reusable structure for FPDF. + */ +class Fpdi extends FpdfTpl +{ + use FpdiTrait; + + /** + * FPDI version + * + * @string + */ + const VERSION = '2.3.6'; + + protected function _enddoc() + { + parent::_enddoc(); + $this->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; + } + } +} diff --git a/vendor/setasign/fpdi/src/FpdiException.php b/vendor/setasign/fpdi/src/FpdiException.php new file mode 100644 index 0000000..2286667 --- /dev/null +++ b/vendor/setasign/fpdi/src/FpdiException.php @@ -0,0 +1,18 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi; + +/** + * Base exception class for the FPDI package. + */ +class FpdiException extends \Exception +{ +} diff --git a/vendor/setasign/fpdi/src/FpdiTrait.php b/vendor/setasign/fpdi/src/FpdiTrait.php new file mode 100644 index 0000000..3b29857 --- /dev/null +++ b/vendor/setasign/fpdi/src/FpdiTrait.php @@ -0,0 +1,559 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi; + +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; +use setasign\Fpdi\PdfParser\Filter\FilterException; +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\PdfParserException; +use setasign\Fpdi\PdfParser\StreamReader; +use setasign\Fpdi\PdfParser\Type\PdfArray; +use setasign\Fpdi\PdfParser\Type\PdfBoolean; +use setasign\Fpdi\PdfParser\Type\PdfDictionary; +use setasign\Fpdi\PdfParser\Type\PdfHexString; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference; +use setasign\Fpdi\PdfParser\Type\PdfName; +use setasign\Fpdi\PdfParser\Type\PdfNull; +use setasign\Fpdi\PdfParser\Type\PdfNumeric; +use setasign\Fpdi\PdfParser\Type\PdfStream; +use setasign\Fpdi\PdfParser\Type\PdfString; +use setasign\Fpdi\PdfParser\Type\PdfToken; +use setasign\Fpdi\PdfParser\Type\PdfType; +use setasign\Fpdi\PdfParser\Type\PdfTypeException; +use setasign\Fpdi\PdfReader\PageBoundaries; +use setasign\Fpdi\PdfReader\PdfReader; +use setasign\Fpdi\PdfReader\PdfReaderException; +use /* This namespace/class is used by the commercial FPDI PDF-Parser add-on. */ + /** @noinspection PhpUndefinedClassInspection */ + /** @noinspection PhpUndefinedNamespaceInspection */ + setasign\FpdiPdfParser\PdfParser\PdfParser as FpdiPdfParser; + +/** + * The FpdiTrait + * + * This trait offers the core functionalities of FPDI. By passing them to a trait we can reuse it with e.g. TCPDF in a + * very easy way. + */ +trait FpdiTrait +{ + /** + * The pdf reader instances. + * + * @var PdfReader[] + */ + protected $readers = []; + + /** + * Instances created internally. + * + * @var array + */ + protected $createdReaders = []; + + /** + * The current reader id. + * + * @var string|null + */ + protected $currentReaderId; + + /** + * Data of all imported pages. + * + * @var array + */ + protected $importedPages = []; + + /** + * A map from object numbers of imported objects to new assigned object numbers by FPDF. + * + * @var array + */ + protected $objectMap = []; + + /** + * An array with information about objects, which needs to be copied to the resulting document. + * + * @var array + */ + protected $objectsToCopy = []; + + /** + * Release resources and file handles. + * + * This method is called internally when the document is created successfully. By default it only cleans up + * stream reader instances which were created internally. + * + * @param bool $allReaders + */ + public function cleanUp($allReaders = false) + { + $readers = $allReaders ? array_keys($this->readers) : $this->createdReaders; + foreach ($readers as $id) { + $this->readers[$id]->getParser()->getStreamReader()->cleanUp(); + unset($this->readers[$id]); + } + + $this->createdReaders = []; + } + + /** + * Set the minimal PDF version. + * + * @param string $pdfVersion + */ + protected function setMinPdfVersion($pdfVersion) + { + if (\version_compare($pdfVersion, $this->PDFVersion, '>')) { + $this->PDFVersion = $pdfVersion; + } + } + + /** @noinspection PhpUndefinedClassInspection */ + /** + * Get a new pdf parser instance. + * + * @param StreamReader $streamReader + * @return PdfParser|FpdiPdfParser + */ + protected function getPdfParserInstance(StreamReader $streamReader) + { + // note: if you get an exception here - turn off errors/warnings on not found 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 PdfParser($streamReader); + } + + /** + * Get an unique reader id by the $file parameter. + * + * @param string|resource|PdfReader|StreamReader $file An open file descriptor, a path to a file, a PdfReader + * instance or a StreamReader instance. + * @return string + */ + protected function getPdfReaderId($file) + { + if (\is_resource($file)) { + $id = (string) $file; + } elseif (\is_string($file)) { + $id = \realpath($file); + if ($id === false) { + $id = $file; + } + } elseif (\is_object($file)) { + $id = \spl_object_hash($file); + } else { + throw new \InvalidArgumentException( + \sprintf('Invalid type in $file parameter (%s)', \gettype($file)) + ); + } + + /** @noinspection OffsetOperationsInspection */ + if (isset($this->readers[$id])) { + return $id; + } + + if (\is_resource($file)) { + $streamReader = new StreamReader($file); + } elseif (\is_string($file)) { + $streamReader = StreamReader::createByFile($file); + $this->createdReaders[] = $id; + } else { + $streamReader = $file; + } + + $reader = new PdfReader($this->getPdfParserInstance($streamReader)); + /** @noinspection OffsetOperationsInspection */ + $this->readers[$id] = $reader; + + return $id; + } + + /** + * Get a pdf reader instance by its id. + * + * @param string $id + * @return PdfReader + */ + protected function getPdfReader($id) + { + if (isset($this->readers[$id])) { + return $this->readers[$id]; + } + + throw new \InvalidArgumentException( + \sprintf('No pdf reader with the given id (%s) exists.', $id) + ); + } + + /** + * Set the source PDF file. + * + * @param string|resource|StreamReader $file Path to the file or a stream resource or a StreamReader instance. + * @return int The page count of the PDF document. + * @throws PdfParserException + */ + public function setSourceFile($file) + { + $this->currentReaderId = $this->getPdfReaderId($file); + $this->objectsToCopy[$this->currentReaderId] = []; + + $reader = $this->getPdfReader($this->currentReaderId); + $this->setMinPdfVersion($reader->getPdfVersion()); + + return $reader->getPageCount(); + } + + /** + * Imports a page. + * + * @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). + * @return string A unique string identifying the imported page. + * @throws CrossReferenceException + * @throws FilterException + * @throws PdfParserException + * @throws PdfTypeException + * @throws PdfReaderException + * @see PageBoundaries + */ + public function importPage($pageNumber, $box = PageBoundaries::CROP_BOX, $groupXObject = true) + { + if (null === $this->currentReaderId) { + throw new \BadMethodCallException('No reader initiated. Call setSourceFile() first.'); + } + + $pageId = $this->currentReaderId; + + $pageNumber = (int)$pageNumber; + $pageId .= '|' . $pageNumber . '|' . ($groupXObject ? '1' : '0'); + + // for backwards compatibility with FPDI 1 + $box = \ltrim($box, '/'); + if (!PageBoundaries::isValidName($box)) { + throw new \InvalidArgumentException( + \sprintf('Box name is invalid: "%s"', $box) + ); + } + + $pageId .= '|' . $box; + + if (isset($this->importedPages[$pageId])) { + return $pageId; + } + + $reader = $this->getPdfReader($this->currentReaderId); + $page = $reader->getPage($pageNumber); + + $bbox = $page->getBoundary($box); + if ($bbox === false) { + throw new PdfReaderException( + \sprintf("Page doesn't have a boundary box (%s).", $box), + PdfReaderException::MISSING_DATA + ); + } + + $dict = new PdfDictionary(); + $dict->value['Type'] = PdfName::create('XObject'); + $dict->value['Subtype'] = PdfName::create('Form'); + $dict->value['FormType'] = PdfNumeric::create(1); + $dict->value['BBox'] = $bbox->toPdfArray(); + + if ($groupXObject) { + $this->setMinPdfVersion('1.4'); + $dict->value['Group'] = PdfDictionary::create([ + 'Type' => PdfName::create('Group'), + 'S' => PdfName::create('Transparency') + ]); + } + + $resources = $page->getAttribute('Resources'); + if ($resources !== null) { + $dict->value['Resources'] = $resources; + } + + list($width, $height) = $page->getWidthAndHeight($box); + + $a = 1; + $b = 0; + $c = 0; + $d = 1; + $e = -$bbox->getLlx(); + $f = -$bbox->getLly(); + + $rotation = $page->getRotation(); + + if ($rotation !== 0) { + $rotation *= -1; + $angle = $rotation * M_PI / 180; + $a = \cos($angle); + $b = \sin($angle); + $c = -$b; + $d = $a; + + switch ($rotation) { + case -90: + $e = -$bbox->getLly(); + $f = $bbox->getUrx(); + break; + case -180: + $e = $bbox->getUrx(); + $f = $bbox->getUry(); + break; + case -270: + $e = $bbox->getUry(); + $f = -$bbox->getLlx(); + break; + } + } + + // we need to rotate/translate + if ($a != 1 || $b != 0 || $c != 0 || $d != 1 || $e != 0 || $f != 0) { + $dict->value['Matrix'] = PdfArray::create([ + PdfNumeric::create($a), PdfNumeric::create($b), PdfNumeric::create($c), + PdfNumeric::create($d), PdfNumeric::create($e), PdfNumeric::create($f) + ]); + } + + // try to use the existing content stream + $pageDict = $page->getPageDictionary(); + + 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 { + $streamContent = $this->compress + ? \gzcompress($page->getContentStream()) + : $page->getContentStream(); + + $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, ''); + } + + $this->importedPages[$pageId] = [ + 'objectNumber' => null, + 'readerId' => $this->currentReaderId, + 'id' => 'TPL' . $this->getNextTemplateId(), + 'width' => $width / $this->k, + 'height' => $height / $this->k, + 'stream' => $stream + ]; + + return $pageId; + } + + /** + * Draws an imported page onto the page. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $pageId The page 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 useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) + { + if (\is_array($x)) { + /** @noinspection OffsetOperationsInspection */ + unset($x['pageId']); + \extract($x, EXTR_IF_EXISTS); + /** @noinspection NotOptimalIfConditionsInspection */ + if (\is_array($x)) { + $x = 0; + } + } + + if (!isset($this->importedPages[$pageId])) { + throw new \InvalidArgumentException('Imported page does not exist!'); + } + + $importedPage = $this->importedPages[$pageId]; + + $originalSize = $this->getTemplateSize($pageId); + $newSize = $this->getTemplateSize($pageId, $width, $height); + if ($adjustPageSize) { + $this->setPageFormat($newSize, $newSize['orientation']); + } + + $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, + $importedPage['id'] + ) + ); + + return $newSize; + } + + /** + * Get the size of an imported page. + * + * 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 getImportedPageSize($tpl, $width = null, $height = null) + { + if (isset($this->importedPages[$tpl])) { + $importedPage = $this->importedPages[$tpl]; + + if ($width === null && $height === null) { + $width = $importedPage['width']; + $height = $importedPage['height']; + } elseif ($width === null) { + $width = $height * $importedPage['width'] / $importedPage['height']; + } + + if ($height === null) { + $height = $width * $importedPage['height'] / $importedPage['width']; + } + + if ($height <= 0. || $width <= 0.) { + throw new \InvalidArgumentException('Width or height parameter needs to be larger than zero.'); + } + + return [ + 'width' => $width, + 'height' => $height, + 0 => $width, + 1 => $height, + 'orientation' => $width > $height ? 'L' : 'P' + ]; + } + + return false; + } + + /** + * Writes a PdfType object to the resulting buffer. + * + * @param PdfType $value + * @throws PdfTypeException + */ + protected function writePdfType(PdfType $value) + { + if ($value instanceof PdfNumeric) { + if (\is_int($value->value)) { + $this->_put($value->value . ' ', false); + } 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 . '>'); + } 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) { + $this->_put('/' . $name . ' ', false); + $this->writePdfType($entry); + } + $this->_put('>>'); + } elseif ($value instanceof PdfToken) { + $this->_put($value->value); + } elseif ($value instanceof PdfNull) { + $this->_put('null '); + } 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] = []; + } + + if (!isset($this->objectMap[$this->currentReaderId][$value->value])) { + $this->objectMap[$this->currentReaderId][$value->value] = ++$this->n; + $this->objectsToCopy[$this->currentReaderId][] = $value->value; + } + + $this->_put($this->objectMap[$this->currentReaderId][$value->value] . ' 0 R ', false); + } elseif ($value instanceof PdfIndirectObject) { + /** + * @var PdfIndirectObject $value + */ + $n = $this->objectMap[$this->currentReaderId][$value->objectNumber]; + $this->_newobj($n); + $this->writePdfType($value->value); + $this->_put('endobj'); + } + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/AbstractReader.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/AbstractReader.php new file mode 100644 index 0000000..b54237b --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/AbstractReader.php @@ -0,0 +1,95 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\CrossReference; + +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\Type\PdfDictionary; +use setasign\Fpdi\PdfParser\Type\PdfToken; +use setasign\Fpdi\PdfParser\Type\PdfTypeException; + +/** + * Abstract class for cross-reference reader classes. + */ +abstract class AbstractReader +{ + /** + * @var PdfParser + */ + protected $parser; + + /** + * @var PdfDictionary + */ + protected $trailer; + + /** + * AbstractReader constructor. + * + * @param PdfParser $parser + * @throws CrossReferenceException + * @throws PdfTypeException + */ + public function __construct(PdfParser $parser) + { + $this->parser = $parser; + $this->readTrailer(); + } + + /** + * Get the trailer dictionary. + * + * @return PdfDictionary + */ + public function getTrailer() + { + return $this->trailer; + } + + /** + * Read the trailer dictionary. + * + * @throws CrossReferenceException + * @throws PdfTypeException + */ + protected function readTrailer() + { + try { + $trailerKeyword = $this->parser->readValue(null, PdfToken::class); + if ($trailerKeyword->value !== 'trailer') { + throw new CrossReferenceException( + \sprintf( + 'Unexpected end of cross reference. "trailer"-keyword expected, got: %s.', + $trailerKeyword->value + ), + CrossReferenceException::UNEXPECTED_END + ); + } + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + 'Unexpected end of cross reference. "trailer"-keyword expected, got an invalid object type.', + CrossReferenceException::UNEXPECTED_END, + $e + ); + } + + try { + $trailer = $this->parser->readValue(null, PdfDictionary::class); + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + 'Unexpected end of cross reference. Trailer not found.', + CrossReferenceException::UNEXPECTED_END, + $e + ); + } + + $this->trailer = $trailer; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReference.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReference.php new file mode 100644 index 0000000..c47e8c4 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReference.php @@ -0,0 +1,326 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\CrossReference; + +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\Type\PdfDictionary; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; +use setasign\Fpdi\PdfParser\Type\PdfNumeric; +use setasign\Fpdi\PdfParser\Type\PdfStream; +use setasign\Fpdi\PdfParser\Type\PdfToken; +use setasign\Fpdi\PdfParser\Type\PdfTypeException; + +/** + * Class CrossReference + * + * This class processes the standard cross reference of a PDF document. + */ +class CrossReference +{ + /** + * The byte length in which the "startxref" keyword should be searched. + * + * @var int + */ + public static $trailerSearchLength = 5500; + + /** + * @var int + */ + protected $fileHeaderOffset = 0; + + /** + * @var PdfParser + */ + protected $parser; + + /** + * @var ReaderInterface[] + */ + protected $readers = []; + + /** + * CrossReference constructor. + * + * @param PdfParser $parser + * @throws CrossReferenceException + * @throws PdfTypeException + */ + public function __construct(PdfParser $parser, $fileHeaderOffset = 0) + { + $this->parser = $parser; + $this->fileHeaderOffset = $fileHeaderOffset; + + $offset = $this->findStartXref(); + $reader = null; + /** @noinspection TypeUnsafeComparisonInspection */ + while ($offset != false) { // By doing an unsafe comparsion we ignore faulty references to byte offset 0 + try { + $reader = $this->readXref($offset + $this->fileHeaderOffset); + } catch (CrossReferenceException $e) { + // sometimes the file header offset is part of the byte offsets, so let's retry by resetting it to zero. + if ($e->getCode() === CrossReferenceException::INVALID_DATA && $this->fileHeaderOffset !== 0) { + $this->fileHeaderOffset = 0; + $reader = $this->readXref($offset + $this->fileHeaderOffset); + } else { + throw $e; + } + } + + $trailer = $reader->getTrailer(); + $this->checkForEncryption($trailer); + $this->readers[] = $reader; + + if (isset($trailer->value['Prev'])) { + $offset = $trailer->value['Prev']->value; + } else { + $offset = false; + } + } + + // fix faulty sub-section header + if ($reader instanceof FixedReader) { + /** + * @var FixedReader $reader + */ + $reader->fixFaultySubSectionShift(); + } + + if ($reader === null) { + throw new CrossReferenceException('No cross-reference found.', CrossReferenceException::NO_XREF_FOUND); + } + } + + /** + * Get the size of the cross reference. + * + * @return integer + */ + public function getSize() + { + return $this->getTrailer()->value['Size']->value; + } + + /** + * Get the trailer dictionary. + * + * @return PdfDictionary + */ + public function getTrailer() + { + return $this->readers[0]->getTrailer(); + } + + /** + * Get the cross reference readser instances. + * + * @return ReaderInterface[] + */ + public function getReaders() + { + return $this->readers; + } + + /** + * Get the offset by an object number. + * + * @param int $objectNumber + * @return integer|bool + */ + public function getOffsetFor($objectNumber) + { + foreach ($this->getReaders() as $reader) { + $offset = $reader->getOffsetFor($objectNumber); + if ($offset !== false) { + return $offset; + } + } + + return false; + } + + /** + * Get an indirect object by its object number. + * + * @param int $objectNumber + * @return PdfIndirectObject + * @throws CrossReferenceException + */ + public function getIndirectObject($objectNumber) + { + $offset = $this->getOffsetFor($objectNumber); + if ($offset === false) { + throw new CrossReferenceException( + \sprintf('Object (id:%s) not found.', $objectNumber), + CrossReferenceException::OBJECT_NOT_FOUND + ); + } + + $parser = $this->parser; + + $parser->getTokenizer()->clearStack(); + $parser->getStreamReader()->reset($offset + $this->fileHeaderOffset); + + try { + /** @var PdfIndirectObject $object */ + $object = $parser->readValue(null, PdfIndirectObject::class); + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + \sprintf('Object (id:%s) not found at location (%s).', $objectNumber, $offset), + CrossReferenceException::OBJECT_NOT_FOUND, + $e + ); + } + + if ($object->objectNumber !== $objectNumber) { + throw new CrossReferenceException( + \sprintf('Wrong object found, got %s while %s was expected.', $object->objectNumber, $objectNumber), + CrossReferenceException::OBJECT_NOT_FOUND + ); + } + + return $object; + } + + /** + * Read the cross-reference table at a given offset. + * + * Internally the method will try to evaluate the best reader for this cross-reference. + * + * @param int $offset + * @return ReaderInterface + * @throws CrossReferenceException + * @throws PdfTypeException + */ + protected function readXref($offset) + { + $this->parser->getStreamReader()->reset($offset); + $this->parser->getTokenizer()->clearStack(); + $initValue = $this->parser->readValue(); + + return $this->initReaderInstance($initValue); + } + + /** + * Get a cross-reference reader instance. + * + * @param PdfToken|PdfIndirectObject $initValue + * @return ReaderInterface|bool + * @throws CrossReferenceException + * @throws PdfTypeException + */ + protected function initReaderInstance($initValue) + { + $position = $this->parser->getStreamReader()->getPosition() + + $this->parser->getStreamReader()->getOffset() + $this->fileHeaderOffset; + + if ($initValue instanceof PdfToken && $initValue->value === 'xref') { + try { + return new FixedReader($this->parser); + } catch (CrossReferenceException $e) { + $this->parser->getStreamReader()->reset($position); + $this->parser->getTokenizer()->clearStack(); + + return new LineReader($this->parser); + } + } + + if ($initValue instanceof PdfIndirectObject) { + try { + $stream = PdfStream::ensure($initValue->value); + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + 'Invalid object type at xref reference offset.', + CrossReferenceException::INVALID_DATA, + $e + ); + } + + $type = PdfDictionary::get($stream->value, 'Type'); + if ($type->value !== 'XRef') { + throw new CrossReferenceException( + 'The xref position points to an incorrect object type.', + CrossReferenceException::INVALID_DATA + ); + } + + $this->checkForEncryption($stream->value); + + throw new CrossReferenceException( + 'This PDF document probably uses a compression technique which is not supported by the ' . + 'free parser shipped with FPDI. (See https://www.setasign.com/fpdi-pdf-parser for more details)', + CrossReferenceException::COMPRESSED_XREF + ); + } + + throw new CrossReferenceException( + 'The xref position points to an incorrect object type.', + CrossReferenceException::INVALID_DATA + ); + } + + /** + * Check for encryption. + * + * @param PdfDictionary $dictionary + * @throws CrossReferenceException + */ + protected function checkForEncryption(PdfDictionary $dictionary) + { + if (isset($dictionary->value['Encrypt'])) { + throw new CrossReferenceException( + 'This PDF document is encrypted and cannot be processed with FPDI.', + CrossReferenceException::ENCRYPTED + ); + } + } + + /** + * Find the start position for the first cross-reference. + * + * @return int The byte-offset position of the first cross-reference. + * @throws CrossReferenceException + */ + protected function findStartXref() + { + $reader = $this->parser->getStreamReader(); + $reader->reset(-self::$trailerSearchLength, self::$trailerSearchLength); + + $buffer = $reader->getBuffer(false); + $pos = \strrpos($buffer, 'startxref'); + $addOffset = 9; + if ($pos === false) { + // Some corrupted documents uses startref, instead of startxref + $pos = \strrpos($buffer, 'startref'); + if ($pos === false) { + throw new CrossReferenceException( + 'Unable to find pointer to xref table', + CrossReferenceException::NO_STARTXREF_FOUND + ); + } + $addOffset = 8; + } + + $reader->setOffset($pos + $addOffset); + + try { + $value = $this->parser->readValue(null, PdfNumeric::class); + } catch (PdfTypeException $e) { + throw new CrossReferenceException( + 'Invalid data after startxref keyword.', + CrossReferenceException::INVALID_DATA, + $e + ); + } + + return $value->value; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReferenceException.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReferenceException.php new file mode 100644 index 0000000..7d88d5d --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/CrossReferenceException.php @@ -0,0 +1,79 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\CrossReference; + +use setasign\Fpdi\PdfParser\PdfParserException; + +/** + * Exception used by the CrossReference and Reader classes. + */ +class CrossReferenceException extends PdfParserException +{ + /** + * @var int + */ + const INVALID_DATA = 0x0101; + + /** + * @var int + */ + const XREF_MISSING = 0x0102; + + /** + * @var int + */ + const ENTRIES_TOO_LARGE = 0x0103; + + /** + * @var int + */ + const ENTRIES_TOO_SHORT = 0x0104; + + /** + * @var int + */ + const NO_ENTRIES = 0x0105; + + /** + * @var int + */ + const NO_TRAILER_FOUND = 0x0106; + + /** + * @var int + */ + const NO_STARTXREF_FOUND = 0x0107; + + /** + * @var int + */ + const NO_XREF_FOUND = 0x0108; + + /** + * @var int + */ + const UNEXPECTED_END = 0x0109; + + /** + * @var int + */ + const OBJECT_NOT_FOUND = 0x010A; + + /** + * @var int + */ + const COMPRESSED_XREF = 0x010B; + + /** + * @var int + */ + const ENCRYPTED = 0x010C; +} diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/FixedReader.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/FixedReader.php new file mode 100644 index 0000000..883feec --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/FixedReader.php @@ -0,0 +1,199 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\CrossReference; + +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\StreamReader; + +/** + * Class FixedReader + * + * This reader allows a very less overhead parsing of single entries of the cross-reference, because the main entries + * are only read when needed and not in a single run. + */ +class FixedReader extends AbstractReader implements ReaderInterface +{ + /** + * @var StreamReader + */ + protected $reader; + + /** + * Data of subsections. + * + * @var array + */ + protected $subSections; + + /** + * FixedReader constructor. + * + * @param PdfParser $parser + * @throws CrossReferenceException + */ + public function __construct(PdfParser $parser) + { + $this->reader = $parser->getStreamReader(); + $this->read(); + parent::__construct($parser); + } + + /** + * Get all subsection data. + * + * @return array + */ + public function getSubSections() + { + return $this->subSections; + } + + /** + * @inheritdoc + */ + public function getOffsetFor($objectNumber) + { + foreach ($this->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') { + return false; + } + + return (int) \substr($line, 0, 10); + } + } + + return false; + } + + /** + * Read the cross-reference. + * + * This reader will only read the subsections in this method. The offsets were resolved individually by this + * information. + * + * @throws CrossReferenceException + */ + protected function read() + { + $subSections = []; + + $startObject = $entryCount = $lastLineStart = null; + $validityChecked = false; + while (($line = $this->reader->readLine(20)) !== false) { + if (\strpos($line, 'trailer') !== false) { + $this->reader->reset($lastLineStart); + break; + } + + // jump over if line content doesn't match the expected string + if (\sscanf($line, '%d %d', $startObject, $entryCount) !== 2) { + continue; + } + + $oldPosition = $this->reader->getPosition(); + $position = $oldPosition + $this->reader->getOffset(); + + if (!$validityChecked && $entryCount > 0) { + $nextLine = $this->reader->readBytes(21); + /* Check the next line for maximum of 20 bytes and not longer + * By catching 21 bytes and trimming the length should be still 21. + */ + if (\strlen(\trim($nextLine)) !== 21) { + throw new CrossReferenceException( + 'Cross-reference entries are larger than 20 bytes.', + CrossReferenceException::ENTRIES_TOO_LARGE + ); + } + + /* Check for less than 20 bytes: cut the line to 20 bytes and trim; have to result in exactly 18 bytes. + * If it would have less bytes the substring would get the first bytes of the next line which would + * evaluate to a 20 bytes long string after trimming. + */ + if (\strlen(\trim(\substr($nextLine, 0, 20))) !== 18) { + throw new CrossReferenceException( + 'Cross-reference entries are less than 20 bytes.', + CrossReferenceException::ENTRIES_TOO_SHORT + ); + } + + $validityChecked = true; + } + + $subSections[$position] = [$startObject, $entryCount]; + + $lastLineStart = $position + $entryCount * 20; + $this->reader->reset($lastLineStart); + } + + // reset after the last correct parsed line + $this->reader->reset($lastLineStart); + + if (\count($subSections) === 0) { + throw new CrossReferenceException( + 'No entries found in cross-reference.', + CrossReferenceException::NO_ENTRIES + ); + } + + $this->subSections = $subSections; + } + + /** + * Fixes an invalid object number shift. + * + * This method can be used to repair documents with an invalid subsection header: + * + * <code> + * xref + * 1 7 + * 0000000000 65535 f + * 0000000009 00000 n + * 0000412075 00000 n + * 0000412172 00000 n + * 0000412359 00000 n + * 0000412417 00000 n + * 0000412468 00000 n + * </code> + * + * It shall only be called on the first table. + * + * @return bool + */ + public function fixFaultySubSectionShift() + { + $subSections = $this->getSubSections(); + if (\count($subSections) > 1) { + return false; + } + + $subSection = \current($subSections); + if ($subSection[0] != 1) { + return false; + } + + if ($this->getOffsetFor(1) === false) { + foreach ($subSections as $offset => list($startObject, $objectCount)) { + $this->subSections[$offset] = [$startObject - 1, $objectCount]; + } + return true; + } + + return false; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/LineReader.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/LineReader.php new file mode 100644 index 0000000..b6f0e42 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/LineReader.php @@ -0,0 +1,167 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\CrossReference; + +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\StreamReader; + +/** + * Class LineReader + * + * This reader class read all cross-reference entries in a single run. + * It supports reading cross-references with e.g. invalid data (e.g. entries with a length < or > 20 bytes). + */ +class LineReader extends AbstractReader implements ReaderInterface +{ + /** + * The object offsets. + * + * @var array + */ + protected $offsets; + + /** + * LineReader constructor. + * + * @param PdfParser $parser + * @throws CrossReferenceException + */ + public function __construct(PdfParser $parser) + { + $this->read($this->extract($parser->getStreamReader())); + parent::__construct($parser); + } + + /** + * @inheritdoc + */ + public function getOffsetFor($objectNumber) + { + if (isset($this->offsets[$objectNumber])) { + return $this->offsets[$objectNumber][0]; + } + + return false; + } + + /** + * Get all found offsets. + * + * @return array + */ + public function getOffsets() + { + return $this->offsets; + } + + /** + * Extracts the cross reference data from the stream reader. + * + * @param StreamReader $reader + * @return string + * @throws CrossReferenceException + */ + protected function extract(StreamReader $reader) + { + $bytesPerCycle = 100; + $reader->reset(null, $bytesPerCycle); + + $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( + 'Unexpected end of cross reference. "trailer"-keyword not found.', + CrossReferenceException::NO_TRAILER_FOUND + ); + } + + $xrefContent = \substr($reader->getBuffer(false), 0, $trailerPos); + $reader->reset($reader->getPosition() + $trailerPos); + + return $xrefContent; + } + + /** + * Read the cross-reference entries. + * + * @param string $xrefContent + * @throws CrossReferenceException + */ + protected function read($xrefContent) + { + // get eol markers in the first 100 bytes + \preg_match_all("/(\r\n|\n|\r)/", \substr($xrefContent, 0, 100), $m); + + if (\count($m[0]) === 0) { + throw new CrossReferenceException( + 'No data found in cross-reference.', + CrossReferenceException::INVALID_DATA + ); + } + + // count(array_count_values()) is faster then count(array_unique()) + // @see https://github.com/symfony/symfony/pull/23731 + // can be reverted for php7.2 + $differentLineEndings = \count(\array_count_values($m[0])); + if ($differentLineEndings > 1) { + $lines = \preg_split("/(\r\n|\n|\r)/", $xrefContent, -1, PREG_SPLIT_NO_EMPTY); + } else { + $lines = \explode($m[0][0], $xrefContent); + } + + unset($differentLineEndings, $m); + if (!\is_array($lines)) { + $this->offsets = []; + return; + } + + $start = 0; + $offsets = []; + + // 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 + ); + } + } + + $this->offsets = $offsets; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/CrossReference/ReaderInterface.php b/vendor/setasign/fpdi/src/PdfParser/CrossReference/ReaderInterface.php new file mode 100644 index 0000000..d2dfdd0 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/CrossReference/ReaderInterface.php @@ -0,0 +1,34 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\CrossReference; + +use setasign\Fpdi\PdfParser\Type\PdfDictionary; + +/** + * ReaderInterface for cross-reference readers. + */ +interface ReaderInterface +{ + /** + * Get an offset by an object number. + * + * @param int $objectNumber + * @return int|bool False if the offset was not found. + */ + public function getOffsetFor($objectNumber); + + /** + * Get the trailer related to this cross reference. + * + * @return PdfDictionary + */ + public function getTrailer(); +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85.php b/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85.php new file mode 100644 index 0000000..1dc936d --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85.php @@ -0,0 +1,102 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Filter; + +/** + * Class for handling ASCII base-85 encoded data + */ +class Ascii85 implements FilterInterface +{ + /** + * Decode ASCII85 encoded string. + * + * @param string $data The input string + * @return string + * @throws Ascii85Exception + */ + public function decode($data) + { + $out = ''; + $state = 0; + $chn = null; + + $data = \preg_replace('/\s/', '', $data); + + $l = \strlen($data); + + /** @noinspection ForeachInvariantsInspection */ + for ($k = 0; $k < $l; ++$k) { + $ch = \ord($data[$k]) & 0xff; + + //Start <~ + if ($k === 0 && $ch === 60 && isset($data[$k + 1]) && (\ord($data[$k + 1]) & 0xFF) === 126) { + $k++; + continue; + } + //End ~> + if ($ch === 126 && isset($data[$k + 1]) && (\ord($data[$k + 1]) & 0xFF) === 62) { + break; + } + + if ($ch === 122 /* z */ && $state === 0) { + $out .= \chr(0) . \chr(0) . \chr(0) . \chr(0); + continue; + } + + if ($ch < 33 /* ! */ || $ch > 117 /* u */) { + throw new Ascii85Exception( + 'Illegal character found while ASCII85 decode.', + Ascii85Exception::ILLEGAL_CHAR_FOUND + ); + } + + $chn[$state] = $ch - 33;/* ! */ + $state++; + + if ($state === 5) { + $state = 0; + $r = 0; + for ($j = 0; $j < 5; ++$j) { + /** @noinspection UnnecessaryCastingInspection */ + $r = (int)($r * 85 + $chn[$j]); + } + + $out .= \chr($r >> 24) + . \chr($r >> 16) + . \chr($r >> 8) + . \chr($r); + } + } + + if ($state === 1) { + throw new Ascii85Exception( + 'Illegal length while ASCII85 decode.', + Ascii85Exception::ILLEGAL_LENGTH + ); + } + + if ($state === 2) { + $r = $chn[0] * 85 * 85 * 85 * 85 + ($chn[1] + 1) * 85 * 85 * 85; + $out .= \chr($r >> 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); + $out .= \chr($r >> 16); + $out .= \chr($r >> 8); + } + + return $out; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85Exception.php b/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85Exception.php new file mode 100644 index 0000000..f4b6758 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/Ascii85Exception.php @@ -0,0 +1,27 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Filter; + +/** + * Exception for Ascii85 filter class + */ +class Ascii85Exception extends FilterException +{ + /** + * @var integer + */ + const ILLEGAL_CHAR_FOUND = 0x0301; + + /** + * @var integer + */ + const ILLEGAL_LENGTH = 0x0302; +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/AsciiHex.php b/vendor/setasign/fpdi/src/PdfParser/Filter/AsciiHex.php new file mode 100644 index 0000000..d0c3436 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/AsciiHex.php @@ -0,0 +1,47 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Filter; + +/** + * Class for handling ASCII hexadecimal encoded data + */ +class AsciiHex implements FilterInterface +{ + /** + * Converts an ASCII hexadecimal encoded string into its binary representation. + * + * @param string $data The input string + * @return string + */ + public function decode($data) + { + $data = \preg_replace('/[^0-9A-Fa-f]/', '', \rtrim($data, '>')); + if ((\strlen($data) % 2) === 1) { + $data .= '0'; + } + + return \pack('H*', $data); + } + + /** + * Converts a string into ASCII hexadecimal representation. + * + * @param string $data The input string + * @param boolean $leaveEOD + * @return string + */ + public function encode($data, $leaveEOD = false) + { + $t = \unpack('H*', $data); + return \current($t) + . ($leaveEOD ? '' : '>'); + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/FilterException.php b/vendor/setasign/fpdi/src/PdfParser/Filter/FilterException.php new file mode 100644 index 0000000..c55a7a8 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/FilterException.php @@ -0,0 +1,23 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Filter; + +use setasign\Fpdi\PdfParser\PdfParserException; + +/** + * Exception for filters + */ +class FilterException extends PdfParserException +{ + const UNSUPPORTED_FILTER = 0x0201; + + const NOT_IMPLEMENTED = 0x0202; +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/FilterInterface.php b/vendor/setasign/fpdi/src/PdfParser/Filter/FilterInterface.php new file mode 100644 index 0000000..3700190 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/FilterInterface.php @@ -0,0 +1,25 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Filter; + +/** + * Interface for filters + */ +interface FilterInterface +{ + /** + * Decode a string. + * + * @param string $data The input string + * @return string + */ + public function decode($data); +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/Flate.php b/vendor/setasign/fpdi/src/PdfParser/Filter/Flate.php new file mode 100644 index 0000000..b8f79d1 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/Flate.php @@ -0,0 +1,86 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Filter; + +/** + * Class for handling zlib/deflate encoded data + */ +class Flate implements FilterInterface +{ + /** + * Checks whether the zlib extension is loaded. + * + * Used for testing purpose. + * + * @return boolean + * @internal + */ + protected function extensionLoaded() + { + return \extension_loaded('zlib'); + } + + /** + * Decodes a flate compressed string. + * + * @param string|false $data The input string + * @return string + * @throws FlateException + */ + public function decode($data) + { + if ($this->extensionLoaded()) { + $oData = $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]); + fseek($fh, 0); + $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) < ($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; + } + + if (!$data) { + throw new FlateException( + 'Error while decompressing stream.', + FlateException::DECOMPRESS_ERROR + ); + } + } + } else { + throw new FlateException( + 'To handle FlateDecode filter, enable zlib support in PHP.', + FlateException::NO_ZLIB + ); + } + + return $data; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/FlateException.php b/vendor/setasign/fpdi/src/PdfParser/Filter/FlateException.php new file mode 100644 index 0000000..d897ac8 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/FlateException.php @@ -0,0 +1,27 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Filter; + +/** + * Exception for flate filter class + */ +class FlateException extends FilterException +{ + /** + * @var integer + */ + const NO_ZLIB = 0x0401; + + /** + * @var integer + */ + const DECOMPRESS_ERROR = 0x0402; +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/Lzw.php b/vendor/setasign/fpdi/src/PdfParser/Filter/Lzw.php new file mode 100644 index 0000000..bedb5b7 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/Lzw.php @@ -0,0 +1,187 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Filter; + +/** + * Class for handling LZW encoded data + */ +class Lzw implements FilterInterface +{ + /** + * @var null|string + */ + protected $data; + + /** + * @var array + */ + protected $sTable = []; + + /** + * @var int + */ + protected $dataLength = 0; + + /** + * @var int + */ + protected $tIdx; + + /** + * @var int + */ + protected $bitsToGet = 9; + + /** + * @var int + */ + protected $bytePointer; + + /** + * @var int + */ + protected $nextData = 0; + + /** + * @var int + */ + protected $nextBits = 0; + + /** + * @var array + */ + protected $andTable = [511, 1023, 2047, 4095]; + + /** + * Method to decode LZW compressed data. + * + * @param string $data The compressed data + * @return string The uncompressed data + * @throws LzwException + */ + public function decode($data) + { + if ($data[0] === "\x00" && $data[1] === "\x01") { + throw new LzwException( + 'LZW flavour not supported.', + LzwException::LZW_FLAVOUR_NOT_SUPPORTED + ); + } + + $this->initsTable(); + + $this->data = $data; + $this->dataLength = \strlen($data); + + // Initialize pointers + $this->bytePointer = 0; + + $this->nextData = 0; + $this->nextBits = 0; + + $oldCode = 0; + + $uncompData = ''; + + while (($code = $this->getNextCode()) !== 257) { + if ($code === 256) { + $this->initsTable(); + $code = $this->getNextCode(); + + if ($code === 257) { + break; + } + + $uncompData .= $this->sTable[$code]; + $oldCode = $code; + } 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; + } + } + } + + return $uncompData; + } + + /** + * Initialize the string table. + */ + protected function initsTable() + { + $this->sTable = []; + + for ($i = 0; $i < 256; $i++) { + $this->sTable[$i] = \chr($i); + } + + $this->tIdx = 258; + $this->bitsToGet = 9; + } + + /** + * Add a new string to the string table. + * + * @param string $oldString + * @param string $newString + */ + protected function addStringToTable($oldString, $newString = '') + { + $string = $oldString . $newString; + + // Add this new String to the table + $this->sTable[$this->tIdx++] = $string; + + if ($this->tIdx === 511) { + $this->bitsToGet = 10; + } elseif ($this->tIdx === 1023) { + $this->bitsToGet = 11; + } elseif ($this->tIdx === 2047) { + $this->bitsToGet = 12; + } + } + + /** + * Returns the next 9, 10, 11 or 12 bits. + * + * @return integer + */ + protected function getNextCode() + { + if ($this->bytePointer === $this->dataLength) { + return 257; + } + + $this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff); + $this->nextBits += 8; + + if ($this->nextBits < $this->bitsToGet) { + $this->nextData = ($this->nextData << 8) | (\ord($this->data[$this->bytePointer++]) & 0xff); + $this->nextBits += 8; + } + + $code = ($this->nextData >> ($this->nextBits - $this->bitsToGet)) & $this->andTable[$this->bitsToGet - 9]; + $this->nextBits -= $this->bitsToGet; + + return $code; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Filter/LzwException.php b/vendor/setasign/fpdi/src/PdfParser/Filter/LzwException.php new file mode 100644 index 0000000..6ebad4f --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Filter/LzwException.php @@ -0,0 +1,22 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Filter; + +/** + * Exception for LZW filter class + */ +class LzwException extends FilterException +{ + /** + * @var integer + */ + const LZW_FLAVOUR_NOT_SUPPORTED = 0x0501; +} diff --git a/vendor/setasign/fpdi/src/PdfParser/PdfParser.php b/vendor/setasign/fpdi/src/PdfParser/PdfParser.php new file mode 100644 index 0000000..f724314 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/PdfParser.php @@ -0,0 +1,381 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser; + +use setasign\Fpdi\PdfParser\CrossReference\CrossReference; +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; +use setasign\Fpdi\PdfParser\Type\PdfArray; +use setasign\Fpdi\PdfParser\Type\PdfBoolean; +use setasign\Fpdi\PdfParser\Type\PdfDictionary; +use setasign\Fpdi\PdfParser\Type\PdfHexString; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference; +use setasign\Fpdi\PdfParser\Type\PdfName; +use setasign\Fpdi\PdfParser\Type\PdfNull; +use setasign\Fpdi\PdfParser\Type\PdfNumeric; +use setasign\Fpdi\PdfParser\Type\PdfStream; +use setasign\Fpdi\PdfParser\Type\PdfString; +use setasign\Fpdi\PdfParser\Type\PdfToken; +use setasign\Fpdi\PdfParser\Type\PdfType; + +/** + * A PDF parser class + */ +class PdfParser +{ + /** + * @var StreamReader + */ + protected $streamReader; + + /** + * @var Tokenizer + */ + protected $tokenizer; + + /** + * The file header. + * + * @var string + */ + protected $fileHeader; + + /** + * The offset to the file header. + * + * @var int + */ + protected $fileHeaderOffset; + + /** + * @var CrossReference|null + */ + protected $xref; + + /** + * All read objects. + * + * @var array + */ + protected $objects = []; + + /** + * PdfParser constructor. + * + * @param StreamReader $streamReader + */ + public function __construct(StreamReader $streamReader) + { + $this->streamReader = $streamReader; + $this->tokenizer = new Tokenizer($streamReader); + } + + /** + * Removes cycled references. + * + * @internal + */ + public function cleanUp() + { + $this->xref = null; + } + + /** + * Get the stream reader instance. + * + * @return StreamReader + */ + public function getStreamReader() + { + return $this->streamReader; + } + + /** + * Get the tokenizer instance. + * + * @return Tokenizer + */ + public function getTokenizer() + { + return $this->tokenizer; + } + + /** + * Resolves the file header. + * + * @throws PdfParserException + * @return int + */ + protected function resolveFileHeader() + { + if ($this->fileHeader) { + return $this->fileHeaderOffset; + } + + $this->streamReader->reset(0); + $maxIterations = 1000; + while (true) { + $buffer = $this->streamReader->getBuffer(false); + $offset = \strpos($buffer, '%PDF-'); + if ($offset === false) { + if (!$this->streamReader->increaseLength(100) || (--$maxIterations === 0)) { + throw new PdfParserException( + 'Unable to find PDF file header.', + PdfParserException::FILE_HEADER_NOT_FOUND + ); + } + continue; + } + break; + } + + $this->fileHeaderOffset = $offset; + $this->streamReader->setOffset($offset); + + $this->fileHeader = \trim($this->streamReader->readLine()); + return $this->fileHeaderOffset; + } + + /** + * Get the cross reference instance. + * + * @return CrossReference + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getCrossReference() + { + if ($this->xref === null) { + $this->xref = new CrossReference($this, $this->resolveFileHeader()); + } + + return $this->xref; + } + + /** + * Get the PDF version. + * + * @return int[] An array of major and minor version. + * @throws PdfParserException + */ + public function getPdfVersion() + { + $this->resolveFileHeader(); + + if (\preg_match('/%PDF-(\d)\.(\d)/', $this->fileHeader, $result) === 0) { + throw new PdfParserException( + 'Unable to extract PDF version from file header.', + PdfParserException::PDF_VERSION_NOT_FOUND + ); + } + list(, $major, $minor) = $result; + + $catalog = $this->getCatalog(); + if (isset($catalog->value['Version'])) { + $versionParts = \explode( + '.', + PdfName::unescape(PdfType::resolve($catalog->value['Version'], $this)->value) + ); + if (count($versionParts) === 2) { + list($major, $minor) = $versionParts; + } + } + + return [(int) $major, (int) $minor]; + } + + /** + * Get the catalog dictionary. + * + * @return PdfDictionary + * @throws Type\PdfTypeException + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getCatalog() + { + $trailer = $this->getCrossReference()->getTrailer(); + + $catalog = PdfType::resolve(PdfDictionary::get($trailer, 'Root'), $this); + + return PdfDictionary::ensure($catalog); + } + + /** + * Get an indirect object by its object number. + * + * @param int $objectNumber + * @param bool $cache + * @return PdfIndirectObject + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getIndirectObject($objectNumber, $cache = false) + { + $objectNumber = (int) $objectNumber; + if (isset($this->objects[$objectNumber])) { + return $this->objects[$objectNumber]; + } + + $object = $this->getCrossReference()->getIndirectObject($objectNumber); + + if ($cache) { + $this->objects[$objectNumber] = $object; + } + + return $object; + } + + /** + * Read a PDF value. + * + * @param null|bool|string $token + * @param null|string $expectedType + * @return false|PdfArray|PdfBoolean|PdfDictionary|PdfHexString|PdfIndirectObject|PdfIndirectObjectReference|PdfName|PdfNull|PdfNumeric|PdfStream|PdfString|PdfToken + * @throws Type\PdfTypeException + */ + public function readValue($token = null, $expectedType = null) + { + if ($token === null) { + $token = $this->tokenizer->getNextToken(); + } + + if ($token === false) { + if ($expectedType !== null) { + throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE); + } + return false; + } + + switch ($token) { + case '(': + $this->ensureExpectedType($token, $expectedType); + return PdfString::parse($this->streamReader); + + case '<': + if ($this->streamReader->getByte() === '<') { + $this->ensureExpectedType('<<', $expectedType); + $this->streamReader->addOffset(1); + return PdfDictionary::parse($this->tokenizer, $this->streamReader, $this); + } + + $this->ensureExpectedType($token, $expectedType); + return PdfHexString::parse($this->streamReader); + + case '/': + $this->ensureExpectedType($token, $expectedType); + return PdfName::parse($this->tokenizer, $this->streamReader); + + case '[': + $this->ensureExpectedType($token, $expectedType); + return PdfArray::parse($this->tokenizer, $this); + + default: + if (\is_numeric($token)) { + if (($token2 = $this->tokenizer->getNextToken()) !== false) { + if (\is_numeric($token2) && ($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( + (int) $token, + (int) $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((int) $token, (int) $token2); + } + + $this->tokenizer->pushStack($token3); + } + + $this->tokenizer->pushStack($token2); + } + + if ($expectedType !== null && $expectedType !== PdfNumeric::class) { + throw new Type\PdfTypeException( + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE + ); + } + return PdfNumeric::create($token + 0); + } + + if ($token === 'true' || $token === 'false') { + $this->ensureExpectedType($token, $expectedType); + return PdfBoolean::create($token === 'true'); + } + + if ($token === 'null') { + $this->ensureExpectedType($token, $expectedType); + return new PdfNull(); + } + + if ($expectedType !== null && $expectedType !== PdfToken::class) { + throw new Type\PdfTypeException( + 'Got unexpected token type.', + Type\PdfTypeException::INVALID_DATA_TYPE + ); + } + + $v = new PdfToken(); + $v->value = $token; + + return $v; + } + } + + /** + * Ensures that the token will evaluate to an expected object type (or not). + * + * @param string $token + * @param string|null $expectedType + * @return bool + * @throws Type\PdfTypeException + */ + private function ensureExpectedType($token, $expectedType) + { + static $mapping = [ + '(' => PdfString::class, + '<' => PdfHexString::class, + '<<' => PdfDictionary::class, + '/' => PdfName::class, + '[' => PdfArray::class, + 'true' => PdfBoolean::class, + 'false' => PdfBoolean::class, + 'null' => PdfNull::class + ]; + + if ($expectedType === null || $mapping[$token] === $expectedType) { + return true; + } + + throw new Type\PdfTypeException('Got unexpected token type.', Type\PdfTypeException::INVALID_DATA_TYPE); + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/PdfParserException.php b/vendor/setasign/fpdi/src/PdfParser/PdfParserException.php new file mode 100644 index 0000000..6d034d8 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/PdfParserException.php @@ -0,0 +1,49 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser; + +use setasign\Fpdi\FpdiException; + +/** + * Exception for the pdf parser class + */ +class PdfParserException extends FpdiException +{ + /** + * @var int + */ + const NOT_IMPLEMENTED = 0x0001; + + /** + * @var int + */ + const IMPLEMENTED_IN_FPDI_PDF_PARSER = 0x0002; + + /** + * @var int + */ + const INVALID_DATA_TYPE = 0x0003; + + /** + * @var int + */ + const FILE_HEADER_NOT_FOUND = 0x0004; + + /** + * @var int + */ + const PDF_VERSION_NOT_FOUND = 0x0005; + + /** + * @var int + */ + const INVALID_DATA_SIZE = 0x0006; +} diff --git a/vendor/setasign/fpdi/src/PdfParser/StreamReader.php b/vendor/setasign/fpdi/src/PdfParser/StreamReader.php new file mode 100644 index 0000000..ee40ebb --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/StreamReader.php @@ -0,0 +1,471 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser; + +/** + * A stream reader class + */ +class StreamReader +{ + /** + * Creates a stream reader instance by a string value. + * + * @param string $content + * @param int $maxMemory + * @return StreamReader + */ + public static function createByString($content, $maxMemory = 2097152) + { + $h = \fopen('php://temp/maxmemory:' . ((int) $maxMemory), 'r+b'); + \fwrite($h, $content); + \rewind($h); + + return new self($h, true); + } + + /** + * Creates a stream reader instance by a filename. + * + * @param string $filename + * @return StreamReader + */ + public static function createByFile($filename) + { + $h = \fopen($filename, 'rb'); + return new self($h, true); + } + + /** + * Defines whether the stream should be closed when the stream reader instance is deconstructed or not. + * + * @var bool + */ + protected $closeStream; + + /** + * The stream resource. + * + * @var resource + */ + protected $stream; + + /** + * The byte-offset position in the stream. + * + * @var int + */ + protected $position; + + /** + * The byte-offset position in the buffer. + * + * @var int + */ + protected $offset; + + /** + * The buffer length. + * + * @var int + */ + protected $bufferLength; + + /** + * The total length of the stream. + * + * @var int + */ + protected $totalLength; + + /** + * The buffer. + * + * @var string + */ + protected $buffer; + + /** + * StreamReader constructor. + * + * @param resource $stream + * @param bool $closeStream Defines whether to close the stream resource if the instance is destructed or not. + */ + public function __construct($stream, $closeStream = false) + { + if (!\is_resource($stream)) { + throw new \InvalidArgumentException( + 'No stream given.' + ); + } + + $metaData = \stream_get_meta_data($stream); + if (!$metaData['seekable']) { + throw new \InvalidArgumentException( + 'Given stream is not seekable!' + ); + } + + $this->stream = $stream; + $this->closeStream = $closeStream; + $this->reset(); + } + + /** + * The destructor. + */ + public function __destruct() + { + $this->cleanUp(); + } + + /** + * Closes the file handle. + */ + public function cleanUp() + { + if ($this->closeStream && is_resource($this->stream)) { + \fclose($this->stream); + } + } + + /** + * Returns the byte length of the buffer. + * + * @param bool $atOffset + * @return int + */ + public function getBufferLength($atOffset = false) + { + if ($atOffset === false) { + return $this->bufferLength; + } + + return $this->bufferLength - $this->offset; + } + + /** + * Get the current position in the stream. + * + * @return int + */ + public function getPosition() + { + return $this->position; + } + + /** + * Returns the current buffer. + * + * @param bool $atOffset + * @return string + */ + public function getBuffer($atOffset = true) + { + if ($atOffset === false) { + return $this->buffer; + } + + $string = \substr($this->buffer, $this->offset); + + return (string) $string; + } + + /** + * Gets a byte at a specific position in the buffer. + * + * If the position is invalid the method will return false. + * + * If the $position parameter is set to null the value of $this->offset will be used. + * + * @param int|null $position + * @return string|bool + */ + public function getByte($position = null) + { + $position = (int) ($position !== null ? $position : $this->offset); + if ( + $position >= $this->bufferLength + && (!$this->increaseLength() || $position >= $this->bufferLength) + ) { + return false; + } + + return $this->buffer[$position]; + } + + /** + * Returns a byte at a specific position, and set the offset to the next byte position. + * + * If the position is invalid the method will return false. + * + * If the $position parameter is set to null the value of $this->offset will be used. + * + * @param int|null $position + * @return string|bool + */ + public function readByte($position = null) + { + if ($position !== null) { + $position = (int) $position; + // check if needed bytes are available in the current buffer + if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) { + $this->reset($position); + $offset = $this->offset; + } else { + $offset = $position - $this->position; + } + } else { + $offset = $this->offset; + } + + if ( + $offset >= $this->bufferLength + && ((!$this->increaseLength()) || $offset >= $this->bufferLength) + ) { + return false; + } + + $this->offset = $offset + 1; + return $this->buffer[$offset]; + } + + /** + * Read bytes from the current or a specific offset position and set the internal pointer to the next byte. + * + * If the position is invalid the method will return false. + * + * If the $position parameter is set to null the value of $this->offset will be used. + * + * @param int $length + * @param int|null $position + * @return string|false + */ + public function readBytes($length, $position = null) + { + $length = (int) $length; + if ($position !== null) { + // check if needed bytes are available in the current buffer + if (!($position >= $this->position && $position < $this->position + $this->bufferLength)) { + $this->reset($position, $length); + $offset = $this->offset; + } else { + $offset = $position - $this->position; + } + } else { + $offset = $this->offset; + } + + if ( + ($offset + $length) > $this->bufferLength + && ((!$this->increaseLength($length)) || ($offset + $length) > $this->bufferLength) + ) { + return false; + } + + $bytes = \substr($this->buffer, $offset, $length); + $this->offset = $offset + $length; + + return $bytes; + } + + /** + * Read a line from the current position. + * + * @param int $length + * @return string|bool + */ + public function readLine($length = 1024) + { + if ($this->ensureContent() === false) { + return false; + } + + $line = ''; + while ($this->ensureContent()) { + $char = $this->readByte(); + + if ($char === "\n") { + break; + } + + if ($char === "\r") { + if ($this->getByte() === "\n") { + $this->addOffset(1); + } + break; + } + + $line .= $char; + + if (\strlen($line) >= $length) { + break; + } + } + + return $line; + } + + /** + * Set the offset position in the current buffer. + * + * @param int $offset + */ + public function setOffset($offset) + { + if ($offset > $this->bufferLength || $offset < 0) { + throw new \OutOfRangeException( + \sprintf('Offset (%s) out of range (length: %s)', $offset, $this->bufferLength) + ); + } + + $this->offset = (int) $offset; + } + + /** + * Returns the current offset in the current buffer. + * + * @return int + */ + public function getOffset() + { + return $this->offset; + } + + /** + * Add an offset to the current offset. + * + * @param int $offset + */ + public function addOffset($offset) + { + $this->setOffset($this->offset + $offset); + } + + /** + * Make sure that there is at least one character beyond the current offset in the buffer. + * + * @return bool + */ + public function ensureContent() + { + while ($this->offset >= $this->bufferLength) { + if (!$this->increaseLength()) { + return false; + } + } + return true; + } + + /** + * Returns the stream. + * + * @return resource + */ + public function getStream() + { + return $this->stream; + } + + /** + * Gets the total available length. + * + * @return int + */ + public function getTotalLength() + { + if ($this->totalLength === null) { + $stat = \fstat($this->stream); + $this->totalLength = $stat['size']; + } + + return $this->totalLength; + } + + /** + * Resets the buffer to a position and re-read the buffer with the given length. + * + * If the $pos parameter is negative the start buffer position will be the $pos'th position from + * the end of the file. + * + * If the $pos parameter is negative and the absolute value is bigger then the totalLength of + * the file $pos will set to zero. + * + * @param int|null $pos Start position of the new buffer + * @param int $length Length of the new buffer. Mustn't be negative + */ + public function reset($pos = 0, $length = 200) + { + if ($pos === null) { + $pos = $this->position + $this->offset; + } elseif ($pos < 0) { + $pos = \max(0, $this->getTotalLength() + $pos); + } + + \fseek($this->stream, $pos); + + $this->position = $pos; + $this->buffer = $length > 0 ? \fread($this->stream, $length) : ''; + $this->bufferLength = \strlen($this->buffer); + $this->offset = 0; + + // 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->bufferLength = \strlen($this->buffer); + } + } + + /** + * Ensures bytes in the buffer with a specific length and location in the file. + * + * @param int $pos + * @param int $length + * @see reset() + */ + public function ensure($pos, $length) + { + if ( + $pos >= $this->position + && $pos < ($this->position + $this->bufferLength) + && ($this->position + $this->bufferLength) >= ($pos + $length) + ) { + $this->offset = $pos - $this->position; + } else { + $this->reset($pos, $length); + } + } + + /** + * Forcefully read more data into the buffer. + * + * @param int $minLength + * @return bool Returns false if the stream reaches the end + */ + public function increaseLength($minLength = 100) + { + $length = \max($minLength, 100); + + if (\feof($this->stream) || $this->getTotalLength() === $this->position + $this->bufferLength) { + return false; + } + + $newLength = $this->bufferLength + $length; + do { + $this->buffer .= \fread($this->stream, $newLength - $this->bufferLength); + $this->bufferLength = \strlen($this->buffer); + } while (($this->bufferLength !== $newLength) && !\feof($this->stream)); + + return true; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Tokenizer.php b/vendor/setasign/fpdi/src/PdfParser/Tokenizer.php new file mode 100644 index 0000000..a3bcd01 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Tokenizer.php @@ -0,0 +1,154 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser; + +/** + * A tokenizer class. + */ +class Tokenizer +{ + /** + * @var StreamReader + */ + protected $streamReader; + + /** + * A token stack. + * + * @var string[] + */ + protected $stack = []; + + /** + * Tokenizer constructor. + * + * @param StreamReader $streamReader + */ + public function __construct(StreamReader $streamReader) + { + $this->streamReader = $streamReader; + } + + /** + * Get the stream reader instance. + * + * @return StreamReader + */ + public function getStreamReader() + { + return $this->streamReader; + } + + /** + * Clear the token stack. + */ + public function clearStack() + { + $this->stack = []; + } + + /** + * Push a token onto the stack. + * + * @param string $token + */ + public function pushStack($token) + { + $this->stack[] = $token; + } + + /** + * Get next token. + * + * @return bool|string + */ + public function getNextToken() + { + $token = \array_pop($this->stack); + if ($token !== null) { + return $token; + } + + if (($byte = $this->streamReader->readByte()) === false) { + return false; + } + + if (\in_array($byte, ["\x20", "\x0A", "\x0D", "\x0C", "\x09", "\x00"], true)) { + if ($this->leapWhiteSpaces() === false) { + return false; + } + $byte = $this->streamReader->readByte(); + } + + switch ($byte) { + case '/': + case '[': + case ']': + case '(': + case ')': + case '{': + case '}': + case '<': + case '>': + return $byte; + case '%': + $this->streamReader->readLine(); + return $this->getNextToken(); + } + + /* This way is faster than checking single bytes. + */ + $bufferOffset = $this->streamReader->getOffset(); + do { + $lastBuffer = $this->streamReader->getBuffer(false); + $pos = \strcspn( + $lastBuffer, + "\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%", + $bufferOffset + ); + } 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() + ) + ); + + $result = \substr($lastBuffer, $bufferOffset - 1, $pos + 1); + $this->streamReader->setOffset($bufferOffset + $pos); + + return $result; + } + + /** + * Leap white spaces. + * + * @return boolean + */ + public function leapWhiteSpaces() + { + do { + if (!$this->streamReader->ensureContent()) { + return false; + } + + $buffer = $this->streamReader->getBuffer(false); + $matches = \strspn($buffer, "\x20\x0A\x0C\x0D\x09\x00", $this->streamReader->getOffset()); + if ($matches > 0) { + $this->streamReader->addOffset($matches); + } + } while ($this->streamReader->getOffset() >= $this->streamReader->getBufferLength()); + + return true; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfArray.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfArray.php new file mode 100644 index 0000000..5d0bbbd --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfArray.php @@ -0,0 +1,85 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\Tokenizer; + +/** + * Class representing a PDF array object + * + * @property array $value The value of the PDF type. + */ +class PdfArray extends PdfType +{ + /** + * Parses an array of the passed tokenizer and parser. + * + * @param Tokenizer $tokenizer + * @param PdfParser $parser + * @return bool|self + * @throws PdfTypeException + */ + public static function parse(Tokenizer $tokenizer, PdfParser $parser) + { + $result = []; + + // Recurse into this function until we reach the end of the array. + while (($token = $tokenizer->getNextToken()) !== ']') { + if ($token === false || ($value = $parser->readValue($token)) === false) { + return false; + } + + $result[] = $value; + } + + $v = new self(); + $v->value = $result; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param PdfType[] $values + * @return self + */ + public static function create(array $values = []) + { + $v = new self(); + $v->value = $values; + + return $v; + } + + /** + * Ensures that the passed array is a PdfArray instance with a (optional) specific size. + * + * @param mixed $array + * @param null|int $size + * @return self + * @throws PdfTypeException + */ + public static function ensure($array, $size = null) + { + $result = PdfType::ensureType(self::class, $array, 'Array value expected.'); + + if ($size !== null && \count($array->value) !== $size) { + throw new PdfTypeException( + \sprintf('Array with %s entries expected.', $size), + PdfTypeException::INVALID_DATA_SIZE + ); + } + + return $result; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfBoolean.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfBoolean.php new file mode 100644 index 0000000..ba4233a --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfBoolean.php @@ -0,0 +1,42 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +/** + * Class representing a boolean PDF object + */ +class PdfBoolean extends PdfType +{ + /** + * Helper method to create an instance. + * + * @param bool $value + * @return self + */ + public static function create($value) + { + $v = new self(); + $v->value = (bool) $value; + return $v; + } + + /** + * Ensures that the passed value is a PdfBoolean instance. + * + * @param mixed $value + * @return self + * @throws PdfTypeException + */ + public static function ensure($value) + { + return PdfType::ensureType(self::class, $value, 'Boolean value expected.'); + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfDictionary.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfDictionary.php new file mode 100644 index 0000000..2818842 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfDictionary.php @@ -0,0 +1,134 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\StreamReader; +use setasign\Fpdi\PdfParser\Tokenizer; + +/** + * Class representing a PDF dictionary object + */ +class PdfDictionary extends PdfType +{ + /** + * Parses a dictionary of the passed tokenizer, stream-reader and parser. + * + * @param Tokenizer $tokenizer + * @param StreamReader $streamReader + * @param PdfParser $parser + * @return bool|self + * @throws PdfTypeException + */ + public static function parse(Tokenizer $tokenizer, StreamReader $streamReader, PdfParser $parser) + { + $entries = []; + + while (true) { + $token = $tokenizer->getNextToken(); + if ($token === '>' && $streamReader->getByte() === '>') { + $streamReader->addOffset(1); + break; + } + + $key = $parser->readValue($token); + if ($key === false) { + return false; + } + + // ensure the first value to be a Name object + if (!($key instanceof PdfName)) { + $lastToken = null; + // ignore all other entries and search for the closing brackets + while (($token = $tokenizer->getNextToken()) !== '>' && $token !== false && $lastToken !== '>') { + $lastToken = $token; + } + + if ($token === false) { + return false; + } + + break; + } + + + $value = $parser->readValue(); + if ($value === false) { + return false; + } + + if ($value instanceof PdfNull) { + continue; + } + + // catch missing value + if ($value instanceof PdfToken && $value->value === '>' && $streamReader->getByte() === '>') { + $streamReader->addOffset(1); + break; + } + + $entries[$key->value] = $value; + } + + $v = new self(); + $v->value = $entries; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param PdfType[] $entries The keys are the name entries of the dictionary. + * @return self + */ + public static function create(array $entries = []) + { + $v = new self(); + $v->value = $entries; + + return $v; + } + + /** + * Get a value by its key from a dictionary or a default value. + * + * @param mixed $dictionary + * @param string $key + * @param PdfType|null $default + * @return PdfNull|PdfType + * @throws PdfTypeException + */ + public static function get($dictionary, $key, PdfType $default = null) + { + $dictionary = self::ensure($dictionary); + + if (isset($dictionary->value[$key])) { + return $dictionary->value[$key]; + } + + return $default === null + ? new PdfNull() + : $default; + } + + /** + * Ensures that the passed value is a PdfDictionary instance. + * + * @param mixed $dictionary + * @return self + * @throws PdfTypeException + */ + public static function ensure($dictionary) + { + return PdfType::ensureType(self::class, $dictionary, 'Dictionary value expected.'); + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfHexString.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfHexString.php new file mode 100644 index 0000000..0084ada --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfHexString.php @@ -0,0 +1,77 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +use setasign\Fpdi\PdfParser\StreamReader; + +/** + * Class representing a hexadecimal encoded PDF string object + */ +class PdfHexString extends PdfType +{ + /** + * Parses a hexadecimal string object from the stream reader. + * + * @param StreamReader $streamReader + * @return bool|self + */ + public static function parse(StreamReader $streamReader) + { + $bufferOffset = $streamReader->getOffset(); + + while (true) { + $buffer = $streamReader->getBuffer(false); + $pos = \strpos($buffer, '>', $bufferOffset); + if ($pos === false) { + if (!$streamReader->increaseLength()) { + return false; + } + continue; + } + + break; + } + + $result = \substr($buffer, $bufferOffset, $pos - $bufferOffset); + $streamReader->setOffset($pos + 1); + + $v = new self(); + $v->value = $result; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param string $string The hex encoded string. + * @return self + */ + public static function create($string) + { + $v = new self(); + $v->value = $string; + + return $v; + } + + /** + * Ensures that the passed value is a PdfHexString instance. + * + * @param mixed $hexString + * @return self + * @throws PdfTypeException + */ + public static function ensure($hexString) + { + return PdfType::ensureType(self::class, $hexString, 'Hex string value expected.'); + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObject.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObject.php new file mode 100644 index 0000000..15786d0 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObject.php @@ -0,0 +1,103 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\StreamReader; +use setasign\Fpdi\PdfParser\Tokenizer; + +/** + * Class representing an indirect object + */ +class PdfIndirectObject extends PdfType +{ + /** + * Parses an indirect object from a tokenizer, parser and stream-reader. + * + * @param int $objectNumberToken + * @param int $objectGenerationNumberToken + * @param PdfParser $parser + * @param Tokenizer $tokenizer + * @param StreamReader $reader + * @return bool|self + * @throws PdfTypeException + */ + public static function parse( + $objectNumberToken, + $objectGenerationNumberToken, + PdfParser $parser, + Tokenizer $tokenizer, + StreamReader $reader + ) { + $value = $parser->readValue(); + if ($value === false) { + return false; + } + + $nextToken = $tokenizer->getNextToken(); + if ($nextToken === 'stream') { + $value = PdfStream::parse($value, $reader, $parser); + } elseif ($nextToken !== false) { + $tokenizer->pushStack($nextToken); + } + + $v = new self(); + $v->objectNumber = (int) $objectNumberToken; + $v->generationNumber = (int) $objectGenerationNumberToken; + $v->value = $value; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param int $objectNumber + * @param int $generationNumber + * @param PdfType $value + * @return self + */ + public static function create($objectNumber, $generationNumber, PdfType $value) + { + $v = new self(); + $v->objectNumber = (int) $objectNumber; + $v->generationNumber = (int) $generationNumber; + $v->value = $value; + + return $v; + } + + /** + * Ensures that the passed value is a PdfIndirectObject instance. + * + * @param mixed $indirectObject + * @return self + * @throws PdfTypeException + */ + public static function ensure($indirectObject) + { + return PdfType::ensureType(self::class, $indirectObject, 'Indirect object expected.'); + } + + /** + * The object number. + * + * @var int + */ + public $objectNumber; + + /** + * The generation number. + * + * @var int + */ + public $generationNumber; +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObjectReference.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObjectReference.php new file mode 100644 index 0000000..2725d0c --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfIndirectObjectReference.php @@ -0,0 +1,52 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +/** + * Class representing an indirect object reference + */ +class PdfIndirectObjectReference extends PdfType +{ + /** + * Helper method to create an instance. + * + * @param int $objectNumber + * @param int $generationNumber + * @return self + */ + public static function create($objectNumber, $generationNumber) + { + $v = new self(); + $v->value = (int) $objectNumber; + $v->generationNumber = (int) $generationNumber; + + return $v; + } + + /** + * Ensures that the passed value is a PdfIndirectObject instance. + * + * @param mixed $value + * @return self + * @throws PdfTypeException + */ + public static function ensure($value) + { + return PdfType::ensureType(self::class, $value, 'Indirect reference value expected.'); + } + + /** + * The generation number. + * + * @var int + */ + public $generationNumber; +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfName.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfName.php new file mode 100644 index 0000000..194a13f --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfName.php @@ -0,0 +1,82 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +use setasign\Fpdi\PdfParser\StreamReader; +use setasign\Fpdi\PdfParser\Tokenizer; + +/** + * Class representing a PDF name object + */ +class PdfName extends PdfType +{ + /** + * Parses a name object from the passed tokenizer and stream-reader. + * + * @param Tokenizer $tokenizer + * @param StreamReader $streamReader + * @return self + */ + public static function parse(Tokenizer $tokenizer, StreamReader $streamReader) + { + $v = new self(); + if (\strspn($streamReader->getByte(), "\x00\x09\x0A\x0C\x0D\x20()<>[]{}/%") === 0) { + $v->value = (string) $tokenizer->getNextToken(); + return $v; + } + + $v->value = ''; + return $v; + } + + /** + * Unescapes a name string. + * + * @param string $value + * @return string + */ + public static function unescape($value) + { + if (strpos($value, '#') === false) { + return $value; + } + + return preg_replace_callback('/#([a-fA-F\d]{2})/', function ($matches) { + return chr(hexdec($matches[1])); + }, $value); + } + + /** + * Helper method to create an instance. + * + * @param string $string + * @return self + */ + public static function create($string) + { + $v = new self(); + $v->value = $string; + + return $v; + } + + /** + * Ensures that the passed value is a PdfName instance. + * + * @param mixed $name + * @return self + * @throws PdfTypeException + */ + public static function ensure($name) + { + return PdfType::ensureType(self::class, $name, 'Name value expected.'); + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfNull.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfNull.php new file mode 100644 index 0000000..0c4c108 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfNull.php @@ -0,0 +1,19 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +/** + * Class representing a PDF null object + */ +class PdfNull extends PdfType +{ + // empty body +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfNumeric.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfNumeric.php new file mode 100644 index 0000000..9de912b --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfNumeric.php @@ -0,0 +1,43 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +/** + * Class representing a numeric PDF object + */ +class PdfNumeric extends PdfType +{ + /** + * Helper method to create an instance. + * + * @param int|float $value + * @return PdfNumeric + */ + public static function create($value) + { + $v = new self(); + $v->value = $value + 0; + + return $v; + } + + /** + * Ensures that the passed value is a PdfNumeric instance. + * + * @param mixed $value + * @return self + * @throws PdfTypeException + */ + public static function ensure($value) + { + return PdfType::ensureType(self::class, $value, 'Numeric value expected.'); + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfStream.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfStream.php new file mode 100644 index 0000000..6d4c5a8 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfStream.php @@ -0,0 +1,326 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; +use setasign\Fpdi\PdfParser\Filter\Ascii85; +use setasign\Fpdi\PdfParser\Filter\AsciiHex; +use setasign\Fpdi\PdfParser\Filter\FilterException; +use setasign\Fpdi\PdfParser\Filter\Flate; +use setasign\Fpdi\PdfParser\Filter\Lzw; +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\PdfParserException; +use setasign\Fpdi\PdfParser\StreamReader; +use setasign\FpdiPdfParser\PdfParser\Filter\Predictor; + +/** + * Class representing a PDF stream object + */ +class PdfStream extends PdfType +{ + /** + * Parses a stream from a stream reader. + * + * @param PdfDictionary $dictionary + * @param StreamReader $reader + * @param PdfParser $parser Optional to keep backwards compatibility + * @return self + * @throws PdfTypeException + */ + public static function parse(PdfDictionary $dictionary, StreamReader $reader, PdfParser $parser = null) + { + $v = new self(); + $v->value = $dictionary; + $v->reader = $reader; + $v->parser = $parser; + + $offset = $reader->getOffset(); + + // Find the first "newline" + while (($firstByte = $reader->getByte($offset)) !== false) { + if ($firstByte !== "\n" && $firstByte !== "\r") { + $offset++; + } else { + break; + } + } + + 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++; + } + + if ($sndByte === "\n" && $firstByte !== "\n") { + $offset++; + } + + $reader->setOffset($offset); + // let's only save the byte-offset and read the stream only when needed + $v->stream = $reader->getPosition() + $reader->getOffset(); + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param PdfDictionary $dictionary + * @param string $stream + * @return self + */ + public static function create(PdfDictionary $dictionary, $stream) + { + $v = new self(); + $v->value = $dictionary; + $v->stream = (string) $stream; + + return $v; + } + + /** + * Ensures that the passed value is a PdfStream instance. + * + * @param mixed $stream + * @return self + * @throws PdfTypeException + */ + public static function ensure($stream) + { + return PdfType::ensureType(self::class, $stream, 'Stream value expected.'); + } + + /** + * The stream or its byte-offset position. + * + * @var int|string + */ + protected $stream; + + /** + * The stream reader instance. + * + * @var StreamReader|null + */ + protected $reader; + + /** + * The PDF parser instance. + * + * @var PdfParser + */ + protected $parser; + + /** + * Get the stream data. + * + * @param bool $cache Whether cache the stream data or not. + * @return bool|string + * @throws PdfTypeException + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getStream($cache = false) + { + if (\is_int($this->stream)) { + $length = PdfDictionary::get($this->value, 'Length'); + if ($this->parser !== null) { + $length = PdfType::resolve($length, $this->parser); + } + + if (!($length instanceof PdfNumeric) || $length->value === 0) { + $this->reader->reset($this->stream, 100000); + $buffer = $this->extractStream(); + } else { + $this->reader->reset($this->stream, $length->value); + $buffer = $this->reader->getBuffer(false); + if ($this->parser !== null) { + $this->reader->reset($this->stream + strlen($buffer)); + $this->parser->getTokenizer()->clearStack(); + $token = $this->parser->readValue(); + if ($token === false || !($token instanceof PdfToken) || $token->value !== 'endstream') { + $this->reader->reset($this->stream, 100000); + $buffer = $this->extractStream(); + $this->reader->reset($this->stream + strlen($buffer)); + } + } + } + + if ($cache === false) { + return $buffer; + } + + $this->stream = $buffer; + $this->reader = null; + } + + return $this->stream; + } + + /** + * Extract the stream "manually". + * + * @return string + * @throws PdfTypeException + */ + protected function extractStream() + { + while (true) { + $buffer = $this->reader->getBuffer(false); + $length = \strpos($buffer, 'endstream'); + if ($length === false) { + if (!$this->reader->increaseLength(100000)) { + throw new PdfTypeException('Cannot extract stream.'); + } + continue; + } + break; + } + + $buffer = \substr($buffer, 0, $length); + $lastByte = \substr($buffer, -1); + + /* Check for EOL marker = + * CARRIAGE RETURN (\r) and a LINE FEED (\n) or just a LINE FEED (\n}, + * and not by a CARRIAGE RETURN (\r) alone + */ + if ($lastByte === "\n") { + $buffer = \substr($buffer, 0, -1); + + $lastByte = \substr($buffer, -1); + if ($lastByte === "\r") { + $buffer = \substr($buffer, 0, -1); + } + } + + // There are streams in the wild, which have only white signs in them but need to be parsed manually due + // to a problem encountered before (e.g. Length === 0). We should set them to empty streams to avoid problems + // in further processing (e.g. applying of filters). + if (trim($buffer) === '') { + $buffer = ''; + } + + return $buffer; + } + + /** + * Get the unfiltered stream data. + * + * @return string + * @throws FilterException + * @throws PdfParserException + */ + public function getUnfilteredStream() + { + $stream = $this->getStream(); + $filters = PdfDictionary::get($this->value, 'Filter'); + if ($filters instanceof PdfNull) { + return $stream; + } + + if ($filters instanceof PdfArray) { + $filters = $filters->value; + } else { + $filters = [$filters]; + } + + $decodeParams = PdfDictionary::get($this->value, 'DecodeParms'); + if ($decodeParams instanceof PdfArray) { + $decodeParams = $decodeParams->value; + } else { + $decodeParams = [$decodeParams]; + } + + foreach ($filters as $key => $filter) { + if (!($filter instanceof PdfName)) { + continue; + } + + $decodeParam = null; + if (isset($decodeParams[$key])) { + $decodeParam = ($decodeParams[$key] instanceof PdfDictionary ? $decodeParams[$key] : null); + } + + switch ($filter->value) { + case 'FlateDecode': + case 'Fl': + case 'LZWDecode': + case 'LZW': + if (\strpos($filter->value, 'LZW') === 0) { + $filterObject = new Lzw(); + } else { + $filterObject = new Flate(); + } + + $stream = $filterObject->decode($stream); + + if ($decodeParam instanceof PdfDictionary) { + $predictor = PdfDictionary::get($decodeParam, 'Predictor', PdfNumeric::create(1)); + if ($predictor->value !== 1) { + if (!\class_exists(Predictor::class)) { + throw new PdfParserException( + 'This PDF document makes use of features which are only implemented in the ' . + 'commercial "FPDI PDF-Parser" add-on (see https://www.setasign.com/fpdi-pdf-' . + 'parser).', + PdfParserException::IMPLEMENTED_IN_FPDI_PDF_PARSER + ); + } + + $colors = PdfDictionary::get($decodeParam, 'Colors', PdfNumeric::create(1)); + $bitsPerComponent = PdfDictionary::get( + $decodeParam, + 'BitsPerComponent', + PdfNumeric::create(8) + ); + + $columns = PdfDictionary::get($decodeParam, 'Columns', PdfNumeric::create(1)); + + $filterObject = new Predictor( + $predictor->value, + $colors->value, + $bitsPerComponent->value, + $columns->value + ); + + $stream = $filterObject->decode($stream); + } + } + + break; + case 'ASCII85Decode': + case 'A85': + $filterObject = new Ascii85(); + $stream = $filterObject->decode($stream); + break; + + case 'ASCIIHexDecode': + case 'AHx': + $filterObject = new AsciiHex(); + $stream = $filterObject->decode($stream); + break; + + default: + throw new FilterException( + \sprintf('Unsupported filter "%s".', $filter->value), + FilterException::UNSUPPORTED_FILTER + ); + } + } + + return $stream; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfString.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfString.php new file mode 100644 index 0000000..1636e68 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfString.php @@ -0,0 +1,172 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +use setasign\Fpdi\PdfParser\StreamReader; + +/** + * Class representing a PDF string object + */ +class PdfString extends PdfType +{ + /** + * Parses a string object from the stream reader. + * + * @param StreamReader $streamReader + * @return self + */ + public static function parse(StreamReader $streamReader) + { + $pos = $startPos = $streamReader->getOffset(); + $openBrackets = 1; + do { + $buffer = $streamReader->getBuffer(false); + for ($length = \strlen($buffer); $openBrackets !== 0 && $pos < $length; $pos++) { + switch ($buffer[$pos]) { + case '(': + $openBrackets++; + break; + case ')': + $openBrackets--; + break; + case '\\': + $pos++; + } + } + } while ($openBrackets !== 0 && $streamReader->increaseLength()); + + $result = \substr($buffer, $startPos, $openBrackets + $pos - $startPos - 1); + $streamReader->setOffset($pos); + + $v = new self(); + $v->value = $result; + + return $v; + } + + /** + * Helper method to create an instance. + * + * @param string $value The string needs to be escaped accordingly. + * @return self + */ + public static function create($value) + { + $v = new self(); + $v->value = $value; + + return $v; + } + + /** + * Ensures that the passed value is a PdfString instance. + * + * @param mixed $string + * @return self + * @throws PdfTypeException + */ + public static function ensure($string) + { + return PdfType::ensureType(self::class, $string, 'String value expected.'); + } + + /** + * Unescapes escaped sequences in a PDF string according to the PDF specification. + * + * @param string $s + * @return string + */ + public static function unescape($s) + { + $out = ''; + /** @noinspection ForeachInvariantsInspection */ + for ($count = 0, $n = \strlen($s); $count < $n; $count++) { + if ($s[$count] !== '\\') { + $out .= $s[$count]; + } else { + // A backslash at the end of the string - ignore it + if ($count === ($n - 1)) { + break; + } + + switch ($s[++$count]) { + case ')': + case '(': + case '\\': + $out .= $s[$count]; + break; + + case 'f': + $out .= "\x0C"; + break; + + case 'b': + $out .= "\x08"; + break; + + case 't': + $out .= "\x09"; + break; + + case 'r': + $out .= "\x0D"; + break; + + case 'n': + $out .= "\x0A"; + break; + + case "\r": + if ($count !== $n - 1 && $s[$count + 1] === "\n") { + $count++; + } + break; + + case "\n": + break; + + default: + $actualChar = \ord($s[$count]); + // ascii 48 = number 0 + // ascii 57 = number 9 + if ($actualChar >= 48 && $actualChar <= 57) { + $oct = '' . $s[$count]; + + /** @noinspection NotOptimalIfConditionsInspection */ + 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 + ) { + $oct .= $s[++$count]; + } + } + + $out .= \chr(\octdec($oct)); + } else { + // If the character is not one of those defined, the backslash is ignored + $out .= $s[$count]; + } + } + } + } + return $out; + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfToken.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfToken.php new file mode 100644 index 0000000..012b9fd --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfToken.php @@ -0,0 +1,43 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +/** + * Class representing PDF token object + */ +class PdfToken extends PdfType +{ + /** + * Helper method to create an instance. + * + * @param string $token + * @return self + */ + public static function create($token) + { + $v = new self(); + $v->value = $token; + + return $v; + } + + /** + * Ensures that the passed value is a PdfToken instance. + * + * @param mixed $token + * @return self + * @throws PdfTypeException + */ + public static function ensure($token) + { + return PdfType::ensureType(self::class, $token, 'Token value expected.'); + } +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfType.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfType.php new file mode 100644 index 0000000..7672dcd --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfType.php @@ -0,0 +1,78 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\PdfParserException; + +/** + * A class defining a PDF data type + */ +class PdfType +{ + /** + * Resolves a PdfType value to its value. + * + * This method is used to evaluate indirect and direct object references until a final value is reached. + * + * @param PdfType $value + * @param PdfParser $parser + * @param bool $stopAtIndirectObject + * @return PdfType + * @throws CrossReferenceException + * @throws PdfParserException + */ + public static function resolve(PdfType $value, PdfParser $parser, $stopAtIndirectObject = false) + { + 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); + } + + return $value; + } + + /** + * Ensure that a value is an instance of a specific PDF type. + * + * @param string $type + * @param PdfType $value + * @param string $errorMessage + * @return mixed + * @throws PdfTypeException + */ + protected static function ensureType($type, $value, $errorMessage) + { + if (!($value instanceof $type)) { + throw new PdfTypeException( + $errorMessage, + PdfTypeException::INVALID_DATA_TYPE + ); + } + + return $value; + } + + /** + * The value of the PDF type. + * + * @var mixed + */ + public $value; +} diff --git a/vendor/setasign/fpdi/src/PdfParser/Type/PdfTypeException.php b/vendor/setasign/fpdi/src/PdfParser/Type/PdfTypeException.php new file mode 100644 index 0000000..593d147 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfParser/Type/PdfTypeException.php @@ -0,0 +1,24 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfParser\Type; + +use setasign\Fpdi\PdfParser\PdfParserException; + +/** + * Exception class for pdf type classes + */ +class PdfTypeException extends PdfParserException +{ + /** + * @var int + */ + const NO_NEWLINE_AFTER_STREAM_KEYWORD = 0x0601; +} diff --git a/vendor/setasign/fpdi/src/PdfReader/DataStructure/Rectangle.php b/vendor/setasign/fpdi/src/PdfReader/DataStructure/Rectangle.php new file mode 100644 index 0000000..9b19ff8 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfReader/DataStructure/Rectangle.php @@ -0,0 +1,173 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfReader\DataStructure; + +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\PdfParserException; +use setasign\Fpdi\PdfParser\Type\PdfArray; +use setasign\Fpdi\PdfParser\Type\PdfNumeric; +use setasign\Fpdi\PdfParser\Type\PdfType; +use setasign\Fpdi\PdfParser\Type\PdfTypeException; + +/** + * Class representing a rectangle + */ +class Rectangle +{ + /** + * @var int|float + */ + protected $llx; + + /** + * @var int|float + */ + protected $lly; + + /** + * @var int|float + */ + protected $urx; + + /** + * @var int|float + */ + protected $ury; + + /** + * Create a rectangle instance by a PdfArray. + * + * @param PdfArray|mixed $array + * @param PdfParser $parser + * @return Rectangle + * @throws PdfTypeException + * @throws CrossReferenceException + * @throws PdfParserException + */ + public static function byPdfArray($array, PdfParser $parser) + { + $array = PdfArray::ensure(PdfType::resolve($array, $parser), 4)->value; + $ax = PdfNumeric::ensure(PdfType::resolve($array[0], $parser))->value; + $ay = PdfNumeric::ensure(PdfType::resolve($array[1], $parser))->value; + $bx = PdfNumeric::ensure(PdfType::resolve($array[2], $parser))->value; + $by = PdfNumeric::ensure(PdfType::resolve($array[3], $parser))->value; + + return new self($ax, $ay, $bx, $by); + } + + /** + * Rectangle constructor. + * + * @param float|int $ax + * @param float|int $ay + * @param float|int $bx + * @param float|int $by + */ + public function __construct($ax, $ay, $bx, $by) + { + $this->llx = \min($ax, $bx); + $this->lly = \min($ay, $by); + $this->urx = \max($ax, $bx); + $this->ury = \max($ay, $by); + } + + /** + * Get the width of the rectangle. + * + * @return float|int + */ + public function getWidth() + { + return $this->urx - $this->llx; + } + + /** + * Get the height of the rectangle. + * + * @return float|int + */ + public function getHeight() + { + return $this->ury - $this->lly; + } + + /** + * Get the lower left abscissa. + * + * @return float|int + */ + public function getLlx() + { + return $this->llx; + } + + /** + * Get the lower left ordinate. + * + * @return float|int + */ + public function getLly() + { + return $this->lly; + } + + /** + * Get the upper right abscissa. + * + * @return float|int + */ + public function getUrx() + { + return $this->urx; + } + + /** + * Get the upper right ordinate. + * + * @return float|int + */ + public function getUry() + { + return $this->ury; + } + + /** + * Get the rectangle as an array. + * + * @return array + */ + public function toArray() + { + return [ + $this->llx, + $this->lly, + $this->urx, + $this->ury + ]; + } + + /** + * Get the rectangle as a PdfArray. + * + * @return PdfArray + */ + public function toPdfArray() + { + $array = new PdfArray(); + $array->value[] = PdfNumeric::create($this->llx); + $array->value[] = PdfNumeric::create($this->lly); + $array->value[] = PdfNumeric::create($this->urx); + $array->value[] = PdfNumeric::create($this->ury); + + return $array; + } +} diff --git a/vendor/setasign/fpdi/src/PdfReader/Page.php b/vendor/setasign/fpdi/src/PdfReader/Page.php new file mode 100644 index 0000000..b207c79 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfReader/Page.php @@ -0,0 +1,271 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfReader; + +use setasign\Fpdi\PdfParser\Filter\FilterException; +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\PdfParserException; +use setasign\Fpdi\PdfParser\Type\PdfArray; +use setasign\Fpdi\PdfParser\Type\PdfDictionary; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; +use setasign\Fpdi\PdfParser\Type\PdfNull; +use setasign\Fpdi\PdfParser\Type\PdfNumeric; +use setasign\Fpdi\PdfParser\Type\PdfStream; +use setasign\Fpdi\PdfParser\Type\PdfType; +use setasign\Fpdi\PdfParser\Type\PdfTypeException; +use setasign\Fpdi\PdfReader\DataStructure\Rectangle; +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; + +/** + * Class representing a page of a PDF document + */ +class Page +{ + /** + * @var PdfIndirectObject + */ + protected $pageObject; + + /** + * @var PdfDictionary + */ + protected $pageDictionary; + + /** + * @var PdfParser + */ + protected $parser; + + /** + * Inherited attributes + * + * @var null|array + */ + protected $inheritedAttributes; + + /** + * Page constructor. + * + * @param PdfIndirectObject $page + * @param PdfParser $parser + */ + public function __construct(PdfIndirectObject $page, PdfParser $parser) + { + $this->pageObject = $page; + $this->parser = $parser; + } + + /** + * Get the indirect object of this page. + * + * @return PdfIndirectObject + */ + public function getPageObject() + { + return $this->pageObject; + } + + /** + * Get the dictionary of this page. + * + * @return PdfDictionary + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + */ + public function getPageDictionary() + { + if (null === $this->pageDictionary) { + $this->pageDictionary = PdfDictionary::ensure(PdfType::resolve($this->getPageObject(), $this->parser)); + } + + return $this->pageDictionary; + } + + /** + * Get a page attribute. + * + * @param string $name + * @param bool $inherited + * @return PdfType|null + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + */ + public function getAttribute($name, $inherited = true) + { + $dict = $this->getPageDictionary(); + + if (isset($dict->value[$name])) { + return $dict->value[$name]; + } + + $inheritedKeys = ['Resources', 'MediaBox', 'CropBox', 'Rotate']; + if ($inherited && \in_array($name, $inheritedKeys, true)) { + if ($this->inheritedAttributes === null) { + $this->inheritedAttributes = []; + $inheritedKeys = \array_filter($inheritedKeys, function ($key) use ($dict) { + return !isset($dict->value[$key]); + }); + + if (\count($inheritedKeys) > 0) { + $parentDict = PdfType::resolve(PdfDictionary::get($dict, 'Parent'), $this->parser); + while ($parentDict instanceof PdfDictionary) { + foreach ($inheritedKeys as $index => $key) { + if (isset($parentDict->value[$key])) { + $this->inheritedAttributes[$key] = $parentDict->value[$key]; + unset($inheritedKeys[$index]); + } + } + + /** @noinspection NotOptimalIfConditionsInspection */ + if (isset($parentDict->value['Parent']) && \count($inheritedKeys) > 0) { + $parentDict = PdfType::resolve(PdfDictionary::get($parentDict, 'Parent'), $this->parser); + } else { + break; + } + } + } + } + + if (isset($this->inheritedAttributes[$name])) { + return $this->inheritedAttributes[$name]; + } + } + + return null; + } + + /** + * Get the rotation value. + * + * @return int + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + */ + public function getRotation() + { + $rotation = $this->getAttribute('Rotate'); + if (null === $rotation) { + return 0; + } + + $rotation = PdfNumeric::ensure(PdfType::resolve($rotation, $this->parser))->value % 360; + + if ($rotation < 0) { + $rotation += 360; + } + + return $rotation; + } + + /** + * Get a boundary of this page. + * + * @param string $box + * @param bool $fallback + * @return bool|Rectangle + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + * @see PageBoundaries + */ + public function getBoundary($box = PageBoundaries::CROP_BOX, $fallback = true) + { + $value = $this->getAttribute($box); + + if ($value !== null) { + return Rectangle::byPdfArray($value, $this->parser); + } + + if ($fallback === false) { + return false; + } + + switch ($box) { + case PageBoundaries::BLEED_BOX: + case PageBoundaries::TRIM_BOX: + case PageBoundaries::ART_BOX: + return $this->getBoundary(PageBoundaries::CROP_BOX, true); + case PageBoundaries::CROP_BOX: + return $this->getBoundary(PageBoundaries::MEDIA_BOX, true); + } + + return false; + } + + /** + * Get the width and height of this page. + * + * @param string $box + * @param bool $fallback + * @return array|bool + * @throws PdfParserException + * @throws PdfTypeException + * @throws CrossReferenceException + */ + public function getWidthAndHeight($box = PageBoundaries::CROP_BOX, $fallback = true) + { + $boundary = $this->getBoundary($box, $fallback); + if ($boundary === false) { + return false; + } + + $rotation = $this->getRotation(); + $interchange = ($rotation / 90) % 2; + + return [ + $interchange ? $boundary->getHeight() : $boundary->getWidth(), + $interchange ? $boundary->getWidth() : $boundary->getHeight() + ]; + } + + /** + * Get the raw content stream. + * + * @return string + * @throws PdfReaderException + * @throws PdfTypeException + * @throws FilterException + * @throws PdfParserException + */ + public function getContentStream() + { + $dict = $this->getPageDictionary(); + $contents = PdfType::resolve(PdfDictionary::get($dict, 'Contents'), $this->parser); + if ($contents instanceof PdfNull) { + return ''; + } + + if ($contents instanceof PdfArray) { + $result = []; + foreach ($contents->value as $content) { + $content = PdfType::resolve($content, $this->parser); + if (!($content instanceof PdfStream)) { + continue; + } + $result[] = $content->getUnfilteredStream(); + } + + return \implode("\n", $result); + } + + if ($contents instanceof PdfStream) { + return $contents->getUnfilteredStream(); + } + + throw new PdfReaderException( + 'Array or stream expected.', + PdfReaderException::UNEXPECTED_DATA_TYPE + ); + } +} diff --git a/vendor/setasign/fpdi/src/PdfReader/PageBoundaries.php b/vendor/setasign/fpdi/src/PdfReader/PageBoundaries.php new file mode 100644 index 0000000..9a6a1f3 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfReader/PageBoundaries.php @@ -0,0 +1,94 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfReader; + +/** + * An abstract class for page boundary constants and some helper methods + */ +abstract class PageBoundaries +{ + /** + * MediaBox + * + * The media box defines the boundaries of the physical medium on which the page is to be printed. + * + * @see PDF 32000-1:2008 - 14.11.2 Page Boundaries + * @var string + */ + const MEDIA_BOX = 'MediaBox'; + + /** + * CropBox + * + * The crop box defines the region to which the contents of the page shall be clipped (cropped) when displayed or + * printed. + * + * @see PDF 32000-1:2008 - 14.11.2 Page Boundaries + * @var string + */ + const CROP_BOX = 'CropBox'; + + /** + * BleedBox + * + * The bleed box defines the region to which the contents of the page shall be clipped when output in a + * production environment. + * + * @see PDF 32000-1:2008 - 14.11.2 Page Boundaries + * @var string + */ + const BLEED_BOX = 'BleedBox'; + + /** + * TrimBox + * + * The trim box defines the intended dimensions of the finished page after trimming. + * + * @see PDF 32000-1:2008 - 14.11.2 Page Boundaries + * @var string + */ + const TRIM_BOX = 'TrimBox'; + + /** + * ArtBox + * + * The art box defines the extent of the page’s meaningful content (including potential white space) as intended + * by the page’s creator. + * + * @see PDF 32000-1:2008 - 14.11.2 Page Boundaries + * @var string + */ + const ART_BOX = 'ArtBox'; + + /** + * All page boundaries + * + * @var array + */ + public static $all = array( + self::MEDIA_BOX, + self::CROP_BOX, + self::BLEED_BOX, + self::TRIM_BOX, + self::ART_BOX + ); + + /** + * Checks if a name is a valid page boundary name. + * + * @param string $name The boundary name + * @return boolean A boolean value whether the name is valid or not. + */ + public static function isValidName($name) + { + return \in_array($name, self::$all, true); + } +} diff --git a/vendor/setasign/fpdi/src/PdfReader/PdfReader.php b/vendor/setasign/fpdi/src/PdfReader/PdfReader.php new file mode 100644 index 0000000..3ee8878 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfReader/PdfReader.php @@ -0,0 +1,234 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfReader; + +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; +use setasign\Fpdi\PdfParser\PdfParser; +use setasign\Fpdi\PdfParser\PdfParserException; +use setasign\Fpdi\PdfParser\Type\PdfArray; +use setasign\Fpdi\PdfParser\Type\PdfDictionary; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObjectReference; +use setasign\Fpdi\PdfParser\Type\PdfNumeric; +use setasign\Fpdi\PdfParser\Type\PdfType; +use setasign\Fpdi\PdfParser\Type\PdfTypeException; + +/** + * A PDF reader class + */ +class PdfReader +{ + /** + * @var PdfParser + */ + protected $parser; + + /** + * @var int + */ + protected $pageCount; + + /** + * Indirect objects of resolved pages. + * + * @var PdfIndirectObjectReference[]|PdfIndirectObject[] + */ + protected $pages = []; + + /** + * PdfReader constructor. + * + * @param PdfParser $parser + */ + public function __construct(PdfParser $parser) + { + $this->parser = $parser; + } + + /** + * PdfReader destructor. + */ + public function __destruct() + { + if ($this->parser !== null) { + $this->parser->cleanUp(); + } + } + + /** + * Get the pdf parser instance. + * + * @return PdfParser + */ + public function getParser() + { + return $this->parser; + } + + /** + * Get the PDF version. + * + * @return string + * @throws PdfParserException + */ + public function getPdfVersion() + { + return \implode('.', $this->parser->getPdfVersion()); + } + + /** + * Get the page count. + * + * @return int + * @throws PdfTypeException + * @throws CrossReferenceException + * @throws PdfParserException + */ + public function getPageCount() + { + if ($this->pageCount === null) { + $catalog = $this->parser->getCatalog(); + + $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser); + $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser); + + $this->pageCount = PdfNumeric::ensure($count)->value; + } + + return $this->pageCount; + } + + /** + * Get a page instance. + * + * @param int $pageNumber + * @return Page + * @throws PdfTypeException + * @throws CrossReferenceException + * @throws PdfParserException + * @throws \InvalidArgumentException + */ + public function getPage($pageNumber) + { + if (!\is_numeric($pageNumber)) { + throw new \InvalidArgumentException( + 'Page number needs to be a number.' + ); + } + + if ($pageNumber < 1 || $pageNumber > $this->getPageCount()) { + throw new \InvalidArgumentException( + \sprintf( + 'Page number "%s" out of available page range (1 - %s)', + $pageNumber, + $this->getPageCount() + ) + ); + } + + $this->readPages(); + + $page = $this->pages[$pageNumber - 1]; + + if ($page instanceof PdfIndirectObjectReference) { + $readPages = function ($kids) use (&$readPages) { + $kids = PdfArray::ensure($kids); + + /** @noinspection LoopWhichDoesNotLoopInspection */ + foreach ($kids->value as $reference) { + $reference = PdfIndirectObjectReference::ensure($reference); + $object = $this->parser->getIndirectObject($reference->value); + $type = PdfDictionary::get($object->value, 'Type'); + + if ($type->value === 'Pages') { + return $readPages(PdfDictionary::get($object->value, 'Kids')); + } + + return $object; + } + + throw new PdfReaderException( + 'Kids array cannot be empty.', + PdfReaderException::KIDS_EMPTY + ); + }; + + $page = $this->parser->getIndirectObject($page->value); + $dict = PdfType::resolve($page, $this->parser); + $type = PdfDictionary::get($dict, 'Type'); + + if ($type->value === 'Pages') { + $kids = PdfType::resolve(PdfDictionary::get($dict, 'Kids'), $this->parser); + try { + $page = $this->pages[$pageNumber - 1] = $readPages($kids); + } catch (PdfReaderException $e) { + if ($e->getCode() !== PdfReaderException::KIDS_EMPTY) { + throw $e; + } + + // 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 { + $this->pages[$pageNumber - 1] = $page; + } + } + + return new Page($page, $this->parser); + } + + /** + * Walk the page tree and resolve all indirect objects of all pages. + * + * @param bool $readAll + * @throws CrossReferenceException + * @throws PdfParserException + * @throws PdfTypeException + */ + protected function readPages($readAll = false) + { + if (\count($this->pages) > 0) { + return; + } + + $readPages = function ($kids, $count) use (&$readPages, $readAll) { + $kids = PdfArray::ensure($kids); + $isLeaf = ($count->value === \count($kids->value)); + + foreach ($kids->value as $reference) { + $reference = PdfIndirectObjectReference::ensure($reference); + + if (!$readAll && $isLeaf) { + $this->pages[] = $reference; + continue; + } + + $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')); + } else { + $this->pages[] = $object; + } + } + }; + + $catalog = $this->parser->getCatalog(); + $pages = PdfType::resolve(PdfDictionary::get($catalog, 'Pages'), $this->parser); + $count = PdfType::resolve(PdfDictionary::get($pages, 'Count'), $this->parser); + $kids = PdfType::resolve(PdfDictionary::get($pages, 'Kids'), $this->parser); + $readPages($kids, $count); + } +} diff --git a/vendor/setasign/fpdi/src/PdfReader/PdfReaderException.php b/vendor/setasign/fpdi/src/PdfReader/PdfReaderException.php new file mode 100644 index 0000000..99f7d12 --- /dev/null +++ b/vendor/setasign/fpdi/src/PdfReader/PdfReaderException.php @@ -0,0 +1,34 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\PdfReader; + +use setasign\Fpdi\FpdiException; + +/** + * Exception for the pdf reader class + */ +class PdfReaderException extends FpdiException +{ + /** + * @var int + */ + const KIDS_EMPTY = 0x0101; + + /** + * @var int + */ + const UNEXPECTED_DATA_TYPE = 0x0102; + + /** + * @var int + */ + const MISSING_DATA = 0x0103; +} diff --git a/vendor/setasign/fpdi/src/Tcpdf/Fpdi.php b/vendor/setasign/fpdi/src/Tcpdf/Fpdi.php new file mode 100644 index 0000000..159f97b --- /dev/null +++ b/vendor/setasign/fpdi/src/Tcpdf/Fpdi.php @@ -0,0 +1,270 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\Tcpdf; + +use setasign\Fpdi\FpdiTrait; +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; +use setasign\Fpdi\PdfParser\Filter\AsciiHex; +use setasign\Fpdi\PdfParser\PdfParserException; +use setasign\Fpdi\PdfParser\Type\PdfHexString; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; +use setasign\Fpdi\PdfParser\Type\PdfNull; +use setasign\Fpdi\PdfParser\Type\PdfNumeric; +use setasign\Fpdi\PdfParser\Type\PdfStream; +use setasign\Fpdi\PdfParser\Type\PdfString; +use setasign\Fpdi\PdfParser\Type\PdfType; +use setasign\Fpdi\PdfParser\Type\PdfTypeException; + +/** + * Class Fpdi + * + * This class let you import pages of existing PDF documents into a reusable structure for TCPDF. + * + * @method _encrypt_data(int $n, string $s) string + */ +class Fpdi extends \TCPDF +{ + use FpdiTrait { + writePdfType as fpdiWritePdfType; + useImportedPage as fpdiUseImportedPage; + } + + /** + * FPDI version + * + * @string + */ + const VERSION = '2.3.6'; + + /** + * A counter for template ids. + * + * @var int + */ + protected $templateId = 0; + + /** + * The currently used object number. + * + * @var int|null + */ + protected $currentObjectNumber; + + protected function _enddoc() + { + parent::_enddoc(); + $this->cleanUp(); + } + + /** + * Get the next template id. + * + * @return int + */ + protected function getNextTemplateId() + { + return $this->templateId++; + } + + /** + * Draws an imported page 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 FpdiTrait::getTemplateSize() + */ + public function useTemplate($tpl, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) + { + return $this->useImportedPage($tpl, $x, $y, $width, $height, $adjustPageSize); + } + + /** + * Draws an imported page onto the page. + * + * Give only one of the size parameters (width, height) to calculate the other one automatically in view to the + * aspect ratio. + * + * @param mixed $pageId The page 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 useImportedPage($pageId, $x = 0, $y = 0, $width = null, $height = null, $adjustPageSize = false) + { + $size = $this->fpdiUseImportedPage($pageId, $x, $y, $width, $height, $adjustPageSize); + if ($this->inxobj) { + $importedPage = $this->importedPages[$pageId]; + $this->xobjects[$this->xobjid]['importedPages'][$importedPage['id']] = $pageId; + } + + return $size; + } + + /** + * Get the size of an imported page. + * + * 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) + { + return $this->getImportedPageSize($tpl, $width, $height); + } + + /** + * @inheritdoc + */ + protected function _getxobjectdict() + { + $out = parent::_getxobjectdict(); + + foreach ($this->importedPages as $key => $pageData) { + $out .= '/' . $pageData['id'] . ' ' . $pageData['objectNumber'] . ' 0 R '; + } + + return $out; + } + + /** + * @inheritdoc + * @throws CrossReferenceException + * @throws PdfParserException + */ + protected function _putxobjects() + { + foreach ($this->importedPages as $key => $pageData) { + $this->currentObjectNumber = $this->_newobj(); + $this->importedPages[$key]['objectNumber'] = $this->currentObjectNumber; + $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); + } + } + + // let's prepare resources for imported pages in templates + foreach ($this->xobjects as $xObjectId => $data) { + if (!isset($data['importedPages'])) { + continue; + } + + foreach ($data['importedPages'] as $id => $pageKey) { + $page = $this->importedPages[$pageKey]; + $this->xobjects[$xObjectId]['xobjects'][$id] = ['n' => $page['objectNumber']]; + } + } + + + parent::_putxobjects(); + $this->currentObjectNumber = null; + } + + /** + * Append content to the buffer of TCPDF. + * + * @param string $s + * @param bool $newLine + */ + protected function _put($s, $newLine = true) + { + if ($newLine) { + $this->setBuffer($s . "\n"); + } else { + $this->setBuffer($s); + } + } + + /** + * Begin a new object and return the object number. + * + * @param int|string $objid Object ID (leave empty to get a new ID). + * @return int object number + */ + protected function _newobj($objid = '') + { + $this->_out($this->_getobj($objid)); + return $this->n; + } + + /** + * Writes a PdfType object to the resulting buffer. + * + * @param PdfType $value + * @throws PdfTypeException + */ + protected function writePdfType(PdfType $value) + { + if (!$this->encrypted) { + $this->fpdiWritePdfType($value); + return; + } + + if ($value instanceof PdfString) { + $string = PdfString::unescape($value->value); + $string = $this->_encrypt_data($this->currentObjectNumber, $string); + $value->value = \TCPDF_STATIC::_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 PdfIndirectObject $value + */ + $this->currentObjectNumber = $this->objectMap[$this->currentReaderId][$value->objectNumber]; + } + + $this->fpdiWritePdfType($value); + } +} diff --git a/vendor/setasign/fpdi/src/TcpdfFpdi.php b/vendor/setasign/fpdi/src/TcpdfFpdi.php new file mode 100644 index 0000000..9e6825b --- /dev/null +++ b/vendor/setasign/fpdi/src/TcpdfFpdi.php @@ -0,0 +1,23 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi; + +/** + * Class TcpdfFpdi + * + * This class let you import pages of existing PDF documents into a reusable structure for TCPDF. + * + * @deprecated Class was moved to \setasign\Fpdi\Tcpdf\Fpdi + */ +class TcpdfFpdi extends \setasign\Fpdi\Tcpdf\Fpdi +{ + // this class is moved to \setasign\Fpdi\Tcpdf\Fpdi +} diff --git a/vendor/setasign/fpdi/src/Tfpdf/FpdfTpl.php b/vendor/setasign/fpdi/src/Tfpdf/FpdfTpl.php new file mode 100644 index 0000000..c00d53d --- /dev/null +++ b/vendor/setasign/fpdi/src/Tfpdf/FpdfTpl.php @@ -0,0 +1,23 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\Tfpdf; + +use setasign\Fpdi\FpdfTplTrait; + +/** + * Class FpdfTpl + * + * We need to change some access levels and implement the setPageFormat() method to bring back compatibility to tFPDF. + */ +class FpdfTpl extends \tFPDF +{ + use FpdfTplTrait; +} diff --git a/vendor/setasign/fpdi/src/Tfpdf/Fpdi.php b/vendor/setasign/fpdi/src/Tfpdf/Fpdi.php new file mode 100644 index 0000000..77f340f --- /dev/null +++ b/vendor/setasign/fpdi/src/Tfpdf/Fpdi.php @@ -0,0 +1,154 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +namespace setasign\Fpdi\Tfpdf; + +use setasign\Fpdi\FpdiTrait; +use setasign\Fpdi\PdfParser\CrossReference\CrossReferenceException; +use setasign\Fpdi\PdfParser\PdfParserException; +use setasign\Fpdi\PdfParser\Type\PdfIndirectObject; +use setasign\Fpdi\PdfParser\Type\PdfNull; + +/** + * Class Fpdi + * + * This class let you import pages of existing PDF documents into a reusable structure for tFPDF. + */ +class Fpdi extends FpdfTpl +{ + use FpdiTrait; + + /** + * FPDI version + * + * @string + */ + const VERSION = '2.3.6'; + + public function _enddoc() + { + parent::_enddoc(); + $this->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; + } + } +} diff --git a/vendor/setasign/fpdi/src/autoload.php b/vendor/setasign/fpdi/src/autoload.php new file mode 100644 index 0000000..4c3df9d --- /dev/null +++ b/vendor/setasign/fpdi/src/autoload.php @@ -0,0 +1,21 @@ +<?php + +/** + * This file is part of FPDI + * + * @package setasign\Fpdi + * @copyright Copyright (c) 2020 Setasign GmbH & Co. KG (https://www.setasign.com) + * @license http://opensource.org/licenses/mit-license The MIT License + */ + +spl_autoload_register(static function ($class) { + if (strpos($class, 'setasign\Fpdi\\') === 0) { + $filename = str_replace('\\', DIRECTORY_SEPARATOR, substr($class, 14)) . '.php'; + $fullpath = __DIR__ . DIRECTORY_SEPARATOR . $filename; + + if (is_file($fullpath)) { + /** @noinspection PhpIncludeInspection */ + require_once $fullpath; + } + } +}); diff --git a/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..d402046 --- /dev/null +++ b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,21 @@ +--- +name: Bug report +about: Use this if you believe there is a bug in this repo +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +Please provide a clear and concise description of the suspected issue. + +**How to reproduce** +If possible, provide information - possibly including code snippets - on how to reproduce the issue. + +**Logs** +If possible, provide logs that indicate the issue. See https://github.com/Textalk/websocket-php/blob/master/docs/Examples.md#echo-logger on how to use the EchoLog. + +**Versions** +* Version of this library +* PHP version diff --git a/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..ce777f6 --- /dev/null +++ b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,14 @@ +--- +name: Feature request +about: Suggest an idea for this library +title: '' +labels: feature request +assignees: '' + +--- + +**Is it within the scope of this library?** +Consider and describe why the feature would be beneficial in this library, and not implemented as a separate project using this as a dependency. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. diff --git a/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md new file mode 100644 index 0000000..fe5cc8d --- /dev/null +++ b/vendor/textalk/websocket/.github/ISSUE_TEMPLATE/other-issue.md @@ -0,0 +1,10 @@ +--- +name: Other issue +about: Use this for other issues +title: '' +labels: '' +assignees: '' + +--- + +**Describe your issue** diff --git a/vendor/textalk/websocket/.github/workflows/acceptance.yml b/vendor/textalk/websocket/.github/workflows/acceptance.yml new file mode 100644 index 0000000..7bab97c --- /dev/null +++ b/vendor/textalk/websocket/.github/workflows/acceptance.yml @@ -0,0 +1,97 @@ +name: Acceptance + +on: [push, pull_request] + +jobs: + test-7-2: + runs-on: ubuntu-latest + name: Test PHP 7.2 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 7.2 + uses: shivammathur/setup-php@v2 + with: + php-version: '7.2' + - name: Composer + run: make install + - name: Test + run: make test + + test-7-3: + runs-on: ubuntu-latest + name: Test PHP 7.3 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 7.3 + uses: shivammathur/setup-php@v2 + with: + php-version: '7.3' + - name: Composer + run: make install + - name: Test + run: make test + + test-7-4: + runs-on: ubuntu-latest + name: Test PHP 7.4 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 7.4 + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + - name: Composer + run: make install + - name: Test + run: make test + + test-8-0: + runs-on: ubuntu-latest + name: Test PHP 8.0 + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 8.0 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + - name: Composer + run: make install + - name: Test + run: make test + + cs-check: + runs-on: ubuntu-latest + name: Code standard + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 8.0 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + - name: Composer + run: make install + - name: Code standard + run: make cs-check + + coverage: + runs-on: ubuntu-latest + name: Code coverage + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Set up PHP 8.0 + uses: shivammathur/setup-php@v2 + with: + php-version: '8.0' + extensions: xdebug + - name: Composer + run: make install + - name: Code coverage + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: make coverage diff --git a/vendor/textalk/websocket/.gitignore b/vendor/textalk/websocket/.gitignore new file mode 100644 index 0000000..379ab4b --- /dev/null +++ b/vendor/textalk/websocket/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +.phpunit.result.cache +build/ +composer.lock +composer.phar +vendor/
\ No newline at end of file diff --git a/vendor/textalk/websocket/COPYING.md b/vendor/textalk/websocket/COPYING.md new file mode 100644 index 0000000..ba96480 --- /dev/null +++ b/vendor/textalk/websocket/COPYING.md @@ -0,0 +1,16 @@ +# Websocket: License + +Websocket PHP is free software released under the following license: + +[ISC License](http://en.wikipedia.org/wiki/ISC_license) + +Permission to use, copy, modify, and/or distribute this software for any purpose with or without +fee is hereby granted, provided that the above copyright notice and this permission notice appear +in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF +THIS SOFTWARE. diff --git a/vendor/textalk/websocket/Makefile b/vendor/textalk/websocket/Makefile new file mode 100644 index 0000000..930a9ed --- /dev/null +++ b/vendor/textalk/websocket/Makefile @@ -0,0 +1,32 @@ +install: composer.phar + ./composer.phar install + +update: composer.phar + ./composer.phar self-update + ./composer.phar update + +test: composer.lock + ./vendor/bin/phpunit + +cs-check: composer.lock + ./vendor/bin/phpcs --standard=codestandard.xml lib tests examples + +coverage: composer.lock build + XDEBUG_MODE=coverage ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml + ./vendor/bin/php-coveralls -v + +composer.phar: + curl -s http://getcomposer.org/installer | php + +composer.lock: composer.phar + ./composer.phar --no-interaction install + +vendor/bin/phpunit: install + +build: + mkdir build + +clean: + rm composer.phar + rm -r vendor + rm -r build diff --git a/vendor/textalk/websocket/README.md b/vendor/textalk/websocket/README.md new file mode 100644 index 0000000..921efef --- /dev/null +++ b/vendor/textalk/websocket/README.md @@ -0,0 +1,67 @@ +# Websocket Client and Server for PHP + +[![Build Status](https://github.com/Textalk/websocket-php/actions/workflows/acceptance.yml/badge.svg)](https://github.com/Textalk/websocket-php/actions) +[![Coverage Status](https://coveralls.io/repos/github/Textalk/websocket-php/badge.svg?branch=master)](https://coveralls.io/github/Textalk/websocket-php) + +This library contains WebSocket client and server for PHP. + +The client and server provides methods for reading and writing to WebSocket streams. +It does not include convenience operations such as listeners and implicit error handling. + +## Documentation + +- [Client](docs/Client.md) +- [Server](docs/Server.md) +- [Message](docs/Message.md) +- [Examples](docs/Examples.md) +- [Changelog](docs/Changelog.md) +- [Contributing](docs/Contributing.md) + +## Installing + +Preferred way to install is with [Composer](https://getcomposer.org/). +``` +composer require textalk/websocket +``` + +* Current version support PHP versions `^7.2|8.0`. +* For PHP `7.1` support use version `1.4`. +* For PHP `^5.4` and `7.0` support use version `1.3`. + +## Client + +The [client](docs/Client.md) can read and write on a WebSocket stream. +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); +$client->text("Hello WebSocket.org!"); +echo $client->receive(); +$client->close(); +``` + +## Server + +The library contains a rudimentary single stream/single thread [server](docs/Server.md). +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +Note that it does **not** support threading or automatic association ot continuous client requests. +If you require this kind of server behavior, you need to build it on top of provided server implementation. + +```php +$server = new WebSocket\Server(); +$server->accept(); +$message = $server->receive(); +$server->text($message); +$server->close(); +``` + +### License and Contributors + +[ISC License](COPYING.md) + +Fredrik Liljegren, Armen Baghumian Sankbarani, Ruslan Bekenev, +Joshua Thijssen, Simon Lipp, Quentin Bellus, Patrick McCarren, swmcdonnell, +Ignas Bernotas, Mark Herhold, Andreas Palm, Sören Jensen, pmaasz, Alexey Stavrov, +Michael Slezak, Pierre Seznec, rmeisler, Nickolay V. Shmyrev, Christoph Kempen, +Marc Roberts, Antonio Mora, Simon Podlipsky. diff --git a/vendor/textalk/websocket/codestandard.xml b/vendor/textalk/websocket/codestandard.xml new file mode 100644 index 0000000..bb1cd26 --- /dev/null +++ b/vendor/textalk/websocket/codestandard.xml @@ -0,0 +1,10 @@ +<?xml version="1.0" encoding="UTF-8"?> + +<ruleset name="PSR with exception"> + <arg name="encoding" value="UTF-8"/> + <arg name="report" value="full"/> + <arg name="colors"/> + + <rule ref="PSR1"/> + <rule ref="PSR12"/> +</ruleset>
\ No newline at end of file diff --git a/vendor/textalk/websocket/composer.json b/vendor/textalk/websocket/composer.json new file mode 100644 index 0000000..9bc0dcc --- /dev/null +++ b/vendor/textalk/websocket/composer.json @@ -0,0 +1,34 @@ +{ + "name": "textalk/websocket", + "description": "WebSocket client and server", + "license": "ISC", + "type": "library", + "authors": [ + { + "name": "Fredrik Liljegren" + }, + { + "name": "Sören Jensen", + "email": "soren@abicart.se" + } + ], + "autoload": { + "psr-4": { + "WebSocket\\": "lib" + } + }, + "autoload-dev": { + "psr-4": { + "WebSocket\\": "tests/mock" + } + }, + "require": { + "php": "^7.2 | ^8.0", + "psr/log": "^1 | ^2 | ^3" + }, + "require-dev": { + "phpunit/phpunit": "^8.0|^9.0", + "php-coveralls/php-coveralls": "^2.0", + "squizlabs/php_codesniffer": "^3.5" + } +} diff --git a/vendor/textalk/websocket/docs/Changelog.md b/vendor/textalk/websocket/docs/Changelog.md new file mode 100644 index 0000000..6a45453 --- /dev/null +++ b/vendor/textalk/websocket/docs/Changelog.md @@ -0,0 +1,130 @@ +[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • Changelog • [Contributing](Contributing.md) + +# Websocket: Changelog + +## `v1.5` + + > PHP version `^7.2|^8.0` + +### `1.5.5` + + * Support for psr/log v2 and v3 (@simPod) + * GitHub Actions replaces Travis (@sirn-se) + +### `1.5.4` + + * Keep open connection on read timeout (@marcroberts) + +### `1.5.3` + + * Fix for persistent connection (@sirn-se) + +### `1.5.2` + + * Fix for getName() method (@sirn-se) + +### `1.5.1` + + * Fix for persistent connections (@rmeisler) + +### `1.5.0` + + * Convenience send methods; text(), binary(), ping(), pong() (@sirn-se) + * Optional Message instance as receive() method return (@sirn-se) + * Opcode filter for receive() method (@sirn-se) + * Added PHP `8.0` support (@webpatser) + * Dropped PHP `7.1` support (@sirn-se) + * Fix for unordered fragmented messages (@sirn-se) + * Improved error handling on stream calls (@sirn-se) + * Various code re-write (@sirn-se) + +## `v1.4` + + > PHP version `^7.1` + +#### `1.4.3` + + * Solve stream closure/get meta conflict (@sirn-se) + * Examples and documentation overhaul (@sirn-se) + +#### `1.4.2` + + * Force stream close on read error (@sirn-se) + * Authorization headers line feed (@sirn-se) + * Documentation (@matias-pool, @sirn-se) + +#### `1.4.1` + + * Ping/Pong, handled internally to avoid breaking fragmented messages (@nshmyrev, @sirn-se) + * Fix for persistent connections (@rmeisler) + * Fix opcode bitmask (@peterjah) + +#### `1.4.0` + + * Dropped support of old PHP versions (@sirn-se) + * Added PSR-3 Logging support (@sirn-se) + * Persistent connection option (@slezakattack) + * TimeoutException on connection time out (@slezakattack) + +## `v1.3` + + > PHP version `^5.4` and `^7.0` + +#### `1.3.1` + + * Allow control messages without payload (@Logioniz) + * Error code in ConnectionException (@sirn-se) + +#### `1.3.0` + + * Implements ping/pong frames (@pmccarren @Logioniz) + * Close behaviour (@sirn-se) + * Various fixes concerning connection handling (@sirn-se) + * Overhaul of Composer, Travis and Coveralls setup, PSR code standard and unit tests (@sirn-se) + +## `v1.2` + + > PHP version `^5.4` and `^7.0` + +#### `1.2.0` + + * Adding stream context options (to set e.g. SSL `allow_self_signed`). + +## `v1.1` + + > PHP version `^5.4` and `^7.0` + +#### `1.1.2` + + * Fixed error message on broken frame. + +#### `1.1.1` + + * Adding license information. + +#### `1.1.0` + + * Supporting huge payloads. + +## `v1.0` + + > PHP version `^5.4` and `^7.0` + +#### `1.0.3` + + * Bugfix: Correcting address in error-message + +#### `1.0.2` + + * Bugfix: Add port in request-header. + +#### `1.0.1` + + * Fixing a bug from empty payloads. + +#### `1.0.0` + + * Release as production ready. + * Adding option to set/override headers. + * Supporting basic authentication from user:pass in URL. + diff --git a/vendor/textalk/websocket/docs/Client.md b/vendor/textalk/websocket/docs/Client.md new file mode 100644 index 0000000..e6154b6 --- /dev/null +++ b/vendor/textalk/websocket/docs/Client.md @@ -0,0 +1,137 @@ +Client • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Client + +The client can read and write on a WebSocket stream. +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +## Class synopsis + +```php +WebSocket\Client { + + public __construct(string $uri, array $options = []) + public __destruct() + public __toString() : string + + public text(string $payload) : void + public binary(string $payload) : void + public ping(string $payload = '') : void + public pong(string $payload = '') : void + public send(mixed $payload, string $opcode = 'text', bool $masked = true) : void + public receive() : mixed + public close(int $status = 1000, mixed $message = 'ttfn') : mixed + + public getName() : string|null + public getPier() : string|null + public getLastOpcode() : string + public getCloseStatus() : int + public isConnected() : bool + public setTimeout(int $seconds) : void + public setFragmentSize(int $fragment_size) : self + public getFragmentSize() : int + public setLogger(Psr\Log\LoggerInterface $logger = null) : void +} +``` + +## Examples + +### Simple send-receive operation + +This example send a single message to a server, and output the response. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); +$client->text("Hello WebSocket.org!"); +echo $client->receive(); +$client->close(); +``` + +### Listening to a server + +To continuously listen to incoming messages, you need to put the receive operation within a loop. +Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out. +By consuming exceptions, the code will re-connect the socket in next loop iteration. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); +while (true) { + try { + $message = $client->receive(); + // Act on received message + // Break while loop to stop listening + } catch (\WebSocket\ConnectionException $e) { + // Possibly log errors + } +} +$client->close(); +``` + +### Filtering received messages + +By default the `receive()` method return messages of 'text' and 'binary' opcode. +The filter option allows you to specify which message types to return. + +```php +$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text']]); +$client->receive(); // Only return 'text' messages + +$client = new WebSocket\Client("ws://echo.websocket.org/", ['filter' => ['text', 'binary', 'ping', 'pong', 'close']]); +$client->receive(); // Return all messages +``` + +### Sending messages + +There are convenience methods to send messages with different opcodes. +```php +$client = new WebSocket\Client("ws://echo.websocket.org/"); + +// Convenience methods +$client->text('A plain text message'); // Send an opcode=text message +$client->binary($binary_string); // Send an opcode=binary message +$client->ping(); // Send an opcode=ping frame +$client->pong(); // Send an unsolicited opcode=pong frame + +// Generic send method +$client->send($payload); // Sent as masked opcode=text +$client->send($payload, 'binary'); // Sent as masked opcode=binary +$client->send($payload, 'binary', false); // Sent as unmasked opcode=binary +``` + +## Constructor options + +The `$options` parameter in constructor accepts an associative array of options. + +* `context` - A stream context created using [stream_context_create](https://www.php.net/manual/en/function.stream-context-create). +* `filter` - Array of opcodes to return on receive, default `['text', 'binary']` +* `fragment_size` - Maximum payload size. Default 4096 chars. +* `headers` - Additional headers as associative array name => content. +* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger. +* `persistent` - Connection is re-used between requests until time out is reached. Default false. +* `return_obj` - Return a [Message](Message.md) instance on receive, default false +* `timeout` - Time out in seconds. Default 5 seconds. + +```php +$context = stream_context_create(); +stream_context_set_option($context, 'ssl', 'verify_peer', false); +stream_context_set_option($context, 'ssl', 'verify_peer_name', false); + +$client = new WebSocket\Client("ws://echo.websocket.org/", [ + 'context' => $context, // Attach stream context created above + 'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return + 'headers' => [ // Additional headers, used to specify subprotocol + 'Sec-WebSocket-Protocol' => 'soap', + 'origin' => 'localhost', + ], + 'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger + 'return_obj' => true, // Return Message instance rather than just text + 'timeout' => 60, // 1 minute time out +]); +``` + +## Exceptions + +* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid. +* `WebSocket\BadUriException` - Thrown if provided URI is invalid. +* `WebSocket\ConnectionException` - Thrown on any socket I/O failure. +* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out. diff --git a/vendor/textalk/websocket/docs/Contributing.md b/vendor/textalk/websocket/docs/Contributing.md new file mode 100644 index 0000000..263d868 --- /dev/null +++ b/vendor/textalk/websocket/docs/Contributing.md @@ -0,0 +1,44 @@ +[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • Contributing + +# Websocket: Contributing + +Everyone is welcome to help out! +But to keep this project sustainable, please ensure your contribution respects the requirements below. + +## PR Requirements + +Requirements on pull requests; +* All tests **MUST** pass. +* Code coverage **MUST** remain at 100%. +* Code **MUST** adhere to PSR-1 and PSR-12 code standards. + +## Dependency management + +Install or update dependencies using [Composer](https://getcomposer.org/). + +``` +# Install dependencies +make install + +# Update dependencies +make update +``` + +## Code standard + +This project uses [PSR-1](https://www.php-fig.org/psr/psr-1/) and [PSR-12](https://www.php-fig.org/psr/psr-12/) code standards. +``` +# Check code standard adherence +make cs-check +``` + +## Unit testing + +Unit tests with [PHPUnit](https://phpunit.readthedocs.io/), coverage with [Coveralls](https://github.com/php-coveralls/php-coveralls) +``` +# Run unit tests +make test + +# Create coverage +make coverage +``` diff --git a/vendor/textalk/websocket/docs/Examples.md b/vendor/textalk/websocket/docs/Examples.md new file mode 100644 index 0000000..7dd4e0c --- /dev/null +++ b/vendor/textalk/websocket/docs/Examples.md @@ -0,0 +1,98 @@ +[Client](Client.md) • [Server](Server.md) • [Message](Message.md) • Examples • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Examples + +Here are some examples on how to use the WebSocket library. + +## Echo logger + +In dev environment (as in having run composer to include dev dependencies) you have +access to a simple echo logger that print out information synchronously. + +This is usable for debugging. For production, use a proper logger. + +```php +namespace WebSocket; + +$logger = new EchoLogger(); + +$client = new Client('ws://echo.websocket.org/'); +$client->setLogger($logger); + +$server = new Server(); +$server->setLogger($logger); +``` + +An example of server output; +``` +info | Server listening to port 8000 [] +debug | Wrote 129 of 129 bytes. [] +info | Server connected to port 8000 [] +info | Received 'text' message [] +debug | Wrote 9 of 9 bytes. [] +info | Sent 'text' message [] +debug | Received 'close', status: 1000. [] +debug | Wrote 32 of 32 bytes. [] +info | Sent 'close' message [] +info | Received 'close' message [] +``` + +## The `send` client + +Source: [examples/send.php](../examples/send.php) + +A simple, single send/receive client. + +Example use: +``` +php examples/send.php --opcode text "A text message" // Send a text message to localhost +php examples/send.php --opcode ping "ping it" // Send a ping message to localhost +php examples/send.php --uri ws://echo.websocket.org "A text message" // Send a text message to echo.websocket.org +php examples/send.php --opcode text --debug "A text message" // Use runtime debugging +``` + +## The `echoserver` server + +Source: [examples/echoserver.php](../examples/echoserver.php) + +A simple server that responds to recevied commands. + +Example use: +``` +php examples/echoserver.php // Run with default settings +php examples/echoserver.php --port 8080 // Listen on port 8080 +php examples/echoserver.php --debug // Use runtime debugging +``` + +These strings can be sent as message to trigger server to perform actions; +* `exit` - Server will initiate close procedure +* `ping` - Server will send a ping message +* `headers` - Server will respond with all headers provided by client +* `auth` - Server will respond with auth header if provided by client +* For other sent strings, server will respond with the same strings + +## The `random` client + +Source: [examples/random_client.php](../examples/random_client.php) + +The random client will use random options and continuously send/receive random messages. + +Example use: +``` +php examples/random_client.php --uri ws://echo.websocket.org // Connect to echo.websocket.org +php examples/random_client.php --timeout 5 --fragment_size 16 // Specify settings +php examples/random_client.php --debug // Use runtime debugging +``` + +## The `random` server + +Source: [examples/random_server.php](../examples/random_server.php) + +The random server will use random options and continuously send/receive random messages. + +Example use: +``` +php examples/random_server.php --port 8080 // // Listen on port 8080 +php examples/random_server.php --timeout 5 --fragment_size 16 // Specify settings +php examples/random_server.php --debug // Use runtime debugging +``` diff --git a/vendor/textalk/websocket/docs/Message.md b/vendor/textalk/websocket/docs/Message.md new file mode 100644 index 0000000..9bd0f2b --- /dev/null +++ b/vendor/textalk/websocket/docs/Message.md @@ -0,0 +1,60 @@ +[Client](Client.md) • [Server](Server.md) • Message • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Messages + +If option `return_obj` is set to `true` on [client](Client.md) or [server](Server.md), +the `receive()` method will return a Message instance instead of a string. + +Available classes correspond to opcode; +* WebSocket\Message\Text +* WebSocket\Message\Binary +* WebSocket\Message\Ping +* WebSocket\Message\Pong +* WebSocket\Message\Close + +Additionally; +* WebSocket\Message\Message - abstract base class for all messages above +* WebSocket\Message\Factory - Factory class to create Msssage instances + +## Message abstract class synopsis + +```php +WebSocket\Message\Message { + + public __construct(string $payload = '') + public __toString() : string + + public getOpcode() : string + public getLength() : int + public getTimestamp() : DateTime + public getContent() : string + public setContent(string $payload = '') : void + public hasContent() : bool +} +``` + +## Factory class synopsis + +```php +WebSocket\Message\Factory { + + public create(string $opcode, string $payload = '') : Message +} +``` + +## Example + +Receving a Message and echo some methods. + +```php +$client = new WebSocket\Client('ws://echo.websocket.org/', ['return_obj' => true]); +$client->text('Hello WebSocket.org!'); +// Echo return same message as sent +$message = $client->receive(); +echo $message->getOpcode(); // -> "text" +echo $message->getLength(); // -> 20 +echo $message->getContent(); // -> "Hello WebSocket.org!" +echo $message->hasContent(); // -> true +echo $message->getTimestamp()->format('H:i:s'); // -> 19:37:18 +$client->close(); +``` diff --git a/vendor/textalk/websocket/docs/Server.md b/vendor/textalk/websocket/docs/Server.md new file mode 100644 index 0000000..7d01a41 --- /dev/null +++ b/vendor/textalk/websocket/docs/Server.md @@ -0,0 +1,136 @@ +[Client](Client.md) • Server • [Message](Message.md) • [Examples](Examples.md) • [Changelog](Changelog.md) • [Contributing](Contributing.md) + +# Websocket: Server + +The library contains a rudimentary single stream/single thread server. +It internally supports Upgrade handshake and implicit close and ping/pong operations. + +Note that it does **not** support threading or automatic association ot continuous client requests. +If you require this kind of server behavior, you need to build it on top of provided server implementation. + +## Class synopsis + +```php +WebSocket\Server { + + public __construct(array $options = []) + public __destruct() + public __toString() : string + + public accept() : bool + public text(string $payload) : void + public binary(string $payload) : void + public ping(string $payload = '') : void + public pong(string $payload = '') : void + public send(mixed $payload, string $opcode = 'text', bool $masked = true) : void + public receive() : mixed + public close(int $status = 1000, mixed $message = 'ttfn') : mixed + + public getPort() : int + public getPath() : string + public getRequest() : array + public getHeader(string $header_name) : string|null + + public getName() : string|null + public getPier() : string|null + public getLastOpcode() : string + public getCloseStatus() : int + public isConnected() : bool + public setTimeout(int $seconds) : void + public setFragmentSize(int $fragment_size) : self + public getFragmentSize() : int + public setLogger(Psr\Log\LoggerInterface $logger = null) : void +} +``` + +## Examples + +### Simple receive-send operation + +This example reads a single message from a client, and respond with the same message. + +```php +$server = new WebSocket\Server(); +$server->accept(); +$message = $server->receive(); +$server->text($message); +$server->close(); +``` + +### Listening to clients + +To continuously listen to incoming messages, you need to put the receive operation within a loop. +Note that these functions **always** throw exception on any failure, including recoverable failures such as connection time out. +By consuming exceptions, the code will re-connect the socket in next loop iteration. + +```php +$server = new WebSocket\Server(); +while ($server->accept()) { + try { + $message = $server->receive(); + // Act on received message + // Break while loop to stop listening + } catch (\WebSocket\ConnectionException $e) { + // Possibly log errors + } +} +$server->close(); +``` + +### Filtering received messages + +By default the `receive()` method return messages of 'text' and 'binary' opcode. +The filter option allows you to specify which message types to return. + +```php +$server = new WebSocket\Server(['filter' => ['text']]); +$server->receive(); // only return 'text' messages + +$server = new WebSocket\Server(['filter' => ['text', 'binary', 'ping', 'pong', 'close']]); +$server->receive(); // return all messages +``` + +### Sending messages + +There are convenience methods to send messages with different opcodes. +```php +$server = new WebSocket\Server(); + +// Convenience methods +$server->text('A plain text message'); // Send an opcode=text message +$server->binary($binary_string); // Send an opcode=binary message +$server->ping(); // Send an opcode=ping frame +$server->pong(); // Send an unsolicited opcode=pong frame + +// Generic send method +$server->send($payload); // Sent as masked opcode=text +$server->send($payload, 'binary'); // Sent as masked opcode=binary +$server->send($payload, 'binary', false); // Sent as unmasked opcode=binary +``` + +## Constructor options + +The `$options` parameter in constructor accepts an associative array of options. + +* `filter` - Array of opcodes to return on receive, default `['text', 'binary']` +* `fragment_size` - Maximum payload size. Default 4096 chars. +* `logger` - A [PSR-3](https://www.php-fig.org/psr/psr-3/) compatible logger. +* `port` - The server port to listen to. Default 8000. +* `return_obj` - Return a [Message](Message.md) instance on receive, default false +* `timeout` - Time out in seconds. Default 5 seconds. + +```php +$server = new WebSocket\Server([ + 'filter' => ['text', 'binary', 'ping'], // Specify message types for receive() to return + 'logger' => $my_psr3_logger, // Attach a PSR3 compatible logger + 'port' => 9000, // Listening port + 'return_obj' => true, // Return Message insatnce rather than just text + 'timeout' => 60, // 1 minute time out +]); +``` + +## Exceptions + +* `WebSocket\BadOpcodeException` - Thrown if provided opcode is invalid. +* `WebSocket\ConnectionException` - Thrown on any socket I/O failure. +* `WebSocket\TimeoutException` - Thrown when the socket experiences a time out. diff --git a/vendor/textalk/websocket/examples/echoserver.php b/vendor/textalk/websocket/examples/echoserver.php new file mode 100644 index 0000000..231c4c9 --- /dev/null +++ b/vendor/textalk/websocket/examples/echoserver.php @@ -0,0 +1,87 @@ +<?php + +/** + * This file is used for the tests, but can also serve as an example of a WebSocket\Server. + * Run in console: php examples/echoserver.php + * + * Console options: + * --port <int> : The port to listen to, default 8000 + * --timeout <int> : Timeout in seconds, default 200 seconds + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +echo "> Random server\n"; + +// Server options specified or random +$options = array_merge([ + 'port' => 8000, + 'timeout' => 200, + 'filter' => ['text', 'binary', 'ping', 'pong'], +], getopt('', ['port:', 'timeout:', 'debug'])); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +// Setting timeout to 200 seconds to make time for all tests and manual runs. +try { + $server = new Server($options); +} catch (ConnectionException $e) { + echo "> ERROR: {$e->getMessage()}\n"; + die(); +} + +echo "> Listening to port {$server->getPort()}\n"; + +// Force quit to close server +while (true) { + try { + while ($server->accept()) { + echo "> Accepted on port {$server->getPort()}\n"; + while (true) { + $message = $server->receive(); + $opcode = $server->getLastOpcode(); + if (is_null($message)) { + echo "> Closing connection\n"; + continue 2; + } + echo "> Got '{$message}' [opcode: {$opcode}]\n"; + if (in_array($opcode, ['ping', 'pong'])) { + $server->send($message); + continue; + } + // Allow certain string to trigger server action + switch ($message) { + case 'exit': + echo "> Client told me to quit. Bye bye.\n"; + $server->close(); + echo "> Close status: {$server->getCloseStatus()}\n"; + exit; + case 'headers': + $server->text(implode("\r\n", $server->getRequest())); + break; + case 'ping': + $server->ping($message); + break; + case 'auth': + $auth = $server->getHeader('Authorization'); + $server->text("{$auth} - {$message}"); + break; + default: + $server->text($message); + } + } + } + } catch (ConnectionException $e) { + echo "> ERROR: {$e->getMessage()}\n"; + } +} diff --git a/vendor/textalk/websocket/examples/random_client.php b/vendor/textalk/websocket/examples/random_client.php new file mode 100644 index 0000000..b23bd6b --- /dev/null +++ b/vendor/textalk/websocket/examples/random_client.php @@ -0,0 +1,94 @@ +<?php + +/** + * Websocket client that read/write random data. + * Run in console: php examples/random_client.php + * + * Console options: + * --uri <uri> : The URI to connect to, default ws://localhost:8000 + * --timeout <int> : Timeout in seconds, random default + * --fragment_size <int> : Fragment size as bytes, random default + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +$randStr = function (int $maxlength = 4096) { + $string = ''; + $length = rand(1, $maxlength); + for ($i = 0; $i < $length; $i++) { + $string .= chr(rand(33, 126)); + } + return $string; +}; + +echo "> Random client\n"; + +// Server options specified or random +$options = array_merge([ + 'uri' => 'ws://localhost:8000', + 'timeout' => rand(1, 60), + 'fragment_size' => rand(1, 4096) * 8, +], getopt('', ['uri:', 'timeout:', 'fragment_size:', 'debug'])); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +// Main loop +while (true) { + try { + $client = new Client($options['uri'], $options); + $info = json_encode([ + 'uri' => $options['uri'], + 'timeout' => $options['timeout'], + 'framgemt_size' => $client->getFragmentSize(), + ]); + echo "> Creating client {$info}\n"; + + try { + while (true) { + // Random actions + switch (rand(1, 10)) { + case 1: + echo "> Sending text\n"; + $client->text("Text message {$randStr()}"); + break; + case 2: + echo "> Sending binary\n"; + $client->binary("Binary message {$randStr()}"); + break; + case 3: + echo "> Sending close\n"; + $client->close(rand(1000, 2000), "Close message {$randStr(8)}"); + break; + case 4: + echo "> Sending ping\n"; + $client->ping("Ping message {$randStr(8)}"); + break; + case 5: + echo "> Sending pong\n"; + $client->pong("Pong message {$randStr(8)}"); + break; + default: + echo "> Receiving\n"; + $received = $client->receive(); + echo "> Received {$client->getLastOpcode()}: {$received}\n"; + } + sleep(rand(1, 5)); + } + } catch (\Throwable $e) { + echo "ERROR I/O: {$e->getMessage()} [{$e->getCode()}]\n"; + } + } catch (\Throwable $e) { + echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n"; + } + sleep(rand(1, 5)); +} diff --git a/vendor/textalk/websocket/examples/random_server.php b/vendor/textalk/websocket/examples/random_server.php new file mode 100644 index 0000000..0b0849c --- /dev/null +++ b/vendor/textalk/websocket/examples/random_server.php @@ -0,0 +1,93 @@ +<?php + +/** + * Websocket server that read/write random data. + * Run in console: php examples/random_server.php + * + * Console options: + * --port <int> : The port to listen to, default 8000 + * --timeout <int> : Timeout in seconds, random default + * --fragment_size <int> : Fragment size as bytes, random default + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +$randStr = function (int $maxlength = 4096) { + $string = ''; + $length = rand(1, $maxlength); + for ($i = 0; $i < $length; $i++) { + $string .= chr(rand(33, 126)); + } + return $string; +}; + +echo "> Random server\n"; + +// Server options specified or random +$options = array_merge([ + 'port' => 8000, + 'timeout' => rand(1, 60), + 'fragment_size' => rand(1, 4096) * 8, +], getopt('', ['port:', 'timeout:', 'fragment_size:', 'debug'])); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +// Force quit to close server +while (true) { + try { + // Setup server + $server = new Server($options); + $info = json_encode([ + 'port' => $server->getPort(), + 'timeout' => $options['timeout'], + 'framgemt_size' => $server->getFragmentSize(), + ]); + echo "> Creating server {$info}\n"; + + while ($server->accept()) { + while (true) { + // Random actions + switch (rand(1, 10)) { + case 1: + echo "> Sending text\n"; + $server->text("Text message {$randStr()}"); + break; + case 2: + echo "> Sending binary\n"; + $server->binary("Binary message {$randStr()}"); + break; + case 3: + echo "> Sending close\n"; + $server->close(rand(1000, 2000), "Close message {$randStr(8)}"); + break; + case 4: + echo "> Sending ping\n"; + $server->ping("Ping message {$randStr(8)}"); + break; + case 5: + echo "> Sending pong\n"; + $server->pong("Pong message {$randStr(8)}"); + break; + default: + echo "> Receiving\n"; + $received = $server->receive(); + echo "> Received {$server->getLastOpcode()}: {$received}\n"; + } + sleep(rand(1, 5)); + } + } + } catch (\Throwable $e) { + echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n"; + } + sleep(rand(1, 5)); +} diff --git a/vendor/textalk/websocket/examples/send.php b/vendor/textalk/websocket/examples/send.php new file mode 100644 index 0000000..30e48e0 --- /dev/null +++ b/vendor/textalk/websocket/examples/send.php @@ -0,0 +1,51 @@ +<?php + +/** + * Simple send & receive client for test purpose. + * Run in console: php examples/send.php <options> <message> + * + * Console options: + * --uri <uri> : The URI to connect to, default ws://localhost:8000 + * --opcode <string> : Opcode to send, default 'text' + * --debug : Output log data (if logger is available) + */ + +namespace WebSocket; + +require __DIR__ . '/../vendor/autoload.php'; + +error_reporting(-1); + +echo "> Send client\n"; + +// Server options specified or random +$options = array_merge([ + 'uri' => 'ws://localhost:8000', + 'opcode' => 'text', +], getopt('', ['uri:', 'opcode:', 'debug'])); +$message = array_pop($argv); + +// If debug mode and logger is available +if (isset($options['debug']) && class_exists('WebSocket\EchoLog')) { + $logger = new EchoLog(); + $options['logger'] = $logger; + echo "> Using logger\n"; +} + +try { + // Create client, send and recevie + $client = new Client($options['uri'], $options); + $client->send($message, $options['opcode']); + echo "> Sent '{$message}' [opcode: {$options['opcode']}]\n"; + if (in_array($options['opcode'], ['text', 'binary'])) { + $message = $client->receive(); + $opcode = $client->getLastOpcode(); + if (!is_null($message)) { + echo "> Got '{$message}' [opcode: {$opcode}]\n"; + } + } + $client->close(); + echo "> Closing client\n"; +} catch (\Throwable $e) { + echo "ERROR: {$e->getMessage()} [{$e->getCode()}]\n"; +} diff --git a/vendor/textalk/websocket/lib/BadOpcodeException.php b/vendor/textalk/websocket/lib/BadOpcodeException.php new file mode 100644 index 0000000..a518715 --- /dev/null +++ b/vendor/textalk/websocket/lib/BadOpcodeException.php @@ -0,0 +1,7 @@ +<?php + +namespace WebSocket; + +class BadOpcodeException extends Exception +{ +} diff --git a/vendor/textalk/websocket/lib/BadUriException.php b/vendor/textalk/websocket/lib/BadUriException.php new file mode 100644 index 0000000..39e975d --- /dev/null +++ b/vendor/textalk/websocket/lib/BadUriException.php @@ -0,0 +1,7 @@ +<?php + +namespace WebSocket; + +class BadUriException extends Exception +{ +} diff --git a/vendor/textalk/websocket/lib/Base.php b/vendor/textalk/websocket/lib/Base.php new file mode 100644 index 0000000..f460289 --- /dev/null +++ b/vendor/textalk/websocket/lib/Base.php @@ -0,0 +1,486 @@ +<?php + +/** + * Copyright (C) 2014-2020 Textalk/Abicart and contributors. + * + * This file is part of Websocket PHP and is free software under the ISC License. + * License text: https://raw.githubusercontent.com/Textalk/websocket-php/master/COPYING + */ + +namespace WebSocket; + +use Psr\Log\{LoggerAwareInterface, LoggerInterface, NullLogger}; +use WebSocket\Message\Factory; + +class Base implements LoggerAwareInterface +{ + protected $socket; + protected $options = []; + protected $is_closing = false; + protected $last_opcode = null; + protected $close_status = null; + protected $logger; + private $read_buffer; + + protected static $opcodes = [ + 'continuation' => 0, + 'text' => 1, + 'binary' => 2, + 'close' => 8, + 'ping' => 9, + 'pong' => 10, + ]; + + public function getLastOpcode(): ?string + { + return $this->last_opcode; + } + + public function getCloseStatus(): ?int + { + return $this->close_status; + } + + public function isConnected(): bool + { + return $this->socket && + (get_resource_type($this->socket) == 'stream' || + get_resource_type($this->socket) == 'persistent stream'); + } + + public function setTimeout(int $timeout): void + { + $this->options['timeout'] = $timeout; + + if ($this->isConnected()) { + stream_set_timeout($this->socket, $timeout); + } + } + + public function setFragmentSize(int $fragment_size): self + { + $this->options['fragment_size'] = $fragment_size; + return $this; + } + + public function getFragmentSize(): int + { + return $this->options['fragment_size']; + } + + public function setLogger(LoggerInterface $logger = null): void + { + $this->logger = $logger ?: new NullLogger(); + } + + public function send(string $payload, string $opcode = 'text', bool $masked = true): void + { + if (!$this->isConnected()) { + $this->connect(); + } + + if (!in_array($opcode, array_keys(self::$opcodes))) { + $warning = "Bad opcode '{$opcode}'. Try 'text' or 'binary'."; + $this->logger->warning($warning); + throw new BadOpcodeException($warning); + } + + $payload_chunks = str_split($payload, $this->options['fragment_size']); + $frame_opcode = $opcode; + + for ($index = 0; $index < count($payload_chunks); ++$index) { + $chunk = $payload_chunks[$index]; + $final = $index == count($payload_chunks) - 1; + + $this->sendFragment($final, $chunk, $frame_opcode, $masked); + + // all fragments after the first will be marked a continuation + $frame_opcode = 'continuation'; + } + + $this->logger->info("Sent '{$opcode}' message", [ + 'opcode' => $opcode, + 'content-length' => strlen($payload), + 'frames' => count($payload_chunks), + ]); + } + + /** + * Convenience method to send text message + * @param string $payload Content as string + */ + public function text(string $payload): void + { + $this->send($payload); + } + + /** + * Convenience method to send binary message + * @param string $payload Content as binary string + */ + public function binary(string $payload): void + { + $this->send($payload, 'binary'); + } + + /** + * Convenience method to send ping + * @param string $payload Optional text as string + */ + public function ping(string $payload = ''): void + { + $this->send($payload, 'ping'); + } + + /** + * Convenience method to send unsolicited pong + * @param string $payload Optional text as string + */ + public function pong(string $payload = ''): void + { + $this->send($ |