diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000000..0964d80653 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,23 @@ +[run] +source = PyPDF2 +branch = True + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + @overload + + # Don't complain about missing debug-only code: + def __repr__ + def __str__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if 0: + if __name__ == .__main__.: diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000000..f6da507c42 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,35 @@ +--- +name: Report a bug +about: Something broke! +title: "" +labels: Bug +assignees: MartinThoma + +--- + +Replace this: What happened? What were you trying to achieve? + +## Environment + +Which environment were you using when you encountered the problem? + +```python +$ python -m platform +# TODO: Your output goes here + +$ python -c "import PyPDF2;print(PyPDF2.__version__)" +# TODO: Your output goes here +``` + +## Code + +This is a minimal, complete example that shows the issue: + +```python +# TODO: Your code goes here +``` + +## PDF + +Share here the PDF file(s) that cause the issue. The smaller they are, the +better. Let us know if we may add them to our tests! diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000000..c167106a0f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,20 @@ +--- +name: Request a Feature +about: What do you think is missing in PyPDF2? +title: "" +labels: Feature Request +assignees: MartinThoma +--- + +## Explanation + +Explain briefly what you want to achive. + +## Code Example + +How would your feature be used? (Remove this if it is not applicable.) + +```python +from PyPDF2 import PdfFileReader, PdfFileWriter +... # your new feature in action! +``` \ No newline at end of file diff --git a/.github/workflows/github-ci.yaml b/.github/workflows/github-ci.yaml new file mode 100644 index 0000000000..19ca301443 --- /dev/null +++ b/.github/workflows/github-ci.yaml @@ -0,0 +1,98 @@ +# This workflow will install Python dependencies, run tests and lint with a variety of Python versions +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions + +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + tests: + name: pytest on ${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["2.7", "3.6", "3.7", "3.8", "3.9", "3.10"] + + steps: + - name: Checkout Code + uses: actions/checkout@v3 + - name: Setup Python + uses: actions/setup-python@v3 + with: + python-version: ${{ matrix.python-version }} + - name: Upgrade pip + run: | + python -m pip install --upgrade pip + - name: Install requirements (Python 3) + if: matrix.python-version != '2.7' + run: | + pip install -r requirements/ci.txt + - name: Install requirements (Python 2) + if: matrix.python-version == '2.7' + run: | + pip install pillow pytest coverage + - name: Install PyPDF2 + run: | + pip install . + - name: Test with flake8 + run: | + flake8 . --ignore=E203,W503,W504,E,F403,F405 --exclude build + if: matrix.python-version != '2.7' + - name: Test with pytest + run: | + python -m coverage run --parallel-mode -m pytest Tests -vv + - name: Upload coverage data + uses: actions/upload-artifact@v3 + with: + name: coverage-data + path: .coverage.* + if-no-files-found: ignore + + package: + name: Build & verify package + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + with: + python-version: ${{env.PYTHON_LATEST}} + + - run: python -m pip install build twine check-wheel-contents + - run: python -m build --sdist --wheel . + - run: ls -l dist + - run: check-wheel-contents dist/*.whl + - name: Check long_description + run: python -m twine check dist/* + + coverage: + name: Combine & check coverage. + runs-on: ubuntu-latest + needs: tests + + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v3 + with: + # Use latest Python, so it understands all syntax. + python-version: ${{env.PYTHON_LATEST}} + + - run: python -m pip install --upgrade coverage[toml] + + - uses: actions/download-artifact@v3 + with: + name: coverage-data + + - name: Combine coverage & create xml report + run: | + python -m coverage combine + python -m coverage xml + - name: Upload Coverage to Codecov + uses: codecov/codecov-action@v2 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml \ No newline at end of file diff --git a/.gitignore b/.gitignore index 43861330af..4a9c9ab80b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,29 @@ *.pyc *.swp .DS_Store +.tox build .idea/* +*.egg-info/ +dist/* +# +.mutmut-cache +mutmut-results.* + +# Code coverage artifacts +.coverage* +coverage.xml + +# Editors / IDEs +.vscode/ + +# Docs +docs/_build/ + +# Files generated by some of the scripts +dont_commit_merged.pdf +dont_commit_writer.pdf +PyPDF2-output.pdf +Image9.png +PyPDF2_pdfLocation.txt diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..ddd651483d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,39 @@ +# pre-commit run --all-files +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.2.0 + hooks: + - id: check-ast + - id: check-byte-order-marker + - id: check-case-conflict + - id: check-docstring-first + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + - id: trailing-whitespace + - id: mixed-line-ending + - id: check-added-large-files + args: ['--maxkb=1000'] +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + args: ["--ignore", "E,W,F"] +# - repo: https://github.com/pre-commit/mirrors-mypy +# rev: v0.942 +# hooks: +# - id: mypy +# - repo: https://github.com/psf/black +# rev: 22.3.0 +# hooks: +# - id: black +# - repo: https://github.com/asottile/pyupgrade +# rev: v2.31.1 +# hooks: +# - id: pyupgrade +# args: [--py36-plus] +- repo: https://github.com/asottile/blacken-docs + rev: v1.12.1 + hooks: + - id: blacken-docs + additional_dependencies: [black==22.1.0] diff --git a/.python-version b/.python-version new file mode 100644 index 0000000000..cd337510be --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.6.15 diff --git a/CHANGELOG b/CHANGELOG index fb89b6dc66..99f27268d1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,160 @@ -Patch 1.25.1, 2015-07-20 +Version 1.27.5, 2022-04-15 +-------------------------- + +Security (SEC): + +- ContentStream_readInlineImage had potential infinite loop (#740) + +Bug fixes (BUG): + +- Fix merging encrypted files (#757) +- CCITTFaxDecode decodeParms can be an ArrayObject (#756) + +Robustness improvements (ROBUST): + +- title sometimes None (#744) + +Documentation (DOC): + +- Adjust short description of the package + +Tests and Test setup (TST): + +- Rewrite JS tests from unittest to pytest (#746) +- Increase Test coverage, mainly with filters (#756) +- Add test for inline images (#758) + +Developer Experience Improvements (DEV): + +- Remove unused Travis-CI configuration (#747) +- Show code coverage (#754, #755) +- Add mutmut (#760) + +Miscellaneous: + +- STY: Closing file handles, explicit exports, ... (#743) + +All changes: https://github.com/py-pdf/PyPDF2/compare/1.27.4...1.27.5 + + +Version 1.27.4, 2022-04-12 +-------------------------- + +Bug fixes (BUG): + +- Guard formatting of __init__.__doc__ string (#738) + +Packaging (PKG): + +- Add more precise license field to setup (#733) + +Testing (TST): + +- Add test for issue #297 + +Miscellaneous: + +- DOC: Miscallenious ➔ Miscellaneous (Typo) +- TST: Fix CI triggering (master ➔ main) (#739) +- STY: Fix various style issues (#742) + +All changes: https://github.com/py-pdf/PyPDF2/compare/1.27.3...1.27.4 + +Version 1.27.0, 2022-04-07 +-------------------------- + +Features: + + - Add alpha channel support for png files in Script (#614) + +Bug fixes (BUG): + + - Fix formatWarning for filename without slash (#612) + - Add whitespace between words for extractText() (#569, #334) + - "invalid escape sequence" SyntaxError (#522) + - Avoid error when printing warning in pythonw (#486) + - Stream operations can be List or Dict (#665) + +Documentation (DOC): + + - Added Scripts/pdf-image-extractor.py + - Documentation improvements (#550, #538, #324, #426, #394) + +Tests and Test setup (TST): + + - Add Github Action which automatically run unit tests via pytest and + static code analysis with Flake8 (#660) + - Add several unit tests (#661, #663) + - Add .coveragerc to create coverage reports + +Developer Experience Improvements (DEV): + + - Pre commit: Developers can now `pre-commit install` to avoid tiny issues + like trailing whitespaces + +Miscellaneous: + + - Add the LICENSE file to the distributed packages (#288) + - Use setuptools instead of distutils (#599) + - Improvements for the PyPI page (#644) + - Python 3 changes (#504, #366) + +You can see the full changelog at: https://github.com/py-pdf/PyPDF2/compare/1.26.0...1.27.0 + +Patch release 1.27.1, 2022-04-08 + +- Fixed project links on PyPI page after migration from mstamy2 + to MartinThoma to the py-pdf organization on GitHub +- Documentation is now at https://pypdf2.readthedocs.io/en/latest/ + +Patch release 1.27.2, 2022-04-09 + +- Add Scripts (including `pdfcat`), Resources, Tests, and Sample_Code back to + PyPDF2. It was removed by accident in 1.27.0, but might get removed with 2.0.0 + See https://github.com/py-pdf/PyPDF2/discussions/718 for discussion + +Patch release 1.27.3, 2022-04-10 + +- PKG: Make Tests not a subpackage (#728) +- BUG: Fix ASCII85Decode.decode assertion (#729) +- BUG: Error in Chinese character encoding (#463) +- BUG: Code duplication in Scripts/2-up.py +- ROBUST: Guard 'obj.writeToStream' with 'if obj is not None' +- ROBUST: Ignore a /Prev entry with the value 0 in the trailer +- MAINT: Remove Sample_Code (#726) +- TST: Close file handle in test_writer (#722) +- TST: Fix test_get_images (#730) +- DEV: Make tox use pytest and add more Python versions (#721) +- DOC: Many (#720, #723-725, #469) + +Version 1.26.0, 2016-05-18 +-------------------------- + + - NOTE: Active maintenance on PyPDF2 is resuming after a hiatus + + - Fixed a bug where image resources where incorrectly + overwritten when merging pages + + - Added dictionary for JavaScript actions to the root (louib) + + - Added unit tests for the JS functionality (louib) + + - Add more Python 3 compatibility when reading inline images (im2703 + and (VyacheslavHashov) + + - Return NullObject instead of raising error when failing to resolve + object (ctate) + + - Don't output warning for non-zeroed xref table when strict=False + (BenRussert) + + - Remove extraneous zeroes from output formatting (speedplane) + + - Fix bug where reading an inline image would cut off prematurely + in certain cases (speedplane) + + +Patch 1.25.1, 2015-07-20 - Fix bug when parsing inline images. Occurred when merging certain pages with inline images @@ -141,7 +297,7 @@ Version 1.23, 2014-08-11 - Annotations (links, comment windows, etc.) are now preserved when pages are merged together - - Used the Destination class in addLink() and addBookmark() so that + - Used the Destination class in addLink() and addBookmark() so that the page fit option could be properly customized @@ -161,7 +317,7 @@ Version 1.22, 2014-05-29 - Fixed bug where an exception was thrown upon reading a NULL string (by speedplane) - - Allow string literals (non-unicode strings in Python 2) to be passed + - Allow string literals (non-unicode strings in Python 2) to be passed to PdfFileReader - Allow ConvertFunctionsToVirtualList to be indexed with slices and @@ -172,7 +328,7 @@ Version 1.22, 2014-05-29 - General code clean-up and improvements (with Steve Witham and Henry Keiter) - - Fixed bug that caused crash when comments are present at end of + - Fixed bug that caused crash when comments are present at end of dictionary @@ -291,7 +447,7 @@ OTHER: UPCOMING: - More bugfixes (We have received many problematic PDFs via email, we will work with them) - + - Documentation - It's time for PyPDF2 to get its own documentation since it has grown much since the original pyPdf @@ -301,11 +457,11 @@ UPCOMING: Version 1.18, 2013-08-19 ------------------------ - - Fixed a bug where older verions of objects were incorrectly added to the + - Fixed a bug where older verions of objects were incorrectly added to the cache, resulting in outdated or missing pages, images, and other objects (from speedplane) - - Fixed a bug in parsing the xref table where new xref values were + - Fixed a bug in parsing the xref table where new xref values were overwritten; also cleaned up code (from speedplane) - New method mergeRotatedAroundPointPage which merges a page while rotating @@ -328,8 +484,8 @@ Other Changes: Version 1.17, 2013-07-25 ------------------------ - - Removed one (from pdf.py) of the two Destination classes. Both - classes had the same name, but were slightly different in content, + - Removed one (from pdf.py) of the two Destination classes. Both + classes had the same name, but were slightly different in content, causing some errors. (from Janne Vanhala) - Corrected and Expanded README file to demonstrate PdfFileMerger @@ -346,9 +502,9 @@ Versions -1.16, -2013-06-30 - Note: This ChangeLog has not been kept up-to-date for a while. Hopefully we can keep better track of it from now on. Some of the changes listed here come from previous versions 1.14 and 1.15; they - were only vaguely defined. With the new _version.py file we should + were only vaguely defined. With the new _version.py file we should have more structured and better documented versioning from now on. - + - Defined PyPDF2.__version__ - Fixed encrypt() method (from Martijn The) @@ -361,14 +517,14 @@ Versions -1.16, -2013-06-30 - Fixed an bug caused by DecimalError Exception (from Adam Morris) - - Many other bug fixes and features by: - + - Many other bug fixes and features by: + jeansch Anton Vlasenko Joseph Walton Jan Oliver Oelerich Fabian Henze - And any others I missed. + And any others I missed. Thanks for contributing! @@ -508,7 +664,7 @@ Version 1.6, 2006-06-06 stream filters more easily, including compressed streams. - Add a graphics state push/pop around page merges. Improves quality of - page merges when one page's content stream leaves the graphics + page merges when one page's content stream leaves the graphics in an abnormal state. - Add PageObject.compressContentStreams function, which filters all content @@ -601,4 +757,3 @@ Version 1.0, 2006-01-17 - Does not support some PDF 1.5 features, such as object streams, cross-reference streams. - diff --git a/MANIFEST.in b/MANIFEST.in index f7aec421fe..c41e937399 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,5 @@ include CHANGELOG +include LICENSE +recursive-include Resources * +recursive-include Scripts * +recursive-include Tests * diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..1a9b5244f4 --- /dev/null +++ b/Makefile @@ -0,0 +1,24 @@ +maint: + pre-commit autoupdate + pip-compile -U requirements/ci.in + pip-compile -U requirements/dev.in + pip-compile -U requirements/docs.in + +upload: + make clean + python setup.py sdist bdist_wheel && twine upload -s dist/* + +clean: + python setup.py clean --all + pyclean . + rm -rf Tests/__pycache__ PyPDF2/__pycache__ Image9.png htmlcov docs/_build dist dont_commit_merged.pdf dont_commit_writer.pdf PyPDF2.egg-info PyPDF2_pdfLocation.txt + +test: + pytest Tests --cov --cov-report term-missing -vv --cov-report html --durations=3 --timeout=30 + +mutation-test: + mutmut run + +mutmut-results: + mutmut junitxml --suspicious-policy=ignore --untested-policy=ignore > mutmut-results.xml + junit2html mutmut-results.xml mutmut-results.html diff --git a/PDF_Samples/GeoBase_NHNC1_Data_Model_UML_EN.pdf b/PDF_Samples/GeoBase_NHNC1_Data_Model_UML_EN.pdf new file mode 100644 index 0000000000..608056d8f2 Binary files /dev/null and b/PDF_Samples/GeoBase_NHNC1_Data_Model_UML_EN.pdf differ diff --git a/PDF_Samples/README.txt b/PDF_Samples/README.md similarity index 58% rename from PDF_Samples/README.txt rename to PDF_Samples/README.md index 78f03d7fbb..640d110334 100644 --- a/PDF_Samples/README.txt +++ b/PDF_Samples/README.md @@ -1,5 +1,4 @@ -PDF Sample Folder ------------------ +# PDF Sample Folder PDF files are generated by a large variety of sources for many different purposes. One of the goals of PyPDF2 @@ -12,15 +11,7 @@ may be complicated or unconventional files, or they may just be good for testing. The purpose is to insure that when changes to PyPDF2 are made, we keep them in mind. -If you have confidential PDFs that don't work with -PyPDF2, feel free to still e-mail them for debugging - -we won't add PDFs without expressed permission. - (This folder is available through GitHub only) - -Feel free to add any type of PDF file or sample code, -either by - - 1) sending it via email to PyPDF2@phaseit.net - 2) including it in a pull request on GitHub \ No newline at end of file +Feel free to add any type of PDF file +either by including it in a pull request on GitHub diff --git a/PDF_Samples/Seige_of_Vicksburg_Sample_OCR.pdf b/PDF_Samples/Seige_of_Vicksburg_Sample_OCR.pdf new file mode 100644 index 0000000000..234f39b75b Binary files /dev/null and b/PDF_Samples/Seige_of_Vicksburg_Sample_OCR.pdf differ diff --git a/PDF_Samples/jpeg.pdf b/PDF_Samples/jpeg.pdf new file mode 100644 index 0000000000..07a7fbbcb0 Binary files /dev/null and b/PDF_Samples/jpeg.pdf differ diff --git a/PyPDF2/__init__.py b/PyPDF2/__init__.py index f458c0ea6e..07f211c2b6 100644 --- a/PyPDF2/__init__.py +++ b/PyPDF2/__init__.py @@ -1,5 +1,14 @@ -from .pdf import PdfFileReader, PdfFileWriter +from ._version import __version__ from .merger import PdfFileMerger from .pagerange import PageRange, parse_filename_page_ranges -from ._version import __version__ -__all__ = ["pdf", "PdfFileMerger"] +from .pdf import PdfFileReader, PdfFileWriter + +__all__ = [ + "__version__", + "PageRange", + "parse_filename_page_ranges", + "pdf", + "PdfFileMerger", + "PdfFileReader", + "PdfFileWriter", +] diff --git a/PyPDF2/_version.py b/PyPDF2/_version.py index 760870ccf1..4205e7e6c4 100644 --- a/PyPDF2/_version.py +++ b/PyPDF2/_version.py @@ -1 +1 @@ -__version__ = '1.25.1' +__version__ = "1.27.5" diff --git a/PyPDF2/constants.py b/PyPDF2/constants.py new file mode 100644 index 0000000000..77fc028436 --- /dev/null +++ b/PyPDF2/constants.py @@ -0,0 +1,186 @@ +""" +See Portable Document Format Reference Manual, 1993. ISBN 0-201-62628-4. + +See https://ia802202.us.archive.org/8/items/pdfy-0vt8s-egqFwDl7L2/PDF%20Reference%201.0.pdf + +PDF Reference, third edition, Version 1.4, 2001. ISBN 0-201-75839-3. + +PDF Reference, sixth edition, Version 1.7, 2006. +""" + + +class PagesAttributes: + """Page Attributes, Table 6.2, Page 52""" + + TYPE = "/Type" # name, required; must be /Pages + KIDS = "/Kids" # array, required; List of indirect references + COUNT = "/Count" # integer, required; the number of all nodes und this node + PARENT = "/Parent" # dictionary, required; indirect reference to pages object + + +class PageAttributes: + """Page attributes, Table 6.3, Page 53""" + + TYPE = "/Type" # name, required; must be /Page + MEDIABOX = "/MediaBox" # array, required; rectangle specifying page size + PARENT = "/Parent" # dictionary, required; a pages object + RESOURCES = "/Resources" # dictionary, required if there are any + CONTENTS = "/Contents" # stream or array, optional + CROPBOX = "/CropBox" # array, optional; rectangle + ROTATE = "/Rotate" # integer, optional; page rotation in degrees + THUMB = "/Thumb" # stream, optional; indirect reference to image of the page + ANNOTS = "/Annots" # array, optional; an array of annotations + + +class Ressources: + PROCSET = "/ProcSet" # Chapter 6.8.1 + FONT = "/Font" # Chapter 6.8.2 + # encoding + # font descriptors : 6.8.4 + COLOR_SPACE = "/ColorSpace" # Chapter 6.8.5 + XOBJECT = "/XObject" # Chapter 6.8.6 + + +class StreamAttributes: + """Table 4.2""" + + LENGTH = "/Length" # integer, required + FILTER = "/Filter" # name or array of names, optional + DECODE_PARMS = "/DecodeParms" # variable, optional -- 'decodeParams is wrong + + +class FilterTypes: + """ + Table 4.3 of the 1.4 Manual + + Page 354 of the 1.7 Manual + """ + + ASCII_HEX_DECODE = "/ASCIIHexDecode" # abbreviation: AHx + ASCII_85_DECODE = "/ASCII85Decode" # abbreviation: A85 + LZW_DECODE = "/LZWDecode" # abbreviation: LZW + FLATE_DECODE = "/FlateDecode" # abbreviation: Fl, PDF 1.2 + RUN_LENGTH_DECODE = "/RunLengthDecode" # abbreviation: RL + CCITT_FAX_DECODE = "/CCITTFaxDecode" # abbreviation: CCF + DCT_DECODE = "/DCTDecode" # abbreviation: DCT + + +class FilterTypeAbbreviations: + """ + Table 4.44 of the 1.7 Manual (page 353ff) + """ + + AHx = "/AHx" + A85 = "/A85" + LZW = "/LZW" + FL = "/Fl" # FlateDecode + RL = "/RL" + CCF = "/CCF" + DCT = "/DCT" + + +class LzwFilterParameters: + """Table 4.4""" + + PREDICTOR = "/Predictor" # integer + COLUMNS = "/Columns" # integer + COLORS = "/Colors" # integer + BITS_PER_COMPONENT = "/BitsPerComponent" # integer + EARLY_CHANGE = "/EarlyChange" # integer + + +class CcittFaxDecodeParameters: + """Table 4.5""" + + K = "/K" # integer + END_OF_LINE = "/EndOfLine" # boolean + ENCODED_BYTE_ALIGN = "/EncodedByteAlign" # boolean + COLUMNS = "/Columns" # integer + ROWS = "/Rows" # integer + END_OF_BLOCK = "/EndOfBlock" # boolean + BLACK_IS_1 = "/BlackIs1" # boolean + DAMAGED_ROWS_BEFORE_ERROR = "/DamagedRowsBeforeError" # integer + + +class ImageAttributes: + """Table 6.20.""" + + TYPE = "/Type" # name, required; must be /XObject + SUBTYPE = "/Subtype" # name, required; must be /Image + NAME = "/Name" # name, required + WIDTH = "/Width" # integer, required + HEIGHT = "/Height" # integer, required + BITS_PER_COMPONENT = "/BitsPerComponent" # integer, required + COLOR_SPACE = "/ColorSpace" # name, required + DECODE = "/Decode" # array, optional + INTERPOLATE = "/Interpolate" # boolean, optional + IMAGE_MASK = "/ImageMask" # boolean, optional + + +class ColorSpaces: + DEVICE_RGB = "/DeviceRGB" + DEVICE_CMYK = "/DeviceCMYK" + DEVICE_GRAY = "/DeviceGray" + + +class TypArguments: + """Table 8.2 of the PDF 1.7 reference""" + + LEFT = "/Left" + RIGHT = "/Right" + BOTTOM = "/Bottom" + TOP = "/Top" + + +class TypFitArguments: + """Table 8.2 of the PDF 1.7 reference""" + + FIT = "/Fit" + FIT_V = "/FitV" + FIT_BV = "/FitBV" + FIT_B = "/FitB" + FIT_H = "/FitH" + FIT_BH = "/FitBH" + FIT_R = "/FitR" + + +class PageLayouts: + """Page 84, PDF 1.4 reference""" + + SINGLE_PAGE = "/SinglePage" + ONE_COLUMN = "/OneColumn" + TWO_COLUMN_LEFT = "/TwoColumnLeft" + TWO_COLUMN_RIGHT = "/TwoColumnRight" + + +class GraphicsStateParameters: + """Table 4.8 of the 1.7 reference""" + + TYPE = "/Type" # name, optional + LW = "/LW" # number, optional + # TODO: Many more! + FONT = "/Font" # array, optional + S_MASK = "/SMask" # dictionary or name, optional + + +class CatalogDictionary: + """Table 3.25 in the 1.7 reference""" + + TYPE = "/Type" # name, required; must be /Catalog + # TODO: Many more! + + +PDF_KEYS = [ + PagesAttributes, + PageAttributes, + Ressources, + ImageAttributes, + StreamAttributes, + FilterTypes, + LzwFilterParameters, + TypArguments, + TypFitArguments, + PageLayouts, + GraphicsStateParameters, + CatalogDictionary, +] diff --git a/PyPDF2/filters.py b/PyPDF2/filters.py index 3717fd4c58..bb56234a76 100644 --- a/PyPDF2/filters.py +++ b/PyPDF2/filters.py @@ -1,5 +1,3 @@ -# vim: sw=4:expandtab:foldmethod=marker -# # Copyright (c) 2006, Mathieu Fenniak # All rights reserved. # @@ -28,19 +26,29 @@ # POSSIBILITY OF SUCH DAMAGE. -""" -Implementation of stream filters for PDF. -""" +"""Implementation of stream filters for PDF.""" __author__ = "Mathieu Fenniak" __author_email__ = "biziqe@mathieu.fenniak.net" -from .utils import PdfReadError, ord_, chr_ +import math from sys import version_info + +from PyPDF2.constants import CcittFaxDecodeParameters as CCITT +from PyPDF2.constants import ColorSpaces +from PyPDF2.constants import FilterTypeAbbreviations as FTA +from PyPDF2.constants import FilterTypes as FT +from PyPDF2.constants import ImageAttributes as IA +from PyPDF2.constants import LzwFilterParameters as LZW +from PyPDF2.constants import StreamAttributes as SA + +from .utils import PdfReadError, ord_, paethPredictor + if version_info < ( 3, 0 ): from cStringIO import StringIO else: from io import StringIO - import struct + +import struct try: import zlib @@ -55,7 +63,7 @@ def compress(data): # Unable to import zlib. Attempt to use the System.IO.Compression # library from the .NET framework. (IronPython only) import System - from System import IO, Collections, Array + from System import IO, Array def _string_to_bytearr(buf): retval = Array.CreateInstance(System.Byte, len(buf)) @@ -112,13 +120,18 @@ def decode(data, decodeParms): predictor = 1 if decodeParms: try: - predictor = decodeParms.get("/Predictor", 1) + from PyPDF2.generic import ArrayObject + if isinstance(decodeParms, ArrayObject): + for decodeParm in decodeParms: + if '/Predictor' in decodeParm: + predictor = decodeParm['/Predictor'] + else: + predictor = decodeParms.get("/Predictor", 1) except AttributeError: - pass # usually an array with a null object was read - + pass # usually an array with a null object was read # predictor 1 == no predictor if predictor != 1: - columns = decodeParms["/Columns"] + columns = decodeParms[LZW.COLUMNS] # PNG prediction: if predictor >= 10 and predictor <= 15: output = StringIO() @@ -137,6 +150,18 @@ def decode(data, decodeParms): elif filterByte == 2: for i in range(1, rowlength): rowdata[i] = (rowdata[i] + prev_rowdata[i]) % 256 + elif filterByte == 3: + for i in range(1, rowlength): + left = rowdata[i-1] if i > 1 else 0 + floor = math.floor(left + prev_rowdata[i])/2 + rowdata[i] = (rowdata[i] + int(floor)) % 256 + elif filterByte == 4: + for i in range(1, rowlength): + left = rowdata[i - 1] if i > 1 else 0 + up = prev_rowdata[i] + up_left = prev_rowdata[i - 1] if i > 1 else 0 + paeth = paethPredictor(left, up, up_left) + rowdata[i] = (rowdata[i] + paeth) % 256 else: # unsupported PNG filter raise PdfReadError("Unsupported PNG filter %r" % filterByte) @@ -147,11 +172,11 @@ def decode(data, decodeParms): # unsupported predictor raise PdfReadError("Unsupported flatedecode predictor %r" % predictor) return data - decode = staticmethod(decode) + decode = staticmethod(decode) # type: ignore def encode(data): return compress(data) - encode = staticmethod(encode) + encode = staticmethod(encode) # type: ignore class ASCIIHexDecode(object): @@ -173,7 +198,7 @@ def decode(data, decodeParms=None): x += 1 assert char == "" return retval - decode = staticmethod(decode) + decode = staticmethod(decode) # type: ignore class LZWDecode(object): @@ -202,7 +227,7 @@ def nextCode(self): while fillbits>0 : if self.bytepos >= len(self.data): return -1 - nextbits=ord(self.data[self.bytepos]) + nextbits=ord_(self.data[self.bytepos]) bitsfromhere=8-self.bitpos if bitsfromhere>fillbits: bitsfromhere=fillbits @@ -221,17 +246,17 @@ def decode(self): http://www.rasip.fer.hr/research/compress/algorithms/fund/lz/lzw.html and the PDFReference """ - cW = self.CLEARDICT; + cW = self.CLEARDICT baos="" while True: - pW = cW; - cW = self.nextCode(); + pW = cW + cW = self.nextCode() if cW == -1: raise PdfReadError("Missed the stop code in LZWDecode!") if cW == self.STOP: - break; + break elif cW == self.CLEARDICT: - self.resetDict(); + self.resetDict() elif pW == self.CLEARDICT: baos+=self.dict[cW] else: @@ -243,7 +268,7 @@ def decode(self): else: p=self.dict[pW]+self.dict[pW][0] baos+=p - self.dict[self.dictlen] = p; + self.dict[self.dictlen] = p self.dictlen+=1 if (self.dictlen >= (1 << self.bitspercode) - 1 and self.bitspercode < 12): @@ -251,7 +276,7 @@ def decode(self): return baos @staticmethod - def decode(data,decodeParams=None): + def decode(data, decodeParms=None): return LZWDecode.decoder(data).decode() @@ -263,13 +288,13 @@ def decode(data, decodeParms=None): x = 0 hitEod = False # remove all whitespace from data - data = [y for y in data if not (y in ' \n\r\t')] + data = [y for y in data if y not in ' \n\r\t'] while not hitEod: c = data[x] if len(retval) == 0 and c == "<" and data[x+1] == "~": x += 2 continue - #elif c.isspace(): + # elif c.isspace(): # x += 1 # continue elif c == 'z': @@ -296,7 +321,7 @@ def decode(data, decodeParms=None): group[2] * (85**2) + \ group[3] * 85 + \ group[4] - assert b < (2**32 - 1) + assert b <= (2**32 - 1) c4 = chr((b >> 0) % 256) c3 = chr((b >> 8) % 256) c2 = chr((b >> 16) % 256) @@ -329,12 +354,64 @@ def decode(data, decodeParms=None): out += struct.pack(b'>L',b)[:n-1] break return bytes(out) - decode = staticmethod(decode) + decode = staticmethod(decode) # type: ignore + +class DCTDecode(object): + def decode(data, decodeParms=None): + return data + decode = staticmethod(decode) # type: ignore + +class JPXDecode(object): + def decode(data, decodeParms=None): + return data + decode = staticmethod(decode) # type: ignore +class CCITTFaxDecode(object): + def decode(data, decodeParms=None, height=0): + k = 1 + width = 0 + if decodeParms: + from PyPDF2.generic import ArrayObject + if isinstance(decodeParms, ArrayObject): + for decodeParm in decodeParms: + if CCITT.COLUMNS in decodeParm: + width = decodeParm[CCITT.COLUMNS] + if CCITT.K in decodeParm: + k = decodeParm[CCITT.K] + else: + width = decodeParms[CCITT.COLUMNS] + k = decodeParms[CCITT.K] + if k == -1: + CCITTgroup = 4 + else: + CCITTgroup = 3 + + imgSize = len(data) + tiff_header_struct = '<2shlh' + 'hhll' * 8 + 'h' + tiffHeader = struct.pack(tiff_header_struct, + b'II', # Byte order indication: Little endian + 42, # Version number (always 42) + 8, # Offset to first IFD + 8, # Number of tags in IFD + 256, 4, 1, width, # ImageWidth, LONG, 1, width + 257, 4, 1, height, # ImageLength, LONG, 1, length + 258, 3, 1, 1, # BitsPerSample, SHORT, 1, 1 + 259, 3, 1, CCITTgroup, # Compression, SHORT, 1, 4 = CCITT Group 4 fax encoding + 262, 3, 1, 0, # Thresholding, SHORT, 1, 0 = WhiteIsZero + 273, 4, 1, struct.calcsize(tiff_header_struct), # StripOffsets, LONG, 1, length of header + 278, 4, 1, height, # RowsPerStrip, LONG, 1, length + 279, 4, 1, imgSize, # StripByteCounts, LONG, 1, size of image + 0 # last IFD + ) + + return tiffHeader + data + + decode = staticmethod(decode) # type: ignore def decodeStreamData(stream): from .generic import NameObject - filters = stream.get("/Filter", ()) + filters = stream.get(SA.FILTER, ()) + if len(filters) and not isinstance(filters[0], NameObject): # we have a single filter instance filters = (filters,) @@ -342,17 +419,24 @@ def decodeStreamData(stream): # If there is not data to decode we should not try to decode the data. if data: for filterType in filters: - if filterType == "/FlateDecode" or filterType == "/Fl": - data = FlateDecode.decode(data, stream.get("/DecodeParms")) - elif filterType == "/ASCIIHexDecode" or filterType == "/AHx": + if filterType == FT.FLATE_DECODE or filterType == FTA.FL: + data = FlateDecode.decode(data, stream.get(SA.DECODE_PARMS)) + elif filterType == FT.ASCII_HEX_DECODE or filterType == FTA.AHx: data = ASCIIHexDecode.decode(data) - elif filterType == "/LZWDecode" or filterType == "/LZW": - data = LZWDecode.decode(data, stream.get("/DecodeParms")) - elif filterType == "/ASCII85Decode" or filterType == "/A85": + elif filterType == FT.LZW_DECODE or filterType == FTA.LZW: + data = LZWDecode.decode(data, stream.get(SA.DECODE_PARMS)) + elif filterType == FT.ASCII_85_DECODE or filterType == FTA.A85: data = ASCII85Decode.decode(data) + elif filterType == FT.DCT_DECODE: + data = DCTDecode.decode(data) + elif filterType == "/JPXDecode": + data = JPXDecode.decode(data) + elif filterType == FT.CCITT_FAX_DECODE: + height = stream.get(IA.HEIGHT, ()) + data = CCITTFaxDecode.decode(data, stream.get(SA.DECODE_PARMS), height) elif filterType == "/Crypt": - decodeParams = stream.get("/DecodeParams", {}) - if "/Name" not in decodeParams and "/Type" not in decodeParams: + decodeParms = stream.get(SA.DECODE_PARMS, {}) + if "/Name" not in decodeParms and "/Type" not in decodeParms: pass else: raise NotImplementedError("/Crypt filter with /Name or /Type not supported yet") @@ -360,3 +444,55 @@ def decodeStreamData(stream): # unsupported filter raise NotImplementedError("unsupported filter %s" % filterType) return data + + +def _xobj_to_image(x_object_obj): + """ + Users need to have the pillow package installed. + + It's unclear if PyPDF2 will keep this function here, hence it's private. + It might get removed at any point. + + :return: Tuple[file extension, bytes] + """ + import io + + from PIL import Image + + from PyPDF2.constants import GraphicsStateParameters as G + + size = (x_object_obj[IA.WIDTH], x_object_obj[IA.HEIGHT]) + data = x_object_obj.getData() + if x_object_obj[IA.COLOR_SPACE] == ColorSpaces.DEVICE_RGB: + mode = "RGB" + else: + mode = "P" + extension = None + if SA.FILTER in x_object_obj: + if x_object_obj[SA.FILTER] == FT.FLATE_DECODE: + extension = ".png" + img = Image.frombytes(mode, size, data) + if G.S_MASK in x_object_obj: # add alpha channel + alpha = Image.frombytes("L", size, x_object_obj[G.S_MASK].getData()) + img.putalpha(alpha) + img_byte_arr = io.BytesIO() + img.save(img_byte_arr, format="PNG") + data = img_byte_arr.getvalue() + elif x_object_obj[SA.FILTER] in ([FT.LZW_DECODE], [FT.ASCII_85_DECODE], [FT.CCITT_FAX_DECODE]): + from PyPDF2.utils import b_ + extension = ".png" + data = b_(data) + elif x_object_obj[SA.FILTER] == FT.DCT_DECODE: + extension = ".jpg" + elif x_object_obj[SA.FILTER] == "/JPXDecode": + extension = ".jp2" + elif x_object_obj[SA.FILTER] == FT.CCITT_FAX_DECODE: + extension = ".tiff" + else: + extension = ".png" + img = Image.frombytes(mode, size, data) + img_byte_arr = io.BytesIO() + img.save(img_byte_arr, format="PNG") + data = img_byte_arr.getvalue() + + return extension, data diff --git a/PyPDF2/generic.py b/PyPDF2/generic.py index c4332297d3..09a38ac0d0 100644 --- a/PyPDF2/generic.py +++ b/PyPDF2/generic.py @@ -1,5 +1,3 @@ -# vim: sw=4:expandtab:foldmethod=marker -# # Copyright (c) 2006, Mathieu Fenniak # All rights reserved. # @@ -34,21 +32,22 @@ __author__ = "Mathieu Fenniak" __author_email__ = "biziqe@mathieu.fenniak.net" +import codecs +import decimal import re -from .utils import readNonWhitespace, RC4_encrypt, skipOverComment -from .utils import b_, u_, chr_, ord_ -from .utils import PdfStreamError import warnings -from . import filters -from . import utils -import decimal -import codecs -import sys -#import debugging + +from PyPDF2.constants import FilterTypes as FT +from PyPDF2.constants import StreamAttributes as SA +from PyPDF2.utils import ERR_STREAM_TRUNCATED_PREMATURELY + +from . import filters, utils +from .utils import (PdfStreamError, RC4_encrypt, b_, chr_, ord_, + readNonWhitespace, skipOverComment, u_) ObjectPrefix = b_('/<[tf(n%') NumberSigns = b_('+-') -IndirectPattern = re.compile(b_(r"(\d+)\s+(\d+)\s+R[^a-zA-Z]")) +IndirectPattern = re.compile(b_(r"[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z]")) def readObject(stream, pdf): @@ -82,17 +81,18 @@ def readObject(stream, pdf): # comment while tok not in (b_('\r'), b_('\n')): tok = stream.read(1) + # Prevents an infinite loop by raising an error if the stream is at + # the EOF + if len(tok) <= 0: + raise PdfStreamError("File ended unexpectedly.") tok = readNonWhitespace(stream) stream.seek(-1, 1) return readObject(stream, pdf) else: # number object OR indirect reference - if tok in NumberSigns: - # number - return NumberObject.readFromStream(stream) peek = stream.read(20) stream.seek(-len(peek), 1) # reset to start - if IndirectPattern.match(peek) != None: + if IndirectPattern.match(peek) is not None: return IndirectObject.readFromStream(stream, pdf) else: return NumberObject.readFromStream(stream) @@ -113,7 +113,7 @@ def readFromStream(stream): if nulltxt != b_("null"): raise utils.PdfReadError("Could not read Null object") return NullObject() - readFromStream = staticmethod(readFromStream) + readFromStream = staticmethod(readFromStream) # type: ignore class BooleanObject(PdfObject): @@ -135,7 +135,7 @@ def readFromStream(stream): return BooleanObject(False) else: raise utils.PdfReadError('Could not read Boolean object') - readFromStream = staticmethod(readFromStream) + readFromStream = staticmethod(readFromStream) # type: ignore class ArrayObject(list, PdfObject): @@ -165,7 +165,7 @@ def readFromStream(stream, pdf): # read and append obj arr.append(readObject(stream, pdf)) return arr - readFromStream = staticmethod(readFromStream) + readFromStream = staticmethod(readFromStream) # type: ignore class IndirectObject(PdfObject): @@ -182,7 +182,7 @@ def __repr__(self): def __eq__(self, other): return ( - other != None and + other is not None and isinstance(other, IndirectObject) and self.idnum == other.idnum and self.generation == other.generation and @@ -200,8 +200,7 @@ def readFromStream(stream, pdf): while True: tok = stream.read(1) if not tok: - # stream has truncated prematurely - raise PdfStreamError("Stream has ended unexpectedly") + raise PdfStreamError(ERR_STREAM_TRUNCATED_PREMATURELY) if tok.isspace(): break idnum += tok @@ -209,8 +208,7 @@ def readFromStream(stream, pdf): while True: tok = stream.read(1) if not tok: - # stream has truncated prematurely - raise PdfStreamError("Stream has ended unexpectedly") + raise PdfStreamError(ERR_STREAM_TRUNCATED_PREMATURELY) if tok.isspace(): if not generation: continue @@ -220,14 +218,14 @@ def readFromStream(stream, pdf): if r != b_("R"): raise utils.PdfReadError("Error reading indirect object reference at byte %s" % utils.hexStr(stream.tell())) return IndirectObject(int(idnum), int(generation), pdf) - readFromStream = staticmethod(readFromStream) + readFromStream = staticmethod(readFromStream) # type: ignore class FloatObject(decimal.Decimal, PdfObject): def __new__(cls, value="0", context=None): try: return decimal.Decimal.__new__(cls, utils.str_(value), context) - except: + except Exception: return decimal.Decimal.__new__(cls, str(value)) def __repr__(self): @@ -271,13 +269,14 @@ def readFromStream(stream): return FloatObject(num) else: return NumberObject(num) - readFromStream = staticmethod(readFromStream) + readFromStream = staticmethod(readFromStream) # type: ignore -## -# Given a string (either a "str" or "unicode"), create a ByteStringObject or a -# TextStringObject to represent the string. def createStringObject(string): + """ + Given a string (either a "str" or "unicode"), create a ByteStringObject or a + TextStringObject to represent the string. + """ if isinstance(string, utils.string_type): return TextStringObject(string) elif isinstance(string, utils.bytes_type): @@ -307,8 +306,7 @@ def readHexStringFromStream(stream): while True: tok = readNonWhitespace(stream) if not tok: - # stream has truncated prematurely - raise PdfStreamError("Stream has ended unexpectedly") + raise PdfStreamError(ERR_STREAM_TRUNCATED_PREMATURELY) if tok == b_(">"): break x += tok @@ -329,8 +327,7 @@ def readStringFromStream(stream): while True: tok = stream.read(1) if not tok: - # stream has truncated prematurely - raise PdfStreamError("Stream has ended unexpectedly") + raise PdfStreamError(ERR_STREAM_TRUNCATED_PREMATURELY) if tok == b_("("): parens += 1 elif tok == b_(")"): @@ -339,69 +336,71 @@ def readStringFromStream(stream): break elif tok == b_("\\"): tok = stream.read(1) - if tok == b_("n"): - tok = b_("\n") - elif tok == b_("r"): - tok = b_("\r") - elif tok == b_("t"): - tok = b_("\t") - elif tok == b_("b"): - tok = b_("\b") - elif tok == b_("f"): - tok = b_("\f") - elif tok == b_("c"): - tok = b_("\c") - elif tok == b_("("): - tok = b_("(") - elif tok == b_(")"): - tok = b_(")") - elif tok == b_("/"): - tok = b_("/") - elif tok == b_("\\"): - tok = b_("\\") - elif tok in (b_(" "), b_("/"), b_("%"), b_("<"), b_(">"), b_("["), - b_("]"), b_("#"), b_("_"), b_("&"), b_('$')): - # odd/unnessecary escape sequences we have encountered - tok = b_(tok) - elif tok.isdigit(): - # "The number ddd may consist of one, two, or three - # octal digits; high-order overflow shall be ignored. - # Three octal digits shall be used, with leading zeros - # as needed, if the next character of the string is also - # a digit." (PDF reference 7.3.4.2, p 16) - for i in range(2): - ntok = stream.read(1) - if ntok.isdigit(): - tok += ntok - else: - break - tok = b_(chr(int(tok, base=8))) - elif tok in b_("\n\r"): - # This case is hit when a backslash followed by a line - # break occurs. If it's a multi-char EOL, consume the - # second character: - tok = stream.read(1) - if not tok in b_("\n\r"): - stream.seek(-1, 1) - # Then don't add anything to the actual string, since this - # line break was escaped: - tok = b_('') - else: - raise utils.PdfReadError(r"Unexpected escaped string: %s" % tok) + ESCAPE_DICT = {b_("n") : b_("\n"), + b_("r") : b_("\r"), + b_("t") : b_("\t"), + b_("b") : b_("\b"), + b_("f") : b_("\f"), + b_("c") : b_(r"\c"), + b_("(") : b_("("), + b_(")") : b_(")"), + b_("/") : b_("/"), + b_("\\") : b_("\\"), + b_(" ") : b_(" "), + b_("/") : b_("/"), + b_("%") : b_("%"), + b_("<") : b_("<"), + b_(">") : b_(">"), + b_("[") : b_("["), + b_("]") : b_("]"), + b_("#") : b_("#"), + b_("_") : b_("_"), + b_("&") : b_("&"), + b_('$') : b_('$'), + } + try: + tok = ESCAPE_DICT[tok] + except KeyError: + if tok.isdigit(): + # "The number ddd may consist of one, two, or three + # octal digits; high-order overflow shall be ignored. + # Three octal digits shall be used, with leading zeros + # as needed, if the next character of the string is also + # a digit." (PDF reference 7.3.4.2, p 16) + for _ in range(2): + ntok = stream.read(1) + if ntok.isdigit(): + tok += ntok + else: + break + tok = b_(chr(int(tok, base=8))) + elif tok in b_("\n\r"): + # This case is hit when a backslash followed by a line + # break occurs. If it's a multi-char EOL, consume the + # second character: + tok = stream.read(1) + if tok not in b_("\n\r"): + stream.seek(-1, 1) + # Then don't add anything to the actual string, since this + # line break was escaped: + tok = b_('') + else: + raise utils.PdfReadError(r"Unexpected escaped string: %s" % tok) txt += tok return createStringObject(txt) -## -# Represents a string object where the text encoding could not be determined. -# This occurs quite often, as the PDF spec doesn't provide an alternate way to -# represent strings -- for example, the encryption data stored in files (like -# /O) is clearly not text, but is still stored in a "String" object. -class ByteStringObject(utils.bytes_type, PdfObject): +class ByteStringObject(utils.bytes_type, PdfObject): # type: ignore + """ + Represents a string object where the text encoding could not be determined. + This occurs quite often, as the PDF spec doesn't provide an alternate way to + represent strings -- for example, the encryption data stored in files (like + /O) is clearly not text, but is still stored in a "String" object. + """ ## # For compatibility with TextStringObject.original_bytes. This method - # returns self. + # self. original_bytes = property(lambda self: self) def writeToStream(self, stream, encryption_key): @@ -413,12 +412,14 @@ def writeToStream(self, stream, encryption_key): stream.write(b_(">")) -## -# Represents a string object that has been decoded into a real unicode string. -# If read from a PDF document, this string appeared to match the -# PDFDocEncoding, or contained a UTF-16BE BOM mark to cause UTF-16 decoding to -# occur. -class TextStringObject(utils.string_type, PdfObject): +class TextStringObject(utils.string_type, PdfObject): # type: ignore + """ + Represents a string object that has been decoded into a real unicode string. + If read from a PDF document, this string appeared to match the + PDFDocEncoding, or contained a UTF-16BE BOM mark to cause UTF-16 decoding to + occur. + """ + autodetect_pdfdocencoding = False autodetect_utf16 = False @@ -477,12 +478,16 @@ def readFromStream(stream, pdf): name = stream.read(1) if name != NameObject.surfix: raise utils.PdfReadError("name read error") - name += utils.readUntilRegex(stream, NameObject.delimiterPattern, + name += utils.readUntilRegex(stream, NameObject.delimiterPattern, ignore_eof=True) if debug: print(name) try: - return NameObject(name.decode('utf-8')) - except (UnicodeEncodeError, UnicodeDecodeError) as e: + try: + ret=name.decode('utf-8') + except (UnicodeEncodeError, UnicodeDecodeError): + ret=name.decode('gbk') + return NameObject(ret) + except (UnicodeEncodeError, UnicodeDecodeError): # Name objects should represent irregular characters # with a '#' followed by the symbol's hex number if not pdf.strict: @@ -491,7 +496,7 @@ def readFromStream(stream, pdf): else: raise utils.PdfReadError("Illegal character in Name Object") - readFromStream = staticmethod(readFromStream) + readFromStream = staticmethod(readFromStream) # type: ignore class DictionaryObject(dict, PdfObject): @@ -525,7 +530,7 @@ def __getitem__(self, key): # return None if no metadata was found on the document root. def getXmpMetadata(self): metadata = self.get("/Metadata", None) - if metadata == None: + if metadata is None: return None metadata = metadata.getObject() from . import xmp @@ -565,8 +570,7 @@ def readFromStream(stream, pdf): skipOverComment(stream) continue if not tok: - # stream has truncated prematurely - raise PdfStreamError("Stream has ended unexpectedly") + raise PdfStreamError(ERR_STREAM_TRUNCATED_PREMATURELY) if debug: print(("Tok:", tok)) if tok == b_(">"): @@ -601,8 +605,8 @@ def readFromStream(stream, pdf): if stream.read(1) != b_('\n'): stream.seek(-1, 1) # this is a stream object, not a dictionary - assert "/Length" in data - length = data["/Length"] + assert SA.LENGTH in data + length = data[SA.LENGTH] if debug: print(data) if isinstance(length, IndirectObject): t = stream.tell() @@ -610,7 +614,7 @@ def readFromStream(stream, pdf): stream.seek(t, 0) data["__streamdata__"] = stream.read(length) if debug: print("here") - #if debug: print(binascii.hexlify(data["__streamdata__"])) + # if debug: print(binascii.hexlify(data["__streamdata__"])) e = readNonWhitespace(stream) ndstream = stream.read(8) if (e + ndstream) != b_("endstream"): @@ -627,7 +631,6 @@ def readFromStream(stream, pdf): # we found it by looking back one character further. data["__streamdata__"] = data["__streamdata__"][:-1] else: - if debug: print(("E", e, ndstream, debugging.toHex(end))) stream.seek(pos, 0) raise utils.PdfReadError("Unable to find 'endstream' marker after stream at byte %s." % utils.hexStr(stream.tell())) else: @@ -638,7 +641,7 @@ def readFromStream(stream, pdf): retval = DictionaryObject() retval.update(data) return retval - readFromStream = staticmethod(readFromStream) + readFromStream = staticmethod(readFromStream) # type: ignore class TreeObject(DictionaryObject): @@ -702,9 +705,9 @@ def removeChild(self, child): cur = curRef.getObject() lastRef = self[NameObject('/Last')] last = lastRef.getObject() - while cur != None: + while cur is not None: if cur == childObj: - if prev == None: + if prev is None: if NameObject('/Next') in cur: # Removing first tree node nextRef = cur[NameObject('/Next')] @@ -778,9 +781,9 @@ def __init__(self): self.decodedSelf = None def writeToStream(self, stream, encryption_key): - self[NameObject("/Length")] = NumberObject(len(self._data)) + self[NameObject(SA.LENGTH)] = NumberObject(len(self._data)) DictionaryObject.writeToStream(self, stream, encryption_key) - del self["/Length"] + del self[SA.LENGTH] stream.write(b_("\nstream\n")) data = self._data if encryption_key: @@ -789,22 +792,22 @@ def writeToStream(self, stream, encryption_key): stream.write(b_("\nendstream")) def initializeFromDictionary(data): - if "/Filter" in data: + if SA.FILTER in data: retval = EncodedStreamObject() else: retval = DecodedStreamObject() retval._data = data["__streamdata__"] del data["__streamdata__"] - del data["/Length"] + del data[SA.LENGTH] retval.update(data) return retval - initializeFromDictionary = staticmethod(initializeFromDictionary) + initializeFromDictionary = staticmethod(initializeFromDictionary) # type: ignore def flateEncode(self): - if "/Filter" in self: - f = self["/Filter"] + if SA.FILTER in self: + f = self[SA.FILTER] if isinstance(f, ArrayObject): - f.insert(0, NameObject("/FlateDecode")) + f.insert(0, NameObject(FT.FLATE_DECODE)) else: newf = ArrayObject() newf.append(NameObject("/FlateDecode")) @@ -813,7 +816,7 @@ def flateEncode(self): else: f = NameObject("/FlateDecode") retval = EncodedStreamObject() - retval[NameObject("/Filter")] = f + retval[NameObject(SA.FILTER)] = f retval._data = filters.FlateDecode.encode(self._data) return retval @@ -840,7 +843,7 @@ def getData(self): decoded._data = filters.decodeStreamData(self) for key, value in list(self.items()): - if not key in ("/Length", "/Filter", "/DecodeParms"): + if key not in (SA.LENGTH, SA.FILTER, SA.DECODE_PARMS): decoded[key] = value self.decodedSelf = decoded return decoded._data @@ -1027,20 +1030,31 @@ class Destination(TreeObject): See section 8.2.1 of the PDF 1.6 reference. :param str title: Title of this destination. - :param int page: Page number of this destination. + :param IndirectObject page: Reference to the page of this destination. Should + be an instance of :class:`IndirectObject`. :param str typ: How the destination is displayed. :param args: Additional arguments may be necessary depending on the type. :raises PdfReadError: If destination type is invalid. - Valid ``typ`` arguments (see PDF spec for details): - /Fit No additional arguments - /XYZ [left] [top] [zoomFactor] - /FitH [top] - /FitV [left] - /FitR [left] [bottom] [right] [top] - /FitB No additional arguments - /FitBH [top] - /FitBV [left] + .. list-table:: Valid ``typ`` arguments (see PDF spec for details) + :widths: 50 50 + + * - /Fit + - No additional arguments + * - /XYZ + - [left] [top] [zoomFactor] + * - /FitH + - [top] + * - /FitV + - [left] + * - /FitR + - [left] [bottom] [right] [top] + * - /FitB + - No additional arguments + * - /FitBH + - [top] + * - /FitBV + - [left] """ def __init__(self, title, page, typ, *args): DictionaryObject.__init__(self) @@ -1048,18 +1062,21 @@ def __init__(self, title, page, typ, *args): self[NameObject("/Page")] = page self[NameObject("/Type")] = typ + from PyPDF2.constants import TypArguments as TA + from PyPDF2.constants import TypFitArguments as TF + # from table 8.2 of the PDF 1.7 reference. if typ == "/XYZ": - (self[NameObject("/Left")], self[NameObject("/Top")], + (self[NameObject(TA.LEFT)], self[NameObject(TA.TOP)], self[NameObject("/Zoom")]) = args - elif typ == "/FitR": - (self[NameObject("/Left")], self[NameObject("/Bottom")], - self[NameObject("/Right")], self[NameObject("/Top")]) = args - elif typ in ["/FitH", "/FitBH"]: - self[NameObject("/Top")], = args - elif typ in ["/FitV", "/FitBV"]: - self[NameObject("/Left")], = args - elif typ in ["/Fit", "/FitB"]: + elif typ == TF.FIT_R: + (self[NameObject(TA.LEFT)], self[NameObject(TA.BOTTOM)], + self[NameObject(TA.RIGHT)], self[NameObject(TA.TOP)]) = args + elif typ in [TF.FIT_H, TF.FIT_BH]: + self[NameObject(TA.TOP)], = args + elif typ in [TF.FIT_V, TF.FIT_BV]: + self[NameObject(TA.LEFT)], = args + elif typ in [TF.FIT, TF.FIT_B]: pass else: raise utils.PdfReadError("Unknown Destination Type: %r" % typ) diff --git a/PyPDF2/merger.py b/PyPDF2/merger.py index 27702add39..1f2ab2d6b2 100644 --- a/PyPDF2/merger.py +++ b/PyPDF2/merger.py @@ -1,5 +1,3 @@ -# vim: sw=4:expandtab:foldmethod=marker -# # Copyright (c) 2006, Mathieu Fenniak # All rights reserved. # @@ -27,11 +25,15 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. +from sys import version_info + +from PyPDF2.constants import PagesAttributes as PA + from .generic import * -from .utils import isString, str_ -from .pdf import PdfFileReader, PdfFileWriter from .pagerange import PageRange -from sys import version_info +from .pdf import PdfFileReader, PdfFileWriter +from .utils import isString, str_ + if version_info < ( 3, 0 ): from cStringIO import StringIO StreamIO = StringIO @@ -65,9 +67,12 @@ class PdfFileMerger(object): :param bool strict: Determines whether user should be warned of all problems and also causes some correctable problems to be fatal. Defaults to ``True``. + :param bool overwriteWarnings: Determines whether to override Python's + ``warnings.py`` module with a custom implementation (defaults to + ``True``). """ - def __init__(self, strict=True): + def __init__(self, strict=True, overwriteWarnings=True): self.inputs = [] self.pages = [] self.output = PdfFileWriter() @@ -75,6 +80,7 @@ def __init__(self, strict=True): self.named_dests = [] self.id_count = 0 self.strict = strict + self.overwriteWarnings = overwriteWarnings def merge(self, position, fileobj, bookmark=None, pages=None, import_bookmarks=True): """ @@ -91,7 +97,7 @@ def merge(self, position, fileobj, bookmark=None, pages=None, import_bookmarks=T :param str bookmark: Optionally, you may specify a bookmark to be applied at the beginning of the included file by supplying the text of the bookmark. - :param pages: can be a :ref:`Page Range ` or a ``(start, stop[, step])`` tuple + :param pages: can be a :class:`PageRange` or a ``(start, stop[, step])`` tuple to merge only the specified range of pages from the source document into the output document. @@ -113,29 +119,29 @@ def merge(self, position, fileobj, bookmark=None, pages=None, import_bookmarks=T if isString(fileobj): fileobj = file(fileobj, 'rb') my_file = True - elif isinstance(fileobj, file): + elif hasattr(fileobj, "seek") and hasattr(fileobj, "read"): fileobj.seek(0) filecontent = fileobj.read() fileobj = StreamIO(filecontent) my_file = True elif isinstance(fileobj, PdfFileReader): + if hasattr(fileobj, '_decryption_key'): + decryption_key = fileobj._decryption_key orig_tell = fileobj.stream.tell() fileobj.stream.seek(0) filecontent = StreamIO(fileobj.stream.read()) fileobj.stream.seek(orig_tell) # reset the stream to its original location fileobj = filecontent - if hasattr(fileobj, '_decryption_key'): - decryption_key = fileobj._decryption_key my_file = True # Create a new PdfFileReader instance using the stream # (either file or BytesIO or StringIO) created above - pdfr = PdfFileReader(fileobj, strict=self.strict) + pdfr = PdfFileReader(fileobj, strict=self.strict, overwriteWarnings=self.overwriteWarnings) if decryption_key is not None: pdfr._decryption_key = decryption_key # Find the range of pages to merge. - if pages == None: + if pages is None: pages = (0, pdfr.getNumPages()) elif isinstance(pages, PageRange): pages = pages.indices(pdfr.getNumPages()) @@ -192,7 +198,7 @@ def append(self, fileobj, bookmark=None, pages=None, import_bookmarks=True): :param str bookmark: Optionally, you may specify a bookmark to be applied at the beginning of the included file by supplying the text of the bookmark. - :param pages: can be a :ref:`Page Range ` or a ``(start, stop[, step])`` tuple + :param pages: can be a :class:`PageRange` or a ``(start, stop[, step])`` tuple to merge only the specified range of pages from the source document into the output document. @@ -218,9 +224,9 @@ def write(self, fileobj): # The commented out line below was replaced with the two lines below it to allow PdfFileMerger to work with PyPdf 1.13 for page in self.pages: self.output.addPage(page.pagedata) - page.out_pagedata = self.output.getReference(self.output._pages.getObject()["/Kids"][-1].getObject()) - #idnum = self.output._objects.index(self.output._pages.getObject()["/Kids"][-1].getObject()) + 1 - #page.out_pagedata = IndirectObject(idnum, 0, self.output) + page.out_pagedata = self.output.getReference(self.output._pages.getObject()[PA.KIDS][-1].getObject()) + # idnum = self.output._objects.index(self.output._pages.getObject()[PA.KIDS][-1].getObject()) + 1 + # page.out_pagedata = IndirectObject(idnum, 0, self.output) # Once all pages are added, create bookmarks to point at those pages self._write_dests() @@ -238,7 +244,7 @@ def close(self): usage. """ self.pages = [] - for fo, pdfr, mine in self.inputs: + for fo, _pdfr, mine in self.inputs: if mine: fo.close() @@ -261,14 +267,23 @@ def setPageLayout(self, layout): :param str layout: The page layout to be used - Valid layouts are: - /NoLayout Layout explicitly not specified - /SinglePage Show one page at a time - /OneColumn Show one column at a time - /TwoColumnLeft Show pages in two columns, odd-numbered pages on the left - /TwoColumnRight Show pages in two columns, odd-numbered pages on the right - /TwoPageLeft Show two pages at a time, odd-numbered pages on the left - /TwoPageRight Show two pages at a time, odd-numbered pages on the right + .. list-table:: Valid ``layout`` arguments + :widths: 50 200 + + * - /NoLayout + - Layout explicitly not specified + * - /SinglePage + - Show one page at a time + * - /OneColumn + - Show one column at a time + * - /TwoColumnLeft + - Show pages in two columns, odd-numbered pages on the left + * - /TwoColumnRight + - Show pages in two columns, odd-numbered pages on the right + * - /TwoPageLeft + - Show two pages at a time, odd-numbered pages on the left + * - /TwoPageRight + - Show two pages at a time, odd-numbered pages on the right """ self.output.setPageLayout(layout) @@ -278,13 +293,21 @@ def setPageMode(self, mode): :param str mode: The page mode to use. - Valid modes are: - /UseNone Do not show outlines or thumbnails panels - /UseOutlines Show outlines (aka bookmarks) panel - /UseThumbs Show page thumbnails panel - /FullScreen Fullscreen view - /UseOC Show Optional Content Group (OCG) panel - /UseAttachments Show attachments panel + .. list-table:: Valid ``mode`` arguments + :widths: 50 200 + + * - /UseNone + - Do not show outlines or thumbnails panels + * - /UseOutlines + - Show outlines (aka bookmarks) panel + * - /UseThumbs + - Show page thumbnails panel + * - /FullScreen + - Fullscreen view + * - /UseOC + - Show Optional Content Group (OCG) panel + * - /UseAttachments + - Show attachments panel """ self.output.setPageMode(mode) @@ -294,7 +317,6 @@ def _trim_dests(self, pdf, dests, pages): page set. """ new_dests = [] - prev_header_added = True for k, o in list(dests.items()): for j in range(*pages): if pdf.getPage(j).getObject() == o['/Page'].getObject(): @@ -339,14 +361,14 @@ def _write_dests(self): if p.id == v['/Page']: v[NameObject('/Page')] = p.out_pagedata pageno = i - pdf = p.src + pdf = p.src # noqa: F841 break - if pageno != None: + if pageno is not None: self.output.addNamedDestinationObject(v) def _write_bookmarks(self, bookmarks=None, parent=None): - if bookmarks == None: + if bookmarks is None: bookmarks = self.bookmarks last_added = None @@ -360,10 +382,10 @@ def _write_bookmarks(self, bookmarks=None, parent=None): if '/Page' in b: for i, p in enumerate(self.pages): if p.id == b['/Page']: - #b[NameObject('/Page')] = p.out_pagedata + # b[NameObject('/Page')] = p.out_pagedata args = [NumberObject(p.id), NameObject(b['/Type'])] - #nothing more to add - #if b['/Type'] == '/Fit' or b['/Type'] == '/FitB' + # nothing more to add + # if b['/Type'] == '/Fit' or b['/Type'] == '/FitB' if b['/Type'] == '/FitH' or b['/Type'] == '/FitBH': if '/Top' in b and not isinstance(b['/Top'], NullObject): args.append(FloatObject(b['/Top'])) @@ -412,9 +434,9 @@ def _write_bookmarks(self, bookmarks=None, parent=None): b[NameObject('/A')] = DictionaryObject({NameObject('/S'): NameObject('/GoTo'), NameObject('/D'): ArrayObject(args)}) pageno = i - pdf = p.src + pdf = p.src # noqa: F841 break - if pageno != None: + if pageno is not None: del b['/Page'], b['/Type'] last_added = self.output.addBookmarkDict(b, parent) @@ -430,13 +452,13 @@ def _associate_dests_to_pages(self, pages): if np.getObject() == p.pagedata.getObject(): pageno = p.id - if pageno != None: + if pageno is not None: nd[NameObject('/Page')] = NumberObject(pageno) else: raise ValueError("Unresolved named destination '%s'" % (nd['/Title'],)) def _associate_bookmarks_to_pages(self, pages, bookmarks=None): - if bookmarks == None: + if bookmarks is None: bookmarks = self.bookmarks for b in bookmarks: @@ -454,13 +476,13 @@ def _associate_bookmarks_to_pages(self, pages, bookmarks=None): if bp.getObject() == p.pagedata.getObject(): pageno = p.id - if pageno != None: + if pageno is not None: b[NameObject('/Page')] = NumberObject(pageno) else: raise ValueError("Unresolved bookmark '%s'" % (b['/Title'],)) def findBookmark(self, bookmark, root=None): - if root == None: + if root is None: root = self.bookmarks for i, b in enumerate(root): @@ -482,7 +504,7 @@ def addBookmark(self, title, pagenum, parent=None): :param parent: A reference to a parent bookmark to create nested bookmarks. """ - if parent == None: + if parent is None: iloc = [len(self.bookmarks)-1] elif isinstance(parent, list): iloc = parent @@ -491,7 +513,7 @@ def addBookmark(self, title, pagenum, parent=None): dest = Bookmark(TextStringObject(title), NumberObject(pagenum), NameObject('/FitH'), NumberObject(826)) - if parent == None: + if parent is None: self.bookmarks.append(dest) else: bmparent = self.bookmarks @@ -529,7 +551,7 @@ def remove(self, index): self.tree.removeChild(obj) def add(self, title, pagenum): - pageRef = self.pdf.getObject(self.pdf._pages)['/Kids'][pagenum] + pageRef = self.pdf.getObject(self.pdf._pages)[PA.KIDS][pagenum] action = DictionaryObject() action.update({ NameObject('/D') : ArrayObject([pageRef, NameObject('/FitH'), NumberObject(826)]), diff --git a/PyPDF2/pagerange.py b/PyPDF2/pagerange.py index ce96ec5f3f..aa532e7048 100644 --- a/PyPDF2/pagerange.py +++ b/PyPDF2/pagerange.py @@ -4,10 +4,11 @@ Copyright (c) 2014, Steve Witham . All rights reserved. This software is available under a BSD license; -see https://github.com/mstamy2/PyPDF2/blob/master/LICENSE +see https://github.com/py-pdf/PyPDF2/blob/main/LICENSE """ import re + from .utils import isString _INT_RE = r"(0|-?[1-9]\d*)" # A decimal int, don't allow "-0". @@ -37,14 +38,17 @@ class PageRange(object): """ A slice-like representation of a range of page indices, i.e. page numbers, only starting at zero. + The syntax is like what you would put between brackets [ ]. The slice is one of the few Python types that can't be subclassed, but this class converts to and from slices, and allows similar use. - o PageRange(str) parses a string representing a page range. - o PageRange(slice) directly "imports" a slice. - o to_slice() gives the equivalent slice. - o str() and repr() allow printing. - o indices(n) is like slice.indices(n). + + - PageRange(str) parses a string representing a page range. + - PageRange(slice) directly "imports" a slice. + - to_slice() gives the equivalent slice. + - str() and repr() allow printing. + - indices(n) is like slice.indices(n). + """ def __init__(self, arg): @@ -80,15 +84,13 @@ def __init__(self, arg): self._slice = slice(*[int(g) if g else None for g in m.group(4, 6, 8)]) - # Just formatting this when there is __doc__ for __init__ - if __init__.__doc__: + if __init__.__doc__: # see https://github.com/py-pdf/PyPDF2/issues/737 __init__.__doc__ = __init__.__doc__.format(page_range_help=PAGE_RANGE_HELP) @staticmethod def valid(input): """ True if input is a valid initializer for a PageRange. """ - return isinstance(input, slice) or \ - isinstance(input, PageRange) or \ + return isinstance(input, (slice, PageRange)) or \ (isString(input) and bool(re.match(PAGE_RANGE_RE, input))) @@ -99,14 +101,14 @@ def to_slice(self): def __str__(self): """ A string like "1:2:3". """ s = self._slice - if s.step == None: - if s.start != None and s.stop == s.start + 1: + if s.step is None: + if s.start is not None and s.stop == s.start + 1: return str(s.start) indices = s.start, s.stop else: indices = s.start, s.stop, s.step - return ':'.join("" if i == None else str(i) for i in indices) + return ':'.join("" if i is None else str(i) for i in indices) def __repr__(self): """ A string like "PageRange('1:2:3')". """ @@ -119,6 +121,11 @@ def indices(self, n): """ return self._slice.indices(n) + def __eq__(self, other): + if not isinstance(other, PageRange): + return False + return self._slice == other._slice + PAGE_RANGE_ALL = PageRange(":") # The range of all pages. @@ -137,7 +144,7 @@ def parse_filename_page_ranges(args): for arg in args + [None]: if PageRange.valid(arg): if not pdf_filename: - raise ValueError("The first argument must be a filename, " \ + raise ValueError("The first argument must be a filename, " "not a page range.") pairs.append( (pdf_filename, PageRange(arg)) ) diff --git a/PyPDF2/pdf.py b/PyPDF2/pdf.py index 91bc28671a..c4cb3f8db6 100644 --- a/PyPDF2/pdf.py +++ b/PyPDF2/pdf.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- # -# vim: sw=4:expandtab:foldmethod=marker -# # Copyright (c) 2006, Mathieu Fenniak # Copyright (c) 2007, Ashish Kulkarni # @@ -31,23 +29,17 @@ # ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE # POSSIBILITY OF SUCH DAMAGE. -""" -A pure-Python PDF library with an increasing number of capabilities. -See README for links to FAQ, documentation, homepage, etc. -""" +"""A pure-Python PDF library with an increasing number of capabilities.""" __author__ = "Mathieu Fenniak" __author_email__ = "biziqe@mathieu.fenniak.net" -__maintainer__ = "Phaseit, Inc." -__maintainer_email = "PyPDF2@phaseit.net" - -import string import math import struct import sys import uuid from sys import version_info + if version_info < ( 3, 0 ): from cStringIO import StringIO else: @@ -58,13 +50,18 @@ else: from io import BytesIO -from . import filters -from . import utils -import warnings import codecs +import warnings + +from PyPDF2.constants import PageAttributes as PG +from PyPDF2.constants import PagesAttributes as PA +from PyPDF2.constants import Ressources as RES +from PyPDF2.constants import StreamAttributes as SA + +from . import utils from .generic import * -from .utils import readNonWhitespace, readUntilWhitespace, ConvertFunctionsToVirtualList -from .utils import isString, b_, u_, ord_, chr_, str_, formatWarning +from .utils import (ConvertFunctionsToVirtualList, b_, formatWarning, isString, + ord_, readNonWhitespace, readUntilWhitespace, str_, u_) if version_info < ( 2, 4 ): from sets import ImmutableSet as frozenset @@ -73,7 +70,6 @@ from md5 import md5 else: from hashlib import md5 -import uuid class PdfFileWriter(object): @@ -88,9 +84,9 @@ def __init__(self): # The root of our page tree node. pages = DictionaryObject() pages.update({ - NameObject("/Type"): NameObject("/Pages"), - NameObject("/Count"): NumberObject(0), - NameObject("/Kids"): ArrayObject(), + NameObject(PA.TYPE): NameObject("/Pages"), + NameObject(PA.COUNT): NumberObject(0), + NameObject(PA.KIDS): ArrayObject(), }) self._pages = self._addObject(pages) @@ -104,7 +100,7 @@ def __init__(self): # root object root = DictionaryObject() root.update({ - NameObject("/Type"): NameObject("/Catalog"), + NameObject(PA.TYPE): NameObject("/Catalog"), NameObject("/Pages"): self._pages, }) self._root = None @@ -120,12 +116,12 @@ def getObject(self, ido): return self._objects[ido.idnum - 1] def _addPage(self, page, action): - assert page["/Type"] == "/Page" + assert page[PA.TYPE] == "/Page" page[NameObject("/Parent")] = self._pages page = self._addObject(page) pages = self.getObject(self._pages) - action(pages["/Kids"], page) - pages[NameObject("/Count")] = NumberObject(pages["/Count"] + 1) + action(pages[PA.KIDS], page) + pages[NameObject(PA.COUNT)] = NumberObject(pages[PA.COUNT] + 1) def addPage(self, page): """ @@ -159,7 +155,7 @@ def getPage(self, pageNumber): """ pages = self.getObject(self._pages) # XXX: crude hack - return pages["/Kids"][pageNumber].getObject() + return pages[PA.KIDS][pageNumber].getObject() def getNumPages(self): """ @@ -222,7 +218,7 @@ def addJS(self, javascript): """ js = DictionaryObject() js.update({ - NameObject("/Type"): NameObject("/Action"), + NameObject(PA.TYPE): NameObject("/Action"), NameObject("/S"): NameObject("/JavaScript"), NameObject("/JS"): NameObject("(%s)" % javascript) }) @@ -250,17 +246,17 @@ def addAttachment(self, fname, fdata): :param str fname: The filename to display. :param str fdata: The data in the file. - + Reference: https://www.adobe.com/content/dam/Adobe/en/devnet/acrobat/pdfs/PDF32000_2008.pdf Section 7.11.3 """ - + # We need 3 entries: # * The file's data # * The /Filespec entry # * The file's name, which goes in the Catalog - + # The entry for the file """ Sample: @@ -272,12 +268,12 @@ def addAttachment(self, fname, fdata): stream Hello world! endstream - endobj + endobj """ file_entry = DecodedStreamObject() file_entry.setData(fdata) file_entry.update({ - NameObject("/Type"): NameObject("/EmbeddedFile") + NameObject(PA.TYPE): NameObject("/EmbeddedFile") }) # The Filespec entry @@ -291,14 +287,14 @@ def addAttachment(self, fname, fdata): """ efEntry = DictionaryObject() efEntry.update({ NameObject("/F"):file_entry }) - + filespec = DictionaryObject() filespec.update({ - NameObject("/Type"): NameObject("/Filespec"), + NameObject(PA.TYPE): NameObject("/Filespec"), NameObject("/F"): createStringObject(fname), # Perhaps also try TextStringObject NameObject("/EF"): efEntry }) - + # Then create the entry for the root, as it needs a reference to the Filespec """ Sample: 1 0 obj @@ -309,13 +305,13 @@ def addAttachment(self, fname, fdata): /Names << /EmbeddedFiles << /Names [(hello.txt) 7 0 R] >> >> >> endobj - + """ embeddedFilesNamesDictionary = DictionaryObject() embeddedFilesNamesDictionary.update({ NameObject("/Names"): ArrayObject([createStringObject(fname), filespec]) }) - + embeddedFilesDictionary = DictionaryObject() embeddedFilesDictionary.update({ NameObject("/EmbeddedFiles"): embeddedFilesNamesDictionary @@ -329,15 +325,14 @@ def appendPagesFromReader(self, reader, after_page_append=None): """ Copy pages from reader to writer. Includes an optional callback parameter which is invoked after pages are appended to the writer. - + :param reader: a PdfFileReader object from which to copy page annotations to this writer object. The writer's annots - will then be updated + will then be updated :callback after_page_append (function): Callback function that is invoked after each page is appended to the writer. Callback signature: - - :param writer_pageref (PDF page reference): Reference to the page - appended to the writer. + :param writer_pageref (PDF page reference): Reference to the page + appended to the writer. """ # Get page count from writer and reader reader_num_pages = reader.getNumPages() @@ -362,8 +357,8 @@ def updatePageFormFieldValues(self, page, fields): values (/V) ''' # Iterate through pages, update field values - for j in range(0, len(page['/Annots'])): - writer_annot = page['/Annots'][j].getObject() + for j in range(0, len(page[PG.ANNOTS])): + writer_annot = page[PG.ANNOTS][j].getObject() for field in fields: if writer_annot.get('/T') == field: writer_annot.update({ @@ -373,9 +368,9 @@ def updatePageFormFieldValues(self, page, fields): def cloneReaderDocumentRoot(self, reader): ''' Copy the reader document root to the writer. - + :param reader: PdfFileReader from the document root should be copied. - :callback after_page_append + :callback after_page_append: ''' self._root_object = reader.trailer['/Root'] @@ -408,8 +403,9 @@ def encrypt(self, user_pwd, owner_pwd = None, use_128bit = True): encryption. When false, 40bit encryption will be used. By default, this flag is on. """ - import time, random - if owner_pwd == None: + import random + import time + if owner_pwd is None: owner_pwd = user_pwd if use_128bit: V = 2 @@ -431,10 +427,10 @@ def encrypt(self, user_pwd, owner_pwd = None, use_128bit = True): assert rev == 3 U, key = _alg35(user_pwd, rev, keylen, O, P, ID_1, False) encrypt = DictionaryObject() - encrypt[NameObject("/Filter")] = NameObject("/Standard") + encrypt[NameObject(SA.FILTER)] = NameObject("/Standard") encrypt[NameObject("/V")] = NumberObject(V) if V == 2: - encrypt[NameObject("/Length")] = NumberObject(keylen * 8) + encrypt[NameObject(SA.LENGTH)] = NumberObject(keylen * 8) encrypt[NameObject("/R")] = NumberObject(rev) encrypt[NameObject("/O")] = ByteStringObject(O) encrypt[NameObject("/U")] = ByteStringObject(U) @@ -469,7 +465,7 @@ def write(self, stream): # copying in a new copy of the page object. for objIndex in range(len(self._objects)): obj = self._objects[objIndex] - if isinstance(obj, PageObject) and obj.indirectRef != None: + if isinstance(obj, PageObject) and obj.indirectRef is not None: data = obj.indirectRef if data.pdf not in externalReferenceMap: externalReferenceMap[data.pdf] = {} @@ -485,21 +481,24 @@ def write(self, stream): # Begin writing: object_positions = [] stream.write(self._header + b_("\n")) + stream.write(b_("%\xE2\xE3\xCF\xD3\n")) for i in range(len(self._objects)): - idnum = (i + 1) obj = self._objects[i] - object_positions.append(stream.tell()) - stream.write(b_(str(idnum) + " 0 obj\n")) - key = None - if hasattr(self, "_encrypt") and idnum != self._encrypt.idnum: - pack1 = struct.pack("` for details. """ - pageRef = self.getObject(self._pages)['/Kids'][pagenum] + pageRef = self.getObject(self._pages)[PA.KIDS][pagenum] action = DictionaryObject() zoomArgs = [] for a in args: @@ -720,7 +723,7 @@ def addBookmark(self, title, pagenum, parent=None, color=None, bold=False, itali outlineRef = self.getOutlineRoot() - if parent == None: + if parent is None: parent = outlineRef bookmark = TreeObject() @@ -757,7 +760,7 @@ def addNamedDestinationObject(self, dest): return destRef def addNamedDestination(self, title, pagenum): - pageRef = self.getObject(self._pages)['/Kids'][pagenum] + pageRef = self.getObject(self._pages)[PA.KIDS][pagenum] dest = DictionaryObject() dest.update({ NameObject('/D') : ArrayObject([pageRef, NameObject('/FitH'), NumberObject(826)]), @@ -775,11 +778,11 @@ def removeLinks(self): """ Removes links and annotations from this output. """ - pages = self.getObject(self._pages)['/Kids'] + pages = self.getObject(self._pages)[PA.KIDS] for page in pages: pageRef = self.getObject(page) - if "/Annots" in pageRef: - del pageRef['/Annots'] + if PG.ANNOTS in pageRef: + del pageRef[PG.ANNOTS] def removeImages(self, ignoreByteStringObject=False): """ @@ -788,7 +791,12 @@ def removeImages(self, ignoreByteStringObject=False): :param bool ignoreByteStringObject: optional parameter to ignore ByteString Objects. """ - pages = self.getObject(self._pages)['/Kids'] + pages = self.getObject(self._pages)[PA.KIDS] + jump_operators = [ + b_('cm'), b_('w'), b_('J'), b_('j'), b_('M'), b_('d'), b_('ri'), b_('i'), + b_('gs'), b_('W'), b_('b'), b_('s'), b_('S'), b_('f'), b_('F'), b_('n'), b_('m'), b_('l'), + b_('c'), b_('v'), b_('y'), b_('h'), b_('B'), b_('Do'), b_('sh') + ] for j in range(len(pages)): page = pages[j] pageRef = self.getObject(page) @@ -799,36 +807,29 @@ def removeImages(self, ignoreByteStringObject=False): _operations = [] seq_graphics = False for operands, operator in content.operations: - if operator == b_('Tj'): - text = operands[0] - if ignoreByteStringObject: - if not isinstance(text, TextStringObject): - operands[0] = TextStringObject() - elif operator == b_("'"): + if operator in [b_('Tj'), b_("'")]: text = operands[0] if ignoreByteStringObject: if not isinstance(text, TextStringObject): operands[0] = TextStringObject() elif operator == b_('"'): text = operands[2] - if ignoreByteStringObject: - if not isinstance(text, TextStringObject): - operands[2] = TextStringObject() + if ignoreByteStringObject and not isinstance(text, TextStringObject): + operands[2] = TextStringObject() elif operator == b_("TJ"): for i in range(len(operands[0])): - if ignoreByteStringObject: - if not isinstance(operands[0][i], TextStringObject): - operands[0][i] = TextStringObject() + if ( + ignoreByteStringObject + and not isinstance(operands[0][i], TextStringObject) + ): + operands[0][i] = TextStringObject() if operator == b_('q'): seq_graphics = True if operator == b_('Q'): seq_graphics = False - if seq_graphics: - if operator in [b_('cm'), b_('w'), b_('J'), b_('j'), b_('M'), b_('d'), b_('ri'), b_('i'), - b_('gs'), b_('W'), b_('b'), b_('s'), b_('S'), b_('f'), b_('F'), b_('n'), b_('m'), b_('l'), - b_('c'), b_('v'), b_('y'), b_('h'), b_('B'), b_('Do'), b_('sh')]: - continue + if seq_graphics and operator in jump_operators: + continue if operator == b_('re'): continue _operations.append((operands, operator)) @@ -838,12 +839,12 @@ def removeImages(self, ignoreByteStringObject=False): def removeText(self, ignoreByteStringObject=False): """ - Removes images from this output. + Removes text from this output. :param bool ignoreByteStringObject: optional parameter to ignore ByteString Objects. """ - pages = self.getObject(self._pages)['/Kids'] + pages = self.getObject(self._pages)[PA.KIDS] for j in range(len(pages)): page = pages[j] pageRef = self.getObject(page) @@ -851,23 +852,13 @@ def removeText(self, ignoreByteStringObject=False): if not isinstance(content, ContentStream): content = ContentStream(content, pageRef) for operands,operator in content.operations: - if operator == b_('Tj'): + if operator in [b_('Tj'), b_("'")]: text = operands[0] if not ignoreByteStringObject: if isinstance(text, TextStringObject): operands[0] = TextStringObject() else: - if isinstance(text, TextStringObject) or \ - isinstance(text, ByteStringObject): - operands[0] = TextStringObject() - elif operator == b_("'"): - text = operands[0] - if not ignoreByteStringObject: - if isinstance(text, TextStringObject): - operands[0] = TextStringObject() - else: - if isinstance(text, TextStringObject) or \ - isinstance(text, ByteStringObject): + if isinstance(text, (TextStringObject, ByteStringObject)): operands[0] = TextStringObject() elif operator == b_('"'): text = operands[2] @@ -875,8 +866,7 @@ def removeText(self, ignoreByteStringObject=False): if isinstance(text, TextStringObject): operands[2] = TextStringObject() else: - if isinstance(text, TextStringObject) or \ - isinstance(text, ByteStringObject): + if isinstance(text, (TextStringObject, ByteStringObject)): operands[2] = TextStringObject() elif operator == b_("TJ"): for i in range(len(operands[0])): @@ -884,12 +874,69 @@ def removeText(self, ignoreByteStringObject=False): if isinstance(operands[0][i], TextStringObject): operands[0][i] = TextStringObject() else: - if isinstance(operands[0][i], TextStringObject) or \ - isinstance(operands[0][i], ByteStringObject): + if isinstance(operands[0][i], (TextStringObject, ByteStringObject)): operands[0][i] = TextStringObject() pageRef.__setitem__(NameObject('/Contents'), content) + def addURI(self, pagenum, uri, rect, border=None): + """ + Add an URI from a rectangular area to the specified page. + This uses the basic structure of AddLink + + :param int pagenum: index of the page on which to place the URI action. + :param int uri: string -- uri of resource to link to. + :param rect: :class:`RectangleObject` or array of four + integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]``, or string in the form ``"[ xLL yLL xUR yUR ]"``. + :param border: if provided, an array describing border-drawing + properties. See the PDF spec for details. No border will be + drawn if this argument is omitted. + + REMOVED FIT/ZOOM ARG + -John Mulligan + """ + + pageLink = self.getObject(self._pages)[PA.KIDS][pagenum] + pageRef = self.getObject(pageLink) + + if border is not None: + borderArr = [NameObject(n) for n in border[:3]] + if len(border) == 4: + dashPattern = ArrayObject([NameObject(n) for n in border[3]]) + borderArr.append(dashPattern) + else: + borderArr = [NumberObject(2)] * 3 + + if isString(rect): + rect = NameObject(rect) + elif isinstance(rect, RectangleObject): + pass + else: + rect = RectangleObject(rect) + + lnk2 = DictionaryObject() + lnk2.update({ + NameObject('/S'): NameObject('/URI'), + NameObject('/URI'): TextStringObject(uri) + }); + lnk = DictionaryObject() + lnk.update({ + NameObject('/Type'): NameObject(PG.ANNOTS), + NameObject('/Subtype'): NameObject('/Link'), + NameObject('/P'): pageLink, + NameObject('/Rect'): rect, + NameObject('/H'): NameObject('/I'), + NameObject('/Border'): ArrayObject(borderArr), + NameObject('/A'): lnk2 + }) + lnkRef = self._addObject(lnk) + + if PG.ANNOTS in pageRef: + pageRef[PG.ANNOTS].append(lnkRef) + else: + pageRef[NameObject(PG.ANNOTS)] = ArrayObject([lnkRef]) + def addLink(self, pagenum, pagedest, rect, border=None, fit='/Fit', *args): """ Add an internal link from a rectangular area to the specified page. @@ -905,19 +952,29 @@ def addLink(self, pagenum, pagedest, rect, border=None, fit='/Fit', *args): :param str fit: Page fit or 'zoom' option (see below). Additional arguments may need to be supplied. Passing ``None`` will be read as a null value for that coordinate. - Valid zoom arguments (see Table 8.2 of the PDF 1.7 reference for details): - /Fit No additional arguments - /XYZ [left] [top] [zoomFactor] - /FitH [top] - /FitV [left] - /FitR [left] [bottom] [right] [top] - /FitB No additional arguments - /FitBH [top] - /FitBV [left] - """ - - pageLink = self.getObject(self._pages)['/Kids'][pagenum] - pageDest = self.getObject(self._pages)['/Kids'][pagedest] #TODO: switch for external link + .. list-table:: Valid ``zoom`` arguments (see Table 8.2 of the PDF 1.7 reference for details) + :widths: 50 200 + + * - /Fit + - No additional arguments + * - /XYZ + - [left] [top] [zoomFactor] + * - /FitH + - [top] + * - /FitV + - [left] + * - /FitR + - [left] [bottom] [right] [top] + * - /FitB + - No additional arguments + * - /FitBH + - [top] + * - /FitBV + - [left] + """ + + pageLink = self.getObject(self._pages)[PA.KIDS][pagenum] + pageDest = self.getObject(self._pages)[PA.KIDS][pagedest] # TODO: switch for external link pageRef = self.getObject(pageLink) if border is not None: @@ -941,12 +998,12 @@ def addLink(self, pagenum, pagedest, rect, border=None, fit='/Fit', *args): zoomArgs.append(NumberObject(a)) else: zoomArgs.append(NullObject()) - dest = Destination(NameObject("/LinkName"), pageDest, NameObject(fit), *zoomArgs) #TODO: create a better name for the link + dest = Destination(NameObject("/LinkName"), pageDest, NameObject(fit), *zoomArgs) # TODO: create a better name for the link destArray = dest.getDestArray() lnk = DictionaryObject() lnk.update({ - NameObject('/Type'): NameObject('/Annot'), + NameObject('/Type'): NameObject(PG.ANNOTS), NameObject('/Subtype'): NameObject('/Link'), NameObject('/P'): pageLink, NameObject('/Rect'): rect, @@ -955,10 +1012,10 @@ def addLink(self, pagenum, pagedest, rect, border=None, fit='/Fit', *args): }) lnkRef = self._addObject(lnk) - if "/Annots" in pageRef: - pageRef['/Annots'].append(lnkRef) + if PG.ANNOTS in pageRef: + pageRef[PG.ANNOTS].append(lnkRef) else: - pageRef[NameObject('/Annots')] = ArrayObject([lnkRef]) + pageRef[NameObject(PG.ANNOTS)] = ArrayObject([lnkRef]) _valid_layouts = ['/NoLayout', '/SinglePage', '/OneColumn', '/TwoColumnLeft', '/TwoColumnRight', '/TwoPageLeft', '/TwoPageRight'] @@ -981,14 +1038,23 @@ def setPageLayout(self, layout): :param str layout: The page layout to be used - Valid layouts are: - /NoLayout Layout explicitly not specified - /SinglePage Show one page at a time - /OneColumn Show one column at a time - /TwoColumnLeft Show pages in two columns, odd-numbered pages on the left - /TwoColumnRight Show pages in two columns, odd-numbered pages on the right - /TwoPageLeft Show two pages at a time, odd-numbered pages on the left - /TwoPageRight Show two pages at a time, odd-numbered pages on the right + .. list-table:: Valid ``layout`` arguments + :widths: 50 200 + + * - /NoLayout + - Layout explicitly not specified + * - /SinglePage + - Show one page at a time + * - /OneColumn + - Show one column at a time + * - /TwoColumnLeft + - Show pages in two columns, odd-numbered pages on the left + * - /TwoColumnRight + - Show pages in two columns, odd-numbered pages on the right + * - /TwoPageLeft + - Show two pages at a time, odd-numbered pages on the left + * - /TwoPageRight + - Show two pages at a time, odd-numbered pages on the right """ if not isinstance(layout, NameObject): if layout not in self._valid_layouts: @@ -1022,13 +1088,21 @@ def setPageMode(self, mode): :param str mode: The page mode to use. - Valid modes are: - /UseNone Do not show outlines or thumbnails panels - /UseOutlines Show outlines (aka bookmarks) panel - /UseThumbs Show page thumbnails panel - /FullScreen Fullscreen view - /UseOC Show Optional Content Group (OCG) panel - /UseAttachments Show attachments panel + .. list-table:: Valid ``mode`` arguments + :widths: 50 200 + + * - /UseNone + - Do not show outlines or thumbnails panels + * - /UseOutlines + - Show outlines (aka bookmarks) panel + * - /UseThumbs + - Show page thumbnails panel + * - /FullScreen + - Fullscreen view + * - /UseOC + - Show Optional Content Group (OCG) panel + * - /UseAttachments + - Show attachments panel """ if not isinstance(mode, NameObject): if mode not in self._valid_modes: @@ -1066,7 +1140,11 @@ def _showwarning(message, category, filename, lineno, file=warndest, line=None): if file is None: file = sys.stderr try: - file.write(formatWarning(message, category, filename, lineno, line)) + # It is possible for sys.stderr to be defined as None, most commonly in the case that the script + # is being run vida pythonw.exe on Windows. In this case, just swallow the warning. + # See also https://docs.python.org/3/library/sys.html# sys.__stderr__ + if file is not None: + file.write(formatWarning(message, category, filename, lineno, line)) except IOError: pass warnings.showwarning = _showwarning @@ -1078,9 +1156,8 @@ def _showwarning(message, category, filename, lineno, file=warndest, line=None): if hasattr(stream, 'mode') and 'b' not in stream.mode: warnings.warn("PdfFileReader stream/file object is not in binary mode. It may not be read correctly.", utils.PdfReadWarning) if isString(stream): - fileobj = open(stream, 'rb') - stream = BytesIO(b_(fileobj.read())) - fileobj.close() + with open(stream, 'rb') as fileobj: + stream = BytesIO(b_(fileobj.read())) self.read(stream) self.stream = stream @@ -1146,12 +1223,12 @@ def getNumPages(self): self._override_encryption = True self.decrypt('') return self.trailer["/Root"]["/Pages"]["/Count"] - except: + except Exception: raise utils.PdfReadError("File has not been decrypted") finally: self._override_encryption = False else: - if self.flattenedPages == None: + if self.flattenedPages is None: self._flatten() return len(self.flattenedPages) @@ -1171,8 +1248,8 @@ def getPage(self, pageNumber): :rtype: :class:`PageObject` """ ## ensure that we're not trying to access an encrypted PDF - #assert not self.trailer.has_key("/Encrypt") - if self.flattenedPages == None: + # assert not self.trailer.has_key("/Encrypt") + if self.flattenedPages is None: self._flatten() return self.flattenedPages[pageNumber] @@ -1202,7 +1279,7 @@ def getFields(self, tree = None, retval = None, fileobj = None): "/T" : "Field Name", "/TU" : "Alternate Field Name", "/TM" : "Mapping Name", "/Ff" : "Field Flags", "/V" : "Value", "/DV" : "Default Value"} - if retval == None: + if retval is None: retval = {} catalog = self.trailer["/Root"] # get the AcroForm tree @@ -1210,7 +1287,7 @@ def getFields(self, tree = None, retval = None, fileobj = None): tree = catalog["/AcroForm"] else: return None - if tree == None: + if tree is None: return retval self._checkKids(tree, retval, fileobj) @@ -1244,9 +1321,9 @@ def _buildField(self, field, retval, fileobj, fieldAttributes): retval[key] = Field(field) def _checkKids(self, tree, retval, fileobj): - if "/Kids" in tree: + if PA.KIDS in tree: # recurse down the tree - for kid in tree["/Kids"]: + for kid in tree[PA.KIDS]: self.getFields(kid.getObject(), retval, fileobj) def _writeField(self, fileobj, field, fieldAttributes): @@ -1291,7 +1368,7 @@ def getNamedDestinations(self, tree=None, retval=None): :class:`Destinations`. :rtype: dict """ - if retval == None: + if retval is None: retval = {} catalog = self.trailer["/Root"] @@ -1303,12 +1380,12 @@ def getNamedDestinations(self, tree=None, retval=None): if "/Dests" in names: tree = names['/Dests'] - if tree == None: + if tree is None: return retval - if "/Kids" in tree: + if PA.KIDS in tree: # recurse down the tree - for kid in tree["/Kids"]: + for kid in tree[PA.KIDS]: self.getNamedDestinations(kid.getObject(), retval) if "/Names" in tree: @@ -1319,7 +1396,7 @@ def getNamedDestinations(self, tree=None, retval=None): if isinstance(val, DictionaryObject) and '/D' in val: val = val['/D'] dest = self._buildDestination(key, val) - if dest != None: + if dest is not None: retval[key] = dest return retval @@ -1336,7 +1413,7 @@ def getOutlines(self, node=None, outlines=None): :return: a nested list of :class:`Destinations`. """ - if outlines == None: + if outlines is None: outlines = [] catalog = self.trailer["/Root"] @@ -1354,7 +1431,7 @@ def getOutlines(self, node=None, outlines=None): node = lines["/First"] self._namedDests = self.getNamedDestinations() - if node == None: + if node is None: return outlines # see if there are any more outlines @@ -1495,25 +1572,27 @@ def getPageMode(self): def _flatten(self, pages=None, inherit=None, indirectRef=None): inheritablePageAttributes = ( - NameObject("/Resources"), NameObject("/MediaBox"), - NameObject("/CropBox"), NameObject("/Rotate") + NameObject(PG.RESOURCES), NameObject(PG.MEDIABOX), + NameObject(PG.CROPBOX), NameObject(PG.ROTATE) ) - if inherit == None: + if inherit is None: inherit = dict() - if pages == None: - self.flattenedPages = [] + if pages is None: + # Fix issue 327: set flattenedPages attribute only for + # decrypted file catalog = self.trailer["/Root"].getObject() pages = catalog["/Pages"].getObject() + self.flattenedPages = [] t = "/Pages" - if "/Type" in pages: - t = pages["/Type"] + if PA.TYPE in pages: + t = pages[PA.TYPE] if t == "/Pages": for attr in inheritablePageAttributes: if attr in pages: inherit[attr] = pages[attr] - for page in pages["/Kids"]: + for page in pages[PA.KIDS]: addt = {} if isinstance(page, IndirectObject): addt["indirectRef"] = page @@ -1561,7 +1640,7 @@ def _getObjectFromStream(self, indirectReference): streamData.seek(0, 0) lines = streamData.readlines() for i in range(0, len(lines)): - print((lines[i])) + print(lines[i]) streamData.seek(pos, 0) try: obj = readObject(streamData, self) @@ -1586,7 +1665,7 @@ def getObject(self, indirectReference): if debug: print(("looking at:", indirectReference.idnum, indirectReference.generation)) retval = self.cacheGetIndirectObject(indirectReference.generation, indirectReference.idnum) - if retval != None: + if retval is not None: return retval if indirectReference.generation == 0 and \ indirectReference.idnum in self.xref_objStm: @@ -1603,11 +1682,12 @@ def getObject(self, indirectReference): raise utils.PdfReadError("Expected object ID (%d %d) does not match actual (%d %d); xref table not zero-indexed." \ % (indirectReference.idnum, indirectReference.generation, idnum, generation)) else: pass # xref table is corrected in non-strict mode - elif idnum != indirectReference.idnum: + elif idnum != indirectReference.idnum and self.strict: # some other problem raise utils.PdfReadError("Expected object ID (%d %d) does not match actual (%d %d)." \ % (indirectReference.idnum, indirectReference.generation, idnum, generation)) - assert generation == indirectReference.generation + if self.strict: + assert generation == indirectReference.generation retval = readObject(self.stream, self) # override encryption is used for the /Encrypt dictionary @@ -1627,14 +1707,14 @@ def getObject(self, indirectReference): else: warnings.warn("Object %d %d not defined."%(indirectReference.idnum, indirectReference.generation), utils.PdfReadWarning) - #if self.strict: - raise utils.PdfReadError("Could not find object.") + if self.strict: + raise utils.PdfReadError("Could not find object.") self.cacheIndirectObject(indirectReference.generation, indirectReference.idnum, retval) return retval def _decryptObject(self, obj, key): - if isinstance(obj, ByteStringObject) or isinstance(obj, TextStringObject): + if isinstance(obj, (ByteStringObject, TextStringObject)): obj = createStringObject(utils.RC4_encrypt(key, obj.original_bytes)) elif isinstance(obj, StreamObject): obj._data = utils.RC4_encrypt(key, obj._data) @@ -1657,11 +1737,15 @@ def readObjectHeader(self, stream): idnum = readUntilWhitespace(stream) extra |= utils.skipOverWhitespace(stream); stream.seek(-1, 1) generation = readUntilWhitespace(stream) - obj = stream.read(3) + extra |= utils.skipOverWhitespace(stream); stream.seek(-1, 1) + + # although it's not used, it might still be necessary to read + _obj = stream.read(3) # noqa: F841 + readNonWhitespace(stream) stream.seek(-1, 1) if (extra and self.strict): - #not a fatal error + # not a fatal error warnings.warn("Superfluous whitespace found in object header %s %s" % \ (idnum, generation), utils.PdfReadWarning) return int(idnum), int(generation) @@ -1694,7 +1778,7 @@ def read(self, stream): while line[:5] != b_("%%EOF"): if stream.tell() < last1K: raise utils.PdfReadError("EOF marker not found") - line = self.readNextEndLine(stream) + line = self.readNextEndLine(stream, last1K) if debug: print(" line:",line) # find startxref entry - the location of the xref table @@ -1734,8 +1818,8 @@ def read(self, stream): self.xrefIndex = num if self.strict: warnings.warn("Xref table not zero-indexed. ID numbers for objects will be corrected.", utils.PdfReadWarning) - #if table not zero indexed, could be due to error from when PDF was created - #which will lead to mismatched indices later on, only warned and corrected if self.strict=True + # if table not zero indexed, could be due to error from when PDF was created + # which will lead to mismatched indices later on, only warned and corrected if self.strict=True firsttime = False readNonWhitespace(stream) stream.seek(-1, 1) @@ -1843,8 +1927,8 @@ def used_before(num, generation): # The rest of the elements depend on the xref_type if xref_type == 0: # linked list of free objects - next_free_object = getEntry(1) - next_generation = getEntry(2) + next_free_object = getEntry(1) # noqa: F841 + next_generation = getEntry(2) # noqa: F841 elif xref_type == 1: # objects that are in use but are not compressed byte_offset = getEntry(1) @@ -1877,6 +1961,15 @@ def used_before(num, generation): else: break else: + # some PDFs have /Prev=0 in the trailer, instead of no /Prev + if startxref == 0: + if self.strict: + raise utils.PdfReadError("/Prev=0 in the trailer (try" + " opening with strict=False)") + else: + warnings.warn("/Prev=0 in the trailer - assuming there" + " is no previous xref table") + break # bad xref character at startxref. Let's see if we can find # the xref table nearby, as we've observed this error with an # off-by-one before. @@ -1899,7 +1992,7 @@ def used_before(num, generation): continue # no xref table found at specified location raise utils.PdfReadError("Could not find xref table at specified location") - #if not zero-indexed, verify that the table is correct; change it if necessary + # if not zero-indexed, verify that the table is correct; change it if necessary if self.xrefIndex and not self.strict: loc = stream.tell() for gen in self.xref: @@ -1913,8 +2006,8 @@ def used_before(num, generation): if pid == id - self.xrefIndex: self._zeroXref(gen) break - #if not, then either it's just plain wrong, or the non-zero-index is actually correct - stream.seek(loc, 0) #return to where it was + # if not, then either it's just plain wrong, or the non-zero-index is actually correct + stream.seek(loc, 0) # return to where it was def _zeroXref(self, generation): self.xref[generation] = dict( (k-self.xrefIndex, v) for (k, v) in list(self.xref[generation].items()) ) @@ -1927,13 +2020,13 @@ def _pairs(self, array): if (i+1) >= len(array): break - def readNextEndLine(self, stream): + def readNextEndLine(self, stream, limit_offset=0): debug = False if debug: print(">>readNextEndLine") line = b_("") while True: # Prevent infinite loops in malformed PDFs - if stream.tell() == 0: + if stream.tell() == 0 or stream.tell() == limit_offset: raise utils.PdfReadError("Could not read malformed PDF file") x = stream.read(1) if debug: print((" x:", x, "%x"%ord(x))) @@ -1953,7 +2046,7 @@ def readNextEndLine(self, stream): if stream.tell() < 2: raise utils.PdfReadError("EOL marker not found") stream.seek(-2, 1) - stream.seek(2 if crlf else 1, 1) #if using CR+LF, go back 2 bytes, else 1 + stream.seek(2 if crlf else 1, 1) # if using CR+LF, go back 2 bytes, else 1 break else: if debug: print(" x is neither") @@ -1993,7 +2086,7 @@ def _decrypt(self, password): if encrypt['/Filter'] != '/Standard': raise NotImplementedError("only Standard PDF encryption handler is available") if not (encrypt['/V'] in (1, 2)): - raise NotImplementedError("only algorithm code 1 and 2 are supported") + raise NotImplementedError("only algorithm code 1 and 2 are supported. This PDF uses code %s" % encrypt['/V']) user_password, key = self._authenticateUserPassword(password) if user_password: self._decryption_key = key @@ -2003,7 +2096,7 @@ def _decrypt(self, password): if rev == 2: keylen = 5 else: - keylen = encrypt['/Length'].getObject() // 8 + keylen = encrypt[SA.LENGTH].getObject() // 8 key = _alg33_1(password, rev, keylen) real_O = encrypt["/O"].getObject() if rev == 2: @@ -2034,7 +2127,7 @@ def _authenticateUserPassword(self, password): U, key = _alg34(password, owner_entry, p_entry, id1_entry) elif rev >= 3: U, key = _alg35(password, rev, - encrypt["/Length"].getObject() // 8, owner_entry, + encrypt[SA.LENGTH].getObject() // 8, owner_entry, p_entry, id1_entry, encrypt.get("/EncryptMetadata", BooleanObject(False)).getObject()) U, real_U = U[:16], real_U[:16] @@ -2055,10 +2148,10 @@ def getRectangle(self, name, defaults): retval = self.get(name) if isinstance(retval, RectangleObject): return retval - if retval == None: + if retval is None: for d in defaults: retval = self.get(d) - if retval != None: + if retval is not None: break if isinstance(retval, IndirectObject): retval = self.pdf.getObject(retval) @@ -2125,7 +2218,7 @@ def createBlankPage(pdf=None, width=None, height=None): # Creates a new page (cf PDF Reference 7.7.3.3) page.__setitem__(NameObject('/Type'), NameObject('/Page')) page.__setitem__(NameObject('/Parent'), NullObject()) - page.__setitem__(NameObject('/Resources'), DictionaryObject()) + page.__setitem__(NameObject(PG.RESOURCES), DictionaryObject()) if width is None or height is None: if pdf is not None and pdf.getNumPages() > 0: lastpage = pdf.getPage(pdf.getNumPages() - 1) @@ -2133,11 +2226,11 @@ def createBlankPage(pdf=None, width=None, height=None): height = lastpage.mediaBox.getHeight() else: raise utils.PageSizeNotDefinedError() - page.__setitem__(NameObject('/MediaBox'), + page.__setitem__(NameObject(PG.MEDIABOX), RectangleObject([0, 0, width, height])) return page - createBlankPage = staticmethod(createBlankPage) + createBlankPage = staticmethod(createBlankPage) # type: ignore def rotateClockwise(self, angle): """ @@ -2162,7 +2255,8 @@ def rotateCounterClockwise(self, angle): return self def _rotate(self, angle): - currentAngle = self.get("/Rotate", 0) + rotateObj = self.get("/Rotate", 0) + currentAngle = rotateObj if isinstance(rotateObj, int) else rotateObj.getObject() self[NameObject("/Rotate")] = NumberObject(currentAngle + angle) def _mergeResources(res1, res2, resource): @@ -2178,19 +2272,27 @@ def _mergeResources(res1, res2, resource): elif key not in newRes: newRes[key] = page2Res.raw_get(key) return newRes, renameRes - _mergeResources = staticmethod(_mergeResources) + _mergeResources = staticmethod(_mergeResources) # type: ignore def _contentStreamRename(stream, rename, pdf): if not rename: return stream stream = ContentStream(stream, pdf) - for operands, operator in stream.operations: - for i in range(len(operands)): - op = operands[i] - if isinstance(op, NameObject): - operands[i] = rename.get(op,op) + for operands, _operator in stream.operations: + if isinstance(operands, list): + for i in range(len(operands)): + op = operands[i] + if isinstance(op, NameObject): + operands[i] = rename.get(op,op) + elif isinstance(operands, dict): + for i in operands: + op = operands[i] + if isinstance(op, NameObject): + operands[i] = rename.get(op,op) + else: + raise KeyError ("type of operands is %s" % type (operands)) return stream - _contentStreamRename = staticmethod(_contentStreamRename) + _contentStreamRename = staticmethod(_contentStreamRename) # type: ignore def _pushPopGS(contents, pdf): # adds a graphics state "push" and "pop" to the beginning and end @@ -2200,7 +2302,7 @@ def _pushPopGS(contents, pdf): stream.operations.insert(0, [[], "q"]) stream.operations.append([[], "Q"]) return stream - _pushPopGS = staticmethod(_pushPopGS) + _pushPopGS = staticmethod(_pushPopGS) # type: ignore def _addTransformationMatrix(contents, pdf, ctm): # adds transformation matrix at the beginning of the given @@ -2211,7 +2313,7 @@ def _addTransformationMatrix(contents, pdf, ctm): FloatObject(c), FloatObject(d), FloatObject(e), FloatObject(f)], " cm"]) return contents - _addTransformationMatrix = staticmethod(_addTransformationMatrix) + _addTransformationMatrix = staticmethod(_addTransformationMatrix) # type: ignore def getContents(self): """ @@ -2245,27 +2347,27 @@ def _mergePage(self, page2, page2transformation=None, ctm=None, expand=False): newResources = DictionaryObject() rename = {} - originalResources = self["/Resources"].getObject() - page2Resources = page2["/Resources"].getObject() + originalResources = self[PG.RESOURCES].getObject() + page2Resources = page2[PG.RESOURCES].getObject() newAnnots = ArrayObject() for page in (self, page2): - if "/Annots" in page: - annots = page["/Annots"] + if PG.ANNOTS in page: + annots = page[PG.ANNOTS] if isinstance(annots, ArrayObject): for ref in annots: newAnnots.append(ref) - for res in "/ExtGState", "/Font", "/XObject", "/ColorSpace", "/Pattern", "/Shading", "/Properties": + for res in "/ExtGState", RES.FONT, RES.XOBJECT, RES.COLOR_SPACE, "/Pattern", "/Shading", "/Properties": new, newrename = PageObject._mergeResources(originalResources, page2Resources, res) if new: newResources[NameObject(res)] = new rename.update(newrename) # Combine /ProcSet sets. - newResources[NameObject("/ProcSet")] = ArrayObject( - frozenset(originalResources.get("/ProcSet", ArrayObject()).getObject()).union( - frozenset(page2Resources.get("/ProcSet", ArrayObject()).getObject()) + newResources[NameObject(RES.PROCSET)] = ArrayObject( + frozenset(originalResources.get(RES.PROCSET, ArrayObject()).getObject()).union( + frozenset(page2Resources.get(RES.PROCSET, ArrayObject()).getObject()) ) ) @@ -2278,6 +2380,10 @@ def _mergePage(self, page2, page2transformation=None, ctm=None, expand=False): page2Content = page2.getContents() if page2Content is not None: + page2Content = ContentStream(page2Content, self.pdf) + page2Content.operations.insert(0, [map(FloatObject, [page2.trimBox.getLowerLeft_x(), page2.trimBox.getLowerLeft_y(), page2.trimBox.getWidth(), page2.trimBox.getHeight()]), "re"]) + page2Content.operations.insert(1, [[], "W"]) + page2Content.operations.insert(2, [[], "n"]) if page2transformation is not None: page2Content = page2transformation(page2Content) page2Content = PageObject._contentStreamRename( @@ -2309,8 +2415,8 @@ def _mergePage(self, page2, page2transformation=None, ctm=None, expand=False): self.mediaBox.setUpperRight(upperright) self[NameObject('/Contents')] = ContentStream(newContentArray, self.pdf) - self[NameObject('/Resources')] = newResources - self[NameObject('/Annots')] = newAnnots + self[NameObject(PG.RESOURCES)] = newResources + self[NameObject(PG.ANNOTS)] = newAnnots def mergeTransformedPage(self, page2, ctm, expand=False): """ @@ -2490,11 +2596,6 @@ def mergeRotatedScaledTranslatedPage(self, page2, rotation, scale, tx, ty, expan ctm[1][0], ctm[1][1], ctm[2][0], ctm[2][1]], expand) - ## - # Applys a transformation matrix the page. - # - # @param ctm A 6 elements tuple containing the operands of the - # transformation matrix def addTransformation(self, ctm): """ Applies a transformation matrix to the page. @@ -2578,7 +2679,7 @@ def compressContentStreams(self): content = ContentStream(content, self.pdf) self[NameObject("/Contents")] = content.flateEncode() - def extractText(self): + def extractText(self, Tj_sep="", TJ_sep=" "): """ Locate all text drawing commands, in the order they are provided in the content stream, and extract the text. This works well for some PDF @@ -2600,7 +2701,9 @@ def extractText(self): if operator == b_("Tj"): _text = operands[0] if isinstance(_text, TextStringObject): + text += Tj_sep text += _text + text += "\n" elif operator == b_("T*"): text += "\n" elif operator == b_("'"): @@ -2616,18 +2719,19 @@ def extractText(self): elif operator == b_("TJ"): for i in operands[0]: if isinstance(i, TextStringObject): + text += TJ_sep text += i text += "\n" return text - mediaBox = createRectangleAccessor("/MediaBox", ()) + mediaBox = createRectangleAccessor(PG.MEDIABOX, ()) """ A :class:`RectangleObject`, expressed in default user space units, defining the boundaries of the physical medium on which the page is intended to be displayed or printed. """ - cropBox = createRectangleAccessor("/CropBox", ("/MediaBox",)) + cropBox = createRectangleAccessor("/CropBox", (PG.MEDIABOX,)) """ A :class:`RectangleObject`, expressed in default user space units, defining the visible region of default user space. When the page is @@ -2636,20 +2740,20 @@ def extractText(self): implementation-defined manner. Default value: same as :attr:`mediaBox`. """ - bleedBox = createRectangleAccessor("/BleedBox", ("/CropBox", "/MediaBox")) + bleedBox = createRectangleAccessor("/BleedBox", ("/CropBox", PG.MEDIABOX)) """ A :class:`RectangleObject`, expressed in default user space units, defining the region to which the contents of the page should be clipped when output in a production enviroment. """ - trimBox = createRectangleAccessor("/TrimBox", ("/CropBox", "/MediaBox")) + trimBox = createRectangleAccessor("/TrimBox", ("/CropBox", PG.MEDIABOX)) """ A :class:`RectangleObject`, expressed in default user space units, defining the intended dimensions of the finished page after trimming. """ - artBox = createRectangleAccessor("/ArtBox", ("/CropBox", "/MediaBox")) + artBox = createRectangleAccessor("/ArtBox", ("/CropBox", PG.MEDIABOX)) """ A :class:`RectangleObject`, expressed in default user space units, defining the extent of the page's meaningful content as intended by the @@ -2667,7 +2771,7 @@ def __init__(self, stream, pdf): if isinstance(stream, ArrayObject): data = b_("") for s in stream: - data += s.getObject().getData() + data += b_(s.getObject().getData()) stream = BytesIO(b_(data)) else: stream = BytesIO(b_(stream.getData())) @@ -2723,16 +2827,25 @@ def _readInlineImage(self, stream): # left at beginning of ID tmp = stream.read(3) assert tmp[:2] == b_("ID") - data = b_("") + data = BytesIO() + # Read the inline image, while checking for EI (End Image) operator. while True: - # Read the inline image, while checking for EI (End Image) operator. - tok = stream.read(1) - if not tok: - if self.strict: - raise utils.PdfReadError("No end to inline image.") - # Even though we're not raising, this is almost certainly bad. - break - if tok == b_("E"): + # Read 8 kB at a time and check if the chunk contains the E operator. + buf = stream.read(8192) + # We have reached the end of the stream, but haven't found the EI operator. + if not buf: + raise utils.PdfReadError("Unexpected end of stream") + loc = buf.find(b_("E")) + + if loc == -1: + data.write(buf) + else: + # Write out everything before the E. + data.write(buf[0:loc]) + + # Seek back in the stream to read the E next. + stream.seek(loc - len(buf), 1) + tok = stream.read(1) # Check for End Image tok2 = stream.read(1) if tok2 == b_("I"): @@ -2754,14 +2867,12 @@ def _readInlineImage(self, stream): stream.seek(-1, 1) break else: - stream.seek(-1,1) - data += info + stream.seek(-1, 1) + data.write(info) else: stream.seek(-1, 1) - data += tok - else: - data += tok - return {"settings": settings, "data": data} + data.write(tok) + return {"settings": settings, "data": data.getvalue()} def _getData(self): newdata = BytesIO() @@ -2792,7 +2903,7 @@ class DocumentInformation(DictionaryObject): """ A class representing the basic document metadata provided in a PDF File. This class is accessible through - :meth:`getDocumentInfo()` + :meth:`.getDocumentInfo()` All text properties of the document metadata have *two* properties, eg. author and author_raw. The non-raw property will @@ -2812,7 +2923,7 @@ def getText(self, key): return retval return None - title = property(lambda self: self.getText("/Title")) + title = property(lambda self: self.getText("/Title") or self.get("/Title").getObject() if self.get("/Title") else None) """Read-only property accessing the document's **title**. Returns a unicode string (``TextStringObject``) or ``None`` if the title is not specified.""" @@ -2900,7 +3011,7 @@ def _alg32(password, rev, keylen, owner_entry, p_entry, id1_entry, metadata_encr # encryption key as defined by the value of the encryption dictionary's # /Length entry. if rev >= 3: - for i in range(50): + for _ in range(50): md5_hash = md5(md5_hash[:keylen]).digest() # 9. Set the encryption key to the first n bytes of the output from the # final MD5 hash, where n is always 5 for revision 2 but, for revision 3 or @@ -2950,7 +3061,7 @@ def _alg33_1(password, rev, keylen): # from the previous MD5 hash and pass it as input into a new MD5 hash. md5_hash = m.digest() if rev >= 3: - for i in range(50): + for _ in range(50): md5_hash = md5(md5_hash).digest() # 4. Create an RC4 encryption key using the first n bytes of the output # from the final MD5 hash, where n is always 5 for revision 2 but, for @@ -3002,8 +3113,8 @@ def _alg35(password, rev, keylen, owner_entry, p_entry, id1_entry, metadata_encr # counter (from 1 to 19). for i in range(1, 20): new_key = b_('') - for l in range(len(key)): - new_key += b_(chr(ord_(key[l]) ^ i)) + for k in key: + new_key += b_(chr(ord_(k) ^ i)) val = utils.RC4_encrypt(new_key, val) # 6. Append 16 bytes of arbitrary padding to the output from the final # invocation of the RC4 function and store the 32-byte result as the value diff --git a/PyPDF2/utils.py b/PyPDF2/utils.py index 718a875c5b..f528bfa06b 100644 --- a/PyPDF2/utils.py +++ b/PyPDF2/utils.py @@ -35,17 +35,18 @@ import sys try: - import __builtin__ as builtins -except ImportError: # Py3 import builtins + from typing import Dict +except ImportError: # Py2.7 + import __builtin__ as builtins # type: ignore - +ERR_STREAM_TRUNCATED_PREMATURELY = "Stream has ended unexpectedly" xrange_fn = getattr(builtins, "xrange", range) _basestring = getattr(builtins, "basestring", str) bytes_type = type(bytes()) # Works the same in Python 2.X and 3.X string_type = getattr(builtins, "unicode", str) -int_types = (int, long) if sys.version_info[0] < 3 else (int,) +int_types = (int, long) if sys.version_info[0] < 3 else (int,) # type: ignore # noqa # Make basic type tests more consistent @@ -61,12 +62,14 @@ def isInt(n): def isBytes(b): """Test if arg is a bytes instance. Compatible with Python 2 and 3.""" + import warnings + warnings.warn("PyPDF2.utils.isBytes will be deprecated", DeprecationWarning) return isinstance(b, bytes_type) -#custom implementation of warnings.formatwarning def formatWarning(message, category, filename, lineno, line=None): - file = filename.replace("/", "\\").rsplit("\\", 1)[1] # find the file name + """custom implementation of warnings.formatwarning""" + file = filename.replace("/", "\\").rsplit("\\", 1)[-1] # find the file name return "%s: %s [%s:%s]\n" % (category.__name__, message, file, lineno) @@ -120,7 +123,7 @@ def skipOverComment(stream): def readUntilRegex(stream, regex, ignore_eof=False): """ Reads until the regular expression pattern matched (ignore the match) - Raise PdfStreamError on premature end-of-file. + :raises PdfStreamError: on premature end-of-file :param bool ignore_eof: If true, ignore end-of-line and return immediately """ name = b_('') @@ -128,10 +131,10 @@ def readUntilRegex(stream, regex, ignore_eof=False): tok = stream.read(16) if not tok: # stream has truncated prematurely - if ignore_eof == True: + if ignore_eof: return name else: - raise PdfStreamError("Stream has ended unexpectedly") + raise PdfStreamError(ERR_STREAM_TRUNCATED_PREMATURELY) m = regex.search(tok) if m is not None: name += tok[:m.start()] @@ -172,14 +175,14 @@ def RC4_encrypt(key, plaintext): j = (j + S[i] + ord_(key[i % len(key)])) % 256 S[i], S[j] = S[j], S[i] i, j = 0, 0 - retval = b_("") + retval = [] for x in range(len(plaintext)): i = (i + 1) % 256 j = (j + S[i]) % 256 S[i], S[j] = S[j], S[i] t = S[(S[i] + S[j]) % 256] - retval += b_(chr(ord_(plaintext[x]) ^ t)) - return retval + retval.append(b_(chr(ord_(plaintext[x]) ^ t))) + return b_("").join(retval) def matrixMultiply(a, b): @@ -194,11 +197,10 @@ def markLocation(stream): # Mainly for debugging RADIUS = 5000 stream.seek(-RADIUS, 1) - outputDoc = open('PyPDF2_pdfLocation.txt', 'w') - outputDoc.write(stream.read(RADIUS)) - outputDoc.write('HERE') - outputDoc.write(stream.read(RADIUS)) - outputDoc.close() + with open('PyPDF2_pdfLocation.txt', 'wb') as output_fh: + output_fh.write(stream.read(RADIUS)) + output_fh.write(b'HERE') + output_fh.write(stream.read(RADIUS)) stream.seek(-RADIUS, 1) @@ -226,7 +228,7 @@ class PdfStreamError(PdfReadError): def b_(s): return s else: - B_CACHE = {} + B_CACHE = {} # type: Dict[str, bytes] def b_(s): bc = B_CACHE @@ -235,15 +237,21 @@ def b_(s): if type(s) == bytes: return s else: - r = s.encode('latin-1') - if len(s) < 2: - bc[s] = r - return r + try: + r = s.encode('latin-1') + if len(s) < 2: + bc[s] = r + return r + except Exception: + r = s.encode('utf-8') + if len(s) < 2: + bc[s] = r + return r def u_(s): if sys.version_info[0] < 3: - return unicode(s, 'unicode_escape') + return unicode(s, 'unicode_escape') # noqa else: return s @@ -293,3 +301,17 @@ def hexStr(num): WHITESPACES = [b_(x) for x in [' ', '\n', '\r', '\t', '\x00']] + + +def paethPredictor(left, up, up_left): + p = left + up - up_left + dist_left = abs(p - left) + dist_up = abs(p - up) + dist_up_left = abs(p - up_left) + + if dist_left <= dist_up and dist_left <= dist_up_left: + return left + elif dist_up <= dist_up_left: + return up + else: + return up_left diff --git a/PyPDF2/xmp.py b/PyPDF2/xmp.py index 7ba62f0dd9..206317f355 100644 --- a/PyPDF2/xmp.py +++ b/PyPDF2/xmp.py @@ -1,9 +1,9 @@ -import re import datetime import decimal -from .generic import PdfObject -from xml.dom import getDOMImplementation +import re from xml.dom.minidom import parseString + +from .generic import PdfObject from .utils import u_ RDF_NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" @@ -70,7 +70,7 @@ def getElement(self, aboutUri, namespace, name): for desc in self.rdfRoot.getElementsByTagNameNS(RDF_NAMESPACE, "Description"): if desc.getAttributeNS(RDF_NAMESPACE, "about") == aboutUri: attr = desc.getAttributeNodeNS(namespace, name) - if attr != None: + if attr is not None: yield attr for element in desc.getElementsByTagNameNS(namespace, name): yield element @@ -191,7 +191,7 @@ def get(self): else: value = self._getText(element) break - if value != None: + if value is not None: value = converter(value) ns_cache = self.cache.setdefault(namespace, {}) ns_cache[name] = value @@ -217,7 +217,7 @@ def get(self): dc_date = property(_getter_seq(DC_NAMESPACE, "date", _converter_date)) """ - A sorted array of dates (datetime.datetime instances) of signifigance to + A sorted array of dates (datetime.datetime instances) of significance to the resource. The dates and times are in UTC. """ diff --git a/README.md b/README.md index 23a1eef766..884e9a4cf4 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,100 @@ -#PyPDF2 +[![PyPI version](https://badge.fury.io/py/PyPDF2.svg)](https://badge.fury.io/py/PyPDF2) +[![Python Support](https://img.shields.io/pypi/pyversions/PyPDF2.svg)](https://pypi.org/project/PyPDF2/) +[![](https://img.shields.io/badge/-documentation-green)](https://pypdf2.readthedocs.io/en/latest/) +![GitHub last commit](https://img.shields.io/github/last-commit/py-pdf/PyPDF2) +[![codecov](https://codecov.io/gh/py-pdf/PyPDF2/branch/main/graph/badge.svg?token=id42cGNZ5Z)](https://codecov.io/gh/py-pdf/PyPDF2) -PyPDF2 is a pure-python PDF library capable of -splitting, merging together, cropping, and transforming -the pages of PDF files. It can also add custom -data, viewing options, and passwords to PDF files. -It can retrieve text and metadata from PDFs as well -as merge entire files together. +# PyPDF2 -Homepage -http://mstamy2.github.io/PyPDF2/ +PyPDF2 is a free and open-source pure-python PDF library capable of splitting, +[merging](https://pypdf2.readthedocs.io/en/latest/user/merging-pdfs.html), +[cropping, and transforming](https://pypdf2.readthedocs.io/en/latest/user/cropping-and-transforming.html) +the pages of PDF files. It can also add +custom data, viewing options, and +[passwords](https://pypdf2.readthedocs.io/en/latest/user/encryption-decryption.html) +to PDF files. PyPDF2 can +[retrieve text](https://pypdf2.readthedocs.io/en/latest/user/extract-text.html) +and +[metadata](https://pypdf2.readthedocs.io/en/latest/user/metadata.html) +from PDFs as well. -##Examples -Please see `sample code` folder +## Installation -##Documentation +You can install PyPDF2 via pip: -Documentation is available at -https://pythonhosted.org/PyPDF2/ +``` +pip install PyPDF2 +``` +## Usage -##FAQ -Please see -http://mstamy2.github.io/PyPDF2/FAQ.html +```python +from PyPDF2 import PdfFileReader +reader = PdfFileReader("example.pdf") +number_of_pages = reader.numPages +page = reader.pages[0] +text = page.extractText() +``` -##Tests -PyPDF2 includes a test suite built on the unittest framework. All tests are located in the "Tests" folder. -Tests can be run from the command line by: +PyPDF2 can do a lot more, e.g. splitting, merging, reading and creating +annotations, decrypting and encrypting, and more. + +Please see [the documentation](https://pypdf2.readthedocs.io/en/latest/) +and [`Scripts`](https://github.com/py-pdf/PyPDF2/tree/main/Scripts) for +more usage examples! + +A lot of questions are asked and answered +[on StackOverflow](https://stackoverflow.com/questions/tagged/pypdf2). + +## Contributions + +Maintaining PyPDF2 is a collaborative effort. You can support PyPDF2 by writing +documentation, helping to narrow down issues, and adding code. + +### Q&A + +The experience PyPDF2 users have covers the whole range from beginners who +want to make their live easier to experts who developed software before PDF +existed. You can contribute to the PyPDF2 community by answering questions +on [StackOverflow](https://stackoverflow.com/questions/tagged/pypdf2), +helping in [discussions](https://github.com/py-pdf/PyPDF2/discussions), +and asking users who report issues for [MCVE](https://stackoverflow.com/help/minimal-reproducible-example)'s (Code + example PDF!). + + +### Issues + +A good bug ticket includes a MCVE - a minimal complete verifiable example. +For PyPDF2, this means that you must upload a PDF that causes the bug to occur +as well as the code you're executing with all of the output. Use +`print(PyPDF2.__version__)` to tell us which version you're using. + +### Code + +All code contributions are welcome, but smaller ones have a better chance to +get included in a timely manner. Adding unit tests for new features or test +cases for bugs you've fixed help us to ensure that the Pull Request (PR) is fine. + +PyPDF2 includes a test suite which can be executed with `pytest`: ```bash -python -m unittest Tests.tests -``` \ No newline at end of file +$ pytest +========================= test session starts ========================= +platform linux -- Python 3.6.15, pytest-7.0.1, pluggy-1.0.0 +rootdir: /home/moose/Github/Martin/PyPDF2 +plugins: cov-3.0.0 +collected 57 items + +Tests/test_basic_features.py .. [ 3%] +Tests/test_merger.py . [ 5%] +Tests/test_page.py . [ 7%] +Tests/test_pagerange.py ....... [ 19%] +Tests/test_reader.py .......... [ 36%] +Tests/test_utils.py ...................... [ 75%] +Tests/test_workflows.py .......... [ 92%] +Tests/test_writer.py .. [ 96%] +Tests/test_xmp.py .. [100%] + +========================= 57 passed in 1.06s ========================== +``` diff --git a/Resources/attachment.pdf b/Resources/attachment.pdf new file mode 100644 index 0000000000..34b4802efa Binary files /dev/null and b/Resources/attachment.pdf differ diff --git a/Resources/commented-xmp.pdf b/Resources/commented-xmp.pdf new file mode 100644 index 0000000000..4d6f04c50a Binary files /dev/null and b/Resources/commented-xmp.pdf differ diff --git a/Resources/commented.pdf b/Resources/commented.pdf new file mode 100644 index 0000000000..4d9fcdd4a8 Binary files /dev/null and b/Resources/commented.pdf differ diff --git a/Resources/crazyones.txt b/Resources/crazyones.txt index 1e6078965e..468a57e90d 100644 --- a/Resources/crazyones.txt +++ b/Resources/crazyones.txt @@ -1 +1 @@ -TheCrazyOnesOctober14,1998Herestothecrazyones.Themis˝ts.Therebels.Thetroublemakers.Theroundpegsinthesquareholes.Theoneswhoseethingsdi˙erently.Theyrenotfondofrules.Andtheyhavenorespectforthestatusquo.Youcanquotethem,disagreewiththem,glorifyorvilifythem.Abouttheonlythingyoucantdoisignorethem.Becausetheychangethings.Theyinvent.Theyimagine.Theyheal.Theyexplore.Theycreate.Theyinspire.Theypushthehumanraceforward.Maybetheyhavetobecrazy.Howelsecanyoustareatanemptycanvasandseeaworkofart?Orsitinsilenceandhearasongthatsneverbeenwritten?Orgazeataredplanetandseealaboratoryonwheels?Wemaketoolsforthesekindsofpeople.Whilesomeseethemasthecrazyones,weseegenius.Becausethepeoplewhoarecrazyenoughtothinktheycanchangetheworld,aretheoneswhodo. \ No newline at end of file + The Cr azy Ones Octob er 14, 1998 Heres to the crazy ones. The mis˝ts. The reb els. The troublemak ers. The round p egs in the square holes. The ones who see things di˙eren tly . Theyre not fond of rules. And they ha v e no resp ect for the status quo. Y ou can quote them, disagree with them, glorify or vilify them. Ab out the only thing y ou cant do is ignore them. Because they c hange things. They in v en t. They imagine. They heal. They explore. They create. They inspire. They push the h uman race forw ard. Ma yb e they ha v e to b e crazy . Ho w else can y ou stare at an empt y can v as and see a w ork of art? Or sit in silence and hear a song thats nev er b een written? Or gaze at a red planet and see a lab oratory on wheels? W e mak e to ols for these kinds of p eople. While some see them as the crazy ones, w e see genius. Because the p eople who are crazy enough to think they can c hange the w orld, are the ones who do. \ No newline at end of file diff --git a/Resources/encrypted-file.pdf b/Resources/encrypted-file.pdf new file mode 100644 index 0000000000..c300a5fed5 Binary files /dev/null and b/Resources/encrypted-file.pdf differ diff --git a/Resources/git.pdf b/Resources/git.pdf new file mode 100644 index 0000000000..2d1133ace3 Binary files /dev/null and b/Resources/git.pdf differ diff --git a/Resources/imagemagick-ASCII85Decode.pdf b/Resources/imagemagick-ASCII85Decode.pdf new file mode 100644 index 0000000000..46aabc0fc4 Binary files /dev/null and b/Resources/imagemagick-ASCII85Decode.pdf differ diff --git a/Resources/imagemagick-CCITTFaxDecode.pdf b/Resources/imagemagick-CCITTFaxDecode.pdf new file mode 100644 index 0000000000..e5cbe2043b Binary files /dev/null and b/Resources/imagemagick-CCITTFaxDecode.pdf differ diff --git a/Resources/imagemagick-images.pdf b/Resources/imagemagick-images.pdf new file mode 100644 index 0000000000..a5b13392a2 Binary files /dev/null and b/Resources/imagemagick-images.pdf differ diff --git a/Resources/imagemagick-lzw.pdf b/Resources/imagemagick-lzw.pdf new file mode 100644 index 0000000000..b57e07f25e Binary files /dev/null and b/Resources/imagemagick-lzw.pdf differ diff --git a/Resources/issue-297.pdf b/Resources/issue-297.pdf new file mode 100644 index 0000000000..ad5d2372f0 Binary files /dev/null and b/Resources/issue-297.pdf differ diff --git a/Resources/jpeg.pdf b/Resources/jpeg.pdf new file mode 100644 index 0000000000..07a7fbbcb0 Binary files /dev/null and b/Resources/jpeg.pdf differ diff --git a/Resources/jpeg.txt b/Resources/jpeg.txt new file mode 100644 index 0000000000..6d95b07114 --- /dev/null +++ b/Resources/jpeg.txt @@ -0,0 +1 @@ +ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc20011080258032003012200021101031101ffc4001d000100010501010100000000000000000000050203040607010809ffc4001c0101000203010101000000000000000000000304010205060708ffda000c03010002100310000001f950000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cab1ae2a6365f550e977ba07be8a1d12eefb9672fc7e978d1e79ad3d121b8926a8918ef236020c800000000000000000007b7f5d71d9f9fa4702cfc2df7a46db800000000000000000000000000000000002fef8b12d3131efaae14eef3d87af1fceb81db78f48c5f7cc6f4f064db8ff63cdea3cf60cf96f3b1e1cc3416d719c99355a64b03e736e81c990000000000000000af6e86bead3bb7e673793aec84fe651e6c24d64e4d7abe5491a3cdd3795fd33edab9f27bb472bf4bebe2c5de88000000000000000000000000000000d8afe91fb35d7d528decfc2f2675acce296a0cecdab62474b897cad5f2a0cecd7b5fccecc720b17a1ce458bf6a7c58f2f79be21a1b708be349a8f92f11f2ebc1ccdc0000000000013135bff2f8d19277ee70fcddbab325659e0aacbc1860cac98ec9df79cdbb4bedd2cdcc63ba7f27a1cdaade1c9435f89689f5670aef7a6d207a0f50000000000000000000000000000b956e1de8ac491f54a2f3df6c63cf2f55066cd19d174b68c8cccc3e5ef6fcabde4c9e5da6425c5ebd876ba5a4f65c24cfa18ae48741eb9c097e54c7ebbc9bb91476b3b9e0f377d295d1f21e806000000000cbc6b6fa4484cf9ff314dff2aa5cef7da33f5d3a06e393b1fa1f57c3746e95cc39dc9b1662e2ea50df379e419b1c5f44f32d52c4d676891d467b95c4ca8c9eb55aa70bd27e92e11ec3de420ebf740000000000000000000000017acedbd68f2336fd3f57a365ef9775ab271aaad9b96ed4b53da3b23bd6d1e6a6f9161bade87576b193294f4b4c08fcf839b164317368d4e6ae63ec3ea5c87b07ceadf2ef95fe91f9c7d9d78db52b67d8d7d320b7bd23e6376d8f2938000000a8bfd63036cf3fe5e8aaba79fcab757a8e25cb5e1d5a7f86e374fb3b073eb51d1c35506db65e4d32ba690fe64531c3eeeda6f528a1bb87b46a7479d662a564659be5db3d67937aef7416ae8000000000000000000000197bf68d39eeaaec78b8befad82ea8f7a1a54f3130af7ae57ef979feb9e53ccad71a4af371f63edc76b5494d7f47b47be67018cf976d3674ae8df3ecc7563d9e323b2ed624ac5cc78d15acecd85536d29553f21e80600000369d5bb4f3f97396b3e57cf794d66ade3579ed46e3fb83a47974c561997116ed4f67d79eef2012fb2e97b0e359684dde220adadf5ce45b061f4e729da22a7b3cf33706df278539f307d15cfaff004f960f59ee00000000000000000000000d96fcdc9fd739faee4ccd57b11989b659ab9d371b68898f346545c6e1278f8793d2d26a5a0aef1a489c7f69800cbcf7c05c2995c05bc4fdcc7bde8e0c9b567c8f2b77a8dda945ecdacfc8ef870650000243b8f27ebde6fc94bf60e49dbb48f1f8c76ff9ff00b7e8b49c3c2b9c5f3d3dbdec1d5fa7d8f99745fa1386691c47be5da5428f7a0c9cf63956565e0435fb359d8b65af57e6cbf2bad69a741cdd6a66ad1aad64d31c58fe5ff34d3e7bc1ea9cafd9fd002dde000000000000000000000eb92dad6c7f53a3730ba4e6d2db915dced9bb11e931bd679ed6da063f7394e4efc7bcd9a07bb1d6a6c51da8f0c6078cb268da77c45f6687fa83cfcdc6f81fdf7f2af3f7e3927ab4a7d1e9e7fb1be753491f7065e5c60e8bd3b9c7ce6de38f0b680000dc7abf1eeabe6bc849ecda4e250e6743e5f2917358d6e468ae7b1f4574ef94773ebf7a4b834fe9f5eafbba693d3e283bfe06efabf67d0fcf5a2eeba8f1f81f42efbccb69e7f2b48e4bdaf9e56abafed5a7ec38d6664e224638b6dd5fb046f4bafc5be7cfa5392edb73a1e9bd80000000000000000000006dfb972be9ff0047a7b46ebc9e7e9ed3f85660ad6bd6357c291f3f345f41e41b3e16b9cf66e6f2e35988db627d2c3abaaa707819bb6e95b0eee97f55fc37d578527d43f1eed5f3ed5da1b2f173bd3439b8bb1c7fa386d4b62e576e3b5a0740e79f3cb7647cd6e0000199daf8475fe3f0767b176be1f9ca31a4bcd34d5312720ed5c88a6aa2d5db7e57e6fbf931135631df6238ff009358cacc8798af53a4ecbcff0067a5cfb5096e361af6bdc5b3b6db75d8592ad5376dcb8f665bbd91a4ec7621aff38afd8f6ff460ce40000000000000000000742e7b2bd98fa45ca6bfa8d1b8f2b9176f635c851f76ac1e1c9b3c3625ee748c191a2deba2e2cc43cf81e63345cb5e54ce5dbb55d8c7b4d77a7c62ca64dbea4797663323a5a4de458bdd4d3179ded3ab7c7fa02ef94b169b149d7ec694deae457b4175dcd87a5c576ede71e3a73d5dabbc2f97557ac55af9fb11bb86f77fa5f3c4776be4b73a11a20ac07a78c5d908992db7d92a838c2e46dacd924f26eec954a5e6558ab48f33222bcaf5253c84c2d34e5560f6bf430ce4000000000000000000003a34f72ceabf51a2afcbdde8a8c8b556b990819c8fe249177ed5ca5b5dae9bbae60751e85a34d8c3f3df375bf2e4c57cc35eef51f1b92cc6557eb20f632431fa1a43e565e5d0de8c096caf3fe879a4eee177e3bef62b67c2eb7cff00458bb6cf697d1f19571f9484a5e9727222a5ea7a185b925996391834ec5090f46d7985469ccecbd67e2ceffdaf95e5f00ed7c1f1e4e1c50e61ee56566e4c24975bb12319bc95f95cde91e3cedebd4b9cabca99bd4556b18a54d51c546a7b8f25bdd1851e8bd580000000000000000000000e87cf32ba91f61928597faa5258db65b97bf39a65ec74f487f36782e349897eddee7ef569bb8c7498e7abf66d63cef7c57e97833dba137cd73c1d9f9135cdab5efac57c5af3673cbfb48495c97cd7e83e53579c2edc75fbdb1cb435ca7e879dbbe6be4a77de0315ff0021acebcc4b6f7cba567e5fdb7b7fc67876b85f4a70fd57cabdebd5e05fa5e9756c4db75c9f97b5476b9b1c7f2188f73b0f3e4ecdec3b534fb061c564e76a6e65ed1a471b3db566c9368f66522a973bdf482b79e55eb1e548dce62f98e4e37a5f5e166e0000000000000000000000006d5d2f85746f7b57a76e3cd6627c4fc3c9c0e71bfeaf6e6bcccfa56f3aace5fd2175aea9aa7435e3d81bce8bda8a7b77d1f23d243f54e95c9257e31f43899eccfa13c97a7d02466b9d6fc8c0d2e8b153bf5dea7c8af57d4795e6ef0f57d3755d5731c9ea7731f31625bcfc99b930cd8b2b38d6762ae312cde0c759c36b908dcd8ef5d55e55ef69b6372d52d79cdba3a89ea5f02d3f0f758a9a3d7e46ff00a5fe91cf67b48faa6b1ac614f62e6394f9e1a4635dde592e578d63d07a80bbd1000000000000000000000000015523a6ed9c23a1fd169efd463dff005f06549e0e4f1e4c4c8b345bd76343e5f9e961393761e3d7b5a247569fe2763a0e6e853bf1ff00b5cf7b4d50def69c5c1ce92b5c1646d1cc75ae25d1ed79feeb6fe74e71d1f11d8b8b624d55f418f973187ad9ca8bd729da1ccc579893df016ae799d7629185ea91dbd1abebda8ef5b4dc6cab5174759df75d95adf2f93879bc3a5f2f87f2559db0a46e578d71ac67624f656f22d16ad61739b36e6f493bfea0269c00000000000000000000000000000365e89c59e8a1fa2b2be7fd9fd145d8a3f4fcce9c7b3d5af61f435bfa0664351daeef105b0fc8fe9f5554fbc1f4397133146f535a92be8ba397abf45e77a4d6f13dc2b3c4c8cbc49292a48cfc646e24c981a81e1917738b4eb19f3f2b89fb251715ddfbea8e1bf4fdef2da945eff19678bf24e0ccc379afb7798b9789a72e7fdb1721fcd97acd56e1ad72e47e4e9a576a9d427b3b9e8da645767bd7ec1d6ee8672000000000000000000000000000000000000001d1e6a226783f57f445d0b8a19d5ae6c91bbd58bf306f494f13cb5724a7952713218df363bda7597df2437d92af326e3a76b23ba70afb467e5e5e3eef8bd1f1dc27e6dfb2fe34a3ea37efb7bf3a3adc57bec0e7dcbb9ab6b1e3ce57bf5357873ebbee99daf8b744f79d31e7b728a824b3dfb059b619c8000000000000000000000000000000000000000007469ed4f6ce17d51ed1ec5d0abd7b8dd6ee666d06a517f46f15ea783d42abd62bf66fe462646b2e4deb3d271276a9ddbfdebfcef9ff00cd9f617ce9075b8d7d63f2748d2f4dfa11e7ce90f7bcac9fce3936a87ab4ec16743d3da7cf7ca7e94053550c6bfccfb0f20ebfcee91d1f1a00000000000000000000000000000000000000000000006c9d0792f5ae57bfa55297a4c8b59746f5f1ef5a6963a041e548f7be4dc37065b0287adb39f1f271ddbbd63956e7a4ff006e55f3b74aeaf83dd7e27dcf8a55ee9e2a77803c0f7ca32de987934bd2d6f3dd65f28aace71739375be7577cceae3b5f3200000000000000000000000000000000000000000000002aebdc7fa7d3f4930ae9e47d0aed3458b3c5c8f7371abf5eecf6b5aa5bf3f459abd92945ca60674777dd835ec7c49b8e1ebd676af72edbaf5b1ef8f7593c7a3c78614d546712f3da5ef115e558f7f4b2b17ad696ebd2f75d627e4f3a1ddf9500000000000000000000000000000000000000000000000dd34b938ee75db176df07eb1e5bbb4e63df698f96ec7cdf43d7b6cd7b97efa13df2ab5e7f12b918bd2cdec7cbaa3b3837b2aac2cfb79a4d6aaad8cd36efda6b47977dcb1ed65e1ef1d33faf48e77d96ff004d9e9b9dc568db354e7fadb509390d678fcc476fe5e000000000000000000000000000000000000000000000001d733b51dbf83f587af62bd21b66893b7fc953ab6c507075b56b945e9f8fb5697baeb1acd45db9454ec514fb7718b74e5dac6d8deafed1def24ac2dc65a96e973f178d6276be3d3d68deefc2feb49a86fd6f6fb577ca726f987ebff8f391f42c883dab49afd9d4c77fe4800000000000000000000000000000000000000000000000137d4b89760e67b7929bb1b6c76f9dfb231f57bfe535dbc49aa5db155ef253b67132239efe1db57ede3dec762be559a33d88eb9d035d9a847d8c445d1fa47a24bcbf77e65a3718fa578beb6be67fa0b8ee6f0fea5f7579f1f43dff0025b9f35a25f97eeed721ddf9cf4bc5d23a9e0c000000000000000000000000000000000000000000000001bbe91950f43b96d7a7ec1cdf6d7a0368d5731d319916a8fa981c09b8ae8f8ebd971d9da661733072a2bb53c476333ea3e35f5774bc8e9d19d1636f79df8d20fac6a1c2fab7d51d0fe27eb577cbf79f983dd360eb6af81b2c573bd8dec1ae8ce9277e9d0a4ad1513ef9e83e3a1bc000000000000000000000000000000000000000000000000006f7d1780f62e47d0f6c8fb7e53f455d5896a3b98713b05ab5c3d7a9dce2e7e569f564c3631354dfcd8ba5b6fda7f01f71b9c2fa2e334fe47bf3f5b8c381f5cc4a323037af2be5aab4b34e267626d144466369fd1f1b37add0eaf8209290000000000000000000000000000000000000000000000000003aaf2ae97cc9b74f362c3f3ff4a8faae48dcf431945fb6cd7911d5cdcdd5346de345b7c09b93d5645bccca51374bd555579e56e955e53eb2c0ceb5b53a6fe7741e6f97d73649fb7caf23f206b3be687f45e105cd400000000000000000000000000000000000000000000000000006ff00a06ebcfdbaa6d5cd76cf136a623e62333dec5c3b15f5bd7d56aab96ee691a96d5aff0043ca6064534e61ca9bd733e29363af176fa18d7765de6e712088dd79d6c973ce336760f97891b98b97c197e63e71d3b98fd5a8874b400000000000000000000000000000000000000000000000000001b66a7b055cf4f979792f0f661ed4be1c3985b37a1ec6332d62513ef9317969e5c2bb7d98ee487bb54f06b8dda2b7c456cfcf25b8d24b4eeb937067abe9b2b816f58f90c0a3cecdc079a6f9a1fd3a9074350000000000000000000000000000000000000000000000000000176d30fadf64e7fd37e796f578dcfb442d9dd6bf4f0738d77adf3ea9b412f51476b7e55e65b0740e47b05dd7a36bf15812e35fc79fbfc8923b6a8c92a5b6c7b668bbe4b8c7d5f73d360dbe65d6367d63e81502ce000000000000000000000000000000000000000000000000000000379fa7fe27fa5bcc4db1dccbf7c9cf231f6a2af6b8b0d97667c62db97918d019fb6667a9874bc7df203832c4d797e7224c6b977d628a2e51b2a9dd6aee5b2c4e1c5ecf9cb18fa4530c8000000000000000000000000000000000000000000000000000001b2eb48df64d3c7bb17cd6e62634a62d2da2e2e6706de2c4879976f590cad7d3624e328b55f376e63e74b8f32b3f2fd0c50d19b2eb3c692d635fa39fbdbe43b9f12f470da1e9e1000000000000000000000000000000000000000000000000000000000c9fa8be54d839127d558f1f21f3cb545fa657a9a6164e5627a3861e3b3b0fcbcf6bdb88f34d7e7994bdc845cd72e36eda62ceb507cafafa5dc03d4c01b0000000000000000000000000000000000000000000000000000000000093eb9c41cfdbec3c8f91f7af2d3fd0185ccf2a8edb9dbd4b0776f7e685adcd8ec1e70d81bbafd1daaf04a2fe9d479f47bada05cd4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ffc40032100001040201030303030402020300000002000103040511120610131420212231600730501523324016332434357090ffda0008010100010502ff00ee4f4d2ec68ce69b1765d7f47b5a2c6d9067ad2b310b8fe36358c9474db941000bad2015f66374e8c06469b1d1934b4658bfde66db8d790d0519e45fd12e6ca859017171fe52be3ca5186a08a870f24a9f0251b156f1268d3b6979348a5da77edada36d2936a58f922171ff6a184a7283162a3a62286a6d47534a18f8b0ab38e83230dae8aad2bdcc55aa127f1f0c2539d6a410b047ccb0984f3bc1838abc798920846d4cc47e564669dddd37716532e3b5342a40443c5ff00d611737ab86d20a4c2c359990c08604d132105ad287685d70172ca74641236471b3e2ec7f1956af99e3008d0b285f81e2f30359aef546c6f654a7792c7c8d9f9697685f7df926747f2b49c7934d029634eda7ff528634efbd4a215a318d78d30f6674ce85d548bcae18afa26aef1391697954f0437a2cf74bc98d7fe2aae3db8ebb33a725e42446ee8cf48cd73d21990d84361348ce874b69fb9372696153c1f1fe9e3b024682261661ed142f2a6c6bea5afe35b4c4849519bc7362a509a3cbd06656be8367f98d11f16ea1c37a59bf8708ca42ab486bfbb86d18e9a5745df92127dc65a5ea1467cbb0839bb63a6d4911469db68e356e1f117fa10c2760e9e12085985337611e4588c5f35363580325598548da223e282cfc84cb1398784ace5c658af4ec5246ec85d9949f28e0e4f9fc03e3ff8686379a4af5c6b87766dae09856f4d348a474ebeeb83a18dc9c61e2ce5da1241f52c0613ce87a6c7c7d4183f4e89b8bbb6d4b173121702fdead564b5250a034c75db7da9ff00df8311e378599b304ccf6256679a75e57dc563e06d38a6ca9e8af72515bda80f9260da31d2b910d9832f8d2c65afe0e288a63ab59abc7dd932725c9712913616795ade2ce04103bbc5513d443598558f8ee0fa7c67d763a56a3387859756d6660bc3ab09d964a0d3feec15cecc98fa234e161f681f03c565fc637338c4194c97379ec6dddf7da174edf1b75cdd4523f2a24eb969a4916f6b27870ca559622825fe0b1f51e216044ddd9d725b546815b930fd26dc2ce32bd38ba86787c916b62c8e46152d9f890f93f7c7cde39ba43241c1e6061eafca03a99fcb23448e3d29e3630901e23fdb66727c2637d242c3ef1b0f129b266ed3d9737fbf7ae09e1db144e2fc5461fdcc5d2f235aa0e0130b838a89759e18f97f035a3f2cc2ec9df489fda72715d39918e1793ac638a0cc7561d9534a531d689dd48fe209a6722dfb40b8be1b365511f596e2bb962b842e994aa459187f73a7e9958b7c112d3a76d76224c48e6d296c239797b6b3a89b93495b6d2c5c5c7e1fa76d8a92b858af94a9e32dfcc66a4e13c36ab9d4b1fc0566d20b1a4d3ed7919725bed24ba5211128865140331227667841423c46f4ff00b109a17db28dd6d484894c1ce376717fdac355f494c476a0c73ca8b17c06d47e3452239114c8e42744efee8a4e2aad8faa21e63720d22fa5f1b6bc478ccc7f6b2b65a646df2c5a4d22eaea62507f015e2d42d5ddd0d7d26817a6f8281d91ed937d4a38c53943134d6f9a050a924e31cc7ccfde2fa78a45b4cfa5e44e7bececafc7c65fd9a15ded5b8db4d483c92e2e8338deacc23986e0f2cfa22976a8d37b45174ffd190c67894a1e33efaed58b5362a2f20652ab82b1f495693e6b592063b447d9d93329231963bb54a95aff007ebd5fa1abe9b5a4c81db460cea5aec8ab2900c53b3b3a075112b127d3ef18c8d156905a22d3c7f2daf612bf1f38ff0067a74395f1658f2e1362ac0f1ca58060ce5a67233d9c40e65d378a1d4948423cdc0dac80ea4400f2163f02f20d9c1f8c6e55f13c1ff6f4f45c87318f630ca527028cb815693b332715c7b75653f257ff007e87cd5d6dbd01c8a5ac712675a72442fa315c76a6aa8e3e2e2e80d4a5bf7042e4a0c73cc784e95f20643a4b50e671ef4a48255cbe392dae4beea683c913b69ff63a69ff00f20097978153cbf05772de41bf2bc84c1f3037197a72c070b328bc5d4168595d979cbb5d3b4bd44d8cc58857c9556e39a8599e01dcfd35f036b44198aac4ad41c0ea48a22d8b3aa80d213633616e9bc0f2c632c76ea9d2b1fef626467a5117d58e6026bb8f131b757c278da63235dc43305baaf13d3c6f9dece09e30b94dc1fc2fc8e3e0cefed887915781b581a8324d88a22104d558a3eb3aa20f19f17693e1cd79503ed4428b5c676d4ffb1d3e5aba048d73767e5b69c130e9cdbe3159a7aea4ea6dc593caf9dddf6eba4a5112a320955cbc82019db0ce548794f8697c4076f935d3636bb5f6b878e4ad276a5378e5c648134797c7b6ad45e33eadada2ff007b0528b0b16953b9c541739b5f8d9d52b5e278ed34c393acceb1d2b42432c73c797c7327afc4a4876a68b5edadf787fc7a7a768ec6225692ad995a283ae320252289f6bc7b670f984137c313ab3ff7fec54b0f56787e5b8edbc49a353448e3d29117df93fb31b71e9cf47a9f88653a81e456ac3ce741b4f4e7d3797e89e7f994f929436a1f840fb658ac9bc4f6724d2c5902e679aa9eaf1dfef5395a0b480f8bd7b7a53dbe4c32e8eb5ad2b365881e6e12d4b8a736962ba1a974ad027f87ef19f1286c28af7864c4759fa71ccf5bbcb0e46f15d99561db8c5f49836e36ec4a6ffbbf67116dadd7675c931226db4c2a545f75aedaecd213273724cabbf154a4db9d86109ad33395c647676867f98664d26d312f567a33e6e42c633c7e19bfdec4daf515937c2dbbf61371453bbb48fb786c383faffa253f2169580d8ccda2f60cae2bcceeb6fdf5b507d29ec699a4e483ecad1f8e2fdae9fb651cccfd996d483b5252335354305f6f78a89405c14f73e2c5b7777989d47cc9431ba16d262764d2a6993cc9a6f99cda49bfdec75cf4761b44dec61e4a4896b49bb98edadc7af737708b920abf120f14ea141f6e5a595b1f4768e2399e1c0da9545d36ee9fa7e38c63c542ca0c3d4e058fa8bd152276c2541766d7b28d47b0f1e17fb795c5f06bb0f8cfddcfe63953d95358daff00278abed435f49875ecf94e6ead484357f80c358f354f647f738d88260d3b26eda572246dc4bd8cc981d4706d450696b4d28ed3c4a1816b8b4b2715204d7a68ba6a4750e0aa44abd6398a8742deb4c1d055e11b9d394ab85f8c02587e40abf312ac60f17367dadadaf2332e9d08e662aec30e6819872dfe7ec68ddd7851c7a4c4ecb9ba6672786b28a1e3d9930a76efc57504de2adfc050b4f56cf6e1d9be1049f137cad264ddac86c2c8e8bbe3a83dc931fd23ca2c974d7a7450784b69dd12664df09a0925418d615e95c1d88d90c8ceba4f3b8ec5c32f5ee3c1af75e94aaee5ad5e23f91ab61a27f3724efb50d1b1617f40bfab54ec545c96d7f57b18fb9d3bd7f16522ce5e676c81f293bc21b431368b42a676ec11f25057411f1eecb9277f665720f7ac7f0384ba56636fbc6db628dd7174df089714ec993276e4d7e0d3f7e8dc5b12ad41a38f398f12ab9987c53ebb34446a3a0e82b847dca42120998970174f13271b0c724a51304e4616724bd713ae98c54f9ebf8be89c763d18c75c32d9da75472f997c89228f9058ac50133b8bd5cc1da8ed06fbbba8a5d2f51f073a2772411727af53682ae98878fec66f20f521fe0ab593a92d6986c451baa90b48a7c6b70b159e378e0f2a2c5bb0cb1bc6fc769a1364ceaf47cc661e26cdb50c3f3d16e2cc3fe39e95a3a79a91a4b1c5451ec84599bd840c4b11d256f30f8fe80a9585f0b46a0e627a9542dcc3666b9776cefb5b543256719347d7d97269f3d72e212e488f487992fa99492f319a178d7d9e097d546703b398bb27edb7401b410ede9639cd4189766929346369b8fbee6421a236272b337f0784c935636552c7050db631bb0312accd1491bc724791a2ceaad7d491d0138ef63b839c1b6c856e2f047b401c56072af4e4abd5a2d0e77a88ed8c9ca53c6f4e5dc9bd3fd3b3012e90a900e62856a43cd3cc85fe1e4658cfd403a3566fd449cdaef555fb8af5eda966724ecb8ae3da382499e0c71448ad41033e41c9eb9b908ba76dae02436eb3d692bcbe097834832c08eba285042863d2807eac6388b3da161bb736a53e6fedbb763a315dba77a6fe170b9569a302d2827d22b3c82493ebaf61d9148d24723f03ad6dd98f538598389656ba093c72472726555a475f24b1875e3bb0f55e1e18ee75d5216c87574f694f24b39f89c9344ccde35e164154dd5813ac335c39138ed3b218c8d35395d478b98d34352b23be4ec44e4eff0029bef4fe636f8ef34632059aaf0ac658e61c392920470ae1aed0a8a728d15e91d14845eec9e5c6934f39d997f8612717c4e65adb09e934ce9df6a3274337d3216de23504da52fd6b2306e3b70f19427f1a1b5f152519615af66dbb08b99627a0649c6b74de3a80daf1c23d45d4d540423daf1ed45519d340310cb948c14d6649fda4a83fd04c87b3a76626707a36a16e4c61f1282f1af128e3d211440b5d9fb492842393cf736fe259dc5f159869d850fdc5971461a4c84f49a459093fb57cb7228a3d450b4d0bc5912db1b3f692761452917688f83c79bf45603f562f30ddfd4ccb586c866efe4de2075146eea1aaad64e3aca6b125a266d7b7ec9fe5b1a4b19d3a5607fa05761ca609eb022653c3ea21c4cfceb11271da78d30ae299912d7ce93ab97e2a4190cacb7dff008cc6e6a4a45572b56cb09a134edb626d3b3a7361590b7f13b737a95fcb60231064c854d5e40379a507a7e4b44740c95981e296210679a667919ddd032841440c037324f32e2def343f6e8cc77f51cb853d315453d3621bd07a6b9da0fec5deeff099fb12d277601c8f51844734f2583fe3eb64ed54683ab660287acab701ea2a33306629984d97aeadde12457878e2b5246dde3f953ff918b18e2ee044f213461349e69663d3321513287e15bb6f3bfb6288e7928744ec2c745d770cae366c65805fa5602791681140a581662469b29dac37d31c8d207625cbe45d58b11d68ef752c211dec94f903fe6713ff00c7b771271445c9d6463e32bdc9658d9485c8d90a894f37d3eee81c1b141e911d45d63896b1870fbf46e71b039d8e4194175767a3c4d2fbf776db4f7ace36e37545943d58dcbfe531bc45d486e32e6ee4af24f24cff00cde2beac7fbaf8728173d0264280b4b7da9509f212c7d13270c8f4cd9a21dba36b8ffc73d3a3aebaa8460c10fdd74ef56dfc555b3fa85909a39ec496a5f675043f3f80e14f950f718f36b18bb7523fba76d3b265cbb43114f361b011e2e9bd352d45d518cfe9b945fa61990b7874edb5fa9d9f8f88b69d634fe3dd998bc957f01e9f27f0a74deca9ff00b4d7ddc3378e1a16e41f84c87e7b742e3bd4e742b7c15752d75fa920216963f2363156e8feadb3459afd54b36a2924298fb513e161fdd683c9010f12fc03046e3713a6431f26d7688b84ab3157d4d24edc5d90b6900f33e9eb638cca36b4a506e3d63976cc66fde25c0d9f90fb75b6c845e2b5f805493c567b328dfe256f9ed426f2c2edb6983c734cc9beea07d4cceba77af7d046fd758568fabff50e5c98fec12a07e5acefddfecddb3b1f19ff00006771780fcf076074526dfb4133c06397aaf1ca7e4949b6cdf7ed0db63053d8189b7ccff61d6325e1297c38bfb197508fe05d3d2b4b41db4fd9bef0539acb1038129c78cfda56d1f6dfcf98d3bed0b6bd9af6ba02e06cfe7899f5dc937cace8ee0fc07a66df09e66fabb6962ec157af9610ba0af8ea5ed61be03fc53ad2105a5a5a5a5a45f0b4b4b48bb50b5e34c43221f9644c85d66bff005ff01c6cfe9ef99727ef8e3dc4acc5e29b223f43762a8670c3f23a5c1715a5af69326ee6b6a8c2766ce33a523aa078789c72f8af4088d10e9653fc7f02a537a8a9de94de2956487ebbadb85937df5f0c1e3b0c2b4b4b4b8ad7666da08d144b8ebb13fc71dbafd38c434ccd511d559fa0d2e3064215ea496489f5f8174f58e517b2aded35db0d31596fed7d9f4a07f241783c77b827f85b4cdd8bb46a36f89155a92df9eaf46c42177a361902ed49684c47b5fa5e23274ff859140ba9e41a387fbaaf578b66a6e47f81622d7a6b8a28de53b5859eac5de61fec17dc3e5a94bc0b3317f685988246ecceb9277ec05a432e94b22e8fc1b56c6fa352545d7d8d61ab5ebf98fa2f350606c09318c920c41d7dd49fd5ac54afb56e4f0c17a4e72fe078eb3eaaad5061986c18b5d87c737690bff1a66d482fda79de4a94e6dd632edb5b54e84f90903a365e390c0d9c78ed37d4f8cacde8bd3a92bafd460d626031ac3ea6395aa750dfa11647a8b257da19ce526d8acc5cf822727fc0fa6a76e71fd062fb6b31b1878c5e49dda2197e20b208546eb4a12f14bbef56b1dcb18ac1478eaaf4d58a2c639ba1fd3722cdc9743e6432b85521080754661b2d7ed3b7976abc9138d91613a11686e596af1deb0e67f8256b075278a509e3a92ed8db60e3f320ba9bfc4fe51c7a76519ed5bfeddaeffa7f59ac7500d6f82aca5aebace93499b1a2cca85a9b0b660fd47b3e3cbf545ccbb13a96b01a9aaf06ac02724d1b0ca538d4832175d7dff06e99bac2e25c5fd5bb8691eb50c0d2c76ab3c49d6bb5d7d9573e43dba0ee352ea4d2d2ba71d583256defde53288b4fd89b69ddd94903a79783dbc8edc8dcdff0612702c4dc7bd476bcaca4179998258822825b0bd36a46a719358a5e25918b8c719f028a092c26a91409ecbae9afd47f0407d798a68f3fd533e6dfb4adb1fb267e4da5a56240886f65b829ed14df85748f12ab3c442410b0ad271765c886311110066528f10ce46c35954bc7182fba82142da6f618f1785d0bf90ea74cd9b29b014a8b752d46a39cfc2ba5dfc708719466a8f1a1fbb331338f07d693277775d40fff008e9be157b3c15701918078adadf6d27760668e6b6ab619f950863acde6465c975bc0307517e158191fd2d5b1b509b1b4b45897078de664ecb5db2e5e590a3d2fb273676199c24feaf2090e6a25fd661516424b44183c9ca38cc4d2aee528945e1faa3f843f3dbafe330ea1fc2b004ef1452717ab610cff001624da39c99c66675cc59d859d5eac5099b2765a4c2eea3a33ccabf4fc922a982a30aa9622ac366c3cac53bc458c95e677abf12b78e48c932fd438bc79efc2ba77eab035ddd431383348ec8cb6893ba775b66464c48a9c2efe8a04d5e1140fc109484dc645e530786cede23621b41b2c37c1beb8dff828cd01afd40b052e7bf0ac09f0cac5590d5455f4a4894df09cdd727f755afcd4543e0293ab94f8b485e292bddd27979bd13e07eab6d607c89e3e09a4d2eb6363ea1fc2a290a1930cc56e9bc1c54eda721da38369a93129b1ccc881c0bd98e974abccdc7d530abd719dac97291b6a090956251caea08f9c7660d0bfc3f536bfaefe17fa6b91f5b8fb65a7b0fb789b920aece9ab833db8435763fab82e2b4b48798bc16e415eadc9a4d9af4c9ab21afa51b69464a94ade29cd9c64ff003ea532933bf85f45e53fa4f50dc2faa46e4a11d381b32b122b16113791de05e976869a0a6ce9e832f49c5dabaf0b2e0c98596bb0baaf678095ae4a59c628ec98c963f0ce94ccbe631bc57f8a23533393490edd878a16da8419470b218019e5005337cfb1fb7d9f9ae6b2d686ae1ff0dc0664b077a29c2c444e9f69d48c8fe1c1d467c5e29d93caca495116d6d33261401b470a20d390a74c4bab725ea2cfe1dd0d971765a462a404516d4702f0271e2b6b7de2f9402ca360653b8a98be7922f959eca7f4c82490a693f0ead664a93e1733066aa224c1c97a567415348a0d35814fdb4b484b8bc761914cca59368fe57d96673418c82f5b2bd6bf10c366a7c2d8c56561cb552fb87dc3eceda52c8fa98f7fb1a52ca104791eb18e3566cc9727fc4b1d919f176b0fd6b14e0c98dd93caea437745edda2904066eaac744577ad5c8e5c85a9c3f16ad92b749e1ebcb8317fcfea78e2eb5c74aff00f2dc6139f5951647d67480a5eb89dded7575d9cace6af5b444e65ffed8ff00ffc4003d110001030204030604040404070000000001000203041105122131101341061420225051324061b1233071a1334291f0151624345262728081c1e1ffda0008010301013f01f5e240dd3a6017783ecb9ce5cf284c3aa041dbf3c9037466886ee0839aed8fccec9d2f46a0c2ed4a7b40e21aac9aeb26baff00992cec84799495ae7fc3a27484f54fd538b987337754d8c4f0e927982a6af82aff008675f6ebf2ce706ee9cf2fe19ca1aa2c56e17e0d726bb37e55557867922dd3dee71b94ccce1e545ae0754ed06aa9e364c2c54f46637a7b397a858662666fc19f7e87dfe51efc889277e16e0df0109b1dd38654c7e53f904868b95575ce90e58f409a0754eb1365491343157583bcab217ee9a0c2fb853cc6409f7ea83b5d161958e999cb94f9be49eeca11b9d780574d8fdd1d3c0533652ebc2275c5bc75b5265772dbb04e6d95d0dd32b0b5b9423779b9e0e1c1f173765c8e53bccb98e87cccdd5254b6aa1120fecfc8c86e51e2d1645eaf7f0dc857e0d394f8b10a8e445a6e546e321b052d3c8d172a360ea8b3314d65b89e0d7f29d7553cb959991d45961137226319d9df2278857e03c247823376f871792f2867b2c35adcd72aa4b72d964b2a7a7045caa988336e0d81c45d3865d13911982b906c9c2c55faaa3a8ef3087f5f90eab22b5f4562d56ba22de160ba70447187dbc389479a72999a1d90a973ceab3282701aa793985462ee4db06aa8233294d828e5f7528ea131de61752d2874799ab0a98c53728f5f909058dd31fee89b146ce09853878586c9ce478c1bf8714668d90279b85184ddd05757b2ef0eb5913752eba042072e5bc0d51690a2aa2c6e529b296c824081bea3f3dedcc38dd0d15efe3278c42c1005c6c1458357cdab623ff9d3ee99d9dac3f1587f7f44dec9d465bba468feaaa7b23513c45b148d72acc0313a13f8d09fbfd96ad3aaa681d31ba92131f832a1105a354925f6595b6d53e31d1362cc6c80b69f21236c7f24b4aba2500a8b01acadd436cdf72a93b39490ff00b8bbbf61fb290330f80ba8e1b9f60a6abc6ea0d83720586d15546d32554a5d7e89c79b1e5b26e1c2239f36509b5b4d7e5b6504feab10c330fc45b967841faf5feaaa7051417301bb3f75552b5da0e207073c353e42e4d013b85045cc96fd07c8bdb9820b22d50f034ea8eca3a692a1d9626dcac3fb272cfe6a97651f4dd51e0b4545fc366bee5594d27756991eeb354fda5a36e8c6977ecb0dc49d5cd27965a3dcaacc40b7f0e32856cd948075526175b54733e5cca8b09a6a1b4d2eae51b9b2ebd154d364d59b2c7f09eecfef707c2771ec86a805b2be6d02ee65c2e53e20c57574d6b9e6cd54b00a78f2f5f92959fcc131fd0a75815a10989e2cb75b1583f679f58c135568de81565f0b688a8a0bfd931b8f55ff00c83fa7ff00552b6be38f94e7171f729b054b0e6bdcaa8c3fbe0b556c9b4386d2eac6027faaa8aa7c9a0d022dbae5a60c86e139eecdaa9dee89cdcba28a5cec0557411bb342762ab699f4552e81dd3ec84964e7dd326c86e9f5ae76c9cf2ee11c6e95d958a9691b4fa9d5df292476d5bc05c2bd917dc20b0ca2ef73f9b60a0aa6e401da26398fd5a6e9f335ba26cfaecb16acabd194247d4a761f573bb35449fbdd51534548db052381fd10713b713aaac9a18e95b2c8561f8dd349680dc1faaaf61b87aed7511e4c75adfd0a0f2b3942ee4c1ee9c47454f48f9cdfa2861640dcacf967461cb94e08b0ab1401581537269f39ddca56e7610a9ae0a0f6b5c03ceea7823eaaa4b5a796c1c77e1b276274cd765ba63c3ce66ecb1595cf9f21d9bc221cc81bccf60b16a06d661f2530f6d10664d1c9fbad428a29a6f802a6c3037cd36bf44001a0f9ca11fe9e3ffa47db8582ab66788aa5abfe590a2ecc7314384d550c1a48e514cc98668cdd62b2164161d7861249611f555585bab9d9a23e6541d9e90481f55b0e9c718a08a3ac782dfaa387537fc3fb94ca4819b35016dbe7b0d7f328e33f4fb68afadb84cec91b9e7a2a6ab131ca7429a784afe5c6e7fb2738bce676eb0a796d4651d555d3f798b22186d49765b2a7a76d3c618140ec9283c4aed245695927bfa0e012e6a731fb1fbab6a992de42d29ed0f6969ea9ec7d24d94eed4d2988b43da5a7aa968a689f96d7586d1987f164dfc1b2130e5730a6b83c5c271b15da1873d267f6f41c0e731cc59efc26bc7502dd507b5c6c0ac5a8a39b2c8775b2893cd82ba60b0f0151b8be9dccf6540fbddaaa9d923cc156bbbcd049efe83472f26763d3764f682a49a4a2ae73c6f753913d3676febc213aa73332115b756564ed9593f40a9e611bb31d9371ca6a798e4692107c75f4dcc84dc14d6bb952b0fa150cfde2999270c670d74c79f08b9eaa8637b685ac937b2b26795e15acad745a80b94e8c06aa9ac8697479d5475b154fc0abdc5b01038766b3f25fed758b3b914ce73bafa1767aa3307539fd47fed62788cb433b58068a195b3b048cd8af759ace21145e1cc050364f94345dc5475d4f23b2b5daa74848b29a432c85ee5401c6a58d66e50c3217d398a5dcaff2c4c1fac832aa1a58a9630c8c681769ab448e10b7d0a82abb9d4b26e837fd17686939f0b6a63d6df65d9c9dae89d13fa153e80953b2cecc146ee8544ed32f0c62625e22e9c282964a8a7648f3b858860733273c8d41584609242ee6c9bfd93e27353b56aafaf65041941d54f33a790bdde87805789a3ee526e36fd3dbfbe8a1a4a5a73e46d9171276d154d337e31a0429f3fc250cd0c96720d73f61a2c530e7cc44b0ea7aaa1c0aaaaa41cc6e56f5253182368637609fe57dd66f652683313655f8d4507962d4aa9ab96a9d779f4485ee8de1ed3a8586e2acaaf24da3fee9ceb3ac9cd0f6dca10c6d398055a7f1953565bf0e554f0dfccb3376babaaa9a285b9a57594fda3631b9606dcfd554d7d4d59bcae4edfd15bba0553e2f34360ff305498bd2cc329758fd5492306b752de4797159509e4885b3582ff14e56a0dd4d8e55cba35d65248f90e679ba69e0ff0088fa28dfc0d95ecf84aef539fe646690eee45c8395f80d1029df11f461a8e04a1c48ba0d564139351dfd1a33d15916ab5b85d375f0ede8fb269b8f0655b2bacc5037e0f3d3d218ecbc1c802871caac8bfdbd2838b50902ccd5982ce17302e622e27fec43ffc400391100010302040403050801040300000000010002030411051221311013224132505106142040d12330334261718191a1157280b1c1e1f0ffda0008010201013f01f3e869e5a976589b7549ecc54d47e23837fca6fb214c3c729ff0a4f65e85ba073bfb1f447d93a722e2423fa553ecc4ecb981e1dfe0fd14d04b4eec92b6c7efc90374658c6ee41c0edf32c63a5706305c9587fb3ad60e6571fe3eaaab13828dbcb845bf6586574b2baee1a22f73b64d1adca9aac30d9365e66cab6844a3a85d57503a94e61e1fbc9256c43a93ea9cef0e89cfba72712d399aa0c4e68b47f5050564351e03afa7cb5150cd5f272e11f4587e1d0e1cce91777aa7e791d65fe990976672a89194ccb3028310be8e4da86386e9ec648991868d16555944d91a4762abe89d4725bb1dbeeaa2b037a63dd39c49b94dcce1a221c0a768140c6ca2c54b4a58e4e6e4d42a0c40c9f652efebf2985e14fc45d73a306e7e8a9e9e2a5672e2161c1d2b1a8485cefd155c99cdb8465fd936578eea9a4ced5598e329a4c81525536ae3cc16298736b212d1bf644169b1f8c9005caa8ab2f3666c9a11d4d95346d0d55760ed165cdba6831bae14d21784fbf740aa0a932b7249bfc9619446ba70c3e1eea1e5c2d10c6341c250e3b2e5068ccf5598c323e889412ba717726b02b01c2090b6ed588b1e273758139d132ce59c15ed051f267e7b3677fdfc7553991d91bb22db2ba0995240b047a8dcf0770747ccd972b967559dd17531534e2a230f1f2383c6628330fcca08edd4e59c14f7860b955b399c646aa7c2b3393e99b4e2df0036374ca582a7a9c13e95ad1d0afd96214a2ae99d19fe3f7445b43f0d6cdca8f4ee984bce8a485e05d31a3ba2db941b6e27835dcb3753e491b74750b0d97932643b3be46189b1b1acf456d1465d9ac54f258d9ca386376a9b66e81543b349f0d34f95d64d3982e58ba2c58dd37bbd63bd1dafc3893ef206aa10dba9ed956550422d72a78f2f010b88ba22c8a3a857b1b222caea9a6e7c41ff0020db985920f40998b81272dea5a98e26734a33c758cbb10a910bad754f52254ffc43c5adbac567745d2d587d53f3d8aa5a8cc2c5660bb2f6963be497f8f86b9979936f16c84ee71d566514a0354afce983a90b00a6f1290e89922907709875d5494e0b333561d272e5e59eff0021815409a9794efcab12c3339e644a0a5927a6c8f54cc9a927c9d96270b9afe63561d339c8756bc5af016274c67ea6aa0a2746733d4370fd130e6d131a0357b58472a303d7e1af6ecf09c6e1461350e17b2e73ad644a935d1729cb238044151d4168ca50932bc3c21aebf7f8555fba5482e3d274283504f8987a88550c6c9a151c2d8bc2986dc0aba6ea106176ca9e980d4a6b407279b3345ed0d489aa4463f2ff00e56c8d444dee8d5c7d97bfb3b052d532461639aacaf650446437524593e0ca84616813df7595bdd3a31d9363b9b2dbe4305acf7aa6ca776e9f45b276c88bbac9e32bb834f070cba954f55149272d182c06450c391b72a790df455ded152d0332976677a0553884b3c864f542f2bad2393594ccfd54d230e8c6af09bae6e6d2cb96fdc8593d55316b058aa8901d07103839d64e7dd342770a48f3c97f4f91c36b4d0ce1ff0097ba97ae30f66c86231b4f2dc98d8c0e629df1c87a166ca8390d557c0f14f995397098109b89b29e10e99d655dedb4510cb4accc7d4ecab71baeaf3f68fd3d069c1a33e81368e43b9534222eea38afa95cb6dd09e3668059493be5e91b22084c7df74f6907304100b657be817bb122e53a3caafc034b8d8282110b2ddfe4b00c505bdca73fedfa7d162f8638fda46a879b35398de87369aa32bb6588b646012316172b2a4653ba686c32752e5c7531e5ecb18ada6c3e731526ae1b9f45cd7d63b3cef47dd63fd53f944dc2ccc2992f2fc08cb33f729ad01665991d559345c6a88b14dea6ea8ddaeb20fb22fba6cb90a7d5128b89e0c63a4395aa9e9843af7f94c231a1503ddaacf5763ebffbff00bfdf76c6d66c14f1c32c9d41490472c79145869a6973314e40d5cb10c664a2a7223d09d027b09374411ba0d256550363de55cf8dbe00a47ba43aa03e18dae2fb052d33c75288f6551ea1071598add307aa2428299d37eca389b10b37e5a8b1ca9a36e43d4dfd5331da3947582d3fdfff007f4a9f11a57e8240b9d09d43c7f6aa24a6683cc78fed63552d9ea4b587a5a81b14f56d344d714c17d4fc5c97a22ca00036fc0e8e3647ab74d6db74edd6ca38e497c0141401bd52ea80b6df39278cf18cd9c9ecf456b716b1ced91696eea0177709f74c9b943552d58b599c628e39580b82f7183d1369a16ecd5b7cf4c2d21e2d17364f8f2f168b9b202da29c74a8df90dd73989efce6e9c2e3e0a377491e4354db3afc0b74ba06c6e810f6a28ad8dd364690a6933683e1cbad96dc291d67dbc86a5b76df83756ab150485ba70721c0fc2747dd4a3ba60b9518c928f219066691c0141a248c04de97db8395ec8bd6657415d3754e6dc2f767bdba956313ece5f981f2295b91e470a79837a5ca5239970ae8ea38df804d89d26c84259ba60bbf8565b3050753ec3c8aadb6b3d430895a4a734b4d8f022e102ad63c036fb2313da2e4209a328b29080d24a33bb3e60bdf5b6db552bcbcdcaa38ac331f22959cc616aa47e57642ab1a736609a9a74b27b7ba70efc295ba66e123db1b880a2aa696f52a8a90e19420e057750c46575d35b945bc8eae2ca79813a47bf7e0c7f659adba2330d165b6ea0903742a5aa630686e513737286a380f4515397eae4c6066de49887e028ea03ba4a01036598a8fc28b7d13dfd958f00a7ad8e3db55356ba46900aa3767a68ddfa0f25ae17854d1f70a0ab923e976a10998e4dd7643416e0ec8357296aa067eaa4af75ed1b6c993195a1a555c39023a2c38de8e2fdbc96a85e17232665a26b2e9ad2dd8a0f78ee8bddeaa2a6e6ea54b439429204d3cb7053fdb37452c56365878cb4910fd0792eea78cc12b98531a1e5414cccaaa2311ecafc21a9e5296b83859194294872a376a41556066b854df80cfd87936314fa09c7f2b9a46ca1ae7b549506551bb33aca3a7616aac6f2ce8b31574d17562c7688b4cae0d40585bc9a463656963b62aae98d2ca632815182ed908decd6cbdf1e3445e653d4991b49b28e8e22d553072cf4a37eeb0ba624f39dfc79462345ef71f4f882735d1b8b5dbaa291ac3d4a49e073148466d15d0714caa7b519cbd5261cf91d9e61609a034651e5353430d58eb1afaa9704a867e19bff85ee558ddd850a3a93f90a661d52ffca9983cc7c44051e0d18fc475d43490c1e06ffc10ffc4003f1000010203040607060406030101000000010002031121101231510420223041611323324252607140508191b1f0143362a105247282c1e14353d17090ffda0008010100063f02ff00ec92e89f3fe954847e345d8fdc29f47f0bc112611a29986471aaa823d7cb784bd5554c31a0e72d7939a1c39a177608585f1fa7dbe8c284a19ae744d943bd3c9c1173b478a00a92585544b8d7de9789ba3eabab629c94e5bbae39aafb549a1031093c82d8601aa61c660764ee2df45d43dd0bd6b2c7e7c0704d6468575e4175d0e0e32ce9ef0bad409179f9a92145b520881bcafb449a0b8e410744abbc3c10a6e43a42f0a4d07e864c123187dabdf338fdd17431c00ec448ce633f76ccd1a382935a1b6042aa57d1afb8cc8dc60e2800d17a4017018ee6606a7451e1b62b3272e9747bf1f469578b99ebcb9fbaef4518f77571553ee36c4d228cff00af8a9012032d49ea8b0908ea44d220868807b83bbee89344cabd39bbd8a404d4e4b685b3181f61b90da5eee4a1b9c0ba28919cf8ea0010a2c11d5133afd3c107a0388f07b99ac1c5506d115de61ae28b04ea295841441a11bfbb0db3e7c02201267c380d56a6d851d4a2c759d0a209b1c8b2a611ab1e78fb92eb7152e271dc5029c9555770d0536c253ad1107a1df5c86dbce418daf1279eb029b558a35d496a0d588d0d636391b311cdc13e1bc49ec3748e7ee3bceed3b865b8025441ce6a3390443772d299557af0922d6944da5a7028b4e23780013278057dfdb898b72dc62b1dc84160ab69fe22c20b241b11b412e7cfefe1ee168226de3b917949b20886ba6a6ea9dd0aa95faa333aa220f8ef3a4ba0b19c5c38fb23415346d7c28826c78bae1c94482fed30cbdc25db9a192da79526e39d9352dfb8291a1ddb1bb533521c703c6cc161ad5dd8574a28dacd28025ed3709184befebee16f313dcd549b41ec97b3dd438633aa0820648d11d4eca3452d66a08db43aae63c4dae12214480ec5871cfdc0d12972d5c352bbcd913532ddc4f8b7753c86765536a8d51b00cd368b046d0d1895392c1609bea9b444847710f486b7f2e8e74f87b8209fd02ca2a8b6a2da6ec043654eea9cb5dcdcf7513d2c0562b146c61e69b5b0d6dbc7d10a2344534734d52475b0b1d0de26d709109f06276db97b7c39114a192084d12db02ecdb31455dd0f54289c3927d370ff005dc9c88dc4af497691acd4ec68e20ac51450435c584816418e063b2e33f97f9f6f88c96d4e73cfeffcdb22a7648a2a4a5208c957735cd0927927827069aee1febb96c413a70071dce3a80f02a57911791b029a3ac01b63325370179b49d47b7c379a006b6e3ad8eec38200bd4ba445c4ee1fea7741dc703bdc554eee62d2d70041a1054487e1716d7dbeee0e87b3f0dcca7ec8f3c409eedd07161ad06075a9bcc6cc563b888e680017120012fdbdbe67b0ea39022a0f1f6c10fc58db286c73ce4d134661b0e5e328f49165fd2109bdcf77c82ac2fdca6ce0368813a342a7e8087f2f0a9fa420e84d74278c1ed7198d69c91a7b2c620c8861a8f4f7086f187b38fb1e1b9d8639e277448511e92335b95d135561886739bcab9061de393420e89284d2bac885c51e1cca774756d996b8ba4195851f6410bbcf3970f70b1d7a4c9ed7a7b0f243657614b568b0a299a95b2aa2c947675b3ed2d9bce5d4419732bac7d3258c94899f3b7ab82f7fa053fc3b87aaeb61965a1fa3452c2d4347d28745a40c327232f6535ea9a7607b89cc88fbd11b9e56d372751b308001132aaa5b40b69506a5560a8bb3b2ab545d76415d01488a2106038018b89e01073d9f888be288b65ad6019511bd19ae3936aa4d1761d844e45570cd4c508421463378c0e7ec9d1b2f08913070e1ee31121991faa6c46998395b416cd48aa2ecd93b426208cf75386db90bc6ec10fc43dd18aeaf47604ebc193c93881761ab8cc2de97468ae84fcdaa474d7aebb4988ffee539cd52aa73b2eb997829c8ddb2bda1b8c161b99c4757c0314e8aeed3b2f7218515c7a377649346db236554b50d110752486d221a68a725d54237732a71e306f20aae738ad832765666ab64380ed15af0c129b5757a3359ea519c5e8db93519ba67352141abb0d2e538d103464a4d9b951a00449b4b5c26d2a58b78141ca638eb0437379e6bdd6e68bdd41c1b3a0f737451e275a302eef7fbd7a6a4d116ccba8aaa1bb4913820d64a50e2060ca4baa0f888884d10c22e79bc79aa95458d9b309c7d029c486f1fdaa43645b46cd761545c1995b47a677252860436f2532666d3a92709a9e2d5d19c46a61ab53ac58cda8d97018628c4886f3ce27dce083223884d851691fd3b5bcc6c691ad8d81ad1327804d89a6c4e89bff5b715b1a334bbc4fa947b2c013e14170d2221a53016f655e790c6f35282dbc7c456dbbe1bbba704d3dd437979eeba1747a29207189eea041911c426c38ce9469e27bdbac6c68152a6d98404467cada54ac6c993209916056230cc12b6b4380e39cca943e8607f4b668fe27498914653a6a5d85d644cf80537ba7b8210891a6d69c1aa5d10462429968c45a471e0a47b4da1dd92f70bf2a338952fcb85e007ebeedbb10ba2c1c2ecf04d2c8ad04d2eb8c8cf5e5601c1506a4d910adafdd600346255225df822c2f2e5b7323923d18bace13d4bcea008b21ecc3faee83089c366d39616104514685e175b2eec4faee49719018928b3460224a85cec3e1fbabf11e5eee7ef022146201ceabac810dedc9bb28749062b5dc43644227a4b9fd6394d5e1a443f8992a46867fb94ef897aacd3a27396a85228e8f1361d3a1cd173a80273f352cf56e8a431fbeb358c6973dd40020ed29fb5e062eacbd8ef59ae8a28f477036699e2e8c7d753497370be6dbc316d5070e3ad7e2bc31b9945ba34e23c8ed60029c576cf81bd9f7d42f8fd75c3b34d86f792d161d4b83e3aeed39edda71bacf4b62bfbf076c1b20c67fe4bf6227a26bd8439aea822c7436ba7a544126b72e7a9244b5e4b3c0e336a33870b96284f4690e327ff00a46701c227013a7cd194101dc0ce8bf36e0c9945d64473f8ed19fbf217c7ebaf3cac3afd1c065f3c792eb34801d935b345edeba18c6588b74097fd76e9cf761d19b58c85103e18ee44aabb0d90e09f1013462457988f38b9daad7f909a3c248d72dcd08917477b219c1c4535d909957bdc1a13213057bceccd982706894389b6db3f04e775fa39c336da3f85c175e776a291c395ae6eb9e5e428ada5d0e9f3d785ea8c38ac6c584ea39a57553e81f564f87253d5d1defc1b370f96a6863bd75d63349d16218515981080d334126278a13b1f8230b4081f8507fe4799bbe09cf7b8b9ee3324f1b7d75de11197904b67473709ebb1d91b1de266d0b25680b458a7b0d749de982a616138051624333830fab67a6e1a503ac538675f20c2764e1ab3b403da6a927b723253b5b63745d35ae8905b46c56f69aaf7e289fd221ba69da1e840e8fa39ed38f69dff009ba9711ae0f90410644714c8b295e1396a4adbc15fe90097778a7bfc467ab2ef5999533babbc0a96b34f90ae7186658fc7549870cbe59293848e46c70d7ed6f0141e3c90f807bf56fafdfd355b728ba6bb76334565deb01ced050f6091c14c6afcfc8501ff00aa584f565958e09a6d27d85b0a18bce720628e92271c94ba31f257c7e59fd949a27633c8709f3bc4b6a79f1d4e46c69d40a2339fb046d31c267b0db74804776769bde1f21c4824f64cc57efeceadd7fcd00dc0236b4a61f18ddf4505b79c5031897bb9608f424c37f09e09d0a2b64e0a4153b42219dba4c475366ed979c9d2e34f21b66fb90dd47656068c4ae93b6de5a8e36dd3814d883ba5039ee9919cdeb636d1f4b61e900541ba5068f8afc3c5373478bdec8a0e69983c422e7b835a3894347d1eba2c3ef788abc512a59791213dd8915f54082889cc1e051bbd936bbd751f0dc27440654d5b90597b33c02da8c01e415f22fc3f136c00a812c2e0fa5b0e1f17c45202aa4ea2bba369ae0cf09aafe63497446e5c14a53b25c1aa66a7c89120388aed342054d578293705218a60cd4ec958f66a43830c4def3209b0983d4e7616b9b307828b07ba2a3d2c84dbd3d2200b8f16173c86b4624a3d1fe443a37ff519597620973526ba615e5fa8a2d9faf9159159da6e69b121baf31d81574a2a8aa9ba9253f8ea36627721976a197804ec11b458861bfeab6f4584e7e6090ae3dc2141f032d98aa93e815d18213c72539ed9f23bb45762e379bfe5514acaa2ec4298ab751a7929656e8ce7761fd59394ed7c6886eb1a264a8d1cf7cd3705c1735437ca9b8ccf91c1064460426c4719c41b2e90b688c8d85aee1661319a6bb9a9ad814cd758fe91d905260b83926e8ffc498e89768d8ccc7e2af35f1221f0862b92e87470690c1c7d771b46a8b45397159372f25446feb52fdf50dd59b8e3682077ace88bb63777218311f9354e3bc68ecc855c85c877dfe27d56950439cfa875e74a64913e1ebe4b26b22ee2a454c545925246d863f55b27f654da6635a64c975101f1078e526aebe27f6314a1b036dd22eb8baf86b8ccce54f5fbf4f25fa388b24a6da1522a7a81bc1b6b69eaa7066cf8a01ec556382ecb95dd1f467c477241d18b3456658b95e883a7899c4aabad000e48ea3cb9ce21d0da5b7b80e5f19f92e2b692067ab2e164a6aae47bcd3c757661b8aeb223618f9a9bc18c7f52bb098d863f48dc34df73afc16ba4e3d9a914f9792e2b7f4e1b9c5779764fcd7e505b2c68f86a0dc163990c746c01ae69a918d6b9cfc970364baf1bb413df60b04662cc75e3c8ce4d683b2070fdfef2f25b2230c9ed37815062b800e7344e584f972d4c161648ee0ea8d4d32e96117bfe312187d7c9913467340768c40986c8169c3e38a3bb98d5c358ad2cb8106f4aad970f2668f11d1190a0bfab885e642e9e72ce48ea62b1b70582c3752b22458864c602e71e4a2b9a5ee697120c4ed1f5e7e4d9457dfd220c9aea1c3819ceb391f66d3223dbd236e5dbb9ce9fe7c9c6386078730c333e1cd322c337a1c468734e63758eed9a3437de870bb52f17dfd4f93e2683167d293d235ee7622405dfdbee5ec9b37ba678d93c07dfde29cf799b9c664f93d91a0bcc388c330e0ba585b2f147c338b4fb151c0c6346b653af3f98f9a7c77e2ef289890aad7483d9989cff00d7c532342f08bc3c2729fdfefbf2f88f6c360c5ce320a5a1b1b1bf5be63f64e8d19d7a23b1329794dba468eebaf1c38386454283a6cd91e774c5ee1e672dd973886b45493c139bd317913ec34fc933f0d09cc635e092e226f1c47245913498d118716b9e48f2bb4c0d222439700ea6787c4fcd4a2c2644781473693a77bfd4937f978d7e931490ceab6ba585fd6cff00cfbaafce70c6a587eeaa4de91c66409890f5f4fb92126c57b4b419b4607228f47a34368952f126aa70ee4096176b4e73a7ec8f49a4c4208ba5ad37411e8117389738d493c7ff00db2fffc4002d10010002020103030304030101010100000100112131411051617181912060a13050b1f040c1d1e1f17090ffda0008010100013f21ff00f64cac0652d3547fb73eb2c312b8ddf9969077e07e65426e9a47e2b7b8ac103829a2adaf722f4ef615e3edca4a19a76858a7405f4bccbd0b98a7304285c2e3a9666fd6670f272fc7f9c8005ae009be7c6ab9a8f0b96ab1fca2c07ac6b5d5e738eddafda0c1ea080efa8dd33a282b0967e3f74d89051d9bbf88bd08bcedf980e5ccb4635a550229d2ad8b05d0bb58972ce50857f94cb3775d1283a61d5ea46fb0ebdffeb13685001536cc1c42201ef2e347c4bc0ae32ae6969f202e956abba0339483684d1873e1edfb801dce6dd10416fb68f48c1dd2d89ccbb6012aaefc45b5316e59cc4207a06662889532abc4cb42351c71fe39d69d05b3d74155c3528ca068ed3889e285cc14108b530699611456245720d597ec7c10512f36ce1c14bdb4e3b998137710b901f0ef3fb6aed41687308eccc1b96c044a7c8fac7a91c658f95cb369df4351aa97084a8152e4c53b9cd530a4661dff008ae2382b173e3fbcc039c00e3cb071880db00e3ab64c50298cae0c758c74d8318acf05d35567672e4cca328b24c02dc0f57e5e7f6a75d2e96aaf58c054a8e9a4e190ab47d65497c1a9e6957305e7ae9408afa8c36c45d1926bfc34402176bb7aeab86121e206012981516c1322232984a7a19e69848c5c243f12c6f3158139c969dc2e1b3868ed7fcea8fda2e5fb4442c94ef8319f5fa2fa175c2e9593125d4111d0a6d9835d75d928878623544333c32b6eff006ff8235638137b777a3c358bef3174d8c4ad5862f6a9e954670971084a6970dacc11a3de5cf789763bc66e692160ee499126a02f42adbc8b79f6edfb36d216fb430000b8adbf42c1874112dd2d8a53a1d1542a6ea711d34c49032b896ef84af2835a8f4615cd6a2aad92a42d69861f207f5c3312d3fc87d9800ccbb8ceb9e79570768418eaae6d4762dc63d418b7e65371894888a84adbd226f2c04a64a1982f1318417ad8d9870e430e3bce13cae9ff004d7e79fd90d9daf83cca13399f9ff9f41b6003ad30d9992012df294f1301886f105b488287d0f4cd0dc6aa1b2e024ab464b947357d60a01f93f58bb10ba05fefaca605dd669bfc4aa2743a573c333a8a0f674b2fb31de7a5830935732994e88ac2c5895513a621a85c346eaf60ebde5ddb6d5d0693f637e541b19feb1d0a3ad1186d0b8b28613df335a42595bed3279979059dc7d379df4c7137c54b1bdc432e19e4365e6a769094c153d98cfd476cea02d58f6cd42857899e7bff006ea26a32a6a5c49a3c2303f945598b6cf5babcf4dd3310bca3d598f9318f745608c598244e26909c4fc0672638fd887900e5588ea8225afd2423751afe61a44a989c3da5b85388e10a2ee98b79fa6a62c2e5441e06ae2f26f172e9acd26d2941e3febfbe3f506ef3b89e064cf9cd7c43945505e22c6a51d1013cd196e5dfd0188073295160a87926a295b538e23e3fbcc22999d09b1568a482cd3dd5d3e4be1dfec2c03bd46da00964f54af79604e2455a2259e01d0f59b51ca06e149758fd37b4c3198a66c364bb3de5607b0fe9e51405c2c0a1e2ee52a881b469ed389429d2a26b0635689f5599702e67a6435d2b32e1a138e605fca7cf4bfd00574ca6d73eff00d63f61f2a0dae29186a3aebad20f72cf339152d2b3da3c86f3721d52c1fd268b12f431e913a4fb547f3fa5cf6d925d06ff00acaa76864ed1e5d4917605ceea618292abb1d93ed15298e3e8b767a379914b504c25a9954188014cabeb97bcfbd563863bb743c1b1f729fd81866103943d573c553162343a51ba2666b0c11a527307ac199a56935a2fd7b97d12ec48d4388a16230ea3c5e4bfd2c7397b3f1cff001f89921f6907885456a4211b8ed53baf54ec49e21baa621b0c06aefd3b448436937de667b633d52a70e33e81030931264f110389604332c223e65aaeb1d184556106dac5673e717ace3fcf16c16ab457040d1360f47aa8680b3781eb2b88392025c2748a194feb5cc5dcb8b6c6a130cea25b0a6998a6715652a3e70133c688883b31fa3562e198152823a8a42e4eff00c4405b993098d021317307226bbc3b14b2e81de5b422456eb7033627989afc252c6351b3686308cc84d63d0aa94a665bb238428b96a86bd5625302f05a6d63c8fc7f9e36ca780fc1fdefb9be9869728a22e128ba864972c00f6e233c3536579ed0c0384760ccd422077fac4cc33030c29822d02ad3182b3c740be12e7aa59982e664a8e77dfe8a12f46bcff6ff00af4acd3d007b99632d651080ac3898238f78c0f62e3b2e7a7fee412b95f499c480a3191c10f2673115426b52d74b2afa64da800c6a70452ce1a3da5ade300bc7a3fcfa0218a362bf82a1d5c6a6666c8c20f31a9bd4b41770d44be25a1909550f89660cf479fd5eaa82228b7062559b282f2d79e8d84d98c69334782629f96e6ff46d61ca853b4642b963765730cb9359be3b6d6f98f5283fe885871aef090b5f98a4ea52be65706652c75cca86ae3bcad834dc48352ef64144dca23442ed947975effe7bace5076d3d12e23d639f966f130333632ea664d32e09bcb4e205c69899b9a86d3e9230dcba149ccc7857762f3f062024be798f4f291499a953467f4ddff48cb7569763f01aad74cac16134a6fa77f557410701953743eb05b3158416cdf63c6c8ee485cba4487b4a91292514ff00711161e06a0b13b4572ed8a0386b85fe5ff3c8980441cd30fe3f12a36398c3a45562d937a85e07b3ea4fb17a1259e696bb63051a67312ff48220bb23e3f4df023e63605fa7f719979d154318fac01431f5ab88aa77f5ed02d453b8c49e53d197b2aa72630e995f8982512e05ca2a38182f166bd2ff00cf60ec467077f6ff00b1a580b03848102109425046ce90812fe5e7ab1fa01154d8a8c70457b9b74da2286ae59706bf3fc757a465d855ed0376aaee7c5c1a8aac64cd7fdb9521f7549edffb111be9bcff00d25c1d612d5d5efbeff8ed2c80b5e038acf78a1caaca2e2bde2606bfc98b3f1d38e3ade2b10f240b30963f1f52c01044ac8b6cc57bd198b001885aca862398a34cb91e73b1291b7ec20e50d6326b87c7fe4a8421d2b17d0084ca5e2cb43a31254e4ac12c651edd20522744bccbdd4add74876cd567314b9c3fe66b99eec357f9a94282a2cfb51454b5c557bc5ccec8a8ee12f4d47dd207ce898d51d6d87b459286c423d0c399733136859a8431c74c287abfa13dcbc5f995a97710440a82024abab76089a562b4ce1d6c3e7f61b30635cd3a643756ca81052aa2da68425a1387404bb7e83a83c0bb562591e3da29743d25db2921496c30d63050b66bebdcc53e423691859770b89176e1b98c57c421215cab7e10d97ea314d4f11da6e2c7c802d4bf148d20778c369c9315387d6070365fc12ef46bbe8e442452e2675472c67ac4d38863e8604bfa1d28eb38ab8c67579abcfec4a71b6616d7f3ff00a74860ed2e2464a8583d14cc23b82a779ac750b6889e74e181193dad4bca017d15312ac765578e906a36cf1a986153729186c5454603965f81e89842796369b31027842ba8bfc481ee3e6f825541b8009e750d6626abb81db02e01170d9c46e66b5df09ba8b12500452c225952aba7e9a79a5fcc463ea8f4c44d517aae5f40e86e96e1079cf7fe37dbf63a283b1d0ecc175cbb579d873e2572b6c9908324311dcaec6505a82742d9c9254e70f445f44588eb799903310a1a483483988eab7328038b8080afa77e4b16ba93af647a0664d1161e41332a8e3885c1784ea70c1e08aade835a6ac94f77d633f2584aff002995f0b4b16a7a4bcc4640a778b61270b0524693998df2a20c4e36598b52ee5e9a88a881fe90a69321dc14840952a540e99b8c6a95f9aed0dd04b44d7e57f642a4a0c859e381bdc751d99a97a370d53995082584cb05c3a53dd9a3912843412235094112cb71c33131a2aa5325733647bc0f7d71510cb0e63ebeb2c596f05a2610d197626f402571006a7e3e6333b337329961c35300adb4dacb5e8e3e13d32aa563d120879eb298fa3a854f9f4fe2525f0f6085233a48e6d767be451b5cfa4a1d42e2d6a276cc499654e8d8426375702d91979cfd4b0f71ee3ad7fec6bdb7791f1f1fb30a20f5894e33cff59ccb72d998003da57623a665cda9918345c77e05d5640041e65c38cc0818c9001d4d11722b88aee0c0a29e358b043d2a14abe7989453950edbec4ab0a8b60e4567e6e163b2be54b888d92db2b9b4bd04d1a8ed90ed62f90f8d278e55bcb52ef2c14980e90ed2efa22008fd32f3da5c2767d3a18752a6766d01bd41cc33170188fd4536df573b8e43a71300938038ae3f6746ceb1291898708986377e1c45e52957115c2442917c928252ab953d272fcc140fb25d86622238c9d32815d1cca787cc11d31603a80b563e56cd77ef7887eadc3fcd1ad5c960083121a67eb32cdb2aaa29793e6505ce551117dbc916ca9d983a5f514dcbd4e523b3a6445036b89615bb87c4b46c7a4b17a7d13b2835288d1958ea7201173e057f032c3821c5e1b012ce33e3f6a76ceb129196d768051d9ad76f89a417d19725ab98b1bab4d02665127638945e778b6e2e3909731306c70ec740f71b313a8f6cb943f05314003cdafccb111ce9f0313b4e985baf785a47f45e65d73e0e0f695be95a758145657cafacff00ef29c8113c74e7276479843dfc8986e5332660095c7328d219bb88bbb95e8637379515a8f9eefdb4e501932d725f60d6a211207a654efda0c27981d302a8165991048d5ced032cc6a3cf4a241644d90cd5d4a56de12c08078cdef6576de5983bcd54b3ee12b66c597cbea2643395855bef733e09552fe9788ee31102a7f0408306a58111491d8e63a054566857d1a984e81c182e6f0a21ad1404626d82df1c1c9ff119aa72b5e0ec7ee1488cb01a946ef8a3d88d5a861557eadc4f8ba57ba9fc4b93af143fd2cf53c966821d17be1cca6fb208b5176f283c0bc04433567fb534e8626021f82280b9b6276ea1b6a56b1d2e57317dff000eae88416e0399e3083f97d481939258cad6e4c1eac6471a5a2722592d3e22dc116b47d2082024df89be6d5d5abb9a441302e5c18e15da586e2e27e5df34777c4d6252b6739c994c62abccc680aab81e6bbef3fbd7f6bca1c7573112c7a50fc66b80a3fefa2c1c6baab246fad0d728bcd0dbf3fc431d4c5a954ebe0db3e3a1fc11d2e7da0716b2b13a152a21b1ca8aa55b5e7a932d32be4d057c1c7b43174d680a6f9ce799547b72b50f48bada30951e7873c4c90dc8d05f6f4f33005c41862b7bfccb927792e541cf80f83f7cba71a182b940a3eabee59c351bae421d0e54b8d9bdbd2f6f6ae07758fb50b821f5b23ec642341e9d6d67097dddfe652b53c531300b75970749d4c041aaa1e9da5b063369f98d3eed16bf4330de37f61163b84f5bff007d16a0dfd1852e953baeb15ef1295118628e70e86a501eead1002b5bab3de98b50d182af351dbb9f3d1212d2272ae13d35f1d036820d68e63b3d7ccb4e9776ff00532ff4a8e3ec27ab0033b267f83f3f5f5cd27f0a8f091164f24ee5e92eee108a0b1d3b0eef7453b098753c3015ce7d2ce8c27b2f6767b9e2174c64f5ee6be606b2d353fe07e659590f6aeb5a7184c1afa582c9626a333b54fd8358822d52ebb1cbbfcc3a378ccdc68e7a78e060d90ab97ffb32a5cf42b4f31b314bef864fe65c1c9644897068402d5e22f9f7c8dbeedcbfa6fa7815943f27d0b52ee34dde19dafb044354c8a6b179cf44b20a954e27775b71c0c06591c3386efcbd6612754b9757bf469a9f601d939256edaffe0260cb6ec7db1a7d37f4b3a635543acaa11cd1a7ec172c8b06c653c89d9c75a278bf5dbeee7788530657e1531455b5f565b9311e1e834dc26da18e8a69ec92e9917f47597ae39bd92c2ba31299d92a3567e7ec20f88d46d4e0f8da7b4be3a2d116f0e95b42461e10e9ebd5f5b7f39977d1590d4c15688b2dca97d6a5a53f4dc1d8c2da2b32e4bbe98a4748982aeb9fb081db477629bc79398aba1d405cb6072631a358e07fde9e3f75f46622bf4ba0a6099cafd1ce988cba5eb5777608ac462cf4b894b00ec18cbdbec2624ba08b60e1c1cd332dd2a5665bf3512ca753b03764b3b0669d0cc717517b309cb8813eb1961c448916610bbd7404baa664e9ed2f8afc45c2fbab306d5f731d5ea623cfd87a0e582bb3f2bfa28ade07a7aeca96be3a754c6bd54f07aaf4ea58cba5af4b92de2668d4f4aa0da7132e7894431b1e3bc30d4c3a9a7e21e2a69597b57739b3269fb0f72193e0ee8f0fd0201f0ea3379c9de5d2ae87bb814ced0d0faebfe42843193d20e961b6043a5867f0004e747c551888db2e1ae7e7f9854d12af4a710a2126a3c19a1e5639510ff0507094b07b7d87e67b36c357efcca9b0f2a0c41caf1eaec964d4b9cc583921bbd7f3cab79fcf007a03049cf4d8be2cb1286e5b31102c78e04c0d4c1a8090b857114e6dca562502ee3bf0f0e7096302225ab412cbd9c83def4976bc43d1e289fd0f2fd883706ab71630bf274e66381530270aca1d12e59e7a449568fcc276344fa4efd4af8bd5bfb6783d4626fdbb691952b7c5ebd03842c2d7e3d76a470d4f04a62e86bc4c87f258d2efa21c3ad2d25684f7d7c2106044355443b79e9dd88decdbf621ab3cc395e6b19c573c7acc01aa653f74ba98aa486d9145dd55cbe871153395ed086f251b75a8a194d12cd795de63d41f06a57328fc27bad4454457906d671a7a15d1b55011995f17befd408f5c7459390731c53d8212426d1dfb7d8ae197594b1ee43604b099f326a557c40b5e98179f996cd28268b5349bedc17f7d40d9675d5d0b1ce0ff0070f84c1a9e18c1c060acb396fbad0ec91a0346b0be917fa318fabccc34189c0d31f50819a0c96e3742b887ae4d2763bc552adaf2fd8d87cb586dac1f60fcc4b153149994ab95188be31494bf88bb08dc7c26a335bc2645be85a8056e5b4702fbd40110c0b6f22774043c1c743a980e1e97006372c04bf594b01ed0ed5c215a0be704bb8f27d8e819d68a461287384c9ff94fbc28746aa2a0e353c45d0bae6e18abbaa536abcc5cfd0a004f64abf94d8de5d4cdfc490e522d435497654ec3fdcc198b143f9956e4961e0f57b24bda38b2dd16ca3b399ccdf9bfe4c1fd9502e2af25ce68d7698070c05dd83e2119254b15866f9ca5ea9b3cdca2e2cdc00c138f13702d44d530dd6c8a254b19812202662bb877bd59ff00239f7cce652a566109e81b5c7d96f5074a94f931eded306dcdf91c22f68aae34d36e65d6ee094b64a7bbb7e3a2db712963dfda0f277882653a37d15c43bb013ce143dddfb4696eef80f983fd1e4124c7e6602615f48c6cd786aaf060bf657f238f61ff0072a16c02b231f7d1214ccbea6e3c91156731330f6815ef2e6255a03615a1cc3de8bca03163798d3e2331a70afa41ee4d50b8e1d17000797f6101979a344b8d33d454950097bd2a8aac9c517195c72fd95a28a339b4cff07e630e66233d050672e46e0a97183b41943d9895b58fa234302cd72774c4587b00ca56f9058f880fb01444799b2621816fccce6a2762609913b8ab01a0ec36aeebdfecb0da19bed7bf8e7fb98c4c32611e9c976c186ee0db312c070ed1735f28172fac3167bb987b7788ede814c42336ae8d0c13187bc393c41426ae6283880b0bb76981b022ae069e4afb2ea5451178b37e2b6bdae3a6a2d6a170801a85b54b9514e7ea7dcdb88fcd71c531c208947f128040592a52154b95077185cb93390d12b576799973c687d974b0ba5748d91151de333e56d258948a370f8c34250d746f249980af48cb71f49d078e9bbad743ada008977302c8b86319a677331532e70c4db90a5f2ed77e6f5afb32a9040ac2e9bb17c76db70fc6e88107e49ff5103855d710db07bc5917d0aed2ac52085ee5f5c0b0b3a831675d2e725447c6353f2e1a858f4400d5bc0679de2ebecccf68faf92cac171aca16c6efc74469899619aa47d65d03c12ce2bcc991db47810a5102e276d11da574d46d338b98fee85d02dd78885de7b25c2ff2fb35eef17734a64ad61719bc72cd11bb89b046cb9f58d830219b26dd4d52006a8866074a9529edd0b2e5810bf333433d858c691abca9bafb3aa0522ba34d0b2e906acbc965d982e02a5859b974349dac1fa0415a705e9ad4ca2317b4aba829a9644b49620779562250de699e1a3e3ecf1b45908a2baf3805781d401181b8953904278989888e997ef185b84aed1e5a45350a3538706c102c048082cef9dd6f03c5d525c73bbe5cbf67f630113fbda16d256bff00687879f5b07a742146599cac03dba298741bc50e662726e70d965330dee7713002ec56392f0699854abd70017acd1d8f43ed1657261c11f40d0da9a144672a18a3ef11695f14e85ece855137082eaf5f5b544020e36cb861c500e3b0b335b3238aabc782615a8a30783ed3d0e4abd40727f772e9d5081d8c9fc2b9c1a08f4911d136fa2fa09f8a9d00dab28e176029c1d37c3ac6e0778c1f0b4e5dc789b6b818f65fb5f191f4ac0503a25a538cbbcf2892d58c337947c3da5ac8bf570c2d9acd60bf129d4eeafc0b793dabd9e682442f03c3468f51f9a712e86c5696a93bb5f02e1ecb3aa429b4bb25358c8a67c4d0da874b6f158ae3def894d45370ab719168ce5daad942f7c204a0771ff8a9dabbbfff006cbfffda000c03010002000300000010f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cffc0ae93edbcf3cf3cf3cf3cf3cf3cf37e3df3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cee10d0fd2b6b23bf3cf3cf3cf3cf3cf34030438e79f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3b66af9e1d8d847d9f3cf3cf3cf3cf3c8153d2af8280ff00cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3d611c95047440e1e2f6f3cf3cf3ce464b0683f41b8765cf3cf3cf3cf3cf3cf3cf3cf3cf3dbb68e945009cf48456696f3cf3cf386ec92789777447cb2057cf3cf3cf3cf3cf3cf3cf3cf165b3fd0852c15eac9688cbd3cf3cf3b76c40bf3ff00bdc4238d37cf3cf3cf3cf3cf3cf3cf3cf0791a4a1ead9fedbd20316e7bcf3cf369c010fb7a19f71ea0d4dff3cf3cf3cf3cf3cf3cf3cf03f437334887ff00ada206f8a47cf3cf356e516ccedcaf8362fa1b0f3cf3cf3cf3cf3cf3cf3cf25c61f38cb6fdfc930d49d8d3cf3cf1bf829ba33fca4eb434a924dbcf3cf3cf3cf3cf3cf3cf28f50ad82577ee400fa52d373fdfbcba203a8eff00fed22bd98fa0a7bcf3cf3cf3cf3cf3cf3cf3c761dbab6bc7ee356fd06e56c176ba83d02eaff18eb10d7c23bff3cf3cf3cf3cf3cf3cf3cf3cbde2b8fbe3e0b3300f0ffa8e42fb1ce62e142d5dbb2dfdb127df3cf3cf3cf3cf3cf3cf3cf3cf3ed25581d3b14f73414b801d7cfb45f9dbcd2698a76dd7a77cf3cf3cf3cf3cf3cf3cf3cf3cf2e8ce99cafaaec0daa187b454ef9ee09cf8f64ca3d837fef3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf1df9fb7f55d1c87b17cafcef7c2dcd74be441a9b5f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3c5de36a8422976b38f0667900c557ce7bcf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf88e8b1e8a9b250121de9c415b4f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3c632567f85716b00bfbbe81cb0b1f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3c717a7b2ae83407debaae5aaa729f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf212fbc0f6e7840823b3b9adff008f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3dbdff004f0ed08149435ca605ccf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf2ddf671de301701811e2fe555df3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3bc63482c07f8d57e681438b77f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3ca07cc0542510c9dd2d36df3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3ce80fd6b8d82b67192257ef3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf4ed7a5af2a2fb67e883ef3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf794ea5ed07d17dec1cb5f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf90cab769713d68b57735f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf095d43215041934ec09cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3d6f3f8eeddbd0aea47cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf26dc3092db8bd67fcf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3f10f43fd957bcf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cffc4002b1101000201020503040301010100000000010011213141105161719181a1b14050d1f020c1e130f180ffda0008010301013f10fbf0b6a68e5c76441f509d123b45406d7fdf2e822b42f526993d9fa9505b1df963164328801c2ccc693650f175ff00a5c9e5d0e73058bdfcc66db30a3306ba0d1226fa96b79dfc5d8e98a6ecaaee98f0dfd17e985b8633a428250510af3391164148a759710610bff95ba5f36c7e7e3bc52f6b30c25560ccdb5a2be5311e9344c31613f31a3d7973efafd198ade3b6b809940415c10944a98a6e2aa8d64d7f9b87a096a29dcbb7bfe23330c621b6a68e803829693cc289ae904ec0d1e675ea794ceb6fd164f7962dc104b38223980688b2f80bcc448162554cc753f96919aff73fe6de653b65f4262178d00eaa6f4c1a92bd4da81170f432465cb750db71fbb7d0e07947b129817354cac83978eb1c520dd7856bfc929e44edbf10a0005c6884331e06c8eb51c364621aca4dcb93e89a79d3c7d0a557821506e4511b61a3f812e2254bc4195dd3f8a6c03ddfd2320649b385442e1e0e2ab4c1b84b50bab899225841865c7d0f73e2f5aebf40ea22eac835162998af8234e1a4d2164c3a954a66f16bfc4eae61f112db95c71ba40c334482370408752c250d41b22434c29361a7f334f259d5afa0efa94620ed23b30929844d2135897344c2704d11c45ccb59fe2807a58bf1fdcd1a5ce758ae9341081ab2329acb1ae4bcd4680462259ea3223e201341ff00be93acb8c1691aa2b544fe0cbae1ad90d66779c3076bb1ac00007f4d0ca3f3d5f827bc4fbb8efb1fef48c3647168d9a561cdf503d6a28a61b853cda384299ca086f10b83611b635660e020f687b7151cf88001b7d066f6781aceb06ce0f033085c39a58cbb11b1fca2bc1ab0aabbebee2fdf6ef072f2105eabab5eb17a3a057bb98dc018d875b778956c25303ba9cd22868b5015f788d53c943b0a7de28f173d1df99d6640e0661cba8644238b62342329ebbaf7dbdfe3e8705bcc1a6592c8e550219957c4f184b4bace91293a732f2d0f78788a3a8fbc45543260dd6a65d9ccc3cb9f69449344b1e86081ea776133f185d9e78d652b17aad7a3550cb8392f67a06fe61d7179202737c7fc8b419bb8773a3ecf78883881835c105c4198608c032b5e5046e657aff9feeff4597c90a19926ac8870c67646c625b744096c86a9cde47bc2e869a861deb2bde2342fa248cc80ce0f42f35ef3dc61a7994aa55bcae3dc87801beaf7b0807a610364397132837e2202ac8c4e95b7789a889160d9e9d1dbf137dc61e6b23e236b8a6ef8788a8c93c0615afee7a41358d5d8e87e757a69f49d0d15758830c12b982674410a5e77af23d6553b85748052ec6e214cb01034256cb2f216742f1eb97b4cb85f34bf1ef1f22aeabbff004476eea11a347120a95234c1cde81fbd6377560694bcac5af5863e9a429dd4e8e9e1bf31e663441921b9518d41e4573fe39fc4a41ddddeff004cb5e8c741985da51a91ba108cf7fd36fcc5035813b4ace9817cf95f588aaaaf57e2e0e11595dfb5c1b844351c1416e93281ea18fdec41e96b46047c001df2fef4828d910a6d6cef45fbc14355ea327bcbc9526194c097948c83a73dbce9131adc9a1df9fc77808141f5953edfd2e01361994fdcc9e9191bd4617a6df88afa8e62845e80f2caf82e51c8fdd7943cfae9ed57c149a18789410034744efb311a06ba377df6af9811aa806e969eb9dab7b8b6ff0047ac5ecfd73f370050a3eb992d81fa76e38c85816bb1158f23affb2c20b8a8801745f047e96b2b0768047d0b21b5a9d47acbc2a73b2bf3ed3382995e6cef9579952e2c4afcd09e3ff7ec36d7f965f23159c7e72c4d1282798c70a7efa243c26f08e6685e11ece23c0c6c858fe3d62ce0580e5cefaff05459b4085c5402f6329061102d63e70fd86c3e0dd753fc5836591eda43f98f0ca6a5e48ae10c29b9b5c0a011d8cd1b8350e15c344066b97a450bb663bea95284e83e3ec28235483d9c3eccb86e3a2975163c15f51dbd49ab2101f3c33088710b28aca4c70858b9aa658daddda5cabc594781d7d6a72981cc4d939ca1358af7afb115ce529ee61f73804da00d5e49cfac30aacc3a9ad7b42325b30ab300a085cc4402b02321b196028db36758acea83c287e4577acff510d64ffe7d899769e0703b5d3dd61ba596f5ceced12cb0b21cdcbfa9927382cc6d033b92d46f50376191bea2fb5f089accac2e2d2aba567da229873d1da9e8c6afac1775db4f78e73e67758bde0d7d3fdaaecfd8ae4d4f261ef8c9d427712d66d6e74998bd0ecede650528a8d0c5a83296dc06ee05bd57f1fdc146c95ee01eae3fb8e87555b97b72ed2cad6df9679c0e96c204711be839e97fdbb13702f63ec6e06c56da3b9752fd7b32ad24b7eb2e194085de4b9f10b9deb1f94ccf778e0ff7d26396287c273eb0214dc0a6ba0e57db9c2f68003a1823652c58997504b77fd7dbe7a4718dcbf75f5fb2504b209b4aea761dbb3af4872ebe0a9a656c42b87223513068f2e9da055b640b012ce84a7baf37fad59dc58c0f1abed2c2539183c10533afd9a61b25d1a39ea1dff33b5d587be904a04e9113d2b1718e71baba3c4aa008e47fe10c683a50fb46eb5d5b9643ecc1d0953316a6a19d9489297e66ae7963de596c21c9326780efecc2a18b51ee3b95c52908c208b135becdbae016124a22ee28a9519a92f57d9c5564cb70658ca328d115510654cf0db7da2c6748225902e900c18952a2185210542de7ed3a4456b8966f3ab11de3b117b13507ff00843fffc4002b1101000201020405050101010100000000010011213141105161718191a1b1d1204050c1f0e13080f1ffda0008010201013f10fcf5f3be869ddd0f188616ce6bae9463be7a6b1cc93a03dc76e9f107b77f87dbfec5b836e2cf91b6de37b41460347238c1af36a91da8b9fe9d1ee7fdf5554c001e24d4c3f737d44006eb065cb34ed8dc564e4639a92a141a000f22367119b58886b65b24ecd17941fef38832dbc4efe3a7cffd2f8b3ca7253d623b5b65d33032526e441f10d7cfdf5e99bb26bb861feed7f6db00655c0eed3aec52be0ca816e332f6cb58d87305a690d5b604a152bd9302872d94e8a2532805853e32c5ce65faee7f72ff00958a5f36c7cfb47c96b31065259ba89352749cc6c09b91117a39ba3d7dfbebf677dac56e5d5d6cbc977a0de74824e3cb9baaf37ff9a450d65a45c9883e408bca5bca0240e595cae079d21072b93f0e8f9ed1c1d2611d47eb449412fd57a980e58c0104b5c0a31c063454148a5a21d4b3a3ccf9f7d79fd90b4872cd8f1dd706aef543156a2a0e18f9565c1184ef812a5980288c301d48845cc1e3431c395c5cfe46ff00333defeb75a1f5654b65b4260dbc1f5a4e906f306b2bd4de9316094999a9dba9c9dcfb1a694ab7b183e7c586eccc15432c8c59a732557029d6134e0e4210644288aa85286b3542abe81a7c766224293e9c1eac3e65222d482cc68d0f8c591d6a28ec9467594aa5cfc4269e7a797d8f2ba0f23c3da2ca0cbd8728e624c359d1113e850cb0aa38839a03784911c1b1f1d7d6ff00b07d09b007bc75d8dd23462a00cce3233a6546e1242b0e25cb2586cd65c1ae8f73e75f1fb06b1769cf6251a82b20cb2d99a2666e598ef897652b4415b84101758e2e1333339f06a7efe9767a11b5c52946c92b8cbd8848b8616384a14a30c260353440d054d2f79f3a77afb0a185baf0722fa9d6b9dc6dc2313c9a4b386d0f6e4dd32441e6e389673278024082a2d6649021ea9f23fdfa5c0dd17dbf734a96398b351541992ce10ce589c106da340c1313c1b5408c40068ffdce80f24d9f069ed7370c8ca8504bbc8822188f2896aa6b15114b71b92620462ecc34b0ac51d0d34f7a3ed5edb4505b3189f0cfb41ee7fbac6ca57ca2622fde286165f0f138f596601b8d398380410b866628b9a0500fb002dbd76dde457718a6a9938a90883c3658e710c2d887ed1c83589bcc15718a0bdd3cdda069052bbb97afc467a0736096b0b862b781a88b1a91a948769563485326700b85c04c6705cb11b70aeae32f8f5fb142adb00dceda29a9058ad022688e47c66e365b4e22155c42a581a91d711619aaadc2f443563cf091e4d5f48e571741e92dbb65b0ede933807ac402abc881871a8a62171af30c624d668354da24430634209c19492a98625c2c76b086f65eff00e7d930d0373e2a9fe6a72229bf09f2211e57660918e01454d1146b9433c756c2e4734f220d5bae6fb6c45d479ff91d380e444b5544b57ae60503ca67dcb05b104c54a6003109b5434236008513546133103a26b3c0c85ac0ed9e7f8f9fb3146c8401b13068c2be47b2197aa007292d7311f3e2180db12af26737c3de35bddf9cc00a9908d4bb835b3a102d7a352c5b255de256bc48cce5c9a4e902950d1bc622f598a039c08d21121aae7f8e72affcbdfed88545a16b3b3cba367297ced8a7999f46bbeb3d7e9af7a944507a3e62a0f17a2ebb6af86b2c05804d177654328914a469111442b970d386b062ea20a759596af07df59aa6e80284ae04b7446a9539ede72812dcb6ff00600a147de3f35f7e16e92826f98500478654c628d41b5db8100f48741c44bd577e20d574f28ab7ef7e62361e39f7b800a0c7df557afbe65638508de21b1b224ef28f998001a406eedc0491771154b93892ee45fc0d4f39ed2f12822200da58cd1208a0a03696aba83c9fd0671158e22a98164ef23f0265cbc28f7b440b497e749ac105b2a2b7e8210b9a515054b3a4ff00023d123ac4348dcd2a16e76e0317031c88a9689b9722b46a8d6245031a4a92294bf0889176368a7a371a4cd8ab16a1666d1e63d671cf81c814e1773aa1ab63f057837c3fa8d47311eb1394a5a94618d8225c6542d9a70414c311b4d220801a24bb070bd72c6fc1573c1d3bcbec37ef0437099a12eb22184cc734a8237ab8e054da3096949b11f7e00622d231ef0007e0f23e1d7bf3fede0595c0035cc4c3cb1da409a9a99e5bc15c644fd203f711b519a0955accda170ed020d43f097b539b2663a7de585c4588a94b0e32acc5784b32928de5cced11436f225752f9625b0daf9d45fe16e5e49f1fb8c70082b51a7ac1ae084234cd401de0e96ae9f3a44508e6e5f8f48a1db33b0aa5d3a7d0afee7afe154e725f2cfea692216e55b994a2688fce2f57e6f0205950cd23b188794650403fd8bdff00b963f0a80a749b453e9a9e950c5b1c7247e00cd650a594d31cdc32c850eb2172dfe83f0cc87a63cd87d6bc8e049d2c12d6382845b2120e59bc532dc2c21459b431ae5a80036fc3093614ccb0d68f32649821713bc03a4bb28754b5d9c1cad151b07ab9fe20eee8b6be63fae4f760974308f06d8019886c5f6816c60f571ba72b0050734eaf73639de7a6f058d014763f13e148309aefcb3a3888d99f37938f59596874cfb4c67a49ef16c57ba12adf5c7d31eb328bd807cfea0544699d5c757ff087ffc4002d1001000202010303020603010101000000010011213141516171108191a1c1205060b1d1f03040e1f17090ffda0008010100013f10ff00ec8291400da0dd295a69f8657e4af434abe1d1e735a689ce0b15cac28adb379ad3cd08a0150d0415974a6dd3786eb1757a006550704a30c9619e8d36ec8650163346eaebd1ad8f1c85855be889e47f4d8b6b1c5297da05e5b6687e332e3d946178dd5f329750695e446ab372d0986bbc120cd0e46aae9e63262201195ecdaef37c9c0118cb9a0adcef6305f264cffbc899c016aba0224d50d958bc85bd1df4a7a3040810d420dd8adf3bd9825b5f09213658614a5aa285bb3e9f86916a5a000e6196d221204cf0a11e447f34097b5a72a083a32c6f363551bec365c1cadac18d4360c5a4abbd7de1b501437a658a355bb86324ec53ab256a18cd562be22182295eb32add1a80ecbae8e7559dea8c76e2e52cae8991ff006b4c0abd42bb5feb863994b88072adb7d2c480555008d5006de682f6de6e165d2fa46c5ddf30500717a97a9fba1072b83b5a76aece1a08963847502e476c71c303b9118715b935620096a2bacafcbea7c255b406d41ae9e52138b915a61c1d556f7bd6a12f6aa2a9b0e1abc7fec37755e42cc11ac60ac36c6dad0d6e5c405cc9ab4714be200cb99772e812ccd62862fe95e26f20760160f4f111a32dba9febd8c02c931780ed1c99a8050bc919b4dddf4c43021001a7f4891574ed2f4e9e60c300957d20841d4b203476c130148f1312c722162b48360574e7b04a5e0ed0c4a959002c82b02178cda5a0c82d403a83f2dbe764209c3578291d8de4c6494b620aa106addbcee13ed13ed23c4ee713048ff0b547f7b4aeac9e55c4425b778c0403e5dee662ee5985468f694388fde651a962cb84810e0d8915d5b70a5d4d293a7faadbe45e285a0614a2f2553ac12d094d04f22abbe618a0c1d20f07c44f16f306a20ca9b820970179aa9aae5ac22f2c1d710134ea5a9bc4b35bb46d2b7baa8405e121a5d2da112b180800192987f2b3e396748f5a237db8e73a3ce371d18c4128b78699f020620f234bc06e25b3e88ac2be52e9f7c04249de604dacc612d443d1c09999155914aec87688a44a4d8ffa6972185b842e385c02f46a0fde0e00140070054b36938b28ae1ab4dccb9af78a0a4a8874e18180c379dc0bca41199764553032a6ff00ae38833948257f586d32c34a371ec00e1ee058ba2025620b3f282cdfc3745d5bd0c99712963828cc0a51545b7270725b131e96956ea25828956aa602b1ce920b46a7250b05c23f031421a8a8b55f43ea0d60c1e62a283587f881d16e78b9404cc3b4ca080b0ad983cbbfd9ff0046993757a8b0b5d05a5ad07315882aa1d381440500bd5750ceae7500dc011700063cc25514ca7cc7e50eaa94352a560c6639ce6059e7d61aa96222f0cb80ec14d2070893aab8828a32ae62f040a25ad74d13282e72f30b00b781d0913c10035cadf92ab5549e86d765d02d76850d13423297acaf4dfe0d04a2cc21c912bccba972cb9c422e72c62065129c41d6a1582bcc3387b92d5bbccd6584ada01d565b0ed4aa657ac46aa2c4e42882b5b20f783bd2d303b843aace912c226755701ff382f42c728b7c4527569a1712bc02981906b5b616802b84ed85f425547082acbcb778c41305ae0b6e6930099e095924d42026a177028ad3df11489dae5aef55b622177bbe61a0f625a06c7ac22a093952c97e88852a03d0d08a15541144086380632c531636d71401f926cd315d3ca783fbb9c1ad82dd0106868dd5f56527aeda5fb100d46bcc7872f217172e6ac7103b2c72c559034f2412cfa600631d0353c6063d693a32961700757c4bedadba948afa409ee555695cd40385384a7a476dadc05c5c0a0ae13abd7b0747fccdab834ceb4342d16a0b2d0b609294e81068ad5f03ace504a494dc0b0c119e68bee73089acc2387c4460b966f70aadb2f313aecbd27d12e186b18494b0a6a50fbcb8302f104dc698a44ab9e603333d39ecc1f5d52926b36092e86d3a718dbf5aac0a3489635f91d4d9f5c45c5df39262ab3a9766a098efd11c5a8fca5f2519398301616063a40cca30d590a092a50cf3d3de1294652b5aa37131701610acde2fd56a9963228ba6076a7ec53eff0011ad54bb067b79c9f32d2ad94ec38859d4b88305762d5c84016744be4730cd06a6d63c89edfe43ad070834006d5e22e5d2d900b48badb9553835714fb6b96298443e86a12cb251da0d1d2395a672da648b7de3d8adf524b7b444a36461b412a4a66b38c213efd6438990851947689b0473894d533008315090140cac3202154cb5171fc88a0843f7ca7e39edb05a567a4501b97df81c198f6998dd456bd8dc3e3ead60feea5ed050b3516b42ecd9ef169371b5a0d67b439a81dcdd27dff02dc750d661f3114af4ff00b50219c0baf1adea3c4e56d93acbb6e562f0955c16e6247a66fa2c7b5aff009178b62c80129d51454c14b816871cb77e3311504777f88a644f312972f304cac5f4fbcaec3ea8d295759659cfe0ad9170f3105c65e269549d270125ca798b2deb9944362faabff6305aaf14ef11d2487cb51675feed56044b14b1bef0472840a5c54142828588f3f9093c2992b559fef89556482ad9d44039a542ed1ef1b85a2094b32cc9e90eb52f4b8244eede1a0f6d71de346e24fb4605cacb68305e63b465ef16d566fd57d352842e093799752f6d08de035b8ee9467be00e1c5e2fbb100c6c693dbfc772081da69001083395cb2a0e9e023fdf2c6388ed509ca4adb4a8282621db9a8388e61990382a22003549a8b70dfe0c1b83ace09bf595a0b13701e04561d2079d0f785ceb02dcd408c2670dc0f791686c23b42d7de5ea2380752745301c668de9f9052aac05aa5e7a637afdf6e1230e64f799839bda4a26a6e3ea588e8e6de651162675f80acc0135f1dd8ac2b6c4429469ae232ca8317f1a5972dacccd711bb514a5c6b6632dc044413c446044b514062aeab46aef0f15fe2a10012a7292223828052a0ee3a3b05b7cf3145a635c30985a1c730a0051d2130c52c51ac447f7408a96b225f8fef58f9e35659a8669416420d16c9f505402d7820f74d4d38e669ef29a56d4c662618e157c46a58ce09445d4204a774ec9721bac5cb5d5b5e599ac3d0ca9131448588962e4ccb1c69a05a14c5ab28bc5d393f20b10b32a14555e2f5b8a1655694f022aec22cb7b39224b480a812a67f720c09d499a55d55b2865020654b506bde58af2fe05f56d29fa312923de78806e4348ec611663b19ee7a567d2803cc6e556816956589fdd74bff120d0b7055076d1be2526d6a20731bbac1c4396a5585d7796f0078f681b6b1c5c1642df797294b80dbb3b55d5bb88a45260eadbfdbeb098468545ca8d3c4ae9917a03d6d452e7d886b4c9465ed72fc2b3a222e415c563996d6ba6cbc730b3ec8b4432761a0a8fa316cafa3fbda5856af238859f0944d33888c6057688e5e5575894c5cb7202e05c8478ad9cacfe401140910b5b0183c12a20b812ef5d4a1bc6e9a1c3fdfde2eb3e97841511740a97179e91f048f58c8ace91261ae92a65266729645bf55a205b46e55b92f8a85fc5cab997cc2858e2bac0bc86e0bfa12a6d02715d1dd55e3de1205cc459588aaa57619d21ed395372c230b9aba786b9ac3013a451dcff000a1ed97cc03295d6dbbe987b5a3ac5eb1ab0728912056f074f31141e8739888130d60c5cc1aa5aef8961e93977062f1c16e2139617cc0a288375d3d04334046b39bf8fa4b52517c200d703c43d073e212eb1e174e7db705f8b36537e7fbc4772588207371f6dde7714b191801ca4a55046a5ccc6c3db349348b55fde84cae671640b292c5c90004d6b4801e8a1cd39c8367fbe1a9aca616d10a1cdb8bcdddac342c5dc146d625e93161b8806534ac1d21b5c0d99fa44159b11000f78953046944381170166b297dee03532d731c3ea55b6f746444a79bfc0b72acab08c3831d2293882de3ccd4595e22ba3b2cb0c7684060d13bab51073447a47de16fa3b452570c451bc90f365840b6155632aa50671972e76fcff0086de45b2a8d8450e00c9c1757605d1314ebb45dc09862f2e69946531cb3355a9484e77d67c4ca66b2d38e2f1d3bc3552e189c9e974db3158cab7d2e8166d5d2cf3da14366cf66ee58e4d3cc1b7ce63147911c5bfd660a146ae3a480e1b350259712e0d97b38840ac6986323676897149b3ccbdf647f1c438ec153fbe235528bc8f58b962bc102ed050f00b55cb9ff79e20a05d861a31c85ab0614b36388d9b9ef00aed9ccd701a41fceeb0cde08108b5775729216348ff007ccb5860c9157a16ef0afeb18601d2587118e155eabd1961eb0cc60cc1c8b75a806868e36d9bfda1e62556e7247786004b5e37d32d4c8c77cb72f80965ca9394c26ae05425eef40e0840c468b0b2e72ff8689c78cef8a3871c78a6909b7f12a8459a39836d5ca008d7796342e5db530f4e1283d88ad997a11eb0d4e5cba9fb7fe4bfd0387684c158a2d89990b5db9b962edb66374b08b04d622361f32a04b23011e95a866bb48841a46c6235154d38738ef09ed19ea5c288c9d6398d80eee0c0ee81b99cf7a7fde3063e58c534d94b605a691a485d1f173b4d3b9dc44376782b3d59bda577964b364155afde8c3c13bcb9b4d177894f46cd45710d3919c131955316bd6adbde2031040bbff28b50015c0f1fdf12f9586db07ce7bfd0966066ddab5777aafde6f1c1ae60eea5d5465a48291a5c390335d6aa3b4c675366dfe27382eb506b222ecc0335c4fd982b3a85d3309435421abf40971f7424800c247685ff0071da1e86bc294040a11de9b70742022b822dbb8cb30881814f46337ce59856e219f647b45588c329b2ab30b0b9e158be5e1922943844c54e56f845b20038e03a2eff00dfbcd9235819b96c25b9b62c25ee296291252a411d29c47696aaa28b43d9545c9dd6af72d64439eb0b152c9ad4bd7ac5bf4544cd2841b950cc5af3a765e3550dd131b7a147d3d2a1431d2c554c08c3b30ef5bb799a909861166451d963fc79d8b9b09ab361070d50e168c4f0c6dc9190dbed2ea5cb9a5dc642f05c70622eaaa38041b1fc742d131c76861c250b0aef0b1b2f2c5cd3b06e2bfd812551ca15b6a13b978b6d030b2a0a45200832c0006494100020e02ad5bbff007d46ba1cd2cd436e5c3841bb878582946444d8c1f4841150e91d1a8b427e0550ba94c648695f5d660cdce1e89546e166a5175e52100d5116d666671cc2ba3092f4172e1564b6ea0f914fbbd6c6a08c582e838b4cf7855d438d628d070136d0889637355a454146554b0a2a8b01b2e8564cba41bc18298c30275864b5db04f312d2b312a464e16ec6c4a360046115a05b600a14b86cba7609a703845b64050a5c365d3b0420427641e0682588ab1620abb3bc2c18305885b5b477eac71b2a5d26a0dec2f35158601b1f7fc37441186adc42c6c97e087620b4b5cc54543f163a30700c9cc1dc48cc4a86e4993b5a8eea040482269377dbf21262901b5050550da56b2aa301020822a8604cd665b21a835322bd228a7998a371783bc7a0b3d212c64427635a98643a348ec52e1f6352be09a61f1305fb2302774439a865890a740397b40eec590038b25392ae2f75347eaee55de9e3addbaacd7859152aa95861a46edbb3108f2aca429ae5555e55973c5d4afbe0fac6436676fe0947d0ca9ac79c6b89675142455c5f683161d0cc28cec601b945c60ba8679260b6877980567292a5200368f8bc47b0976f696b95b80c297a3cfe00b40db3b2471c9f319a987b4128d3880d32605bde5d160229045004b1970487623b9b2a6b88b1221475acd39014b5ad323f90988916acc4ec021bcf108b9a8c2e5d532d106b4ec97c225c0acc63e7d2a70b4de223df5a7d16218e70c532b7bcce9af2c3fbfc44d510ef4a8334063138d15cf596d48693d822b68d022ded9083c813808359439097658e570bdaf24e397167eed476afd083f9ae616cb692df862fc26fa2600a607f6daa70c41b2f06e28db3387750f96a16662f81336af4b2475dabd589b2b035700f3774399575d595e65efb43424a6bbc75936b6de3d6a59b05ae3c410b44bfc5bc47704c875702d2ee879301f14f4f4260dc20bec160618226e1181480da9a74d2ebf2207545d001aca11b5f2370149464cc71576711c08eb92a3dccbaef2c02fa256cc1da28f20a8b5289d388b563b31e8a416b8a8b620a6b9577fdef014d94b0dc3594f7318861b4706c38978b23c3879ac444afe88414af56002828e84ce0c45020f74a122ef1706cea476d8cb0e20517036512890c51cfb7f58dc1b9b605b09ec8fa8ebbb80e5e08bc0b3841f848feb71a43da5281307f8588b16e11e35fe258a8e7920dddd06159835ff504c912a44e6235b42c90e7bce735d972d73066097986e96c6254552d604bd0882f50a16a85ac4f1312a512f1724b38832c45b297d10a975440082961194628cdb236fc8b44f790cd9723fc2520c231974641a0584025a2eaea5b330b20dd5ac76b697cca440e21a02ef1538185d916215a166e59c74089614ce4d454020d23029978a7110d482bc4040b886341cf98354550f6fbf3356313c246163793d3e9297d2e893a50518823c783d56bd05e6eb293ed889f7793f986b58bb9fda6608da153d5651c02893ed514015aefc134163904296b05ce487df16a2a747ac34e98540fb32d67bb1be8205f4dda663a0fd4e221a174c08b406fb730db4522a7cf98ed5585fa5f58819560e180ae2747a9d628185dc2db4d74991b8cf74cd0bbde25145621167a45d1b0b2d559b22ac7a8f69588d44e610bf4d3e994272a1aa5127072d18abba1273a710d062c06357468a003f23b33e8a89496c2321a42f1698d0cad48449856618a14bc732a8583a7994f435056a86c413b47c75ba8607655051151042c710e07552c704cc1f67961d79982b08934ff007f98eed04344b8c00d4170f189697d5d8cc0be509f2f31369c8adaf39942156bdd7fe7c798a569a4b2fb426be5974d5c410b34bc4b5aa9642fe5b38bd39fe9c0d913221f6889414696bcc01b3aaf910f029df2f99b6f305d2f68aadbde2ceaa5a9dd463e5c4dad5d0e4f2fda5c2dd4d2f762354ed6d1f01471548841a7da2f4e4e6a00ec23fa9de7084c7c0f72688ad1ebca227120a8c5fb206dc60ce4982a370f3950464bcebb45240d0bda2f15055a99f89421874fde5dee8b7728d7e1369b5d69d814345d0a28b2f64765138ea0a0e940a816e68d7e4c1920f09e816702d56d29cad1c788b982a2716ecdca860cec99d38620b0d5372b8a8be23315748b2838c9d3bcaeba86a10aa4c0cce34b45e3ac1d48f4635888f08c29bf34f98e0bf39453a15f8b3920f96a0482bccaaabc6b7df339540732b84af22a399bd8ca105d637b84fb9da879c5e7ec90e87e407cd473b17ab1f5cdb5cc7e11caf7b21b81e5088acf2060e5a22ef69abfb7d595fbb417f2fe0891336a597873178cc1ee48b4b1a07afa31c3ab35dc9997adcfb18f858bbf31d89459efd434a35c45aba1da6ea87ed144caee555d83ad4642a2f37a88146f60ccbe97eb57141b4b494c0738b144e19d49e0ca0300341c7e4e1da4e026c44d23ccd95a515090a280563474dd0c42733d90476d75839657796e2cbd2ddb994c0ea2b2c3a42dc660e4386602565996c462fd587116f9539879f452b5830b5306ea58cd9de05c0f86121f3e4b401959aac28298ab716ce297598f1af48fef017256d04eec5e041466af43e0b970465bf131933d618f24672e2042ed6caaa58a91f06dfa4ec21e27d8f47d5854abdf495305a5e3d0de40364a54218798bd8f8822e158f9837c530055ef17fce259b36f12e438954bd216c7bcc25d92b1832e3be96ec94a0ca88a2da1968daa85ca1cb06d1155d5fca4e841c00d889a479815503b695a50118a357a2ad092482f501ab871be0622aab139ca63d65cc051758632a4e5d4ba81963329cf937d25cf9a6b4f923eeb8a55ef503128f4655d23114bf6089371e011576c7c72ddb71c81200234d7308101492af54196dc22c09e5a7d23b2eba88f0554f68b4ba17cc001d31294df2a0edd86d69dd36ec7ccc97ee81f406081573cb16a2dfa9d8ea0307925a27487b863145d7a3c4c114adb7f55cd9ba6727a9dbd16e04b9594ef1d50512e71a7e20b4d2768e98c719263539811440c88548d4720861865ccfaf6fdaa28151458a19928c03d5b1528bd825945145dafe595eee04da2d0b480b99710d0dc4bc18b5ab42ec2e0595cb698a150a398583537188e1c8174434ae354cc850cb501f15f240478067d05241a6cb2254bc4bf5f717fc46c7fb8f7e601555c0efa07598bebf43e4b8bc8abd6b2fac16f37ad1f231f316382f2af76e2d95f797896f0830c2ce809c1d3460bbf43b7cccd700341447f0b0ceb287b443b6f5883f5186400074856fb61eb6906c664611fc5d9f449b29c90ef5cc439b3a714fe62db96e09e112961945788cc2c135138c19ae23d7f1408b557001cb2dabf65ef5559555d17614336aa6dea5ad1ab168282f07e618500d0c150694305603415a3121db14a852af15c99c65c15df2b1aa4dc53a6d335685f1b6d5bb282eeab0740bb62e2d5478dacd63dccf88027103373f32cd509a0d0b2c2f39131d23e454b030b9ae98802f6381500ca19c970e6a9e48ecc0b8ad2943300238ca0a5298b2e98340160ceb81e8ea061b21058a5c1d0e0f894a9d6edfd7de6d0e497317ec15ab41093a9f23abec7a2d45bf532c91ed5c137d2f8c3d14b5f18f30b4a39438b194b183ee23ee389884497285de4bfb40b6047140c0a85675631bb06b7cebd781958de37f48421284991bc458a196000d2183f0962b4d0b5343740d0b5ac12cc965f78052e45b607c272ca11104a35bcb22b9abaa0fce45fa5c6bd2c4f1d2267b7d2b863279263406cdf4b735de250b95e3834f045a8f2422140b434a1df43d17f0e464b13503e9763c409fc702df6ca9325838466b2853f11e476889d02d577cd7aa07e61b72de88b11f458b064565038c6bbc7208b536ac5a8b704cb04621209e062eae5869bcaef34a40e5f56350c8bd2a956651bc52a57c825c651016b8b2fa912d6f15266815d03d19ce2f38ad9a00d4574ba6735c460e51482cd565c972a9a4a42b348f64590da734efa0707e78db81d0850260378cbb5cb96623d02fd42a014652fdb99a99693588fd08b314ca4b7d908cc9b32afa062f0061cd5ac07d5e2e007bdd794227b10532a9faa39d9dc62d45b95bd7ff22fd46300a9823407d88503666d3599bbc436c99f451a2e8ac797359d47a6a0a5bca5a8f899ff007823ede08b37e8e89416f0a38079f9af9fd0461c9bb2b6edb0a71d29559bb080330859e815e9612b603962b2ba4bb1d05f786ab2336847d66895a30654eb9f4cbe496a983e520f461b93dafd8382111124225226e502cc0516c16780bee7a02d12e11a0e44d9efd5e8050b8c188a9132c9ee1c62258d7a5e0ed67a2fe079948a22735321ee9fa089ab820d058bb0aa9a2f93580b9b54c14815e810db0201a7a9923fa5e48f641daba8ac795cbd5c88f59403b9e23a953285efe806299138d3ecb7ed0412044a9fb25801400c0eabf71fafa5904984e43d21856186254046e5a7f722e88e7baf429f597aa39043455b5576beb633430155c3f856580599b458ca1fbf56ac6bf40900949d88468c1c3142af14e9373d112d1e088c609e89d673c5e6002223913995aa05b9c187b972a88e4788a871af1167bccbbb676d6239a1adc02abc09f6897c008588e981b17193196001b57820e4215284b49dd5e123177eb71f4282d56c1d78d6bafe0336ce482ad504ccb94b317855bfa8beff00a04ceae46da0164012c6d0a72d419acdc7b2ee5410c06b68112ca9950c1bb4e1846d341e465c23876d844c28d618be72302ba80c14bbc4a29c395250c715014d875c06f10021bd2925ef6d63792cfdf35cf3aa4e4157afe05afc2c821e418ea618e65ab393afa1b515769a1329842ae2dc98f039ff009fa04d031d40362269215778545264c8597a6a92930902a04d38b8d679efe815090d02afc0e9104097d5ce1cb38e92deb3b12c6beb05fe4c7989338721e8841a46c610725addcb1cddc14153032f12ab5f4b97f8162dfa40870d65c4ba852a05ef4c849858d32836e58b936a707eff003dbf410b4238148a4d85075be78194e1f4474dc6a72b82ac6d0b8cb770047c4a9d2e68f781025d2b18fef0204d398b2f508708ea57a3f32c489e56e641b65afa59e276221c46fd75236b40666baa2b28c71307a64a053c99212719aa2f065f7abafd0543be0520d414a1cad8d0ce20127240a86d4106aad8f8c2e8db059828013971a67fec0a94260cbe6040a9a1ed6f796dec8ea229514c1716cf86125388767a9c5098ae308710535e8ca6c111e2000a1eb0c2f0dfa7b1caf18086c5cb83960fd05bb9d01c7646c505e6b0ea547026e695058426dfb286c168a49467da595499a2e641eb021ec558e6d648826ec91d6037134a10ed8094128e92e16e89921a898be8892f75a95350f48ff7a7a043ba3216ce84282df407e619059cd97463d79a5bb4c0168ef514afe83b87a8104d1470d31d3102a05ca940aa40882363c9280942af370fad564c4480cfab251c069fda58e50c01d58fa242788108441ac413e91298b54ac3946b62640470d4b2ef01da88af7817531770d6d3a0d30a3bdd7c403f6c1163000651d5958f1ff6282a57231a036e23cc5ccaab27e83c9118af758e002d4c5f0e42fd02a0a6e26680aa2fe65b58dccd994cd9301840079f0248649444ad883e86308604b2004017378617b581b614836988b729af295e08b8b97413d8e63a89c89777371df2d3d0701e8ca33442f1b628b162b20b63ef9feec14c46081949f7609aaf72e678aae8db068dbb1413752405a194f7acf17fa0f9352ff50f4553b05c82c50c1fecc2f44aa24e63e640b8105340c05884e8110ac408624a5d10c1b17274ff00a089188df72e32d4ba900800d4eba58ce7f1d626084177312244653e2c67de534fe20da6b963592b27d65d928784219bf088c105d9d30f970201d448aaac30075580a8eb4ba70e69588a184d0c5fa228778a28d1553742fb557d7f420fc0031682300a257098352b1e9c3153cf1048db5d7385d9e20545a51699964e9fc23a4e412563ad09541330a015de5a391fb42712f578d9f465d732d6158a63acaa5c0ebc12d0097783caa43dfaf5aceac6c359ef2c1861a8688f8b80a54e9da10a034fd9281db005946d5f67eb03da2ab661db7bbb6e13fbd075c67306ae281f48c7fec769376423ec9a9403654397017e6a2394da73fa108100a714a23a048d8c812c3bb805978a847360b8bae195c72e33965179aace2275278c056e0f898674ee08d9876969498788c768551dc7f88feb4fc63755dbd8dc14b64f90d423f862f431b6078942907e73e1750a0ddb573357cfb056fba43e9e8ca0a482dab02015b3062fc9c76877d573e629dacbba5806a2b76f332ef668c3004aba1de5e4d96979b69d269f3fa1409558008887445314e708e6710085a7ee236239111c90299d673059d2e19a8d75a881b5e18ab686538854391c444739ba7848005a3a5e6291afda53fb30892c4b1f566a777b060be73aff009083ed847f0c3a66962f38d34d7bd4a37c0413d28ddf39106904689b7b57cbb880c379fdc5700bacb0af30057bb258779023f942bde65a714dcb642e0ed7f895d88cd9f13e87de98e5956a5abd7f4364f6d4b4e62f194638b3a203a8ca605c4164662320a731e9152b98462c6c4d9138318e8f680ab1a487eecbf5a7fecccd78fdb88b0366a533f1e8cd235408040080e419c64eee02fe65a902b1a703e03d33399ffa0936c74d887060e1394460a6e900d9c996e5423e2ae7c5a7edfb62346ae56b3747433afd0e13fcd026c44c88f312dc8a1bb0d6ad6d58b555a329af328c5da70660c691db2f4ab2235946c712b059d2814aee8998d971c1e658881b3a5977f48a164d0ea4a9bccb7083ccb850e78ec1ca2d349d332b24800c4a3969d0f923951b843d1681f31237380516c516d3ad1eb99370340d99f4c02710eb62429161caf69b00cd3a055f42239e357125aad62dda72bcfed83cfe8ac97e5eb3b5814aac765be0a519af854055dd596007c45b61cdc5ce6735018728ea2885182ad060296432042d771843a1634c59e2fa4c65bbcdf58f436c454908014fa5d44b98c969880ac712d6f1a8fa568977deb07bc1578986879d7082597aed9ce4a21db73981ad06800028afd176cdbe8201c669c2ca5bd6487061d2ec983d7c86a1a2f6466222749c266bc4218e07f11b92f9831a1c3c42e61f4e8bf98350d600aaa6059ca83bfe4472ff0096ebf88270110e6e2dd6267ba09ed1f9fed6a185929402b7c0f76e207ab48f28ecc0507350be5db04979af9952cd2ef247b641b0d25a58082c0297e8b807b03292c790a195828ed6aa40e5c665645433132fa2625a08d5c125b37322179088214653221d2afdd2dbfb406e3d18205331b55374298d7d62f8c8094dbde36020ae171a59f002dfb4af7a029fcca8ee08cbbd18f784cb08b8ba5687de654af7a04766b730632843d8313006de08c2464e253ab9399bc4a97323a74b66963dd15fa2f4316a41a8b176154359e4d60a7aa506e0f301168958b87916c6433f0c1c19cb8c6576d302a04c173f32e7ae219af332b326a78400306a8cc5c56734f9331c257f038c4548e74b7e8c4c0be942ff001b9920d971896139be66d08da5588e552a0332183ae6e209276f585d4facf972cfe83f45100a8cd7512e8d0e4e725618198c3d61452d7585d0d76883239ccd1103aa84201f24c2af740d3b148c8fd22a2c734009476d3c3fb4be279bd1120dc2cae1be18ce6ab9c712fd579eb50c1205d830ac73e61a14b4965ea8ca510aaa91b78a9dbf1e6559ba8c45ef116b0e83fa64442f0ad8ff0045a8c402094ade8a814838588a057e25e1af88abb774980d2a144234f1069289370d8b7f873797dc15ff00b1372c6707d6674c0e798d53b5d891da281de076848695705846e026acddce2c23822d461a8d6941b19419a85d43de01af5c1318bc6c3fa2b1d6285312911a430950fc8186b40d2bb16a8002c6345858a86e16b97dc93ad4c8d06ed0fef32ff52802bde887296d29ad0fdcf45c7a3a8cd9175a94d928318f885bb1c585cce20bef8bb80f38738c66016064423b304c660b066e01dd973041ec978a852dee341aa7ace52007a2d0c0a1de4b44d3f45ae63d9d84b72652cbaa2802346001a95af5e22a0e734cc8cf152cad0c34bb1a2cc26269aa19afeb38c611d4592d85afc7c4ba02ef38e618f794baefe2240b9cd27d25d9bae906e48adb46d98df64104f7479a2f748ee0acc15d65e9488e91dd3d5b867c214dbdcb0505f1400a3f458ad1bf105d82d4268581722dcece46f9ebcf98954eb70c58a6e60a1dd96f4a75491576decdfda66b67de1a620bc127897bdcba9903be9550584be4986d3c406502763ec4779405638831c7e2351a2aa0148966c8a4c3cff788c905869e267890ab52505683805c6213f9eaca818508a36dfe8d64b8b477139c160a951abb0f9950a33da355141f30167bd4716d31d11f50c4da24a2018d2da9fda99853dad95870618150e96408da78c3352c6f1a9a4e08bb0710d469eb016768e5416ef8000a16016acd7e8e37072ed600bd6e802c44b4a304049404b11a4139251a0f05cbca2ae2eb5f5864c2dea5b08d7c4bc1bf7950d6392528a791d46962bc5cb2eaccb96d7b2c242e0852ec90ea6af7e22db17f58caec878e0c2ae202d69fb30aa8938ba0cec3010b40b761fa3a822b56140b278166a016ae20a5cb16a5a27ed15259784ed2b285933548fcf129a53fbc6bcaaf336b76c658978c6b843164acf48257bb3009027351c36bce3304a609b80d0241381f64a2821a9579aae81794a319575fa3c3f115cad387088a284451114887e29ab0d0eb09440038040688456b0c2c85b0d545f79a143bdccf50e3304c35d8620b99b5e9d425c82d6fac3420355cc0acf745496dee0b1771b1c468071538c4add81400485c61db450e900502a02dd0507e90cb0b962282ed2134a305b2c3a27a06f40b5619b5626314e9300dd1181639802d0f11d673554cb358b589e8a8bdbd514ab423dd28157955416805a87948da068a50b2e7208376450a7b727a03100501838fd27ad025562650b68c611044021535a3d564d16b4348bb22473825667116867de6cda965ae7d6e34223ac040d2055a180015583863aab605112ed0ee5165d0f4a88996bd8d5c403cd14983c2ea25a06907c87e97c650cfbb6268c4a6d32b24120ca9ad52968dabadd9269b6b0aee49a156b0039a8b9d46a101de5a12b9ba0a1467a8da857658e0b8b00e92b5338ae7230b9a52cb5229b0c91ea4410660b53b2e28c505356e5ca602296058ce6266e1a927a10a70037064b005d80b16151530e023e0692ab6a3955555ff00f6cbffd9 \ No newline at end of file diff --git a/Resources/libreoffice-writer-password.pdf b/Resources/libreoffice-writer-password.pdf new file mode 100644 index 0000000000..de3e0c1609 Binary files /dev/null and b/Resources/libreoffice-writer-password.pdf differ diff --git a/Resources/metadata.pdf b/Resources/metadata.pdf new file mode 100644 index 0000000000..a69369d74e Binary files /dev/null and b/Resources/metadata.pdf differ diff --git a/Resources/pdflatex-forms.pdf b/Resources/pdflatex-forms.pdf new file mode 100644 index 0000000000..a2d0dcab92 Binary files /dev/null and b/Resources/pdflatex-forms.pdf differ diff --git a/Resources/pdflatex-outline.pdf b/Resources/pdflatex-outline.pdf new file mode 100644 index 0000000000..a27ca26451 Binary files /dev/null and b/Resources/pdflatex-outline.pdf differ diff --git a/Resources/reportlab-inline-image.pdf b/Resources/reportlab-inline-image.pdf new file mode 100644 index 0000000000..e38695c590 Binary files /dev/null and b/Resources/reportlab-inline-image.pdf differ diff --git a/Resources/side-by-side-subfig.pdf b/Resources/side-by-side-subfig.pdf new file mode 100644 index 0000000000..56ba399e49 Binary files /dev/null and b/Resources/side-by-side-subfig.pdf differ diff --git a/Sample_Code/README.txt b/Sample_Code/README.txt deleted file mode 100644 index ea0430c8a7..0000000000 --- a/Sample_Code/README.txt +++ /dev/null @@ -1,14 +0,0 @@ -PyPDF2 Sample Code Folder -------------------------- - -This will contain demonstrations of the many features -PyPDF2 is capable of. Example code should make it easy -for users to know how to use all aspects of PyPDF2. - - - -Feel free to add any type of PDF file or sample code, -either by - - 1) sending it via email to PyPDF2@phaseit.net - 2) including it in a pull request on GitHub \ No newline at end of file diff --git a/Sample_Code/basic_features.py b/Sample_Code/basic_features.py deleted file mode 100644 index 5e874970da..0000000000 --- a/Sample_Code/basic_features.py +++ /dev/null @@ -1,45 +0,0 @@ -from PyPDF2 import PdfFileWriter, PdfFileReader - -output = PdfFileWriter() -input1 = PdfFileReader(open("document1.pdf", "rb")) - -# print how many pages input1 has: -print "document1.pdf has %d pages." % input1.getNumPages() - -# add page 1 from input1 to output document, unchanged -output.addPage(input1.getPage(0)) - -# add page 2 from input1, but rotated clockwise 90 degrees -output.addPage(input1.getPage(1).rotateClockwise(90)) - -# add page 3 from input1, rotated the other way: -output.addPage(input1.getPage(2).rotateCounterClockwise(90)) -# alt: output.addPage(input1.getPage(2).rotateClockwise(270)) - -# add page 4 from input1, but first add a watermark from another PDF: -page4 = input1.getPage(3) -watermark = PdfFileReader(open("watermark.pdf", "rb")) -page4.mergePage(watermark.getPage(0)) -output.addPage(page4) - - -# add page 5 from input1, but crop it to half size: -page5 = input1.getPage(4) -page5.mediaBox.upperRight = ( - page5.mediaBox.getUpperRight_x() / 2, - page5.mediaBox.getUpperRight_y() / 2 -) -output.addPage(page5) - -# add some Javascript to launch the print window on opening this PDF. -# the password dialog may prevent the print dialog from being shown, -# comment the the encription lines, if that's the case, to try this out -output.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") - -# encrypt your new PDF and add a password -password = "secret" -output.encrypt(password) - -# finally, write "output" to document-output.pdf -outputStream = file("PyPDF2-output.pdf", "wb") -output.write(outputStream) diff --git a/Sample_Code/basic_merging.py b/Sample_Code/basic_merging.py deleted file mode 100644 index 86562effc4..0000000000 --- a/Sample_Code/basic_merging.py +++ /dev/null @@ -1,20 +0,0 @@ -from PyPDF2 import PdfFileMerger - -merger = PdfFileMerger() - -input1 = open("document1.pdf", "rb") -input2 = open("document2.pdf", "rb") -input3 = open("document3.pdf", "rb") - -# add the first 3 pages of input1 document to output -merger.append(fileobj = input1, pages = (0,3)) - -# insert the first page of input2 into the output beginning after the second page -merger.merge(position = 2, fileobj = input2, pages = (0,1)) - -# append entire input3 document to the end of the output document -merger.append(input3) - -# Write to an output PDF document -output = open("document-output.pdf", "wb") -merger.write(output) diff --git a/Sample_Code/hierarchical_toc.md b/Sample_Code/hierarchical_toc.md new file mode 100644 index 0000000000..e53ee6bc4c --- /dev/null +++ b/Sample_Code/hierarchical_toc.md @@ -0,0 +1,3 @@ +# Example of hierarchical TOC from several pdf files + +https://github.com/vb64/pdftoc diff --git a/Sample_Code/makesimple.py b/Sample_Code/makesimple.py deleted file mode 100755 index a05ac7c941..0000000000 --- a/Sample_Code/makesimple.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python -"Make some simple multipage pdf files." - -from __future__ import print_function -from sys import argv - -from reportlab.pdfgen import canvas - -point = 1 -inch = 72 - -TEXT = """%s page %d of %d - -a wonderful file -created with Sample_Code/makesimple.py""" - - -def make_pdf_file(output_filename, np): - title = output_filename - c = canvas.Canvas(output_filename, pagesize=(8.5 * inch, 11 * inch)) - c.setStrokeColorRGB(0,0,0) - c.setFillColorRGB(0,0,0) - c.setFont("Helvetica", 12 * point) - for pn in range(1, np + 1): - v = 10 * inch - for subtline in (TEXT % (output_filename, pn, np)).split( '\n' ): - c.drawString( 1 * inch, v, subtline ) - v -= 12 * point - c.showPage() - c.save() - -if __name__ == "__main__": - nps = [None, 5, 11, 17] - for i, np in enumerate(nps): - if np: - filename = "simple%d.pdf" % i - make_pdf_file(filename, np) - print ("Wrote", filename) diff --git a/Sample_Code/makesimple.sh b/Sample_Code/makesimple.sh deleted file mode 100755 index c0407a1271..0000000000 --- a/Sample_Code/makesimple.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/sh - -n=1 -for np in 5 11 17; do - p=1 - f=simple$n.pdf - while expr $p \<= $np > /dev/null; do - if [ $p != 1 ]; then - echo " \c" - fi - echo "$f page $p of $np" - echo "" - echo "an incredible, yet simple example" - echo "Created with Sample_Code/makesimple.sh" - p=$(expr $p + 1) - done | enscript --no-header -o - |ps2pdf - $f - echo $f - n=$(expr $n + 1) - done \ No newline at end of file diff --git a/Scripts/2-up.py b/Scripts/2-up.py index 41e2b2a424..2540e01147 100644 --- a/Scripts/2-up.py +++ b/Scripts/2-up.py @@ -1,54 +1,36 @@ -from PyPDF2 import PdfFileWriter, PdfFileReader -import sys -import math - +""" +Create a booklet-style PDF from a single input. -def main(): - if (len(sys.argv) != 3): - print("usage: python 2-up.py input_file output_file") - sys.exit(1) - print ("2-up input " + sys.argv[1]) - input1 = PdfFileReader(open(sys.argv[1], "rb")) - output = PdfFileWriter() - for iter in range (0, input1.getNumPages()-1, 2): - lhs = input1.getPage(iter) - rhs = input1.getPage(iter+1) - lhs.mergeTranslatedPage(rhs, lhs.mediaBox.getUpperRight_x(),0, True) - output.addPage(lhs) - print (str(iter) + " "), - sys.stdout.flush() +Pairs of two pages will be put on one page (left and right) - print("writing " + sys.argv[2]) - outputStream = file(sys.argv[2], "wb") - output.write(outputStream) - print("done.") +usage: python 2-up.py input_file output_file +""" -if __name__ == "__main__": - main() -from PyPDF2 import PdfFileWriter, PdfFileReader import sys -import math + +from PyPDF2 import PdfFileReader, PdfFileWriter def main(): - if (len(sys.argv) != 3): + if len(sys.argv) != 3: print("usage: python 2-up.py input_file output_file") sys.exit(1) - print ("2-up input " + sys.argv[1]) - input1 = PdfFileReader(open(sys.argv[1], "rb")) - output = PdfFileWriter() - for iter in range (0, input1.getNumPages()-1, 2): - lhs = input1.getPage(iter) - rhs = input1.getPage(iter+1) - lhs.mergeTranslatedPage(rhs, lhs.mediaBox.getUpperRight_x(),0, True) - output.addPage(lhs) - print (str(iter) + " "), + print("2-up input " + sys.argv[1]) + reader = PdfFileReader(sys.argv[1]) + writer = PdfFileWriter() + for iter in range(0, reader.getNumPages() - 1, 2): + lhs = reader.getPage(iter) + rhs = reader.getPage(iter + 1) + lhs.mergeTranslatedPage(rhs, lhs.mediaBox.getUpperRight_x(), 0, True) + writer.addPage(lhs) + print(str(iter) + " "), sys.stdout.flush() print("writing " + sys.argv[2]) - outputStream = open(sys.argv[2], "wb") - output.write(outputStream) + with open(sys.argv[2], "wb") as fp: + writer.write(fp) print("done.") + if __name__ == "__main__": main() diff --git a/Scripts/booklet.py b/Scripts/booklet.py new file mode 100644 index 0000000000..9eeac3de73 --- /dev/null +++ b/Scripts/booklet.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python + +""" +Layout the pages from a PDF file to print a booklet or brochure. + +The resulting media size is twice the size of the first page +of the source document. If you print the resulting PDF in duplex +(short edge), you get a center fold brochure that you can staple +together and read as a booklet. +""" + +from __future__ import division, print_function +import PyPDF2 +import argparse + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument("input", type=argparse.FileType("rb")) + parser.add_argument("output") + args = parser.parse_args() + + reader = PyPDF2.PdfFileReader(args.input) + numPages = reader.getNumPages() + print("Pages in file:", numPages) + + pagesPerSheet = 4 + virtualPages = (numPages + pagesPerSheet - 1) // pagesPerSheet * pagesPerSheet + + firstPage = reader.getPage(0) + mb = firstPage.mediaBox + pageWidth = 2 * mb.getWidth() + pageHeight = mb.getHeight() + print("Medium size:", "{}x{}".format(pageWidth, pageHeight)) + + writer = PyPDF2.PdfFileWriter() + + def scale(page): + return min( + mb.getWidth() / page.mediaBox.getWidth(), + mb.getHeight() / page.mediaBox.getHeight(), + ) + + def mergePage(dst, src, xOffset): + pageScale = scale(src) + print("scaling by", pageScale) + dx = (mb.getWidth() - pageScale * src.mediaBox.getWidth()) / 2 + dy = (mb.getHeight() - pageScale * src.mediaBox.getHeight()) / 2 + dst.mergeScaledTranslatedPage(src, scale(src), xOffset + dx, dy) + + def mergePageByNumber(dstPage, pageNumber, xOffset): + if pageNumber >= numPages: + return + print("merging page", pageNumber, "with offset", xOffset) + page = reader.getPage(pageNumber) + mergePage(dstPage, page, xOffset) + + for i in range(virtualPages // 2): + page = writer.addBlankPage(width=pageWidth, height=pageHeight) + offsets = [0, pageWidth // 2] + if i % 2 == 0: + offsets.reverse() + mergePageByNumber(page, i, offsets[0]) + mergePageByNumber(page, virtualPages - i - 1, offsets[1]) + + with open(args.output, "wb") as fp: + writer.write(fp) + + +if __name__ == "__main__": + main() diff --git a/Scripts/pdf-image-extractor.py b/Scripts/pdf-image-extractor.py new file mode 100644 index 0000000000..cc935aad4b --- /dev/null +++ b/Scripts/pdf-image-extractor.py @@ -0,0 +1,37 @@ +""" +Extract images from PDF without resampling or altering. + +Adapted from work by Sylvain Pelissier +http://stackoverflow.com/questions/2693820/extract-images-from-pdf-without-resampling-in-python +""" + +import sys +import PyPDF2 +from PyPDF2.filters import _xobj_to_image +from PyPDF2.constants import PageAttributes as PG, ImageAttributes as IA, Ressources as RES + +def main(pdf: str): + reader = PyPDF2.PdfFileReader(pdf) + page = reader.pages[30] + + if RES.XOBJECT in page[PG.RESOURCES]: + xObject = page[PG.RESOURCES][RES.XOBJECT].getObject() + + for obj in xObject: + if xObject[obj][IA.SUBTYPE] == "/Image": + extension, byte_stream = _xobj_to_image(xObject[obj]) + if extension is not None: + filename = obj[1:] + ".png" + with open(filename, "wb") as img: + img.write(byte_stream) + else: + print("No image found.") + + +if __name__ == "__main__": + if len(sys.argv) != 2: + print("\nUsage: python {} input_file\n".format(sys.argv[0])) + sys.exit(1) + + pdf = sys.argv[1] + main(pdf) diff --git a/Scripts/pdfcat b/Scripts/pdfcat index dfb67f8792..b98607a4f5 100644 --- a/Scripts/pdfcat +++ b/Scripts/pdfcat @@ -20,7 +20,7 @@ EXAMPLES """ # Copyright (c) 2014, Steve Witham . # All rights reserved. This software is available under a BSD license; -# see https://github.com/mstamy2/PyPDF2/LICENSE +# see https://github.com/py-pdf/PyPDF2/LICENSE from __future__ import print_function import argparse diff --git a/Tests/test_basic_features.py b/Tests/test_basic_features.py new file mode 100644 index 0000000000..0c9e9257f7 --- /dev/null +++ b/Tests/test_basic_features.py @@ -0,0 +1,67 @@ +import os + +import pytest + +from PyPDF2 import PdfFileReader, PdfFileWriter +from PyPDF2.pdf import convertToInt +from PyPDF2.utils import PdfReadError + +TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "Resources") + + +def test_basic_features(): + pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + reader = PdfFileReader(pdf_path) + writer = PdfFileWriter() + + # print how many pages input1 has: + print("document1.pdf has %d pages." % reader.getNumPages()) + + # add page 1 from input1 to output document, unchanged + writer.addPage(reader.getPage(0)) + + # add page 2 from input1, but rotated clockwise 90 degrees + writer.addPage(reader.getPage(0).rotateClockwise(90)) + + # add page 3 from input1, rotated the other way: + writer.addPage(reader.getPage(0).rotateCounterClockwise(90)) + # alt: output.addPage(input1.getPage(0).rotateClockwise(270)) + + # add page 4 from input1, but first add a watermark from another PDF: + page4 = reader.getPage(0) + watermark_pdf = pdf_path + watermark = PdfFileReader(watermark_pdf) + page4.mergePage(watermark.getPage(0)) + writer.addPage(page4) + + # add page 5 from input1, but crop it to half size: + page5 = reader.getPage(0) + page5.mediaBox.upperRight = ( + page5.mediaBox.getUpperRight_x() / 2, + page5.mediaBox.getUpperRight_y() / 2, + ) + writer.addPage(page5) + + # add some Javascript to launch the print window on opening this PDF. + # the password dialog may prevent the print dialog from being shown, + # comment the the encription lines, if that's the case, to try this out + writer.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") + + # encrypt your new PDF and add a password + password = "secret" + writer.encrypt(password) + + # finally, write "output" to PyPDF2-output.pdf + tmp_path = "PyPDF2-output.pdf" + with open(tmp_path, "wb") as output_stream: + writer.write(output_stream) + + # cleanup + os.remove(tmp_path) + + +def test_convertToInt(): + with pytest.raises(PdfReadError): + convertToInt(256, 16) diff --git a/Tests/test_constants.py b/Tests/test_constants.py new file mode 100644 index 0000000000..fa34357c55 --- /dev/null +++ b/Tests/test_constants.py @@ -0,0 +1,15 @@ +import re + +from PyPDF2.constants import PDF_KEYS + + +def test_slash_prefix(): + pattern = re.compile(r"^\/[A-Z]+[a-zA-Z0-9]*$") + for cls in PDF_KEYS: + for attr in dir(cls): + if attr.startswith("__") and attr.endswith("__"): + continue + constant_value = getattr(cls, attr) + assert constant_value.startswith("/") + assert pattern.match(constant_value) + assert attr.replace("_", "").lower() == constant_value[1:].lower() diff --git a/Tests/test_javascript.py b/Tests/test_javascript.py new file mode 100644 index 0000000000..48ad9530e8 --- /dev/null +++ b/Tests/test_javascript.py @@ -0,0 +1,50 @@ +import os + +import pytest + +from PyPDF2 import PdfFileReader, PdfFileWriter + +# Configure path environment +TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "Resources") + + +@pytest.fixture +def pdf_file_writer(): + reader = PdfFileReader(os.path.join(RESOURCE_ROOT, "crazyones.pdf")) + writer = PdfFileWriter() + writer.appendPagesFromReader(reader) + yield writer + + +def test_add_js(pdf_file_writer): + pdf_file_writer.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") + + assert ( + "/Names" in pdf_file_writer._root_object + ), "addJS should add a name catalog in the root object." + assert ( + "/JavaScript" in pdf_file_writer._root_object["/Names"] + ), "addJS should add a JavaScript name tree under the name catalog." + assert ( + "/OpenAction" in pdf_file_writer._root_object + ), "addJS should add an OpenAction to the catalog." + + +def test_overwrite_js(pdf_file_writer): + def get_javascript_name(): + assert "/Names" in pdf_file_writer._root_object + assert "/JavaScript" in pdf_file_writer._root_object["/Names"] + assert "/Names" in pdf_file_writer._root_object["/Names"]["/JavaScript"] + return pdf_file_writer._root_object["/Names"]["/JavaScript"]["/Names"][0] + + pdf_file_writer.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") + first_js = get_javascript_name() + + pdf_file_writer.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") + second_js = get_javascript_name() + + assert ( + first_js != second_js + ), "addJS should overwrite the previous script in the catalog." diff --git a/Tests/test_merger.py b/Tests/test_merger.py new file mode 100644 index 0000000000..072fbcb145 --- /dev/null +++ b/Tests/test_merger.py @@ -0,0 +1,51 @@ +import os +import sys + +import PyPDF2 + +TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "Resources") + +sys.path.append(PROJECT_ROOT) + + +def test_merge(): + pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + outline = os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf") + pdf_forms = os.path.join(RESOURCE_ROOT, "pdflatex-forms.pdf") + pdf_pw = os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf") + + file_merger = PyPDF2.PdfFileMerger() + + # string path: + file_merger.append(pdf_path) + file_merger.append(outline) + file_merger.append(pdf_path, pages=PyPDF2.pagerange.PageRange(slice(0, 0))) + file_merger.append(pdf_forms) + + # Merging an encrypted file + pdfr = PyPDF2.PdfFileReader(pdf_pw) + pdfr.decrypt("openpassword") + file_merger.append(pdfr) + + # PdfFileReader object: + file_merger.append(PyPDF2.PdfFileReader(pdf_path, "rb"), bookmark=True) + + # File handle + with open(pdf_path, "rb") as fh: + file_merger.append(fh) + + bookmark = file_merger.addBookmark("A bookmark", 0) + file_merger.addBookmark("deeper", 0, parent=bookmark) + file_merger.addMetadata({"author": "Martin Thoma"}) + file_merger.addNamedDestination("title", 0) + file_merger.setPageLayout("/SinglePage") + file_merger.setPageMode("/UseThumbs") + + tmp_path = "dont_commit_merged.pdf" + file_merger.write(tmp_path) + file_merger.close() + + # Clean up + os.remove(tmp_path) diff --git a/Tests/test_page.py b/Tests/test_page.py new file mode 100644 index 0000000000..c5ea986622 --- /dev/null +++ b/Tests/test_page.py @@ -0,0 +1,69 @@ +import os + +import pytest + +from PyPDF2 import PdfFileReader + +TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "Resources") + + +@pytest.mark.parametrize( + "pdf_path, password", + [ + ("crazyones.pdf", None), + ("attachment.pdf", None), + # ("side-by-side-subfig.pdf", None), + ( + "libreoffice-writer-password.pdf", + "openpassword", + ), + ("imagemagick-images.pdf", None), + ("imagemagick-lzw.pdf", None), + ("reportlab-inline-image.pdf", None), + ], +) +def test_page_operations(pdf_path, password): + """ + This test just checks if the operation throws an exception. + + This should be done way more thoroughly: It should be checked if the + output is as expected. + """ + pdf_path = os.path.join(RESOURCE_ROOT, pdf_path) + reader = PdfFileReader(pdf_path) + + if password: + reader.decrypt(password) + + page = reader.pages[0] + page.mergeRotatedScaledPage(page, 90, 1, 1) + page.mergeScaledTranslatedPage(page, 1, 1, 1) + page.mergeRotatedScaledTranslatedPage(page, 90, 1, 1, 1, 1) + page.addTransformation([1, 0, 0, 0, 0, 0]) + page.scale(2, 2) + page.scaleBy(0.5) + page.scaleTo(100, 100) + page.compressContentStreams() + page.extractText() + + +@pytest.mark.parametrize( + "pdf_path, password", + [ + (os.path.join(RESOURCE_ROOT, "crazyones.pdf"), None), + (os.path.join(RESOURCE_ROOT, "attachment.pdf"), None), + (os.path.join(RESOURCE_ROOT, "side-by-side-subfig.pdf"), None), + ( + os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf"), + "openpassword", + ), + ], +) +def test_compress_content_streams(pdf_path, password): + reader = PdfFileReader(pdf_path) + if password: + reader.decrypt(password) + for page in reader.pages: + page.compressContentStreams() diff --git a/Tests/test_pagerange.py b/Tests/test_pagerange.py new file mode 100644 index 0000000000..dd3c97954f --- /dev/null +++ b/Tests/test_pagerange.py @@ -0,0 +1,68 @@ +import pytest + +from PyPDF2.pagerange import PageRange, ParseError, parse_filename_page_ranges + + +def test_equality(): + pr1 = PageRange(slice(0, 5)) + pr2 = PageRange(slice(0, 5)) + assert pr1 == pr2 + + +def test_equality_other_objectc(): + pr1 = PageRange(slice(0, 5)) + pr2 = "PageRange(slice(0, 5))" + assert pr1 != pr2 + + +def test_idempotency(): + pr = PageRange(slice(0, 5)) + pr2 = PageRange(pr) + assert pr == pr2 + + +@pytest.mark.parametrize( + "range_str,expected", + [ + ("42", slice(42, 43)), + ("1:2", slice(1, 2)), + ], +) +def test_str_init(range_str, expected): + pr = PageRange(range_str) + assert pr._slice == expected + assert PageRange.valid + + +def test_str_init_error(): + init_str = "1-2" + assert PageRange.valid(init_str) is False + with pytest.raises(ParseError): + PageRange(init_str) + + +@pytest.mark.parametrize( + "params,expected", + [ + (["foo.pdf", "1:5"], [("foo.pdf", PageRange("1:5"))]), + ( + ["foo.pdf", "1:5", "bar.pdf"], + [("foo.pdf", PageRange("1:5")), ("bar.pdf", PageRange(":"))], + ), + ], +) +def test_parse_filename_page_ranges(params, expected): + assert parse_filename_page_ranges(params) == expected + + +def test_parse_filename_page_ranges_err(): + with pytest.raises(ValueError): + parse_filename_page_ranges(["1:5", "foo.pdf"]) + + +def test_page_range_help(): + from PyPDF2.pagerange import PAGE_RANGE_HELP + + assert len(PAGE_RANGE_HELP) > 20 + assert "0:3" in PAGE_RANGE_HELP + assert PAGE_RANGE_HELP.endswith("\n") diff --git a/Tests/test_reader.py b/Tests/test_reader.py new file mode 100644 index 0000000000..d7d88bb646 --- /dev/null +++ b/Tests/test_reader.py @@ -0,0 +1,228 @@ +import io +import os + +import pytest + +import PyPDF2.utils +from PyPDF2 import PdfFileReader +from PyPDF2.constants import ImageAttributes as IA +from PyPDF2.constants import PageAttributes as PG +from PyPDF2.constants import Ressources as RES +from PyPDF2.filters import _xobj_to_image + +TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "Resources") + + +@pytest.mark.parametrize( + "pdf_path, expected", + [ + ( + os.path.join(RESOURCE_ROOT, "crazyones.pdf"), + { + "/CreationDate": "D:20150604133406-06'00'", + "/Creator": " XeTeX output 2015.06.04:1334", + "/Producer": "xdvipdfmx (20140317)", + }, + ), + ( + os.path.join(RESOURCE_ROOT, "metadata.pdf"), + { + "/CreationDate": "D:20220415093243+02'00'", + "/ModDate": "D:20220415093243+02'00'", + "/Creator": "pdflatex, or other tool", + "/Producer": "Latex with hyperref, or other system", + "/Author": "Martin Thoma", + "/Keywords": "Some Keywords, other keywords; more keywords", + "/Subject": "The Subject", + "/Title": "The Title", + "/Trapped": "/False", + "/PTEX.Fullbanner": ( + "This is pdfTeX, Version " + "3.141592653-2.6-1.40.23 (TeX Live 2021) " + "kpathsea version 6.3.3" + ), + }, + ), + ], + ids=["crazyones", "metadata"], +) +def test_read_metadata(pdf_path, expected): + with open(pdf_path, "rb") as inputfile: + reader = PdfFileReader(inputfile) + docinfo = reader.getDocumentInfo() + metadict = dict(docinfo) + assert metadict == expected + if "/Title" in metadict: + assert metadict["/Title"] == docinfo.title + + +@pytest.mark.parametrize( + "src", + [ + (os.path.join(RESOURCE_ROOT, "crazyones.pdf")), + (os.path.join(RESOURCE_ROOT, "commented.pdf")), + ], +) +def test_get_annotations(src): + reader = PdfFileReader(src) + + for page in reader.pages: + if PG.ANNOTS in page: + for annot in page[PG.ANNOTS]: + subtype = annot.getObject()[IA.SUBTYPE] + if subtype == "/Text": + annot.getObject()["/Contents"] + + +@pytest.mark.parametrize( + "src", + [ + (os.path.join(RESOURCE_ROOT, "attachment.pdf")), + (os.path.join(RESOURCE_ROOT, "crazyones.pdf")), + ], +) +def test_get_attachments(src): + reader = PdfFileReader(src) + + attachments = {} + for i in range(reader.getNumPages()): + page = reader.getPage(i) + if PG.ANNOTS in page: + for annotation in page[PG.ANNOTS]: + annotobj = annotation.getObject() + if annotobj[IA.SUBTYPE] == "/FileAttachment": + fileobj = annotobj["/FS"] + attachments[fileobj["/F"]] = fileobj["/EF"]["/F"].getData() + return attachments + + +@pytest.mark.parametrize( + "src,outline_elements", + [ + (os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf"), 9), + (os.path.join(RESOURCE_ROOT, "crazyones.pdf"), 0), + ], +) +def test_get_outlines(src, outline_elements): + reader = PdfFileReader(src) + outlines = reader.getOutlines() + assert len(outlines) == outline_elements + + +@pytest.mark.parametrize( + "src,nb_images", + [ + ("pdflatex-outline.pdf", 0), + ("crazyones.pdf", 0), + ("git.pdf", 1), + ("imagemagick-lzw.pdf", 1), + ("imagemagick-ASCII85Decode.pdf", 1), + ("imagemagick-CCITTFaxDecode.pdf", 1), + ], +) +def test_get_images(src, nb_images): + src = os.path.join(RESOURCE_ROOT, src) + reader = PdfFileReader(src) + + with pytest.raises(TypeError): + page = reader.pages["0"] + + page = reader.pages[-1] + page = reader.pages[0] + + images_extracted = [] + + if RES.XOBJECT in page[PG.RESOURCES]: + xObject = page[PG.RESOURCES][RES.XOBJECT].getObject() + + for obj in xObject: + if xObject[obj][IA.SUBTYPE] == "/Image": + extension, byte_stream = _xobj_to_image(xObject[obj]) + if extension is not None: + filename = obj[1:] + ".png" + with open(filename, "wb") as img: + img.write(byte_stream) + images_extracted.append(filename) + + assert len(images_extracted) == nb_images + + # Cleanup + for filepath in images_extracted: + os.remove(filepath) + + +@pytest.mark.parametrize( + "strict,with_prev_0,should_fail", + [ + (True, True, True), + (True, False, False), + (False, True, False), + (False, False, False), + ], +) +def test_get_images_raw(strict, with_prev_0, should_fail): + pdf_data = ( + b"%%PDF-1.7\n" + b"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n" + b"2 0 obj << >> endobj\n" + b"3 0 obj << >> endobj\n" + b"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]" + b" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R" + b" /Resources << /Font << >> >>" + b" /Rotate 0 /Type /Page >> endobj\n" + b"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\n" + b"xref 1 5\n" + b"%010d 00000 n\n" + b"%010d 00000 n\n" + b"%010d 00000 n\n" + b"%010d 00000 n\n" + b"%010d 00000 n\n" + b"trailer << %s/Root 5 0 R /Size 6 >>\n" + b"startxref %d\n" + b"%%%%EOF" + ) + pdf_data = pdf_data % ( + pdf_data.find(b"1 0 obj"), + pdf_data.find(b"2 0 obj"), + pdf_data.find(b"3 0 obj"), + pdf_data.find(b"4 0 obj"), + pdf_data.find(b"5 0 obj"), + b"/Prev 0 " if with_prev_0 else b"", + pdf_data.find(b"xref"), + ) + pdf_stream = io.BytesIO(pdf_data) + if should_fail: + with pytest.raises(PyPDF2.utils.PdfReadError): + PdfFileReader(pdf_stream, strict=strict) + else: + PdfFileReader(pdf_stream, strict=strict) + + +@pytest.mark.xfail( + reason=( + "It's still broken - and unclear what the issue is. " + "Help would be appreciated!" + ) +) +def test_issue297(): + path = os.path.join(RESOURCE_ROOT, "issue-297.pdf") + reader = PdfFileReader(path, "rb") + reader.getPage(0) + + +def test_get_page_of_encrypted_file(): + """ + Check if we can read a page of an encrypted file. + + This is a regression test for issue 327: + IndexError for getPage() of decrypted file + """ + path = os.path.join(RESOURCE_ROOT, "encrypted-file.pdf") + reader = PdfFileReader(path) + + # Password is correct:) + reader.decrypt("test") + + reader.getPage(0) diff --git a/Tests/test_utils.py b/Tests/test_utils.py new file mode 100644 index 0000000000..998172bbc2 --- /dev/null +++ b/Tests/test_utils.py @@ -0,0 +1,100 @@ +import io +import os + +import pytest + +import PyPDF2.utils +from PyPDF2 import PdfFileReader + +TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "Resources") + + +@pytest.mark.parametrize( + "value,expected", [(0, True), (-1, True), (1, True), ("1", False), (1.5, False)] +) +def test_isInt(value, expected): + assert PyPDF2.utils.isInt(value) == expected + + +def test_isBytes(): + assert PyPDF2.utils.isBytes(b"") + + +@pytest.mark.parametrize( + "stream,expected", + [ + (io.BytesIO(b"foo"), False), + (io.BytesIO(b""), False), + (io.BytesIO(b" "), True), + (io.BytesIO(b" "), True), + (io.BytesIO(b" \n"), True), + (io.BytesIO(b" \n"), True), + ], +) +def test_skipOverWhitespace(stream, expected): + assert PyPDF2.utils.skipOverWhitespace(stream) == expected + + +def test_readUntilWhitespace(): + assert PyPDF2.utils.readUntilWhitespace(io.BytesIO(b"foo"), maxchars=1) == b"f" + + +@pytest.mark.parametrize( + "stream,remainder", + [ + (io.BytesIO(b"% foobar\n"), b""), + (io.BytesIO(b""), b""), + (io.BytesIO(b" "), b" "), + (io.BytesIO(b"% foo%\nbar"), b"bar"), + ], +) +def test_skipOverComment(stream, remainder): + PyPDF2.utils.skipOverComment(stream) + assert stream.read() == remainder + + +def test_readUntilRegex_premature_ending_raise(): + import re + + stream = io.BytesIO(b"") + with pytest.raises(PyPDF2.utils.PdfStreamError): + PyPDF2.utils.readUntilRegex(stream, re.compile(b".")) + + +def test_readUntilRegex_premature_ending_name(): + import re + + stream = io.BytesIO(b"") + assert PyPDF2.utils.readUntilRegex(stream, re.compile(b"."), ignore_eof=True) == b"" + + +@pytest.mark.parametrize( + "a,b,expected", + [ + ([[3]], [[7]], [[21]]), + ([[3, 7]], [[5], [13]], [[3 * 5.0 + 7 * 13]]), + ([[3], [7]], [[5, 13]], [[3 * 5, 3 * 13], [7 * 5, 7 * 13]]), + ], +) +def test_matrixMultiply(a, b, expected): + assert PyPDF2.utils.matrixMultiply(a, b) == expected + + +def test_markLocation(): + stream = io.BytesIO(b"abde" * 6000) + PyPDF2.utils.markLocation(stream) + os.remove("PyPDF2_pdfLocation.txt") # cleanup + + +def test_ConvertFunctionsToVirtualList(): + pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + reader = PdfFileReader(pdf_path) + + # Test if getting as slice throws an error + assert len(reader.pages[:]) == 1 + + +def test_hexStr(): + assert PyPDF2.utils.hexStr(10) == "0xa" diff --git a/Tests/test_workflows.py b/Tests/test_workflows.py new file mode 100644 index 0000000000..3004306575 --- /dev/null +++ b/Tests/test_workflows.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- + +import binascii +import os +import sys + +import pytest + +from PyPDF2 import PdfFileReader +from PyPDF2.constants import PageAttributes as PG + +TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "Resources") + +sys.path.append(PROJECT_ROOT) + + +def test_PdfReaderFileLoad(): + """ + Test loading and parsing of a file. Extract text of the file and compare to expected + textual output. Expected outcome: file loads, text matches expected. + """ + + with open(os.path.join(RESOURCE_ROOT, "crazyones.pdf"), "rb") as inputfile: + # Load PDF file from file + reader = PdfFileReader(inputfile) + page = reader.getPage(0) + + # Retrieve the text of the PDF + with open(os.path.join(RESOURCE_ROOT, "crazyones.txt"), "rb") as pdftext_file: + pdftext = pdftext_file.read() + + text = page.extractText().replace("\n", "").encode("utf-8") + + # Compare the text of the PDF to a known source + assert text == pdftext, ( + "PDF extracted text differs from expected value.\n\nExpected:\n\n%r\n\nExtracted:\n\n%r\n\n" + % (pdftext, text) + ) + + +def test_PdfReaderJpegImage(): + """ + Test loading and parsing of a file. Extract the image of the file and compare to expected + textual output. Expected outcome: file loads, image matches expected. + """ + + with open(os.path.join(RESOURCE_ROOT, "jpeg.pdf"), "rb") as inputfile: + # Load PDF file from file + reader = PdfFileReader(inputfile) + + # Retrieve the text of the image + with open(os.path.join(RESOURCE_ROOT, "jpeg.txt"), "r") as pdftext_file: + imagetext = pdftext_file.read() + + page = reader.getPage(0) + x_object = page[PG.RESOURCES]["/XObject"].getObject() + data = x_object["/Im4"].getData() + + # Compare the text of the PDF to a known source + assert binascii.hexlify(data).decode() == imagetext, ( + "PDF extracted image differs from expected value.\n\nExpected:\n\n%r\n\nExtracted:\n\n%r\n\n" + % (imagetext, binascii.hexlify(data).decode()) + ) + + +def test_decrypt(): + with open( + os.path.join(RESOURCE_ROOT, "libreoffice-writer-password.pdf"), "rb" + ) as inputfile: + reader = PdfFileReader(inputfile) + assert reader.isEncrypted == True + reader.decrypt("openpassword") + assert reader.getNumPages() == 1 + assert reader.isEncrypted == True + metadict = reader.getDocumentInfo() + assert dict(metadict) == { + "/CreationDate": "D:20220403203552+02'00'", + "/Creator": "Writer", + "/Producer": "LibreOffice 6.4", + } + # Is extractText() broken for encrypted files? + # assert reader.getPage(0).extractText().replace('\n', '') == "\n˘\n\u02c7\u02c6˙\n\n\n˘\u02c7\u02c6˙\n\n" + + +@pytest.mark.parametrize("degree", [0, 90, 180, 270, 360, -90]) +def test_rotate(degree): + with open(os.path.join(RESOURCE_ROOT, "crazyones.pdf"), "rb") as inputfile: + reader = PdfFileReader(inputfile) + page = reader.getPage(0) + page.rotateCounterClockwise(degree) + + +def test_rotate_45(): + with open(os.path.join(RESOURCE_ROOT, "crazyones.pdf"), "rb") as inputfile: + reader = PdfFileReader(inputfile) + page = reader.getPage(0) + with pytest.raises(AssertionError): + page.rotateCounterClockwise(45) diff --git a/Tests/test_writer.py b/Tests/test_writer.py new file mode 100644 index 0000000000..c7b0588783 --- /dev/null +++ b/Tests/test_writer.py @@ -0,0 +1,115 @@ +import os + +import pytest + +from PyPDF2 import PdfFileReader, PdfFileWriter +from PyPDF2.generic import RectangleObject +from PyPDF2.utils import PageSizeNotDefinedError + +TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "Resources") + + +def test_writer_operations(): + """ + This test just checks if the operation throws an exception. + + This should be done way more thoroughly: It should be checked if the + output is as expected. + """ + pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + pdf_outline_path = os.path.join(RESOURCE_ROOT, "pdflatex-outline.pdf") + + reader = PdfFileReader(pdf_path) + reader_outline = PdfFileReader(pdf_outline_path) + + writer = PdfFileWriter() + page = reader.pages[0] + with pytest.raises(PageSizeNotDefinedError): + writer.addBlankPage() + writer.insertPage(page, 1) + writer.removeText() + writer.insertPage(reader_outline.pages[0], 0) + writer.addBookmarkDestination(page) + writer.addBookmark("A bookmark", 0) + # output.addNamedDestination("A named destination", 1) + writer.removeLinks() + # assert output.getNamedDestRoot() == ['A named destination', IndirectObject(9, 0, output)] + writer.addBlankPage() + writer.addURI(2, "https://example.com", RectangleObject([0, 0, 100, 100])) + writer.addLink(2, 1, RectangleObject([0, 0, 100, 100])) + assert writer.getPageLayout() is None + writer.setPageLayout("SinglePage") + assert writer.getPageLayout() == "SinglePage" + assert writer.getPageMode() is None + writer.setPageMode("UseNone") + assert writer.getPageMode() == "UseNone" + writer.insertBlankPage(width=100, height=100) + writer.insertBlankPage() # without parameters + + # This gives "KeyError: '/Contents'" - is that a bug? + # output.removeImages() + + writer.addMetadata({"author": "Martin Thoma"}) + + writer.addAttachment("foobar.gif", b"foobarcontent") + + # finally, write "output" to PyPDF2-output.pdf + tmp_path = "dont_commit_writer.pdf" + with open(tmp_path, "wb") as output_stream: + writer.write(output_stream) + + # cleanup + os.remove(tmp_path) + + +def test_remove_images(): + pdf_path = os.path.join(RESOURCE_ROOT, "side-by-side-subfig.pdf") + + reader = PdfFileReader(pdf_path) + writer = PdfFileWriter() + + page = reader.pages[0] + writer.insertPage(page, 0) + writer.removeImages() + + # finally, write "output" to PyPDF2-output.pdf + tmp_filename = "dont_commit_writer_removed_image.pdf" + with open(tmp_filename, "wb") as output_stream: + writer.write(output_stream) + + with open(tmp_filename, "rb") as input_stream: + reader = PdfFileReader(input_stream) + assert "Lorem ipsum dolor sit amet" in reader.getPage(0).extractText() + + # Cleanup + os.remove(tmp_filename) + + +def test_write_metadata(): + pdf_path = os.path.join(RESOURCE_ROOT, "crazyones.pdf") + + reader = PdfFileReader(pdf_path) + writer = PdfFileWriter() + + for page in reader.pages: + writer.addPage(page) + + metadata = reader.getDocumentInfo() + writer.addMetadata(metadata) + + writer.addMetadata({"/Title": "The Crazy Ones"}) + + # finally, write data to PyPDF2-output.pdf + tmp_filename = "dont_commit_writer_added_metadata.pdf" + with open(tmp_filename, "wb") as output_stream: + writer.write(output_stream) + + # Check if the title was set + reader = PdfFileReader(tmp_filename) + metadata = reader.getDocumentInfo() + assert metadata.get("/Title") == "The Crazy Ones" + + # Cleanup + os.remove(tmp_filename) diff --git a/Tests/test_xmp.py b/Tests/test_xmp.py new file mode 100644 index 0000000000..941f9d30d3 --- /dev/null +++ b/Tests/test_xmp.py @@ -0,0 +1,44 @@ +import os + +import pytest + +import PyPDF2.xmp +from PyPDF2 import PdfFileReader + +TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) +PROJECT_ROOT = os.path.dirname(TESTS_ROOT) +RESOURCE_ROOT = os.path.join(PROJECT_ROOT, "Resources") + + +@pytest.mark.parametrize( + "src,has_xmp", + [ + (os.path.join(RESOURCE_ROOT, "commented-xmp.pdf"), True), + (os.path.join(RESOURCE_ROOT, "crazyones.pdf"), False), + ], +) +def test_read_xmp(src, has_xmp): + reader = PdfFileReader(src) + xmp = reader.getXmpMetadata() + assert (xmp is None) == (not has_xmp) + if has_xmp: + for el in xmp.getElement( + aboutUri="", namespace=PyPDF2.xmp.RDF_NAMESPACE, name="Artist" + ): + print("el={el}".format(el=el)) + + assert get_all_tiff(xmp) == {"tiff:Artist": ["me"]} + assert xmp.dc_contributor == [] + + +def get_all_tiff(xmp): + data = {} + tiff_ns = xmp.getNodesInNamespace( + aboutUri="", namespace="http://ns.adobe.com/tiff/1.0/" + ) + for tag in tiff_ns: + contents = [] + for content in tag.childNodes: + contents.append(content.data) + data[tag.tagName] = contents + return data diff --git a/Tests/tests.py b/Tests/tests.py deleted file mode 100644 index fa93c10ecf..0000000000 --- a/Tests/tests.py +++ /dev/null @@ -1,69 +0,0 @@ -import os -import sys -import unittest - -from PyPDF2 import PdfFileReader, PdfFileWriter - - -# Configure path environment -TESTS_ROOT = os.path.abspath(os.path.dirname(__file__)) -PROJECT_ROOT = os.path.dirname(TESTS_ROOT) -RESOURCE_ROOT = os.path.join(PROJECT_ROOT, 'Resources') - -sys.path.append(PROJECT_ROOT) - - -class PdfReaderTestCases(unittest.TestCase): - - def test_PdfReaderFileLoad(self): - ''' - Test loading and parsing of a file. Extract text of the file and compare to expected - textual output. Expected outcome: file loads, text matches expected. - ''' - - with open(os.path.join(RESOURCE_ROOT, 'crazyones.pdf'), 'rb') as inputfile: - # Load PDF file from file - ipdf = PdfFileReader(inputfile) - ipdf_p1 = ipdf.getPage(0) - - # Retrieve the text of the PDF - pdftext_file = open(os.path.join(RESOURCE_ROOT, 'crazyones.txt'), 'r') - pdftext = pdftext_file.read() - ipdf_p1_text = ipdf_p1.extractText().replace('\n', '') - - # Compare the text of the PDF to a known source - self.assertEqual(ipdf_p1_text.encode('utf-8', errors='ignore'), pdftext, - msg='PDF extracted text differs from expected value.\n\nExpected:\n\n%r\n\nExtracted:\n\n%r\n\n' - % (pdftext, ipdf_p1_text.encode('utf-8', errors='ignore'))) - - -class AddJsTestCase(unittest.TestCase): - - def setUp(self): - ipdf = PdfFileReader(os.path.join(RESOURCE_ROOT, 'crazyones.pdf')) - self.pdf_file_writer = PdfFileWriter() - self.pdf_file_writer.appendPagesFromReader(ipdf) - - def test_add(self): - - self.pdf_file_writer.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") - - self.assertIn('/Names', self.pdf_file_writer._root_object, "addJS should add a name catalog in the root object.") - self.assertIn('/JavaScript', self.pdf_file_writer._root_object['/Names'], "addJS should add a JavaScript name tree under the name catalog.") - self.assertIn('/OpenAction', self.pdf_file_writer._root_object, "addJS should add an OpenAction to the catalog.") - - def test_overwrite(self): - - self.pdf_file_writer.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") - first_js = self.get_javascript_name() - - self.pdf_file_writer.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") - second_js = self.get_javascript_name() - - self.assertNotEqual(first_js, second_js, "addJS should overwrite the previous script in the catalog.") - - def get_javascript_name(self): - self.assertIn('/Names', self.pdf_file_writer._root_object) - self.assertIn('/JavaScript', self.pdf_file_writer._root_object['/Names']) - self.assertIn('/Names', self.pdf_file_writer._root_object['/Names']['/JavaScript']) - return self.pdf_file_writer._root_object['/Names']['/JavaScript']['/Names'][0] diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000000..d4bb2cbb9e --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000000..18cc5134eb --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,77 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# 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. +import os +import sys + +sys.path.insert(0, os.path.abspath(".")) +sys.path.insert(0, os.path.abspath("../")) + +# -- Project information ----------------------------------------------------- + +project = "PyPDF2" +copyright = "2006 - 2008, Mathieu Fenniak" +author = "Mathieu Fenniak" + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.coverage", + "sphinx.ext.mathjax", + "sphinx.ext.viewcode", + "sphinx.ext.napoleon", + "myst_parser", +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] + + +# -- 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 = "sphinx_rtd_theme" + +# 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 = { + "canonical_url": "", + "analytics_id": "", + "logo_only": False, + "display_version": True, + "prev_next_buttons_location": "bottom", + "style_external_links": False, + # Toc options + "collapse_navigation": True, + "sticky_navigation": True, + "navigation_depth": 4, + "includehidden": True, + "titles_only": False, +} + +# 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"] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000000..e5cdcd2386 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,71 @@ +.. PyPDF2 documentation main file, created by + sphinx-quickstart on Thu Apr 7 20:13:19 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to PyPDF2 +================= + +PyPDF2 is a `free `_ and open +source pure-python PDF library capable of splitting, +merging, cropping, and transforming the pages of PDF files. It can also add +custom data, viewing options, and passwords to PDF files. +PyPDF2 can retrieve text and metadata from PDFs as well. + +You can contribute to `PyPDF2 on Github `_. + +.. toctree:: + :caption: User Guide + :maxdepth: 1 + + user/installation + user/metadata + user/extract-text + user/encryption-decryption + user/merging-pdfs + user/cropping-and-transforming + user/add-watermark + user/reading-pdf-annotations + user/adding-pdf-annotations + + +.. toctree:: + :caption: API Reference + :maxdepth: 1 + + modules/PdfFileReader + modules/PdfFileMerger + modules/PageObject + modules/PdfFileWriter + modules/DocumentInformation + modules/XmpInformation + modules/Destination + modules/RectangleObject + modules/Field + modules/PageRange + + +.. toctree:: + :caption: About PyPDF2 + :maxdepth: 1 + + user/history + user/comparisons + user/faq + + +.. toctree:: + :caption: Scripts + :maxdepth: 1 + + user/pdfcat + + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000000..32bb24529f --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/docs/modules/Destination.rst b/docs/modules/Destination.rst new file mode 100644 index 0000000000..30a631f33a --- /dev/null +++ b/docs/modules/Destination.rst @@ -0,0 +1,7 @@ +The Destination Class +--------------------- + +.. autoclass:: PyPDF2.pdf.Destination + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/DocumentInformation.rst b/docs/modules/DocumentInformation.rst new file mode 100644 index 0000000000..4002f632b9 --- /dev/null +++ b/docs/modules/DocumentInformation.rst @@ -0,0 +1,7 @@ +The DocumentInformation Class +----------------------------- + +.. autoclass:: PyPDF2.pdf.DocumentInformation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/Field.rst b/docs/modules/Field.rst new file mode 100644 index 0000000000..d34fc79d1c --- /dev/null +++ b/docs/modules/Field.rst @@ -0,0 +1,7 @@ +The Field Class +--------------- + +.. autoclass:: PyPDF2.pdf.Field + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/PageObject.rst b/docs/modules/PageObject.rst new file mode 100644 index 0000000000..80b7247445 --- /dev/null +++ b/docs/modules/PageObject.rst @@ -0,0 +1,7 @@ +The PageObject Class +-------------------- + +.. autoclass:: PyPDF2.pdf.PageObject + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/PageRange.rst b/docs/modules/PageRange.rst new file mode 100644 index 0000000000..c9481900d8 --- /dev/null +++ b/docs/modules/PageRange.rst @@ -0,0 +1,7 @@ +The PageRange Class +------------------- + +.. autoclass:: PyPDF2.pagerange.PageRange + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/PdfFileMerger.rst b/docs/modules/PdfFileMerger.rst new file mode 100644 index 0000000000..cf936b1b27 --- /dev/null +++ b/docs/modules/PdfFileMerger.rst @@ -0,0 +1,7 @@ +The PdfFileMerger Class +----------------------- + +.. autoclass:: PyPDF2.merger.PdfFileMerger + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/PdfFileReader.rst b/docs/modules/PdfFileReader.rst new file mode 100644 index 0000000000..11ae94dc3a --- /dev/null +++ b/docs/modules/PdfFileReader.rst @@ -0,0 +1,7 @@ +The PdfFileReader Class +----------------------- + +.. autoclass:: PyPDF2.pdf.PdfFileReader + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/PdfFileWriter.rst b/docs/modules/PdfFileWriter.rst new file mode 100644 index 0000000000..5da6c9a2a3 --- /dev/null +++ b/docs/modules/PdfFileWriter.rst @@ -0,0 +1,7 @@ +The PdfFileWriter Class +----------------------- + +.. autoclass:: PyPDF2.pdf.PdfFileWriter + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/RectangleObject.rst b/docs/modules/RectangleObject.rst new file mode 100644 index 0000000000..a0ae1f30b0 --- /dev/null +++ b/docs/modules/RectangleObject.rst @@ -0,0 +1,7 @@ +The RectangleObject Class +------------------------- + +.. autoclass:: PyPDF2.generic.RectangleObject + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/modules/XmpInformation.rst b/docs/modules/XmpInformation.rst new file mode 100644 index 0000000000..49a6641cbb --- /dev/null +++ b/docs/modules/XmpInformation.rst @@ -0,0 +1,7 @@ +The XmpInformation Class +------------------------- + +.. autoclass:: PyPDF2.xmp.XmpInformation + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/user/add-watermark.md b/docs/user/add-watermark.md new file mode 100644 index 0000000000..33fd7f43f5 --- /dev/null +++ b/docs/user/add-watermark.md @@ -0,0 +1,24 @@ +# Adding a Watermark to a PDF + +```python +from PyPDF2 import PdfFileWriter, PdfFileReader + + +# Read the watermark +watermark = PdfFileReader("watermark.pdf") + +# Read the page without watermark +reader = PdfFileReader("example.pdf") +page = reader.pages[0] + +# Add the watermark to the page +page.mergePage(watermark.pages[0]) + +# Add the page to the writer +writer = PdfFileWriter() +writer.addPage(page) + +# finally, write the new document with a watermark +with open("PyPDF2-output.pdf", "wb") as fp: + output.write(fp) +``` diff --git a/docs/user/adding-pdf-annotations.md b/docs/user/adding-pdf-annotations.md new file mode 100644 index 0000000000..215dcfdc70 --- /dev/null +++ b/docs/user/adding-pdf-annotations.md @@ -0,0 +1,16 @@ +# Adding PDF Annotations + +## Attachments + +```python +from PyPDF2 import PdfFileWriter + +writer = PdfFileWriter() +writer.addBlankPage(width=200, height=200) + +data = b"any bytes - typically read from a file" +writer.addAttachment("smile.png", data) + +with open("output.pdf", "wb") as output_stream: + writer.write(output_stream) +``` diff --git a/docs/user/comparisons.md b/docs/user/comparisons.md new file mode 100644 index 0000000000..a75a7be5dc --- /dev/null +++ b/docs/user/comparisons.md @@ -0,0 +1,66 @@ +# PyPDF2 vs X + +PyPDF2 is a [free] and open source pure-python PDF library capable of +splitting, merging, cropping, and transforming the pages of PDF files. +It can also add custom data, viewing options, and passwords to PDF +files. PyPDF2 can retrieve text and metadata from PDFs as well. + +## PyMuPDF and PikePDF + +[PyMuPDF] is a Python binding to [MuPDF] and [PikePDF] is the Python +binding to [QPDF]. + +While both are excellent libraries for various use-cases, using them is +not always possible even when they support the use-case. Both of them +are powered by C libraries which makes installation harder and might +cause security concerns. For MuPDF you might also need to buy a +commercial license. + +A core feature of PyPDF2 is that it's pure Python. That means there is +no C dependency. It has been used for over 10 years and for this reason +a lot of support via StackOverflow and examples on the internet. + +## pyPDF + +PyPDF2 was forked from pyPDF. pyPDF has been unmaintained for a long +time. + +## PyPDF3 and PyPDF4 + +Developing and maintaining open source software is extremely +time-intensive and in the case of PyPDF2 not paid at all. Having a +continuous support is hard. + +PyPDF2 was initially released in 2012 on PyPI and received releases +until 2016. From 2016 to 2022 there was no update - but people were +still using it. + +As PyPDF2 is free software, there were attempts to fork it and continue +the development. PyPDF3 was first released in 2018 and still receives +updates. PyPDF4 has only one release from 2018. + +I, Martin Thoma, the current maintainer of PyPDF2, hope that we can +bring the community back to one path of development. Let's see. + + [free]: https://en.wikipedia.org/wiki/Free_software + [PyMuPDF]: https://pypi.org/project/PyMuPDF/ + [MuPDF]: https://mupdf.com/ + [PikePDF]: https://pypi.org/project/pikepdf/ + [QPDF]: https://github.com/qpdf/qpdf + + +## pdfrw and pdfminer + +I don't have experience with either of those libraries. Please add a +comparison if you know PyPDF2 and [`pdfrw`](https://pypi.org/project/pdfrw/) or +[`pdfminer.six`](https://pypi.org/project/pdfminer.six/)! + +Please be aware that there is also +[`pdfminer`](https://pypi.org/project/pdfminer/) which is not maintained. +Then there is [`pdfrw2`](https://pypi.org/project/pdfrw2/) which doesn't have +a large community behind it. + +And there are more: + + +* [`pyfpdf`](https://github.com/reingart/pyfpdf) diff --git a/docs/user/cropping-and-transforming.md b/docs/user/cropping-and-transforming.md new file mode 100644 index 0000000000..e1958a4f43 --- /dev/null +++ b/docs/user/cropping-and-transforming.md @@ -0,0 +1,31 @@ +# Cropping and Transforming PDFs + +```python +from PyPDF2 import PdfFileWriter, PdfFileReader + +reader = PdfFileReader("example.pdf") +writer = PdfFileWriter() + +# add page 1 from reader to output document, unchanged: +writer.addPage(reader.pages[0]) + +# add page 2 from reader, but rotated clockwise 90 degrees: +writer.addPage(reader.pages[1].rotateClockwise(90)) + +# add page 3 from reader, but crop it to half size: +page3 = reader.pages[2] +page3.mediaBox.upperRight = ( + page3.mediaBox.getUpperRight_x() / 2, + page3.mediaBox.getUpperRight_y() / 2, +) +writer.addPage(page3) + +# add some Javascript to launch the print window on opening this PDF. +# the password dialog may prevent the print dialog from being shown, +# comment the the encription lines, if that's the case, to try this out: +writer.addJS("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") + +# write to document-output.pdf +with open("PyPDF2-output.pdf", "wb") as fp: + writer.write(fp) +``` diff --git a/docs/user/encryption-decryption.md b/docs/user/encryption-decryption.md new file mode 100644 index 0000000000..a7aaed2fb4 --- /dev/null +++ b/docs/user/encryption-decryption.md @@ -0,0 +1,47 @@ +# Encryption and Decryption of PDFs + +## Encrypt + +Add a password to a PDF (encrypt it): + +```python +from PyPDF2 import PdfFileReader, PdfFileWriter + +reader = PdfFileReader("example.pdf") +writer = PdfFileWriter() + +# Add all pages to the writer +for i in range(reader.numPages): + page = reader.pages[i] + writer.addPage(page) + +# Add a password to the new PDF +writer.encrypt("my-secret-password") + +# Save the new PDF to a file +with open("encrypted-pdf.pdf", "wb") as f: + writer.write(f) +``` + +## Decrypt + +Remove the password from a PDF (decrypt it): + +```python +from PyPDF2 import PdfFileReader, PdfFileWriter + +reader = PdfFileReader("encrypted-pdf.pdf") +writer = PdfFileWriter() + +if reader.isEncrypted: + reader.decrypt("my-secret-password") + +# Add all pages to the writer +for i in range(reader.numPages): + page = reader.pages[i] + writer.addPage(page) + +# Save the new PDF to a file +with open("decrypted-pdf.pdf", "wb") as f: + writer.write(f) +``` diff --git a/docs/user/extract-text.md b/docs/user/extract-text.md new file mode 100644 index 0000000000..bf6c5fa73f --- /dev/null +++ b/docs/user/extract-text.md @@ -0,0 +1,41 @@ +# Extract Text from a PDF + +You can extract text from a PDF like this: + +```python +from PyPDF2 import PdfFileReader + +reader = PdfFileReader("example.pdf") +page = reader.pages[0] +print(page.extractText()) +``` + +## Why Text Extraction is hard + +Extracting text from a PDF can be pretty tricky. In several cases there is no +clear answer what the expected result should look like: + +1. **Paragraphs**: Should the text of a paragraph have line breaks at the same places + where the original PDF had them or should it rather be one block of text? +2. **Page numbers**: Should they be included in the extract? +3. **Outlines**: Should outlines be extracted at all? +4. **Formatting**: If text is **bold** or *italic*, should it be included in the + output? +5. **Captions**: Should image and table captions be included? + +Then there are issues where most people would agree on the correct output, but +the way PDF stores information just makes it hard to achieve that: + +1. **Tables**: Typically, tables are just absolutely positioned text. In the worst + case, ever single letter could be absolutely positioned. That makes it hard + to tell where columns / rows are. +2. **Images**: Sometimes PDFs do not contain the text as it's displayed, but + instead an image. You notice that when you cannot copy the text. Then there + are PDF files that contain an image and a text layer in the background. + That typically happens when a document was scanned. Although the scanning + software (OCR) is pretty good today, it still fails once in a while. PyPDF2 + is no OCR software; it will not be able to detect those failures. PyPDF2 + will also never be able to extract text from images. + +And finally there are issues that PyPDF2 will deal with. If you find such a +text extraction bug, please share the PDF with us so we can work on it! diff --git a/docs/user/faq.md b/docs/user/faq.md new file mode 100644 index 0000000000..b88a85a295 --- /dev/null +++ b/docs/user/faq.md @@ -0,0 +1,36 @@ +# Frequently-Asked Questions + +## How is PyPDF2 related to pyPdf? + +PyPDF2 is a fork from the no-longer-maintained pyPdf approved by the +latter's founder. + +## Which Python versions are supported? + +As [Matthew] writes, "... the intention is for PyPDF2 to work with +Python 2 as well as Python 3." ([source]) + +In January 2014, the main branch works with 2.6-2.7 and 3.1-3.3 \[and +maybe 2.5?\]. Notice that 1.19--the latest in PyPI as of this +writing--(mostly) did not work with 3.x. + +I often merge \[concatenate\] various PDF instances, and my application +'craters' with certain files produced by {AutoCAD, my departmental +scanner, ...}, even though the original files display OK. What do I do +now? Crucial ideas we want you to know: + +- All of us contend with [this sort of thing]. Vendors often produce + PDF with questionable syntax, or at least syntax that isn't what + PyPDF2 expects. +- We're committed to resolving all these problems, so that your + applications (and ours) can handle any PDF instances that come their + way. Write whenever you have a problem a [GitHub issue]. +- In the meantime, while you're waiting on us, you have at least a + couple of choices: you can debug PyPDF2 yourself; or use Acrobat or + Preview or a similar consumer-grade PDF tool to 'mollify' + \[explain\] your PDF instances so you get the results you are after. + + [Matthew]: https://github.com/mstamy2 + [source]: https://github.com/py-pdf/PyPDF2/commit/24b270d876518d15773224b5d0d6c2206db29f64#commitcomment-5038317 + [this sort of thing]: https://github.com/py-pdf/PyPDF2/issues/24 + [GitHub issue]: https://github.com/py-pdf/PyPDF2/issues diff --git a/docs/user/history.md b/docs/user/history.md new file mode 100644 index 0000000000..66392b6d10 --- /dev/null +++ b/docs/user/history.md @@ -0,0 +1,51 @@ +# History of PyPDF2 + +## The Origins: pyPDF + +In 2005, [Mathieu Fenniak] launched pyPdf "as a PDF toolkit..." +focused on + +- document manipulation: by-page splitting, concatenation, and + merging; +- document introspection; +- page cropping; and +- document encryption and decryption. + +## PyPDF2 is born + +At the end of 2011, after consultation with Mathieu and others, Phaseit +sponsored PyPDF2 as a fork of pyPdf on GitHub. The initial impetus was +to handle a wider range of input PDF instances; Phaseit\'s commercial +work often encounters PDF instances \"in the wild\" that it needs to +manage (mostly concatenate and paginate), but that deviate so much from +PDF standards that pyPdf can\'t read them. PyPDF2 reads a considerably +wider range of real-world PDF instances. + +Neither pyPdf nor PyPDF2 aims to be universal, that is, to provide all +possible PDF-related functionality. Note that the similar-appearing +[pyfpdf] of Mariano Reingart is most comparable to [ReportLab], in that +both ReportLab and pyfpdf emphasize document generation. Interestingly +enough, pyfpdf builds in a basic HTML→PDF converter while PyPDF2 has no +knowledge of HTML. + +So what is PyPDF2 truly about? Think about popular [pdftk] for a moment. +PyPDF2 does what pdftk does, and it does so within your current Python +process, and it handles a wider range of variant PDF formats +\[explain\]. PyPDF2 has its own FAQ to answer other questions that have +arisen. + +## PyPDF2 rises + +The Reddit [/r/python crowd chatted] obliquely and briefly about PyPDF2 +in March 2012. + +## PyPDF2: Reborn + +Martin Thoma took over maintenance of PyPDF2 in April 2022. + + + [Mathieu Fenniak]: https://mathieu.fenniak.net/ + [pyfpdf]: https://github.com/reingart/pyfpdf + [ReportLab]: https://www.reportlab.com/software/opensource/rl-toolkit/ + [pdftk]: https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/ + [/r/python crowd chatted]: https://www.reddit.com/r/Python/comments/qsvfm/pypdf2_updates_pypdf_pypdf2_is_an_opensource/ diff --git a/docs/user/installation.md b/docs/user/installation.md new file mode 100644 index 0000000000..62f92c5b3f --- /dev/null +++ b/docs/user/installation.md @@ -0,0 +1,35 @@ +# Installation + +There are several ways to install PyPDF2. The most common option is to use pip. + +## pip + +PyPDF2 requires Python 2.7+ to run, but [Python 2 is dead](https://pythonclock.org/). +Please use a recent version of Python 3 instead. + +Typically Python comes with `pip`, a package installer. Using it you can +install PyPDF2: + +```bash +pip install PyPDF2 +``` + +If you are not a super-user (a system adminstrator / root), you can also just +install PyPDF2 for your current user: + +```bash +pip install --user PyPDF2 +``` + +## Anaconda + +Anaconda users can [install PyPDF2 via conda-forge](https://anaconda.org/conda-forge/pypdf2). + + +## Development Version + +In case you want to use the current version under development: + +```bash +pip install git+https://github.com/py-pdf/PyPDF2.git +``` diff --git a/docs/user/merging-pdfs.md b/docs/user/merging-pdfs.md new file mode 100644 index 0000000000..8325c140f0 --- /dev/null +++ b/docs/user/merging-pdfs.md @@ -0,0 +1,48 @@ +# Merging PDF files + +## Basic Example + +```python +from PyPDF2 import PdfFileMerger + +merger = PdfFileMerger() + +for pdf in ["file1.pdf", "file2.pdf", "file3.pdf"]: + merger.append(pdf) + +merger.write("merged-pdf.pdf") +merger.close() +``` + +For more details, see an excellent answer on +[StackOverflow](https://stackoverflow.com/questions/3444645/merge-pdf-files) +by Paul Rooney. + +## Showing more merging options + +```python +from PyPDF2 import PdfFileMerger + +merger = PdfFileMerger() + +input1 = open("document1.pdf", "rb") +input2 = open("document2.pdf", "rb") +input3 = open("document3.pdf", "rb") + +# add the first 3 pages of input1 document to output +merger.append(fileobj=input1, pages=(0, 3)) + +# insert the first page of input2 into the output beginning after the second page +merger.merge(position=2, fileobj=input2, pages=(0, 1)) + +# append entire input3 document to the end of the output document +merger.append(input3) + +# Write to an output PDF document +output = open("document-output.pdf", "wb") +merger.write(output) + +# Close File Descriptors +merger.close() +output.close() +``` \ No newline at end of file diff --git a/docs/user/metadata.md b/docs/user/metadata.md new file mode 100644 index 0000000000..eb80c3ad71 --- /dev/null +++ b/docs/user/metadata.md @@ -0,0 +1,47 @@ +# Metadata + +## Reading metadata + +```python +from PyPDF2 import PdfFileReader + +reader = PdfFileReader("example.pdf") + +info = reader.getDocumentInfo() + +print(reader.numPages) + +# All of the following could be None! +print(info.author) +print(info.creator) +print(info.producer) +print(info.subject) +print(info.title) +``` + +## Writing metadata + +```python +from PyPDF2 import PdfFileReader, PdfFileWriter + +reader = PdfFileReader("example.pdf") +writer = PdfFileWriter() + +# Add all pages to the writer +for i in range(reader.numPages): + page = reader.pages[i] + writer.addPage(page) + +# Add the metadata +writer.addMetadata( + { + "/Author": "Martin", + "/Producer": "Libre Writer", + } +) + +# Save the new PDF to a file +with open("meta-pdf.pdf", "wb") as f: + writer.write(f) + +``` diff --git a/docs/user/pdfcat.md b/docs/user/pdfcat.md new file mode 100644 index 0000000000..68fcd805b6 --- /dev/null +++ b/docs/user/pdfcat.md @@ -0,0 +1,70 @@ +# pdfcat + +**PyPDF2** contains a growing variety of sample programs meant to demonstrate its +features. It also contains useful scripts such as `pdfcat`, located within the +`Scripts` folder. This script makes it easy to concatenate PDF files by using +Python slicing syntax. Because we are slicing PDF pages, we refer to the slices +as *page ranges*. + +```{admonition} Deprecation Discussion +We are thinking about moving pdfcat to a separate package. +Please [participate in the discussion](https://github.com/py-pdf/PyPDF2/discussions/718). +``` + +**Page range** expression examples: + +| : | all pages | -1 | last page | +| --- | -------------------------- | ----- | ----------------------- | +| 22 | just the 23rd page | :-1 | all but the last page | +| 0:3 | the first three pages | -2 | second-to-last page | +| :3 | the first three pages | -2: | last two pages | +| 5: | from the sixth page onward | -3:-1 | third & second to last | + +The third stride or step number is also recognized: + +| ::2 | 0 2 4 ... to the end | +| ------ | -------------------------- | +| 1:10:2 | 1 3 5 7 9 | +| ::-1 | all pages in reverse order | +| 3:0:-1 | 3 2 1 but not 0 | +| 2::-1 | 2 1 0 | + + +Usage for pdfcat is as follows: + +```console +$ pdfcat [-h] [-o output.pdf] [-v] input.pdf [page_range...] ... +``` + +You can add as many input files as you like. You may also specify as many page +ranges as needed for each file. + +**Optional arguments:** + + +| -h | --help | Show the help message and exit +| -- |---------- | ------------------------------ +| -o | --output | Follow this argument with the output PDF file. Will be created if it doesn’t exist. +| -v | --verbose | Show page ranges as they are being read + +## Examples + +```console +$ pdfcat -o output.pdf head.pdf content.pdf :6 7: tail.pdf -1 +``` + +Concatenates all of head.pdf, all but page seven of content.pdf, and the last page of tail.pdf, producing output.pdf. + +```console +$ pdfcat chapter*.pdf >book.pdf +``` + +You can specify the output file by redirection. + +```console +$ pdfcat chapter?.pdf chapter10.pdf >book.pdf +``` + +In case you don’t want chapter 10 before chapter 2. + +Thanks to **Steve Witham** for this script! diff --git a/docs/user/reading-pdf-annotations.md b/docs/user/reading-pdf-annotations.md new file mode 100644 index 0000000000..e84abd0a18 --- /dev/null +++ b/docs/user/reading-pdf-annotations.md @@ -0,0 +1,67 @@ +# Reading PDF Annotations + +PDF 1.7 defines 25 different annotation types: + +* Text +* Link +* FreeText +* Line, Square, Circle, Polygon, PolyLine, Highlight, Underline, Squiggly, StrikeOut +* Stamp, Caret, Ink +* Popup +* FileAttachment +* Sound, Movie +* Widget, Screen +* PrinterMark +* TrapNet +* Watermark +* 3D + +Reading the most common ones is described here. + +## Text + +```python +from PyPDF2 import PdfFileReader + +reader = PdfFileReader("example.pdf") + +for page in reader.pages: + if "/Annots" in page: + for annot in page["/Annots"]: + subtype = annot.getObject()["/Subtype"] + if subtype == "/Text": + print(annot.getObject()["/Contents"]) +``` + +## Highlights + +```python +from PyPDF2 import PdfFileReader + +reader = PdfFileReader("commented.pdf") + +for page in reader.pages: + if "/Annots" in page: + for annot in page["/Annots"]: + subtype = annot.getObject()["/Subtype"] + if subtype == "/Highlight": + coords = annot.getObject()["/QuadPoints"] + x1, y1, x2, y2, x3, y3, x4, y4 = coords +``` + +## Attachments + +```python +from PyPDF2 import PdfFileReader + +reader = PdfFileReader("example.pdf") + +attachments = {} +for page in reader.pages: + if "/Annots" in page: + for annotation in page["/Annots"]: + subtype = annot.getObject()["/Subtype"] + if subtype == "/FileAttachment": + fileobj = annotobj["/FS"] + attachments[fileobj["/F"]] = fileobj["/EF"]["/F"].getData() +``` diff --git a/mutmut-test.sh b/mutmut-test.sh new file mode 100644 index 0000000000..94cbc86a81 --- /dev/null +++ b/mutmut-test.sh @@ -0,0 +1,2 @@ +#!/bin/bash -e +pytest -x \ No newline at end of file diff --git a/requirements/ci.in b/requirements/ci.in new file mode 100644 index 0000000000..a7592ecf58 --- /dev/null +++ b/requirements/ci.in @@ -0,0 +1,6 @@ +coverage +flake8 +flake8_implicit_str_concat +flake8-bugbear +pillow +pytest \ No newline at end of file diff --git a/requirements/ci.txt b/requirements/ci.txt new file mode 100644 index 0000000000..fcca42ffec --- /dev/null +++ b/requirements/ci.txt @@ -0,0 +1,54 @@ +# +# This file is autogenerated by pip-compile with python 3.6 +# To update, run: +# +# pip-compile requirements/ci.in +# +attrs==20.3.0 + # via + # flake8-bugbear + # flake8-implicit-str-concat + # pytest +coverage==6.2 + # via -r requirements/ci.in +flake8==4.0.1 + # via + # -r requirements/ci.in + # flake8-bugbear +flake8-bugbear==22.3.23 + # via -r requirements/ci.in +flake8-implicit-str-concat==0.2.0 + # via -r requirements/ci.in +importlib-metadata==4.2.0 + # via + # flake8 + # pluggy + # pytest +iniconfig==1.1.1 + # via pytest +mccabe==0.6.1 + # via flake8 +more-itertools==8.12.0 + # via flake8-implicit-str-concat +packaging==21.3 + # via pytest +pillow==8.4.0 + # via -r requirements/ci.in +pluggy==1.0.0 + # via pytest +py==1.11.0 + # via pytest +pycodestyle==2.8.0 + # via flake8 +pyflakes==2.4.0 + # via flake8 +pyparsing==3.0.7 + # via packaging +pytest==7.0.1 + # via -r requirements/ci.in +tomli==1.2.3 + # via pytest +typing-extensions==4.1.1 + # via importlib-metadata +zipp==3.6.0 + # via importlib-metadata diff --git a/requirements/dev.in b/requirements/dev.in new file mode 100644 index 0000000000..6a2b12f2b7 --- /dev/null +++ b/requirements/dev.in @@ -0,0 +1,6 @@ +black +pip-tools +pre-commit +pytest-cov +twine +wheel diff --git a/requirements/dev.txt b/requirements/dev.txt new file mode 100644 index 0000000000..ff79c1b38d --- /dev/null +++ b/requirements/dev.txt @@ -0,0 +1,158 @@ +# +# This file is autogenerated by pip-compile with python 3.6 +# To update, run: +# +# pip-compile requirements/dev.in +# +attrs==21.4.0 + # via pytest +black==22.3.0 + # via -r requirements/dev.in +bleach==4.1.0 + # via readme-renderer +certifi==2021.10.8 + # via requests +cffi==1.15.0 + # via cryptography +cfgv==3.3.1 + # via pre-commit +charset-normalizer==2.0.12 + # via requests +click==8.0.4 + # via + # black + # pip-tools +colorama==0.4.4 + # via twine +coverage[toml]==6.2 + # via pytest-cov +cryptography==36.0.2 + # via secretstorage +dataclasses==0.8 + # via black +distlib==0.3.4 + # via virtualenv +docutils==0.18.1 + # via readme-renderer +filelock==3.4.1 + # via virtualenv +identify==2.4.4 + # via pre-commit +idna==3.3 + # via requests +importlib-metadata==4.8.3 + # via + # click + # keyring + # pep517 + # pluggy + # pre-commit + # pytest + # twine + # virtualenv +importlib-resources==5.2.3 + # via + # pre-commit + # tqdm + # virtualenv +iniconfig==1.1.1 + # via pytest +jeepney==0.7.1 + # via + # keyring + # secretstorage +keyring==23.4.1 + # via twine +mypy-extensions==0.4.3 + # via black +nodeenv==1.6.0 + # via pre-commit +packaging==21.3 + # via + # bleach + # pytest +pathspec==0.9.0 + # via black +pep517==0.12.0 + # via pip-tools +pip-tools==6.4.0 + # via -r requirements/dev.in +pkginfo==1.8.2 + # via twine +platformdirs==2.4.0 + # via + # black + # virtualenv +pluggy==1.0.0 + # via pytest +pre-commit==2.17.0 + # via -r requirements/dev.in +py==1.11.0 + # via pytest +pycparser==2.21 + # via cffi +pygments==2.11.2 + # via readme-renderer +pyparsing==3.0.8 + # via packaging +pytest==7.0.1 + # via pytest-cov +pytest-cov==3.0.0 + # via -r requirements/dev.in +pyyaml==6.0 + # via pre-commit +readme-renderer==34.0 + # via twine +requests==2.27.1 + # via + # requests-toolbelt + # twine +requests-toolbelt==0.9.1 + # via twine +rfc3986==1.5.0 + # via twine +secretstorage==3.3.1 + # via keyring +six==1.16.0 + # via + # bleach + # virtualenv +toml==0.10.2 + # via pre-commit +tomli==1.2.3 + # via + # black + # coverage + # pep517 + # pytest +tqdm==4.64.0 + # via twine +twine==3.8.0 + # via -r requirements/dev.in +typed-ast==1.5.2 + # via black +typing-extensions==4.1.1 + # via + # black + # importlib-metadata +urllib3==1.26.9 + # via + # requests + # twine +virtualenv==20.14.0 + # via pre-commit +webencodings==0.5.1 + # via bleach +wheel==0.37.1 + # via + # -r requirements/dev.in + # pip-tools +zipp==3.6.0 + # via + # importlib-metadata + # importlib-resources + # pep517 + +# The following packages are considered to be unsafe in a requirements file: +# pip +# setuptools diff --git a/requirements/docs.in b/requirements/docs.in new file mode 100644 index 0000000000..3dd64d0cfd --- /dev/null +++ b/requirements/docs.in @@ -0,0 +1,3 @@ +sphinx +sphinx_rtd_theme +myst_parser diff --git a/requirements/docs.txt b/requirements/docs.txt new file mode 100644 index 0000000000..749759f9b5 --- /dev/null +++ b/requirements/docs.txt @@ -0,0 +1,84 @@ +# +# This file is autogenerated by pip-compile with python 3.6 +# To update, run: +# +# pip-compile requirements/docs.in +# +alabaster==0.7.12 + # via sphinx +attrs==21.4.0 + # via markdown-it-py +babel==2.9.1 + # via sphinx +certifi==2021.10.8 + # via requests +charset-normalizer==2.0.12 + # via requests +docutils==0.17.1 + # via + # myst-parser + # sphinx + # sphinx-rtd-theme +idna==3.3 + # via requests +imagesize==1.3.0 + # via sphinx +importlib-metadata==4.8.3 + # via sphinx +jinja2==3.0.3 + # via + # myst-parser + # sphinx +markdown-it-py==2.0.1 + # via + # mdit-py-plugins + # myst-parser +markupsafe==2.0.1 + # via jinja2 +mdit-py-plugins==0.3.0 + # via myst-parser +mdurl==0.1.0 + # via markdown-it-py +myst-parser==0.16.1 + # via -r requirements/docs.in +packaging==21.3 + # via sphinx +pygments==2.11.2 + # via sphinx +pyparsing==3.0.7 + # via packaging +pytz==2022.1 + # via babel +pyyaml==6.0 + # via myst-parser +requests==2.27.1 + # via sphinx +snowballstemmer==2.2.0 + # via sphinx +sphinx==4.5.0 + # via + # -r requirements/docs.in + # myst-parser + # sphinx-rtd-theme +sphinx-rtd-theme==1.0.0 + # via -r requirements/docs.in +sphinxcontrib-applehelp==1.0.2 + # via sphinx +sphinxcontrib-devhelp==1.0.2 + # via sphinx +sphinxcontrib-htmlhelp==2.0.0 + # via sphinx +sphinxcontrib-jsmath==1.0.1 + # via sphinx +sphinxcontrib-qthelp==1.0.3 + # via sphinx +sphinxcontrib-serializinghtml==1.1.5 + # via sphinx +typing-extensions==4.1.1 + # via + # importlib-metadata + # markdown-it-py +urllib3==1.26.9 + # via requests +zipp==3.6.0 + # via importlib-metadata diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000000..6b42699b63 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,36 @@ +[metadata] +name = PyPDF2 + +author = Mathieu Fenniak +author_email = biziqe@mathieu.fenniak.net +maintainer = Martin Thoma +maintainer_email = info@martin-thoma.de + +description = A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files +long_description = file: README.md +long_description_content_type = text/markdown + +license = BSD-3-Clause + +url = https://pypdf2.readthedocs.io/en/latest/ +project_urls = + Source = https://github.com/py-pdf/PyPDF2 + Bug Reports = https://github.com/py-pdf/PyPDF2/issues + Changelog = https://raw.githubusercontent.com/py-pdf/PyPDF2/main/CHANGELOG +classifiers = + Development Status :: 5 - Production/Stable + Intended Audience :: Developers + License :: OSI Approved :: BSD License + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.6 + Programming Language :: Python :: 3.7 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3.10 + Operating System :: OS Independent + Topic :: Software Development :: Libraries :: Python Modules + +[options] +packages = PyPDF2 +python_requires = >=2.7 diff --git a/setup.py b/setup.py index 9e16ec8ab4..76a5bc3804 100644 --- a/setup.py +++ b/setup.py @@ -1,27 +1,12 @@ #!/usr/bin/env python -from distutils.core import setup +from setuptools import setup import re -long_description = """ -A Pure-Python library built as a PDF toolkit. It is capable of: -- extracting document information (title, author, ...) -- splitting documents page by page -- merging documents page by page -- cropping pages -- merging multiple pages into a single page -- encrypting and decrypting PDF files -- and more! - -By being Pure-Python, it should run on any Python platform without any -dependencies on external libraries. It can also work entirely on StringIO -objects rather than file streams, allowing for PDF manipulation in memory. -It is therefore a useful tool for websites that manage or manipulate PDFs. -""" - -VERSIONFILE="PyPDF2/_version.py" -verstrline = open(VERSIONFILE, "rt").read() +VERSIONFILE = "PyPDF2/_version.py" +with open(VERSIONFILE, "rt") as fp: + verstrline = fp.read() VSRE = r"^__version__ = ['\"]([^'\"]*)['\"]" mo = re.search(VSRE, verstrline, re.M) if mo: @@ -29,24 +14,4 @@ else: raise RuntimeError("Unable to find version string in %s." % (VERSIONFILE)) -setup( - name="PyPDF2", - version=verstr, - description="PDF toolkit", - long_description=long_description, - author="Mathieu Fenniak", - author_email="biziqe@mathieu.fenniak.net", - maintainer="Phaseit, Inc.", - maintainer_email="PyPDF2@phaseit.net", - url="http://mstamy2.github.com/PyPDF2", - classifiers = [ - "Development Status :: 5 - Production/Stable", - "Intended Audience :: Developers", - "License :: OSI Approved :: BSD License", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 3", - "Operating System :: OS Independent", - "Topic :: Software Development :: Libraries :: Python Modules", - ], - packages=["PyPDF2"], - ) +setup(version=verstr) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000000..517e48ebaa --- /dev/null +++ b/tox.ini @@ -0,0 +1,10 @@ +[tox] +envlist = + py{27,34,35,36,37,38,39,310,py,py3} + +[testenv] +deps = + pillow + pytest + pytest-cov +commands = pytest Tests --cov --cov-report term-missing -vv