Repository: Bacon/BaconPdf Branch: master Commit: 5f2e6b4eea07 Files: 59 Total size: 118.6 KB Directory structure: gitextract_s1qjpede/ ├── .coveralls.yml ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── benchmark/ │ └── ObjectWriterEvent.php ├── composer.json ├── doc/ │ ├── .gitignore │ ├── Makefile │ ├── _static/ │ │ └── theme_overrides.css │ ├── conf.py │ ├── index.rst │ └── metadata.rst ├── example/ │ ├── .gitignore │ └── empty-page.php ├── phpcs.xml ├── phpunit.xml.dist ├── src/ │ ├── DocumentInformation.php │ ├── Encryption/ │ │ ├── AbstractEncryption.php │ │ ├── BitMask.php │ │ ├── EncryptionInterface.php │ │ ├── NullEncryption.php │ │ ├── Pdf11Encryption.php │ │ ├── Pdf14Encryption.php │ │ ├── Pdf16Encryption.php │ │ └── Permissions.php │ ├── Exception/ │ │ ├── DomainException.php │ │ ├── ExceptionInterface.php │ │ ├── InvalidArgumentException.php │ │ ├── OutOfBoundsException.php │ │ ├── OutOfRangeException.php │ │ ├── RuntimeException.php │ │ ├── UnexpectedValueException.php │ │ ├── UnsupportedPasswordException.php │ │ └── WriterClosedException.php │ ├── Options/ │ │ ├── EncryptionOptions.php │ │ ├── PdfWriterOptions.php │ │ └── RasterImageOptions.php │ ├── Page.php │ ├── PdfWriter.php │ ├── RasterImage.php │ ├── Rectangle.php │ ├── Utils/ │ │ └── StringUtils.php │ └── Writer/ │ ├── DocumentWriter.php │ ├── ObjectWriter.php │ └── PageWriter.php └── test/ ├── Encryption/ │ ├── AbstractEncryptionTest.php │ ├── AbstractEncryptionTestCase.php │ ├── BitMaskTest.php │ ├── NullEncryptionTest.php │ ├── Pdf11EncryptionTest.php │ ├── Pdf14EncryptionTest.php │ ├── Pdf16EncryptionTest.php │ ├── PermissionsTest.php │ └── _files/ │ ├── pdf11-encrypt-entry.txt │ ├── pdf14-encrypt-entry.txt │ └── pdf16-encrypt-entry.txt ├── TestHelper/ │ └── MemoryObjectWriter.php └── Writer/ └── ObjectWriterTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .coveralls.yml ================================================ coverage_clover: clover.xml json_path: coveralls-upload.json src_dir: src ================================================ FILE: .gitignore ================================================ /nbproject /vendor /composer.lock ================================================ FILE: .travis.yml ================================================ sudo: false language: php cache: directories: - $HOME/.composer/cache matrix: fast_finish: true include: - php: 5.5 env: - EXECUTE_CS_CHECK=true - php: 5.6 env: - EXECUTE_TEST_COVERALLS=true - php: 7 - php: hhvm before_install: - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then phpenv config-rm xdebug.ini || return 0 ; fi - composer self-update - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then composer require --dev --no-update satooshi/php-coveralls dev-master ; fi install: - travis_retry composer install --no-interaction script: - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/phpunit --coverage-clover clover.xml ; fi - if [[ $EXECUTE_TEST_COVERALLS != 'true' ]]; then ./vendor/bin/phpunit ; fi - if [[ $EXECUTE_CS_CHECK == 'true' ]]; then ./vendor/bin/phpcs ; fi after_script: - if [[ $EXECUTE_TEST_COVERALLS == 'true' ]]; then ./vendor/bin/coveralls ; fi ================================================ FILE: LICENSE ================================================ Copyright (c) 2015, Ben Scholzen (DASPRiD) All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ================================================ FILE: README.md ================================================ # Bacon PDF [![Build Status](https://api.travis-ci.org/Bacon/BaconPdf.png?branch=master)](http://travis-ci.org/Bacon/BaconPdf) [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/Bacon/BaconPdf/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/Bacon/BaconPdf/?branch=master) [![Coverage Status](https://coveralls.io/repos/Bacon/BaconPdf/badge.svg?branch=master&service=github)](https://coveralls.io/github/Bacon/BaconPdf?branch=master) [![Documentation Status](https://readthedocs.org/projects/baconpdf/badge/?version=latest)](http://baconpdf.readthedocs.org/en/latest/?badge=latest) ## Introduction BaconPdf is a new PDF library for PHP with a clean interface. It comes with both writing and reading capabilities for PDFs up to version 1.7. ## Documentation You can find the latest documentation over at Read the Docs: https://baconpdf.readthedocs.org/en/latest/ ## Running benchmarks When doing performance sensitive changes to core classes, make sure to run the benchmarks before and after making your changes to ensure that they don't cause a huge impact: ```bash php vendor/bin/athletic -p benchmark -b vendor/autoload.php ``` ================================================ FILE: benchmark/ObjectWriterEvent.php ================================================ objectWriter = new ObjectWriter(new SplFileObject('php://memory', 'w+')); } /** * @iterations 10000 */ public function writeRawLine() { $this->objectWriter->writeRawLine('foo'); } /** * @iterations 10000 */ public function startDictionary() { $this->objectWriter->startDictionary(); } /** * @iterations 10000 */ public function endDictionary() { $this->objectWriter->endDictionary(); } /** * @iterations 10000 */ public function startArray() { $this->objectWriter->startArray(); } /** * @iterations 10000 */ public function endArray() { $this->objectWriter->endArray(); } /** * @iterations 10000 */ public function writeNull() { $this->objectWriter->writeNull(); } /** * @iterations 10000 */ public function writeBoolean() { $this->objectWriter->writeBoolean(true); } /** * @iterations 10000 */ public function writeIntegerNumber() { $this->objectWriter->writeNumber(1); } /** * @iterations 10000 */ public function writeFloatNumber() { $this->objectWriter->writeNumber(1.1); } /** * @iterations 10000 */ public function writeName() { $this->objectWriter->writeName('foo'); } /** * @iterations 10000 */ public function writeLiteralString() { $this->objectWriter->writeLiteralString('foo'); } /** * @iterations 10000 */ public function writeHexadecimalString() { $this->objectWriter->writeHexadecimalString('foo'); } } ================================================ FILE: composer.json ================================================ { "name": "bacon/bacon-pdf", "description": "BaconPdf is a powerful PDF library.", "license" : "BSD-2-Clause", "homepage": "https://github.com/Bacon/BaconPdf", "require": { "php": "^5.5.11|^7.0" }, "authors": [ { "name": "Ben Scholzen 'DASPRiD'", "email": "mail@dasprids.de", "homepage": "http://www.dasprids.de", "role": "Developer" } ], "autoload": { "psr-4": { "Bacon\\Pdf\\": "src/" } }, "autoload-dev": { "psr-4": { "Bacon\\PdfBenchmark\\": "benchmark/", "Bacon\\PdfTest\\": "test/" } }, "require-dev": { "ext-openssl": "*", "squizlabs/php_codesniffer": "^2.4", "phpunit/phpunit": "^4.8|^5.0", "athletic/athletic": "^0.1.8" }, "suggest": { "ext-openssl": "Allows using the encryption features" } } ================================================ FILE: doc/.gitignore ================================================ /_build ================================================ FILE: doc/Makefile ================================================ # Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # User-friendly check for sphinx-build ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) endif # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest coverage gettext help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " livehtml to make live HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" clean: rm -rf $(BUILDDIR)/* html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." livehtml: sphinx-autobuild -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/BaconPdf.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/BaconPdf.qhc" applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/BaconPdf" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/BaconPdf" @echo "# devhelp" epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." ================================================ FILE: doc/_static/theme_overrides.css ================================================ /* override table width restrictions */ @media screen and (min-width: 767px) { .wy-table-responsive table td { white-space: normal !important; } .wy-table-responsive { overflow: visible !important; } } ================================================ FILE: doc/conf.py ================================================ # -*- coding: utf-8 -*- # # BaconPdf documentation build configuration file, created by # sphinx-quickstart on Sat Nov 28 15:36:16 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. import sys import os import shlex # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. #sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. #needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. #source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'BaconPdf' copyright = u'2015, Ben Scholzen (DASPRiD)' author = u'Ben Scholzen (DASPRiD)' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = '1.0' # The full version, including alpha/beta/rc tags. release = '1.0.0dev' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: #today = '' # Else, today_fmt is used as the format for a strftime call. #today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. #default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. #add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). #add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. #show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. #modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. #keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'alabaster' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. #html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". #html_title = None # A shorter title for the navigation bar. Default is the same as html_title. #html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. #html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. #html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. #html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. #html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. #html_use_smartypants = True # Custom sidebar templates, maps document names to template names. #html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} # If false, no module index is generated. #html_domain_indices = True # If false, no index is generated. #html_use_index = True # If true, the index is split into individual pages for each letter. #html_split_index = False # If true, links to the reST sources are added to the pages. #html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. #html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. #html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. #html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). #html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr' #html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # Now only 'ja' uses this config value #html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. #html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'BaconPdfdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). #'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). #'pointsize': '10pt', # Additional stuff for the LaTeX preamble. #'preamble': '', # Latex figure (float) alignment #'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'BaconPdf.tex', u'BaconPdf Documentation', u'Ben Scholzen (DASPRiD)', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. #latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. #latex_use_parts = False # If true, show page references after internal links. #latex_show_pagerefs = False # If true, show URL addresses after external links. #latex_show_urls = False # Documents to append as an appendix to all manuals. #latex_appendices = [] # If false, no module index is generated. #latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'baconpdf', u'BaconPdf Documentation', [author], 1) ] # If true, show URL addresses after external links. #man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'BaconPdf', u'BaconPdf Documentation', author, 'BaconPdf', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. #texinfo_appendices = [] # If false, no module index is generated. #texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. #texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. #texinfo_no_detailmenu = False # Set up ReadTheDocs theme import sphinx_rtd_theme html_theme = 'sphinx_rtd_theme' html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] def setup(app): # overrides for wide tables in RTD theme app.add_stylesheet('theme_overrides.css') # Set up PHP syntax highlights from sphinx.highlighting import lexers from pygments.lexers.web import PhpLexer lexers["php"] = PhpLexer(startinline=True, linenos=1) lexers["php-annotations"] = PhpLexer(startinline=True, linenos=1) primary_domain = "php" highlight_language = "php" rst_prolog = """ .. role:: hidden :class: hidden """ ================================================ FILE: doc/index.rst ================================================ Welcome to BaconPdf's documentation! ==================================== `BaconPdf`_ is a powerful yet simple PDF library written in PHP. It aims to support the full PDF 1.7 standard. This documentation aims to guide you through the usage of the library. If you encounter issues in the documentation or think that some topic needs more explanations, feel free to `open an issue`_ on GitHub. In cases where this documentation refers to chapters of the PDF specification, those are always for version `1.7`_. .. _BaconPdf: https://github.com/Bacon/BaconPdf .. _open an issue: https://github.com/Bacon/BaconPdf/issues .. _1.7: http://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/pdf_reference_1-7.pdf .. toctree:: :caption: Table of Contents :maxdepth: 2 metadata ================================================ FILE: doc/metadata.rst ================================================ Handling document metadata ========================== PDF documents can hold general information, like the document's title, author and such, also known as metadata. To set or retrieve them, you have to obtain the ``DocumentInformation`` object from the PDF writer:: $information = $pdfWriter->getDocumentInformation(); You can then set or remove metadata with their respective methods:: $information->set('Title', 'My awesome PDF document'); $information->remove('Author'); When retrieving metadata, it can happen that these do not exist. Since BaconPdf follows a strict API, it will throw an exception when the requested entry does not exist. You are advised to always check for the existence of the entry you want to retrieve before actually retrieving it:: if ($information->has('Title') { $title = $information->get('Title'); } else { $title = ''; } Since the ``get()`` method will always return a string, there are two special cases in the metadata which must be retrieved via special methods, namely ``CreationDate`` and ``ModDate``. Even though those entries have their own methods for retrieval, checking their existence is still done via the ``has()`` method:: if ($information->has('CreationDate')) { $creationDate = $information->getCreationDate(); } if ($information->has('ModDate')) { $modificationDate = $information->getModificationDate(); } While the PDF specification names a list of standard entries in the metadata, it also allows arbitrary entries, thus the ``Info`` object does not distinguish between them, except for a few exceptions. Those exceptions are ``CreationDate`` and ``ModDate``, which cannot be set manually, but will always be manages by BaconPdf itself. Another exception is the ``Trapped`` entry, which is limited to three possible values (``True``, ``False`` and ``Unkown``). Every other entry can be any kind of text string. Keep in mind that the key names are case-sensitive, so setting the standard-entries with all lower-cased keys will not work. The standard entries are the following: Standard PDF Metadata --------------------- .. list-table:: :widths: 1 9 :header-rows: 1 * - Key - Description * - ``Title`` - The document's title. * - ``Author`` - The name of the person who created the document. * - ``Subject`` - The subject of the document. * - ``Keywords`` - Keywords associated with the document. * - ``Creator`` - If the document was converted to PDF from another format, the name of the application that created the original document from which it was converted. This would usually be the name of your application. * - ``Producer`` - If the document was converted to PDF from another format, the name of the application that converted it to PDF. This entry defaults to "BaconPdf". * - ``Trapped`` - Whether the document contains trapping information. For more information on those entries, see chapter 10.2.1 of the PDF specification. ================================================ FILE: example/.gitignore ================================================ /*.pdf ================================================ FILE: example/empty-page.php ================================================ getDocumentInformation()->set('Title', 'Empty Page Example'); $writer->addPage(595, 842); $writer->endDocument(); ================================================ FILE: phpcs.xml ================================================ ./src ./test test/* ================================================ FILE: phpunit.xml.dist ================================================ ./test ./src ================================================ FILE: src/DocumentInformation.php ================================================ 'BaconPdf']; /** * Sets an entry in the information dictionary. * * The CreationData and ModDate values are restricted for internal use, so trying to set them will trigger an * exception. Setting the "Trapped" value is allowed, but it must be one of the values "True", "False" or "Unknown". * You can set any key in here, but the following are the standard keys recognized by the PDF standard. Keep in mind * that the keys are case-sensitive: * * Title, Author, Subject, Keywords, Creator, Producer and Trapped. * * @param string $key * @param string $value * @throws DomainException */ public function set($key, $value) { if ('CreationDate' === $key || 'ModDate' === $key) { throw new DomainException('CreationDate and ModDate must not be set manually'); } if ('Trapped' === $key) { if (!in_array($value, ['True', 'False', 'Unknown'])) { throw new DomainException('Value for "Trapped" must be either "True", "False" or "Unknown"'); } $this->data['Trapped'] = $value; return; } $this->data[$key] = $value; } /** * Removes an entry from the information dictionary. * * @param string $key */ public function remove($key) { unset($this->data[$key]); } /** * Checks whether an entry exists in the information dictionary. * * @param string $key * @return bool */ public function has($key) { return array_key_exists($key, $this->data); } /** * Retrieves the value for a specific entry in the information dictionary. * * You may retrieve any entry from the information dictionary through this method, except for "CreationData" and * "ModDate". Those two entries have their own respective methods to be retrieved. * * @param string $key * @return string * @throws DomainException * @throws OutOfBoundsException */ public function get($key) { if ('CreationDate' === $key || 'ModDate' === $key) { throw new DomainException('CreationDate and ModDate must be retrieved through their respective methods'); } if (!array_key_exists($key, $this->data)) { throw new OutOfBoundsException(sprintf('Entry for key "%s" not found', $key)); } return $this->data[$key]; } /** * @return DateTimeImmutable */ public function getCreationDate() { return $this->retrieveDate('CreationDate'); } /** * @return DateTimeImmutable */ public function getModificationDate() { return $this->retrieveDate('ModDate'); } /** * Writes the info dictionary. * * @param ObjectWriter $objectWriter * @internal */ public function writeInfoDictionary(ObjectWriter $objectWriter) { $objectWriter->startDictionary(); foreach ($this->data as $key => $value) { $objectWriter->writeName($key); switch ($key) { case 'CreationDate': case 'ModDate': $objectWriter->writeLiteralString(StringUtils::formatDateTime($value)); break; case 'Trapped': $objectWriter->writeName($value); break; default: $objectWriter->writeLiteralString(StringUtils::encodeString($value)); } } $objectWriter->endDictionary(); } /** * @param string $key * @return DateTimeImmutable * @throws OutOfBoundsException */ private function retrieveDate($key) { if (!array_key_exists($key, $this->data)) { throw new OutOfBoundsException(sprintf('Entry for key "%s" not found', $key)); } return $this->data[$key]; } } ================================================ FILE: src/Encryption/AbstractEncryption.php ================================================ encodePassword($userPassword); $encodedOwnerPassword = $this->encodePassword($ownerPassword); $revision = $this->getRevision(); $keyLength = $this->getKeyLength() / 8; if (!in_array($keyLength, [40 / 8, 128 / 8])) { throw new UnexpectedValueException('Key length must be either 40 or 128'); } $this->ownerEntry = $this->computeOwnerEntry( $encodedOwnerPassword, $encodedUserPassword, $revision, $keyLength ); if (2 === $revision) { list($this->userEntry, $this->encryptionKey) = $this->computeUserEntryRev2( $encodedUserPassword, $this->ownerEntry, $revision, $permanentFileIdentifier ); } else { list($this->userEntry, $this->encryptionKey) = $this->computeUserEntryRev3OrGreater( $encodedUserPassword, $revision, $keyLength, $this->ownerEntry, $userPermissions->toInt($revision), $permanentFileIdentifier ); } $this->userPermissions = $userPermissions; } /** * Returns an encryption fitting for a specific PDF version. * * @param string $pdfVersion * @param string $permanentFileIdentifier * @param EncryptionOptions $options * @return EncryptionInterface */ public static function forPdfVersion($pdfVersion, $permanentFileIdentifier, EncryptionOptions $options) { if (version_compare($pdfVersion, '1.6', '>=')) { $encryptionClassName = Pdf16Encryption::class; } elseif (version_compare($pdfVersion, '1.4', '>=')) { $encryptionClassName = Pdf14Encryption::class; } else { $encryptionClassName = Pdf11Encryption::class; } return new $encryptionClassName( $permanentFileIdentifier, $options->getUserPassword(), $options->getOwnerPassword(), $options->getUserPermissions() ); } /** * {@inheritdoc} */ public function writeEncryptEntry(ObjectWriter $objectWriter) { $objectWriter->writeName('Encrypt'); $objectWriter->startDictionary(); $objectWriter->writeName('Filter'); $objectWriter->writeName('Standard'); $objectWriter->writeName('V'); $objectWriter->writeNumber($this->getAlgorithm()); $objectWriter->writeName('R'); $objectWriter->writeNumber($this->getRevision()); $objectWriter->writeName('O'); $objectWriter->writeHexadecimalString($this->ownerEntry); $objectWriter->writeName('U'); $objectWriter->writeHexadecimalString($this->userEntry); $objectWriter->writeName('P'); $objectWriter->writeNumber($this->userPermissions->toInt($this->getRevision())); $this->writeAdditionalEncryptDictionaryEntries($objectWriter); $objectWriter->endDictionary(); } /** * Adds additional entries to the encrypt dictionary if required. * * @param ObjectWriter $objectWriter */ protected function writeAdditionalEncryptDictionaryEntries(ObjectWriter $objectWriter) { } /** * Returns the revision number of the encryption. * * @return int */ abstract protected function getRevision(); /** * Returns the algorithm number of the encryption. * * @return int */ abstract protected function getAlgorithm(); /** * Returns the key length to be used. * * The returned value must be either 40 or 128. * * @return int */ abstract protected function getKeyLength(); /** * Computes an individual ecryption key for an object. * * @param string $objectNumber * @param string $generationNumber * @return string */ protected function computeIndividualEncryptionKey($objectNumber, $generationNumber) { return substr(md5( $this->encryptionKey . substr(pack('V', $objectNumber), 0, 3) . substr(pack('V', $generationNumber), 0, 2), true ), 0, min(16, strlen($this->encryptionKey) + 5)); } /** * Encodes a given password into latin-1 and performs length check. * * @param string $password * @return string * @throws UnsupportedPasswordException */ private function encodePassword($password) { set_error_handler(function () { }, E_NOTICE); $encodedPassword = iconv('UTF-8', 'ISO-8859-1', $password); restore_error_handler(); if (false === $encodedPassword) { throw new UnsupportedPasswordException('Password contains non-latin-1 characters'); } if (strlen($encodedPassword) > 32) { throw new UnsupportedPasswordException('Password is longer than 32 characters'); } return $encodedPassword; } /** * Computes the encryption key as defined by algorithm 3.2 in 3.5.2. * * @param string $password * @param int $revision * @param int $keyLength * @param string $ownerEntry * @param int $permissions * @param string $permanentFileIdentifier * @param bool $encryptMetadata * @return string */ private function computeEncryptionKey( $password, $revision, $keyLength, $ownerEntry, $permissions, $permanentFileIdentifier, $encryptMetadata = true ) { $string = substr($password . self::ENCRYPTION_PADDING, 0, 32) . $ownerEntry . pack('V', $permissions) . $permanentFileIdentifier; if ($revision >= 4 && $encryptMetadata) { $string .= "\0xff\0xff\0xff\0xff"; } $hash = md5($string, true); if ($revision >= 3) { for ($i = 0; $i < 50; ++$i) { $hash = md5(substr($hash, 0, $keyLength), true); } return substr($hash, 0, $keyLength); } return substr($hash, 0, 5); } /** * Computes the owner entry as defined by algorithm 3.3 in 3.5.2. * * @param string $ownerPassword * @param string $userPassword * @param int $revision * @param int $keyLength * @return string */ private function computeOwnerEntry($ownerPassword, $userPassword, $revision, $keyLength) { $hash = md5(substr($ownerPassword . self::ENCRYPTION_PADDING, 0, 32), true); if ($revision >= 3) { for ($i = 0; $i < 50; ++$i) { $hash = md5($hash, true); } $key = substr($hash, 0, $keyLength); } else { $key = substr($hash, 0, 5); } $value = openssl_encrypt(substr($userPassword . self::ENCRYPTION_PADDING, 0, 32), 'rc4', $key, true); if ($revision >= 3) { $value = self::applyRc4Loop($value, $key, $keyLength); } return $value; } /** * Computes the user entry (rev 2) as defined by algorithm 3.4 in 3.5.2. * * @param string $userPassword * @param string $ownerEntry * @param int $userPermissionFlags * @param string $permanentFileIdentifier * @return string[] */ private function computeUserEntryRev2($userPassword, $ownerEntry, $userPermissionFlags, $permanentFileIdentifier) { $key = self::computeEncryptionKey( $userPassword, 2, 5, $ownerEntry, $userPermissionFlags, $permanentFileIdentifier ); return [ openssl_encrypt(self::ENCRYPTION_PADDING, 'rc4', $key, true), $key ]; } /** * Computes the user entry (rev 3 or greater) as defined by algorithm 3.5 in 3.5.2. * * @param string $userPassword * @param int $revision * @param int $keyLength * @param string $ownerEntry * @param int $permissions * @param string $permanentFileIdentifier * @return string[] */ private function computeUserEntryRev3OrGreater( $userPassword, $revision, $keyLength, $ownerEntry, $permissions, $permanentFileIdentifier ) { $key = self::computeEncryptionKey( $userPassword, $revision, $keyLength, $ownerEntry, $permissions, $permanentFileIdentifier ); $hash = md5(self::ENCRYPTION_PADDING . $permanentFileIdentifier, true); $value = self::applyRc4Loop(openssl_encrypt($hash, 'rc4', $key, true), $key, $keyLength); $value .= openssl_random_pseudo_bytes(16); return [ $value, $key ]; } /** * Applies loop RC4 encryption. * * @param string $value * @param string $key * @param int $keyLength * @return string */ private function applyRc4Loop($value, $key, $keyLength) { for ($i = 1; $i <= 19; ++$i) { $newKey = ''; for ($j = 0; $j < $keyLength; ++$j) { $newKey .= chr(ord($key[$j]) ^ $i); } $value = openssl_encrypt($value, 'rc4', $newKey, true); } return $value; } } ================================================ FILE: src/Encryption/BitMask.php ================================================ value |= (1 << $bit); return; } $this->value &= ~(1 << $bit); } /** * @return int */ public function toInt() { return $this->value; } } ================================================ FILE: src/Encryption/EncryptionInterface.php ================================================ computeIndividualEncryptionKey($objectNumber, $generationNumber), true ); } /** * {@inheritdoc} */ protected function getRevision() { return 2; } /** * {@inheritdoc} */ protected function getAlgorithm() { return 1; } /** * {@inheritdoc} */ protected function getKeyLength() { return 40; } } ================================================ FILE: src/Encryption/Pdf14Encryption.php ================================================ writeName('Length'); $objectWriter->writeNumber(128); } /** * {@inheritdoc} */ protected function getRevision() { return 3; } /** * {@inheritdoc} */ protected function getAlgorithm() { return 2; } /** * {@inheritdoc} */ protected function getKeyLength() { return 128; } } ================================================ FILE: src/Encryption/Pdf16Encryption.php ================================================ writeName('CF'); $objectWriter->startDictionary(); $objectWriter->writeName('StdCF'); $objectWriter->startDictionary(); $objectWriter->writeName('Type'); $objectWriter->writeName('CryptFilter'); $objectWriter->writeName('CFM'); $objectWriter->writeName('AESV2'); $objectWriter->writeName('Length'); $objectWriter->writeNumber(128); $objectWriter->endDictionary(); $objectWriter->endDictionary(); $objectWriter->writeName('StrF'); $objectWriter->writeName('StdCF'); $objectWriter->writeName('StmF'); $objectWriter->writeName('StdCF'); } /** * {@inheritdoc} */ public function encrypt($plaintext, $objectNumber, $generationNumber) { $initializationVector = openssl_random_pseudo_bytes(16); return $initializationVector . openssl_encrypt( $plaintext, 'aes-128-cbc', $this->computeIndividualEncryptionKey($objectNumber, $generationNumber) . "\x73\x41\x6c\x54", true, $initializationVector ); } /** * {@inheritdoc} */ protected function getRevision() { return 4; } /** * {@inheritdoc} */ protected function getAlgorithm() { return 4; } } ================================================ FILE: src/Encryption/Permissions.php ================================================ mayPrint = $mayPrint; $this->mayPrintHighResolution = $mayPrintHighResolution; $this->mayModify = $mayModify; $this->mayCopy = $mayCopy; $this->mayAnnotate = $mayAnnotate; $this->mayFillInForms = $mayFillInForms; $this->mayExtractForAccessibility = $mayExtractForAccessibility; $this->mayAssemble = $mayAssemble; } /** * Creates permissions which allow nothing. * * @return self */ public static function allowNothing() { return new self(false, false, false, false, false, false, false, false); } /** * Creates permissions which allow everything. * * @return self */ public static function allowEverything() { return new self(true, true, true, true, true, true, true, true); } /** * Convert the permissions to am integer bit mask. * * {@internal Keep in mind that the bit positions named in the PDF reference are counted from 1, while in here they * are counted from 0.}} * * @param int $revision * @return int */ public function toInt($revision) { $bitMask = new BitMask(); $bitMask->set(2, $this->mayPrint); $bitMask->set(3, $this->mayModify); $bitMask->set(4, $this->mayCopy); $bitMask->set(5, $this->mayAnnotate); if ($revision >= 3) { $bitMask->set(8, $this->mayFillInForms); $bitMask->set(9, $this->mayExtractForAccessibility); $bitMask->set(10, $this->mayAssemble); $bitMask->set(11, $this->mayPrintHighResolution); } return $bitMask->toInt(); } } ================================================ FILE: src/Exception/DomainException.php ================================================ userPassword = $userPassword; $this->ownerPassword = (null !== $ownerPassword ? $ownerPassword : $userPassword); $this->userPermissions = (null !== $userPermissions ? $userPermissions : Permissions::allowEverything()); } /** * @return string */ public function getUserPassword() { return $this->userPassword; } /** * @return string */ public function getOwnerPassword() { return $this->ownerPassword; } /** * @return Permissions */ public function getUserPermissions() { return $this->userPermissions; } } ================================================ FILE: src/Options/PdfWriterOptions.php ================================================ pdfVersion = $pdfVersion; } /** * Returns the PDF version to use for the document. * * @return string */ public function getPdfVersion() { return $this->pdfVersion; } /** * Sets encryption options. * * @param EncryptionOptions $encryptionOptions */ public function setEncryptionOptions(EncryptionOptions $encryptionOptions) { $this->encryptionOptions = $encryptionOptions; } /** * @param string $permanentFileIdentifier * @return EncryptionInterface */ public function getEncryption($permanentFileIdentifier) { if (null === $this->encryptionOptions) { return new NullEncryption(); } return AbstractEncryption::forPdfVersion($this->pdfVersion, $permanentFileIdentifier, $this->encryptionOptions); } } ================================================ FILE: src/Options/RasterImageOptions.php ================================================ useLossyCompression = $useLossyCompression; $this->lossyCompressionQuality = $lossyCompressionQuality; } public function useLossyCompression() { return $this->useLossyCompression; } public function getLossyCompressionQuality() { return $this->lossyCompressionQuality; } } ================================================ FILE: src/Page.php ================================================ pageWriter = $pageWriter; $this->pageWriter->setBox('MediaBox', new Rectangle(0, 0, $width, $height)); } /** * Sets the crop box to which the page should be cropped to for displaying or printing. * * @param Rectangle $cropBox */ public function setCropBox(Rectangle $cropBox) { $this->pageWriter->setBox('CropBox', $cropBox); } /** * Sets the bleed box to which the page should be clipped in a production environment. * * @param Rectangle $bleedBox */ public function setBleedBox(Rectangle $bleedBox) { $this->pageWriter->setBox('BleedBox', $bleedBox); } /** * Sets the trim box to which the finished page should be trimmed. * * @param Rectangle $trimBox */ public function setTrimBox(Rectangle $trimBox) { $this->pageWriter->setBox('TrimBox', $trimBox); } /** * Sets the art box which contains the meaningful content of the page. * * @param Rectangle $artBox */ public function setArtBox(Rectangle $artBox) { $this->pageWriter->setBox('ArtBox', $artBox); } /** * Rotates the page for output. * * The supplied value must be a multiple of 90, so either 0, 90, 180 oder 270. * * @param int $degrees */ public function rotate($degrees) { $this->pageWriter->setRotation($degrees); } } ================================================ FILE: src/PdfWriter.php ================================================ options = $options; $fileIdentifier = md5(microtime(), true); $this->objectWriter = new ObjectWriter($fileObject); $this->documentWriter = new DocumentWriter($this->objectWriter, $options, $fileIdentifier); $this->encryption = $options->getEncryption($fileIdentifier); } /** * Returns the document information object. * * @return DocumentInformation */ public function getDocumentInformation() { return $this->documentWriter->getDocumentInformation(); } /** * Adds a page to the document. * * @param float $width * @param float $height * @return Page */ public function addPage($width, $height) { $pageWriter = new PageWriter($this->objectWriter); $this->documentWriter->addPageWriter($pageWriter); return new Page($pageWriter, $width, $height); } /** * Imports a raster image into the document. * * @param strings $filename * @param bool $useLossyCompression * @param int $compressionQuality * @return Image */ public function importRasterImage($filename, $useLossyCompression = false, $compressionQuality = 80) { return new RasterImage( $this->objectWriter, $filename, $this->options->getPdfVersion(), $useLossyCompression, $compressionQuality ); } /** * Ends the document by writing all pending data. * * While the PDF writer will remove all references to the passed in file object in itself to avoid further writing * and to allow the file pointer to be closed, the callee may still have a reference to it. If that is the case, * make sure to unset it if you don't need it. * * Any further attempts to append data to the PDF writer will result in an exception. */ public function endDocument() { $this->documentWriter->endDocument($this->encryption); } /** * Creates a PDF writer which writes everything to a file. * * @param string $filename * @param PdfWriterOptions|null $options * @return static */ public static function toFile($filename, PdfWriterOptions $options = null) { return new static(new SplFileObject($filename, 'wb'), $options); } /** * Creates a PDF writer which outputs everything to the STDOUT. * * Make sure to send the appropriate headers beforehand if you are in a web environment. * * @param PdfWriterOptions|null $options * @return static */ public static function output(PdfWriterOptions $options = null) { return new static(new SplFileObject('php://stdout', 'wb'), $options); } } ================================================ FILE: src/RasterImage.php ================================================ 100) { throw new DomainException('Compression quality must be a value between 0 and 100'); } $image = new Imagick($filename); $image->stripImage(); $this->width = $image->getImageWidth(); $this->height = $image->getImageHeight(); $filter = $this->determineFilter($useLossyCompression, $pdfVersion); $colorSpace = $this->determineColorSpace($image); $this->setFitlerParameters($image, $filter, $colorSpace, $compressionQuality); $shadowMaskInData = null; $shadowMaskId = null; if (Imagick::ALPHACHANNEL_UNDEFINED !== $image->getImageAlphaChannel()) { if (version_compare($pdfVersion, '1.4', '>=')) { throw new RuntimeException('Transparent images require PDF version 1.4 or higher'); } if ($filter === 'JPXDecode') { $shadowMaskInData = 1; } else { $shadowMaskId = $this->createShadowMask($objectWriter, $image, $filter); } } $streamData = $image->getImageBlob(); $image->clear(); $this->id = $objectWriter->startObject(); $objectWriter->startDictionary(); $this->writeCommonDictionaryEntries($objectWriter, $colorSpace, strlen($streamData), $filter); if (null !== $shadowMaskInData) { $objectWriter->writeName('SMaskInData'); $objectWriter->writeNumber($shadowMaskInData); } elseif (null !== $shadowMaskId) { $objectWriter->writeName('SMask'); $objectWriter->writeIndirectReference($shadowMaskId); } $objectWriter->startStream(); $objectWriter->writeRaw($streamData); $objectWriter->endStream(); $objectWriter->endObject(); } /** * Returns the object number of the imported image. * * @return int */ public function getId() { return $this->id; } /** * Returns the width of the image in pixels. * * @return int */ public function getWidth() { return $this->width; } /** * Returns the height of the image in pixels. * * @return int */ public function getHeight() { return $this->height; } /** * @param bool $useLossyCompression * @param string $pdfVersion * @return string */ private function determineFilter($useLossyCompression, $pdfVersion) { if (!$useLossyCompression) { return 'FlateDecode'; } if (version_compare($pdfVersion, '1.5', '>=')) { return 'JPXDecode'; } return 'DCTDecode'; } /** * Determines the color space of an image. * * @param Imagick $image * @return string * @throws DomainException */ private function determineColorSpace(Imagick $image) { switch ($image->getColorSpace()) { case Imagick::COLORSPACE_GRAY: return 'DeviceGray'; case Imagick::COLORSPACE_RGB: return 'DeviceRGB'; case Imagick::COLORSPACE_CMYK: return 'DeviceCMYK'; } throw new DomainException('Image has an unsupported colorspace, must be gray, RGB or CMYK'); } /** * Creates a shadow mask from an image's alpha channel. * * @param ObjectWriter $objectWriter * @param Imagick $image * @param string $filter * @return int */ private function createShadowMask(ObjectWriter $objectWriter, Imagick $image, $filter) { $shadowMask = clone $image; $shadowMask->separateImageChannel(Imagick::CHANNEL_ALPHA); if ('FlateDecode' === $filter) { $image->setImageFormat('GRAY'); } $streamData = $shadowMask->getImageBlob(); $shadowMask->clear(); $id = $objectWriter->startObject(); $objectWriter->startDictionary(); $this->writeCommonDictionaryEntries($objectWriter, 'DeviceGray', strlen($streamData), $filter); $objectWriter->endDictionary(); $objectWriter->startStream(); $objectWriter->writeRaw($streamData); $objectWriter->endStream(); $objectWriter->endObject(); return $id; } /** * Writes common dictionary entries shared between actual images and their soft masks. * * @param ObjectWriter $objectWriter * @param string $colorSpace * @param int $length * @param string $filter * @param int|null $shadowMaskId */ private function writeCommonDictionaryEntries(ObjectWriter $objectWriter, $colorSpace, $length, $filter) { $objectWriter->writeName('Type'); $objectWriter->writeName('XObject'); $objectWriter->writeName('Subtype'); $objectWriter->writeName('Image'); $objectWriter->writeName('Width'); $objectWriter->writeNumber($this->width); $objectWriter->writeName('Height'); $objectWriter->writeNumber($this->height); $objectWriter->writeName('ColorSpace'); $objectWriter->writeName($colorSpace); $objectWriter->writeName('BitsPerComponent'); $objectWriter->writeNumber(8); $objectWriter->writeName('Length'); $objectWriter->writeNumber($length); $objectWriter->writeName('Filter'); $objectWriter->writeName($filter); } /** * Sets the filter parameters for the image. * * @param Imagick $image * @param string $filter * @param string $colorSpace * @param int $compressionQuality */ private function setFitlerParameters(Imagick $image, $filter, $colorSpace, $compressionQuality) { switch ($filter) { case 'JPXDecode': $image->setImageFormat('J2K'); $image->setImageCompression(Imagick::COMPRESSION_JPEG2000); break; case 'DCTDecode': $image->setImageFormat('JPEG'); $image->setImageCompression(Imagick::COMPRESSION_JPEG); break; case 'FlateDecode': $image->setImageFormat([ 'DeviceGray' => 'GRAY', 'DeviceRGB' => 'RGB', 'DeviceCMYK' => 'CMYK', ][$colorSpace]); $image->setImageCompression(Imagick::COMPRESSION_ZIP); break; } $image->setImageCompressionQuality($compressionQuality); } } ================================================ FILE: src/Rectangle.php ================================================ x1 = min($x1, $x2); $this->y1 = min($y1, $y2); $this->x2 = max($x1, $x2); $this->y2 = max($y1, $y2); } /** * Writes the rectangle object to a writer. * * @param ObjectWriter $objectWriter * @internal */ public function writeRectangleArray(ObjectWriter $objectWriter) { $objectWriter->startArray(); $objectWriter->writeNumber($this->x1); $objectWriter->writeNumber($this->y1); $objectWriter->writeNumber($this->x2); $objectWriter->writeNumber($this->y2); $objectWriter->endArray(); } } ================================================ FILE: src/Utils/StringUtils.php ================================================ format('\D\:YmdHis'); if (0 === $dateTime->getTimezone()->getOffset()) { return $timeString . 'Z'; } return $timeString . strtr(':', "'", $dateTime->format('P')) . "'"; } } ================================================ FILE: src/Writer/DocumentWriter.php ================================================ objectWriter = $objectWriter; $this->options = $options; $this->objectWriter->writeRawLine(sprintf("%%PDF-%s", $this->options->getPdfVersion())); $this->objectWriter->writeRawLine("%\xff\xff\xff\xff"); $this->permanentFileIdentifier = $this->changingFileIdentifier = $fileIdentifier; $this->pageTreeId = $this->objectWriter->allocateObjectId(); $this->documentInformation = new DocumentInformation(); } /** * Returns the document information object. * * @return DocumentInformation */ public function getDocumentInformation() { return $this->documentInformation; } /** * Adds a page writer for the page tree. * * @param PageWriter $pageWriter */ public function addPageWriter(PageWriter $pageWriter) { $this->pageWriters[] = $pageWriter; } /** * Ends the document. * * @param EncryptionInterface $encryption */ public function endDocument(EncryptionInterface $encryption) { $this->closeRemainingPages(); $this->writePageTree(); $documentInformationId = $this->writeDocumentInformation(); $documentCatalogId = $this->writeDocumentCatalog(); $xrefOffset = $this->writeCrossReferenceTable(); $this->writeTrailer($documentInformationId, $documentCatalogId, $encryption); $this->writeFooter($xrefOffset); } /** * Closes pages which haven't been explicitly closed yet. */ private function closeRemainingPages() { foreach ($this->pageWriters as $key => $pageWriter) { $this->pageIds[] = $pageWriter->writePage($this->objectWriter, $this->pageTreeId); unset($this->pageWriters[$key]); } } /** * Writes the page tree. */ private function writePageTree() { $this->objectWriter->startObject($this->pageTreeId); $this->objectWriter->startDictionary(); $this->objectWriter->writeName('Type'); $this->objectWriter->writeName('Pages'); $this->objectWriter->writeName('Kids'); $this->objectWriter->startArray(); sort($this->pageIds, SORT_NUMERIC); foreach ($this->pageIds as $pageId) { $this->objectWriter->writeIndirectReference($pageId); } $this->objectWriter->endArray(); $this->objectWriter->writeName('Count'); $this->objectWriter->writeNumber(count($this->pageIds)); $this->objectWriter->endDictionary(); $this->objectWriter->endObject(); } /** * Writes the document information. * * @return int */ private function writeDocumentInformation() { $id = $this->objectWriter->startObject(); $this->documentInformation->writeInfoDictionary($this->objectWriter); $this->objectWriter->endObject(); return $id; } /** * Writes the document catalog. * * @return int */ private function writeDocumentCatalog() { $id = $this->objectWriter->startObject(); $this->objectWriter->startDictionary(); $this->objectWriter->writeName('Type'); $this->objectWriter->writeName('Catalog'); $this->objectWriter->writeName('Pages'); $this->objectWriter->writeIndirectReference($this->pageTreeId); $this->objectWriter->endDictionary(); $this->objectWriter->endObject(); return $id; } /** * Writes the cross-reference table. * * @return int */ private function writeCrossReferenceTable() { $xrefOffset = $this->objectWriter->getCurrentOffset(); $objectOffsets = $this->objectWriter->getObjectOffsets(); ksort($objectOffsets, SORT_NUMERIC); $this->objectWriter->writeRawLine('xref'); $this->objectWriter->writeRawLine(sprintf('0 %d', count($objectOffsets) + 1)); $this->objectWriter->writeRawLine(sprintf('%010d %05d f ', 0, 65535)); foreach ($objectOffsets as $offset) { $this->objectWriter->writeRawLine(sprintf('%010d %05d n ', $offset, 0)); } return $xrefOffset; } /** * Writes the trailer. * * @param int $documentInformationId * @param int $documentCatalogId * @param EncryptionInterface $encryption */ private function writeTrailer($documentInformationId, $documentCatalogId, EncryptionInterface $encryption) { $this->objectWriter->writeRawLine('trailer'); $this->objectWriter->startDictionary(); $this->objectWriter->writeName('Id'); $this->objectWriter->startArray(); $this->objectWriter->writeHexadecimalString($this->permanentFileIdentifier); $this->objectWriter->writeHexadecimalString($this->changingFileIdentifier); $this->objectWriter->endArray(); $this->objectWriter->writeName('Info'); $this->objectWriter->writeIndirectReference($documentInformationId); $this->objectWriter->writeName('Root'); $this->objectWriter->writeIndirectReference($documentCatalogId); $encryption->writeEncryptEntry($this->objectWriter); $this->objectWriter->endDictionary(); } /** * Writes the footer. * * @param int $xrefOffset */ private function writeFooter($xrefOffset) { $this->objectWriter->writeRawLine(''); $this->objectWriter->writeRawLine('startxref'); $this->objectWriter->writeRawLine((string) $xrefOffset); $this->objectWriter->writeRawLine("%%%EOF"); } } ================================================ FILE: src/Writer/ObjectWriter.php ================================================ fileObject = $fileObject; } /** * Returns the current position in the file. * * @return int */ public function getCurrentOffset() { return $this->fileObject->ftell(); } /** * Writes a raw data line to the stream. * * A newline character is appended after the data. Keep in mind that you may still be after a token which requires * a following whitespace, depending on the context you are in. * * @param string $data */ public function writeRawLine($data) { $this->fileObject->fwrite($data . "\n"); } /** * Writes raw data to the stream. * * @param string $data */ public function writeRaw($data) { $this->fileObject->fwrite($data); } /** * Returns all object offsets. * * @return int */ public function getObjectOffsets() { return $this->objectOffsets; } /** * Allocates a new ID for an object. * * @return int */ public function allocateObjectId() { return ++$this->lastAllocatedObjectId; } /** * Starts an object. * * If the object ID is omitted, a new one is allocated. * * @param int|null $objectId * @return int */ public function startObject($objectId = null) { if (null === $objectId) { $objectId = ++$this->lastAllocatedObjectId; } $this->objectOffsets[$objectId] = $this->fileObject->ftell(); $this->fileObject->fwrite(sprintf("%d 0 obj\n", $objectId)); return $objectId; } /** * Ends an object. */ public function endObject() { $this->fileObject->fwrite("\nendobj\n"); } /** * Starts a stream. */ public function startStream() { $this->fileObject->fwrite("stream\n"); } public function endStream() { $this->fileObject->fwrite("\nendstream\n"); } /** * Writes an indirect reference * * @param int $objectId */ public function writeIndirectReference($objectId) { if ($this->requiresWhitespace) { $this->fileObject->fwrite(sprintf(' %d 0 R', $objectId)); } else { $this->fileObject->fwrite(sprintf('%d 0 R', $objectId)); } $this->requiresWhitespace = true; } /** * Starts a dictionary. */ public function startDictionary() { $this->fileObject->fwrite('<<'); $this->requiresWhitespace = false; } /** * Ends a dictionary. */ public function endDictionary() { $this->fileObject->fwrite('>>'); $this->requiresWhitespace = false; } /** * Starts an array. */ public function startArray() { $this->fileObject->fwrite('['); $this->requiresWhitespace = false; } /** * Ends an array. */ public function endArray() { $this->fileObject->fwrite(']'); $this->requiresWhitespace = false; } /** * Writes a null value. */ public function writeNull() { if ($this->requiresWhitespace) { $this->fileObject->fwrite(' null'); } else { $this->fileObject->fwrite('null'); } $this->requiresWhitespace = true; } /** * Writes a boolean. * * @param bool $boolean */ public function writeBoolean($boolean) { if ($this->requiresWhitespace) { $this->fileObject->fwrite($boolean ? ' true' : ' false'); } else { $this->fileObject->fwrite($boolean ? 'true' : 'false'); } $this->requiresWhitespace = true; } /** * Writes a number. * * @param int|float $number * @throws InvalidArgumentException */ public function writeNumber($number) { if ($this->requiresWhitespace) { $this->fileObject->fwrite(' ' . (rtrim(sprintf('%.6F', $number), '0.') ?: '0')); } else { $this->fileObject->fwrite(rtrim(sprintf('%.6F', $number), '0.') ?: '0'); } $this->requiresWhitespace = true; } /** * Writes a name. * * @param string $name */ public function writeName($name) { $this->fileObject->fwrite('/' . $name); $this->requiresWhitespace = true; } /** * Writes a literal string. * * The string itself is splitted into multiple lines after 248 characters. We chose that specific limit to avoid * splitting mutli-byte characters in half. * * @param string $string */ public function writeLiteralString($string) { $this->fileObject->fwrite('(' . strtr($string, ['(' => '\\(', ')' => '\\)', '\\' => '\\\\']) . ')'); $this->requiresWhitespace = false; } /** * Writes a hexadecimal string. * * @param string $string */ public function writeHexadecimalString($string) { $this->fileObject->fwrite('<' . bin2hex($string) . '>'); $this->requiresWhitespace = false; } } ================================================ FILE: src/Writer/PageWriter.php ================================================ objectWriter = $objectWriter; $this->pageId = $this->objectWriter->allocateObjectId(); } /** * Sets a box for the page. * * @param string $name * @param Rectangle $box */ public function setBox($name, Rectangle $box) { $this->boxes[$name] = $box; } /** * Sets the rotation of the page. * * @param int $degrees * @throws DomainException */ public function setRotation($degrees) { if (!in_array($degrees, [0, 90, 180, 270])) { throw new DomainException('Degrees value must be a multiple of 90'); } $this->rotation = $degrees; } /** * Appends data to the content stream. * * @param string $data */ public function appendContentStream($data) { $this->contentStream .= $data; } /** * Writes the page contents and definition to the writer. * * @param ObjectWriter $objectWriter * @param int $pageTreeId * @return int */ public function writePage(ObjectWriter $objectWriter, $pageTreeId) { $objectWriter->startObject($this->pageId); $objectWriter->startDictionary(); $objectWriter->writeName('Type'); $objectWriter->writeName('Page'); $objectWriter->writeName('Parent'); $objectWriter->writeIndirectReference($pageTreeId); $objectWriter->writeName('Resources'); $objectWriter->startDictionary(); $objectWriter->endDictionary(); $objectWriter->writeName('Contents'); $objectWriter->startArray(); $objectWriter->endArray(); foreach ($this->boxes as $name => $box) { $objectWriter->writeName($name); $box->writeRectangleArray($objectWriter); } if (null !== $this->rotation) { $objectWriter->writeName('Rotate'); $objectWriter->writeNumber($this->rotation); } $objectWriter->endDictionary(); $objectWriter->endObject(); return $this->pageId; } } ================================================ FILE: test/Encryption/AbstractEncryptionTest.php ================================================ assertInstanceOf( Pdf11Encryption::class, AbstractEncryption::forPdfVersion('1.3', '', new EncryptionOptions('')) ); $this->assertInstanceOf( Pdf14Encryption::class, AbstractEncryption::forPdfVersion('1.4', '', new EncryptionOptions('')) ); $this->assertInstanceOf( Pdf14Encryption::class, AbstractEncryption::forPdfVersion('1.5', '', new EncryptionOptions('')) ); $this->assertInstanceOf( Pdf16Encryption::class, AbstractEncryption::forPdfVersion('1.6', '', new EncryptionOptions('')) ); $this->assertInstanceOf( Pdf16Encryption::class, AbstractEncryption::forPdfVersion('1.7', '', new EncryptionOptions('')) ); } public function testTooLongishUserPassword() { $this->setExpectedException(UnsupportedPasswordException::class, 'Password is longer than 32 characters'); $this->getAbstractEncryption()->__construct('', str_repeat('a', 33), '', Permissions::allowNothing()); } public function testTooLongishOwnerPassword() { $this->setExpectedException(UnsupportedPasswordException::class, 'Password is longer than 32 characters'); $this->getAbstractEncryption()->__construct('', '', str_repeat('a', 33), Permissions::allowNothing()); } public function testUserPasswordWithInvalidCharacters() { $this->setExpectedException(UnsupportedPasswordException::class, 'Password contains non-latin-1 characters'); $this->getAbstractEncryption()->__construct('', 'Ŧ', '', Permissions::allowNothing()); } public function testOwnerPasswordWithInvalidCharacters() { $this->setExpectedException(UnsupportedPasswordException::class, 'Password contains non-latin-1 characters'); $this->getAbstractEncryption()->__construct('', '', 'Ŧ', Permissions::allowNothing()); } public function testAbstractReturnsInvalidKeyLength() { $this->setExpectedException(UnexpectedValueException::class, 'Key length must be either 40 or 128'); $this->getAbstractEncryption(100)->__construct('', '', '', Permissions::allowNothing()); } /** * @return AbstractEncryption */ private function getAbstractEncryption($keyLength = 128) { $encryption = $this->getMockForAbstractClass(AbstractEncryption::class, [], '', false); $encryption->expects($this->any())->method('getKeyLength')->willReturn($keyLength); $encryption->expects($this->any())->method('getRevision')->willReturn(2); $encryption->expects($this->any())->method('getAlgorithm')->willReturn(1); return $encryption; } } ================================================ FILE: test/Encryption/AbstractEncryptionTestCase.php ================================================ createEncryption($userPassword, $ownerPassword); $reflectionClass = new ReflectionClass($encryption); $reflectionMethod = $reflectionClass->getMethod('computeIndividualEncryptionKey'); $reflectionMethod->setAccessible(true); $key = $reflectionMethod->invoke($encryption, $objectNumber, $generationNumber); $encryptedText = $encryption->encrypt($plaintext, $objectNumber, $generationNumber); $decryptedText = $this->decrypt($encryptedText, $key); $this->assertSame($plaintext, $decryptedText); } public function testWriteEncryptEntry() { $encryption = $this->createEncryption('foo', 'bar'); $memoryObjectWriter = new MemoryObjectWriter(); $encryption->writeEncryptEntry($memoryObjectWriter); $this->assertStringMatchesFormat($this->getExpectedEntry(), $memoryObjectWriter->getData()); } /** * @return array */ abstract public function encryptionTestData(); /** * @param string $userPassword * @param string|null $ownerPassword * @param Permissions|null $userPermissions * @return AbstractEncryption */ abstract protected function createEncryption( $userPassword, $ownerPassword = null, Permissions $userPermissions = null ); /** * @param string $encryptedText * @param string $key * @return string */ abstract protected function decrypt($encryptedText, $key); /** * @return string */ abstract protected function getExpectedEntry(); } ================================================ FILE: test/Encryption/BitMaskTest.php ================================================ assertSame(0, $bitMask->toInt()); } public function testSetBit() { $bitMask = new BitMask(); $bitMask->set(0, true); $bitMask->set(1, true); $this->assertSame(3, $bitMask->toInt()); $bitMask->set(0, false); $this->assertSame(2, $bitMask->toInt()); } } ================================================ FILE: test/Encryption/NullEncryptionTest.php ================================================ assertSame('foo', $encryption->encrypt('foo', 1, 1)); } public function testWriteEncryptEntryWritesNothing() { $encryption = new NullEncryption(); $objectWriter = new MemoryObjectWriter(); $encryption->writeEncryptEntry($objectWriter); $this->assertSame('', $objectWriter->getData()); } } ================================================ FILE: test/Encryption/Pdf11EncryptionTest.php ================================================ ['test', 'foo', null, 1, 1], 'changed-generation-number' => ['test', 'foo', null, 1, 2], 'changed-object-number' => ['test', 'foo', null, 2, 1], 'both-numbers-changed' => ['test', 'foo', null, 2, 2], 'changed-user-password' => ['test', 'bar', null, 1, 1], 'added-owner-password' => ['test', 'bar', 'baz', 1, 1], ]; } /** * {@inheritdoc} */ protected function createEncryption( $userPassword, $ownerPassword = null, Permissions $userPermissions = null ) { return new Pdf11Encryption( md5('test', true), $userPassword, $ownerPassword ?: $userPassword, $userPermissions ?: Permissions::allowNothing() ); } /** * {@inheritdoc} */ protected function decrypt($encryptedText, $key) { return openssl_decrypt($encryptedText, 'rc4', $key, OPENSSL_RAW_DATA); } /** * {@inheritdoc} */ protected function getExpectedEntry() { return file_get_contents(__DIR__ . '/_files/pdf11-encrypt-entry.txt'); } } ================================================ FILE: test/Encryption/Pdf14EncryptionTest.php ================================================ ['test', 'foo', null, 1, 1], 'changed-generation-number' => ['test', 'foo', null, 1, 2], 'changed-object-number' => ['test', 'foo', null, 2, 1], 'both-numbers-changed' => ['test', 'foo', null, 2, 2], 'changed-user-password' => ['test', 'bar', null, 1, 1], 'added-owner-password' => ['test', 'bar', 'baz', 1, 1], ]; } /** * {@inheritdoc} */ protected function createEncryption( $userPassword, $ownerPassword = null, Permissions $userPermissions = null ) { return new Pdf14Encryption( md5('test', true), $userPassword, $ownerPassword ?: $userPassword, $userPermissions ?: Permissions::allowNothing() ); } /** * {@inheritdoc} */ protected function decrypt($encryptedText, $key) { return openssl_decrypt($encryptedText, 'rc4', $key, OPENSSL_RAW_DATA); } /** * {@inheritdoc} */ protected function getExpectedEntry() { return file_get_contents(__DIR__ . '/_files/pdf14-encrypt-entry.txt'); } } ================================================ FILE: test/Encryption/Pdf16EncryptionTest.php ================================================ ['test', 'foo', null, 1, 1], 'changed-generation-number' => ['test', 'foo', null, 1, 2], 'changed-object-number' => ['test', 'foo', null, 2, 1], 'both-numbers-changed' => ['test', 'foo', null, 2, 2], 'changed-user-password' => ['test', 'bar', null, 1, 1], 'added-owner-password' => ['test', 'bar', 'baz', 1, 1], ]; } /** * {@inheritdoc} */ protected function createEncryption( $userPassword, $ownerPassword = null, Permissions $userPermissions = null ) { return new Pdf16Encryption( md5('test', true), $userPassword, $ownerPassword ?: $userPassword, $userPermissions ?: Permissions::allowNothing() ); } /** * {@inheritdoc} */ protected function decrypt($encryptedText, $key) { return openssl_decrypt( substr($encryptedText, 16), 'aes-128-cbc', $key, OPENSSL_RAW_DATA, substr($encryptedText, 0, 16) ); } /** * {@inheritdoc} */ protected function getExpectedEntry() { return file_get_contents(__DIR__ . '/_files/pdf16-encrypt-entry.txt'); } } ================================================ FILE: test/Encryption/PermissionsTest.php ================================================ assertSame(0, $permissions->toInt(2)); $this->assertSame(0, $permissions->toInt(3)); } public function testFullPermissions() { $permissions = Permissions::allowEverything(); $this->assertSame(60, $permissions->toInt(2)); $this->assertSame(3900, $permissions->toInt(3)); } /** * @dataProvider individualPermissions */ public function testIndividualPermissions($flagPosition, $rev2Value, $rev3Value) { $args = array_fill(0, 8, false); $args[$flagPosition] = true; $reflectionClass = new ReflectionClass(Permissions::class); $permissions = $reflectionClass->newInstanceArgs($args); $this->assertSame($rev2Value, $permissions->toInt(2)); $this->assertSame($rev3Value, $permissions->toInt(3)); } /** * @return array */ public function individualPermissions() { return [ 'may-print' => [0, 4, 4], 'may-print-high-resolution' => [1, 0, 2048], 'may-modify' => [2, 8, 8], 'may-copy' => [3, 16, 16], 'may-annotate' => [4, 32, 32], 'may-fill-in-forms' => [5, 0, 256], 'may-extract-for-accessibility' => [6, 0, 512], 'may-assemble' => [7, 0, 1024], ]; } } ================================================ FILE: test/Encryption/_files/pdf11-encrypt-entry.txt ================================================ /Encrypt << /Filter /Standard /V 1 /R 2 /O <947319c0b0ba83c01223fc7a3c39ef0a88ac4cc19e6b9e86f889d81f56ba57c4> /U <9dce9fbdfab50815486c48b3d9d8bb48a3d6f86e3a3768a3e8c8fb2b074b0658> /P 0 >> ================================================ FILE: test/Encryption/_files/pdf14-encrypt-entry.txt ================================================ /Encrypt << /Filter /Standard /V 2 /R 3 /O <85dafdd50f5179a0aacf58d9c59c34ab55274f38c85a0b2d9ae68606ecd290be> /U /P 0 /Length 128 >> ================================================ FILE: test/Encryption/_files/pdf16-encrypt-entry.txt ================================================ /Encrypt << /Filter /Standard /V 4 /R 4 /O <85dafdd50f5179a0aacf58d9c59c34ab55274f38c85a0b2d9ae68606ecd290be> /U <71f71069a89277d41eafa571fdeccaa0%x> /P 0 /Length 128 /CF << /StdCF << /Type /CryptFilter /CFM /AESV2 /Length 128 >> >> /StrF /StdCF /StmF /StdCF >> ================================================ FILE: test/TestHelper/MemoryObjectWriter.php ================================================ fileObject = new SplFileObject('php://memory', 'w+b'); } /** * {@inheritdoc} */ public function writeRawLine($data) { $this->fileObject->fwrite($data. "\n"); } /** * {@inheritdoc} */ public function currentOffset() { return $this->fileObject->ftell(); } /** * {@inheritdoc} */ public function startDictionary() { $this->fileObject->fwrite("<<\n"); } /** * {@inheritdoc} */ public function endDictionary() { $this->fileObject->fwrite(">>\n"); } /** * {@inheritdoc} */ public function startArray() { $this->fileObject->fwrite("]\n"); } /** * {@inheritdoc} */ public function endArray() { $this->fileObject->fwrite("[\n"); } /** * {@inheritdoc} */ public function writeNull() { $this->fileObject->fwrite("null\n"); } /** * {@inheritdoc} */ public function writeBoolean($boolean) { $this->fileObject->fwrite(($boolean ? 'true' : 'false') . "\n"); } /** * {@inheritdoc} */ public function writeNumber($number) { if (is_int($number)) { $value = (string) $number; } elseif (is_float($number)) { $value = sprintf('%F', $number); } else { throw new InvalidArgumentException(sprintf( 'Expected int or float, got %s', gettype($number) )); } $this->fileObject->fwrite($value . "\n"); } /** * {@inheritdoc} */ public function writeName($name) { $this->fileObject->fwrite('/' . $name . "\n"); } /** * {@inheritdoc} */ public function writeLiteralString($string) { $this->fileObject->fwrite('(' . strtr($string, ['(' => '\\(', ')' => '\\)', '\\' => '\\\\']) . ")\n"); } /** * {@inheritdoc} */ public function writeHexadecimalString($string) { $this->fileObject->fwrite('<' . bin2hex($string) . ">\n"); } /** * @return string */ public function getData() { $currentPos = $this->fileObject->ftell(); if ($currentPos === 0) { return ''; } $this->fileObject->fseek(0); $data = $this->fileObject->fread($currentPos); $this->fileObject->fseek($currentPos); return $data; } } ================================================ FILE: test/Writer/ObjectWriterTest.php ================================================ fileObject = new SplFileObject('php://memory', 'w+b'); $this->objectWriter = new ObjectWriter($this->fileObject); } public function testGetCurrentOffset() { $this->assertSame(0, $this->objectWriter->getCurrentOffset()); $this->fileObject->fwrite('foo'); $this->assertSame(3, $this->objectWriter->getCurrentOffset()); } public function testObjectNumberAllocation() { $this->assertSame(1, $this->objectWriter->allocateObjectId()); $this->assertSame(2, $this->objectWriter->allocateObjectId()); $this->assertSame(3, $this->objectWriter->allocateObjectId()); } public function testGetObjectOffsets() { $this->objectWriter->startObject(); $this->objectWriter->startObject(); $this->objectWriter->startObject(); $this->assertSame([1 => 0, 2 => 8, 3 => 16], $this->objectWriter->getObjectOffsets()); } public function testStartObjectWithoutObjectId() { $this->objectWriter->startObject(); $this->objectWriter->startObject(); $this->assertSame("1 0 obj\n2 0 obj\n", $this->getFileObjectData()); } public function testStartObjectWithObjectId() { $this->objectWriter->startObject(10); $this->assertSame("10 0 obj\n", $this->getFileObjectData()); } public function testWriteIndirectReference() { $this->objectWriter->writeIndirectReference(1); $this->assertSame('1 0 R', $this->getFileObjectData()); } public function testEndObject() { $this->objectWriter->endObject(); $this->assertSame("\nendobj\n", $this->getFileObjectData()); } public function testWriteRawLine() { $this->objectWriter->writeRawLine('foo'); $this->assertSame("foo\n", $this->getFileObjectData()); } public function testStartDictionary() { $this->objectWriter->startDictionary(); $this->assertSame('<<', $this->getFileObjectData()); } public function testEndDictionary() { $this->objectWriter->endDictionary(); $this->assertSame('>>', $this->getFileObjectData()); } public function testStartArray() { $this->objectWriter->startArray(); $this->assertSame('[', $this->getFileObjectData()); } public function testEndArray() { $this->objectWriter->endArray(); $this->assertSame(']', $this->getFileObjectData()); } public function testWriteNull() { $this->objectWriter->writeNull(); $this->assertSame('null', $this->getFileObjectData()); } public function testWriteBooleanTrue() { $this->objectWriter->writeBoolean(true); $this->assertSame('true', $this->getFileObjectData()); } public function testWriteBooleanFalse() { $this->objectWriter->writeBoolean(false); $this->assertSame('false', $this->getFileObjectData()); } public function testWriteIntegerNumber() { $this->objectWriter->writeNumber(0); $this->assertSame('0', $this->getFileObjectData()); $this->objectWriter->writeNumber(12); $this->assertSame('0 12', $this->getFileObjectData()); $this->objectWriter->writeNumber(0); $this->assertSame('0 12 0', $this->getFileObjectData()); } public function testWriteFloatNumber() { $this->objectWriter->writeNumber(12.3456789123); $this->assertSame('12.345679', $this->getFileObjectData()); $this->objectWriter->writeNumber(12.); $this->assertSame('12.345679 12', $this->getFileObjectData()); } public function testWriteName() { $this->objectWriter->writeName('foo'); $this->assertSame('/foo', $this->getFileObjectData()); } public function testWriteLiteralString() { $this->objectWriter->writeLiteralString('foo(bar\\baz)bat'); $this->assertSame('(foo\\(bar\\\\baz\\)bat)', $this->getFileObjectData()); } public function testWriteHexadecimalString() { $this->objectWriter->writeHexadecimalString('foo'); $this->assertSame('<666f6f>', $this->getFileObjectData()); } /** * @dataProvider whitespaceTestData */ public function testWhitespaceHandling(array $methodCalls, $expectedData) { foreach ($methodCalls as $methodCall) { if (!array_key_exists(1, $methodCall)) { $methodCall[1] = []; } call_user_func_array([$this->objectWriter, $methodCall[0]], $methodCall[1]); } $this->assertSame($expectedData, $this->getFileObjectData()); } /** * @return array */ public function whitespaceTestData() { return [ [[ ['writeIndirectReference', [1]], ['writeIndirectReference', [2]], ], '1 0 R 2 0 R'], [[ ['startDictionary'], ['endDictionary'], ], '<<>>'], [[ ['startArray'], ['endArray'], ], '[]'], [[ ['startDictionary'], ['writeNull'], ['endDictionary'], ], '<>'], [[ ['startArray'], ['writeNull'], ['endArray'], ], '[null]'], [[ ['writeNull'], ['writeNull'], ], 'null null'], [[ ['writeBoolean', [true]], ['writeBoolean', [false]], ], 'true false'], [[ ['writeNumber', [1]], ['writeNumber', [1.1]], ], '1 1.1'], [[ ['writeNumber', [0]], ['writeNumber', [0.0]], ], '0 0'], [[ ['writeLiteralString', ['foo']], ['writeLiteralString', ['bar']], ], '(foo)(bar)'], [[ ['writeHexadecimalString', ['foo']], ['writeHexadecimalString', ['bar']], ], '<666f6f><626172>'], [[ ['writeNull'], ['writeRawLine', ['foo']], ], "nullfoo\n"], ]; } /** * @return string */ private function getFileObjectData() { $offset = $this->fileObject->ftell(); $this->fileObject->fseek(0); $data = $this->fileObject->fread($offset); $this->fileObject->fseek($offset); return $data; } }