From: Bastian Germann Date: Wed, 28 Jun 2023 23:14:36 +0000 (+0100) Subject: Import pymupdf_1.22.5+ds1.orig.tar.xz X-Git-Tag: archive/raspbian/1.22.5+ds1-1+rpi1^2~4 X-Git-Url: https://dgit.raspbian.org/?a=commitdiff_plain;h=205878bffd7b365d1c059db74d024b8f09f6a73c;p=pymupdf.git Import pymupdf_1.22.5+ds1.orig.tar.xz [dgit import orig pymupdf_1.22.5+ds1.orig.tar.xz] --- 205878bffd7b365d1c059db74d024b8f09f6a73c diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..65cbde3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,34 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: +assignees: + +--- + +_**Please provide all mandatory information!**_ + +## Describe the bug (mandatory) +A clear and concise description of what the bug is. + +## To Reproduce (mandatory) +Explain the steps to reproduce the behavior, For example, include a minimal code snippet, example files, etc. + +For problems when building or installing PyMuPDF, give the full output of the build/install command so that, for example, all pip/compiler/linker errors/warnings can be seen. + +## Expected behavior (optional) +Describe what you expected to happen (if not obvious). + +## Screenshots (optional) +If applicable, add screenshots to help explain your problem. + +## Your configuration (mandatory) + - Operating system, potentially version and bitness + - Python version, bitness + - PyMuPDF version, installation method (**wheel** or **generated** from source). + +For example, the output of `print(sys.version, "\n", sys.platform, "\n", fitz.__doc__)` would be sufficient (for the first two bullets). + +## Additional context (optional) +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..92ad708 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Potentially add an issue reference. + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +Are there several options for how your request could be met? + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/build_wheels.yml b/.github/workflows/build_wheels.yml new file mode 100644 index 0000000..8761c45 --- /dev/null +++ b/.github/workflows/build_wheels.yml @@ -0,0 +1,102 @@ +name: Build wheels + +on: + workflow_dispatch: + inputs: + sdist: + type: boolean + wheels: + type: boolean + wheels_linux_aarch64: + type: boolean + wheels_macos_arm64: + type: boolean + +jobs: + + build_sdist: + if: ${{ inputs.sdist }} + name: Build sdist + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + #with: + # fetch-depth: 0 # Optional, use if you use setuptools_scm + # submodules: true # Optional, use if you have submodules + + - name: Build sdist + run: pipx run build --sdist + + - uses: actions/upload-artifact@v2 + with: + path: dist/*.tar.gz + + + build_wheels: + if: ${{ inputs.wheels }} + name: Build wheels on ${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-2019, macos-latest] + + steps: + + - uses: actions/checkout@v2 + + # Get Python for running cibuildwheel. This also ensures that 'python' + # works on MacOS, where it seems only 'python3' is available by default. + # + - uses: actions/setup-python@v2 + + # On Linux, get qemu so we can build for aarch64. + # + - name: Set up QEMU + if: runner.os == 'Linux' + uses: docker/setup-qemu-action@v1 + with: + platforms: all + + # Get cibuildwheel. + # + - name: Build wheels + uses: pypa/cibuildwheel@v2.11.2 + + # Set extra cibuildwheel options using environmental variables. + # + env: + # These exclusions are copied from PyMuPDF-1.19. + # + CIBW_SKIP: "pp* *i686 *-musllinux_* cp36*" + + # On Linux and MacOS, tell cibuildwheel to build archs depending on + # inputs.wheels_linux_aarch64 and inputs.wheels_macos_arm64. + # + # https://github.community/t/possible-to-use-conditional-in-the-env-section-of-a-job/135170 + # Note that it seems that there must not be a space after the ':' in the following, i.e. + # ok: {"false":"auto", "true":"auto aarch64"} + # bad: {"false": "auto", "true": "auto aarch64"} + # + # This is useful: https://yamlchecker.com/ + # + CIBW_ARCHS_LINUX: ${{ fromJSON('{"false":"auto", "true":"auto aarch64"}')[inputs.wheels_linux_aarch64] }} + CIBW_ARCHS_MACOS: ${{ fromJSON('{"false":"auto", "true":"auto arm64"}')[inputs.wheels_macos_arm64] }} + + # For testing, build for single python version. + # + #CIBW_BUILD: "cp311*" + + # Get cibuildwheel to run pytest with each wheel. + # + # Setting verbosity here sometimes seems to result in SEGV's when + # running pytest. + # + CIBW_TEST_REQUIRES: "fontTools pytest" + CIBW_TEST_COMMAND: "pytest -s {project}/tests" + CIBW_BUILD_VERBOSITY: 3 + + # Upload generated wheels, to be accessible from github Actions page. + # + - uses: actions/upload-artifact@v2 + with: + path: ./wheelhouse/*.whl diff --git a/.github/workflows/cla.yml b/.github/workflows/cla.yml new file mode 100644 index 0000000..412e498 --- /dev/null +++ b/.github/workflows/cla.yml @@ -0,0 +1,36 @@ +name: "CLA Assistant" +on: + issue_comment: + types: [created] + pull_request_target: + types: [opened,closed,synchronize] + +jobs: + CLAAssistant: + runs-on: ubuntu-latest + steps: + - name: "CLA Assistant" + if: (github.event.comment.body == 'recheck' || github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA') || github.event_name == 'pull_request_target' + # Beta Release + uses: contributor-assistant/github-action@v2.2.0 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # the below token should have repo scope and must be manually added by you in the repository's secret + PERSONAL_ACCESS_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }} + with: + path-to-signatures: 'signatures/version1/cla.json' + path-to-document: 'https://artifex.com/documents/Artifex%20Contributor%20License%20Agreement.pdf' + # branch should not be protected + branch: 'main' + allowlist: + + # the followings are the optional inputs - If the optional inputs are not given, then default values will be taken + #remote-organization-name: enter the remote organization name where the signatures should be stored (Default is storing the signatures in the same repository) + #remote-repository-name: enter the remote repository name where the signatures should be stored (Default is storing the signatures in the same repository) + #create-file-commit-message: 'For example: Creating file for storing CLA Signatures' + #signed-commit-message: 'For example: $contributorName has signed the CLA in #$pullRequestNo' + #custom-notsigned-prcomment: 'pull request comment with Introductory message to ask new contributors to sign' + #custom-pr-sign-comment: 'The signature to be committed in order to sign the CLA' + #custom-allsigned-prcomment: 'pull request comment when all contributors has signed, defaults to **CLA Assistant Lite bot** All Contributors have signed the CLA.' + #lock-pullrequest-aftermerge: false - if you don't want this bot to automatically lock the pull request after merging (default - true) + #use-dco-flag: true - If you are using DCO instead of CLA diff --git a/.github/workflows/test-valgrind.yml b/.github/workflows/test-valgrind.yml new file mode 100644 index 0000000..3806a1e --- /dev/null +++ b/.github/workflows/test-valgrind.yml @@ -0,0 +1,99 @@ +name: Test valgrind + +on: + schedule: + - cron: '13 5 * * *' + workflow_dispatch: + +jobs: + + test_valgrind: + name: Test valgrind + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + + # Avoid cancelling of all runs after a single failure. + fail-fast: false + + steps: + + - uses: actions/checkout@v2 + + - uses: actions/setup-python@v2 + with: + # python-3.11 seems to generate valgrind errors e.g. 'Use of + # uninitialised value of size 8' in Py_INCREF. + # + # python-3.9 works. + # python-3.10 works. + python-version: '3.10' + + - name: Test valgrind + + run: | + import os + import subprocess + import sys + + def log(text): + print(f'test-valgrind.yml: {text}') + sys.stdout.flush() + + def run(command, env_extra=None): + env = None + if env_extra: + env = os.environ.copy() + env.update(env_extra) + log(f'Adding environment:') + for n, v in env_extra.items(): + log(f' {n}: {v!r}') + log(f'Running: {command}') + sys.stdout.flush() + subprocess.run(command, check=1, shell=1, env=env) + + # Change into parent directory (we will originally be inside the + # PyMuPDF checkout), otherwise there's potential confusion caused + # by the `fitz/` directory not being the installed `fitz` module. + # + log('Changing into parent directory of checkout.') + leaf = os.path.basename(os.getcwd()) + log(f'{os.getcwd()=}') + os.chdir('..') + log(f'{os.getcwd()=}') + + log('Installing valgrind.') + run(f'sudo apt install valgrind') + + log('Creating venv.') + run(f'{sys.executable} -m venv pylocal') + + log('Install required python packages.') + run(f'./pylocal/bin/python -m pip install -U pip') + run(f'./pylocal/bin/python -m pip install pytest fontTools') + + log('Installing PyMuPDF.') + if 0: + # Useful for quick testing - use pypi.org package instead of + # building locally. + run(f'./pylocal/bin/python -m pip install pymupdf') + else: + run( + f'./pylocal/bin/python -m pip install -vv ./{leaf}', + env_extra=dict( + PYMUPDF_SETUP_MUPDF_TGZ='', + PYMUPDF_SETUP_MUPDF_BUILD='git:--recursive --depth 1 --shallow-submodules --branch master https://github.com/ArtifexSoftware/mupdf.git', + PYMUPDF_SETUP_MUPDF_BUILD_TYPE='debug', + ), + ) + + log('Running PyMuPDF tests under valgrind.') + # We ignore memory leaks. + run(f'valgrind --error-exitcode=100 --errors-for-leak-kinds=none --fullpath-after= ./pylocal/bin/python -m pytest -s -vv {leaf}', + env_extra=dict( + PYTHONMALLOC='malloc', + ), + ) + + shell: python diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..214df83 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,51 @@ +name: Test + +on: + schedule: + - cron: '13 5 * * *' + workflow_dispatch: + +jobs: + + test: + # Build+test current PyMuPDF git. + # + name: Test + runs-on: ${{ matrix.os }} + strategy: + matrix: + # 2023-05-09: Builds on Windows-latest do not work because our wdev.py + # (used to build MuPDF) picks up a later VS than setuptools (when + # building SWIG-generated PyMuPDF code). + # + os: [ubuntu-latest, windows-2019, macos-latest] + + # Avoid cancelling of all cibuildwheel runs after a single failure. + fail-fast: false + + steps: + + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + + # Set up cibuildwheel. + # + - name: cibuildwheel + uses: pypa/cibuildwheel@v2.11.2 + + env: + # Build will use the default hard-coded mupdf URL. + + # Build on single cpu. + CIBW_ARCHS_LINUX: x86_64 + + # Build for single python version. + CIBW_BUILD: "cp311*" + + # Don't build for unsupported platforms or win32. + CIBW_SKIP: "pp* *i686 *-musllinux_* cp36* *win32*" + + # Get cibuildwheel to run pytest with each wheel. + CIBW_TEST_REQUIRES: "fontTools pytest" + CIBW_TEST_COMMAND: "pytest -s {project}/tests" + CIBW_BUILD_VERBOSITY: 3 diff --git a/.github/workflows/test_mupdf-master-branch.yml b/.github/workflows/test_mupdf-master-branch.yml new file mode 100644 index 0000000..0bb7f5a --- /dev/null +++ b/.github/workflows/test_mupdf-master-branch.yml @@ -0,0 +1,53 @@ +name: Test mupdf master branch + +on: + schedule: + - cron: '13 6 * * *' + workflow_dispatch: + +jobs: + + test_mupdf_master_branch: + # Build+test current PyMuPDF git with mupdf git master branch. + # + name: Test mupdf master branch + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-2019, macos-latest] + + # Avoid cancelling of all cibuildwheel runs after a single failure. + fail-fast: false + + steps: + + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + + # Set up cibuildwheel. + # + - name: cibuildwheel + uses: pypa/cibuildwheel@v2.11.2 + + env: + # PYMUPDF_SETUP_MUPDF_TGZ="": don't embed mupdf in sdist - no need + # because the build stage gets MuPDF using `git clone ...`. + # + # PYMUPDF_SETUP_MUPDF_BUILD="git:...": build with mupdf from a `git + # clone` command, selecting the current master branch. + # + CIBW_ENVIRONMENT: PYMUPDF_SETUP_MUPDF_TGZ="" PYMUPDF_SETUP_MUPDF_BUILD="git:--recursive --depth 1 --shallow-submodules --branch master https://github.com/ArtifexSoftware/mupdf.git" + + # Build on single cpu. + CIBW_ARCHS_LINUX: x86_64 + + # Build for single python version. + CIBW_BUILD: "cp311*" + + # Don't build for unsupported platforms. + CIBW_SKIP: "pp* *i686 *-musllinux_* cp36*" + + # Get cibuildwheel to run pytest with each wheel. + CIBW_TEST_REQUIRES: "fontTools pytest" + CIBW_TEST_COMMAND: "pytest -s {project}/tests" + CIBW_BUILD_VERBOSITY: 3 diff --git a/.github/workflows/test_mupdf-release-branch.yml b/.github/workflows/test_mupdf-release-branch.yml new file mode 100644 index 0000000..9f87834 --- /dev/null +++ b/.github/workflows/test_mupdf-release-branch.yml @@ -0,0 +1,53 @@ +name: Test mupdf release branch + +on: + schedule: + - cron: '20 6 * * *' + workflow_dispatch: + +jobs: + + test_mupdf_release_branch: + # Build+test current PyMuPDF git with mupdf git release branch. + # + name: Test mupdf release branch + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-2019, macos-latest] + + # Avoid cancelling of all cibuildwheel runs after a single failure. + fail-fast: false + + steps: + + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + + # Set up cibuildwheel. + # + - name: cibuildwheel + uses: pypa/cibuildwheel@v2.11.2 + + env: + # PYMUPDF_SETUP_MUPDF_TGZ="": don't embed mupdf in sdist - no need + # because the build stage gets MuPDF using `git clone ...`. + # + # PYMUPDF_SETUP_MUPDF_BUILD="git:...": build with mupdf from a `git + # clone` command, selecting the current release branch. + # + CIBW_ENVIRONMENT: PYMUPDF_SETUP_MUPDF_TGZ="" PYMUPDF_SETUP_MUPDF_BUILD="git:--recursive --depth 1 --shallow-submodules --branch 1.22.x https://github.com/ArtifexSoftware/mupdf.git" + + # Build on single cpu. + CIBW_ARCHS_LINUX: x86_64 + + # Build for single python version. + CIBW_BUILD: "cp311*" + + # Don't build for unsupported platforms. + CIBW_SKIP: "pp* *i686 *-musllinux_* cp36*" + + # Get cibuildwheel to run pytest with each wheel. + CIBW_TEST_REQUIRES: "fontTools pytest" + CIBW_TEST_COMMAND: "pytest -s {project}/tests" + CIBW_BUILD_VERBOSITY: 3 diff --git a/.github/workflows/test_quick.yml b/.github/workflows/test_quick.yml new file mode 100644 index 0000000..a1fe5bb --- /dev/null +++ b/.github/workflows/test_quick.yml @@ -0,0 +1,51 @@ +name: Test quick + +on: + pull_request: + branches: [main] + + workflow_dispatch: + +jobs: + + test_quick: + name: Test quick + runs-on: ${{ matrix.os }} + strategy: + matrix: + # We test on just Ubuntu, with hard-coded MuPDF, MuPDF master, and current MuPDF branch. + # + os: [ubuntu-latest] + environment: [ + '', + 'PYMUPDF_SETUP_MUPDF_TGZ="" PYMUPDF_SETUP_MUPDF_BUILD="git:--recursive --depth 1 --shallow-submodules --branch master https://github.com/ArtifexSoftware/mupdf.git"', + 'PYMUPDF_SETUP_MUPDF_TGZ="" PYMUPDF_SETUP_MUPDF_BUILD="git:--recursive --depth 1 --shallow-submodules --branch 1.22.x https://github.com/ArtifexSoftware/mupdf.git"', + ] + + # Avoid cancelling of all cibuildwheel runs after a single failure. + fail-fast: false + + steps: + + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + + - name: cibuildwheel + uses: pypa/cibuildwheel@v2.11.2 + + env: + CIBW_ENVIRONMENT: ${{matrix.environment}} + + # Build on single cpu. + CIBW_ARCHS_LINUX: x86_64 + + # Build for single python version. + CIBW_BUILD: "cp311*" + + # Don't build for unsupported platforms or win32. + CIBW_SKIP: "pp* *i686 *-musllinux_* cp36* *win32*" + + # Get cibuildwheel to run pytest with each wheel. + CIBW_TEST_REQUIRES: "fontTools pytest" + CIBW_TEST_COMMAND: "pytest -s {project}/tests" + CIBW_BUILD_VERBOSITY: 3 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..23e9547 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +*.pyc +*.so +*.o +*.swp +build/ +demo/README.rst +docs/build \ No newline at end of file diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..7e016e4 --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,29 @@ +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the version of Python and other tools you might need +build: + os: ubuntu-20.04 + tools: + python: "3.9" + # You can also specify other tool versions: + # nodejs: "16" + # rust: "1.55" + # golang: "1.17" + +# Build documentation in the docs/ directory with Sphinx +sphinx: + configuration: docs/conf.py + +# If using Sphinx, optionally build your docs in additional formats such as PDF +formats: + - pdf + +# Optionally declare the Python requirements required to build your docs +python: + install: + - requirements: docs/requirements.txt diff --git a/.vs/ProjectSettings.json b/.vs/ProjectSettings.json new file mode 100644 index 0000000..8d55e0d --- /dev/null +++ b/.vs/ProjectSettings.json @@ -0,0 +1,3 @@ +{ + "CurrentProjectSetting": "" +} diff --git a/.vs/PyMuPDF/v15/.suo b/.vs/PyMuPDF/v15/.suo new file mode 100644 index 0000000..f061189 Binary files /dev/null and b/.vs/PyMuPDF/v15/.suo differ diff --git a/.vs/PyMuPDF/v15/Browse.VC.db b/.vs/PyMuPDF/v15/Browse.VC.db new file mode 100644 index 0000000..ccb14f9 Binary files /dev/null and b/.vs/PyMuPDF/v15/Browse.VC.db differ diff --git a/.vs/VSWorkspaceState.json b/.vs/VSWorkspaceState.json new file mode 100644 index 0000000..48ce01f --- /dev/null +++ b/.vs/VSWorkspaceState.json @@ -0,0 +1,7 @@ +{ + "ExpandedNodes": [ + "" + ], + "SelectedNode": "", + "PreviewInSolutionExplorer": false +} diff --git a/.vs/slnx.sqlite b/.vs/slnx.sqlite new file mode 100644 index 0000000..98aef7b Binary files /dev/null and b/.vs/slnx.sqlite differ diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..dba13ed --- /dev/null +++ b/COPYING @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..2470e2f --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,5 @@ +include fitz/*.i +include fitz/_config.h +include mupdf.tgz +recursive-include tests * +global-exclude __pycache__/* diff --git a/README.md b/README.md new file mode 100644 index 0000000..2b5131a --- /dev/null +++ b/README.md @@ -0,0 +1,99 @@ +# PyMuPDF 1.22.5 + +![logo](https://artifex.com/images/logos/py-mupdf-github-icon.png) + + +Release date: June 21, 2023 + +On **[PyPI](https://pypi.org/project/PyMuPDF)** since August 2016: [![Downloads](https://static.pepy.tech/personalized-badge/pymupdf?period=total&units=international_system&left_color=black&right_color=orange&left_text=Downloads)](https://pepy.tech/project/pymupdf) + +# Author +[Artifex](mailto:support@artifex.com), based on code by [Jorj X. McKie](mailto:jorj.x.mckie@outlook.de) and [Ruikai Liu](mailto:lrk700@gmail.com). + +# Introduction + +PyMuPDF adds Python bindings and abstractions to [MuPDF](https://mupdf.com/), a lightweight PDF, XPS, and eBook viewer, renderer, and toolkit. Both PyMuPDF and MuPDF are maintained and developed by Artifex Software, Inc. + +MuPDF can access files in PDF, XPS, OpenXPS, CBZ, EPUB and FB2 (eBooks) formats, and it is known for its top performance and exceptional rendering quality. + +With PyMuPDF you can access files with extensions like `.pdf`, `.xps`, `.oxps`, `.cbz`, `.fb2` or `.epub`. In addition, about 10 popular image formats can also be handled like documents: `.png`, `.jpg`, `.bmp`, `.tiff`, etc. + +# Usage +For all supported document types (i.e. **_including images_**) you can +* Decrypt the document. +* Access meta information, links and bookmarks. +* Render pages in raster formats (PNG and some others), or the vector format SVG. +* Search for text. +* Extract text and images. +* Convert to other formats: PDF, (X)HTML, XML, JSON, text. +* Do OCR (Optical Character Recognition) if Tesseract is installed. + +> To some degree, PyMuPDF can also be used as an [image converter](https://github.com/pymupdf/PyMuPDF/wiki/How-to-Convert-Images): it can read a range of input formats and can produce **Portable Network Graphics (PNG)**, **Portable Anymaps** (**PNM**, etc.), **Portable Arbitrary Maps (PAM)**, **Adobe PostScript** and **Adobe Photoshop** documents, making the use of other graphics packages obselete in these cases. But interfacing with e.g. PIL/Pillow for image input and output is easy as well. + +For **PDF documents,** there exists a plethora of additional features: they can be created, joined or split up. Pages can be inserted, deleted, re-arranged or modified in many ways (including annotations and form fields). + +* Images and fonts can be extracted or inserted. + > You may want to have a look at [this](https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/edit-images/edit.py) cool GUI example script, which lets you **_insert, delete, replace_** or **_re-position_** images under your visual control. + + > If [fontTools](https://pypi.org/project/fonttools/) is installed, subsets can be built for eligible fonts based on their usage in the document. Especially for new PDFs, this can lead to significant file size reductions. +* Embedded files are fully supported. +* PDFs can be reformatted to support double-sided printing, posterizing, applying logos or watermarks +* Password protection is fully supported: decryption, encryption, encryption method selection, permission level and user / owner password setting. +* Support of the **PDF Optional Content** concept for images, text and drawings. +* Low-level PDF structures can be accessed and modified. +* **Command line module** ``"python -m fitz ..."``. A versatile utility with the following features + + - **encryption / decryption / optimization** + - creation of **sub-documents** + - document **joining** + - **image / font extraction** + - full support of **embedded files** + - **_layout-preserving text extraction_** (all documents) + + +Have a look at the basic [demos](https://github.com/pymupdf/PyMuPDF-Utilities/tree/master/demo), the [examples](https://github.com/pymupdf/PyMuPDF-Utilities/tree/master/examples) (which contain complete, working programs), and [notebooks](https://github.com/pymupdf/PyMuPDF-Utilities/tree/master/jupyter-notebooks). + + +# Documentation + +Documentation is written using Sphinx and is available online. It is currently a combination of a reference guide and user manual. + +* You can view it online at [Read the Docs](https://pymupdf.readthedocs.io). This site also provides download options for PDF. +* For a **quick start** look at the [tutorial](https://pymupdf.readthedocs.io/en/latest/tutorial.html) and the [recipes](https://pymupdf.readthedocs.io/en/latest/faq.html) chapters. + +The latest changelog can be viewed [here](https://pymupdf.readthedocs.io/en/latest/changes.html). + + +# Installation + +PyMuPDF **requires Python 3.7 or later**. + +For versions 3.7 and up, Python wheels exist for **Windows** (32bit and 64bit), **Linux** (64bit, Intel and ARM) and **Mac OSX** (64bit, Intel only), so it can be installed from [PyPI](https://pypi.org/search/?q=pymupdf) in the usual way. To ensure pip support for the latest wheel platform tags, we strongly recommend to always upgrade pip first. + + python -m pip install --upgrade pip + python -m pip install --upgrade pymupdf + +There are **no mandatory** external dependencies. However, some **optional features** become available only if additional packages are installed: + +* [Pillow](https://pypi.org/project/Pillow/) for using pillow image output directly from PyMuPDF +* [fontTools](https://pypi.org/project/fonttools/) for creating font subsets. +* [pymupdf-fonts](https://pypi.org/project/pymupdf-fonts/) contains some nice fonts for your text output. +* [Tesseract-OCR](https://github.com/tesseract-ocr/tesseract) for optical character recognition in images and document pages. Tesseract is separate software, not a Python package. To enable OCR functions in PyMuPDF, the system environment variable `"TESSDATA_PREFIX"` must be defined and contain the `tessdata` folder name of the Tesseract installation location. + + +Older wheels - also with support for older Python versions - can be found [here](https://github.com/pymupdf/PyMuPDF-Optional-Material/tree/master/wheels-upto-Py3.5) and on PyPI. + +> **Note:** If `pip` cannot find a wheel that is compatible with your platform, it will automatically build and install from source using the PyMuPDF sdist; this requires only that SWIG is installed on your system. + +# License and Copyright + +PyMuPDF and MuPDF are available under both, open-source AGPL and commercial license agreements. + +Please read the full text of the [AGPL license agreement](https://www.gnu.org/licenses/agpl-3.0.html) (which is also included here in file COPYING) to ensure that your use case complies with the guidelines of this license. If you determine you cannot meet the requirements of the AGPL, please contact [Artifex](https://artifex.com/contact/) for more information regarding a commercial license. + +Artifex is the exclusive commercial licensing agent for MuPDF. + +Artifex, the Artifex logo, MuPDF, and the MuPDF logo are registered trademarks of Artifex Software Inc. PyMuPDF and the PyMuPDF logo are trademarks of Artifex Software, Inc. © 2022 Artifex Software, Inc. All rights reserved. + +# Contact +Please use the [Discussions](https://github.com/pymupdf/PyMuPDF/discussions) menu for questions, comments, or asking for help, and submit issues [here](https://github.com/pymupdf/PyMuPDF/issues). diff --git a/changes.txt b/changes.txt new file mode 100644 index 0000000..2b89391 --- /dev/null +++ b/changes.txt @@ -0,0 +1,1733 @@ +Change Log +========== + + +**Changes in version 1.22.5 (2023-06-21)** + +* This release uses ``MuPDF-1.22.2``. + +* Bug fixes: + + * **Fixed** `#2365 `_: Incorrect dictionary values for type "fs" drawings. + * **Fixed** `#2391 `_: Check box automatically uncheck when we update same checkbox more than 1 times. + * **Fixed** `#2400 `_: Gaps within text of same line not filled with spaces. + * **Fixed** `#2404 `_: Blacklining an image in PDF won't remove underlying content in version 1.22.X. + * **Fixed** `#2430 `_: Incorrectly reducing ref count of Py_None. + * **Fixed** `#2450 `_: Empty fill color and fill opacity for paths with fill and stroke operations with 1.22.* + * **Fixed** `#2462 `_: Error at "get_drawing(extended=True )" + * **Fixed** `#2468 `_: Decode error when trying to get drawings + +* New features: + + * **Changed** Annotations now support "cloudy" borders. + The :attr:`Annot.border` property has the new item `clouds`, + and method :meth:`Annot.set_border` supports the corresponding `clouds` argument. + + * **Changed** Radio button widgets in the same RB group + are now consistently updated **if the group is defined in the standard way**. + + * **Added** Support for the `/Locked` key in PDF Optional Content. + This array inside the catalog entry `/OCProperties` can now be extracted and set. + + * **Added** Support for new parameter `tessdata` in OCR functions. + New function :meth:`get_tessdata` locates the language support folder if Tesseract is installed. + + + +**Changes in version 1.22.3 (2023-05-10)** + +* This release uses ``MuPDF-1.22.0``. + +* Bug fixes: + + * **Fixed** `#2333 `_: Unable to set any of button radio group in form + + +**Changes in version 1.22.2 (2023-04-26)** + +* This release uses ``MuPDF-1.22.0``. + +* Bug fixes: + + * **Fixed** `#2369 `_: Image extraction bugs with newer versions + + +**Changes in version 1.22.1 (2023-04-18)** + +* This release uses ``MuPDF-1.22.0``. + +* Bug fixes: + + * **Fixed** `#2345 `_: Turn off print statements in utils.py + * **Fixed** `#2348 `_: extract_image returns an extension "flate" instead of "png" + * **Fixed** `#2350 `_: Can not make widget (checkbox) to read-only by adding flags PDF_FIELD_IS_READ_ONLY + * **Fixed** `#2355 `_: 1.22.0 error when using get_toc (AttributeError: 'SwigPyObject' object has no attribute) + + +**Changes in version 1.22.0 (2023-04-14)** + +* This release uses ``MuPDF-1.22.0``. + +* Behavioural changes: + + * Text extraction now includes glyphs that overlap with clip rect; previously + they were included only if they were entirely contained within the clip + rect. + +* Bug fixes: + + * **Fixed** `#1763 `_: Interactive(smartform) form PDF calculation not working in pymupdf + * **Fixed** `#1995 `_: RuntimeError: image is too high for a long paged pdf file when trying + * **Fixed** `#2093 `_: Image in pdf changes color after applying redactions + * **Fixed** `#2108 `_: Redaction removing more text than expected + * **Fixed** `#2141 `_: Failed to read JPX header when trying to get blocks + * **Fixed** `#2144 `_: Replace image throws an error + * **Fixed** `#2146 `_: Wrong Handling of Reference Count of "None" Object + * **Fixed** `#2161 `_: Support adding images as pages directly + * **Fixed** `#2168 `_: ``page.add_highlight_annot(start=pointa, stop=pointb)`` not working + * **Fixed** `#2173 `_: Double free of ``Colorspace`` used in ``Pixmap`` + * **Fixed** `#2179 `_: Incorrect documentation for ``pixmap.tint_with()`` + * **Fixed** `#2208 `_: Pushbutton widget appears as check box + * **Fixed** `#2210 `_: ``apply_redactions()`` move pdf text to right after redaction + * **Fixed** `#2220 `_: ``Page.delete_image()`` | object has no attribute ``is_image`` + * **Fixed** `#2228 `_: open some pdf cost too much time + * **Fixed** `#2238 `_: Bug - can not extract data from file in the newest version 1.21.1 + * **Fixed** `#2242 `_: Python quits silently in ``Story.element_positions()`` if callback function prototype is wrong + * **Fixed** `#2246 `_: TextWriter write text in a wrong position + * **Fixed** `#2248 `_: After redacting the content, the position of the remaining text changes + * **Fixed** `#2250 `_: docs: unclear or broken link in page.rst + * **Fixed** `#2251 `_: mupdf_display_errors does not apply to Pixmap when loading broken image + * **Fixed** `#2270 `_: ``Annot.get_text("words")`` - doesn't return the first line of words + * **Fixed** `#2275 `_: insert_image: document that rotations are counterclockwise + * **Fixed** `#2278 `_: Can not make widget (checkbox) to read-only by adding flags PDF_FIELD_IS_READ_ONLY + * **Fixed** `#2290 `_: Different image format/data from Page.get_text("dict") and Fitz.get_page_images() + * **Fixed** `#2293 `_: 68 failed tests when installing from sdist on my box + * **Fixed** `#2300 `_: Too much recursion in tree (parents), makes program terminate + * **Fixed** `#2322 `_: add_highlight_annot using clip generates "A Number is Out of Range" error in PDF + +* Other: + + * Add key "/AS (Yes)" to the underlying annot object of a selected button form field. + + * Remove unused ``Document`` methods ``has_xref_streams()`` and + ``has_old_style_xrefs()`` as MuPDF equivalents have been removed. + + * Add new ``Document`` methods and properties for getting/setting + ``/PageMode``, ``/PageLayout`` and ``/MarkInfo``. + + * New ``Document`` property ``version_count``, which contains the number of + incremental saves plus one. + + * New ``Document`` property ``is_fast_webaccess`` which tells whether the + document is linearized. + + * ``DocumentWriter`` is now a context manager. + + * Add support for ``Pixmap`` JPEG output. + + * Add support for drawing rectangles with rounded corners. + + * ``get_drawings()``: added optional ``extended`` arg. + + * Fixed issue where trace devices' state was not being initialised + correctly; data returned from things like ``fitz.Page.get_texttrace()`` + might be slightly altered, e.g. ``linewidth`` values. + + * Output warning to ``stderr`` if it looks like we are being used with + current directory containing an invalid ``fitz/`` directory, because + this can break import of ``fitz`` module. For example this happens + if one attempts to use ``fitz`` when current directory is a PyMuPDF + checkout. + +* Documentation: + + * General rework: + + * Introduces a new home page and new table of contents. + * Structural update to include new About section. + * Comparison & performance graphing. + * Includes performance methodology in appendix. + * Updates conf.py to understand single back-ticks as code. + * Converts double back-ticks to single back-ticks. + * Removes redundant files. + + * Improve ``insert_file()`` documentation. + + * ``get_bboxlog()``: aded optional ``layers`` to ``get_bboxlog()``. + * ``Page.get_texttrace()``: add new dictionary key ``layer``, name of Optional Content Group. + + * Mention use of Python venv in installation documentation. + + * Added missing fix for #2057 to release 1.21.1's changelog. + + * Fixes many links to the PyMuPDF-Utilities repo scripts. + + * Avoid duplication of ``changes.txt`` and ``docs/changes.rst``. + +* Build + + * Added ``pyproject.toml`` file to improve builds using pip etc. + + + +**Changes in Version 1.21.1 (2022-12-13)** + +* This release uses ``MuPDF-1.21.1``. + +* Bug fixes: + + * **Fixed** `#2110 `_: Fully embedded font is extracted only partially if it occupies more than one object + * **Fixed** `#2094 `_: Rectangle Detection Logic + * **Fixed** `#2088 `_: Destination point not set for named links in toc + * **Fixed** `#2087 `_: Image with Filter "[/FlateDecode/JPXDecode]" not extracted + * **Fixed** `#2086 `_: Document.save() owner_pw & user_pw has buffer overflow bug + * **Fixed** `#2076 `_: Segfault in fitz.py + * **Fixed** `#2057 `_: Document.save garbage parameter not working in PyMuPDF 1.21.0 + * **Fixed** `#2051 `_: Missing DPI Parameter + * **Fixed** `#2048 `_: Invalid size of TextPage and bbox with newest version 1.21.0 + * **Fixed** `#2045 `_: SystemError: returned a result with an error set + * **Fixed** `#2039 `_: 1.21.0 fails to build against system libmupdf + * **Fixed** `#2036 `_: Archive::Archive defined twice + +* Other + + * Swallow "&zoom=nan" in link uri strings. + * Add new Page utility methods ``Page.replace_image()`` and ``Page.delete_image()``. + +* Documentation: + + * `#2040 `_: Added note about test failure with non-default build of MuPDF, to ``tests/README.md``. + * `#2037 `_: In ``docs/installation.rst``, mention incompatibility with chocolatey.org on Windows. + * `#2061 `_: Fixed description of ``Annot.file_info``. + * `#2065 `_: Show how to insert internal PDF link. + * Improved description of building from source without an sdist. + * Added information about running tests. + * `#2084 `_: Fixed broken link to PyMuPDF-Utilities. + + +**Changes in Version 1.21.0 (2022-11-8)** + +* This release uses ``MuPDF-1.21.0``. + +* New feature: Stories. + +* Added wheels for Python-3.11. + +* Bug fixes: + + * **Fixed** `#1701 `_: Broken custom image insertion. + * **Fixed** `#1854 `_: `Document.delete_pages()` declines keyword arguments. + * **Fixed** `#1868 `_: Access Violation Error at `page.apply_redactions()`. + * **Fixed** `#1909 `_: Adding text with `fontname="Helvetica"` can silently fail. + * **Fixed** `#1913 `_: `draw_rect()`: does not respect width if color is not specified. + * **Fixed** `#1917 `_: `subset_fonts()`: make it possible to silence the stdout. + * **Fixed** `#1936 `_: Rectangle detection can be incorrect producing wrong output. + * **Fixed** `#1945 `_: Segmentation fault when saving with `clean=True`. + * **Fixed** `#1965 `_: `pdfocr_save()` Hard Crash. + * **Fixed** `#1971 `_: Segmentation fault when using `get_drawings()`. + * **Fixed** `#1946 `_: `block_no` and `block_type` switched in `get_text()` docs. + * **Fixed** `#2013 `_: AttributeError: 'Widget' object has no attribute '_annot' in delete widget. + +* Misc changes to core code: + + * Fixed various compiler warnings and a sequence-point bug. + * Added support for Memento builds. + * Fixed leaks detected by Memento in test suite. + * Fixed handling of exceptions in set_name() and set_rect(). + * Allow build with latest MuPDF, for regular testing of PyMuPDF master. + * Cope with new MuPDF exceptions when setting rect for some Annot types. + * Reduced cosmetic differences between MuPDF's config.h and PyMuPDF's _config.h. + * Cope with various changes to MuPDF API. + +* Other: + + * Fixed various broken links and typos in docs. + * Mention install of `swig-python` on MacOS for #875. + * Added (untested) wheels for macos-arm64. + + + + +**Changes in Version 1.20.2** + +* This release uses ``MuPDF-1.20.3``. + +* **Fixed** `#1787 `_. + Fix linking issues on Unix systems. + +* **Fixed** `#1824 `_. + SegFault when applying redactions overlapping a transparent image. (Fixed + in ``MuPDF-1.20.3``.) + +* Improvements to documentation: + + * Improved information about building from source in ``docs/installation.rst``. + * Clarified memory allocation setting ``JM_MEMORY` in ``docs/tools.rst``. + * Fixed link to PDF Reference manual in ``docs/app3.rst``. + * Fixed building of html documentation on OpenBSD. + * Moved old ``docs/faq.rst`` into separate ``docs/recipes-*`` files. + +* Removed some unused files and directories: + + * ``installation/`` + * ``docs/wheelnames.txt`` + + +**Changes in Version 1.20.1** + +* **Fixed** `#1724 `_. + Fix for building on FreeBSD. + +* **Fixed** `#1771 `_. + `linkDest()` had a broken call to `re.match()`, introduced in 1.20.0. + +* **Fixed** `#1751 `_. + `get_drawings()` and `get_cdrawings()` previously always returned with `closePath=False`. + +* **Fixed** `#1645 `_. + Default FreeText annotation text color is now black. + +* Improvements to sphinx-generated documentation: + + * Use readthedocs theme with enhancements. + * Renamed the `.txt` files to have `.rst` suffixes. + +------ + +**Changes in Version 1.20.0** + +This release uses ``MuPDF-1.20.0``, released 2022-06-15. + +* Cope with new MuPDF link uri format, changed from ``#,,`` to ``#page=&zoom=,,``. + + * In ``tests/test_insertpdf.py``, use new reference output ``joined-1.20.pdf``. We also check that new output values are approximately the same as the old ones. + +* **Fixed** `#1738 `_. Leak of `pdf_graft_map`. + Also fixed a SEGV issue that this seemed to expose, caused by incorrect freeing of underlying fz_document. + +* **Fixed** `#1733 `_. Fixed ownership of `Annotation.get_pixmap()`. + +Changes to build/release process: + +* If pip builds from source because an appropriate wheel is not available, we no longer require MuPDF to be pre-installed. Instead the required MuPDF source is embedded in the sdist and automatically built into PyMuPDF. + +* Various changes to ``setup.py`` to download the required MuPDF release as required. See comments at start of setup.py for details. + +* Added ``.github/workflows/build_wheels.yml`` to control building of wheels on Github. + +------ + +**Changes in Version 1.19.6** + +* **Fixed** `#1620 `_. The :ref:`TextPage` created by :meth:`Page.get_textpage` will now be freed correctly (removed memory leak). +* **Fixed** `#1601 `_. Document open errors should now be more concise and easier to interpret. In the course of this, two PyMuPDF-specific Python exceptions have been **added:** + + - ``EmptyFileError`` -- raised when trying to create a :ref:`Document` (``fitz.open()``) from an empty file or zero-length memory. + - ``FileDataError`` -- raised when MuPDF encounters irrecoverable document structure issues. + +* **Added** :meth:`Page.load_widget` given a PDF field's xref. + +* **Added** Dictionary :attr:`pdfcolor` which provide the about 500 colors defined as PDF color values with the lower case color name as key. + +* **Added** algebra functionality to the :ref:`Quad` class. These objects can now also be added and subtracted among themselves, and be multiplied by numbers and matrices. + +* **Added** new constants defining the default text extraction flags for more comfortable handling. Their naming convention is like :data:`TEXTFLAGS_WORDS` for ``page.get_text("words")``. See :ref:`text_extraction_flags`. + +* **Changed** :meth:`Page.annots` and :meth:`Page.widgets` to detect and prevent reloading the page (illegally) inside the iterator loops via :meth:`Document.reload_page`. Doing this brings down the interpretor. Documented clean ways to do annotation and widget mass updates within properly designed loops. + +* **Changed** several internal utility functions to become standalone ("SWIG inline") as opposed to be part of the :ref:`Tools` class. This, among other things, increases the performance of geometry object creation. + +* **Changed** :meth:`Document.update_stream` to always accept stream updates - whether or not the dictionary object behind the xref already is a stream. Thus the former ``new`` parameter is now ignored and will be removed in v1.20.0. + + +------ + +**Changes in Version 1.19.5** + +* **Fixed** `#1518 `_. A limited "fix": in some cases, rectangles and quadrupels were not correctly encoded to support re-drawing by :ref:`Shape`. + +* **Fixed** `#1521 `_. This had the same ultimate reason behind issue #1510. + +* **Fixed** `#1513 `_. Some Optional Content functions did not support non-ASCII characters. + +* **Fixed** `#1510 `_. Support more soft-mask image subtypes. + +* **Fixed** `#1507 `_. Immunize against items in the outlines chain, that are ``"null"`` objects. + +* **Fixed** re-opened `#1417 `_. ("too many open files"). This was due to insufficient calls to MuPDF's ``fz_drop_document()``. This also fixes `#1550 `_. + +* **Fixed** several undocumented issues in relation to incorrectly setting the text span origin :data:`point_like`. + +* **Fixed** undocumented error computing the character bbox in method :meth:`Page.get_texttrace` when text is **flipped** (as opposed to just rotated). + +* **Added** items to the dictionary returned by :meth:`image_properties`: ``orientation`` and ``transform`` report the natural image orientation (EXIF data). + +* **Added** method :meth:`Document.xref_copy`. It will make a given target PDF object an exact copy of a source object. + + +------ + +**Changes in Version 1.19.4** + + +* **Fixed** `#1505 `_. Immunize against circular outline items. + +* **Fixed** `#1484 `_. Correct CropBox coordinates are now returned in all situations. + +* **Fixed** `#1479 `_. + +* **Fixed** `#1474 `_. TextPage objects are now properly deleted again. + +* **Added** :ref:`Page` methods and attributes for PDF ``/ArtBox``, ``/BleedBox``, ``/TrimBox``. + +* **Added** global attribute :attr:`TESSDATA_PREFIX` for easy checking of OCR support. + +* **Changed** :meth:`Document.xref_set_key` such that dictionary keys will physically be removed if set to value ``"null"``. + +* **Changed** :meth:`Document.extract_font` to optionally return a dictionary (instead of a tuple). + +------ + +**Changes in Version 1.19.3** + +This patch version implements minor improvements for :ref:`Pixmap` and also some important fixes. + +* **Fixed** `#1351 `_. Reverted code that introduced the memory growth in v1.18.15. + +* **Fixed** `#1417 `_. Developped circumvention for growth of open file handles using :meth:`Document.insert_pdf`. + +* **Fixed** `#1418 `_. Developped circumvention for memory growth using :meth:`Document.insert_pdf`. + +* **Fixed** `#1430 `_. Developped circumvention for mass pixmap generations of document pages. + +* **Fixed** `#1433 `_. Solves a bbox error for some Type 3 font in PyMuPDF text processing. + +* **Added** :meth:`Pixmap.color_topusage` to determine the share of the most frequently used color. Solves `#1397 `_. + +* **Added** :meth:`Pixmap.warp` which makes a new pixmap from a given arbitrary convex quad inside the pixmap. + +* **Added** :attr:`Annot.irt_xref` and :meth:`Annot.set_irt_xref` to inquire or set the `/IRT` ("In Responde To") property of an annotation. Implements `#1450 `_. + +* **Added** :meth:`Rect.torect` and :meth:`IRect.torect` which compute a matrix that transforms to a given other rectangle. + +* **Changed** :meth:`Pixmap.color_count` to also return the count of each color. +* **Changed** :meth:`Page.get_texttrace` to also return correct span and character bboxes if ``span["dir"] != (1, 0)``. + +------ + +**Changes in Version 1.19.2** + +This patch version implements minor improvements for :meth:`Page.get_drawings` and also some important fixes. + +* **Fixed** `#1388 `_. Fixed intermittent memory corruption when insert or updating annotations. + +* **Fixed** `#1375 `_. Inconsistencies between line numbers as returned by the "words" and the "dict" options of :meth:`Page.get_text` have been corrected. + +* **Fixed** `#1364 `_. The check for being a ``"rawdict"`` span in :meth:`recover_span_quad` now works correctly. + +* **Fixed** `#1342 `_. Corrected the check for rectangle infiniteness in :meth:`Page.show_pdf_page`. + +* **Changed** :meth:`Page.get_drawings`, :meth:`Page.get_cdrawings` to return an indicator on the area orientation covered by a rectangle. This implements `#1355 `_. Also, the recognition rate for rectangles and quads has been significantly improved. + +* **Changed** all text search and extraction methods to set the new ``flags`` option ``TEXT_MEDIABOX_CLIP`` to ON by default. That bit causes the automatic suppression of all characters that are completely outside a page's mediabox (in as far as that notion is supported for a document type). This eliminates the need for using ``clip=page.rect`` or similar for omitting text outside the visible area. + +* **Added** parameter ``"dpi"`` to :meth:`Page.get_pixmap` and :meth:`Annot.get_pixmap`. When given, parameter ``"matrix"`` is ignored, and a :ref:`Pixmap` with the desired dots per inch is created. + +* **Added** attributes :attr:`Pixmap.is_monochrome` and :attr:`Pixmap.is_unicolor` allowing fast checks of pixmap properties. Addresses `#1397 `_. + +* **Added** method :meth:`Pixmap.color_count` to determine the unique colors in the pixmap. + +* **Added** boolean parameter ``"compress"`` to PDF document method :meth:`Document.update_stream`. Addresses / enables solution for `#1408 `_. + +------ + +**Changes in Version 1.19.1** + +This is the first patch version to support MuPDF v1.19.0. Apart from one bug fix, it includes important improvements for OCR support and the option to **sort extracted text** to the standard reading order "from top-left to bottom-right". + +* **Fixed** `#1328 `_. "words" text extraction again returns correct ``(x0, y0)`` coordinates. + +* **Changed** :meth:`Page.get_textpage_ocr`: it now supports parameter ``dpi`` to control OCR quality. It is also possible to choose whether the **full page** should be OCRed or **only the images displayed** by the page. + +* **Changed** :meth:`Page.get_drawings` and :meth:`Page.get_cdrawings` to automatically convert colors to RGB color tuples. Implements `#1332 `_. Similar change was applied to :meth:`Page.get_texttrace`. + +* **Changed** :meth:`Page.get_text` to support a parameter ``sort``. If set to ``True`` the output is conveniently sorted. + + +------ + +**Changes in Version 1.19.0** + +This is the first version supporting MuPDF 1.19.*, published 2021-10-05. It introduces many new features compared to the previous version 1.18.*. + +PyMuPDF has now picked up integrated Tesseract OCR support, which was already present in MuPDF v1.18.0. + +* Supported images can be OCRed via their :ref:`Pixmap` which results in a 1-page PDF with a text layer. +* All supported document pages (i.e. not only PDFs), can be OCRed using specialized text extraction methods. The result is a mixture of standard and OCR text (depending on which part of the page was deemed to require OCRing) that can be searched and extracted without restrictions. +* All this requires an independent installation of Tesseract. MuPDF actually (only) needs the location of Tesseract's ``"tessdata"`` folder, where its language support data are stored. This location must be available as environment variable ``TESSDATA_PREFIX``. + +A new MuPDF feature is **journalling PDF updates**, which is also supported by this PyMuPDF version. Changes may be logged, rolled back or replayed, allowing to implement a whole new level of control over PDF document integrity -- similar to functions present in modern database systems. + +A third feature (unrelated to the new MuPDF version) includes the ability to detect when page **objects cover or hide each other**. It is now e.g. possible to see that text is covered by a drawing or an image. + +* **Changed** terminology and meaning of important geometry concepts: Rectangles are now characterized as *finite*, *valid* or *empty*, while the definitions of these terms have also changed. Rectangles specifically are now thought of being "open": not all corners and sides are considered part of the retangle. Please do read the :ref:`Rect` section for details. + +* **Added** new parameter `"no_new_id"` to :meth:`Document.save` / :meth:`Document.tobytes` methods. Use it to suppress updating the second item of the document ``/ID`` which in PDF indicates that the original file has been updated. If the PDF has no ``/ID`` at all yet, then no new one will be created either. + +* **Added** a **journalling facility** for PDF updates. This allows logging changes, undoing or redoing them, or saving the journal for later use. Refer to :meth:`Document.journal_enable` and friends. + +* **Added** new :ref:`Pixmap` methods :meth:`Pixmap.pdfocr_save` and :meth:`Pixmap.pdfocr_tobytes`, which generate a 1-page PDF containing the pixmap as PNG image with OCR text layer. + +* **Added** :meth:`Page.get_textpage_ocr` which executes optical character recognition for the page, then extracts the results and stores them together with "normal" page content in a :ref:`TextPage`. Use or reuse this object in subsequent text extractions and text searches to avoid multiple efforts. The existing text search and text extraction methods have been extended to support a separately created textpage -- see next item. + +* **Added** a new parameter ``textpage`` to text extraction and text search methods. This allows reuse of a previously created :ref:`TextPage` and thus achieves significant runtime benefits -- which is especially important for the new OCR features. But "normal" text extractions can definitely also benefit. + +* **Added** :meth:`Page.get_texttrace`, a technical method delivering low-level text character properties. It was present before as a private method, but the author felt it now is mature enough to be officially available. It specifically includes a "sequence number" which indicates the page appearance build operation that painted the text. + +* **Added** :meth:`Page.get_bboxlog` which delivers the list of rectangles of page objects like text, images or drawings. Its significance lies in its sequence: rectangles intersecting areas with a lower index are covering or hiding them. + +* **Changed** methods :meth:`Page.get_drawings` and :meth:`Page.get_cdrawings` to include a "sequence number" indicating the page appearance build operation that created the drawing. + +* **Fixed** `#1311 `_. Field values in comboboxes should now be handled correctly. +* **Fixed** `#1290 `_. Error was caused by incorrect rectangle emptiness check, which is fixed due to new geometry logic of this version. +* **Fixed** `#1286 `_. Text alignment for redact annotations is working again. +* **Fixed** `#1287 `_. Infinite loop issue for non-Windows systems when applying some redactions has been resolved. +* **Fixed** `#1284 `_. Text layout destruction after applying redactions in some cases has been resolved. + +------ + +**Changes in Version 1.18.18 / 1.18.19** + +* **Fixed** issue `#1266 `_. Failure to set :attr:`Pixmap.samples` in important cases, was hotfixed in a new version 1.18.19. + +* **Fixed** issue `#1257 `_. Removing the read-only flag from PDF fields is now possible. + +* **Fixed** issue `#1252 `_. Now correctly specifying the ``zoom`` value for PDF link annotations. + +* **Fixed** issue `#1244 `_. Now correctly computing the transform matrix in :meth:`Page.get_image__bbox`. + +* **Fixed** issue `#1241 `_. Prevent returning artifact characters in :meth:`Page.get_textbox`, which happened in certain constellations. + +* **Fixed** issue `#1234 `_. Avoid creating infinite rectangles in corner cases -- :meth:`Page.get_drawings`, :meth:`Page.get_cdrawings`. + +* **Added** test data and test scripts to the source PyPI source distribution. + +------ + +**Changes in Version 1.18.17** + +Focus of this version are major performance improvements of selected functions. + +* **Fixed** issue `#1199 `_. Using a non-existing page number in :meth:`Document.get_page_images` and friends will no longer lead to segfaults. + +* **Changed** :meth:`Page.get_drawings` to now differentiate between "stroke", "fill" and combined paths. Paths containing more than one rectangle (i.e. "re" items) are now supported. Extracting "clipped" paths is now available as an option. + +* **Added** :meth:`Page.get_cdrawings`, performance-optimized version of :meth:`Page.get_drawings`. + +* **Added** :attr:`Pixmap.samples_mv`, *memoryview* of a pixmap's pixel area. Does not copy and thus always accesses the current state of that area. + +* **Added** :attr:`Pixmap.samples_ptr`, Python "pointer" to a pixmap's pixel area. Allows much faster creation (factor 800+) of Qt images. + + + +------ + +**Changes in Version 1.18.16** + +* **Fixed** issue `#1184 `_. Existing PDF widget fonts in a PDF are now accepted (i.e. not forcedly changed to a Base-14 font). + +* **Fixed** issue `#1154 `_. Text search hits should now be correct when ``clip`` is specified. + +* **Fixed** issue `#1152 `_. + +* **Fixed** issue `#1146 `_. + +* **Added** :attr:`Link.flags` and :meth:`Link.set_flags` to the :ref:`Link` class. Implements enhancement requests `#1187 `_. + +* **Added** option to *simulate* :meth:`TextWriter.fill_textbox` output for predicting the number of lines, that a given text would occupy in the textbox. + +* **Added** text output support as subcommand `gettext` to the ``fitz`` CLI module. Most importantly, original **physical text layout** reproduction is now supported. + + +------ + +**Changes in Version 1.18.15** + +* **Fixed** issue `#1088 `_. Removing an annotation's fill color should now work again both ways, using the ``fill_color=[]`` argument in :meth:`Annot.update` as well as ``fill=[]`` in :meth:`Annot.set_colors`. + +* **Fixed** issue `#1081 `_. :meth:`Document.subset_fonts`: fixed an error which created wrong character widths for some fonts. + +* **Fixed** issue `#1078 `_. :meth:`Page.get_text` and other methods related to text extraction: changed the default value of the :ref:`TextPage` ``flags`` parameter. All whitespace and :data:`ligatures` are now preserved. + +* **Fixed** issue `#1085 `_. The old *snake_cased* alias of ``fitz.detTextlength`` is now defined correctly. + +* **Changed** :meth:`Document.subset_fonts` will now correctly prefix font subsets with an appropriate six letter uppercase tag, complying with the PDF specification. + +* **Added** new method :meth:`Widget.button_states` which returns the possible values that a button-type field can have when being set to "on" or "off". + +* **Added** support of text with **Small Capital** letters to the :ref:`Font` and :ref:`TextWriter` classes. This is reflected by an additional bool parameter ``small_caps`` in various of their methods. + + +------ + +**Changes in Version 1.18.14** + +* **Finished** implementing new, "snake_cased" names for methods and properties, that were "camelCased" and awkward in many aspects. At the end of this documentation, there is section :ref:`Deprecated` with more background and a mapping of old to new names. + +* **Fixed** issue `#1053 `_. :meth:`Page.insert_image`: when given, include image mask in the hash computation. + +* **Fixed** issue `#1043 `_. Added ``Pixmap.getPNGdata`` to the aliases of :meth:`Pixmap.tobytes`. + +* **Fixed** an internal error when computing the enveloping rectangle of drawn paths as returned by :meth:`Page.get_drawings`. + +* **Fixed** an internal error occasionally causing loops when outputting text via :meth:`TextWriter.fill_textbox`. + +* **Added** :meth:`Font.char_lengths`, which returns a tuple of character widths of a string. + +* **Added** more ways to specify pages in :meth:`Document.delete_pages`. Now a sequence (list, tuple or range) can be specified, and the Python ``del`` statement can be used. In the latter case, Python ``slices`` are also accepted. + +* **Changed** :meth:`Document.del_toc_item`, which disables a single item of the TOC: previously, the title text was removed. Instead, now the complete item will be shown grayed-out by supporting viewers. + + +------ + +**Changes in Version 1.18.13** + +* **Fixed** issue `#1014 `_. +* **Fixed** an internal memory leak when computing image bboxes -- :meth:`Page.get_image_bbox`. +* **Added** support for low-level access and modification of the PDF trailer. Applies to :meth:`Document.xref_get_keys`, :meth:`Document.xref_get_key`, and :meth:`Document.xref_set_key`. +* **Added** documentation for maintaining private entries in PDF metadata. +* **Added** documentation for handling transparent image insertions, :meth:`Page.insert_image`. +* **Added** :meth:`Page.get_image_rects`, an improved version of :meth:`Page.get_image_bbox`. +* **Changed** :meth:`Document.delete_pages` to support various ways of specifying pages to delete. Implements `#1042 `_. +* **Changed** :meth:`Page.insert_image` to also accept the xref of an existing image in the file. This allows "copying" images between pages, and extremely fast mutiple insertions. +* **Changed** :meth:`Page.insert_image` to also accept the integer parameter ``alpha``. To be used for performance improvements. +* **Changed** :meth:`Pixmap.set_alpha` to support new parameters for pre-multiplying colors with their alpha values and setting a specific color to fully transparent (e.g. white). +* **Changed** :meth:`Document.embfile_add` to automatically set creation and modification date-time. Correspondingly, :meth:`Document.embfile_upd` automatically maintains modification date-time (``/ModDate`` PDF key), and :meth:`Document.embfile_info` correspondingly reports these data. In addition, the embedded file's associated "collection item" is included via its :data:`xref`. This supports the development of PDF portfolio applications. + +------ + +**Changes in Version 1.18.11 / 1.18.12** + +* **Fixed** issue `#972 `_. Improved layout of source distribution material. +* **Fixed** issue `#962 `_. Stabilized Linux distribution detection for generating PyMuPDF from sources. +* **Added:** :meth:`Page.get_xobjects` delivers the result of :meth:`Document.get_page_xobjects`. +* **Added:** :meth:`Page.get_image_info` delivers meta information for all images shown on the page. +* **Added:** :meth:`Tools.mupdf_display_warnings` allows setting on / off the display of MuPDF-generated warnings. The default is off. +* **Added:** :meth:`Document.ez_save` convenience alias of :meth:`Document.save` with some different defaults. +* **Changed:** Image extractions of document pages now also contain the image's **transformation matrix**. This concerns :meth:`Page.get_image_bbox` and the DICT, JSON, RAWDICT, and RAWJSON variants of :meth:`Page.get_text`. + + +------ + +**Changes in Version 1.18.10** + +* **Fixed** issue `#941 `_. Added old aliases for :meth:`DisplayList.get_pixmap` and :meth:`DisplayList.get_textpage`. +* **Fixed** issue `#929 `_. Stabilized removal of JavaScript objects with :meth:`Document.scrub`. +* **Fixed** issue `#927 `_. Removed a loop in the reworked :meth:`TextWriter.fill_textbox`. +* **Changed** :meth:`Document.xref_get_keys` and :meth:`Document.xref_get_key` to also allow accessing the PDF trailer dictionary. This can be done by using `-1` as the xref number argument. +* **Added** a number of functions for reconstructing the quads for text lines, spans and characters extracted by :meth:`Page.get_text` options "dict" and "rawdict". See :meth:`recover_quad` and friends. +* **Added** :meth:`Tools.unset_quad_corrections` to suppress character quad corrections (occasionally required for erroneous fonts). + +------ + +**Changes in Version 1.18.9** + + +* **Fixed** issue `#888 `_. Removed ambiguous statements concerning PyMuPDF's license, which is now clearly stated to be GNU AGPL V3. +* **Fixed** issue `#895 `_. +* **Fixed** issue `#896 `_. Since v1.17.6 PyMuPDF suppresses the font subset tags and only reports the base fontname in text extraction outputs "dict" / "json" / "rawdict" / "rawjson". Now a new global parameter can request the old behaviour, :meth:`Tools.set_subset_fontnames`. +* **Fixed** issue `#885 `_. Pixmap creation now also works with filenames given as ``pathlib.Paths``. +* **Changed** :meth:`Document.subset_fonts`: Text is **not rewritten** any more and should therefore **retain all its origial properties** -- like being hidden or being controlled by Optional Content mechanisms. +* **Changed** :ref:`TextWriter` output to also accept text in right to left mode (Arabian, Hebrew): :meth:`TextWriter.fill_textbox`, :meth:`TextWriter.append`. These methods now accept a new boolean parameter `right_to_left`, which is *False* by default. Implements `#897 `_. +* **Changed** :meth:`TextWriter.fill_textbox` to return all lines of text, that did not fit in the given rectangle. Also changed the default of the ``warn`` parameter to no longer print a warning message in overflow situations. +* **Added** a utility function :meth:`recover_quad`, which computes the quadrilateral of a span. This function can be used for correctly marking text extracted with the "dict" or "rawdict" options of :meth:`Page.get_text`. + +------ + +**Changes in Version 1.18.8** + + +This is a bug fix version only. We are publishing early because of the potentially widely used functions. + +* **Fixed** issue `#881 `_. Fixed a memory leak in :meth:`Page.insert_image` when inserting images from files or memory. +* **Fixed** issue `#878 `_. ``pathlib.Path`` objects should now correctly handle file path hierarchies. + + +------ + +**Changes in Version 1.18.7** + + +* **Added** an experimental :meth:`Document.subset_fonts` which reduces the size of eligible fonts based on their use by text in the PDF. Implements `#855 `_. +* **Implemented** request `#870 `_: :meth:`Document.convert_to_pdf` now also supports PDF documents. +* **Renamed** ``Document.write`` to :meth:`Document.tobytes` for greater clarity. But the deprecated name remains available for some time. +* **Implemented** request `#843 `_: :meth:`Document.tobytes` now supports linearized PDF output. :meth:`Document.save` now also supports writing to Python **file objects**. In addition, the open function now also supports Python file objects. +* **Fixed** issue `#844 `_. +* **Fixed** issue `#838 `_. +* **Fixed** issue `#823 `_. More logic for better support of OCRed text output (Tesseract, ABBYY). +* **Fixed** issue `#818 `_. +* **Fixed** issue `#814 `_. +* **Added** :meth:`Document.get_page_labels` which returns a list of page label definitions of a PDF. +* **Added** :meth:`Document.has_annots` and :meth:`Document.has_links` to check whether these object types are present anywhere in a PDF. +* **Added** expert low-level functions to simplify inquiry and modification of PDF object sources: :meth:`Document.xref_get_keys` lists the keys of object :data:`xref`, :meth:`Document.xref_get_key` returns type and content of a key, and :meth:`Document.xref_set_key` modifies the key's value. +* **Added** parameter ``thumbnails`` to :meth:`Document.scrub` to also allow removing page thumbnail images. +* **Improved** documentation for how to add valid text marker annotations for non-horizontal text. + +We continued the process of renaming methods and properties from *"mixedCase"* to *"snake_case"*. Documentation usually mentions the new names only, but old, deprecated names remain available for some time. + + + +------ + +**Changes in Version 1.18.6** + +* **Fixed** issue `#812 `_. +* **Fixed** issue `#793 `_. Invalid document metadata previously prevented opening some documents at all. This error has been removed. +* **Fixed** issue `#792 `_. Text search and text extraction will make no rectangle containment checks at all if the default ``clip=None`` is used. +* **Fixed** issue `#785 `_. +* **Fixed** issue `#780 `_. Corrected a parameter check error. +* **Fixed** issue `#779 `_. Fixed typo +* **Added** an option to set the desired line height for text boxes. Implements `#804 `_. +* **Changed** text position retrieval to better cope with Tesseract's glyphless font. Implements `#803 `_. +* **Added** an option to choose the prefix of new annotations, fields and links for providing unique annotation ids. Implements request `#807 `_. +* **Added** getting and setting color and text properties for Table of Contents items for PDFs. Implements `#779 `_. +* **Added** PDF page label handling: :meth:`Page.get_label()` returns the page label, :meth:`Document.get_page_numbers` return all page numbers having a specified label, and :meth:`Document.set_page_labels` adds or updates a PDF's page label definition. + + + +.. note:: + This version introduces **Python type hinting**. The goal is to provide each parameter and the return value of all functions and methods with type information. This still is work in progress although the majority of functions has already been handled. + + +------ + +**Changes in Version 1.18.5** + +Apart from several fixes, this version also focusses on several minor, but important feature improvements. Among the latter is a more precise computation of proper line heights and insertion points for writing / inserting text. As opposed to using font-agnostic constants, these values are now taken from the font's properties. + +Also note that this is the first version which does no longer provide pregenerated wheels for Python versions older than 3.6. PIP also discontinues support for these by end of this year 2020. + +* **Fixed** issue `#771 `_. By using "small glyph heights" option, the full page text can be extracted. +* **Fixed** issue `#768 `_. +* **Fixed** issue `#750 `_. +* **Fixed** issue `#739 `_. The "dict", "rawdict" and corresponding JSON output variants now have two new *span* keys: ``"ascender"`` and ``"descender"``. These floats represent special font properties which can be used to compute bboxes of spans or characters of **exactly fontsize height** (as opposed to the default line height). An example algorithm is shown in section "Span Dictionary" `here `_. Also improved the detection and correction of ill-specified ascender / descender values encountered in some fonts. +* **Added** a new, experimental :meth:`Tools.set_small_glyph_heights` -- also in response to issue `#739 `_. This method sets or unsets a global parameter to **always compute bboxes with fontsize height**. If "on", text searching and all text extractions will returned rectangles, bboxes and quads with a smaller height. +* **Fixed** issue `#728 `_. +* **Changed** fill color logic of 'Polyline' annotations: this parameter now only pertains to line end symbols -- the annotation itself can no longer have a fill color. Also addresses issue `#727 `_. +* **Changed** :meth:`Page.getImageBbox` to also compute the bbox if the image is contained in an XObject. +* **Changed** :meth:`Shape.insertTextbox`, resp. :meth:`Page.insertTextbox`, resp. :meth:`TextWriter.fillTextbox` to respect font's properties "ascender" / "descender" when computing line height and insertion point. This should no longer lead to line overlaps for multi-line output. These methods used to ignore font specifics and used constant values instead. + + +------ + +**Changes in Version 1.18.4** + +This version adds several features to support PDF Optional Content. Among other things, this includes OCMDs (Optional Content Membership Dictionaries) with the full scope of *"visibility expressions"* (PDF key ``/VE``), text insertions (including the :ref:`TextWriter` class) and drawings. + +* **Fixed** issue `#727 `_. Freetext annotations now support an uncolored rectangle when ``fill_color=None``. +* **Fixed** issue `#726 `_. UTF-8 encoding errors are now handled for HTML / XML :meth:`Page.getText` output. +* **Fixed** issue `#724 `_. Empty values are no longer stored in the PDF /Info metadata dictionary. +* **Added** new methods :meth:`Document.set_oc` and :meth:`Document.get_oc` to set or get optional content references for **existing** image and form XObjects. These methods are similar to the same-named methods of :ref:`Annot`. +* **Added** :meth:`Document.set_ocmd`, :meth:`Document.get_ocmd` for handling OCMDs. +* **Added** **Optional Content** support for text insertion and drawing. +* **Added** new method :meth:`Page.deleteWidget`, which deletes a form field from a page. This is analogous to deleting annotations. +* **Added** support for Popup annotations. This includes defining the Popup rectangle and setting the Popup to open or closed. Methods / attributes :meth:`Annot.set_popup`, :meth:`Annot.set_open`, :attr:`Annot.has_popup`, :attr:`Annot.is_open`, :attr:`Annot.popup_rect`, :attr:`Annot.popup_xref`. + +Other changes: + +* The **naming of methods and attributes** in PyMuPDF is far from being satisfactory: we have *CamelCases*, *mixedCases* and *lower_case_with_underscores* all over the place. With the :ref:`Annot` as the first candidate, we have started an activity to clean this up step by step, converting to lower case with underscores for methods and attributes while keeping UPPERCASE for the constants. + + - Old names will remain available to prevent code breaks, but they will no longer be mentioned in the documentation. + - New methods and attributes of all classes will be named according to the new standard. + +------ + +**Changes in Version 1.18.3** + +As a major new feature, this version introduces support for PDF's **Optional Content** concept. + +* **Fixed** issue `#714 `_. +* **Fixed** issue `#711 `_. +* **Fixed** issue `#707 `_: if a PDF user password, but no owner password is supplied nor present, then the user password is also used as the owner password. +* **Fixed** ``expand`` and ``deflate`` parameters of methods :meth:`Document.save` and :meth:`Document.write`. Individual image and font compression should now finally work. Addresses issue `#713 `_. +* **Added** a support of PDF optional content. This includes several new :ref:`Document` methods for inquiring and setting optional content status and adding optional content configurations and groups. In addition, images, form XObjects and annotations now can be bound to optional content specifications. **Resolved** issue `#709 `_. + + + +------ + +**Changes in Version 1.18.2** + +This version contains some interesting improvements for text searching: any number of search hits is now returned and the **hit_max** parameter was removed. The new **clip** parameter in addition allows to restrict the search area. Searching now detects hyphenations at line breaks and accordingly finds hyphenated words. + +* **Fixed** issue `#575 `_: if using ``quads=False`` in text searching, then overlapping rectangles on the same line are joined. Previously, parts of the search string, which belonged to different "marked content" items, each generated their own rectangle -- just as if occurring on separate lines. +* **Added** :attr:`Document.isRepaired`, which is true if the PDF was repaired on open. +* **Added** :meth:`Document.setXmlMetadata` which either updates or creates PDF XML metadata. Implements issue `#691 `_. +* **Added** :meth:`Document.getXmlMetadata` returns PDF XML metadata. +* **Changed** creation of PDF documents: they will now always carry a PDF identification (``/ID`` field) in the document trailer. Implements issue `#691 `_. +* **Changed** :meth:`Page.searchFor`: a new parameter ``clip`` is accepted to restrict the search to this rectangle. Correspondingly, the attribute :attr:`TextPage.rect` is now respected by :meth:`TextPage.search`. +* **Changed** parameter ``hit_max`` in :meth:`Page.searchFor` and :meth:`TextPage.search` is now obsolete: methods will return all hits. +* **Changed** character **selection criteria** in :meth:`Page.getText`: a character is now considered to be part of a ``clip`` if its bbox is fully contained. Before this, a non-empty intersection was sufficient. +* **Changed** :meth:`Document.scrub` to support a new option `redact_images`. This addresses issue `#697 `_. + + +------ + +**Changes in Version 1.18.1** + +* **Fixed** issue `#692 `_. PyMuPDF now detects and recovers from more cyclic resource dependencies in PDF pages and for the first time reports them in the MuPDF warnings store. +* **Fixed** issue `#686 `_. +* **Added** opacity options for the :ref:`Shape` class: Stroke and fill colors can now be set to some transparency value. This means that all :ref:`Page` draw methods, methods :meth:`Page.insertText`, :meth:`Page.insertTextbox`, :meth:`Shape.finish`, :meth:`Shape.insertText`, and :meth:`Shape.insertTextbox` support two new parameters: *stroke_opacity* and *fill_opacity*. +* **Added** new parameter ``mask`` to :meth:`Page.insertImage` for optionally providing an external image mask. Resolves issue `#685 `_. +* **Added** :meth:`Annot.soundGet` for extracting the sound of an audio annotation. + +------ + +**Changes in Version 1.18.0** + +This is the first PyMuPDF version supporting MuPDF v1.18. The focus here is on extending PyMuPDF's own functionality -- apart from bug fixing. Subsequent PyMuPDF patches may address features new in MuPDF. + +* **Fixed** issue `#519 `_. This upstream bug occurred occasionally for some pages only and seems to be fixed now: page layout should no longer be ruined in these cases. + +* **Fixed** issue `#675 `_. + + - Unsuccessful storage allocations should now always lead to exceptions (circumvention of an upstream bug intermittently crashing the interpreter). + - :ref:`Pixmap` size is now based on ``size_t`` instead of ``int`` in C and should be correct even for extremely large pixmaps. + +* **Fixed** issue `#668 `_. Specification of dashes for PDF drawing insertion should now correctly reflect the PDF spec. +* **Fixed** issue `#669 `_. A major source of memory leakage in :meth:`Page.insert_pdf` has been removed. +* **Added** keyword *"images"* to :meth:`Page.apply_redactions` for fine-controlling the handling of images. +* **Added** :meth:`Annot.getText` and :meth:`Annot.getTextbox`, which offer the same functionality as the :ref:`Page` versions. +* **Added** key *"number"* to the block dictionaries of :meth:`Page.getText` / :meth:`Annot.getText` for options "dict" and "rawdict". +* **Added** :meth:`glyph_name_to_unicode` and :meth:`unicode_to_glyph_name`. Both functions do not really connect to a specific font and are now independently available, too. The data are now based on the `Adobe Glyph List `_. +* **Added** convenience functions :meth:`adobe_glyph_names` and :meth:`adobe_glyph_unicodes` which return the respective available data. +* **Added** :meth:`Page.getDrawings` which returns details of drawing operations on a document page. Works for all document types. +* Improved performance of :meth:`Document.insert_pdf`. Multiple object copies are now also suppressed across multiple separate insertions from the same source. This saves time, memory and target file size. Previously this mechanism was only active within each single method execution. The feature can also be suppressed with the new method bool parameter *final=1*, which is the default. +* For PNG images created from pixmaps, the resolution (dpi) is now automatically set from the respective :attr:`Pixmap.xres` and :attr:`Pixmap.yres` values. + + +------ + +**Changes in Version 1.17.7** + +* **Fixed** issue `#651 `_. An upstream bug causing interpreter crashes in corner case redaction processings was fixed by backporting MuPDF changes from their development repo. +* **Fixed** issue `#645 `_. Pixmap top-left coordinates can be set (again) by their own method, :meth:`Pixmap.set_origin`. +* **Fixed** issue `#622 `_. :meth:`Page.insertImage` again accepts a :data:`rect_like` parameter. +* **Added** severeal new methods to improve and speed-up table of contents (TOC) handling. Among other things, TOC items can now changed or deleted individually -- without always replacing the complete TOC. Furthermore, access to some PDF page attributes is now possible without first **loading** the page. This has a very significant impact on the performance of TOC manipulation. +* **Added** an option to :meth:`Document.insert_pdf` which allows displaying progress messages. Adresses `#640 `_. +* **Added** :meth:`Page.getTextbox` which extracts text contained in a rectangle. In many cases, this should obsolete writing your own script for this type of thing. +* **Added** new ``clip`` parameter to :meth:`Page.getText` to simplify and speed up text extraction of page sub areas. +* **Added** :meth:`TextWriter.appendv` to add text in **vertical write mode**. Addresses issue `#653 `_ + + +------ + +**Changes in Version 1.17.6** + +* **Fixed** issue `#605 `_ +* **Fixed** issue `#600 `_ -- text should now be correctly positioned also for pages with a CropBox smaller than MediaBox. +* **Added** text span dictionary key ``origin`` which contains the lower left coordinate of the first character in that span. +* **Added** attribute :attr:`Font.buffer`, a *bytes* copy of the font file. +* **Added** parameter *sanitize* to :meth:`Page.cleanContents`. Allows switching of sanitization, so only syntax cleaning will be done. + +------ + +**Changes in Version 1.17.5** + +* **Fixed** issue `#561 `_ -- second go: certain :ref:`TextWriter` usages with many alternating fonts did not work correctly. +* **Fixed** issue `#566 `_. +* **Fixed** issue `#568 `_. +* **Fixed** -- opacity is now correctly taken from the :ref:`TextWriter` object, if not given in :meth:`TextWriter.writeText`. +* **Added** a new global attribute :attr:`fitz_fontdescriptors`. Contains information about usable fonts from repository `pymupdf-fonts `_. +* **Added** :meth:`Font.valid_codepoints` which returns an array of unicode codepoints for which the font has a glyph. +* **Added** option ``text_as_path`` to :meth:`Page.getSVGimage`. this implements `#580 `_. Generates much smaller SVG files with parseable text if set to *False*. + + +------ + +**Changes in Version 1.17.4** + +* **Fixed** issue `#561 `_. Handling of more than 10 :ref:`Font` objects on one page should now work correctly. +* **Fixed** issue `#562 `_. Annotation pixmaps are no longer derived from the page pixmap, thus avoiding unintended inclusion of page content. +* **Fixed** issue `#559 `_. This **MuPDF** bug is being temporarily fixed with a pre-version of MuPDF's next release. +* **Added** utility function :meth:`repair_mono_font` for correcting displayed character spacing for some mono-spaced fonts. +* **Added** utility method :meth:`Document.need_appearances` for fine-controlling Form PDF behavior. Addresses issue `#563 `_. +* **Added** utility function :meth:`sRGB_to_pdf` to recover the PDF color triple for a given color integer in sRGB format. +* **Added** utility function :meth:`sRGB_to_rgb` to recover the (R, G, B) color triple for a given color integer in sRGB format. +* **Added** utility function :meth:`make_table` which delivers table cells for a given rectangle and desired numbers of columns and rows. +* **Added** support for optional fonts in repository `pymupdf-fonts `_. + +------ + +**Changes in Version 1.17.3** + +* **Fixed** an undocumented issue, which prevented fully cleaning a PDF page when using :meth:`Page.cleanContents`. +* **Fixed** issue `#540 `_. Text extraction for EPUB should again work correctly. +* **Fixed** issue `#548 `_. Documentation now includes ``LINK_NAMED``. +* **Added** new parameter to control start of text in :meth:`TextWriter.fillTextbox`. Implements `#549 `_. +* **Changed** documentation of :meth:`Page.add_redact_annot` to explain the usage of non-builtin fonts. + +------ + +**Changes in Version 1.17.2** + +* **Fixed** issue `#533 `_. +* **Added** options to modify 'Redact' annotation appearance. Implements `#535 `_. + + +------ + +**Changes in Version 1.17.1** + +* **Fixed** issue `#520 `_. +* **Fixed** issue `#525 `_. Vertices for 'Ink' annots should now be correct. +* **Fixed** issue `#524 `_. It is now possible to query and set rotation for applicable annotation types. + +Also significantly improved inline documentation for better support of interactive help. + +------ + +**Changes in Version 1.17.0** + +This version is based on MuPDF v1.17. Following are highlights of new and changed features: + +* **Added** extended language support for annotations and widgets: a mixture of Latin, Greece, Russian, Chinese, Japanese and Korean characters can now be used in 'FreeText' annotations and text widgets. No special arrangement is required to use it. + +* Faster page access is implemented for documents supporting a "chapter" structure. This applies to EPUB documents currently. This comes with several new :ref:`Document` methods and changes for :meth:`Document.loadPage` and the "indexed" page access *doc[n]*: In addition to specifying a page number as before, a tuple *(chaper, pno)* can be specified to identify the desired page. + +* **Changed:** Improved support of redaction annotations: images overlapped by redactions are **permanantly modified** by erasing the overlap areas. Also links are removed if overlapped by redactions. This is now fully in sync with PDF specifications. + +Other changes: + +* **Changed** :meth:`TextWriter.writeText` to support the *"morph"* parameter. +* **Added** methods :meth:`Rect.morph`, :meth:`IRect.morph`, and :meth:`Quad.morph`, which return a new :ref:`Quad`. +* **Changed** :meth:`Page.add_freetext_annot` to support text alignment via a new *"align"* parameter. +* **Fixed** issue `#508 `_. Improved image rectangle calculation to hopefully deliver correct values in most if not all cases. +* **Fixed** issue `#502 `_. +* **Fixed** issue `#500 `_. :meth:`Document.convertToPDF` should no longer cause memory leaks. +* **Fixed** issue `#496 `_. Annotations and widgets / fields are now added or modified using the coordinates of the **unrotated page**. This behavior is now in sync with other methods modifying PDF pages. +* **Added** :attr:`Page.rotationMatrix` and :attr:`Page.derotationMatrix` to support coordinate transformations between the rotated and the original versions of a PDF page. + +Potential code breaking changes: + +* The private method ``Page._getTransformation()`` has been removed. Use the public :attr:`Page.transformationMattrix` instead. + + +------ + +**Changes in Version 1.16.18** + +This version introduces several new features around PDF text output. The motivation is to simplify this task, while at the same time offering extending features. + +One major achievement is using MuPDF's capabilities to dynamically choosing fallback fonts whenever a character cannot be found in the current one. This seemlessly works for Base-14 fonts in combination with CJK fonts (China, Japan, Korea). So a text may contain **any combination of characters** from the Latin, Greek, Russian, Chinese, Japanese and Korean languages. + +* **Fixed** issue `#493 `_. ``Pixmap(doc, xref)`` should now again correctly resemble the loaded image object. +* **Fixed** issue `#488 `_. Widget names are now modifiable. +* **Added** new class :ref:`Font` which represents a font. +* **Added** new class :ref:`TextWriter` which serves as a container for text to be written on a page. +* **Added** :meth:`Page.writeText` to write one or more :ref:`TextWriter` objects to the page. + + +------ + +**Changes in Version 1.16.17** + + +* **Fixed** issue `#479 `_. PyMuPDF should now more correctly report image resolutions. This applies to both, images (either from images files or extracted from PDF documents) and pixmaps created from images. +* **Added** :meth:`Pixmap.set_dpi` which sets the image resolution in x and y directions. + +------ + +**Changes in Version 1.16.16** + + +* **Fixed** issue `#477 `_. +* **Fixed** issue `#476 `_. +* **Changed** annotation line end symbol coloring and fixed an error coloring the interior of 'Polyline' /'Polygon' annotations. + +------ + +**Changes in Version 1.16.14** + + +* **Changed** text marker annotations to accept parameters beyond just quadrilaterals such that now **text lines between two given points can be marked**. + +* **Added** :meth:`Document.scrub` which **removes potentially sensitive data** from a PDF. Implements `#453 `_. + +* **Added** :meth:`Annot.blendMode` which returns the **blend mode** of annotations. + +* **Added** :meth:`Annot.setBlendMode` to set the annotation's blend mode. This resolves issue `#416 `_. +* **Changed** :meth:`Annot.update` to accept additional parameters for setting blend mode and opacity. +* **Added** advanced graphics features to **control the anti-aliasing values**, :meth:`Tools.set_aa_level`. Resolves `#467 `_ + +* **Fixed** issue `#474 `_. +* **Fixed** issue `#466 `_. + + + +------ + +**Changes in Version 1.16.13** + + +* **Added** :meth:`Document.getPageXObjectList` which returns a list of **Form XObjects** of the page. +* **Added** :meth:`Page.setMediaBox` for changing the physical PDF page size. +* **Added** :ref:`Page` methods which have been internal before: :meth:`Page.cleanContents` (= :meth:`Page._cleanContents`), :meth:`Page.getContents` (= :meth:`Page._getContents`), :meth:`Page.getTransformation` (= :meth:`Page._getTransformation`). + + + +------ + +**Changes in Version 1.16.12** + +* **Fixed** issue `#447 `_ +* **Fixed** issue `#461 `_. +* **Fixed** issue `#397 `_. +* **Fixed** issue `#463 `_. +* **Added** JavaScript support to PDF form fields, thereby fixing `#454 `_. +* **Added** a new annotation method :meth:`Annot.delete_responses`, which removes 'Popup' and response annotations referring to the current one. Mainly serves data protection purposes. +* **Added** a new form field method :meth:`Widget.reset`, which resets the field value to its default. +* **Changed** and extended handling of redactions: images and XObjects are removed if *contained* in a redaction rectangle. Any partial only overlaps will just be covered by the redaction background color. Now an *overlay* text can be specified to be inserted in the rectangle area to **take the place the deleted original** text. This resolves `#434 `_. + +------ + +**Changes in Version 1.16.11** + +* **Added** Support for redaction annotations via method :meth:`Page.add_redact_annot` and :meth:`Page.apply_redactions`. +* **Fixed** issue #426 ("PolygonAnnotation in 1.16.10 version"). +* **Fixed** documentation only issues `#443 `_ and `#444 `_. + +------ + +**Changes in Version 1.16.10** + +* **Fixed** issue #421 ("annot.set_rect(rect) has no effect on text Annotation") +* **Fixed** issue #417 ("Strange behavior for page.deleteAnnot on 1.16.9 compare to 1.13.20") +* **Fixed** issue #415 ("Annot.setOpacity throws mupdf warnings") +* **Changed** all "add annotation / widget" methods to store a unique name in the */NM* PDF key. +* **Changed** :meth:`Annot.setInfo` to also accept direct parameters in addition to a dictionary. +* **Changed** :attr:`Annot.info` to now also show the annotation's unique id (*/NM* PDF key) if present. +* **Added** :meth:`Page.annot_names` which returns a list of all annotation names (*/NM* keys). +* **Added** :meth:`Page.load_annot` which loads an annotation given its unique id (*/NM* key). +* **Added** :meth:`Document.reload_page` which provides a new copy of a page after finishing any pending updates to it. + + +------ + +**Changes in Version 1.16.9** + +* **Fixed** #412 ("Feature Request: Allow controlling whether TOC entries should be collapsed") +* **Fixed** #411 ("Seg Fault with page.firstWidget") +* **Fixed** #407 ("Annot.setOpacity trouble") +* **Changed** methods :meth:`Annot.setBorder`, :meth:`Annot.setColors`, :meth:`Link.setBorder`, and :meth:`Link.setColors` to also accept direct parameters, and not just cumbersome dictionaries. + +------ + +**Changes in Version 1.16.8** + +* **Added** several new methods to the :ref:`Document` class, which make dealing with PDF low-level structures easier. I also decided to provide them as "normal" methods (as opposed to private ones starting with an underscore "_"). These are :meth:`Document.xrefObject`, :meth:`Document.xrefStream`, :meth:`Document.xrefStreamRaw`, :meth:`Document.PDFTrailer`, :meth:`Document.PDFCatalog`, :meth:`Document.metadataXML`, :meth:`Document.updateObject`, :meth:`Document.updateStream`. +* **Added** :meth:`Tools.mupdf_disply_errors` which sets the display of mupdf errors on *sys.stderr*. +* **Added** a commandline facility. This a major new feature: you can now invoke several utility functions via *"python -m fitz ..."*. It should obsolete the need for many of the most trivial scripts. Please refer to :ref:`Module`. + + +------ + +**Changes in Version 1.16.7** + +Minor changes to better synchronize the binary image streams of :ref:`TextPage` image blocks and :meth:`Document.extractImage` images. + +* **Fixed** issue #394 ("PyMuPDF Segfaults when using TOOLS.mupdf_warnings()"). +* **Changed** redirection of MuPDF error messages: apart from writing them to Python *sys.stderr*, they are now also stored with the MuPDF warnings. +* **Changed** :meth:`Tools.mupdf_warnings` to automatically empty the store (if not deactivated via a parameter). +* **Changed** :meth:`Page.getImageBbox` to return an **infinite rectangle** if the image could not be located on the page -- instead of raising an exception. + + +------ + +**Changes in Version 1.16.6** + +* **Fixed** issue #390 ("Incomplete deletion of annotations"). +* **Changed** :meth:`Page.searchFor` / :meth:`Document.searchPageFor` to also support the *flags* parameter, which controls the data included in a :ref:`TextPage`. +* **Changed** :meth:`Document.getPageImageList`, :meth:`Document.getPageFontList` and their :ref:`Page` counterparts to support a new parameter *full*. If true, the returned items will contain the :data:`xref` of the *Form XObject* where the font or image is referenced. + +------ + +**Changes in Version 1.16.5** + +More performance improvements for text extraction. + +* **Fixed** second part of issue #381 (see item in v1.16.4). +* **Added** :meth:`Page.getTextPage`, so it is no longer required to create an intermediate display list for text extractions. Page level wrappers for text extraction and text searching are now based on this, which should improve performance by ca. 5%. + +------ + +**Changes in Version 1.16.4** + + +* **Fixed** issue #381 ("TextPage.extractDICT ... failed ... after upgrading ... to 1.16.3") +* **Added** method :meth:`Document.pages` which delivers a generator iterator over a page range. +* **Added** method :meth:`Page.links` which delivers a generator iterator over the links of a page. +* **Added** method :meth:`Page.annots` which delivers a generator iterator over the annotations of a page. +* **Added** method :meth:`Page.widgets` which delivers a generator iterator over the form fields of a page. +* **Changed** :attr:`Document.is_form_pdf` to now contain the number of widgets, and *False* if not a PDF or this number is zero. + + +------ + +**Changes in Version 1.16.3** + +Minor changes compared to version 1.16.2. The code of the "dict" and "rawdict" variants of :meth:`Page.getText` has been ported to C which has greatly improved their performance. This improvement is mostly noticeable with text-oriented documents, where they now should execute almost two times faster. + +* **Fixed** issue #369 ("mupdf: cmsCreateTransform failed") by removing ICC colorspace support. +* **Changed** :meth:`Page.getText` to accept additional keywords "blocks" and "words". These will deliver the results of :meth:`Page.getTextBlocks` and :meth:`Page.getTextWords`, respectively. So all text extraction methods are now available via a uniform API. Correspondingly, there are now new methods :meth:`TextPage.extractBLOCKS` and :meth:`TextPage.extractWords`. +* **Changed** :meth:`Page.getText` to default bit indicator *TEXT_INHIBIT_SPACES* to **off**. Insertion of additional spaces is **not suppressed** by default. + +------ + +**Changes in Version 1.16.2** + +* **Changed** text extraction methods of :ref:`Page` to allow detail control of the amount of extracted data. +* **Added** :meth:`planish_line` which maps a given line (defined as a pair of points) to the x-axis. +* **Fixed** an issue (w/o Github number) which brought down the interpreter when encountering certain non-UTF-8 encodable characters while using :meth:`Page.getText` with te "dict" option. +* **Fixed** issue #362 ("Memory Leak with getText('rawDICT')"). + +------ + +**Changes in Version 1.16.1** + +* **Added** property :attr:`Quad.is_convex` which checks whether a line is contained in the quad if it connects two points of it. +* **Changed** :meth:`Document.insert_pdf` to now allow dropping or including links and annotations independently during the copy. Fixes issue #352 ("Corrupt PDF data and ..."), which seemed to intermittently occur when using the method for some problematic PDF files. +* **Fixed** a bug which, in matrix division using the syntax *"m1/m2"*, caused matrix *"m1"* to be **replaced** by the result instead of delivering a new matrix. +* **Fixed** issue #354 ("SyntaxWarning with Python 3.8"). We now always use *"=="* for literals (instead of the *"is"* Python keyword). +* **Fixed** issue #353 ("mupdf version check"), to no longer refuse the import when there are only patch level deviations from MuPDF. + + + +------ + +**Changes in Version 1.16.0** + +This major new version of MuPDF comes with several nice new or changed features. Some of them imply programming API changes, however. This is a synopsis of what has changed: + +* PDF document encryption and decryption is now **fully supported**. This includes setting **permissions**, **passwords** (user and owner passwords) and the desired encryption method. +* In response to the new encryption features, PyMuPDF returns an integer (ie. a combination of bits) for document permissions, and no longer a dictionary. +* Redirection of MuPDF errors and warnings is now natively supported. PyMuPDF redirects error messages from MuPDF to *sys.stderr* and no longer buffers them. Warnings continue to be buffered and will not be displayed. Functions exist to access and reset the warnings buffer. +* Annotations are now **only supported for PDF**. +* Annotations and widgets (form fields) are now **separate object chains** on a page (although widgets technically still **are** PDF annotations). This means, that you will **never encounter widgets** when using :attr:`Page.firstAnnot` or :meth:`Annot.next`. You must use :attr:`Page.firstWidget` and :meth:`Widget.next` to access form fields. +* As part of MuPDF's changes regarding widgets, only the following four fonts are supported, when **adding** or **changing** form fields: **Courier, Helvetica, Times-Roman** and **ZapfDingBats**. + +List of change details: + +* **Added** :meth:`Document.can_save_incrementally` which checks conditions that are preventing use of option *incremental=True* of :meth:`Document.save`. +* **Added** :attr:`Page.firstWidget` which points to the first field on a page. +* **Added** :meth:`Page.getImageBbox` which returns the rectangle occupied by an image shown on the page. +* **Added** :meth:`Annot.setName` which lets you change the (icon) name field. +* **Added** outputting the text color in :meth:`Page.getText`: the *"dict"*, *"rawdict"* and *"xml"* options now also show the color in sRGB format. +* **Changed** :attr:`Document.permissions` to now contain an integer of bool indicators -- was a dictionary before. +* **Changed** :meth:`Document.save`, :meth:`Document.write`, which now fully support password-based decryption and encryption of PDF files. +* **Changed the names of all Python constants** related to annotations and widgets. Please make sure to consult the **Constants and Enumerations** chapter if your script is dealing with these two classes. This decision goes back to the dropped support for non-PDF annotations. The **old names** (starting with "ANNOT_*" or "WIDGET_*") will be available as deprecated synonyms. +* **Changed** font support for widgets: only *Cour* (Courier), *Helv* (Helvetica, default), *TiRo* (Times-Roman) and *ZaDb* (ZapfDingBats) are accepted when **adding or changing** form fields. Only the plain versions are possible -- not their italic or bold variations. **Reading** widgets, however will show its original font. +* **Changed** the name of the warnings buffer to :meth:`Tools.mupdf_warnings` and the function to empty this buffer is now called :meth:`Tools.reset_mupdf_warnings`. +* **Changed** :meth:`Page.getPixmap`, :meth:`Document.get_page_pixmap`: a new bool argument *annots* can now be used to **suppress the rendering of annotations** on the page. +* **Changed** :meth:`Page.add_file_annot` and :meth:`Page.add_text_annot` to enable setting an icon. +* **Removed** widget-related methods and attributes from the :ref:`Annot` object. +* **Removed** :ref:`Document` attributes *openErrCode*, *openErrMsg*, and :ref:`Tools` attributes / methods *stderr*, *reset_stderr*, *stdout*, and *reset_stdout*. +* **Removed** **thirdparty zlib** dependency in PyMuPDF: there are now compression functions available in MuPDF. Source installers of PyMuPDF may now omit this extra installation step. + +**No version published for MuPDF v1.15.0** + + +------ + +**Changes in Version 1.14.20 / 1.14.21** + +* **Changed** text marker annotations to support multiple rectangles / quadrilaterals. This fixes issue #341 ("Question : How to addhighlight so that a string spread across more than a line is covered by one highlight?") and similar (#285). +* **Fixed** issue #331 ("Importing PyMuPDF changes warning filtering behaviour globally"). + + +------ + +**Changes in Version 1.14.19** + +* **Fixed** issue #319 ("InsertText function error when use custom font"). +* **Added** new method :meth:`Document.get_sigflags` which returns information on whether a PDF is signed. Resolves issue #326 ("How to detect signature in a form pdf?"). + + +------ + +**Changes in Version 1.14.17** + +* **Added** :meth:`Document.fullcopyPage` to make full page copies within a PDF (not just copied references as :meth:`Document.copyPage` does). +* **Changed** :meth:`Page.getPixmap`, :meth:`Document.get_page_pixmap` now use *alpha=False* as default. +* **Changed** text extraction: the span dictionary now (again) contains its rectangle under the *bbox* key. +* **Changed** :meth:`Document.movePage` and :meth:`Document.copyPage` to use direct functions instead of wrapping :meth:`Document.select` -- similar to :meth:`Document.delete_page` in v1.14.16. + +------ + +**Changes in Version 1.14.16** + +* **Changed** :ref:`Document` methods around PDF */EmbeddedFiles* to no longer use MuPDF's "portfolio" functions. That support will be dropped in MuPDF v1.15 -- therefore another solution was required. +* **Changed** :meth:`Document.embfile_Count` to be a function (was an attribute). +* **Added** new method :meth:`Document.embfile_Names` which returns a list of names of embedded files. +* **Changed** :meth:`Document.delete_page` and :meth:`Document.delete_pages` to internally no longer use :meth:`Document.select`, but instead use functions to perform the deletion directly. As it has turned out, the :meth:`Document.select` method yields invalid outline trees (tables of content) for very complex PDFs and sophisticated use of annotations. + + +------ + +**Changes in Version 1.14.15** + +* **Fixed** issues #301 ("Line cap and Line join"), #300 ("How to draw a shape without outlines") and #298 ("utils.updateRect exception"). These bugs pertain to drawing shapes with PyMuPDF. Drawing shapes without any border is fully supported. Line cap styles and line line join style are now differentiated and support all possible PDF values (0, 1, 2) instead of just being a bool. The previous parameter *roundCap* is deprecated in favor of *lineCap* and *lineJoin* and will be deleted in the next release. +* **Fixed** issue #290 ("Memory Leak with getText('rawDICT')"). This bug caused memory not being (completely) freed after invoking the "dict", "rawdict" and "json" versions of :meth:`Page.getText`. + + +------ + +**Changes in Version 1.14.14** + +* **Added** new low-level function :meth:`ImageProperties` to determine a number of characteristics for an image. +* **Added** new low-level function :meth:`Document.is_stream`, which checks whether an object is of stream type. +* **Changed** low-level functions :meth:`Document._getXrefString` and :meth:`Document._getTrailerString` now by default return object definitions in a formatted form which makes parsing easy. + +------ + +**Changes in Version 1.14.13** + +* **Changed** methods working with binary input: while ever supporting bytes and bytearray objects, they now also accept *io.BytesIO* input, using their *getvalue()* method. This pertains to document creation, embedded files, FileAttachment annotations, pixmap creation and others. Fixes issue #274 ("Segfault when using BytesIO as a stream for insertImage"). +* **Fixed** issue #278 ("Is insertImage(keep_proportion=True) broken?"). Images are now correctly presented when keeping aspect ratio. + + +------ + +**Changes in Version 1.14.12** + +* **Changed** the draw methods of :ref:`Page` and :ref:`Shape` to support not only RGB, but also GRAY and CMYK colorspaces. This solves issue #270 ("Is there a way to use CMYK color to draw shapes?"). This change also applies to text insertion methods of :ref:`Shape`, resp. :ref:`Page`. +* **Fixed** issue #269 ("AttributeError in Document.insert_page()"), which occurred when using :meth:`Document.insert_page` with text insertion. + + +------ + +**Changes in Version 1.14.11** + +* **Changed** :meth:`Page.show_pdf_page` to always position the source rectangle centered in the target. This method now also supports **rotation by arbitrary angles**. The argument *reuse_xref* has been deprecated: prevention of duplicates is now **handled internally**. +* **Changed** :meth:`Page.insertImage` to support rotated display of the image and keeping the aspect ratio. Only rotations by multiples of 90 degrees are supported here. +* **Fixed** issue #265 ("TypeError: insertText() got an unexpected keyword argument 'idx'"). This issue only occurred when using :meth:`Document.insert_page` with also inserting text. + +------ + +**Changes in Version 1.14.10** + +* **Changed** :meth:`Page.show_pdf_page` to support rotation of the source rectangle. Fixes #261 ("Cannot rotate insterted pages"). +* **Fixed** a bug in :meth:`Page.insertImage` which prevented insertion of multiple images provided as streams. + + +------ + +**Changes in Version 1.14.9** + +* **Added** new low-level method :meth:`Document._getTrailerString`, which returns the trailer object of a PDF. This is much like :meth:`Document._getXrefString` except that the PDF trailer has no / needs no :data:`xref` to identify it. +* **Added** new parameters for text insertion methods. You can now set stroke and fill colors of glyphs (text characters) independently, as well as the thickness of the glyph border. A new parameter *render_mode* controls the use of these colors, and whether the text should be visible at all. +* **Fixed** issue #258 ("Copying image streams to new PDF without size increase"): For JPX images embedded in a PDF, :meth:`Document.extractImage` will now return them in their original format. Previously, the MuPDF base library was used, which returns them in PNG format (entailing a massive size increase). +* **Fixed** issue #259 ("Morphing text to fit inside rect"). Clarified use of :meth:`get_text_length` and removed extra line breaks for long words. + +------ + +**Changes in Version 1.14.8** + +* **Added** :meth:`Pixmap.set_rect` to change the pixel values in a rectangle. This is also an alternative to setting the color of a complete pixmap (:meth:`Pixmap.clear_with`). +* **Fixed** an image extraction issue with JBIG2 (monochrome) encoded PDF images. The issue occurred in :meth:`Page.getText` (parameters "dict" and "rawdict") and in :meth:`Document.extractImage` methods. +* **Fixed** an issue with not correctly clearing a non-alpha :ref:`Pixmap` (:meth:`Pixmap.clear_with`). +* **Fixed** an issue with not correctly inverting colors of a non-alpha :ref:`Pixmap` (:meth:`Pixmap.invert_irect`). + +------ + +**Changes in Version 1.14.7** + +* **Added** :meth:`Pixmap.set_pixel` to change one pixel value. +* **Added** documentation for image conversion in the :ref:`FAQ`. +* **Added** new function :meth:`get_text_length` to determine the string length for a given font. +* **Added** Postscript image output (changed :meth:`Pixmap.save` and :meth:`Pixmap.tobytes`). +* **Changed** :meth:`Pixmap.save` and :meth:`Pixmap.tobytes` to ensure valid combinations of colorspace, alpha and output format. +* **Changed** :meth:`Pixmap.save`: the desired format is now inferred from the filename. +* **Changed** FreeText annotations can now have a transparent background - see :meth:`Annot.update`. + +------ + +**Changes in Version 1.14.5** + +* **Changed:** :ref:`Shape` methods now strictly use the transformation matrix of the :ref:`Page` -- instead of "manually" calculating locations. +* **Added** method :meth:`Pixmap.pixel` which returns the pixel value (a list) for given pixel coordinates. +* **Added** method :meth:`Pixmap.tobytes` which returns a bytes object representing the pixmap in a variety of formats. Previously, this could be done for PNG outputs only (:meth:`Pixmap.tobytes`). +* **Changed:** output of methods :meth:`Pixmap.save` and (the new) :meth:`Pixmap.tobytes` may now also be PSD (Adobe Photoshop Document). +* **Added** method :meth:`Shape.drawQuad` which draws a :ref:`Quad`. This actually is a shorthand for a :meth:`Shape.drawPolyline` with the edges of the quad. +* **Changed** method :meth:`Shape.drawOval`: the argument can now be **either** a rectangle (:data:`rect_like`) **or** a quadrilateral (:data:`quad_like`). + +------ + +**Changes in Version 1.14.4** + +* **Fixes** issue #239 "Annotation coordinate consistency". + + +------ + +**Changes in Version 1.14.3** + +This patch version contains minor bug fixes and CJK font output support. + +* **Added** support for the four CJK fonts as PyMuPDF generated text output. This pertains to methods :meth:`Page.insertFont`, :meth:`Shape.insertText`, :meth:`Shape.insertTextbox`, and corresponding :ref:`Page` methods. The new fonts are available under "reserved" fontnames "china-t" (traditional Chinese), "china-s" (simplified Chinese), "japan" (Japanese), and "korea" (Korean). +* **Added** full support for the built-in fonts 'Symbol' and 'Zapfdingbats'. +* **Changed:** The 14 standard fonts can now each be referenced by a 4-letter abbreviation. + +------ + +**Changes in Version 1.14.1** + +This patch version contains minor performance improvements. + +* **Added** support for :ref:`Document` filenames given as *pathlib* object by using the Python *str()* function. + + +------ + +**Changes in Version 1.14.0** + +To support MuPDF v1.14.0, massive changes were required in PyMuPDF -- most of them purely technical, with little visibility to developers. But there are also quite a lot of interesting new and improved features. Following are the details: + +* **Added** "ink" annotation. +* **Added** "rubber stamp" annotation. +* **Added** "squiggly" text marker annotation. +* **Added** new class :ref:`Quad` (quadrilateral or tetragon) -- which represents a general four-sided shape in the plane. The special subtype of rectangular, non-empty tetragons is used in text marker annotations and as returned objects in text search methods. +* **Added** a new option "decrypt" to :meth:`Document.save` and :meth:`Document.write`. Now you can **keep encryption** when saving a password protected PDF. +* **Added** suppression and redirection of unsolicited messages issued by the underlying C-library MuPDF. Consult :ref:`RedirectMessages` for details. +* **Changed:** Changes to annotations now **always require** :meth:`Annot.update` to become effective. +* **Changed** free text annotations to support the full Latin character set and range of appearance options. +* **Changed** text searching, :meth:`Page.searchFor`, to optionally return :ref:`Quad` instead :ref:`Rect` objects surrounding each search hit. +* **Changed** plain text output: we now add a *\n* to each line if it does not itself end with this character. +* **Fixed** issue 211 ("Something wrong in the doc"). +* **Fixed** issue 213 ("Rewritten outline is displayed only by mupdf-based applications"). +* **Fixed** issue 214 ("PDF decryption GONE!"). +* **Fixed** issue 215 ("Formatting of links added with pyMuPDF"). +* **Fixed** issue 217 ("extraction through json is failing for my pdf"). + +Behind the curtain, we have changed the implementation of geometry objects: they now purely exist in Python and no longer have "shadow" twins on the C-level (in MuPDF). This has improved processing speed in that area by more than a factor of two. + +Because of the same reason, most methods involving geometry parameters now also accept the corresponding Python sequence. For example, in method *"page.show_pdf_page(rect, ...)"* parameter *rect* may now be any :data:`rect_like` sequence. + +We also invested considerable effort to further extend and improve the :ref:`FAQ` chapter. + + +------ + +**Changes in Version 1.13.19** + +This version contains some technical / performance improvements and bug fixes. + +* **Changed** memory management: for Python 3 builds, Python memory management is exclusively used across all C-level code (i.e. no more native *malloc()* in MuPDF code or PyMuPDF interface code). This leads to improved memory usage profiles and also some runtime improvements: we have seen > 2% shorter runtimes for text extractions and pixmap creations (on Windows machines only to date). +* **Fixed** an error occurring in Python 2.7, which crashed the interpreter when using :meth:`TextPage.extractRAWDICT` (= *Page.getText("rawdict")*). +* **Fixed** an error occurring in Python 2.7, when creating link destinations. +* **Extended** the :ref:`FAQ` chapter with more examples. + +------ + +**Changes in Version 1.13.18** + +* **Added** method :meth:`TextPage.extractRAWDICT`, and a corresponding new string parameter "rawdict" to method :meth:`Page.getText`. It extracts text and images from a page in Python *dict* form like :meth:`TextPage.extractDICT`, but with the detail level of :meth:`TextPage.extractXML`, which is position information down to each single character. + +------ + +**Changes in Version 1.13.17** + +* **Fixed** an error that intermittently caused an exception in :meth:`Page.show_pdf_page`, when pages from many different source PDFs were shown. +* **Changed** method :meth:`Document.extractImage` to now return more meta information about the extracted imgage. Also, its performance has been greatly improved. Several demo scripts have been changed to make use of this method. +* **Changed** method :meth:`Document._getXrefStream` to now return *None* if the object is no stream and no longer raise an exception if otherwise. +* **Added** method :meth:`Document._deleteObject` which deletes a PDF object identified by its :data:`xref`. Only to be used by the experienced PDF expert. +* **Added** a method :meth:`paper_rect` which returns a :ref:`Rect` for a supplied paper format string. Example: *fitz.paper_rect("letter") = fitz.Rect(0.0, 0.0, 612.0, 792.0)*. +* **Added** a :ref:`FAQ` chapter to this document. + +------ + +**Changes in Version 1.13.16** + +* **Added** support for correctly setting transparency (opacity) for certain annotation types. +* **Added** a tool property (:attr:`Tools.fitz_config`) showing the configuration of this PyMuPDF version. +* **Fixed** issue #193 ('insertText(overlay=False) gives "cannot resize a buffer with shared storage" error') by avoiding read-only buffers. + +------ + +**Changes in Version 1.13.15** + +* **Fixed** issue #189 ("cannot find builtin CJK font"), so we are supporting builtin CJK fonts now (CJK = China, Japan, Korea). This should lead to correctly generated pixmaps for documents using these languages. This change has consequences for our binary file size: it will now range between 8 and 10 MB, depending on the OS. +* **Fixed** issue #191 ("Jupyter notebook kernel dies after ca. 40 pages"), which occurred when modifying the contents of an annotation. + +------ + +**Changes in Version 1.13.14** + +This patch version contains several improvements, mainly for annotations. + +* **Changed** :attr:`Annot.lineEnds` is now a list of two integers representing the line end symbols. Previously was a *dict* of strings. +* **Added** support of line end symbols for applicable annotations. PyMuPDF now can generate these annotations including the line end symbols. +* **Added** :meth:`Annot.setLineEnds` adds line end symbols to applicable annotation types ('Line', 'PolyLine', 'Polygon'). +* **Changed** technical implementation of :meth:`Page.insertImage` and :meth:`Page.show_pdf_page`: they now create there own contents objects, thereby avoiding changes of potentially large streams with consequential compression / decompression efforts and high change volumes with incremental updates. + +------ + +**Changes in Version 1.13.13** + +This patch version contains several improvements for embedded files and file attachment annotations. + +* **Added** :meth:`Document.embfile_Upd` which allows changing **file content and metadata** of an embedded file. It supersedes the old method :meth:`Document.embfile_SetInfo` (which will be deleted in a future version). Content is automatically compressed and metadata may be unicode. +* **Changed** :meth:`Document.embfile_Add` to now automatically compress file content. Accompanying metadata can now be unicode (had to be ASCII in the past). +* **Changed** :meth:`Document.embfile_Del` to now automatically delete **all entries** having the supplied identifying name. The return code is now an integer count of the removed entries (was *None* previously). +* **Changed** embedded file methods to now also accept or show the PDF unicode filename as additional parameter *ufilename*. +* **Added** :meth:`Page.add_file_annot` which adds a new file attachment annotation. +* **Changed** :meth:`Annot.fileUpd` (file attachment annot) to now also accept the PDF unicode *ufilename* parameter. The description parameter *desc* correctly works with unicode. Furthermore, **all** parameters are optional, so metadata may be changed without also replacing the file content. +* **Changed** :meth:`Annot.fileInfo` (file attachment annot) to now also show the PDF unicode filename as parameter *ufilename*. +* **Fixed** issue #180 ("page.getText(output='dict') return invalid bbox") to now also work for vertical text. +* **Fixed** issue #185 ("Can't render the annotations created by PyMuPDF"). The issue's cause was the minimalistic MuPDF approach when creating annotations. Several annotation types have no */AP* ("appearance") object when created by MuPDF functions. MuPDF, SumatraPDF and hence also PyMuPDF cannot render annotations without such an object. This fix now ensures, that an appearance object is always created together with the annotation itself. We still do not support line end styles. + +------ + +**Changes in Version 1.13.12** + +* **Fixed** issue #180 ("page.getText(output='dict') return invalid bbox"). Note that this is a circumvention of an MuPDF error, which generates zero-height character rectangles in some cases. When this happens, this fix ensures a bbox height of at least fontsize. +* **Changed** for ListBox and ComboBox widgets, the attribute list of selectable values has been renamed to :attr:`Widget.choice_values`. +* **Changed** when adding widgets, any missing of the :ref:`Base-14-Fonts` is automatically added to the PDF. Widget text fonts can now also be chosen from existing widget fonts. Any specified field values are now honored and lead to a field with a preset value. +* **Added** :meth:`Annot.updateWidget` which allows changing existing form fields -- including the field value. + +------ + +**Changes in Version 1.13.11** + +While the preceeding patch subversions only contained various fixes, this version again introduces major new features: + +* **Added** basic support for PDF widget annotations. You can now add PDF form fields of types Text, CheckBox, ListBox and ComboBox. Where necessary, the PDF is tranformed to a Form PDF with the first added widget. +* **Fixed** issues #176 ("wrong file embedding"), #177 ("segment fault when invoking page.getText()")and #179 ("Segmentation fault using page.getLinks() on encrypted PDF"). + + +------ + +**Changes in Version 1.13.7** + +* **Added** support of variable page sizes for reflowable documents (e-books, HTML, etc.): new parameters *rect* and *fontsize* in :ref:`Document` creation (open), and as a separate method :meth:`Document.layout`. +* **Added** :ref:`Annot` creation of many annotations types: sticky notes, free text, circle, rectangle, line, polygon, polyline and text markers. +* **Added** support of annotation transparency (:attr:`Annot.opacity`, :meth:`Annot.setOpacity`). +* **Changed** :attr:`Annot.vertices`: point coordinates are now grouped as pairs of floats (no longer as separate floats). +* **Changed** annotation colors dictionary: the two keys are now named *"stroke"* (formerly *"common"*) and *"fill"*. +* **Added** :attr:`Document.isDirty` which is *True* if a PDF has been changed in this session. Reset to *False* on each :meth:`Document.save` or :meth:`Document.write`. + +------ + +**Changes in Version 1.13.6** + +* Fix #173: for memory-resident documents, ensure the stream object will not be garbage-collected by Python before document is closed. + +------ + +**Changes in Version 1.13.5** + +* New low-level method :meth:`Page._setContents` defines an object given by its :data:`xref` to serve as the :data:`contents` object. +* Changed and extended PDF form field support: the attribute *widget_text* has been renamed to :attr:`Annot.widget_value`. Values of all form field types (except signatures) are now supported. A new attribute :attr:`Annot.widget_choices` contains the selectable values of listboxes and comboboxes. All these attributes now contain *None* if no value is present. + +------ + +**Changes in Version 1.13.4** + +* :meth:`Document.convertToPDF` now supports page ranges, reverted page sequences and page rotation. If the document already is a PDF, an exception is raised. +* Fixed a bug (introduced with v1.13.0) that prevented :meth:`Page.insertImage` for transparent images. + +------ + +**Changes in Version 1.13.3** + +Introduces a way to convert **any MuPDF supported document** to a PDF. If you ever wanted PDF versions of your XPS, EPUB, CBZ or FB2 files -- here is a way to do this. + +* :meth:`Document.convertToPDF` returns a Python *bytes* object in PDF format. Can be opened like normal in PyMuPDF, or be written to disk with the *".pdf"* extension. + +------ + +**Changes in Version 1.13.2** + +The major enhancement is PDF form field support. Form fields are annotations of type *(19, 'Widget')*. There is a new document method to check whether a PDF is a form. The :ref:`Annot` class has new properties describing field details. + +* :attr:`Document.is_form_pdf` is true if object type */AcroForm* and at least one form field exists. +* :attr:`Annot.widget_type`, :attr:`Annot.widget_text` and :attr:`Annot.widget_name` contain the details of a form field (i.e. a "Widget" annotation). + +------ + +**Changes in Version 1.13.1** + +* :meth:`TextPage.extractDICT` is a new method to extract the contents of a document page (text and images). All document types are supported as with the other :ref:`TextPage` *extract*()* methods. The returned object is a dictionary of nested lists and other dictionaries, and **exactly equal** to the JSON-deserialization of the old :meth:`TextPage.extractJSON`. The difference is that the result is created directly -- no JSON module is used. Because the user needs no JSON module to interpet the information, it should be easier to use, and also have a better performance, because it contains images in their original **binary format** -- they need not be base64-decoded. +* :meth:`Page.getText` correspondingly supports the new parameter value *"dict"* to invoke the above method. +* :meth:`TextPage.extractJSON` (resp. *Page.getText("json")*) is still supported for convenience, but its use is expected to decline. + +------ + +**Changes in Version 1.13.0** + +This version is based on MuPDF v1.13.0. This release is "primarily a bug fix release". + +In PyMuPDF, we are also doing some bug fixes while introducing minor enhancements. There only very minimal changes to the user's API. + +* :ref:`Document` construction is more flexible: the new *filetype* parameter allows setting the document type. If specified, any extension in the filename will be ignored. More completely addresses `issue #156 `_. As part of this, the documentation has been reworked. + +* Changes to :ref:`Pixmap` constructors: + - Colorspace conversion no longer allows dropping the alpha channel: source and target **alpha will now always be the same**. We have seen exceptions and even interpreter crashes when using *alpha = 0*. + - As a replacement, the simple pixmap copy lets you choose the target alpha. + +* :meth:`Document.save` again offers the full garbage collection range 0 thru 4. Because of a bug in :data:`xref` maintenance, we had to temporarily enforce *garbage > 1*. Finally resolves `issue #148 `_. + +* :meth:`Document.save` now offers to "prettify" PDF source via an additional argument. +* :meth:`Page.insertImage` has the additional *stream* \-parameter, specifying a memory area holding an image. + +* Issue with garbled PNGs on Linux systems has been resolved (`"Problem writing PNG" #133) `_. + + +------ + +**Changes in Version 1.12.4** + +This is an extension of 1.12.3. + +* Fix of `issue #147 `_: methods :meth:`Document.getPageFontlist` and :meth:`Document.getPageImagelist` now also show fonts and images contained in :data:`resources` nested via "Form XObjects". +* Temporary fix of `issue #148 `_: Saving to new PDF files will now automatically use *garbage = 2* if a lower value is given. Final fix is to be expected with MuPDF's next version. At that point we will remove this circumvention. +* Preventive fix of illegally using stencil / image mask pixmaps in some methods. +* Method :meth:`Document.getPageFontlist` now includes the encoding name for each font in the list. +* Method :meth:`Document.getPageImagelist` now includes the decode method name for each image in the list. + +------ + +**Changes in Version 1.12.3** + +This is an extension of 1.12.2. + +* Many functions now return *None* instead of *0*, if the result has no other meaning than just indicating successful execution (:meth:`Document.close`, :meth:`Document.save`, :meth:`Document.select`, :meth:`Pixmap.save` and many others). + +------ + +**Changes in Version 1.12.2** + +This is an extension of 1.12.1. + +* Method :meth:`Page.show_pdf_page` now accepts the new *clip* argument. This specifies an area of the source page to which the display should be restricted. + +* New :attr:`Page.CropBox` and :attr:`Page.MediaBox` have been included for convenience. + + +------ + +**Changes in Version 1.12.1** + +This is an extension of version 1.12.0. + +* New method :meth:`Page.show_pdf_page` displays another's PDF page. This is a **vector** image and therefore remains precise across zooming. Both involved documents must be PDF. + +* New method :meth:`Page.getSVGimage` creates an SVG image from the page. In contrast to the raster image of a pixmap, this is a vector image format. The return is a unicode text string, which can be saved in a *.svg* file. + +* Method :meth:`Page.getTextBlocks` now accepts an additional bool parameter "images". If set to true (default is false), image blocks (metadata only) are included in the produced list and thus allow detecting areas with rendered images. + +* Minor bug fixes. + +* "text" result of :meth:`Page.getText` concatenates all lines within a block using a single space character. MuPDF's original uses "\\n" instead, producing a rather ragged output. + +* New properties of :ref:`Page` objects :attr:`Page.MediaBoxSize` and :attr:`Page.CropBoxPosition` provide more information about a page's dimensions. For non-PDF files (and for most PDF files, too) these will be equal to :attr:`Page.rect.bottom_right`, resp. :attr:`Page.rect.top_left`. For example, class :ref:`Shape` makes use of them to correctly position its items. + +------ + +**Changes in Version 1.12.0** + +This version is based on and requires MuPDF v1.12.0. The new MuPDF version contains quite a number of changes -- most of them around text extraction. Some of the changes impact the programmer's API. + +* :meth:`Outline.saveText` and :meth:`Outline.saveXML` have been deleted without replacement. You probably haven't used them much anyway. But if you are looking for a replacement: the output of :meth:`Document.get_toc` can easily be used to produce something equivalent. + +* Class *TextSheet* does no longer exist. + +* Text "spans" (one of the hierarchy levels of :ref:`TextPage`) no longer contain positioning information (i.e. no "bbox" key). Instead, spans now provide the font information for its text. This impacts our JSON output variant. + +* HTML output has improved very much: it now creates valid documents which can be displayed by browsers to produce a similar view as the original document. + +* There is a new output format XHTML, which provides text and images in a browser-readable format. The difference to HTML output is, that no effort is made to reproduce the original layout. + +* All output formats of :meth:`Page.getText` now support creating complete, valid documents, by wrapping them with appropriate header and trailer information. If you are interested in using the HTML output, please make sure to read :ref:`HTMLQuality`. + +* To support finding text positions, we have added special methods that don't need detours like :meth:`TextPage.extractJSON` or :meth:`TextPage.extractXML`: use :meth:`Page.getTextBlocks` or resp. :meth:`Page.getTextWords` to create lists of text blocks or resp. words, which are accompanied by their rectangles. This should be much faster than the standard text extraction methods and also avoids using additional packages for interpreting their output. + + +------ + +**Changes in Version 1.11.2** + +This is an extension of v1.11.1. + +* New :meth:`Page.insertFont` creates a PDF */Font* object and returns its object number. + +* New :meth:`Document.extractFont` extracts the content of an embedded font given its object number. + +* Methods **FontList(...)** items no longer contain the PDF generation number. This value never had any significance. Instead, the font file extension is included (e.g. "pfa" for a "PostScript Font for ASCII"), which is more valuable information. + +* Fonts other than "simple fonts" (Type1) are now also supported. + +* New options to change :ref:`Pixmap` size: + + * Method :meth:`Pixmap.shrink` reduces the pixmap proportionally in place. + + * A new :ref:`Pixmap` copy constructor allows scaling via setting target width and height. + + +------ + +**Changes in Version 1.11.1** + +This is an extension of v1.11.0. + +* New class *Shape*. It facilitates and extends the creation of image shapes on PDF pages. It contains multiple methods for creating elementary shapes like lines, rectangles or circles, which can be combined into more complex ones and be given common properties like line width or colors. Combined shapes are handled as a unit and e.g. be "morphed" together. The class can accumulate multiple complex shapes and put them all in the page's foreground or background -- thus also reducing the number of updates to the page's :data:`contents` object. + +* All *Page* draw methods now use the new *Shape* class. + +* Text insertion methods *insertText()* and *insertTextBox()* now support morphing in addition to text rotation. They have become part of the *Shape* class and thus allow text to be freely combined with graphics. + +* A new *Pixmap* constructor allows creating pixmap copies with an added alpha channel. A new method also allows directly manipulating alpha values. + +* Binary algebraic operations with geometry objects (matrices, rectangles and points) now generally also support lists or tuples as the second operand. You can add a tuple *(x, y)* of numbers to a :ref:`Point`. In this context, such sequences are called ":data:`point_like`" (resp. :data:`matrix_like`, :data:`rect_like`). + +* Geometry objects now fully support in-place operators. For example, *p /= m* replaces point p with *p * 1/m* for a number, or *p * ~m* for a :data:`matrix_like` object *m*. Similarly, if *r* is a rectangle, then *r |= (3, 4)* is the new rectangle that also includes *fitz.Point(3, 4)*, and *r &= (1, 2, 3, 4)* is its intersection with *fitz.Rect(1, 2, 3, 4)*. + +------ + +**Changes in Version 1.11.0** + +This version is based on and requires MuPDF v1.11. + +Though MuPDF has declared it as being mostly a bug fix version, one major new feature is indeed contained: support of embedded files -- also called portfolios or collections. We have extended PyMuPDF functionality to embrace this up to an extent just a little beyond the *mutool* utility as follows. + +* The *Document* class now support embedded files with several new methods and one new property: + + - *embfile_Info()* returns metadata information about an entry in the list of embedded files. This is more than *mutool* currently provides: it shows all the information that was used to embed the file (not just the entry's name). + - *embfile_Get()* retrieves the (decompressed) content of an entry into a *bytes* buffer. + - *embfile_Add(...)* inserts new content into the PDF portfolio. We (in contrast to *mutool*) **restrict** this to entries with a **new name** (no duplicate names allowed). + - *embfile_Del(...)* deletes an entry from the portfolio (function not offered in MuPDF). + - *embfile_SetInfo()* -- changes filename or description of an embedded file. + - *embfile_Count* -- contains the number of embedded files. + +* Several enhancements deal with streamlining geometry objects. These are not connected to the new MuPDF version and most of them are also reflected in PyMuPDF v1.10.0. Among them are new properties to identify the corners of rectangles by name (e.g. *Rect.bottom_right*) and new methods to deal with set-theoretic questions like *Rect.contains(x)* or *IRect.intersects(x)*. Special effort focussed on supporting more "Pythonic" language constructs: *if x in rect ...* is equivalent to *rect.contains(x)*. + +* The :ref:`Rect` chapter now has more background on empty amd infinite rectangles and how we handle them. The handling itself was also updated for more consistency in this area. + +* We have started basic support for **generation** of PDF content: + + - *Document.insert_page()* adds a new page into a PDF, optionally containing some text. + - *Page.insertImage()* places a new image on a PDF page. + - *Page.insertText()* puts new text on an existing page + +* For **FileAttachment** annotations, content and name of the attached file can extracted and changed. + +------ + +**Changes in Version 1.10.0** + +**MuPDF v1.10 Impact** + +MuPDF version 1.10 has a significant impact on our bindings. Some of the changes also affect the API -- in other words, **you** as a PyMuPDF user. + +* Link destination information has been reduced. Several properties of the *linkDest* class no longer contain valuable information. In fact, this class as a whole has been deleted from MuPDF's library and we in PyMuPDF only maintain it to provide compatibilty to existing code. + +* In an effort to minimize memory requirements, several improvements have been built into MuPDF v1.10: + + - A new *config.h* file can be used to de-select unwanted features in the C base code. Using this feature we have been able to reduce the size of our binary *_fitz.o* / *_fitz.pyd* by about 50% (from 9 MB to 4.5 MB). When UPX-ing this, the size goes even further down to a very handy 2.3 MB. + + - The alpha (transparency) channel for pixmaps is now optional. Letting alpha default to *False* significantly reduces pixmap sizes (by 20% -- CMYK, 25% -- RGB, 50% -- GRAY). Many *Pixmap* constructors therefore now accept an *alpha* boolean to control inclusion of this channel. Other pixmap constructors (e.g. those for file and image input) create pixmaps with no alpha alltogether. On the downside, save methods for pixmaps no longer accept a *savealpha* option: this channel will always be saved when present. To minimize code breaks, we have left this parameter in the call patterns -- it will just be ignored. + +* *DisplayList* and *TextPage* class constructors now **require the mediabox** of the page they are referring to (i.e. the *page.bound()* rectangle). There is no way to construct this information from other sources, therefore a source code change cannot be avoided in these cases. We assume however, that not many users are actually employing these rather low level classes explixitely. So the impact of that change should be minor. + +**Other Changes compared to Version 1.9.3** + +* The new :ref:`Document` method *write()* writes an opened PDF to memory (as opposed to a file, like *save()* does). +* An annotation can now be scaled and moved around on its page. This is done by modifying its rectangle. +* Annotations can now be deleted. :ref:`Page` contains the new method *deleteAnnot()*. +* Various annotation attributes can now be modified, e.g. content, dates, title (= author), border, colors. +* Method *Document.insert_pdf()* now also copies annotations of source pages. +* The *Pages* class has been deleted. As documents can now be accessed with page numbers as indices (like *doc[n] = doc.loadPage(n)*), and document object can be used as iterators, the benefit of this class was too low to maintain it. See the following comments. +* *loadPage(n)* / *doc[n]* now accept arbitrary integers to specify a page number, as long as *n < pageCount*. So, e.g. *doc[-500]* is always valid and will load page *(-500) % pageCount*. +* A document can now also be used as an iterator like this: *for page in doc: ... ...*. This will yield all pages of *doc* as *page*. +* The :ref:`Pixmap` method *getSize()* has been replaced with property *size*. As before *Pixmap.size == len(Pixmap)* is true. +* In response to transparency (alpha) being optional, several new parameters and properties have been added to :ref:`Pixmap` and :ref:`Colorspace` classes to support determining their characteristics. +* The :ref:`Page` class now contains new properties *firstAnnot* and *firstLink* to provide starting points to the respective class chains, where *firstLink* is just a mnemonic synonym to method *loadLinks()* which continues to exist. Similarly, the new property *rect* is a synonym for method *bound()*, which also continues to exist. +* :ref:`Pixmap` methods *samplesRGB()* and *samplesAlpha()* have been deleted because pixmaps can now be created without transparency. +* :ref:`Rect` now has a property *irect* which is a synonym of method *round()*. Likewise, :ref:`IRect` now has property *rect* to deliver a :ref:`Rect` which has the same coordinates as floats values. +* Document has the new method *searchPageFor()* to search for a text string. It works exactly like the corresponding *Page.searchFor()* with page number as additional parameter. + + +------ + +**Changes in Version 1.9.3** + +This version is also based on MuPDF v1.9a. Changes compared to version 1.9.2: + +* As a major enhancement, annotations are now supported in a similar way as links. Annotations can be displayed (as pixmaps) and their properties can be accessed. +* In addition to the document *select()* method, some simpler methods can now be used to manipulate a PDF: + + - *copyPage()* copies a page within a document. + - *movePage()* is similar, but deletes the original. + - *delete_page()* deletes a page + - *delete_pages()* deletes a page range + +* *rotation* or *setRotation()* access or change a PDF page's rotation, respectively. +* Available but undocumented before, :ref:`IRect`, :ref:`Rect`, :ref:`Point` and :ref:`Matrix` support the *len()* method and their coordinate properties can be accessed via indices, e.g. *IRect.x1 == IRect[2]*. +* For convenience, documents now support simple indexing: *doc.loadPage(n) == doc[n]*. The index may however be in range *-pageCount < n < pageCount*, such that *doc[-1]* is the last page of the document. + +------ + +**Changes in Version 1.9.2** + +This version is also based on MuPDF v1.9a. Changes compared to version 1.9.1: + +* *fitz.open()* (no parameters) creates a new empty **PDF** document, i.e. if saved afterwards, it must be given a *.pdf* extension. +* :ref:`Document` now accepts all of the following formats (*Document* and *open* are synonyms): + + - *open()*, + - *open(filename)* (equivalent to *open(filename, None)*), + - *open(filetype, area)* (equivalent to *open(filetype, stream = area)*). + + Type of memory area *stream* may be *bytes* or *bytearray*. Thus, e.g. *area = open("file.pdf", "rb").read()* may be used directly (without first converting it to bytearray). +* New method *Document.insert_pdf()* (PDFs only) inserts a range of pages from another PDF. +* *Document* objects doc now support the *len()* function: ``len(doc) == doc.pageCount``. +* New method *Document.getPageImageList()* creates a list of images used on a page. +* New method *Document.getPageFontList()* creates a list of fonts referenced by a page. +* New pixmap constructor *fitz.Pixmap(doc, xref)* creates a pixmap based on an opened PDF document and an :data:`xref` number of the image. +* New pixmap constructor *fitz.Pixmap(cspace, spix)* creates a pixmap as a copy of another one *spix* with the colorspace converted to *cspace*. This works for all colorspace combinations. +* Pixmap constructor *fitz.Pixmap(colorspace, width, height, samples)* now allows *samples* to also be *bytes*, not only *bytearray*. + + +------ + +**Changes in Version 1.9.1** + +This version of PyMuPDF is based on MuPDF library source code version 1.9a published on April 21, 2016. + +Please have a look at MuPDF's website to see which changes and enhancements are contained herein. + +Changes in version 1.9.1 compared to version 1.8.0 are the following: + +* New methods *get_area()* for both *fitz.Rect* and *fitz.IRect* +* Pixmaps can now be created directly from files using the new constructor *fitz.Pixmap(filename)*. +* The Pixmap constructor *fitz.Pixmap(image)* has been extended accordingly. +* *fitz.Rect* can now be created with all possible combinations of points and coordinates. +* PyMuPDF classes and methods now all contain __doc__ strings, most of them created by SWIG automatically. While the PyMuPDF documentation certainly is more detailed, this feature should help a lot when programming in Python-aware IDEs. +* A new document method of *getPermits()* returns the permissions associated with the current access to the document (print, edit, annotate, copy), as a Python dictionary. +* The identity matrix *fitz.Identity* is now **immutable**. +* The new document method *select(list)* removes all pages from a document that are not contained in the list. Pages can also be duplicated and re-arranged. +* Various improvements and new members in our demo and examples collections. Perhaps most prominently: *PDF_display* now supports scrolling with the mouse wheel, and there is a new example program *wxTableExtract* which allows to graphically identify and extract table data in documents. +* *fitz.open()* is now an alias of *fitz.Document()*. +* New pixmap method *tobytes()* which will return a bytearray formatted as a PNG image of the pixmap. +* New pixmap method *samplesRGB()* providing a *samples* version with alpha bytes stripped off (RGB colorspaces only). +* New pixmap method *samplesAlpha()* providing the alpha bytes only of the *samples* area. +* New iterator *fitz.Pages(doc)* over a document's set of pages. +* New matrix methods *invert()* (calculate inverted matrix), *concat()* (calculate matrix product), *pretranslate()* (perform a shift operation). +* New *IRect* methods *intersect()* (intersection with another rectangle), *translate()* (perform a shift operation). +* New *Rect* methods *intersect()* (intersection with another rectangle), *transform()* (transformation with a matrix), *include_point()* (enlarge rectangle to also contain a point), *include_rect()* (enlarge rectangle to also contain another one). +* Documented *Point.transform()* (transform a point with a matrix). +* *Matrix*, *IRect*, *Rect* and *Point* classes now support compact, algebraic formulations for manipulating such objects. +* Incremental saves for changes are possible now using the call pattern *doc.save(doc.name, incremental=True)*. +* A PDF's metadata can now be deleted, set or changed by document method *set_metadata()*. Supports incremental saves. +* A PDF's bookmarks (or table of contents) can now be deleted, set or changed with the entries of a list using document method *set_toc(list)*. Supports incremental saves. diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..3690e84 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,59 @@ +# PyMuPDF documentation + +Welcome to the PyMuPDF documentation. This documentation relies on [Sphinx](https://www.sphinx-doc.org/en/master/) to publish HTML docs from markdown files written with [restructured text](https://en.wikipedia.org/wiki/ReStructuredText) (RST). + +## Sphinx version + +This README assumes you have [Sphinx v5.0.2 installed](https://www.sphinx-doc.org/en/master/usage/installation.html) on your system. + + +## Updating the documentation + +Within `docs` update the associated restructured text (`.rst`) files. These files represent the corresponding document pages. + + + +## Building HTML documentation + +- Ensure you have the `furo` theme installed: + +`pip install furo` + +Furo theme, Copyright (c) 2020 Pradyun Gedam , thank you to: + +https://github.com/pradyunsg/furo/blob/main/LICENSE + + +- From the "docs" location run: + +`sphinx-build -b html . build/html` + +This then creates the HTML documentation within `build/html`. + +> Use: `sphinx-build -a -b html . build/html` to build all, including the assets in `_static` (important if you have updated CSS). + + +## Building PDF documentation + + +- First ensure you have [rst2pdf](https://pypi.org/project/rst2pdf/) installed: + + +`python -m pip install rst2pdf` + + +- Then run: + + +`sphinx-build -b pdf source build/pdf` + +This will then generate a single PDF for all of the documentation within `build/pdf`. + + +--- + + +For full details see: [Using Sphinx](https://www.sphinx-doc.org/en/master/usage/index.html) + + + diff --git a/docs/about-feature-matrix.rst b/docs/about-feature-matrix.rst new file mode 100644 index 0000000..666cc1d --- /dev/null +++ b/docs/about-feature-matrix.rst @@ -0,0 +1,342 @@ + + +.. required image embeds for HTML to reference + + +.. image:: images/icons/icon-pdf.svg + :width: 0 + :height: 0 + +.. image:: images/icons/icon-svg.svg + :width: 0 + :height: 0 + +.. image:: images/icons/icon-xps.svg + :width: 0 + :height: 0 + +.. image:: images/icons/icon-cbz.svg + :width: 0 + :height: 0 + +.. image:: images/icons/icon-mobi.svg + :width: 0 + :height: 0 + +.. image:: images/icons/icon-epub.svg + :width: 0 + :height: 0 + +.. image:: images/icons/icon-image.svg + :width: 0 + :height: 0 + +.. image:: images/icons/icon-fb2.svg + :width: 0 + :height: 0 + +.. raw:: html + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FeaturePyMuPDFpikepdfPyPDF2pdfrw
Supports Multiple Document Formats + PDF + XPS + EPUB + MOBI + FB2 + CBZ + SVG + Image + + PDF + + PDF + + PDF +
ImplementationC and PythonC++ and PythonPythonPython
Render Document PagesAll document typesNo renderingNo renderingNo rendering
Extract TextAll document typesPDF only
Extract Vector GraphicsAll document types
Draw Vector Graphics (PDF)
Based on Existing, Mature LibraryMuPDFQPDF
Automatic Repair of Damaged PDFs
Encrypted PDFsLimited
Linerarized PDFs
Incremental Updates
Integrates with Jupyter and IPython Notebooks
Joining / Merging PDF with other Document TypesAll document typesPDF only PDF only PDF only
OCR API for Seamless Integration with TesseractAll document types
Integrated Checkpoint / Restart Feature (PDF)
PDF Optional Content
PDF Embedded FilesLimited
PDF Redactions
PDF AnnotationsFullLimited
PDF Form FieldsCreate, read, updateLimited, no creation
PDF Page Labels
Support Font Sub-Setting
+ +
diff --git a/docs/about-performance.rst b/docs/about-performance.rst new file mode 100644 index 0000000..3f8171d --- /dev/null +++ b/docs/about-performance.rst @@ -0,0 +1,328 @@ +.. raw:: html + + + +
+
+
Copying

This refers to opening a document and then saving it to a new file. This test measures the speed of reading a PDF and re-writing as a new PDF. This process is also at the core of functions like merging / joining multiple documents. The numbers below therefore apply to PDF joining and merging.

+ +

The results for all 7,031 pages are:

+
+
+ + +
+ +
+ +
+
600
+
500
+
400
+
300
+
200
+
100

seconds
+
+ +
+
+
+
+
+
+
+
+ +
3.05
+
10.54
+
33.57
+
494.04
+ +
+ +
+
PyMuPDF
+
PDFrw
+
PikePDF
+
PyPDF2
+
+ +
+
fastest
+
+
+
slowest
+
+ +
+ +
+
+
Text Extraction

This refers to extracting simple, plain text from every page of the document and storing it in a text file.

+ +

The results for all 7,031 pages are:

+
+
+ +
+ +
+ +
+ +
400
+
300
+
200
+
100

seconds
+
+ +
+ +
+
+
+
+
+ +
8.01
+
27.42
+
101.64
+
227.27
+ +
+ +
+
PyMuPDF
+
XPDF
+
PyPDF2
+
PDFMiner
+
+ +
+
fastest
+
+
+
slowest
+
+ +
+ + +
+ +
+
Rendering

This refers to making an image (like PNG) from every page of a document at a given DPI resolution. This feature is the basis for displaying a document in a GUI window.

+ +

The results for all 7,031 pages are:

+ +
+
+ + +
+ +
+ +
+
1000
+
800
+
600
+
400
+
200

seconds
+
+ +
+
+
+
+
+
+
+ +
367.04
+
646
+
851.52
+ +
+ +
+
PyMuPDF
+
XPDF
+
PDF2JPG
+
+ +
+
fastest
+
+
slowest
+
+ +
+ + +
+ + + diff --git a/docs/about.rst b/docs/about.rst new file mode 100644 index 0000000..14fe690 --- /dev/null +++ b/docs/about.rst @@ -0,0 +1,66 @@ +.. include:: header.rst + +.. _About: + + + +.. _About_Features: + +Features Comparison +----------------------------------------------- + + +.. _About_Feature_Matrix: + +Feature matrix +~~~~~~~~~~~~~~~~~~~ + +The following table illustrates how :title:`PyMuPDF` compares with other typical solutions. + + +.. include:: about-feature-matrix.rst + + +.. _About_Performance: + +Performance +----------------------------------------------- + + + +To benchmark :title:`PyMuPDF` performance against a range of tasks a test suite with a fixed set of :ref:`8 PDFs with a total of 7,031 pages` containing text & images is used to obtain performance timings. + + +Here are current results, grouped by task: + + + +.. include:: about-performance.rst + + +.. note:: + + For more detail regarding the methodology for these performance timings see: :ref:`Performance Comparison Methodology`. + +.. _About_License: + +License and Copyright +---------------------- + + + +:title:`PyMuPDF` and :title:`MuPDF` are now available under both, open-source :title:`AGPL` and commercial license agreements. Please read the full text of the :title:`AGPL` license agreement, available in the distribution material (file COPYING) and `here `_, to ensure that your use case complies with the guidelines of the license. If you determine you cannot meet the requirements of the :title:`AGPL`, please contact `Artifex `_ for more information regarding a commercial license. + +.. raw:: html + + +

+ +:title:`Artifex` is the exclusive commercial licensing agent for :title:`MuPDF`. + +:title:`Artifex`, the :title:`Artifex` logo, :title:`MuPDF`, and the :title:`MuPDF` logo are registered trademarks of :title:`Artifex Software Inc.` + + +.. include:: version.rst + +.. include:: footer.rst diff --git a/docs/algebra.rst b/docs/algebra.rst new file mode 100644 index 0000000..e7e0139 --- /dev/null +++ b/docs/algebra.rst @@ -0,0 +1,209 @@ +.. include:: header.rst + +.. _Algebra: + +Operator Algebra for Geometry Objects +====================================== + +.. highlight:: python + +Instances of classes :ref:`Point`, :ref:`IRect`, :ref:`Rect`, :ref:`Quad` and :ref:`Matrix` are collectively also called "geometry" objects. + +They all are special cases of Python sequences, see :ref:`SequenceTypes` for more background. + +We have defined operators for these classes that allow dealing with them (almost) like ordinary numbers in terms of addition, subtraction, multiplication, division, and some others. + +This chapter is a synopsis of what is possible. + +General Remarks +----------------- +1. Operators can be either **binary** (i.e. involving two objects) or **unary**. + +2. The resulting type of **binary** operations is either a **new object of the left operand's class** or a bool. + +3. The result of **unary** operations is either a **new object** of the same class, a bool or a float. + +4. The binary operators *+, -, *, /* are defined for all classes. They *roughly* do what you would expect -- **except, that the second operand ...** + + - may always be a number which then performs the operation on every component of the first one, + - may always be a numeric sequence of the same length (2, 4 or 6) -- we call such sequences :data:`point_like`, :data:`rect_like`, :data:`quad_like` or :data:`matrix_like`, respectively. + +5. Rectangles support additional binary operations: **intersection** (operator *"&"*), **union** (operator *"|"*) and **containment** checking. + +6. Binary operators fully support in-place operations, so expressions like `a /= b` are valid if b is numeric or "a_like". + + +Unary Operations +------------------ + +=========== =================================================================== +Oper. Result +=========== =================================================================== + bool(OBJ) is false exactly if all components of OBJ are zero + abs(OBJ) the rectangle area -- equal to norm(OBJ) for the other types + norm(OBJ) square root of the component squares (Euclidean norm) + +OBJ new copy of OBJ + -OBJ new copy of OBJ with negated components + ~m inverse of matrix "m", or the null matrix if not invertible +=========== =================================================================== + + +Binary Operations +------------------ +For every geometry object "a" and every number "b", the operations "a ° b" and "a °= b" are always defined for the operators *+, -, *, /*. The respective operation is simply executed for each component of "a". If the **second operand is not a number**, then the following is defined: + +========= ======================================================================= +Oper. Result +========= ======================================================================= +a+b, a-b component-wise execution, "b" must be "a-like". +a*m, a/m "a" can be a point, rectangle or matrix, but "m" must be + :data:`matrix_like`. *"a/m"* is treated as *"a*~m"* (see note below + for non-invertible matrices). If "a" is a **point** or a **rectangle**, + then *"a.transform(m)"* is executed. If "a" is a matrix, then + matrix concatenation takes place. +a&b **intersection rectangle:** "a" must be a rectangle and + "b" :data:`rect_like`. Delivers the **largest rectangle** + contained in both operands. +a|b **union rectangle:** "a" must be a rectangle, and "b" may be + :data:`point_like` or :data:`rect_like`. + Delivers the **smallest rectangle** containing both operands. +b in a if "b" is a number, then `b in tuple(a)` is returned. + If "b" is :data:`point_like`, :data:`rect_like` or :data:`quad_like`, + then "a" must be a rectangle, and `a.contains(b)` is returned. +a == b *True* if *bool(a-b)* is *False* ("b" may be "a-like"). +========= ======================================================================= + + +.. note:: Please note an important difference to usual arithmetic: + + Matrix multiplication is **not commutative**, i.e. in general we have `m*n != n*m` for two matrices. Also, there are non-zero matrices which have no inverse, for example `m = Matrix(1, 0, 1, 0, 1, 0)`. If you try to divide by any of these, you will receive a `ZeroDivisionError` exception using operator *"/"*, e.g. for the expression `fitz.Identity / m`. But if you formulate `fitz.Identity * ~m`, the result will be `fitz.Matrix()` (the null matrix). + + Admittedly, this represents an inconsistency, and we are considering to remove it. For the time being, you can choose to avoid an exception and check whether ~m is the null matrix, or accept a potential *ZeroDivisionError* by using `fitz.Identity / m`. + +.. note:: + + * With these conventions, all the usual algebra rules apply. For example, arbitrarily using brackets **(among objects of the same class!)** is possible: if r1, r2 are rectangles and m1, m2 are matrices, you can do this `(r1 + r2) * m1 * m2`. + * For all objects of the same class, `a + b + c == (a + b) + c == a + (b + c)` is true. + * For matrices in addition the following is true: `(m1 + m2) * m3 == m1 * m3 + m2 * m3` (distributivity property). + * **But the sequence of applying matrices is important:** If r is a rectangle and m1, m2 are matrices, then -- **caution!:** + - `r * m1 * m2 == (r * m1) * m2 != r * (m1 * m2)` + +Some Examples +-------------- + +Manipulation with numbers +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +For the usual arithmetic operations, numbers are always allowed as second operand. In addition, you can formulate `"x in OBJ"`, where x is a number. It is implemented as `"x in tuple(OBJ)"`:: + + >>> fitz.Rect(1, 2, 3, 4) + 5 + fitz.Rect(6.0, 7.0, 8.0, 9.0) + >>> 3 in fitz.Rect(1, 2, 3, 4) + True + >>> + +The following will create the upper left quarter of a document page rectangle:: + + >>> page.rect + Rect(0.0, 0.0, 595.0, 842.0) + >>> page.rect / 2 + Rect(0.0, 0.0, 297.5, 421.0) + >>> + +The following will deliver the **middle point of a line** that connects two points **p1** and **p2**:: + + >>> p1 = fitz.Point(1, 2) + >>> p2 = fitz.Point(4711, 3141) + >>> mp = (p1 + p2) / 2 + >>> mp + Point(2356.0, 1571.5) + >>> + +Manipulation with "like" Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The second operand of a binary operation can always be "like" the left operand. "Like" in this context means "a sequence of numbers of the same length". With the above examples:: + + >>> p1 + p2 + Point(4712.0, 3143.0) + >>> p1 + (4711, 3141) + Point(4712.0, 3143.0) + >>> p1 += (4711, 3141) + >>> p1 + Point(4712.0, 3143.0) + >>> + +To shift a rectangle for 5 pixels to the right, do this:: + + >>> fitz.Rect(100, 100, 200, 200) + (5, 0, 5, 0) # add 5 to the x coordinates + Rect(105.0, 100.0, 205.0, 200.0) + >>> + +Points, rectangles and matrices can be *transformed* with matrices. In PyMuPDF, we treat this like a **"multiplication"** (or resp. **"division"**), where the second operand may be "like" a matrix. Division in this context means "multiplication with the inverted matrix":: + + >>> m = fitz.Matrix(1, 2, 3, 4, 5, 6) + >>> n = fitz.Matrix(6, 5, 4, 3, 2, 1) + >>> p = fitz.Point(1, 2) + >>> p * m + Point(12.0, 16.0) + >>> p * (1, 2, 3, 4, 5, 6) + Point(12.0, 16.0) + >>> p / m + Point(2.0, -2.0) + >>> p / (1, 2, 3, 4, 5, 6) + Point(2.0, -2.0) + >>> + >>> m * n # matrix multiplication + Matrix(14.0, 11.0, 34.0, 27.0, 56.0, 44.0) + >>> m / n # matrix division + Matrix(2.5, -3.5, 3.5, -4.5, 5.5, -7.5) + >>> + >>> m / m # result is equal to the Identity matrix + Matrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0) + >>> + >>> # look at this non-invertible matrix: + >>> m = fitz.Matrix(1, 0, 1, 0, 1, 0) + >>> ~m + Matrix(0.0, 0.0, 0.0, 0.0, 0.0, 0.0) + >>> # we try dividing by it in two ways: + >>> p = fitz.Point(1, 2) + >>> p * ~m # this delivers point (0, 0): + Point(0.0, 0.0) + >>> p / m # but this is an exception: + Traceback (most recent call last): + File "", line 1, in + p / m + File "... /site-packages/fitz/fitz.py", line 869, in __truediv__ + raise ZeroDivisionError("matrix not invertible") + ZeroDivisionError: matrix not invertible + >>> + + +As a specialty, rectangles support additional binary operations: + +* **intersection** -- the common area of rectangle-likes, operator *"&"* +* **inclusion** -- enlarge to include a point-like or rect-like, operator *"|"* +* **containment** check -- whether a point-like or rect-like is inside + +Here is an example for creating the smallest rectangle enclosing given points:: + + >>> # first define some point-likes + >>> points = [] + >>> for i in range(10): + for j in range(10): + points.append((i, j)) + >>> + >>> # now create a rectangle containing all these 100 points + >>> # start with an empty rectangle + >>> r = fitz.Rect(points[0], points[0]) + >>> for p in points[1:]: # and include remaining points one by one + r |= p + >>> r # here is the to be expected result: + Rect(0.0, 0.0, 9.0, 9.0) + >>> (4, 5) in r # this point-like lies inside the rectangle + True + >>> # and this rect-like is also inside + >>> (4, 4, 5, 5) in r + True + >>> + +.. include:: footer.rst \ No newline at end of file diff --git a/docs/annot.rst b/docs/annot.rst new file mode 100644 index 0000000..edcd518 --- /dev/null +++ b/docs/annot.rst @@ -0,0 +1,585 @@ +.. include:: header.rst + +.. _Annot: + +================ +Annot +================ +**This class is supported for PDF documents only.** + +Quote from the :ref:`AdobeManual`: "An annotation associates an object such as a note, sound, or movie with a location on a page of a PDF document, or provides a way to interact with the user by means of the mouse and keyboard." + +There is a parent-child relationship between an annotation and its page. If the page object becomes unusable (closed document, any document structure change, etc.), then so does every of its existing annotation objects -- an exception is raised saying that the object is "orphaned", whenever an annotation property or method is accessed. + +================================== ============================================================== +**Attribute** **Short Description** +================================== ============================================================== +:meth:`Annot.delete_responses` delete all responding annotions +:meth:`Annot.get_file` get attached file content +:meth:`Annot.get_oc` get :data:`xref` of an :data:`OCG` / :data:`OCMD` +:meth:`Annot.get_pixmap` image of the annotation as a pixmap +:meth:`Annot.get_sound` get the sound of an audio annotation +:meth:`Annot.get_text` extract annotation text +:meth:`Annot.get_textbox` extract annotation text +:meth:`Annot.set_border` set annotation's border properties +:meth:`Annot.set_blendmode` set annotation's blend mode +:meth:`Annot.set_colors` set annotation's colors +:meth:`Annot.set_flags` set annotation's flags field +:meth:`Annot.set_irt_xref` define the annotation to being "In Response To" +:meth:`Annot.set_name` set annotation's name field +:meth:`Annot.set_oc` set :data:`xref` to an :data:`OCG` / :data:`OCMD` +:meth:`Annot.set_opacity` change transparency +:meth:`Annot.set_open` open / close annotation or its Popup +:meth:`Annot.set_popup` create a Popup for the annotation +:meth:`Annot.set_rect` change annotation rectangle +:meth:`Annot.set_rotation` change rotation +:meth:`Annot.update_file` update attached file content +:meth:`Annot.update` apply accumulated annot changes +:attr:`Annot.blendmode` annotation BlendMode +:attr:`Annot.border` border details +:attr:`Annot.colors` border / background and fill colors +:attr:`Annot.file_info` get attached file information +:attr:`Annot.flags` annotation flags +:attr:`Annot.has_popup` whether annotation has a Popup +:attr:`Annot.irt_xref` annotation to which this one responds +:attr:`Annot.info` various information +:attr:`Annot.is_open` whether annotation or its Popup is open +:attr:`Annot.line_ends` start / end appearance of line-type annotations +:attr:`Annot.next` link to the next annotation +:attr:`Annot.opacity` the annot's transparency +:attr:`Annot.parent` page object of the annotation +:attr:`Annot.popup_rect` rectangle of the annotation's Popup +:attr:`Annot.popup_xref` the PDF :data:`xref` number of the annotation's Popup +:attr:`Annot.rect` rectangle containing the annotation +:attr:`Annot.type` type of the annotation +:attr:`Annot.vertices` point coordinates of Polygons, PolyLines, etc. +:attr:`Annot.xref` the PDF :data:`xref` number +================================== ============================================================== + +**Class API** + +.. class:: Annot + + .. index:: + pair: matrix; Annot.get_pixmap + pair: colorspace; Annot.get_pixmap + pair: alpha; Annot.get_pixmap + pair: dpi; Annot.get_pixmap + + .. method:: get_pixmap(matrix=fitz.Identity, dpi=None, colorspace=fitz.csRGB, alpha=False) + + * Changed in v1.19.2: added support of dpi parameter. + + Creates a pixmap from the annotation as it appears on the page in untransformed coordinates. The pixmap's :ref:`IRect` equals *Annot.rect.irect* (see below). **All parameters are keyword only.** + + :arg matrix_like matrix: a matrix to be used for image creation. Default is :ref:`Identity`. + + :arg int dpi: (new in v1.19.2) desired resolution in dots per inch. If not `None`, the matrix parameter is ignored. + + :arg colorspace: a colorspace to be used for image creation. Default is *fitz.csRGB*. + :type colorspace: :ref:`Colorspace` + + :arg bool alpha: whether to include transparency information. Default is *False*. + + :rtype: :ref:`Pixmap` + + .. note:: If the annotation has just been created or modified, you should reload the page first via *page = doc.reload_page(page)*. + + + .. index:: + pair: blocks; Annot.get_text + pair: dict; Annot.get_text + pair: clip; Annot.get_text + pair: flags; Annot.get_text + pair: html; Annot.get_text + pair: json; Annot.get_text + pair: rawdict; Annot.get_text + pair: text; Annot.get_text + pair: words; Annot.get_text + pair: xhtml; Annot.get_text + pair: xml; Annot.get_text + + .. method:: get_text(opt, clip=None, flags=None) + + * New in 1.18.0 + + Retrieves the content of the annotation in a variety of formats -- much like the same method for :ref:`Page`.. This currently only delivers relevant data for annotation types 'FreeText' and 'Stamp'. Other types return an empty string (or equivalent objects). + + :arg str opt: (positional only) the desired format - one of the following values. Please note that this method works exactly like the same-named method of :ref:`Page`. + + * "text" -- :meth:`TextPage.extractTEXT`, default + * "blocks" -- :meth:`TextPage.extractBLOCKS` + * "words" -- :meth:`TextPage.extractWORDS` + * "html" -- :meth:`TextPage.extractHTML` + * "xhtml" -- :meth:`TextPage.extractXHTML` + * "xml" -- :meth:`TextPage.extractXML` + * "dict" -- :meth:`TextPage.extractDICT` + * "json" -- :meth:`TextPage.extractJSON` + * "rawdict" -- :meth:`TextPage.extractRAWDICT` + + :arg rect-like clip: (keyword only) restrict the extraction to this area. Should hardly ever be required, defaults to :attr:`Annot.rect`. + :arg int flags: (keyword only) control the amount of data returned. Defaults to simple text extraction. + + .. method:: get_textbox(rect) + + * New in 1.18.0 + + Return the annotation text. Mostly (except line breaks) equal to :meth:`Annot.get_text` with the "text" option. + + :arg rect-like rect: the area to consider, defaults to :attr:`Annot.rect`. + + + .. method:: set_info(info=None, content=None, title=None, creationDate=None, modDate=None, subject=None) + + * Changed in version 1.16.10 + + Changes annotation properties. These include dates, contents, subject and author (title). Changes for *name* and *id* will be ignored. The update happens selectively: To leave a property unchanged, set it to *None*. To delete existing data, use an empty string. + + :arg dict info: a dictionary compatible with the *info* property (see below). All entries must be strings. If this argument is not a dictionary, the other arguments are used instead -- else they are ignored. + :arg str content: *(new in v1.16.10)* see description in :attr:`info`. + :arg str title: *(new in v1.16.10)* see description in :attr:`info`. + :arg str creationDate: *(new in v1.16.10)* date of annot creation. If given, should be in PDF datetime format. + :arg str modDate: *(new in v1.16.10)* date of last modification. If given, should be in PDF datetime format. + :arg str subject: *(new in v1.16.10)* see description in :attr:`info`. + + .. method:: set_line_ends(start, end) + + Sets an annotation's line ending styles. Each of these annotation types is defined by a list of points which are connected by lines. The symbol identified by *start* is attached to the first point, and *end* to the last point of this list. For unsupported annotation types, a no-operation with a warning message results. + + .. note:: + + * While 'FreeText', 'Line', 'PolyLine', and 'Polygon' annotations can have these properties, (Py-) MuPDF does not support line ends for 'FreeText', because the call-out variant of it is not supported. + * *(Changed in v1.16.16)* Some symbols have an interior area (diamonds, circles, squares, etc.). By default, these areas are filled with the fill color of the annotation. If this is *None*, then white is chosen. The *fill_color* argument of :meth:`Annot.update` can now be used to override this and give line end symbols their own fill color. + + :arg int start: The symbol number for the first point. + :arg int end: The symbol number for the last point. + + .. method:: set_oc(xref) + + Set the annotation's visibility using PDF optional content mechanisms. This visibility is controlled by the user interface of supporting PDF viewers. It is independent from other attributes like :attr:`Annot.flags`. + + :arg int xref: the :data:`xref` of an optional contents group (OCG or OCMD). Any previous xref will be overwritten. If zero, a previous entry will be removed. An exception occurs if the xref is not zero and does not point to a valid PDF object. + + .. note:: This does **not require executing** :meth:`Annot.update` to take effect. + + .. method:: get_oc() + + Return the :data:`xref` of an optional content object, or zero if there is none. + + :returns: zero or the xref of an OCG (or OCMD). + + + .. method:: set_irt_xref(xref) + + * New in v1.19.3 + + Set annotation to be "In Response To" another one. + + :arg int xref: The :data:`xref` of another annotation. + + .. note:: Must refer to an existing annotation on this page. Setting this property requires no subsequent `update()`. + + + .. method:: set_open(value) + + * New in v1.18.4 + + Set the annotation's Popup annotation to open or closed -- **or** the annotation itself, if its type is 'Text' ("sticky note"). + + :arg bool value: the desired open state. + + + .. method:: set_popup(rect) + + * New in v1.18.4 + + Create a Popup annotation for the annotation and specify its rectangle. If the Popup already exists, only its rectangle is updated. + + :arg rect_like rect: the desired rectangle. + + + + .. method:: set_opacity(value) + + Set the annotation's transparency. Opacity can also be set in :meth:`Annot.update`. + + :arg float value: a float in range *[0, 1]*. Any value outside is assumed to be 1. E.g. a value of 0.5 sets the transparency to 50%. + + Three overlapping 'Circle' annotations with each opacity set to 0.5: + + .. image:: images/img-opacity.* + + .. attribute:: blendmode + + * New in v1.18.4 + + The annotation's blend mode. See :ref:`AdobeManual`, page 324 for explanations. + + :rtype: str + :returns: the blend mode or *None*. + + + .. method:: set_blendmode(blendmode) + + * New in v1.16.14 + + Set the annotation's blend mode. See :ref:`AdobeManual`, page 324 for explanations. The blend mode can also be set in :meth:`Annot.update`. + + :arg str blendmode: set the blend mode. Use :meth:`Annot.update` to reflect this in the visual appearance. For predefined values see :ref:`BlendModes`. Use `PDF_BM_Normal` to **remove** a blend mode. + + + .. method:: set_name(name) + + * New in version 1.16.0 + + Change the name field of any annotation type. For 'FileAttachment' and 'Text' annotations, this is the icon name, for 'Stamp' annotations the text in the stamp. The visual result (if any) depends on your PDF viewer. See also :ref:`mupdficons`. + + :arg str name: the new name. + + .. caution:: If you set the name of a 'Stamp' annotation, then this will **not change** the rectangle, nor will the text be layouted in any way. If you choose a standard text from :ref:`StampIcons` (the **exact** name piece after `"STAMP_"`), you should receive the original layout. An **arbitrary text** will not be changed to upper case, but be written in font "Times-Bold" as is, horizontally centered in **one line** and be shortened to fit. To get your text fully displayed, its length using fontsize 20 must not exceed 190 pixels. So please make sure that the following inequality is true: `fitz.get_text_length(text, fontname="tibo", fontsize=20) <= 190`. + + .. method:: set_rect(rect) + + Change the rectangle of an annotation. The annotation can be moved around and both sides of the rectangle can be independently scaled. However, the annotation appearance will never get rotated, flipped or sheared. + + :arg rect_like rect: the new rectangle of the annotation (finite and not empty). E.g. using a value of *annot.rect + (5, 5, 5, 5)* will shift the annot position 5 pixels to the right and downwards. + + .. note:: You **need not** invoke :meth:`Annot.update` for activation of the effect. + + + .. method:: set_rotation(angle) + + Set the rotation of an annotation. This rotates the annotation rectangle around its center point. Then a **new annotation rectangle** is calculated from the resulting quad. + + :arg int angle: rotation angle in degrees. Arbitrary values are possible, but will be clamped to the interval `[0, 360)`. + + .. note:: + * You **must invoke** :meth:`Annot.update` to activate the effect. + * For PDF_ANNOT_FREE_TEXT, only one of the values 0, 90, 180 and 270 is possible and will **rotate the text** inside the current rectangle (which remains unchanged). Other values are silently ignored and replaced by 0. + * Otherwise, only the following :ref:`AnnotationTypes` can be rotated: 'Square', 'Circle', 'Caret', 'Text', 'FileAttachment', 'Ink', 'Line', 'Polyline', 'Polygon', and 'Stamp'. For all others the method is a no-op. + + + .. method:: set_border(border=None, width=None, style=None, dashes=None, clouds=None) + + * Changed in version 1.16.9: Allow specification without using a dictionary. The direct parameters are used if *border* is not a dictionary. + + * Changed in version 1.22.4: Support of the "cloudy" border effect. + + PDF only: Change border width, dashing, style and cloud effect. See the :attr:`Annot.border` attribute for more details. + + + :arg dict border: a dictionary as returned by the :attr:`border` property, with keys *"width"* (*float*), *"style"* (*str*), *"dashes"* (*sequence*) and *clouds* (*int*). Omitted keys will leave the resp. property unchanged. Set the border argument to `None` (the default) to use the other arguments. + + :arg float width: A non-negative value will change the border line width. + :arg str style: A value other than `None` will change this border property. + :arg sequence dashes: All items of the sequence must be integers, otherwise the parameter is ignored. To remove dashing use: `dashes=[]`. If dashes is a non-empty sequence, "style" will automatically be set to "D" (dashed). + :arg int clouds: A value >= 0 will change this property. Use `clouds=0` to remove the cloudy appearance completely. + + .. method:: set_flags(flags) + + Changes the annotation flags. Use the `|` operator to combine several. + + :arg int flags: an integer specifying the required flags. + + .. method:: set_colors(colors=None, stroke=None, fill=None) + + * Changed in version 1.16.9: Allow colors to be directly set. These parameters are used if *colors* is not a dictionary. + + Changes the "stroke" and "fill" colors for supported annotation types -- not all annotations accept both. + + :arg dict colors: a dictionary containing color specifications. For accepted dictionary keys and values see below. The most practical way should be to first make a copy of the *colors* property and then modify this dictionary as required. + :arg sequence stroke: see above. + :arg sequence fill: see above. + + *Changed in v1.18.5:* To completely remove a color specification, use an empty sequence like `[]`. If you specify `None`, an existing specification will not be changed. + + + .. method:: delete_responses() + + * New in version 1.16.12 + + Delete annotations referring to this one. This includes any 'Popup' annotations and all annotations responding to it. + + + .. index:: + pair: blend_mode; Annot.update + pair: fontsize; Annot.update + pair: text_color; Annot.update + pair: border_color; Annot.update + pair: fill_color; Annot.update + pair: cross_out; Annot.update + pair: rotate; Annot.update + + .. method:: update(opacity=None, blend_mode=None, fontsize=0, text_color=None, border_color=None, fill_color=None, cross_out=True, rotate=-1) + + Synchronize the appearance of an annotation with its properties after relevant changes. + + You can safely **omit** this method **only** for the following changes: + + * :meth:`Annot.set_rect` + * :meth:`Annot.set_flags` + * :meth:`Annot.set_oc` + * :meth:`Annot.update_file` + * :meth:`Annot.set_info` (except any changes to *"content"*) + + All arguments are optional. *(Changed in v1.16.14)* Blend mode and opacity are applicable to **all annotation types**. The other arguments are mostly special use, as described below. + + Color specifications may be made in the usual format used in PuMuPDF as sequences of floats ranging from 0.0 to 1.0 (including both). The sequence length must be 1, 3 or 4 (supporting GRAY, RGB and CMYK colorspaces respectively). For GRAY, just a float is also acceptable. + + :arg float opacity: *(new in v1.16.14)* **valid for all annotation types:** change or set the annotation's transparency. Valid values are *0 <= opacity < 1*. + :arg str blend_mode: *(new in v1.16.14)* **valid for all annotation types:** change or set the annotation's blend mode. For valid values see :ref:`BlendModes`. + :arg float fontsize: change font size of the text. 'FreeText' annotations only. + :arg sequence,float text_color: change the text color. 'FreeText' annotations only. + :arg sequence,float border_color: change the border color. 'FreeText' annotations only. + :arg sequence,float fill_color: the fill color. + + * 'Line', 'Polyline', 'Polygon' annotations: use it to give applicable line end symbols a fill color other than that of the annotation *(changed in v1.16.16)*. + + :arg bool cross_out: *(new in v1.17.2)* add two diagonal lines to the annotation rectangle. 'Redact' annotations only. If not desired, *False* must be specified even if the annotation was created with *False*. + :arg int rotate: new rotation value. Default (-1) means no change. Supports 'FreeText' and several other annotation types (see :meth:`Annot.set_rotation`), [#f1]_. Only choose 0, 90, 180, or 270 degrees for 'FreeText'. Otherwise any integer is acceptable. + + :rtype: bool + + .. note:: Using this method inside a :meth:`Page.annots` loop is **not recommended!** This is because most annotation updates require the owning page to be reloaded -- which cannot be done inside this loop. Please use the example coding pattern given in the documentation of this generator. + + + .. attribute:: file_info + + Basic information of the annot's attached file. + + :rtype: dict + :returns: a dictionary with keys *filename*, *ufilename*, *desc* (description), *size* (uncompressed file size), *length* (compressed length) for FileAttachment annot types, else *None*. + + .. method:: get_file() + + Returns attached file content. + + :rtype: bytes + :returns: the content of the attached file. + + .. index:: + pair: buffer; Annot.update_file + pair: filename; Annot.update_file + pair: ufilename; Annot.update_file + pair: desc; Annot.update_file + + .. method:: update_file(buffer=None, filename=None, ufilename=None, desc=None) + + Updates the content of an attached file. All arguments are optional. No arguments lead to a no-op. + + :arg bytes|bytearray|BytesIO buffer: the new file content. Omit to only change meta-information. + + *(Changed in version 1.14.13)* *io.BytesIO* is now also supported. + + :arg str filename: new filename to associate with the file. + + :arg str ufilename: new unicode filename to associate with the file. + + :arg str desc: new description of the file content. + + .. method:: get_sound() + + Return the embedded sound of an audio annotation. + + :rtype: dict + :returns: the sound audio file and accompanying properties. These are the possible dictionary keys, of which only "rate" and "stream" are always present. + + =========== ======================================================= + Key Description + =========== ======================================================= + rate (float, requ.) samples per second + channels (int, opt.) number of sound channels + bps (int, opt.) bits per sample value per channel + encoding (str, opt.) encoding format: Raw, Signed, muLaw, ALaw + compression (str, opt.) name of compression filter + stream (bytes, requ.) the sound file content + =========== ======================================================= + + + .. attribute:: opacity + + The annotation's transparency. If set, it is a value in range *[0, 1]*. The PDF default is 1. However, in an effort to tell the difference, we return *-1.0* if not set. + + :rtype: float + + .. attribute:: parent + + The owning page object of the annotation. + + :rtype: :ref:`Page` + + .. attribute:: rotation + + The annot rotation. + + :rtype: int + :returns: a value [-1, 359]. If rotation is not at all, -1 is returned (and implies a rotation angle of 0). Other possible values are normalized to some value value 0 <= angle < 360. + + .. attribute:: rect + + The rectangle containing the annotation. + + :rtype: :ref:`Rect` + + .. attribute:: next + + The next annotation on this page or None. + + :rtype: *Annot* + + .. attribute:: type + + A number and one or two strings describing the annotation type, like **[2, 'FreeText', 'FreeTextCallout']**. The second string entry is optional and may be empty. See the appendix :ref:`AnnotationTypes` for a list of possible values and their meanings. + + :rtype: list + + .. attribute:: info + + A dictionary containing various information. All fields are optional strings. If information is not provided, an empty string is returned. + + * *name* -- e.g. for 'Stamp' annotations it will contain the stamp text like "Sold" or "Experimental", for other annot types you will see the name of the annot's icon here ("PushPin" for FileAttachment). + + * *content* -- a string containing the text for type *Text* and *FreeText* annotations. Commonly used for filling the text field of annotation pop-up windows. + + * *title* -- a string containing the title of the annotation pop-up window. By convention, this is used for the **annotation author**. + + * *creationDate* -- creation timestamp. + * *modDate* -- last modified timestamp. + * *subject* -- subject. + * *id* -- *(new in version 1.16.10)* a unique identification of the annotation. This is taken from PDF key */NM*. Annotations added by PyMuPDF will have a unique name, which appears here. + + :rtype: dict + + + .. attribute:: flags + + An integer whose low order bits contain flags for how the annotation should be presented. + + :rtype: int + + .. attribute:: line_ends + + A pair of integers specifying start and end symbol of annotations types 'FreeText', 'Line', 'PolyLine', and 'Polygon'. *None* if not applicable. For possible values and descriptions in this list, see the :ref:`AdobeManual`, table 1.76 on page 400. + + :rtype: tuple + + .. attribute:: vertices + + A list containing a variable number of point ("vertices") coordinates (each given by a pair of floats) for various types of annotations: + + * 'Line' -- the starting and ending coordinates (2 float pairs). + * 'FreeText' -- 2 or 3 float pairs designating the starting, the (optional) knee point, and the ending coordinates. + * 'PolyLine' / 'Polygon' -- the coordinates of the edges connected by line pieces (n float pairs for n points). + * text markup annotations -- 4 float pairs specifying the *QuadPoints* of the marked text span (see :ref:`AdobeManual`, page 403). + * 'Ink' -- list of one to many sublists of vertex coordinates. Each such sublist represents a separate line in the drawing. + + :rtype: list + + + .. attribute:: colors + + dictionary of two lists of floats in range *0 <= float <= 1* specifying the "stroke" and the interior ("fill") colors. The stroke color is used for borders and everything that is actively painted or written ("stroked"). The fill color is used for the interior of objects like line ends, circles and squares. The lengths of these lists implicitly determine the colorspaces used: 1 = GRAY, 3 = RGB, 4 = CMYK. So "[1.0, 0.0, 0.0]" stands for RGB color red. Both lists can be empty if no color is specified. + + :rtype: dict + + .. attribute:: xref + + The PDF :data:`xref`. + + :rtype: int + + .. attribute:: irt_xref + + The PDF :data:`xref` of an annotation to which this one responds. Return zero if this is no response annotation. + + :rtype: int + + .. attribute:: popup_xref + + The PDF :data:`xref` of the associated Popup annotation. Zero if non-existent. + + :rtype: int + + .. attribute:: has_popup + + Whether the annotation has a Popup annotation. + + :rtype: bool + + .. attribute:: is_open + + Whether the annotation's Popup is open -- **or** the annotation itself ('Text' annotations only). + + :rtype: bool + + .. attribute:: popup_rect + + The rectangle of the associated Popup annotation. Infinite rectangle if non-existent. + + :rtype: :ref:`Rect` + + .. attribute:: rect_delta + + A tuple of four floats representing the `/RD` entry of the annotation. The four numbers describe the numerical differences (left, top, -right, -bottom) between two rectangles: the :attr:`rect` of the annotation and a rectangle contained within that rectangle. If the entry is missing, this property is `(0, 0, 0, 0)`. If the annotation border is a normal, straight line, these numbers are typically border width divided by 2. If the annotation has a "cloudy" border, you will see the breadth of the cloud semi-circles here. In general, the numbers need not be identical. To compute the inner rectangle do `a.rect + a.rect_delta`. + + .. attribute:: border + + A dictionary containing border characteristics. Empty if no border information exists. The following keys may be present: + + * *width* -- a float indicating the border thickness in points. The value is -1.0 if no width is specified. + + * *dashes* -- a sequence of integers specifying a line dashing pattern. *[]* means no dashes, *[n]* means equal on-off lengths of *n* points, longer lists will be interpreted as specifying alternating on-off length values. See the :ref:`AdobeManual` page 126 for more details. + + * *style* -- 1-byte border style: **"S"** (Solid) = solid line surrounding the annotation, **"D"** (Dashed) = dashed line surrounding the annotation, the dash pattern is specified by the *dashes* entry, **"B"** (Beveled) = a simulated embossed rectangle that appears to be raised above the surface of the page, **"I"** (Inset) = a simulated engraved rectangle that appears to be recessed below the surface of the page, **"U"** (Underline) = a single line along the bottom of the annotation rectangle. + + * *clouds* -- an integer indicating a "cloudy" border, where `n` is an integer `-1 <= n <= 2`. A value `n = 0` indicates a straight line (no clouds), 1 means small and 2 means large semi-circles, mimicking the cloudy appearance. If -1, then no specification is present. + + :rtype: dict + + +.. _mupdficons: + +Annotation Icons in MuPDF +------------------------- +This is a list of icons referenceable by name for annotation types 'Text' and 'FileAttachment'. You can use them via the *icon* parameter when adding an annotation, or use the as argument in :meth:`Annot.set_name`. It is left to your discretion which item to choose when -- no mechanism will keep you from using e.g. the "Speaker" icon for a 'FileAttachment'. + +.. image:: images/mupdf-icons.* + + +Example +-------- +Change the graphical image of an annotation. Also update the "author" and the text to be shown in the popup window:: + + doc = fitz.open("circle-in.pdf") + page = doc[0] # page 0 + annot = page.first_annot # get the annotation + annot.set_border(dashes=[3]) # set dashes to "3 on, 3 off ..." + + # set stroke and fill color to some blue + annot.set_colors({"stroke":(0, 0, 1), "fill":(0.75, 0.8, 0.95)}) + info = annot.info # get info dict + info["title"] = "Jorj X. McKie" # set author + + # text in popup window ... + info["content"] = "I changed border and colors and enlarged the image by 20%." + info["subject"] = "Demonstration of PyMuPDF" # some PDF viewers also show this + annot.set_info(info) # update info dict + r = annot.rect # take annot rect + r.x1 = r.x0 + r.width * 1.2 # new location has same top-left + r.y1 = r.y0 + r.height * 1.2 # but 20% longer sides + annot.set_rect(r) # update rectangle + annot.update() # update the annot's appearance + doc.save("circle-out.pdf") # save + +This is how the circle annotation looks like before and after the change (pop-up windows displayed using Nitro PDF viewer): + +|circle| + +.. |circle| image:: images/img-circle.* + + +.. rubric:: Footnotes + +.. [#f1] Rotating an annotation also changes its rectangle. Depending on how the annotation was defined, the original rectangle is **not reconstructible** by setting the rotation value to zero again and will be lost. + +.. include:: footer.rst diff --git a/docs/app1.rst b/docs/app1.rst new file mode 100644 index 0000000..e6ba768 --- /dev/null +++ b/docs/app1.rst @@ -0,0 +1,349 @@ +.. include:: header.rst + +.. _Appendix1: + +====================================== +Appendix 1: Details on Text Extraction +====================================== +This chapter provides background on the text extraction methods of PyMuPDF. + +Information of interest are + +* what do they provide? +* what do they imply (processing time / data sizes)? + +General structure of a TextPage +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:ref:`TextPage` is one of (Py-) MuPDF's classes. It is normally created (and destroyed again) behind the curtain, when :ref:`Page` text extraction methods are used, but it is also available directly and can be used as a persistent object. Other than its name suggests, images may optionally also be part of a text page:: + + + + + + + + + +A **text page** consists of blocks (= roughly paragraphs). + +A **block** consists of either lines and their characters, or an image. + +A **line** consists of spans. + +A **span** consists of adjacent characters with identical font properties: name, size, flags and color. + +Plain Text +~~~~~~~~~~ + +Function :meth:`TextPage.extractText` (or *Page.get_text("text")*) extracts a page's plain **text in original order** as specified by the creator of the document. + +An example output:: + + >>> print(page.get_text("text")) + Some text on first page. + +.. note:: The output may not equal an accustomed "natural" reading order. However, you can request a reordering following the scheme "top-left to bottom-right" by executing `page.get_text("text", sort=True)`. + + +BLOCKS +~~~~~~~~~~ + +Function :meth:`TextPage.extractBLOCKS` (or *Page.get_text("blocks")*) extracts a page's text blocks as a list of items like:: + + (x0, y0, x1, y1, "lines in block", block_no, block_type) + +Where the first 4 items are the float coordinates of the block's bbox. The lines within each block are concatenated by a new-line character. + +This is a high-speed method, which by default also extracts image meta information: Each image appears as a block with one text line, which contains meta information. The image itself is not shown. + +As with simple text output above, the `sort` argument can be used as well to obtain a reading order. + +Example output:: + + >>> print(page.get_text("blocks", sort=False)) + [(50.0, 88.17500305175781, 166.1709747314453, 103.28900146484375, + 'Some text on first page.', 0, 0)] + + +WORDS +~~~~~~~~~~ + +Function :meth:`TextPage.extractWORDS` (or *Page.get_text("words")*) extracts a page's text **words** as a list of items like:: + + (x0, y0, x1, y1, "word", block_no, line_no, word_no) + +Where the first 4 items are the float coordinates of the words's bbox. The last three integers provide some more information on the word's whereabouts. + +This is a high-speed method. As with the previous methods, argument `sort=True` will reorder the words. + +Example output:: + + >>> for word in page.get_text("words", sort=False): + print(word) + (50.0, 88.17500305175781, 78.73200225830078, 103.28900146484375, + 'Some', 0, 0, 0) + (81.79000091552734, 88.17500305175781, 99.5219955444336, 103.28900146484375, + 'text', 0, 0, 1) + (102.57999420166016, 88.17500305175781, 114.8119888305664, 103.28900146484375, + 'on', 0, 0, 2) + (117.86998748779297, 88.17500305175781, 135.5909881591797, 103.28900146484375, + 'first', 0, 0, 3) + (138.64898681640625, 88.17500305175781, 166.1709747314453, 103.28900146484375, + 'page.', 0, 0, 4) + +HTML +~~~~ + +:meth:`TextPage.extractHTML` (or *Page.get_text("html")* output fully reflects the structure of the page's *TextPage* -- much like DICT / JSON below. This includes images, font information and text positions. If wrapped in HTML header and trailer code, it can readily be displayed by an internet browser. Our above example:: + + >>> for line in page.get_text("html").splitlines(): + print(line) + +
+

Some text on first page.

+
+ + +.. _HTMLQuality: + +Controlling Quality of HTML Output +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +While HTML output has improved a lot in MuPDF v1.12.0, it is not yet bug-free: we have found problems in the areas **font support** and **image positioning**. + +* HTML text contains references to the fonts used of the original document. If these are not known to the browser (a fat chance!), it will replace them with others; the results will probably look awkward. This issue varies greatly by browser -- on my Windows machine, MS Edge worked just fine, whereas Firefox looked horrible. + +* For PDFs with a complex structure, images may not be positioned and / or sized correctly. This seems to be the case for rotated pages and pages, where the various possible page bbox variants do not coincide (e.g. *MediaBox != CropBox*). We do not know yet, how to address this -- we filed a bug at MuPDF's site. + +To address the font issue, you can use a simple utility script to scan through the HTML file and replace font references. Here is a little example that replaces all fonts with one of the :ref:`Base-14-Fonts`: serifed fonts will become "Times", non-serifed "Helvetica" and monospaced will become "Courier". Their respective variations for "bold", "italic", etc. are hopefully done correctly by your browser:: + + import sys + filename = sys.argv[1] + otext = open(filename).read() # original html text string + pos1 = 0 # search start poition + font_serif = "font-family:Times" # enter ... + font_sans = "font-family:Helvetica" # ... your choices ... + font_mono = "font-family:Courier" # ... here + found_one = False # true if search successful + + while True: + pos0 = otext.find("font-family:", pos1) # start of a font spec + if pos0 < 0: # none found - we are done + break + pos1 = otext.find(";", pos0) # end of font spec + test = otext[pos0 : pos1] # complete font spec string + testn = "" # the new font spec string + if test.endswith(",serif"): # font with serifs? + testn = font_serif # use Times instead + elif test.endswith(",sans-serif"): # sans serifs font? + testn = font_sans # use Helvetica + elif test.endswith(",monospace"): # monospaced font? + testn = font_mono # becomes Courier + + if testn != "": # any of the above found? + otext = otext.replace(test, testn) # change the source + found_one = True + pos1 = 0 # start over + + if found_one: + ofile = open(filename + ".html", "w") + ofile.write(otext) + ofile.close() + else: + print("Warning: could not find any font specs!") + + + +DICT (or JSON) +~~~~~~~~~~~~~~~~ + +:meth:`TextPage.extractDICT` (or *Page.get_text("dict", sort=False)*) output fully reflects the structure of a *TextPage* and provides image content and position detail (*bbox* -- boundary boxes in pixel units) for every block, line and span. Images are stored as *bytes* for DICT output and base64 encoded strings for JSON output. + +For a visualization of the dictionary structure have a look at :ref:`textpagedict`. + +Here is how this looks like:: + + { + "width": 300.0, + "height": 350.0, + "blocks": [{ + "type": 0, + "bbox": (50.0, 88.17500305175781, 166.1709747314453, 103.28900146484375), + "lines": ({ + "wmode": 0, + "dir": (1.0, 0.0), + "bbox": (50.0, 88.17500305175781, 166.1709747314453, 103.28900146484375), + "spans": ({ + "size": 11.0, + "flags": 0, + "font": "Helvetica", + "color": 0, + "origin": (50.0, 100.0), + "text": "Some text on first page.", + "bbox": (50.0, 88.17500305175781, 166.1709747314453, 103.28900146484375) + }) + }] + }] + } + +RAWDICT (or RAWJSON) +~~~~~~~~~~~~~~~~~~~~~ +:meth:`TextPage.extractRAWDICT` (or *Page.get_text("rawdict", sort=False)*) is an **information superset of DICT** and takes the detail level one step deeper. It looks exactly like the above, except that the *"text"* items (*string*) in the spans are replaced by the list *"chars"*. Each *"chars"* entry is a character *dict*. For example, here is what you would see in place of item *"text": "Text in black color."* above:: + + "chars": [{ + "origin": (50.0, 100.0), + "bbox": (50.0, 88.17500305175781, 57.336997985839844, 103.28900146484375), + "c": "S" + }, { + "origin": (57.33700180053711, 100.0), + "bbox": (57.33700180053711, 88.17500305175781, 63.4530029296875, 103.28900146484375), + "c": "o" + }, { + "origin": (63.4530029296875, 100.0), + "bbox": (63.4530029296875, 88.17500305175781, 72.61600494384766, 103.28900146484375), + "c": "m" + }, { + "origin": (72.61600494384766, 100.0), + "bbox": (72.61600494384766, 88.17500305175781, 78.73200225830078, 103.28900146484375), + "c": "e" + }, { + "origin": (78.73200225830078, 100.0), + "bbox": (78.73200225830078, 88.17500305175781, 81.79000091552734, 103.28900146484375), + "c": " " + < ... deleted ... > + }, { + "origin": (163.11297607421875, 100.0), + "bbox": (163.11297607421875, 88.17500305175781, 166.1709747314453, 103.28900146484375), + "c": "." + }], + + +XML +~~~ + +The :meth:`TextPage.extractXML` (or *Page.get_text("xml")*) version extracts text (no images) with the detail level of RAWDICT:: + + >>> for line in page.get_text("xml").splitlines(): + print(line) + + + + + + + + + + + + ... deleted ... + + + + + + + +.. note:: We have successfully tested `lxml `_ to interpret this output. + +XHTML +~~~~~ +:meth:`TextPage.extractXHTML` (or *Page.get_text("xhtml")*) is a variation of TEXT but in HTML format, containing the bare text and images ("semantic" output):: + +
+

Some text on first page.

+
+ +.. _text_extraction_flags: + +Text Extraction Flags Defaults +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +* New in version 1.16.2: Method :meth:`Page.get_text` supports a keyword parameter *flags* *(int)* to control the amount and the quality of extracted data. The following table shows the defaults settings (flags parameter omitted or None) for each extraction variant. If you specify flags with a value other than *None*, be aware that you must set **all desired** options. A description of the respective bit settings can be found in :ref:`TextPreserve`. + +* New in v1.19.6: The default combinations in the following table are now available as Python constants: :data:`TEXTFLAGS_TEXT`, :data:`TEXTFLAGS_WORDS`, :data:`TEXTFLAGS_BLOCKS`, :data:`TEXTFLAGS_DICT`, :data:`TEXTFLAGS_RAWDICT`, :data:`TEXTFLAGS_HTML`, :data:`TEXTFLAGS_XHTML`, :data:`TEXTFLAGS_XML`, and :data:`TEXTFLAGS_SEARCH`. You can now easily modify a default flag, e.g. + + - **include** images in a "blocks" output: + + `flags = TEXTFLAGS_BLOCKS | TEXT_PRESERVE_IMAGES` + + - **exclude** images from a "dict" output: + + `flags = TEXTFLAGS_DICT & ~TEXT_PRESERVE_IMAGES` + + - set **dehyphenation off** in text searches: + + `flags = TEXTFLAGS_SEARCH & ~TEXT_DEHYPHENATE` + + +=================== ==== ==== ===== === ==== ======= ===== ====== ====== +Indicator text html xhtml xml dict rawdict words blocks search +=================== ==== ==== ===== === ==== ======= ===== ====== ====== +preserve ligatures 1 1 1 1 1 1 1 1 1 +preserve whitespace 1 1 1 1 1 1 1 1 1 +preserve images n/a 1 1 n/a 1 1 n/a 0 0 +inhibit spaces 0 0 0 0 0 0 0 0 0 +dehyphenate 0 0 0 0 0 0 0 0 1 +clip to mediabox 1 1 1 1 1 1 1 1 1 +=================== ==== ==== ===== === ==== ======= ===== ====== ====== + +* **search** refers to the text search function. +* **"json"** is handled exactly like **"dict"** and is hence left out. +* **"rawjson"** is handled exactly like **"rawdict"** and is hence left out. +* An "n/a" specification means a value of 0 and setting this bit never has any effect on the output (but an adverse effect on performance). +* If you are not interested in images when using an output variant which includes them by default, then by all means set the respective bit off: You will experience a better performance and much lower space requirements. + +To show the effect of *TEXT_INHIBIT_SPACES* have a look at this example:: + + >>> print(page.get_text("text")) + H a l l o ! + Mo r e t e x t + i s f o l l o w i n g + i n E n g l i s h + . . . l e t ' s s e e + w h a t h a p p e n s . + >>> print(page.get_text("text", flags=fitz.TEXT_INHIBIT_SPACES)) + Hallo! + More text + is following + in English + ... let's see + what happens. + >>> + + +Performance +~~~~~~~~~~~~ +The text extraction methods differ significantly both: in terms of information they supply, and in terms of resource requirements and runtimes. Generally, more information of course means, that more processing is required and a higher data volume is generated. + +.. note:: Especially images have a **very significant** impact. Make sure to exclude them (via the *flags* parameter) whenever you do not need them. To process the below mentioned 2'700 total pages with default flags settings required 160 seconds across all extraction methods. When all images where excluded, less than 50% of that time (77 seconds) were needed. + +To begin with, all methods are **very fast** in relation to other products out there in the market. In terms of processing speed, we are not aware of a faster (free) tool. Even the most detailed method, RAWDICT, processes all 1'310 pages of the :ref:`AdobeManual` in less than 5 seconds (simple text needs less than 2 seconds here). + +The following table shows average relative speeds ("RSpeed", baseline 1.00 is TEXT), taken across ca. 1400 text-heavy and 1300 image-heavy pages. + +======= ====== ===================================================================== ========== +Method RSpeed Comments no images +======= ====== ===================================================================== ========== +TEXT 1.00 no images, **plain** text, line breaks 1.00 +BLOCKS 1.00 image bboxes (only), **block** level text with bboxes, line breaks 1.00 +WORDS 1.02 no images, **word** level text with bboxes 1.02 +XML 2.72 no images, **char** level text, layout and font details 2.72 +XHTML 3.32 **base64** images, **span** level text, no layout info 1.00 +HTML 3.54 **base64** images, **span** level text, layout and font details 1.01 +DICT 3.93 **binary** images, **span** level text, layout and font details 1.04 +RAWDICT 4.50 **binary** images, **char** level text, layout and font details 1.68 +======= ====== ===================================================================== ========== + +As mentioned: when excluding image extraction (last column), the relative speeds are changing drastically: except RAWDICT and XML, the other methods are almost equally fast, and RAWDICT requires 40% less execution time than the **now slowest XML**. + +Look at chapter **Appendix 1** for more performance information. + +.. include:: footer.rst diff --git a/docs/app2.rst b/docs/app2.rst new file mode 100644 index 0000000..30a0849 --- /dev/null +++ b/docs/app2.rst @@ -0,0 +1,37 @@ +.. include:: header.rst + +.. _Appendix2: + +================================================ +Appendix 2: Considerations on Embedded Files +================================================ +This chapter provides some background on embedded files support in PyMuPDF. + +General +---------- +Starting with version 1.4, PDF supports embedding arbitrary files as part ("Embedded File Streams") of a PDF document file (see chapter "7.11.4 +Embedded File Streams", pp. 103 of the :ref:`AdobeManual`). + +In many aspects, this is comparable to concepts also found in ZIP files or the OLE technique in MS Windows. PDF embedded files do, however, *not* support directory structures as does the ZIP format. An embedded file can in turn contain embedded files itself. + +Advantages of this concept are that embedded files are under the PDF umbrella, benefitting from its permissions / password protection and integrity aspects: all data, which a PDF may reference or even may be dependent on, can be bundled into it and so form a single, consistent unit of information. + +In addition to embedded files, PDF 1.7 adds *collections* to its support range. This is an advanced way of storing and presenting meta information (i.e. arbitrary and extensible properties) of embedded files. + +MuPDF Support +-------------- +After adding initial support for collections (portfolios) and */EmbeddedFiles* in MuPDF version 1.11, this support was dropped again in version 1.15. + +As a consequence, the cli utility *mutool* no longer offers access to embedded files. + +PyMuPDF -- having implemented an */EmbeddedFiles* API in response in its version 1.11.0 -- was therefore forced to change gears starting with its version 1.16.0 (we never published a MuPDF v1.15.x compatible PyMuPDF). + +We are now maintaining our own code basis supporting embedded files. This code makes use of basic MuPDF dictionary and array functions only. + +PyMuPDF Support +------------------ +We continue to support the full old API with respect to embedded files -- with only minor, cosmetic changes. + +There even also is a new function, which delivers a list of all names under which embedded data are resgistered in a PDF, :meth:`Document.embfile_names`. + +.. include:: footer.rst diff --git a/docs/app3.rst b/docs/app3.rst new file mode 100644 index 0000000..afab1f7 --- /dev/null +++ b/docs/app3.rst @@ -0,0 +1,304 @@ +.. include:: header.rst + +.. _Appendix3: + +================================================ +Appendix 3: Assorted Technical Information +================================================ +This section deals with various technical topics, that are not necessarily related to each other. + +------------ + +.. _ImageTransformation: + +Image Transformation Matrix +---------------------------- +Starting with version 1.18.11, the image transformation matrix is returned by some methods for text and image extraction: :meth:`Page.get_text` and :meth:`Page.get_image_bbox`. + +The transformation matrix contains information about how an image was transformed to fit into the rectangle (its "boundary box" = "bbox") on some document page. By inspecting the image's bbox on the page and this matrix, one can determine for example, whether and how the image is displayed scaled or rotated on a page. + +The relationship between image dimension and its bbox on a page is the following: + +1. Using the original image's width and height, + - define the image rectangle `imgrect = fitz.Rect(0, 0, width, height)` + - define the "shrink matrix" `shrink = fitz.Matrix(1/width, 0, 0, 1/height, 0, 0)`. + +2. Transforming the image rectangle with its shrink matrix, will result in the unit rectangle: `imgrect * shrink = fitz.Rect(0, 0, 1, 1)`. + +3. Using the image **transformation matrix** "transform", the following steps will compute the bbox:: + + imgrect = fitz.Rect(0, 0, width, height) + shrink = fitz.Matrix(1/width, 0, 0, 1/height, 0, 0) + bbox = imgrect * shrink * transform + +4. Inspecting the matrix product `shrink * transform` will reveal all information about what happened to the image rectangle to make it fit into the bbox on the page: rotation, scaling of its sides and translation of its origin. Let us look at an example: + + >>> imginfo = page.get_images()[0] # get an image item on a page + >>> imginfo + (5, 0, 439, 501, 8, 'DeviceRGB', '', 'fzImg0', 'DCTDecode') + >>> #------------------------------------------------ + >>> # define image shrink matrix and rectangle + >>> #------------------------------------------------ + >>> shrink = fitz.Matrix(1 / 439, 0, 0, 1 / 501, 0, 0) + >>> imgrect = fitz.Rect(0, 0, 439, 501) + >>> #------------------------------------------------ + >>> # determine image bbox and transformation matrix: + >>> #------------------------------------------------ + >>> bbox, transform = page.get_image_bbox("fzImg0", transform=True) + >>> #------------------------------------------------ + >>> # confirm equality - permitting rounding errors + >>> #------------------------------------------------ + >>> bbox + Rect(100.0, 112.37525939941406, 300.0, 287.624755859375) + >>> imgrect * shrink * transform + Rect(100.0, 112.375244140625, 300.0, 287.6247253417969) + >>> #------------------------------------------------ + >>> shrink * transform + Matrix(0.0, -0.39920157194137573, 0.3992016017436981, 0.0, 100.0, 287.6247253417969) + >>> #------------------------------------------------ + >>> # the above shows: + >>> # image sides are scaled by same factor ~0.4, + >>> # and the image is rotated by 90 degrees clockwise + >>> # compare this with fitz.Matrix(-90) * 0.4 + >>> #------------------------------------------------ + + +------------ + +.. _Base-14-Fonts: + +PDF Base 14 Fonts +--------------------- +The following 14 builtin font names **must be supported by every PDF viewer** application. They are available as a dictionary, which maps their full names amd their abbreviations in lower case to the full font basename. Wherever a **fontname** must be provided in PyMuPDF, any **key or value** from the dictionary may be used:: + + In [2]: fitz.Base14_fontdict + Out[2]: + {'courier': 'Courier', + 'courier-oblique': 'Courier-Oblique', + 'courier-bold': 'Courier-Bold', + 'courier-boldoblique': 'Courier-BoldOblique', + 'helvetica': 'Helvetica', + 'helvetica-oblique': 'Helvetica-Oblique', + 'helvetica-bold': 'Helvetica-Bold', + 'helvetica-boldoblique': 'Helvetica-BoldOblique', + 'times-roman': 'Times-Roman', + 'times-italic': 'Times-Italic', + 'times-bold': 'Times-Bold', + 'times-bolditalic': 'Times-BoldItalic', + 'symbol': 'Symbol', + 'zapfdingbats': 'ZapfDingbats', + 'helv': 'Helvetica', + 'heit': 'Helvetica-Oblique', + 'hebo': 'Helvetica-Bold', + 'hebi': 'Helvetica-BoldOblique', + 'cour': 'Courier', + 'coit': 'Courier-Oblique', + 'cobo': 'Courier-Bold', + 'cobi': 'Courier-BoldOblique', + 'tiro': 'Times-Roman', + 'tibo': 'Times-Bold', + 'tiit': 'Times-Italic', + 'tibi': 'Times-BoldItalic', + 'symb': 'Symbol', + 'zadb': 'ZapfDingbats'} + +In contrast to their obligation, not all PDF viewers support these fonts correctly and completely -- this is especially true for Symbol and ZapfDingbats. Also, the glyph (visual) images will be specific to every reader. + +To see how these fonts can be used -- including the **CJK built-in** fonts -- look at the table in :meth:`Page.insert_font`. + +------------ + +.. _AdobeManual: + +Adobe PDF References +--------------------------- + +This PDF Reference manual published by Adobe is frequently quoted throughout this documentation. It can be viewed and downloaded from `here `_. + +.. note:: For a long time, an older version was also available under `this `_ link. It seems to be taken off of the web site in October 2021. Earlier (pre 1.19.*) versions of the PyMuPDF documentation used to refer to this document. We have undertaken an effort to replace referrals to the current specification above. + +------------ + +.. _SequenceTypes: + +Using Python Sequences as Arguments in PyMuPDF +------------------------------------------------ +When PyMuPDF objects and methods require a Python **list** of numerical values, other Python **sequence types** are also allowed. Python classes are said to implement the **sequence protocol**, if they have a `__getitem__()` method. + +This basically means, you can interchangeably use Python *list* or *tuple* or even *array.array*, *numpy.array* and *bytearray* types in these cases. + +For example, specifying a sequence `"s"` in any of the following ways + +* `s = [1, 2]` -- a list +* `s = (1, 2)` -- a tuple +* `s = array.array("i", (1, 2))` -- an array.array +* `s = numpy.array((1, 2))` -- a numpy array +* `s = bytearray((1, 2))` -- a bytearray + +will make it usable in the following example expressions: + +* `fitz.Point(s)` +* `fitz.Point(x, y) + s` +* `doc.select(s)` + +Similarly with all geometry objects :ref:`Rect`, :ref:`IRect`, :ref:`Matrix` and :ref:`Point`. + +Because all PyMuPDF geometry classes themselves are special cases of sequences, they (with the exception of :ref:`Quad` -- see below) can be freely used where numerical sequences can be used, e.g. as arguments for functions like *list()*, *tuple()*, *array.array()* or *numpy.array()*. Look at the following snippet to see this work. + +>>> import fitz, array, numpy as np +>>> m = fitz.Matrix(1, 2, 3, 4, 5, 6) +>>> +>>> list(m) +[1.0, 2.0, 3.0, 4.0, 5.0, 6.0] +>>> +>>> tuple(m) +(1.0, 2.0, 3.0, 4.0, 5.0, 6.0) +>>> +>>> array.array("f", m) +array('f', [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]) +>>> +>>> np.array(m) +array([1., 2., 3., 4., 5., 6.]) + +.. note:: :ref:`Quad` is a Python sequence object as well and has a length of 4. Its items however are :data:`point_like` -- not numbers. Therefore, the above remarks do not apply. + +------------ + +.. _ReferenialIntegrity: + +Ensuring Consistency of Important Objects in PyMuPDF +------------------------------------------------------------ +PyMuPDF is a Python binding for the C library MuPDF. While a lot of effort has been invested by MuPDF's creators to approximate some sort of an object-oriented behavior, they certainly could not overcome basic shortcomings of the C language in that respect. + +Python on the other hand implements the OO-model in a very clean way. The interface code between PyMuPDF and MuPDF consists of two basic files: *fitz.py* and *fitz_wrap.c*. They are created by the excellent SWIG tool for each new version. + +When you use one of PyMuPDF's objects or methods, this will result in execution of some code in *fitz.py*, which in turn will call some C code compiled with *fitz_wrap.c*. + +Because SWIG goes a long way to keep the Python and the C level in sync, everything works fine, if a certain set of rules is being strictly followed. For example: **never access** a :ref:`Page` object, after you have closed (or deleted or set to *None*) the owning :ref:`Document`. Or, less obvious: **never access** a page or any of its children (links or annotations) after you have executed one of the document methods *select()*, *delete_page()*, *insert_page()* ... and more. + +But just no longer accessing invalidated objects is actually not enough: They should rather be actively deleted entirely, to also free C-level resources (meaning allocated memory). + +The reason for these rules lies in the fact that there is a hierarchical 2-level one-to-many relationship between a document and its pages and also between a page and its links / annotations. To maintain a consistent situation, any of the above actions must lead to a complete reset -- in **Python and, synchronously, in C**. + +SWIG cannot know about this and consequently does not do it. + +The required logic has therefore been built into PyMuPDF itself in the following way. + +1. If a page "loses" its owning document or is being deleted itself, all of its currently existing annotations and links will be made unusable in Python, and their C-level counterparts will be deleted and deallocated. + +2. If a document is closed (or deleted or set to *None*) or if its structure has changed, then similarly all currently existing pages and their children will be made unusable, and corresponding C-level deletions will take place. "Structure changes" include methods like *select()*, *delePage()*, *insert_page()*, *insert_pdf()* and so on: all of these will result in a cascade of object deletions. + +The programmer will normally not realize any of this. If he, however, tries to access invalidated objects, exceptions will be raised. + +Invalidated objects cannot be directly deleted as with Python statements like *del page* or *page = None*, etc. Instead, their *__del__* method must be invoked. + +All pages, links and annotations have the property *parent*, which points to the owning object. This is the property that can be checked on the application level: if *obj.parent == None* then the object's parent is gone, and any reference to its properties or methods will raise an exception informing about this "orphaned" state. + +A sample session: + +>>> page = doc[n] +>>> annot = page.first_annot +>>> annot.type # everything works fine +[5, 'Circle'] +>>> page = None # this turns 'annot' into an orphan +>>> annot.type +<... omitted lines ...> +RuntimeError: orphaned object: parent is None +>>> +>>> # same happens, if you do this: +>>> annot = doc[n].first_annot # deletes the page again immediately! +>>> annot.type # so, 'annot' is 'born' orphaned +<... omitted lines ...> +RuntimeError: orphaned object: parent is None + +This shows the cascading effect: + +>>> doc = fitz.open("some.pdf") +>>> page = doc[n] +>>> annot = page.first_annot +>>> page.rect +fitz.Rect(0.0, 0.0, 595.0, 842.0) +>>> annot.type +[5, 'Circle'] +>>> del doc # or doc = None or doc.close() +>>> page.rect +<... omitted lines ...> +RuntimeError: orphaned object: parent is None +>>> annot.type +<... omitted lines ...> +RuntimeError: orphaned object: parent is None + +.. note:: Objects outside the above relationship are not included in this mechanism. If you e.g. created a table of contents by *toc = doc.get_toc()*, and later close or change the document, then this cannot and does not change variable *toc* in any way. It is your responsibility to refresh such variables as required. + +------------ + +.. _FormXObject: + +Design of Method :meth:`Page.show_pdf_page` +-------------------------------------------- + +Purpose and Capabilities +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The method displays an image of a ("source") page of another PDF document within a specified rectangle of the current ("containing", "target") page. + +* **In contrast** to :meth:`Page.insert_image`, this display is vector-based and hence remains accurate across zooming levels. +* **Just like** :meth:`Page.insert_image`, the size of the display is adjusted to the given rectangle. + +The following variations of the display are currently supported: + +* Bool parameter *keep_proportion* controls whether to maintain the aspect ratio (default) or not. +* Rectangle parameter *clip* restricts the visible part of the source page rectangle. Default is the full page. +* float *rotation* rotates the display by an arbitrary angle (degrees). If the angle is not an integer multiple of 90, only 2 of the 4 corners may be positioned on the target border if also *keep_proportion* is true. +* Bool parameter *overlay* controls whether to put the image on top (foreground, default) of current page content or not (background). + +Use cases include (but are not limited to) the following: + +1. "Stamp" a series of pages of the current document with the same image, like a company logo or a watermark. +2. Combine arbitrary input pages into one output page to support “booklet” or double-sided printing (known as "4-up", "n-up"). +3. Split up (large) input pages into several arbitrary pieces. This is also called “posterization”, because you e.g. can split an A4 page horizontally and vertically, print the 4 pieces enlarged to separate A4 pages, and end up with an A2 version of your original page. + +Technical Implementation +~~~~~~~~~~~~~~~~~~~~~~~~~ + +This is done using PDF **"Form XObjects"**, see section 8.10 on page 217 of :ref:`AdobeManual`. On execution of a *Page.show_pdf_page(rect, src, pno, ...)*, the following things happen: + + 1. The :data:`resources` and :data:`contents` objects of page *pno* in document *src* are copied over to the current document, jointly creating a new **Form XObject** with the following properties. The PDF :data:`xref` number of this object is returned by the method. + + a. */BBox* equals */Mediabox* of the source page + b. */Matrix* equals the identity matrix *[1 0 0 1 0 0]* + c. */Resources* equals that of the source page. This involves a “deep-copy” of hierarchically nested other objects (including fonts, images, etc.). The complexity involved here is covered by MuPDF’s grafting [#f1]_ technique functions. + d. This is a stream object type, and its stream is an exact copy of the combined data of the source page's */Contents* objects. + + This step is only executed once per shown source page. Subsequent displays of the same page only create pointers (done in next step) to this object. + + 2. A second **Form XObject** is then created which the target page uses to invoke the display. This object has the following properties: + + a. */BBox* equals the */CropBox* of the source page (or *clip*). + b. */Matrix* represents the mapping of */BBox* to the target rectangle. + c. */XObject* references the previous XObject via the fixed name *fullpage*. + d. The stream of this object contains exactly one fixed statement: */fullpage Do*. + + 3. The :data:`resources` and :data:`contents` objects of the target page are now modified as follows. + + a. Add an entry to the */XObject* dictionary of */Resources* with the name *fzFrm* (with n chosen such that this entry is unique on the page). + b. Depending on *overlay*, prepend or append a new object to the page's */Contents* array, containing the statement *q /fzFrm Do Q*. + + +.. _RedirectMessages: + +Redirecting Error and Warning Messages +-------------------------------------------- +Since MuPDF version 1.16 error and warning messages can be redirected via an official plugin. + +PyMuPDF will put error messages to *sys.stderr* prefixed with the string "mupdf:". Warnings are internally stored and can be accessed via *fitz.TOOLS.mupdf_warnings()*. There also is a function to empty this store. + + +.. rubric:: Footnotes + +.. [#f1] MuPDF supports "deep-copying" objects between PDF documents. To avoid duplicate data in the target, it uses so-called "graftmaps", like a form of scratchpad: for each object to be copied, its :data:`xref` number is looked up in the graftmap. If found, copying is skipped. Otherwise, the new :data:`xref` is recorded and the copy takes place. PyMuPDF makes use of this technique in two places so far: :meth:`Document.insert_pdf` and :meth:`Page.show_pdf_page`. This process is fast and very efficient, because it prevents multiple copies of typically large and frequently referenced data, like images and fonts. However, you may still want to consider using garbage collection (option 4) in any of the following cases: + + 1. The target PDF is not new / empty: grafting does not check for resources that already existed (e.g. images, fonts) in the target document before opening it. + 2. Using :meth:`Page.show_pdf_page` for more than one source document: each grafting occurs **within one source** PDF only, not across multiple. So if e.g. the same image exists in pages from different source PDFs, then this will not be detected until garbage collection. + +.. include:: footer.rst diff --git a/docs/app4.rst b/docs/app4.rst new file mode 100644 index 0000000..7d29037 --- /dev/null +++ b/docs/app4.rst @@ -0,0 +1,477 @@ +.. include:: header.rst + + +.. role:: red-color +.. role:: orange-color +.. role:: green-color + + + +.. _Appendix4: + +================================================ +Appendix 4: Performance Comparison Methodology +================================================ + +This article documents the approach to measure :title:`PyMuPDF's` performance and the tools and example files used to do comparisons. + +The following three sections deal with different performance aspects: + +* :ref:`Document Copying` - This includes opening and parsing :title:`PDFs`, then writing them to an output file. Because the same basic activities are also used for joining (merging) :title:`PDFs`, the results also apply to these use cases. +* :ref:`Text Extraction` - This extracts plain text from :title:`PDFs` and writes it to an output text file. +* :ref:`Page Rendering` - This converts :title:`PDF` pages to image files looking identical to the pages. This ability is the basic prerequisite for using a tool in :title:`Python GUI` scripts to scroll through documents. We have chosen a medium-quality (resolution 150 DPI) version. + +Please note that in all cases the actual speed in dealing with :title:`PDF` structures is not directly measured: instead, the timings also include the durations of writing files to the operating system's file system. This cannot be avoided because tools other than :title:`PyMuPDF` do not offer the option to e.g., separate the image **creation** step from the following step, which **writes** the image into a file. + +So all timings documented include a common, OS-oriented base effort. Therefore, performance **differences per tool are actually larger** than the numbers suggest. + + + +.. _Appendix4_Files_Used: + +Files used +------------ + +A set of eight files is used for the performance testing. With each file we have the following information: + +- **Name** of the file and download **link**. +- **Size** in bytes. +- Total number of **pages** in file. +- Total number of bookmarks (**Table of Contents** entries). +- Total number of **links**. +- **KB size** per page. +- **Textsize per page** is the amount text in the whole file in KB, divided by the number of pages. +- Any **notes** to generally describe the type of file. + + +.. list-table:: + :header-rows: 1 + + * - **Name** + - **Size (bytes)** + - **Pages** + - **TOC size** + - **Links** + - **KB/page** + - **Textsize/page** + - **Notes** + * - `adobe.pdf`_ + - 32,472,771 + - 1,310 + - 794 + - 32,096 + - 24 + - 1,942 + - linearized, many links / bookmarks + * - `artifex-website.pdf`_ + - 31,570,732 + - 47 + - 46 + - 2,035 + - 656 + - 3,538 + - graphics oriented + * - `db-systems.pdf`_ + - 29,326,355 + - 1,241 + - 0 + - 0 + - 23 + - 2,142 + - + * - `fontforge.pdf`_ + - 8,222,384 + - 214 + - 31 + - 242 + - 38 + - 1,058 + - mix of text & graphics + * - `pandas.pdf`_ + - 10,585,962 + - 3,071 + - 536 + - 16,554 + - 3 + - 1,539 + - many pages + * - `pymupdf.pdf`_ + - 6,805,176 + - 478 + - 276 + - 5,277 + - 14 + - 1,937 + - text oriented + * - `pythonbook.pdf`_ + - 9,983,856 + - 669 + - 198 + - 1,953 + - 15 + - 1,929 + - + * - `sample-50-MB-pdf-file.pdf`_ + - 52,521,850 + - 1 + - 0 + - 0 + - 51,291 + - 23,860 + - single page, graphics oriented, large file size + + + +.. note:: + + **adobe.pdf** and **pymupdf.pdf** are clearly text oriented, **artifex-website.pdf** and **sample-50-MB-pdf-file.pdf** are graphics oriented. Other files are a mix of both. + + +Tools used +------------- + +In each section, the same fixed set of :title:`PDF` files is being processed by a set of tools. The set of tools used per performance aspect however varies, depending on the supported tool features. + +All tools are either platform independent, or at least can run on both, :title:`Windows` and :title:`Unix` / :title:`Linux`. + + +.. list-table:: + :header-rows: 1 + + * - **Tool** + - **Description** + * - :title:`PyMuPDF` + - The tool of this manual. + * - PDFrw_ + - A pure :title:`Python` tool, being used by :title:`rst2pdf`, has interface to :title:`ReportLab`. + * - PyPDF2_ + - A pure :title:`Python` tool with a large function set. + * - PDFMiner_ + - A pure :title:`Python` to extract text and other data from :title:`PDF`. + * - XPDF_ + - A command line utility with multiple functions. + * - PikePDF_ + - A :title:`Python` package similar to :title:`PDFrw`, but based on :title:`C++` library :title:`QPDF`. + * - PDF2JPG_ + - A :title:`Python` package specialized on rendering :title:`PDF` pages to :title:`JPG` images. + + + +.. _app4_copying: + + +Copying / Joining / Merging +---------------------------------- + +How fast is a :title:`PDF` file read and its content parsed for further processing? The sheer parsing performance cannot directly be compared, because batch utilities always execute a requested task completely, in one go, front to end. :title:`PDFrw` too, has a *lazy* strategy for parsing, meaning it only parses those parts of a document that are required in any moment. + +To find an answer to the question, we therefore measure the time to copy a :title:`PDF` file to an output file with each tool, and do nothing else. + +These are the :title:`Python` commands for how each tool is used: + +:title:`PyMuPDF` + +.. code-block:: python + + import fitz + doc = fitz.open("input.pdf") + doc.save("output.pdf") + +:title:`PDFrw` + + +.. code-block:: python + + doc = PdfReader("input.pdf") + writer = PdfWriter() + writer.trailer = doc + writer.write("output.pdf") + +:title:`PikePDF` + +.. code-block:: python + + from pikepdf import Pdf + doc = Pdf.open("input.pdf") + doc.save("output.pdf") + +:title:`PyPDF2` + +.. code-block:: python + + pdfmerge = PyPDF2.PdfMerger() + pdfmerge.append("input.pdf") + pdfmerge.write("output.pdf") + pdfmerge.close() + + + + +**Observations** + +These are our run time findings in **seconds** along with a base rate summary compared to :title:`PyMuPDF`: + +.. list-table:: + :header-rows: 1 + + * - **Name** + - **PyMuPDF** + - **PDFrw** + - **PikePDF** + - **PyPDF2** + * - adobe.pdf + - 1.75 + - 5.15 + - 22.37 + - 374.05 + * - artifex-website.pdf + - 0.26 + - 0.38 + - 1.41 + - 2.81 + * - db-systems.pdf + - 0.15 + - 0.8 + - 1.68 + - 2.46 + * - fontforge.pdf + - 0.09 + - 0.14 + - 0.28 + - 1.1 + * - pandas.pdf + - 0.38 + - 2.21 + - 2.73 + - 70.3 + * - pymupdf.pdf + - 0.11 + - 0.56 + - 0.83 + - 6.05 + * - pythonbook.pdf + - 0.19 + - 1.2 + - 1.34 + - 37.19 + * - sample-50-MB-pdf-file.pdf + - 0.12 + - 0.1 + - 2.93 + - 0.08 + * - **Total** + - **3.05** + - **10.54** + - **33.57** + - **494.04** + * - + - + - + - + - + * - **Rate compared to PyMuPDF** + - :green-color:`1.0` + - :orange-color:`3.5` + - :orange-color:`11.0` + - :red-color:`162` + + + +.. _app4_text_extraction: + +Text Extraction +---------------------------------- + +The following table shows plain text extraction durations. All tools have been used with their most basic functionality - i.e. no layout re-arrangements, etc. + + +**Observations** + +These are our run time findings in **seconds** along with a base rate summary compared to :title:`PyMuPDF`: + +.. list-table:: + :header-rows: 1 + + * - **Name** + - **PyMuPDF** + - **XPDF** + - **PyPDF2** + - **PDFMiner** + * - adobe.pdf + - 2.01 + - 6.19 + - 22.2 + - 49.15 + * - artifex-website.pdf + - 0.18 + - 0.3 + - 1.1 + - 4.06 + * - db-systems.pdf + - 1.57 + - 4.26 + - 25.75 + - 42.19 + * - fontforge.pdf + - 0.24 + - 0.47 + - 2.69 + - 4.2 + * - pandas.pdf + - 2.41 + - 10.54 + - 25.38 + - 76.56 + * - pymupdf.pdf + - 0.49 + - 2.34 + - 6.44 + - 13.55 + * - pythonbook.pdf + - 0.84 + - 2.88 + - 9.28 + - 24.27 + * - sample-50-MB-pdf-file.pdf + - 0.27 + - 0.44 + - 8.8 + - 13.29 + * - **Total** + - **8.01** + - **27.42** + - **101.64** + - **227.27** + * - + - + - + - + - + * - **Rate compared to PyMuPDF** + - :green-color:`1.0` + - :orange-color:`3.42` + - :orange-color:`12.69` + - :red-color:`28.37` + + +.. _app4_page_rendering: + +Page Rendering +-------------------------- + +We have tested rendering speed of :title:`PyMuPDF` against :title:`pdf2jpg` and :title:`XPDF` at a resolution of 150 DPI, + + +These are the :title:`Python` commands for how each tool is used: + + +:title:`PyMuPDF` + +.. code-block:: python + + def ProcessFile(datei): + print "processing:", datei + doc=fitz.open(datei) + for p in fitz.Pages(doc): + pix = p.get_pixmap(dpi=150) + pix.save("t-%s.png" % p.number) + pix = None + doc.close() + return + +:title:`XPDF` + +.. code-block:: python + + pdftopng.exe -r 150 file.pdf ./ + + +:title:`PDF2JPG` + +.. code-block:: python + + def ProcessFile(datei): + print("processing:", datei) + pdf2jpg.convert_pdf2jpg(datei, "images", pages="ALL", dpi=150) + return + + +**Observations** + +These are our run time findings in **seconds** along with a base rate summary compared to :title:`PyMuPDF`: + + +.. list-table:: + :header-rows: 1 + + * - **Name** + - **PyMuPDF** + - **XPDF** + - **PDF2JPG** + * - adobe.pdf + - 51.33 + - 98.16 + - 75.71 + * - artifex-website.pdf + - 26.35 + - 51.28 + - 54.11 + * - db-systems.pdf + - 84.59 + - 143.16 + - 405.22 + * - fontforge.pdf + - 12.23 + - 22.18 + - 20.14 + * - pandas.pdf + - 138.74 + - 241.67 + - 202.06 + * - pymupdf.pdf + - 22.35 + - 39.11 + - 33.38 + * - pythonbook.pdf + - 30.44 + - 49.12 + - 55.68 + * - sample-50-MB-pdf-file.pdf + - 1.01 + - 1.32 + - 5.22 + * - **Total** + - **367.04** + - **646** + - **851.52** + * - + - + - + - + * - **Rate compared to PyMuPDF** + - :green-color:`1.0` + - :orange-color:`1.76` + - :red-color:`2.32` + + +.. include:: footer.rst + + +.. External links + +.. _PDFrw : https://pypi.org/project/pdfrw/ +.. _PyPDF2 : https://pypi.org/project/pypdf/ +.. _PDFMiner : https://pypi.org/project/pdfminer.six/ +.. _PDFtk : https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/ +.. _XPDF : https://www.xpdfreader.com/ +.. _PikePDF : https://pypi.org/search/?q=pikepdf +.. _PDF2JPG : https://pypi.org/project/pdf2jpg/ + +.. _adobe.pdf : https://artifex.com/samples/pdf/adobe.pdf +.. _artifex-website.pdf : https://artifex.com/samples/pdf/artifex-website.pdf +.. _db-systems.pdf : https://artifex.com/samples/pdf/db-systems.pdf +.. _fontforge.pdf : https://artifex.com/samples/pdf/fontforge.pdf +.. _pandas.pdf : https://artifex.com/samples/pdf/pandas.pdf +.. _pymupdf.pdf : https://artifex.com/samples/pdf/pymupdf.pdf +.. _pythonbook.pdf : https://artifex.com/samples/pdf/pythonbook.pdf +.. _sample-50-MB-pdf-file.pdf : https://artifex.com/samples/pdf/sample-50-MB-pdf-file.pdf diff --git a/docs/archive-class.rst b/docs/archive-class.rst new file mode 100644 index 0000000..7efc2f5 --- /dev/null +++ b/docs/archive-class.rst @@ -0,0 +1,103 @@ +.. include:: header.rst + +.. _Archive: + +================ +Archive +================ + +* New in v1.21.0 + +This class represents a generalization of file folders and container files like ZIP and TAR archives. Archives allow accessing arbitrary collections of file folders, ZIP / TAR files and single binary data elements as if they all were part of one hierarchical tree of folders. + +In PyMuPDF, archives are currently only used by :ref:`Story` objects to specify where to look for fonts, images and other resources. + +================================ =================================================== +**Method / Attribute** **Short Description** +================================ =================================================== +:meth:`Archive.add` add new data to the archive +:meth:`Archive.has_entry` check if given name is a member +:meth:`Archive.read_entry` read the data given by the name +:attr:`Archive.entry_list` list[dict] of archive items +================================ =================================================== + +**Class API** + +.. class:: Archive + + .. method:: __init__(self [, content [, path]]) + + Creates a new archive. Without parameters, an empty archive is created. + + If provided, `content` may be one of the following: + + * another Archive: the archive is being made a sub-archive of the new one. + + * a string: this must be the name of a local folder or file. `pathlib.Path` objects are also supported. + + - A **folder** will be converted to a sub-archive, so its files (and any sub-folders) can be accessed by their names. + - A **file** will be read with mode `"rb"` and these binary data (a `bytes` object) be treated as a single-member sub-archive. In this case, the `path` parameter is **mandatory** and should be the member name under which this item can be found / retrieved. + + * a `zipfile.ZipFile` or `tarfile.TarFile` object: Will be added as a sub-archive. + + * a Python binary object (`bytes`, `bytearray`, `io.BytesIO`): this will add a single-member sub-archive. In this case, the `path` parameter is **mandatory** and should be the member name under which this item can be found / retrieved. + + * a tuple `(data, name)`: This will add a single-member sub-archive with the member name `name`. `data` may be a Python binary object or a local file name (in which case its binary file content is used). Use this format if you need to specify `path`. + + * a Python sequence: This is a convenience format to specify any combination of the above. + + If provided, `path` must be a string. + + * If `content` is either binary data or a file name, this parameter is mandatory and must be the name under which the data can be found. + + * Otherwise this parameter is optional. It can be used to simulate a folder name or a mount point, under which this sub-archive's elements can be found. For example this specification `Archive((data, "name"), "path")` means that `data` will be found using the element name `"path/name"`. Similar is true for other sub-archives: to retrieve members of a ZIP sub-archive, their names must be prefixed with `"path/"`. The main purpose of this parameter probably is to differentiate between duplicate names. + + .. note:: If duplicate entry names exist in the archive, always the last entry with that name will be found / retrieved. During archive creation, or appending more data to an archive (see :meth:`Archive.add`) no check for duplicates will be made. Use the `path` parameter to prevent this from happening. + + .. method:: add(content [,path]) + + Append a sub-archive. The meaning of the parameters are exactly the same as explained above. Of course, parametrer `content` is not optional here. + + .. method:: has_entry(name) + + Checks whether an entry exists in any of the sub-archives. + + :arg str name: The fully qualified name of the entry. So must include any `path` prefix under which the entry's sub-archive has been added. + + :returns: `True` or `False`. + + .. method:: read_entry(name) + + Retrieve the data of an entry. + + :arg str name: The fully qualified name of the entry. So must include any `path` prefix under which the entry's sub-archive has been added. + + :returns: The binary data (`bytes`) of the entry. If not found, an exception is raised. + + .. attribute:: entry_list + + A list of the archive's sub-archives. Each list item is a dictionary with the following keys: + + * `entries` -- a list of (top-level) entry names in this sub-archive. + * `fmt` -- the format of the sub-archive. This is one of the strings "dir" (file folder), "zip" (ZIP archive), "tar" (TAR archive), or "tree" for single binary entries or file content. + * `path` -- the value of the `path` parameter under which this sub-archive was added. + + **Example:** + + >>> from pprint import pprint + >>> import fitz + >>> dir1 = "fitz-32" # a folder name + >>> dir2 = "fitz-64" # a folder name + >>> img = ("nur-ruhig.jpg", "img") # an image file + >>> members = (dir1, img, dir2) # we want to append these in one go + >>> arch = fitz.Archive() + >>> arch.add(members, path="mypath") + >>> pprint(arch.entry_list) + [{'entries': ['310', '37', '38', '39'], 'fmt': 'dir', 'path': 'mypath'}, + {'entries': ['img'], 'fmt': 'tree', 'path': 'mypath'}, + {'entries': ['310', '311', '37', '38', '39', 'pypy'], + 'fmt': 'dir', + 'path': 'mypath'}] + >>> + +.. include:: footer.rst diff --git a/docs/changes.rst b/docs/changes.rst new file mode 100644 index 0000000..f4ec1bb --- /dev/null +++ b/docs/changes.rst @@ -0,0 +1,5 @@ +.. include:: header.rst + +.. include:: ../changes.txt + +.. include:: footer.rst diff --git a/docs/classes.rst b/docs/classes.rst new file mode 100644 index 0000000..11cb44f --- /dev/null +++ b/docs/classes.rst @@ -0,0 +1,36 @@ +.. include:: header.rst + +============ +Classes +============ + +.. toctree:: + :maxdepth: 2 + + annot.rst + archive-class.rst + colorspace.rst + displaylist.rst + document.rst + document-writer-class.rst + font.rst + identity.rst + irect.rst + link.rst + linkdest.rst + matrix.rst + outline.rst + page.rst + pixmap.rst + point.rst + quad.rst + rect.rst + shape.rst + story-class.rst + textpage.rst + textwriter.rst + tools.rst + widget.rst + xml-class.rst + +.. include:: footer.rst diff --git a/docs/colors.rst b/docs/colors.rst new file mode 100644 index 0000000..1cd019b --- /dev/null +++ b/docs/colors.rst @@ -0,0 +1,47 @@ +.. include:: header.rst + +.. _ColorDatabase: + +================ +Color Database +================ +Since the introduction of methods involving colors (like :meth:`Page.draw_circle`), a requirement may be to have access to predefined colors. + +The fabulous GUI package `wxPython `_ has a database of over 540 predefined RGB colors, which are given more or less memorizable names. Among them are not only standard names like "green" or "blue", but also "turquoise", "skyblue", and 100 (not only 50 ...) shades of "gray", etc. + +We have taken the liberty to copy this database (a list of tuples) modified into PyMuPDF and make its colors available as PDF compatible float triples: for wxPython's *("WHITE", 255, 255, 255)* we return *(1, 1, 1)*, which can be directly used in *color* and *fill* parameters. We also accept any mixed case of "wHiTe" to find a color. + +Function *getColor()* +------------------------ +As the color database may not be needed very often, one additional import statement seems acceptable to get access to it:: + + >>> # "getColor" is the only method you really need + >>> from fitz.utils import getColor + >>> getColor("aliceblue") + (0.9411764705882353, 0.9725490196078431, 1.0) + >>> # + >>> # to get a list of all existing names + >>> from fitz.utils import getColorList + >>> cl = getColorList() + >>> cl + ['ALICEBLUE', 'ANTIQUEWHITE', 'ANTIQUEWHITE1', 'ANTIQUEWHITE2', 'ANTIQUEWHITE3', + 'ANTIQUEWHITE4', 'AQUAMARINE', 'AQUAMARINE1'] ... + >>> # + >>> # to see the full integer color coding + >>> from fitz.utils import getColorInfoList + >>> il = getColorInfoList() + >>> il + [('ALICEBLUE', 240, 248, 255), ('ANTIQUEWHITE', 250, 235, 215), + ('ANTIQUEWHITE1', 255, 239, 219), ('ANTIQUEWHITE2', 238, 223, 204), + ('ANTIQUEWHITE3', 205, 192, 176), ('ANTIQUEWHITE4', 139, 131, 120), + ('AQUAMARINE', 127, 255, 212), ('AQUAMARINE1', 127, 255, 212)] ... + + +Printing the Color Database +---------------------------- +If you want to actually see how the many available colors look like, use scripts `print by RGB `_ or `print by HSV `_ in the examples directory. They create PDFs (already existing in the same directory) with all these colors. Their only difference is sorting order: one takes the RGB values, the other one the Hue-Saturation-Values as sort criteria. +This is a screen print of what these files look like. + +.. image:: images/img-colordb.* + +.. include:: footer.rst \ No newline at end of file diff --git a/docs/colorspace.rst b/docs/colorspace.rst new file mode 100644 index 0000000..2e4e3e3 --- /dev/null +++ b/docs/colorspace.rst @@ -0,0 +1,43 @@ +.. include:: header.rst + +.. _Colorspace: + +================ +Colorspace +================ + +Represents the color space of a :ref:`Pixmap`. + + +**Class API** + +.. class:: Colorspace + + .. method:: __init__(self, n) + + Constructor + + :arg int n: A number identifying the colorspace. Possible values are :data:`CS_RGB`, :data:`CS_GRAY` and :data:`CS_CMYK`. + + .. attribute:: name + + The name identifying the colorspace. Example: *fitz.csCMYK.name = 'DeviceCMYK'*. + + :type: str + + .. attribute:: n + + The number of bytes required to define the color of one pixel. Example: *fitz.csCMYK.n == 4*. + + :type: int + + + **Predefined Colorspaces** + + For saving some typing effort, there exist predefined colorspace objects for the three available cases. + + * :data:`csRGB` = *fitz.Colorspace(fitz.CS_RGB)* + * :data:`csGRAY` = *fitz.Colorspace(fitz.CS_GRAY)* + * :data:`csCMYK` = *fitz.Colorspace(fitz.CS_CMYK)* + +.. include:: footer.rst \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..ee61620 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,266 @@ +# -*- coding: utf-8 -*- +# +import re +import sys +import os +import datetime + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +sys.path.insert(0, os.path.abspath(".")) + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# needs_sphinx = "4.2.0" + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +# extensions = ["sphinx.ext.autodoc", "sphinx.ext.coverage", "sphinx.ext.ifconfig"] +extensions = [] +# rst2pdf is not available on OpenBSD. +if hasattr(os, "uname") and os.uname()[0] != "OpenBSD": + extensions.append("rst2pdf.pdfbuilder") + +# Add any paths that contain templates here, relative to this directory. +templates_path = ["_templates"] + +# The suffix of source filenames. +source_suffix = ".rst" + +# The encoding of source files. +# source_encoding = 'utf-8-sig' + +# The master toctree document. +root_doc = "index" + +# General information about the project. +project = "PyMuPDF" +thisday = datetime.date.today() +copyright = "2015-" + str(thisday.year) + ", Artifex" + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The full version, including alpha/beta/rc tags. +_path = os.path.abspath(f'{__file__}/../../fitz/version.i') +with open(_path) as f: + for line in f: + match = re.search('VersionBind = "([0-9][.][0-9]+[.][0-9])"', line) + if match: + release = match.group(1) + print(f'{__file__}: setting version from {_path}: {release}') + break + else: + raise Exception(f'Failed to find `VersionBind = ...` in {_path}') + +# The short X.Y version +version = release + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +# today = '' +# Else, today_fmt is used as the format for a strftime call. +# today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ["_build", "build"] + +# The reST default role (used for this markup: `text`) to use for all +# documents. +default_role = "any" + +# If true, '()' will be appended to :func: etc. cross-reference text. +add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = "sphinx" + +# A list of ignored prefixes for module index sorting. +modindex_common_prefix = [] + +# If true, keep warnings as "system message" paragraphs in the built documents. +keep_warnings = False + + +# -- 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 = "furo" + +# 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": "", + "logo_only": False, + "display_version": True, + "prev_next_buttons_location": None, + # Toc options + "collapse_navigation": True, + "sticky_navigation": True, + "navigation_depth": 4, + "includehidden": True, + "titles_only": False, +} + +# Add any paths that contain custom themes here, relative to this directory. +# html_theme_path = [] +# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +# html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +# html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. + + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +html_favicon = "_static/PyMuPDF.ico" + +# 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"] +html_theme_options = { + "light_logo": "pymupdf-sidebar-logo-dark.png", + "dark_logo": "pymupdf-sidebar-logo-light.png", +} + +# A list of CSS files. The entry must be a filename string or a tuple containing +# the filename string and the attributes dictionary. The filename must be +# relative to the html_static_path, or a full URI +html_css_files = ["custom.css"] + +# Add any extra paths that contain custom files (such as robots.txt or +# .htaccess) here, relative to this directory. These files are copied +# directly to the root of the documentation. +# html_extra_path = [] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +html_last_updated_fmt = "%d. %b %Y" + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +# html_use_smartypants = False + +# Custom sidebar templates, maps document names to template names. +# html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +html_additional_pages = {} + +# If false, no module index is generated. +html_domain_indices = True + +# If false, no index is generated. +html_use_index = True + +# If true, the index is split into individual pages for each letter. +html_split_index = True + +# If true, links to the reST sources are added to the pages. +html_show_sourcelink = True +html_sourcelink_suffix = ".rst" +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +html_show_sphinx = False + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +html_use_opensearch = "" + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = "PyMuPDF" + + +# -- Options for LaTeX output --------------------------------------------- +latex_elements = { + # "fontpkg": r"\usepackage[sfdefault]{ClearSans} \usepackage[T1]{fontenc}" +} +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [("index", "PyMuPDF.tex", "PyMuPDF Documentation", "Artifex", "manual")] +# The name of an image file (relative to this directory) to place at the top of +# the title page. +latex_logo = "images/pymupdf-logo.png" + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +# latex_use_parts = False + +# If true, show page references after internal links. +latex_show_pagerefs = False + +# If true, show URL addresses after external links. +# latex_show_urls = True +# latex_use_xindy = True +# Documents to append as an appendix to all manuals. +# latex_appendices = [] + +# If false, no module index is generated. +latex_domain_indices = True + +# -- Options for PDF output -------------------------------------------------- +# Grouping the document tree into PDF files. List of tuples +# (source start file, target name, title, author). + +pdf_documents = [("index", "PyMuPDF", "PyMuPDF Manual", "Artifex")] + +# A comma-separated list of custom stylesheets. Example: +# pdf_stylesheets = ["sphinx", "bahnschrift", "a4"] + +# Create a compressed PDF +pdf_compressed = True + +# A colon-separated list of folders to search for fonts. Example: +# pdf_font_path=['/usr/share/fonts', '/usr/share/texmf-dist/fonts/'] + +# Language to be used for hyphenation support +pdf_language = "en_US" + +# If false, no index is generated. +pdf_use_index = True + +# If false, no modindex is generated. +pdf_use_modindex = True + +# If false, no coverpage is generated. +pdf_use_coverpage = True + +pdf_break_level = 2 + +pdf_verbosity = 0 +pdf_invariant = True diff --git a/docs/coop_low.rst b/docs/coop_low.rst new file mode 100644 index 0000000..bd9cfd5 --- /dev/null +++ b/docs/coop_low.rst @@ -0,0 +1,74 @@ +.. include:: header.rst + +.. _cooperation: + +=============================================================== +Working together: DisplayList and TextPage +=============================================================== +Here are some instructions on how to use these classes together. + +In some situations, performance improvements may be achievable, when you fall back to the detail level explained here. + +Create a DisplayList +--------------------- +A :ref:`DisplayList` represents an interpreted document page. Methods for pixmap creation, text extraction and text search are -- behind the curtain -- all using the page's display list to perform their tasks. If a page must be rendered several times (e.g. because of changed zoom levels), or if text search and text extraction should both be performed, overhead can be saved, if the display list is created only once and then used for all other tasks. + +>>> dl = page.get_displaylist() # create the display list + +You can also create display lists for many pages "on stack" (in a list), may be during document open, during idling times, or you store it when a page is visited for the first time (e.g. in GUI scripts). + +Note, that for everything what follows, only the display list is needed -- the corresponding :ref:`Page` object could have been deleted. + +Generate Pixmap +------------------ +The following creates a Pixmap from a :ref:`DisplayList`. Parameters are the same as for :meth:`Page.get_pixmap`. + +>>> pix = dl.get_pixmap() # create the page's pixmap + +The execution time of this statement may be up to 50% shorter than that of :meth:`Page.get_pixmap`. + +Perform Text Search +--------------------- +With the display list from above, we can also search for text. + +For this we need to create a :ref:`TextPage`. + +>>> tp = dl.get_textpage() # display list from above +>>> rlist = tp.search("needle") # look up "needle" locations +>>> for r in rlist: # work with the found locations, e.g. + pix.invert_irect(r.irect) # invert colors in the rectangles + +Extract Text +---------------- +With the same :ref:`TextPage` object from above, we can now immediately use any or all of the 5 text extraction methods. + +.. note:: Above, we have created our text page without argument. This leads to a default argument of 3 (:data:`ligatures` and white-space are preserved), IAW images will **not** be extracted -- see below. + +>>> txt = tp.extractText() # plain text format +>>> json = tp.extractJSON() # json format +>>> html = tp.extractHTML() # HTML format +>>> xml = tp.extractXML() # XML format +>>> xml = tp.extractXHTML() # XHTML format + +Further Performance improvements +--------------------------------- +Pixmap +~~~~~~~ +As explained in the :ref:`Page` chapter: + +If you do not need transparency set *alpha = 0* when creating pixmaps. This will save 25% memory (if RGB, the most common case) and possibly 5% execution time (depending on the GUI software). + +TextPage +~~~~~~~~~ +If you do not need images extracted alongside the text of a page, you can set the following option: + +>>> flags = fitz.TEXT_PRESERVE_LIGATURES | fitz.TEXT_PRESERVE_WHITESPACE +>>> tp = dl.get_textpage(flags) + +This will save ca. 25% overall execution time for the HTML, XHTML and JSON text extractions and **hugely** reduce the amount of storage (both, memory and disk space) if the document is graphics oriented. + +If you however do need images, use a value of 7 for flags: + +>>> flags = fitz.TEXT_PRESERVE_LIGATURES | fitz.TEXT_PRESERVE_WHITESPACE | fitz.TEXT_PRESERVE_IMAGES + +.. include:: footer.rst diff --git a/docs/deprecated.rst b/docs/deprecated.rst new file mode 100644 index 0000000..adba701 --- /dev/null +++ b/docs/deprecated.rst @@ -0,0 +1,217 @@ +.. Deprecated Names: + +* :index:`_isWrapped` -- :attr:`Page.is_wrapped` +* :index:`addCaretAnnot` -- :meth:`Page.add_caret_annot` +* :index:`addCircleAnnot` -- :meth:`Page.add_circle_annot` +* :index:`addFileAnnot` -- :meth:`Page.add_file_annot` +* :index:`addFreetextAnnot` -- :meth:`Page.add_freetext_annot` +* :index:`addHighlightAnnot` -- :meth:`Page.add_highlight_annot` +* :index:`addInkAnnot` -- :meth:`Page.add_ink_annot` +* :index:`addLineAnnot` -- :meth:`Page.add_line_annot` +* :index:`addPolygonAnnot` -- :meth:`Page.add_polygon_annot` +* :index:`addPolylineAnnot` -- :meth:`Page.add_polyline_annot` +* :index:`addRectAnnot` -- :meth:`Page.add_rect_annot` +* :index:`addRedactAnnot` -- :meth:`Page.add_redact_annot` +* :index:`addSquigglyAnnot` -- :meth:`Page.add_squiggly_annot` +* :index:`addStampAnnot` -- :meth:`Page.add_stamp_annot` +* :index:`addStrikeoutAnnot` -- :meth:`Page.add_strikeout_annot` +* :index:`addTextAnnot` -- :meth:`Page.add_text_annot` +* :index:`addUnderlineAnnot` -- :meth:`Page.add_underline_annot` +* :index:`addWidget` -- :meth:`Page.add_widget` +* :index:`chapterCount` -- :attr:`Document.chapter_count` +* :index:`chapterPageCount` -- :meth:`Document.chapter_page_count` +* :index:`cleanContents` -- :meth:`Page.clean_contents` +* :index:`clearWith` -- :meth:`Pixmap.clear_with` +* :index:`convertToPDF` -- :meth:`Document.convert_to_pdf` +* :index:`copyPage` -- :meth:`Document.copy_page` +* :index:`copyPixmap` -- :meth:`Pixmap.copy` +* :index:`CropBox` -- :attr:`Page.cropbox` +* :index:`CropBoxPosition` -- :attr:`Page.cropbox_position` +* :index:`deleteAnnot` -- :meth:`Page.delete_annot` +* :index:`deleteLink` -- :meth:`Page.delete_link` +* :index:`deletePage` -- :meth:`Document.delete_page` +* :index:`deletePageRange` -- :meth:`Document.delete_pages` +* :index:`deleteWidget` -- :meth:`Page.delete_widget` +* :index:`derotationMatrix` -- :attr:`Page.derotation_matrix` +* :index:`drawBezier` -- :meth:`Page.draw_bezier` +* :index:`drawBezier` -- :meth:`Shape.draw_bezier` +* :index:`drawCircle` -- :meth:`Page.draw_circle` +* :index:`drawCircle` -- :meth:`Shape.draw_circle` +* :index:`drawCurve` -- :meth:`Page.draw_curve` +* :index:`drawCurve` -- :meth:`Shape.draw_curve` +* :index:`drawLine` -- :meth:`Page.draw_line` +* :index:`drawLine` -- :meth:`Shape.draw_line` +* :index:`drawOval` -- :meth:`Page.draw_oval` +* :index:`drawOval` -- :meth:`Shape.draw_oval` +* :index:`drawPolyline` -- :meth:`Page.draw_polyline` +* :index:`drawPolyline` -- :meth:`Shape.draw_polyline` +* :index:`drawQuad` -- :meth:`Page.draw_quad` +* :index:`drawQuad` -- :meth:`Shape.draw_quad` +* :index:`drawRect` -- :meth:`Page.draw_rect` +* :index:`drawRect` -- :meth:`Shape.draw_rect` +* :index:`drawSector` -- :meth:`Page.draw_sector` +* :index:`drawSector` -- :meth:`Shape.draw_sector` +* :index:`drawSquiggle` -- :meth:`Page.draw_squiggle` +* :index:`drawSquiggle` -- :meth:`Shape.draw_squiggle` +* :index:`drawZigzag` -- :meth:`Page.draw_zigzag` +* :index:`drawZigzag` -- :meth:`Shape.draw_zigzag` +* :index:`embeddedFileAdd` -- :meth:`Document.embfile_add` +* :index:`embeddedFileCount` -- :meth:`Document.embfile_count` +* :index:`embeddedFileDel` -- :meth:`Document.embfile_del` +* :index:`embeddedFileGet` -- :meth:`Document.embfile_get` +* :index:`embeddedFileInfo` -- :meth:`Document.embfile_info` +* :index:`embeddedFileNames` -- :meth:`Document.embfile_names` +* :index:`embeddedFileUpd` -- :meth:`Document.embfile_upd` +* :index:`extractFont` -- :meth:`Document.extract_font` +* :index:`extractImage` -- :meth:`Document.extract_image` +* :index:`fileGet` -- :meth:`Annot.get_file` +* :index:`fileUpd` -- :meth:`Annot.update_file` +* :index:`fillTextbox` -- :meth:`TextWriter.fill_textbox` +* :index:`findBookmark` -- :meth:`Document.find_bookmark` +* :index:`firstAnnot` -- :attr:`Page.first_annot` +* :index:`firstLink` -- :attr:`Page.first_link` +* :index:`firstWidget` -- :attr:`Page.first_widget` +* :index:`fullcopyPage` -- :meth:`Document.fullcopy_page` +* :index:`gammaWith` -- :meth:`Pixmap.gamma_with` +* :index:`getArea` -- :meth:`Rect.get_area` +* :index:`getArea` -- :meth:`IRect.get_area` +* :index:`getCharWidths` -- :meth:`Document.get_char_widths` +* :index:`getContents` -- :meth:`Page.get_contents` +* :index:`getDisplayList` -- :meth:`Page.get_displaylist` +* :index:`getDrawings` -- :meth:`Page.get_drawings` +* :index:`getFontList` -- :meth:`Page.get_fonts` +* :index:`getImageBbox` -- :meth:`Page.get_image_bbox` +* :index:`getImageData` -- :meth:`Pixmap.tobytes` +* :index:`getImageList` -- :meth:`Page.get_images` +* :index:`getLinks` -- :meth:`Page.get_links` +* :index:`getOCGs` -- :meth:`Document.get_ocgs` +* :index:`getPageFontList` -- :meth:`Document.get_page_fonts` +* :index:`getPageImageList` -- :meth:`Document.get_page_images` +* :index:`getPagePixmap` -- :meth:`Document.get_page_pixmap` +* :index:`getPageText` -- :meth:`Document.get_page_text` +* :index:`getPageXObjectList` -- :meth:`Document.get_page_xobjects` +* :index:`getPDFnow` -- :meth:`get_pdf_now` +* :index:`getPDFstr` -- :meth:`get_pdf_str` +* :index:`getPixmap` -- :meth:`Page.get_pixmap` +* :index:`getPixmap` -- :meth:`Annot.get_pixmap` +* :index:`getPixmap` -- :meth:`DisplayList.get_pixmap` +* :index:`getPNGData` -- :meth:`Pixmap.tobytes` +* :index:`getPNGdata` -- :meth:`Pixmap.tobytes` +* :index:`getRectArea` -- :meth:`Rect.get_area` +* :index:`getRectArea` -- :meth:`IRect.get_area` +* :index:`getSigFlags` -- :meth:`Document.get_sigflags` +* :index:`getSVGimage` -- :meth:`Page.get_svg_image` +* :index:`getText` -- :meth:`Page.get_text` +* :index:`getText` -- :meth:`Annot.get_text` +* :index:`getTextBlocks` -- :meth:`Page.get_text_blocks` +* :index:`getTextbox` -- :meth:`Page.get_textbox` +* :index:`getTextbox` -- :meth:`Annot.get_textbox` +* :index:`getTextLength` -- :meth:`get_text_length` +* :index:`getTextPage` -- :meth:`Page.get_textpage` +* :index:`getTextPage` -- :meth:`Annot.get_textpage` +* :index:`getTextPage` -- :meth:`DisplayList.get_textpage` +* :index:`getTextWords` -- :meth:`Page.get_text_words` +* :index:`getToC` -- :meth:`Document.get_toc` +* :index:`getXmlMetadata` -- :meth:`Document.get_xml_metadata` +* :index:`ImageProperties` -- :meth:`image_properties` +* :index:`includePoint` -- :meth:`Rect.include_point` +* :index:`includePoint` -- :meth:`IRect.include_point` +* :index:`includeRect` -- :meth:`Rect.include_rect` +* :index:`includeRect` -- :meth:`IRect.include_rect` +* :index:`insertFont` -- :meth:`Page.insert_font` +* :index:`insertImage` -- :meth:`Page.insert_image` +* :index:`insertLink` -- :meth:`Page.insert_link` +* :index:`insertPage` -- :meth:`Document.insert_page` +* :index:`insertPDF` -- :meth:`Document.insert_pdf` +* :index:`insertText` -- :meth:`Page.insert_text` +* :index:`insertText` -- :meth:`Shape.insert_text` +* :index:`insertTextbox` -- :meth:`Page.insert_textbox` +* :index:`insertTextbox` -- :meth:`Shape.insert_textbox` +* :index:`invertIRect` -- :meth:`Pixmap.invert_irect` +* :index:`isConvex` -- :attr:`Quad.is_convex` +* :index:`isDirty` -- :attr:`Document.is_dirty` +* :index:`isEmpty` -- :attr:`Rect.is_empty` +* :index:`isEmpty` -- :attr:`IRect.is_empty` +* :index:`isEmpty` -- :attr:`Quad.is_empty` +* :index:`isFormPDF` -- :attr:`Document.is_form_pdf` +* :index:`isInfinite` -- :attr:`Rect.is_infinite` +* :index:`isInfinite` -- :attr:`IRect.is_infinite` +* :index:`isPDF` -- :attr:`Document.is_pdf` +* :index:`isRectangular` -- :attr:`Quad.is_rectangular` +* :index:`isRectilinear` -- :attr:`Matrix.is_rectilinear` +* :index:`isReflowable` -- :attr:`Document.is_reflowable` +* :index:`isRepaired` -- :attr:`Document.is_repaired` +* :index:`isStream` -- :meth:`Document.is_stream` +* :index:`lastLocation` -- :attr:`Document.last_location` +* :index:`lineEnds` -- :attr:`Annot.line_ends` +* :index:`loadAnnot` -- :meth:`Page.load_annot` +* :index:`loadLinks` -- :meth:`Page.load_links` +* :index:`loadPage` -- :meth:`Document.load_page` +* :index:`makeBookmark` -- :meth:`Document.make_bookmark` +* :index:`MediaBox` -- :attr:`Page.mediabox` +* :index:`MediaBoxSize` -- :attr:`Page.mediabox_size` +* :index:`metadataXML` -- :meth:`Document.xref_xml_metadata` +* :index:`movePage` -- :meth:`Document.move_page` +* :index:`needsPass` -- :attr:`Document.needs_pass` +* :index:`newPage` -- :meth:`Document.new_page` +* :index:`newShape` -- :meth:`Page.new_shape` +* :index:`nextLocation` -- :meth:`Document.next_location` +* :index:`pageCount` -- :attr:`Document.page_count` +* :index:`pageCropBox` -- :meth:`Document.page_cropbox` +* :index:`pageXref` -- :meth:`Document.page_xref` +* :index:`PaperRect` -- :meth:`paper_rect` +* :index:`PaperSize` -- :meth:`paper_size` +* :index:`paperSizes` -- :attr:`paper_sizes` +* :index:`PDFCatalog` -- :meth:`Document.pdf_catalog` +* :index:`PDFTrailer` -- :meth:`Document.pdf_trailer` +* :index:`pillowData` -- :meth:`Pixmap.pil_tobytes` +* :index:`pillowWrite` -- :meth:`Pixmap.pil_save` +* :index:`planishLine` -- :meth:`planish_line` +* :index:`preRotate` -- :meth:`Matrix.prerotate` +* :index:`preScale` -- :meth:`Matrix.prescale` +* :index:`preShear` -- :meth:`Matrix.preshear` +* :index:`preTranslate` -- :meth:`Matrix.pretranslate` +* :index:`previousLocation` -- :meth:`Document.prev_location` +* :index:`readContents` -- :meth:`Page.read_contents` +* :index:`resolveLink` -- :meth:`Document.resolve_link` +* :index:`rotationMatrix` -- :attr:`Page.rotation_matrix` +* :index:`searchFor` -- :meth:`Page.search_for` +* :index:`searchPageFor` -- :meth:`Document.search_page_for` +* :index:`setAlpha` -- :meth:`Pixmap.set_alpha` +* :index:`setBlendMode` -- :meth:`Annot.set_blendmode` +* :index:`setBorder` -- :meth:`Annot.set_border` +* :index:`setColors` -- :meth:`Annot.set_colors` +* :index:`setCropBox` -- :meth:`Page.set_cropbox` +* :index:`setFlags` -- :meth:`Annot.set_flags` +* :index:`setInfo` -- :meth:`Annot.set_info` +* :index:`setLanguage` -- :meth:`Document.set_language` +* :index:`setLineEnds` -- :meth:`Annot.set_line_ends` +* :index:`setMediaBox` -- :meth:`Page.set_mediabox` +* :index:`setMetadata` -- :meth:`Document.set_metadata` +* :index:`setName` -- :meth:`Annot.set_name` +* :index:`setOC` -- :meth:`Annot.set_oc` +* :index:`setOpacity` -- :meth:`Annot.set_opacity` +* :index:`setOrigin` -- :meth:`Pixmap.set_origin` +* :index:`setPixel` -- :meth:`Pixmap.set_pixel` +* :index:`setRect` -- :meth:`Annot.set_rect` +* :index:`setRect` -- :meth:`Pixmap.set_rect` +* :index:`setResolution` -- :meth:`Pixmap.set_dpi` +* :index:`setRotation` -- :meth:`Page.set_rotation` +* :index:`setToC` -- :meth:`Document.set_toc` +* :index:`setXmlMetadata` -- :meth:`Document.set_xml_metadata` +* :index:`showPDFpage` -- :meth:`Page.show_pdf_page` +* :index:`soundGet` -- :meth:`Annot.get_sound` +* :index:`tintWith` -- :meth:`Pixmap.tint_with` +* :index:`transformationMatrix` -- :attr:`Page.transformation_matrix` +* :index:`updateLink` -- :meth:`Page.update_link` +* :index:`updateObject` -- :meth:`Document.update_object` +* :index:`updateStream` -- :meth:`Document.update_stream` +* :index:`wrapContents` -- :meth:`Page.wrap_contents` +* :index:`writeImage` -- :meth:`Pixmap.save` +* :index:`writePNG` -- :meth:`Pixmap.save` +* :index:`writeText` -- :meth:`Page.write_text` +* :index:`writeText` -- :meth:`TextWriter.write_text` +* :index:`xrefLength` -- :meth:`Document.xref_length` +* :index:`xrefObject` -- :meth:`Document.xref_object` +* :index:`xrefStream` -- :meth:`Document.xref_stream` +* :index:`xrefStreamRaw` -- :meth:`Document.xref_stream_raw` diff --git a/docs/device.rst b/docs/device.rst new file mode 100644 index 0000000..3f26416 --- /dev/null +++ b/docs/device.rst @@ -0,0 +1,34 @@ +.. include:: header.rst + +.. _Device: + +================ +Device +================ + +The different format handlers (pdf, xps, etc.) interpret pages to a "device". Devices are the basis for everything that can be done with a page: rendering, text extraction and searching. The device type is determined by the selected construction method. + +**Class API** + +.. class:: Device + + .. method:: __init__(self, object, clip) + + Constructor for either a pixel map or a display list device. + + :arg object: either a *Pixmap* or a *DisplayList*. + :type object: :ref:`Pixmap` or :ref:`DisplayList` + + :arg clip: An optional `IRect` for *Pixmap* devices to restrict rendering to a certain area of the page. If the complete page is required, specify *None*. For display list devices, this parameter must be omitted. + :type clip: :ref:`IRect` + + .. method:: __init__(self, textpage, flags=0) + + Constructor for a text page device. + + :arg textpage: *TextPage* object + :type textpage: :ref:`TextPage` + + :arg int flags: control the way how text is parsed into the text page. Currently 3 options can be coded into this parameter, see :ref:`TextPreserve`. To set these options use something like *flags=0 | TEXT_PRESERVE_LIGATURES | ...*. + +.. include:: footer.rst diff --git a/docs/displaylist.rst b/docs/displaylist.rst new file mode 100644 index 0000000..8fcce83 --- /dev/null +++ b/docs/displaylist.rst @@ -0,0 +1,96 @@ +.. include:: header.rst + +.. _DisplayList: + +================ +DisplayList +================ + +DisplayList is a list containing drawing commands (text, images, etc.). The intent is two-fold: + +1. as a caching-mechanism to reduce parsing of a page +2. as a data structure in multi-threading setups, where one thread parses the page and another one renders pages. This aspect is currently not supported by PyMuPDF. + +A display list is populated with objects from a page, usually by executing :meth:`Page.get_displaylist`. There also exists an independent constructor. + +"Replay" the list (once or many times) by invoking one of its methods :meth:`~DisplayList.run`, :meth:`~DisplayList.get_pixmap` or :meth:`~DisplayList.get_textpage`. + + +================================= ============================================ +**Method** **Short Description** +================================= ============================================ +:meth:`~DisplayList.run` Run a display list through a device. +:meth:`~DisplayList.get_pixmap` generate a pixmap +:meth:`~DisplayList.get_textpage` generate a text page +:attr:`~DisplayList.rect` mediabox of the display list +================================= ============================================ + + +**Class API** + +.. class:: DisplayList + + .. method:: __init__(self, mediabox) + + Create a new display list. + + :arg mediabox: The page's rectangle. + :type mediabox: :ref:`Rect` + + :rtype: *DisplayList* + + .. method:: run(device, matrix, area) + + Run the display list through a device. The device will populate the display list with its "commands" (i.e. text extraction or image creation). The display list can later be used to "read" a page many times without having to re-interpret it from the document file. + + You will most probably instead use one of the specialized run methods below -- :meth:`get_pixmap` or :meth:`get_textpage`. + + :arg device: Device + :type device: :ref:`Device` + + :arg matrix: Transformation matrix to apply to the display list contents. + :type matrix: :ref:`Matrix` + + :arg area: Only the part visible within this area will be considered when the list is run through the device. + :type area: :ref:`Rect` + + .. index:: + pair: matrix; DisplayList.get_pixmap + pair: colorspace; DisplayList.get_pixmap + pair: clip; DisplayList.get_pixmap + pair: alpha; DisplayList.get_pixmap + + .. method:: get_pixmap(matrix=fitz.Identity, colorspace=fitz.csRGB, alpha=0, clip=None) + + Run the display list through a draw device and return a pixmap. + + :arg matrix: matrix to use. Default is the identity matrix. + :type matrix: :ref:`Matrix` + + :arg colorspace: the desired colorspace. Default is RGB. + :type colorspace: :ref:`Colorspace` + + :arg int alpha: determine whether or not (0, default) to include a transparency channel. + + :arg irect_like clip: restrict rendering to the intersection of this area with :attr:`DisplayList.rect`. + + :rtype: :ref:`Pixmap` + :returns: pixmap of the display list. + + .. method:: get_textpage(flags) + + Run the display list through a text device and return a text page. + + :arg int flags: control which information is parsed into a text page. Default value in PyMuPDF is `3 = TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE`, i.e. :data:`ligatures` are **passed through**, white spaces are **passed through** (not translated to spaces), and images are **not included**. See :ref:`TextPreserve`. + + :rtype: :ref:`TextPage` + :returns: text page of the display list. + + .. attribute:: rect + + Contains the display list's mediabox. This will equal the page's rectangle if it was created via :meth:`Page.get_displaylist`. + + :type: :ref:`Rect` + + +.. include:: footer.rst diff --git a/docs/document-writer-class.rst b/docs/document-writer-class.rst new file mode 100644 index 0000000..515d50c --- /dev/null +++ b/docs/document-writer-class.rst @@ -0,0 +1,57 @@ +.. include:: header.rst + +.. _DocumentWriter: + +================ +DocumentWriter +================ + +* New in v1.21.0 + +This class represents a utility which can output various :ref:`document types supported by MuPDF`. + +In PyMuPDF only used for outputting PDF documents whose pages are populated by :ref:`Story` DOMs. + +Using DocumentWriter_ also for other document types might happen in the future. + +================================= =================================================== +**Method / Attribute** **Short Description** +================================= =================================================== +:meth:`DocumentWriter.begin_page` start a new output page +:meth:`DocumentWriter.end_page` finish the current output page +:meth:`DocumentWriter.close` flush pending output and close the file +================================= =================================================== + +**Class API** + +.. class:: DocumentWriter + + .. method:: __init__(self, path, options=None) + + Create a document writer object, passing a Python file pointer or a file path. Options to use when saving the file may also be passed. + + This class can also be used as a Python context manager. + + :arg path: the output file. This may be a string file name, or any Python file pointer. + + .. note:: By using a `io.BytesIO()` object as file pointer, a document writer can create a PDF in memory. Subsequently, this PDF can be re-opened for input and be further manipulated. This technique is used by several example scripts in :ref:`Stories recipes`. + + :arg str options: specify saving options for the output PDF. Typical are "compress" or "clean". More possible values may be taken from help output of the `mutool convert` CLI utility. + + .. method:: begin_page(mediabox) + + Start a new output page of a given dimension. + + :arg rect_like mediabox: a rectangle specifying the page size. After this method, output operations may write content to the page. + + .. method:: end_page() + + Finish a page. This flushes any pending data and appends the page to the output document. + + .. method:: close() + + Close the output file. This method is required for writing any pending data. + + For usage examples consult the section of :ref:`Story`. + +.. include:: footer.rst diff --git a/docs/document.rst b/docs/document.rst new file mode 100644 index 0000000..675a6bb --- /dev/null +++ b/docs/document.rst @@ -0,0 +1,2107 @@ +.. include:: header.rst + +.. _Document: + +================ +Document +================ + +.. highlight:: python + +This class represents a document. It can be constructed from a file or from memory. + +There exists the alias *open* for this class, i.e. `fitz.Document(...)` and `fitz.open(...)` do exactly the same thing. + +For details on **embedded files** refer to Appendix 3. + +.. note:: + + Starting with v1.17.0, a new page addressing mechanism for **EPUB files only** is supported. This document type is internally organized in chapters such that pages can most efficiently be found by their so-called "location". The location is a tuple *(chapter, pno)* consisting of the chapter number and the page number **in that chapter**. Both numbers are zero-based. + + While it is still possible to locate a page via its (absolute) number, doing so may mean that the complete EPUB document must be laid out before the page can be addressed. This may have a significant performance impact if the document is very large. Using the page's *(chapter, pno)* prevents this from happening. + + To maintain a consistent API, PyMuPDF supports the page *location* syntax for **all file types** -- documents without this feature simply have just one chapter. :meth:`Document.load_page` and the equivalent index access now also support a *location* argument. + + There are a number of methods for converting between page numbers and locations, for determining the chapter count, the page count per chapter, for computing the next and the previous locations, and the last page location of a document. + +======================================= ========================================================== +**Method / Attribute** **Short Description** +======================================= ========================================================== +:meth:`Document.add_layer` PDF only: make new optional content configuration +:meth:`Document.add_ocg` PDF only: add new optional content group +:meth:`Document.authenticate` gain access to an encrypted document +:meth:`Document.can_save_incrementally` check if incremental save is possible +:meth:`Document.chapter_page_count` number of pages in chapter +:meth:`Document.close` close the document +:meth:`Document.convert_to_pdf` write a PDF version to memory +:meth:`Document.copy_page` PDF only: copy a page reference +:meth:`Document.del_toc_item` PDF only: remove a single TOC item +:meth:`Document.delete_page` PDF only: delete a page +:meth:`Document.delete_pages` PDF only: delete multiple pages +:meth:`Document.embfile_add` PDF only: add a new embedded file from buffer +:meth:`Document.embfile_count` PDF only: number of embedded files +:meth:`Document.embfile_del` PDF only: delete an embedded file entry +:meth:`Document.embfile_get` PDF only: extract an embedded file buffer +:meth:`Document.embfile_info` PDF only: metadata of an embedded file +:meth:`Document.embfile_names` PDF only: list of embedded files +:meth:`Document.embfile_upd` PDF only: change an embedded file +:meth:`Document.extract_font` PDF only: extract a font by :data:`xref` +:meth:`Document.extract_image` PDF only: extract an embedded image by :data:`xref` +:meth:`Document.ez_save` PDF only: :meth:`Document.save` with different defaults +:meth:`Document.find_bookmark` retrieve page location after laid out document +:meth:`Document.fullcopy_page` PDF only: duplicate a page +:meth:`Document.get_layer` PDF only: lists of OCGs in ON, OFF, RBGroups +:meth:`Document.get_layers` PDF only: list of optional content configurations +:meth:`Document.get_oc` PDF only: get OCG /OCMD xref of image / form xobject +:meth:`Document.get_ocgs` PDF only: info on all optional content groups +:meth:`Document.get_ocmd` PDF only: retrieve definition of an :data:`OCMD` +:meth:`Document.get_page_fonts` PDF only: list of fonts referenced by a page +:meth:`Document.get_page_images` PDF only: list of images referenced by a page +:meth:`Document.get_page_labels` PDF only: list of page label definitions +:meth:`Document.get_page_numbers` PDF only: get page numbers having a given label +:meth:`Document.get_page_pixmap` create a pixmap of a page by page number +:meth:`Document.get_page_text` extract the text of a page by page number +:meth:`Document.get_page_xobjects` PDF only: list of XObjects referenced by a page +:meth:`Document.get_sigflags` PDF only: determine signature state +:meth:`Document.get_toc` extract the table of contents +:meth:`Document.get_xml_metadata` PDF only: read the XML metadata +:meth:`Document.has_annots` PDF only: check if PDF contains any annots +:meth:`Document.has_links` PDF only: check if PDF contains any links +:meth:`Document.insert_page` PDF only: insert a new page +:meth:`Document.insert_pdf` PDF only: insert pages from another PDF +:meth:`Document.insert_file` PDF only: insert pages from arbitrary document +:meth:`Document.journal_can_do` PDF only: which journal actions are possible +:meth:`Document.journal_enable` PDF only: enables journalling for the document +:meth:`Document.journal_load` PDF only: load journal from a file +:meth:`Document.journal_op_name` PDF only: return name of a journalling step +:meth:`Document.journal_position` PDF only: return journalling status +:meth:`Document.journal_redo` PDF only: redo current operation +:meth:`Document.journal_save` PDF only: save journal to a file +:meth:`Document.journal_start_op` PDF only: start an "operation" giving it a name +:meth:`Document.journal_stop_op` PDF only: end current operation +:meth:`Document.journal_undo` PDF only: undo current operation +:meth:`Document.layer_ui_configs` PDF only: list of optional content intents +:meth:`Document.layout` re-paginate the document (if supported) +:meth:`Document.load_page` read a page +:meth:`Document.make_bookmark` create a page pointer in reflowable documents +:meth:`Document.move_page` PDF only: move a page to different location in doc +:meth:`Document.need_appearances` PDF only: get/set `/NeedAppearances` property +:meth:`Document.new_page` PDF only: insert a new empty page +:meth:`Document.next_location` return (chapter, pno) of following page +:meth:`Document.outline_xref` PDF only: :data:`xref` a TOC item +:meth:`Document.page_cropbox` PDF only: the unrotated page rectangle +:meth:`Document.page_xref` PDF only: :data:`xref` of a page number +:meth:`Document.pages` iterator over a page range +:meth:`Document.pdf_catalog` PDF only: :data:`xref` of catalog (root) +:meth:`Document.pdf_trailer` PDF only: trailer source +:meth:`Document.prev_location` return (chapter, pno) of preceding page +:meth:`Document.reload_page` PDF only: provide a new copy of a page +:meth:`Document.save` PDF only: save the document +:meth:`Document.saveIncr` PDF only: save the document incrementally +:meth:`Document.scrub` PDF only: remove sensitive data +:meth:`Document.search_page_for` search for a string on a page +:meth:`Document.select` PDF only: select a subset of pages +:meth:`Document.set_layer_ui_config` PDF only: set OCG visibility temporarily +:meth:`Document.set_layer` PDF only: mass changing OCG states +:meth:`Document.set_markinfo` PDF only: set the MarkInfo values +:meth:`Document.set_metadata` PDF only: set the metadata +:meth:`Document.set_oc` PDF only: attach OCG/OCMD to image / form xobject +:meth:`Document.set_ocmd` PDF only: create or update an :data:`OCMD` +:meth:`Document.set_page_labels` PDF only: add/update page label definitions +:meth:`Document.set_pagemode` PDF only: set the PageMode +:meth:`Document.set_pagelayout` PDF only: set the PageLayout +:meth:`Document.set_toc_item` PDF only: change a single TOC item +:meth:`Document.set_toc` PDF only: set the table of contents (TOC) +:meth:`Document.set_xml_metadata` PDF only: create or update document XML metadata +:meth:`Document.subset_fonts` PDF only: create font subsets +:meth:`Document.switch_layer` PDF only: activate OC configuration +:meth:`Document.tobytes` PDF only: writes document to memory +:meth:`Document.xref_copy` PDF only: copy a PDF dictionary to another :data:`xref` +:meth:`Document.xref_get_key` PDF only: get the value of a dictionary key +:meth:`Document.xref_get_keys` PDF only: list the keys of object at :data:`xref` +:meth:`Document.xref_object` PDF only: get the definition source of :data:`xref` +:meth:`Document.xref_set_key` PDF only: set the value of a dictionary key +:meth:`Document.xref_stream_raw` PDF only: raw stream source at :data:`xref` +:meth:`Document.xref_xml_metadata` PDF only: :data:`xref` of XML metadata +:attr:`Document.chapter_count` number of chapters +:attr:`Document.FormFonts` PDF only: list of global widget fonts +:attr:`Document.is_closed` has document been closed? +:attr:`Document.is_dirty` PDF only: has document been changed yet? +:attr:`Document.is_encrypted` document (still) encrypted? +:attr:`Document.is_fast_webaccess` is PDF linearized? +:attr:`Document.is_form_pdf` is this a Form PDF? +:attr:`Document.is_pdf` is this a PDF? +:attr:`Document.is_reflowable` is this a reflowable document? +:attr:`Document.is_repaired` PDF only: has this PDF been repaired during open? +:attr:`Document.last_location` (chapter, pno) of last page +:attr:`Document.metadata` metadata +:attr:`Document.markinfo` PDF MarkInfo value +:attr:`Document.name` filename of document +:attr:`Document.needs_pass` require password to access data? +:attr:`Document.outline` first `Outline` item +:attr:`Document.page_count` number of pages +:attr:`Document.permissions` permissions to access the document +:attr:`Document.pagemode` PDF PageMode value +:attr:`Document.pagelayout` PDF PageLayout value +:attr:`Document.version_count` PDF count of versions +======================================= ========================================================== + +**Class API** + +.. class:: Document + + .. index:: + pair: filename; open + pair: stream; open + pair: filetype; open + pair: rect; open + pair: width; open + pair: height; open + pair: fontsize; open + pair: open; Document + pair: filename; Document + pair: stream; Document + pair: filetype; Document + pair: rect; Document + pair: fontsize; Document + + .. method:: __init__(self, filename=None, stream=None, *, filetype=None, rect=None, width=0, height=0, fontsize=11) + + * Changed in v1.14.13: support `io.BytesIO` for memory documents. + * Changed in v1.19.6: Clearer, shorter and more consistent exception messages. File type "pdf" is always assumed if not specified. Empty files and memory areas will always lead to exceptions. + + Creates a *Document* object. + + * With default parameters, a **new empty PDF** document will be created. + * If *stream* is given, then the document is created from memory and, if not a PDF, either *filename* or *filetype* must indicate its type. + * If *stream* is `None`, then a document is created from the file given by *filename*. Its type is inferred from the extension. This can be overruled by *filetype.* + + :arg str,pathlib filename: A UTF-8 string or *pathlib* object containing a file path. The document type is inferred from the filename extension. If not present or not matching :ref:`a supported type`, a PDF document is assumed. For memory documents, this argument may be used instead of `filetype`, see below. + + :arg bytes,bytearray,BytesIO stream: A memory area containing a supported document. If not a PDF, its type **must** be specified by either `filename` or `filetype`. + + :arg str filetype: A string specifying the type of document. This may be anything looking like a filename (e.g. "x.pdf"), in which case MuPDF uses the extension to determine the type, or a mime type like *application/pdf*. Just using strings like "pdf" or ".pdf" will also work. May be omitted for PDF documents, otherwise must match :ref:`a supported document type`. + + :arg rect_like rect: a rectangle specifying the desired page size. This parameter is only meaningful for documents with a variable page layout ("reflowable" documents), like e-books or HTML, and ignored otherwise. If specified, it must be a non-empty, finite rectangle with top-left coordinates (0, 0). Together with parameter *fontsize*, each page will be accordingly laid out and hence also determine the number of pages. + + :arg float width: may used together with *height* as an alternative to *rect* to specify layout information. + + :arg float height: may used together with *width* as an alternative to *rect* to specify layout information. + + :arg float fontsize: the default fontsize for reflowable document types. This parameter is ignored if none of the parameters *rect* or *width* and *height* are specified. Will be used to calculate the page layout. + + :raises TypeError: if the *type* of any parameter does not conform. + :raises FileNotFoundError: if the file / path cannot be found. Re-implemented as subclass of `RuntimeError`. + :raises EmptyFileError: if the file / path is empty or the `bytes` object in memory has zero length. A subclass of `FileDataError` and `RuntimeError`. + :raises ValueError: if an unknown file type is explicitly specified. + :raises FileDataError: if the document has an invalid structure for the given type -- or is no file at all (but e.g. a folder). A subclass of `RuntimeError`. + + :return: A document object. If the document cannot be created, an exception is raised in the above sequence. Note that PyMuPDF-specific exceptions, `FileNotFoundError`, `EmptyFileError` and `FileDataError` are intercepted if you check for `RuntimeError`. + + In case of problems you can see more detail in the internal messages store: `print(fitz.TOOLS.mupdf_warnings())` (which will be emptied by this call, but you can also prevent this -- consult :meth:`Tools.mupdf_warnings`). + + .. note:: Not all document types are checked for valid formats already at open time. Raster images for example will raise exceptions only later, when trying to access the content. Other types (notably with non-binary content) may also be opened (and sometimes **accessed**) successfully -- sometimes even when having invalid content for the format: + + * HTM, HTML, XHTML: **always** opened, `metadata["format"]` is "HTML5", resp. "XHTML". + * XML, FB2: **always** opened, `metadata["format"]` is "FictionBook2". + + Overview of possible forms, note: `open` is a synonym of `Document`:: + + >>> # from a file + >>> doc = fitz.open("some.xps") + >>> # handle wrong extension + >>> doc = fitz.open("some.file", filetype="xps") + >>> + >>> # from memory, filetype is required if not a PDF + >>> doc = fitz.open("xps", mem_area) + >>> doc = fitz.open(None, mem_area, "xps") + >>> doc = fitz.open(stream=mem_area, filetype="xps") + >>> + >>> # new empty PDF + >>> doc = fitz.open() + >>> doc = fitz.open(None) + >>> doc = fitz.open("") + + .. note:: Raster images with a wrong (but supported) file extension **are no problem**. MuPDF will determine the correct image type when file **content** is actually accessed and will process it without complaint. So `fitz.open("file.jpg")` will work even for a PNG image. + + The Document class can be also be used as a **context manager**. On exit, the document will automatically be closed. + + >>> import fitz + >>> with fitz.open(...) as doc: + for page in doc: print("page %i" % page.number) + page 0 + page 1 + page 2 + page 3 + >>> doc.is_closed + True + >>> + + + .. method:: get_oc(xref) + + * New in v1.18.4 + + Return the cross reference number of an :data:`OCG` or :data:`OCMD` attached to an image or form xobject. + + :arg int xref: the :data:`xref` of an image or form xobject. Valid such cross reference numbers are returned by :meth:`Document.get_page_images`, resp. :meth:`Document.get_page_xobjects`. For invalid numbers, an exception is raised. + :rtype: int + :returns: the cross reference number of an optional contents object or zero if there is none. + + .. method:: set_oc(xref, ocxref) + + * New in v1.18.4 + + If *xref* represents an image or form xobject, set or remove the cross reference number *ocxref* of an optional contents object. + + :arg int xref: the :data:`xref` of an image or form xobject [#f5]_. Valid such cross reference numbers are returned by :meth:`Document.get_page_images`, resp. :meth:`Document.get_page_xobjects`. For invalid numbers, an exception is raised. + :arg int ocxref: the :data:`xref` number of an :data:`OCG` / :data:`OCMD`. If not zero, an invalid reference raises an exception. If zero, any OC reference is removed. + + + .. method:: get_layers() + + * New in v1.18.3 + + Show optional layer configurations. There always is a standard one, which is not included in the response. + + >>> for item in doc.get_layers(): print(item) + {'number': 0, 'name': 'my-config', 'creator': ''} + >>> # use 'number' as config identifier in add_ocg + + .. method:: add_layer(name, creator=None, on=None) + + * New in v1.18.3 + + Add an optional content configuration. Layers serve as a collection of ON / OFF states for optional content groups and allow fast visibility switches between different views on the same document. + + :arg str name: arbitrary name. + :arg str creator: (optional) creating software. + :arg sequ on: a sequence of OCG :data:`xref` numbers which should be set to ON when this layer gets activated. All OCGs not listed here will be set to OFF. + + + .. method:: switch_layer(number, as_default=False) + + * New in v1.18.3 + + Switch to a document view as defined by the optional layer's configuration number. This is temporary, except if established as default. + + :arg int number: config number as returned by :meth:`Document.layer_configs`. + :arg bool as_default: make this the default configuration. + + Activates the ON / OFF states of OCGs as defined in the identified layer. If *as_default=True*, then additionally all layers, including the standard one, are merged and the result is written back to the standard layer, and **all optional layers are deleted**. + + + .. method:: add_ocg(name, config=-1, on=True, intent="View", usage="Artwork") + + * New in v1.18.3 + + Add an optional content group. An OCG is the most important unit of information to determine object visibility. For a PDF, in order to be regarded as having optional content, at least one OCG must exist. + + :arg str name: arbitrary name. Will show up in supporting PDF viewers. + :arg int config: layer configuration number. Default -1 is the standard configuration. + :arg bool on: standard visibility status for objects pointing to this OCG. + :arg str,list intent: a string or list of strings declaring the visibility intents. There are two PDF standard values to choose from: "View" and "Design". Default is "View". Correct **spelling is important**. + :arg str usage: another influencer for OCG visibility. This will become part of the OCG's `/Usage` key. There are two PDF standard values to choose from: "Artwork" and "Technical". Default is "Artwork". Please only change when required. + + :returns: :data:`xref` of the created OCG. Use as entry for `oc` parameter in supporting objects. + + .. note:: Multiple OCGs with identical parameters may be created. This will not cause problems. Garbage option 3 of :meth:`Document.save` will get rid of any duplicates. + + + .. method:: set_ocmd(xref=0, ocgs=None, policy="AnyOn", ve=None) + + * New in v1.18.4 + + Create or update an :data:`OCMD`, **Optional Content Membership Dictionary.** + + :arg int xref: :data:`xref` of the OCMD to be updated, or 0 for a new OCMD. + :arg list ocgs: a sequence of :data:`xref` numbers of existing :data:`OCG` PDF objects. + :arg str policy: one of "AnyOn" (default), "AnyOff", "AllOn", "AllOff" (mixed or lower case). + :arg list ve: a "visibility expression". This is a list of arbitrarily nested other lists -- see explanation below. Use as an alternative to the combination *ocgs* / *policy* if you need to formulate more complex conditions. + :rtype: int + :returns: :data:`xref` of the OCMD. Use as `oc=xref` parameter in supporting objects, and respectively in :meth:`Document.set_oc` or :meth:`Annot.set_oc`. + + .. note:: + + Like an OCG, an OCMD has a visibility state ON or OFF, and it can be used like an OCG. In contrast to an OCG, the OCMD state is determined by evaluating the state of one or more OCGs via special forms of **boolean expressions.** If the expression evaluates to true, the OCMD state is ON and OFF for false. + + There are two ways to formulate OCMD visibility: + + 1. Use the combination of *ocgs* and *policy*: The *policy* value is interpreted as follows: + + - AnyOn -- (default) true if at least one OCG is ON. + - AnyOff -- true if at least one OCG is OFF. + - AllOn -- true if all OCGs are ON. + - AllOff -- true if all OCGs are OFF. + + Suppose you want two PDF objects be displayed exactly one at a time (if one is ON, then the other one must be OFF): + + Solution: use an **OCG** for object 1 and an **OCMD** for object 2. Create the OCMD via `set_ocmd(ocgs=[xref], policy="AllOff")`, with the :data:`xref` of the OCG. + + 2. Use the **visibility expression** *ve*: This is a list of two or more items. The **first item** is a logical keyword: one of the strings **"and"**, **"or"**, or **"not"**. The **second** and all subsequent items must either be an integer or another list. An integer must be the :data:`xref` number of an OCG. A list must again have at least two items starting with one of the boolean keywords. This syntax is a bit awkward, but quite powerful: + + - Each list must start with a logical keyword. + - If the keyword is a **"not"**, then the list must have exactly two items. If it is **"and"** or **"or"**, any number of other items may follow. + - Items following the logical keyword may be either integers or again a list. An *integer* must be the xref of an OCG. A *list* must conform to the previous rules. + + **Examples:** + + - `set_ocmd(ve=["or", 4, ["not", 5], ["and", 6, 7]])`. This delivers ON if the following is true: **"4 is ON, or 5 is OFF, or 6 and 7 are both ON"**. + - `set_ocmd(ve=["not", xref])`. This has the same effect as the OCMD example created under 1. + + For more details and examples see page 224 of :ref:`AdobeManual`. Also do have a look at example scripts `here `_. + + Visibility expressions, `/VE`, are part of PDF specification version 1.6. So not all PDF viewers / readers may already support this feature and hence will react in some standard way for those cases. + + + .. method:: get_ocmd(xref) + + * New in v1.18.4 + + Retrieve the definition of an :data:`OCMD`. + + :arg int xref: the :data:`xref` of the OCMD. + :rtype: dict + :returns: a dictionary with the keys *xref*, *ocgs*, *policy* and *ve*. + + + .. method:: get_layer(config=-1) + + * New in v1.18.3 + + List of optional content groups by status in the specified configuration. This is a dictionary with lists of cross reference numbers for OCGs that occur in the arrays `/ON`, `/OFF` or in some radio button group (`/RBGroups`). + + :arg int config: the configuration layer (default is the standard config layer). + + >>> pprint(doc.get_layer()) + {'off': [8, 9, 10], 'on': [5, 6, 7], 'rbgroups': [[7, 10]]} + >>> + + .. method:: set_layer(config, *, on=None, off=None, basestate=None, rbgroups=None, locked=None) + + * New in v1.18.3 + + * Changed in v1.22.4: Support list of *locked* OCGs. + + Mass status changes of optional content groups. **Permanently** sets the status of OCGs. + + :arg int config: desired configuration layer, choose -1 for the default one. + :arg list on: list of :data:`xref` of OCGs to set ON. Replaces previous values. An empty list will cause no OCG being set to ON anymore. Should be specified if `basestate="ON"` is used. + :arg list off: list of :data:`xref` of OCGs to set OFF. Replaces previous values. An empty list will cause no OCG being set to OFF anymore. Should be specified if `basestate="OFF"` is used. + :arg str basestate: state of OCGs that are not mentioned in *on* or *off*. Possible values are "ON", "OFF" or "Unchanged". Upper / lower case possible. + :arg list rbgroups: a list of lists. Replaces previous values. Each sublist should contain two or more OCG xrefs. OCGs in the same sublist are handled like buttons in a radio button group: setting one to ON automatically sets all other group members to OFF. + :arg list locked: a list of OCG xref number that cannot be changed by the user interface. + + Values `None` will not change the corresponding PDF array. + + >>> doc.set_layer(-1, basestate="OFF") # only changes the base state + >>> pprint(doc.get_layer()) + {'basestate': 'OFF', 'off': [8, 9, 10], 'on': [5, 6, 7], 'rbgroups': [[7, 10]]} + + + .. method:: get_ocgs() + + * New in v1.18.3 + + Details of all optional content groups. This is a dictionary of dictionaries like this (key is the OCG's :data:`xref`): + + >>> pprint(doc.get_ocgs()) + {13: {'on': True, + 'intent': ['View', 'Design'], + 'name': 'Circle', + 'usage': 'Artwork'}, + 14: {'on': True, + 'intent': ['View', 'Design'], + 'name': 'Square', + 'usage': 'Artwork'}, + 15: {'on': False, 'intent': ['View'], 'name': 'Square', 'usage': 'Artwork'}} + >>> + + .. method:: layer_ui_configs() + + * New in v1.18.3 + + Show the visibility status of optional content that is modifiable by the user interface of supporting PDF viewers. + + * Only reports items contained in the currently selected layer configuration. + + * The meaning of the dictionary keys is as follows: + - *depth:* item's nesting level in the `/Order` array + - *locked:* true if cannot be changed via user interfaces + - *number:* running sequence number + - *on:* item state + - *text:* text string or name field of the originating OCG + - *type:* one of "label" (set by a text string), "checkbox" (set by a single OCG) or "radiobox" (set by a set of connected OCGs) + + .. method:: set_layer_ui_config(number, action=0) + + * New in v1.18.3 + + Modify OC visibility status of content groups. This is analog to what supporting PDF viewers would offer. + + Please note that visibility is **not** a property stored with the OCG. It is not even information necessarily present in the PDF document at all. Instead, the current visibility is **temporarily** set using the user interface of some supporting PDF consumer software. The same type of functionality is offered by this method. + + To make **permanent** changes, use :meth:`Document.set_layer`. + + :arg int,str number: either the sequence number of the item in list :meth:`Document.layer_configs` or the "text" of one of these items. + :arg int action: `PDF_OC_ON` = set on (default), `PDF_OC_TOGGLE` = toggle on/off, `PDF_OC_OFF` = set off. + + + .. method:: authenticate(password) + + Decrypts the document with the string *password*. If successful, document data can be accessed. For PDF documents, the "owner" and the "user" have different privileges, and hence different passwords may exist for these authorization levels. The method will automatically establish the appropriate (owner or user) access rights for the provided password. + + :arg str password: owner or user password. + + :rtype: int + :returns: a positive value if successful, zero otherwise (the string does not match either password). If positive, the indicator :attr:`Document.is_encrypted` is set to *False*. **Positive** return codes carry the following information detail: + + * 1 => authenticated, but the PDF has neither owner nor user passwords. + * 2 => authenticated with the **user** password. + * 4 => authenticated with the **owner** password. + * 6 => authenticated and both passwords are equal -- probably a rare situation. + + .. note:: + + The document may be protected by an owner, but **not** by a user password. Detect this situation via `doc.authenticate("") == 2`. This allows opening and reading the document without authentication, but, depending on the :attr:`Document.permissions` value, other actions may be prohibited. PyMuPDF (like MuPDF) in this case **ignores those restrictions**. So, -- in contrast to any PDF viewers -- you can for example extract text and add or modify content, even if the respective permission flags `PDF_PERM_COPY`, `PDF_PERM_MODIFY`, `PDF_PERM_ANNOTATE`, etc. are set off! It is your responsibility building a legally compliant application where applicable. + + .. method:: get_page_numbers(label, only_one=False) + + * New in v 1.18.6 + + PDF only: Return a list of page numbers that have the specified label -- note that labels may not be unique in a PDF. This implies a sequential search through **all page numbers** to compare their labels. + + .. note:: Implementation detail -- pages are **not loaded** for this purpose. + + :arg str label: the label to look for, e.g. "vii" (Roman number 7). + :arg bool only_one: stop after first hit. Useful e.g. if labelling is known to be unique, or there are many pages, etc. The default will check every page number. + :rtype: list + :returns: list of page numbers that have this label. Empty if none found, no labels defined, etc. + + + .. method:: get_page_labels() + + * New in v1.18.7 + + PDF only: Extract the list of page label definitions. Typically used for modifications before feeding it into :meth:`Document.set_page_labels`. + + :returns: a list of dictionaries as defined in :meth:`Document.set_page_labels`. + + .. method:: set_page_labels(labels) + + * New in v1.18.6 + + PDF only: Add or update the page label definitions of the PDF. + + :arg list labels: a list of dictionaries. Each dictionary defines a label building rule and a 0-based "start" page number. That start page is the first for which the label definition is valid. Each dictionary has up to 4 items and looks like `{'startpage': int, 'prefix': str, 'style': str, 'firstpagenum': int}` and has the following items. + + - `startpage`: (int) the first page number (0-based) to apply the label rule. This key **must be present**. The rule is applied to all subsequent pages until either end of document or superseded by the rule with the next larger page number. + - `prefix`: (str) an arbitrary string to start the label with, e.g. "A-". Default is "". + - `style`: (str) the numbering style. Available are "D" (decimal), "r"/"R" (Roman numbers, lower / upper case), and "a"/"A" (lower / upper case alphabetical numbering: "a" through "z", then "aa" through "zz", etc.). Default is "". If "", no numbering will take place and the pages in that range will receive the same label consisting of the `prefix` value. If prefix is also omitted, then the label will be "". + - `firstpagenum`: (int) start numbering with this value. Default is 1, smaller values are ignored. + + For example:: + + [{'startpage': 6, 'prefix': 'A-', 'style': 'D', 'firstpagenum': 10}, + {'startpage': 10, 'prefix': '', 'style': 'D', 'firstpagenum': 1}] + + will generate the labels "A-10", "A-11", "A-12", "A-13", "1", "2", "3", ... for pages 6, 7 and so on until end of document. Pages 0 through 5 will have the label "". + + + .. method:: make_bookmark(loc) + + * New in v.1.17.3 + + Return a page pointer in a reflowable document. After re-layouting the document, the result of this method can be used to find the new location of the page. + + .. note:: Do not confuse with items of a table of contents, TOC. + + :arg list,tuple loc: page location. Must be a valid *(chapter, pno)*. + + :rtype: pointer + :returns: a long integer in pointer format. To be used for finding the new location of the page after re-layouting the document. Do not touch or re-assign. + + + .. method:: find_bookmark(bookmark) + + * New in v.1.17.3 + + Return the new page location after re-layouting the document. + + :arg pointer bookmark: created by :meth:`Document.make_bookmark`. + + :rtype: tuple + :returns: the new (chapter, pno) of the page. + + + .. method:: chapter_page_count(chapter) + + * New in v.1.17.0 + + Return the number of pages of a chapter. + + :arg int chapter: the 0-based chapter number. + + :rtype: int + :returns: number of pages in chapter. Relevant only for document types with chapter support (EPUB currently). + + + .. method:: next_location(page_id) + + * New in v.1.17.0 + + Return the location of the following page. + + :arg tuple page_id: the current page id. This must be a tuple *(chapter, pno)* identifying an existing page. + + :returns: The tuple of the following page, i.e. either *(chapter, pno + 1)* or *(chapter + 1, 0)*, **or** the empty tuple *()* if the argument was the last page. Relevant only for document types with chapter support (EPUB currently). + + + .. method:: prev_location(page_id) + + * New in v.1.17.0 + + Return the locator of the preceding page. + + :arg tuple page_id: the current page id. This must be a tuple *(chapter, pno)* identifying an existing page. + + :returns: The tuple of the preceding page, i.e. either *(chapter, pno - 1)* or the last page of the preceding chapter, **or** the empty tuple *()* if the argument was the first page. Relevant only for document types with chapter support (EPUB currently). + + + .. method:: load_page(page_id=0) + + * Changed in v1.17.0: For document types supporting a so-called "chapter structure" (like EPUB), pages can also be loaded via the combination of chapter number and relative page number, instead of the absolute page number. This should **significantly speed up access** for large documents. + + Create a :ref:`Page` object for further processing (like rendering, text searching, etc.). + + :arg int,tuple page_id: *(Changed in v1.17.0)* + + Either a 0-based page number, or a tuple *(chapter, pno)*. For an **integer**, any `-∞ < page_id < page_count` is acceptable. While page_id is negative, :attr:`page_count` will be added to it. For example: to load the last page, you can use *doc.load_page(-1)*. After this you have page.number = doc.page_count - 1. + + For a tuple, *chapter* must be in range :attr:`Document.chapter_count`, and *pno* must be in range :meth:`Document.chapter_page_count` of that chapter. Both values are 0-based. Using this notation, :attr:`Page.number` will equal the given tuple. Relevant only for document types with chapter support (EPUB currently). + + :rtype: :ref:`Page` + + .. note:: + + Documents also follow the Python sequence protocol with page numbers as indices: *doc.load_page(n) == doc[n]*. + + For **absolute page numbers** only, expressions like *"for page in doc: ..."* and *"for page in reversed(doc): ..."* will successively yield the document's pages. Refer to :meth:`Document.pages` which allows processing pages as with slicing. + + You can also use index notation with the new chapter-based page identification: use *page = doc[(5, 2)]* to load the third page of the sixth chapter. + + To maintain a consistent API, for document types not supporting a chapter structure (like PDFs), :attr:`Document.chapter_count` is 1, and pages can also be loaded via tuples *(0, pno)*. See this [#f3]_ footnote for comments on performance improvements. + + .. method:: reload_page(page) + + * New in v1.16.10 + + PDF only: Provide a new copy of a page after finishing and updating all pending changes. + + :arg page: page object. + :type page: :ref:`Page` + + :rtype: :ref:`Page` + + :returns: a new copy of the same page. All pending updates (e.g. to annotations or widgets) will be finalized and a fresh copy of the page will be loaded. + + .. note:: In a typical use case, a page :ref:`Pixmap` should be taken after annotations / widgets have been added or changed. To force all those changes being reflected in the page structure, this method re-instates a fresh copy while keeping the object hierarchy "document -> page -> annotations/widgets" intact. + + + .. method:: page_cropbox(pno) + + * New in v1.17.7 + + PDF only: Return the unrotated page rectangle -- **without loading the page** (via :meth:`Document.load_page`). This is meant for internal purpose requiring best possible performance. + + :arg int pno: 0-based page number. + + :returns: :ref:`Rect` of the page like :meth:`Page.rect`, but ignoring any rotation. + + .. method:: page_xref(pno) + + * New in v1.17.7 + + PDF only: Return the :data:`xref` of the page -- **without loading the page** (via :meth:`Document.load_page`). This is meant for internal purpose requiring best possible performance. + + :arg int pno: 0-based page number. + + :returns: :data:`xref` of the page like :attr:`Page.xref`. + + .. method:: pages(start=None, [stop=None, [step=None]]) + + * New in v1.16.4 + + A generator for a range of pages. Parameters have the same meaning as in the built-in function *range()*. Intended for expressions of the form *"for page in doc.pages(start, stop, step): ..."*. + + :arg int start: start iteration with this page number. Default is zero, allowed values are `-∞ < start < page_count`. While this is negative, :attr:`page_count` is added **before** starting the iteration. + :arg int stop: stop iteration at this page number. Default is :attr:`page_count`, possible are `-∞ < stop <= page_count`. Larger values are **silently replaced** by the default. Negative values will cyclically emit the pages in reversed order. As with the built-in *range()*, this is the first page **not** returned. + :arg int step: stepping value. Defaults are 1 if start < stop and -1 if start > stop. Zero is not allowed. + + :returns: a generator iterator over the document's pages. Some examples: + + * "doc.pages()" emits all pages. + * "doc.pages(4, 9, 2)" emits pages 4, 6, 8. + * "doc.pages(0, None, 2)" emits all pages with even numbers. + * "doc.pages(-2)" emits the last two pages. + * "doc.pages(-1, -1)" emits all pages in reversed order. + * "doc.pages(-1, -10)" always emits 10 pages in reversed order, starting with the last page -- **repeatedly** if the document has less than 10 pages. So for a 4-page document the following page numbers are emitted: 3, 2, 1, 0, 3, 2, 1, 0, 3, 2, 1, 0, 3. + + .. index:: + pair: from_page; Document.convert_to_pdf + pair: to_page; Document.convert_to_pdf + pair: rotate; Document.convert_to_pdf + + .. method:: convert_to_pdf(from_page=-1, to_page=-1, rotate=0) + + Create a PDF version of the current document and write it to memory. **All document types** are supported. The parameters have the same meaning as in :meth:`insert_pdf`. In essence, you can restrict the conversion to a page subset, specify page rotation, and revert page sequence. + + :arg int from_page: first page to copy (0-based). Default is first page. + + :arg int to_page: last page to copy (0-based). Default is last page. + + :arg int rotate: rotation angle. Default is 0 (no rotation). Should be *n * 90* with an integer n (not checked). + + :rtype: bytes + :returns: a Python *bytes* object containing a PDF file image. It is created by internally using `tobytes(garbage=4, deflate=True)`. See :meth:`tobytes`. You can output it directly to disk or open it as a PDF. Here are some examples:: + + >>> # convert an XPS file to PDF + >>> xps = fitz.open("some.xps") + >>> pdfbytes = xps.convert_to_pdf() + >>> + >>> # either do this --> + >>> pdf = fitz.open("pdf", pdfbytes) + >>> pdf.save("some.pdf") + >>> + >>> # or this --> + >>> pdfout = open("some.pdf", "wb") + >>> pdfout.tobytes(pdfbytes) + >>> pdfout.close() + + >>> # copy image files to PDF pages + >>> # each page will have image dimensions + >>> doc = fitz.open() # new PDF + >>> imglist = [ ... image file names ...] # e.g. a directory listing + >>> for img in imglist: + imgdoc=fitz.open(img) # open image as a document + pdfbytes=imgdoc.convert_to_pdf() # make a 1-page PDF of it + imgpdf=fitz.open("pdf", pdfbytes) + doc.insert_pdf(imgpdf) # insert the image PDF + >>> doc.save("allmyimages.pdf") + + .. note:: The method uses the same logic as the *mutool convert* CLI. This works very well in most cases -- however, beware of the following limitations. + + * Image files: perfect, no issues detected. However, image transparency is ignored. If you need that (like for a watermark), use :meth:`Page.insert_image` instead. Otherwise, this method is recommended for its much better performance. + * XPS: appearance very good. Links work fine, outlines (bookmarks) are lost, but can easily be recovered [#f2]_. + * EPUB, CBZ, FB2: similar to XPS. + * SVG: medium. Roughly comparable to `svglib `_. + + .. method:: get_toc(simple=True) + + Creates a table of contents (TOC) out of the document's outline chain. + + :arg bool simple: Indicates whether a simple or a detailed TOC is required. If *False*, each item of the list also contains a dictionary with :ref:`linkDest` details for each outline entry. + + :rtype: list + + :returns: a list of lists. Each entry has the form *[lvl, title, page, dest]*. Its entries have the following meanings: + + * *lvl* -- hierarchy level (positive *int*). The first entry is always 1. Entries in a row are either **equal**, **increase** by 1, or **decrease** by any number. + * *title* -- title (*str*) + * *page* -- 1-based page number (*int*). If `-1` either no destination or outside document. + * *dest* -- (*dict*) included only if *simple=False*. Contains details of the TOC item as follows: + + - kind: destination kind, see :ref:`linkDest Kinds`. + - file: filename if kind is :data:`LINK_GOTOR` or :data:`LINK_LAUNCH`. + - page: target page, 0-based, :data:`LINK_GOTOR` or :data:`LINK_GOTO` only. + - to: position on target page (:ref:`Point`). + - zoom: (float) zoom factor on target page. + - xref: :data:`xref` of the item (0 if no PDF). + - color: item color in PDF RGB format `(red, green, blue)`, or omitted (always omitted if no PDF). + - bold: true if bold item text or omitted. PDF only. + - italic: true if italic item text, or omitted. PDF only. + - collapse: true if sub-items are folded, or omitted. PDF only. + + + .. method:: xref_get_keys(xref) + + * New in v1.18.7 + + PDF only: Return the PDF dictionary keys of the :data:`dictionary` object provided by its xref number. + + :arg int xref: the :data:`xref`. *(Changed in v1.18.10)* Use `-1` to access the special dictionary "PDF trailer". + + :returns: a tuple of dictionary keys present in object :data:`xref`. Examples: + + >>> from pprint import pprint + >>> import fitz + >>> doc=fitz.open("pymupdf.pdf") + >>> xref = doc.page_xref(0) # xref of page 0 + >>> pprint(doc.xref_get_keys(xref)) # primary level keys of a page + ('Type', 'Contents', 'Resources', 'MediaBox', 'Parent') + >>> pprint(doc.xref_get_keys(-1)) # primary level keys of the trailer + ('Type', 'Index', 'Size', 'W', 'Root', 'Info', 'ID', 'Length', 'Filter') + >>> + + + .. method:: xref_get_key(xref, key) + + * New in v1.18.7 + + PDF only: Return type and value of a PDF dictionary key of a :data:`dictionary` object given by its xref. + + :arg int xref: the :data:`xref`. *Changed in v1.18.10:* Use `-1` to access the special dictionary "PDF trailer". + + :arg str key: the desired PDF key. Must **exactly** match (case-sensitive) one of the keys contained in :meth:`Document.xref_get_keys`. + + :rtype: tuple + + :returns: A tuple (type, value) of strings, where type is one of "xref", "array", "dict", "int", "float", "null", "bool", "name", "string" or "unknown" (should not occur). Independent of "type", the value of the key is **always** formatted as a string -- see the following example -- and (almost always) a faithful reflection of what is stored in the PDF. In most cases, the format of the value string also gives a clue about the key type: + + * A "name" always starts with a "/" slash. + * An "xref" always ends with " 0 R". + * An "array" is always enclosed in "[...]" brackets. + * A "dict" is always enclosed in "<<...>>" brackets. + * A "bool", resp. "null" always equal either "true", "false", resp. "null". + * "float" and "int" are represented by their string format -- and are thus not always distinguishable. + * A "string" is converted to UTF-8 and may therefore deviate from what is stored in the PDF. For example, the PDF key "Author" may have a value of "" in the file, but the method will return `('string', 'Jorj X. McKie')`. + + >>> for key in doc.xref_get_keys(xref): + print(key, "=" , doc.xref_get_key(xref, key)) + Type = ('name', '/Page') + Contents = ('xref', '1297 0 R') + Resources = ('xref', '1296 0 R') + MediaBox = ('array', '[0 0 612 792]') + Parent = ('xref', '1301 0 R') + >>> # + >>> # Now same thing for the PDF trailer. + >>> # It has no xref, so -1 must be used instead. + >>> # + >>> for key in doc.xref_get_keys(-1): + print(key, "=", doc.xref_get_key(-1, key)) + Type = ('name', '/XRef') + Index = ('array', '[0 8802]') + Size = ('int', '8802') + W = ('array', '[1 3 1]') + Root = ('xref', '8799 0 R') + Info = ('xref', '8800 0 R') + ID = ('array', '[]') + Length = ('int', '21111') + Filter = ('name', '/FlateDecode') + >>> + + + .. method:: xref_set_key(xref, key, value) + + * New in v1.18.7, changed in v 1.18.13 + * Changed in v1.19.4: remove a key "physically" if set to "null". + + PDF only: Set (add, update, delete) the value of a PDF key for the :data:`dictionary` object given by its xref. + + .. caution:: This is an expert function: if you do not know what you are doing, there is a high risk to render (parts of) the PDF unusable. Please do consult :ref:`AdobeManual` about object specification formats (page 18) and the structure of special dictionary types like page objects. + + :arg int xref: the :data:`xref`. *Changed in v1.18.13:* To update the PDF trailer, specify -1. + :arg str key: the desired PDF key (without leading "/"). Must not be empty. Any valid PDF key -- whether already present in the object (which will be overwritten) -- or new. It is possible to use PDF path notation like `"Resources/ExtGState"` -- which sets the value for key `"/ExtGState"` as a sub-object of `"/Resources"`. + :arg str value: the value for the key. It must be a non-empty string and, depending on the desired PDF object type, the following rules must be observed. There is some syntax checking, but **no type checking** and no checking if it makes sense PDF-wise, i.e. **no semantics checking**. Upper / lower case is important! + + * **xref** -- must be provided as `"nnn 0 R"` with a valid :data:`xref` number nnn of the PDF. The suffix "`0 R`" is required to be recognizable as an xref by PDF applications. + * **array** -- a string like `"[a b c d e f]"`. The brackets are required. Array items must be separated by at least one space (not commas like in Python). An empty array `"[]"` is possible and *equivalent* to removing the key. Array items may be any PDF objects, like dictionaries, xrefs, other arrays, etc. Like in Python, array items may be of different types. + * **dict** -- a string like `"<< ... >>"`. The brackets are required and must enclose a valid PDF dictionary definition. The empty dictionary `"<<>>"` is possible and *equivalent* to removing the key. + * **int** -- an integer formatted **as a string**. + * **float** -- a float formatted **as a string**. Scientific notation (with exponents) is **not allowed by PDF**. + * **null** -- the string `"null"`. This is the PDF equivalent to Python's `None` and causes the key to be ignored -- however not necessarily removed, resp. removed on saves with garbage collection. *Changed in v1.19.4:* If the key is no path hierarchy (i.e. contains no slash "/"), then it will be completely removed. + * **bool** -- one of the strings `"true"` or `"false"`. + * **name** -- a valid PDF name with a leading slash like this: `"/PageLayout"`. See page 16 of the :ref:`AdobeManual`. + * **string** -- a valid PDF string. **All PDF strings must be enclosed by brackets**. Denote the empty string as `"()"`. Depending on its content, the possible brackets are + + - "(...)" for ASCII-only text. Reserved PDF characters must be backslash-escaped and non-ASCII characters must be provided as 3-digit backslash-escaped octals -- including leading zeros. Example: 12 = 0x0C must be encoded as `\014`. + - "<...>" for hex-encoded text. Every character must be represented by two hex-digits (lower or upper case). + + - If in doubt, we **strongly recommend** to use :meth:`get_pdf_str`! This function automatically generates the right brackets, escapes, and overall format. It will for example do conversions like these: + + >>> # because of the € symbol, the following yields UTF-16BE BOM + >>> fitz.get_pdf_str("Pay in $ or €.") + '' + >>> # escapes for brackets and non-ASCII + >>> fitz.get_pdf_str("Prices in EUR (USD also accepted). Areas are in m².") + '(Prices in EUR \\(USD also accepted\\). Areas are in m\\262.)' + + + .. method:: get_page_pixmap(pno: int, *, matrix: matrix_like = Identity, dpi=None, colorspace: Colorspace = csRGB, clip: rect_like = None, alpha: bool = False, annots: bool = True) + + Creates a pixmap from page *pno* (zero-based). Invokes :meth:`Page.get_pixmap`. + + All parameters except `pno` are *keyword-only.* + + :arg int pno: page number, 0-based in `-∞ < pno < page_count`. + + :rtype: :ref:`Pixmap` + + .. method:: get_page_xobjects(pno) + + * New in v1.16.13 + * Changed in v1.18.11 + + PDF only: Return a list of all XObjects referenced by a page. + + :arg int pno: page number, 0-based, `-∞ < pno < page_count`. + + :rtype: list + :returns: a list of (non-image) XObjects. These objects typically represent pages *embedded* (not copied) from other PDFs. For example, :meth:`Page.show_pdf_page` will create this type of object. An item of this list has the following layout: `(xref, name, invoker, bbox)`, where + + * **xref** (*int*) is the XObject's :data:`xref`. + * **name** (*str*) is the symbolic name to reference the XObject. + * **invoker** (*int*) the :data:`xref` of the invoking XObject or zero if the page directly invokes it. + * **bbox** (:ref:`Rect`) the boundary box of the XObject's location on the page **in untransformed coordinates**. To get actual, non-rotated page coordinates, multiply with the page's transformation matrix :attr:`Page.transformation_matrix`. *Changed in v.18.11:* the bbox is now formatted as :ref:`Rect`. + + + .. method:: get_page_images(pno, full=False) + + PDF only: Return a list of all images (directly or indirectly) referenced by the page. + + :arg int pno: page number, 0-based, `-∞ < pno < page_count`. + :arg bool full: whether to also include the referencer's :data:`xref` (which is zero if this is the page). + + :rtype: list + + :returns: a list of images **referenced** by this page. Each item looks like + + `(xref, smask, width, height, bpc, colorspace, alt. colorspace, name, filter, referencer)` + + Where + + * **xref** (*int*) is the image object number + * **smask** (*int*) is the object number of its soft-mask image + * **width** and **height** (*ints*) are the image dimensions + * **bpc** (*int*) denotes the number of bits per component (normally 8) + * **colorspace** (*str*) a string naming the colorspace (like **DeviceRGB**) + * **alt. colorspace** (*str*) is any alternate colorspace depending on the value of **colorspace** + * **name** (*str*) is the symbolic name by which the image is referenced + * **filter** (*str*) is the decode filter of the image (:ref:`AdobeManual`, pp. 22). + * **referencer** (*int*) the :data:`xref` of the referencer. Zero if directly referenced by the page. Only present if *full=True*. + + .. note:: In general, this is not the list of images that are **actually displayed**. This method only parses several PDF objects to collect references to embedded images. It does not analyse the page's :data:`contents`, where all the actual image display commands are defined. To get this information, please use :meth:`Page.get_image_info`. Also have a look at the discussion in section :ref:`textpagedict`. + + + .. method:: get_page_fonts(pno, full=False) + + PDF only: Return a list of all fonts (directly or indirectly) referenced by the page. + + :arg int pno: page number, 0-based, `-∞ < pno < page_count`. + :arg bool full: whether to also include the referencer's :data:`xref`. If *True*, the returned items are one entry longer. Use this option if you need to know, whether the page directly references the font. In this case the last entry is 0. If the font is referenced by an `/XObject` of the page, you will find its :data:`xref` here. + + :rtype: list + + :returns: a list of fonts referenced by this page. Each entry looks like + + **(xref, ext, type, basefont, name, encoding, referencer)**, + + where + + * **xref** (*int*) is the font object number (may be zero if the PDF uses one of the builtin fonts directly) + * **ext** (*str*) font file extension (e.g. "ttf", see :ref:`FontExtensions`) + * **type** (*str*) is the font type (like "Type1" or "TrueType" etc.) + * **basefont** (*str*) is the base font name, + * **name** (*str*) is the symbolic name, by which the font is referenced + * **encoding** (*str*) the font's character encoding if different from its built-in encoding (:ref:`AdobeManual`, p. 254): + * **referencer** (*int* optional) the :data:`xref` of the referencer. Zero if directly referenced by the page, otherwise the xref of an XObject. Only present if *full=True*. + + Example:: + + >>> pprint(doc.get_page_fonts(0, full=False)) + [(12, 'ttf', 'TrueType', 'FNUUTH+Calibri-Bold', 'R8', ''), + (13, 'ttf', 'TrueType', 'DOKBTG+Calibri', 'R10', ''), + (14, 'ttf', 'TrueType', 'NOHSJV+Calibri-Light', 'R12', ''), + (15, 'ttf', 'TrueType', 'NZNDCL+CourierNewPSMT', 'R14', ''), + (16, 'ttf', 'Type0', 'MNCSJY+SymbolMT', 'R17', 'Identity-H'), + (17, 'cff', 'Type1', 'UAEUYH+Helvetica', 'R20', 'WinAnsiEncoding'), + (18, 'ttf', 'Type0', 'ECPLRU+Calibri', 'R23', 'Identity-H'), + (19, 'ttf', 'Type0', 'TONAYT+CourierNewPSMT', 'R27', 'Identity-H')] + + .. note:: + * This list has no duplicate entries: the combination of :data:`xref`, *name* and *referencer* is unique. + * In general, this is a superset of the fonts actually in use by this page. The PDF creator may e.g. have specified some global list, of which each page only makes partial use. + + .. method:: get_page_text(pno, output="text", flags=3, textpage=None, sort=False) + + Extracts the text of a page given its page number *pno* (zero-based). Invokes :meth:`Page.get_text`. + + :arg int pno: page number, 0-based, any value `-∞ < pno < page_count`. + + For other parameter refer to the page method. + + :rtype: str + + .. index:: + pair: fontsize; Document.layout + pair: rect; Document.layout + pair: width; Document.layout + pair: height; Document.layout + + .. method:: layout(rect=None, width=0, height=0, fontsize=11) + + Re-paginate ("reflow") the document based on the given page dimension and fontsize. This only affects some document types like e-books and HTML. Ignored if not supported. Supported documents have *True* in property :attr:`is_reflowable`. + + :arg rect_like rect: desired page size. Must be finite, not empty and start at point (0, 0). + :arg float width: use it together with *height* as alternative to *rect*. + :arg float height: use it together with *width* as alternative to *rect*. + :arg float fontsize: the desired default fontsize. + + .. method:: select(s) + + PDF only: Keeps only those pages of the document whose numbers occur in the list. Empty sequences or elements outside `range(doc.page_count)` will cause a *ValueError*. For more details see remarks at the bottom or this chapter. + + :arg sequence s: The sequence (see :ref:`SequenceTypes`) of page numbers (zero-based) to be included. Pages not in the sequence will be deleted (from memory) and become unavailable until the document is reopened. **Page numbers can occur multiple times and in any order:** the resulting document will reflect the sequence exactly as specified. + + .. note:: + + * Page numbers in the sequence need not be unique nor be in any particular order. This makes the method a versatile utility to e.g. select only the even or the odd pages or meeting some other criteria and so forth. + + * On a technical level, the method will always create a new :data:`pagetree`. + + * When dealing with only a few pages, methods :meth:`copy_page`, :meth:`move_page`, :meth:`delete_page` are easier to use. In fact, they are also **much faster** -- by at least one order of magnitude when the document has many pages. + + + .. method:: set_metadata(m) + + PDF only: Sets or updates the metadata of the document as specified in *m*, a Python dictionary. + + :arg dict m: A dictionary with the same keys as *metadata* (see below). All keys are optional. A PDF's format and encryption method cannot be set or changed and will be ignored. If any value should not contain data, do not specify its key or set the value to `None`. If you use *{}* all metadata information will be cleared to the string *"none"*. If you want to selectively change only some values, modify a copy of *doc.metadata* and use it as the argument. Arbitrary unicode values are possible if specified as UTF-8-encoded. + + *(Changed in v1.18.4)* Empty values or "none" are no longer written, but completely omitted. + + .. method:: get_xml_metadata() + + PDF only: Get the document XML metadata. + + :rtype: str + :returns: XML metadata of the document. Empty string if not present or not a PDF. + + .. method:: set_xml_metadata(xml) + + PDF only: Sets or updates XML metadata of the document. + + :arg str xml: the new XML metadata. Should be XML syntax, however no checking is done by this method and any string is accepted. + + + .. method:: set_pagelayout(value) + + * New in v1.22.2 + + PDF only: Set the `/PageLayout`. + + :arg str value: one of the strings "SinglePage", "OneColumn", "TwoColumnLeft", "TwoColumnRight", "TwoPageLeft", "TwoPageRight". Lower case is supported. + + + .. method:: set_pagemode(value) + + * New in v1.22.2 + + PDF only: Set the `/PageMode`. + + :arg str value: one of the strings "UseNone", "UseOutlines", "UseThumbs", "FullScreen", "UseOC", "UseAttachments". Lower case is supported. + + + .. method:: set_markinfo(value) + + * New in v1.22.2 + + PDF only: Set the `/MarkInfo` values. + + :arg dict value: a dictionary like this one: `{"Marked": False, "UserProperties": False, "Suspects": False}`. This dictionary contains information about the usage of Tagged PDF conventions. For details please see the `PDF specifications `_. + + + .. method:: set_toc(toc, collapse=1) + + PDF only: Replaces the **complete current outline** tree (table of contents) with the one provided as the argument. After successful execution, the new outline tree can be accessed as usual via :meth:`Document.get_toc` or via :attr:`Document.outline`. Like with other output-oriented methods, changes become permanent only via :meth:`save` (incremental save supported). Internally, this method consists of the following two steps. For a demonstration see example below. + + - Step 1 deletes all existing bookmarks. + + - Step 2 creates a new TOC from the entries contained in *toc*. + + :arg sequence toc: + + A list / tuple with **all bookmark entries** that should form the new table of contents. Output variants of :meth:`get_toc` are acceptable. To completely remove the table of contents specify an empty sequence or None. Each item must be a list with the following format. + + * [lvl, title, page [, dest]] where + + - **lvl** is the hierarchy level (int > 0) of the item, which **must be 1** for the first item and at most 1 larger than the previous one. + + - **title** (str) is the title to be displayed. It is assumed to be UTF-8-encoded (relevant for multibyte code points only). + + - **page** (int) is the target page number **(attention: 1-based)**. Must be in valid range if positive. Set it to -1 if there is no target, or the target is external. + + - **dest** (optional) is a dictionary or a number. If a number, it will be interpreted as the desired height (in points) this entry should point to on the page. Use a dictionary (like the one given as output by `get_toc(False)`) for a detailed control of the bookmark's properties, see :meth:`Document.get_toc` for a description. + + :arg int collapse: *(new in v1.16.9)* controls the hierarchy level beyond which outline entries should initially show up collapsed. The default 1 will hence only display level 1, higher levels must be unfolded using the PDF viewer. To unfold everything, specify either a large integer, 0 or None. + + :rtype: int + :returns: the number of inserted, resp. deleted items. + + .. method:: outline_xref(idx) + + * New in v1.17.7 + + PDF only: Return the :data:`xref` of the outline item. This is mainly used for internal purposes. + + arg int idx: index of the item in list :meth:`Document.get_toc`. + + :returns: :data:`xref`. + + .. method:: del_toc_item(idx) + + * New in v1.17.7 + * Changed in v1.18.14: no longer remove the item's text, but show it grayed-out. + + PDF only: Remove this TOC item. This is a high-speed method, which **disables** the respective item, but leaves the overall TOC structure intact. Physically, the item still exists in the TOC tree, but is shown grayed-out and will no longer point to any destination. + + This also implies that you can reassign the item to a new destination using :meth:`Document.set_toc_item`, when required. + + :arg int idx: the index of the item in list :meth:`Document.get_toc`. + + + .. method:: set_toc_item(idx, dest_dict=None, kind=None, pno=None, uri=None, title=None, to=None, filename=None, zoom=0) + + * New in v1.17.7 + * Changed in v1.18.6 + + PDF only: Changes the TOC item identified by its index. Change the item **title**, **destination**, **appearance** (color, bold, italic) or collapsing sub-items -- or to remove the item altogether. + + Use this method if you need specific changes for selected entries only and want to avoid replacing the complete TOC. This is beneficial especially when dealing with large table of contents. + + :arg int idx: the index of the entry in the list created by :meth:`Document.get_toc`. + :arg dict dest_dict: the new destination. A dictionary like the last entry of an item in `doc.get_toc(False)`. Using this as a template is recommended. When given, **all other parameters are ignored** -- except title. + :arg int kind: the link kind, see :ref:`linkDest Kinds`. If :data:`LINK_NONE`, then all remaining parameter will be ignored, and the TOC item will be removed -- same as :meth:`Document.del_toc_item`. If None, then only the title is modified and the remaining parameters are ignored. All other values will lead to making a new destination dictionary using the subsequent arguments. + :arg int pno: the 1-based page number, i.e. a value 1 <= pno <= doc.page_count. Required for LINK_GOTO. + :arg str uri: the URL text. Required for LINK_URI. + :arg str title: the desired new title. None if no change. + :arg point_like to: (optional) points to a coordinate on the target page. Relevant for LINK_GOTO. If omitted, a point near the page's top is chosen. + :arg str filename: required for LINK_GOTOR and LINK_LAUNCH. + :arg float zoom: use this zoom factor when showing the target page. + + **Example use:** Change the TOC of the SWIG manual to achieve this: + + Collapse everything below top level and show the chapter on Python support in red, bold and italic:: + + >>> import fitz + >>> doc=fitz.open("SWIGDocumentation.pdf") + >>> toc = doc.get_toc(False) # we need the detailed TOC + >>> # list of level 1 indices and their titles + >>> lvl1 = [(i, item[1]) for i, item in enumerate(toc) if item[0] == 1] + >>> for i, title in lvl1: + d = toc[i][3] # get the destination dict + d["collapse"] = True # collapse items underneath + if "Python" in title: # show the 'Python' chapter + d["color"] = (1, 0, 0) # in red, + d["bold"] = True # bold and + d["italic"] = True # italic + doc.set_toc_item(i, dest_dict=d) # update this toc item + >>> doc.save("NEWSWIG.pdf",garbage=3,deflate=True) + + In the previous example, we have changed only 42 of the 1240 TOC items of the file. + + .. method:: can_save_incrementally() + + * New in v1.16.0 + + Check whether the document can be saved incrementally. Use it to choose the right option without encountering exceptions. + + .. method:: scrub(attached_files=True, clean_pages=True, embedded_files=True, hidden_text=True, javascript=True, metadata=True, redactions=True, redact_images=0, remove_links=True, reset_fields=True, reset_responses=True, thumbnails=True, xml_metadata=True) + + * New in v1.16.14 + + PDF only: Remove potentially sensitive data from the PDF. This function is inspired by the similar "Sanitize" function in Adobe Acrobat products. The process is configurable by a number of options. + + :arg bool attached_files: Search for 'FileAttachment' annotations and remove the file content. + :arg bool clean_pages: Remove any comments from page painting sources. If this option is set to *False*, then this is also done for *hidden_text* and *redactions*. + :arg bool embedded_files: Remove embedded files. + :arg bool hidden_text: Remove OCRed text and invisible text [#f7]_. + :arg bool javascript: Remove JavaScript sources. + :arg bool metadata: Remove PDF standard metadata. + :arg bool redactions: Apply redaction annotations. + :arg int redact_images: how to handle images if applying redactions. One of 0 (ignore), 1 (blank out overlaps) or 2 (remove). + :arg bool remove_links: Remove all links. + :arg bool reset_fields: Reset all form fields to their defaults. + :arg bool reset_responses: Remove all responses from all annotations. + :arg bool thumbnails: Remove thumbnail images from pages. + :arg bool xml_metadata: Remove XML metadata. + + + .. method:: save(outfile, garbage=0, clean=False, deflate=False, deflate_images=False, deflate_fonts=False, incremental=False, ascii=False, expand=0, linear=False, pretty=False, no_new_id=False, encryption=PDF_ENCRYPT_NONE, permissions=-1, owner_pw=None, user_pw=None) + + * Changed in v1.18.7 + * Changed in v1.19.0 + + PDF only: Saves the document in its **current state**. + + :arg str,Path,fp outfile: The file path, `pathlib.Path` or file object to save to. A file object must have been created before via `open(...)` or `io.BytesIO()`. Choosing `io.BytesIO()` is similar to :meth:`Document.tobytes` below, which equals the `getvalue()` output of an internally created `io.BytesIO()`. + + :arg int garbage: Do garbage collection. Positive values exclude "incremental". + + * 0 = none + * 1 = remove unused (unreferenced) objects. + * 2 = in addition to 1, compact the :data:`xref` table. + * 3 = in addition to 2, merge duplicate objects. + * 4 = in addition to 3, check :data:`stream` objects for duplication. This may be slow because such data are typically large. + + :arg bool clean: Clean and sanitize content streams [#f1]_. Corresponds to "mutool clean -sc". + + :arg bool deflate: Deflate (compress) uncompressed streams. + :arg bool deflate_images: *(new in v1.18.3)* Deflate (compress) uncompressed image streams [#f4]_. + :arg bool deflate_fonts: *(new in v1.18.3)* Deflate (compress) uncompressed fontfile streams [#f4]_. + + :arg bool incremental: Only save changes to the PDF. Excludes "garbage" and "linear". Can only be used if *outfile* is a string or a `pathlib.Path` and equal to :attr:`Document.name`. Cannot be used for files that are decrypted or repaired and also in some other cases. To be sure, check :meth:`Document.can_save_incrementally`. If this is false, saving to a new file is required. + + :arg bool ascii: convert binary data to ASCII. + + :arg int expand: Decompress objects. Generates versions that can be better read by some other programs and will lead to larger files. + + * 0 = none + * 1 = images + * 2 = fonts + * 255 = all + + :arg bool linear: Save a linearised version of the document. This option creates a file format for improved performance for Internet access. Excludes "incremental". + + :arg bool pretty: Prettify the document source for better readability. PDF objects will be reformatted to look like the default output of :meth:`Document.xref_object`. + + :arg bool no_new_id: Suppress the update of the file's `/ID` field. If the file happens to have no such field at all, also suppress creation of a new one. Default is `False`, so every save will lead to an updated file identification. + + :arg int permissions: *(new in v1.16.0)* Set the desired permission levels. See :ref:`PermissionCodes` for possible values. Default is granting all. + + :arg int encryption: *(new in v1.16.0)* set the desired encryption method. See :ref:`EncryptionMethods` for possible values. + + :arg str owner_pw: *(new in v1.16.0)* set the document's owner password. *(Changed in v1.18.3)* If not provided, the user password is taken if provided. The string length must not exceed 40 characters. + + :arg str user_pw: *(new in v1.16.0)* set the document's user password. The string length must not exceed 40 characters. + + .. note:: The method does not check, whether a file of that name already exists, will hence not ask for confirmation, and overwrite the file. It is your responsibility as a programmer to handle this. + + .. method:: ez_save(*args, **kwargs) + + * New in v1.18.11 + + PDF only: The same as :meth:`Document.save` but with the changed defaults `deflate=True, garbage=3`. + + .. method:: saveIncr() + + PDF only: saves the document incrementally. This is a convenience abbreviation for *doc.save(doc.name, incremental=True, encryption=PDF_ENCRYPT_KEEP)*. + + .. note:: + + Saving incrementally may be required if the document contains verified signatures which would be invalidated by saving to a new file. + + + .. method:: tobytes(garbage=0, clean=False, deflate=False, deflate_images=False, deflate_fonts=False, ascii=False, expand=0, linear=False, pretty=False, no_new_id=False, encryption=PDF_ENCRYPT_NONE, permissions=-1, owner_pw=None, user_pw=None) + + * Changed in v1.18.7 + * Changed in v1.19.0 + + PDF only: Writes the **current content of the document** to a bytes object instead of to a file. Obviously, you should be wary about memory requirements. The meanings of the parameters exactly equal those in :meth:`save`. Chapter :ref:`FAQ` contains an example for using this method as a pre-processor to `pdfrw `_. + + *(Changed in v1.16.0)* for extended encryption support. + + :rtype: bytes + :returns: a bytes object containing the complete document. + + .. method:: search_page_for(pno, text, quads=False) + + Search for "text" on page number "pno". Works exactly like the corresponding :meth:`Page.search_for`. Any integer `-∞ < pno < page_count` is acceptable. + + .. index:: + pair: append; Document.insert_pdf + pair: join; Document.insert_pdf + pair: merge; Document.insert_pdf + pair: from_page; Document.insert_pdf + pair: to_page; Document.insert_pdf + pair: start_at; Document.insert_pdf + pair: rotate; Document.insert_pdf + pair: links; Document.insert_pdf + pair: annots; Document.insert_pdf + pair: show_progress; Document.insert_pdf + + .. method:: insert_pdf(docsrc, from_page=-1, to_page=-1, start_at=-1, rotate=-1, links=True, annots=True, show_progress=0, final=1) + + * Changed in v1.19.3 - as a fix to issue `#537 `_, form fields are always excluded. + + PDF only: Copy the page range **[from_page, to_page]** (including both) of PDF document *docsrc* into the current one. Inserts will start with page number *start_at*. Value -1 indicates default values. All pages thus copied will be rotated as specified. Links and annotations can be excluded in the target, see below. All page numbers are 0-based. + + :arg docsrc: An opened PDF *Document* which must not be the current document. However, it may refer to the same underlying file. + :type docsrc: *Document* + + :arg int from_page: First page number in *docsrc*. Default is zero. + + :arg int to_page: Last page number in *docsrc* to copy. Defaults to last page. + + :arg int start_at: First copied page, will become page number *start_at* in the target. Default -1 appends the page range to the end. If zero, the page range will be inserted before current first page. + + :arg int rotate: All copied pages will be rotated by the provided value (degrees, integer multiple of 90). + + :arg bool links: Choose whether (internal and external) links should be included in the copy. Default is *True*. Internal links to outside the copied page range are **always excluded**. + :arg bool annots: *(new in v1.16.1)* choose whether annotations should be included in the copy. *(Fixed in v1.19.3)* Form fields can never be copied. + :arg int show_progress: *(new in v1.17.7)* specify an interval size greater zero to see progress messages on `sys.stdout`. After each interval, a message like `Inserted 30 of 47 pages.` will be printed. + :arg int final: *(new in v1.18.0)* controls whether the list of already copied objects should be **dropped** after this method, default *True*. Set it to 0 except for the last one of multiple insertions from the same source PDF. This saves target file size and speeds up execution considerably. + + .. note:: + + 1. If *from_page > to_page*, pages will be **copied in reverse order**. If *0 <= from_page == to_page*, then one page will be copied. + + 2. *docsrc* TOC entries **will not be copied**. It is easy however, to recover a table of contents for the resulting document. Look at the examples below and at program `join.py `_ in the *examples* directory: it can join PDF documents and at the same time piece together respective parts of the tables of contents. + + + .. index:: + pair: append; Document.insert_file + pair: join; Document.insert_file + pair: merge; Document.insert_file + pair: from_page; Document.insert_file + pair: to_page; Document.insert_file + pair: start_at; Document.insert_file + pair: rotate; Document.insert_file + pair: links; Document.insert_file + pair: annots; Document.insert_file + pair: show_progress; Document.insert_file + + .. method:: insert_file(infile, from_page=-1, to_page=-1, start_at=-1, rotate=-1, links=True, annots=True, show_progress=0, final=1) + + * New in v1.22.0 + + PDF only: Add an arbitrary supported document to the current PDF. Opens "infile" as a document, converts it to a PDF and then invokes :meth:`Document.insert_pdf`. Parameters are the same as for that method. Among other things, this features an easy way to append images as full pages to an output PDF. + + :arg multiple infile: the input document to insert. May be a filename specification as is valid for creating a :ref:`Document` or a :ref:`Pixmap`. + + + .. index:: + pair: width; Document.new_page + pair: height; Document.new_page + + .. method:: new_page(pno=-1, width=595, height=842) + + PDF only: Insert an empty page. + + :arg int pno: page number in front of which the new page should be inserted. Must be in *1 < pno <= page_count*. Special values -1 and *doc.page_count* insert **after** the last page. + + :arg float width: page width. + :arg float height: page height. + + :rtype: :ref:`Page` + :returns: the created page object. + + .. index:: + pair: fontsize; Document.insert_page + pair: width; Document.insert_page + pair: height; Document.insert_page + pair: fontname; Document.insert_page + pair: fontfile; Document.insert_page + pair: color; Document.insert_page + + .. method:: insert_page(pno, text=None, fontsize=11, width=595, height=842, fontname="helv", fontfile=None, color=None) + + PDF only: Insert a new page and insert some text. Convenience function which combines :meth:`Document.new_page` and (parts of) :meth:`Page.insert_text`. + + :arg int pno: page number (0-based) **in front of which** to insert. Must be in `range(-1, doc.page_count + 1)`. Special values -1 and `doc.page_count` insert **after** the last page. + + Changed in v1.14.12 + This is now a positional parameter + + For the other parameters, please consult the aforementioned methods. + + :rtype: int + :returns: the result of :meth:`Page.insert_text` (number of successfully inserted lines). + + .. method:: delete_page(pno=-1) + + PDF only: Delete a page given by its 0-based number in `-∞ < pno < page_count - 1`. + + * Changed in v1.18.14: support Python's `del` statement. + + :arg int pno: the page to be deleted. Negative number count backwards from the end of the document (like with indices). Default is the last page. + + .. method:: delete_pages(*args, **kwds) + + * Changed in v1.18.13: more flexibility specifying pages to delete. + * Changed in v1.18.14: support Python's `del` statement. + + PDF only: Delete multiple pages given as 0-based numbers. + + **Format 1:** Use keywords. Represents the old format. A contiguous range of pages is removed. + * "from_page": first page to delete. Zero if omitted. + * "to_page": last page to delete. Last page in document if omitted. Must not be less then "from_page". + + **Format 2:** Two page numbers as positional parameters. Handled like Format 1. + + **Format 3:** One positional integer parameter. Equivalent to :meth:`Page.delete_page`. + + **Format 4:** One positional parameter of type *list*, *tuple* or *range()* of page numbers. The items of this sequence may be in any order and may contain duplicates. + + **Format 5:** *(New in v1.18.14)* Using the Python `del` statement and index / slice notation is now possible. + + .. note:: + + *(Changed in v1.14.17, optimized in v1.17.7)* In an effort to maintain a valid PDF structure, this method and :meth:`delete_page` will also deactivate items in the table of contents which point to deleted pages. "Deactivation" here means, that the bookmark will point to nowhere and the title will be shown grayed-out by supporting PDF viewers. The overall TOC structure is left intact. + + It will also remove any **links on remaining pages** which point to a deleted one. This action may have an extended response time for documents with many pages. + + Following examples will all delete pages 500 through 519: + + * `doc.delete_pages(500, 519)` + * `doc.delete_pages(from_page=500, to_page=519)` + * `doc.delete_pages((500, 501, 502, ... , 519))` + * `doc.delete_pages(range(500, 520))` + * `del doc[500:520]` + * `del doc[(500, 501, 502, ... , 519)]` + * `del doc[range(500, 520)]` + + For the :ref:`AdobeManual` the above takes about 0.6 seconds, because the remaining 1290 pages must be cleaned from invalid links. + + In general, the performance of this method is dependent on the number of remaining pages -- **not** on the number of deleted pages: in the above example, **deleting all pages except** those 20, will need much less time. + + + .. method:: copy_page(pno, to=-1) + + PDF only: Copy a page reference within the document. + + :arg int pno: the page to be copied. Must be in range `0 <= pno < page_count`. + + :arg int to: the page number in front of which to copy. The default inserts **after** the last page. + + .. note:: Only a new **reference** to the page object will be created -- not a new page object, all copied pages will have identical attribute values, including the :attr:`Page.xref`. This implies that any changes to one of these copies will appear on all of them. + + .. method:: fullcopy_page(pno, to=-1) + + * New in v1.14.17 + + PDF only: Make a full copy (duplicate) of a page. + + :arg int pno: the page to be duplicated. Must be in range `0 <= pno < page_count`. + + :arg int to: the page number in front of which to copy. The default inserts **after** the last page. + + .. note:: + + * In contrast to :meth:`copy_page`, this method creates a new page object (with a new :data:`xref`), which can be changed independently from the original. + + * Any Popup and "IRT" ("in response to") annotations are **not copied** to avoid potentially incorrect situations. + + .. method:: move_page(pno, to=-1) + + PDF only: Move (copy and then delete original) a page within the document. + + :arg int pno: the page to be moved. Must be in range `0 <= pno < page_count`. + + :arg int to: the page number in front of which to insert the moved page. The default moves **after** the last page. + + + .. method:: need_appearances(value=None) + + * New in v1.17.4 + + PDF only: Get or set the */NeedAppearances* property of Form PDFs. Quote: *"(Optional) A flag specifying whether to construct appearance streams and appearance dictionaries for all widget annotations in the document ... Default value: false."* This may help controlling the behavior of some readers / viewers. + + :arg bool value: set the property to this value. If omitted or `None`, inquire the current value. + + :rtype: bool + :returns: + * None: not a Form PDF, or property not defined. + * True / False: the value of the property (either just set or existing for inquiries). Has no effect if no Form PDF. + + + + .. method:: get_sigflags() + + PDF only: Return whether the document contains signature fields. This is an optional PDF property: if not present (return value -1), no conclusions can be drawn -- the PDF creator may just not have bothered using it. + + :rtype: int + :returns: + * -1: not a Form PDF / no signature fields recorded / no *SigFlags* found. + * 1: at least one signature field exists. + * 3: contains signatures that may be invalidated if the file is saved (written) in a way that alters its previous contents, as opposed to an incremental update. + + .. index:: + pair: filename; Document.embfile_add + pair: ufilename; Document.embfile_add + pair: desc; Document.embfile_add + + .. method:: embfile_add(name, buffer, filename=None, ufilename=None, desc=None) + + * Changed in v1.14.16: The sequence of positional parameters "name" and "buffer" has been changed to comply with the call pattern of other functions. + + PDF only: Embed a new file. All string parameters except the name may be unicode (in previous versions, only ASCII worked correctly). File contents will be compressed (where beneficial). + + :arg str name: entry identifier, **must not already exist**. + :arg bytes,bytearray,BytesIO buffer: file contents. + + *(Changed in v1.14.13)* *io.BytesIO* is now also supported. + + :arg str filename: optional filename. Documentation only, will be set to *name* if `None`. + :arg str ufilename: optional unicode filename. Documentation only, will be set to *filename* if `None`. + :arg str desc: optional description. Documentation only, will be set to *name* if `None`. + + :rtype: int + :returns: *(Changed in v1.18.13)* The method now returns the :data:`xref` of the inserted file. In addition, the file object now will be automatically given the PDF keys `/CreationDate` and `/ModDate` based on the current date-time. + + + .. method:: embfile_count() + + * Changed in v1.14.16: This is now a method. In previous versions, this was a property. + + PDF only: Return the number of embedded files. + + .. method:: embfile_get(item) + + PDF only: Retrieve the content of embedded file by its entry number or name. If the document is not a PDF, or entry cannot be found, an exception is raised. + + :arg int,str item: index or name of entry. An integer must be in `range(embfile_count())`. + + :rtype: bytes + + .. method:: embfile_del(item) + + * Changed in v1.14.16: Items can now be deleted by index, too. + + PDF only: Remove an entry from `/EmbeddedFiles`. As always, physical deletion of the embedded file content (and file space regain) will occur only when the document is saved to a new file with a suitable garbage option. + + :arg int/str item: index or name of entry. + + .. warning:: When specifying an entry name, this function will only **delete the first item** with that name. Be aware that PDFs not created with PyMuPDF may contain duplicate names. So you may want to take appropriate precautions. + + .. method:: embfile_info(item) + + * Changed in v1.18.13 + + PDF only: Retrieve information of an embedded file given by its number or by its name. + + :arg int/str item: index or name of entry. An integer must be in `range(embfile_count())`. + + :rtype: dict + :returns: a dictionary with the following keys: + + * *name* -- (*str*) name under which this entry is stored + * *filename* -- (*str*) filename + * *ufilename* -- (*unicode*) filename + * *desc* -- (*str*) description + * *size* -- (*int*) original file size + * *length* -- (*int*) compressed file length + * *creationDate* -- *(New in v1.18.13)* (*str*) date-time of item creation in PDF format + * *modDate* -- *(New in v1.18.13)* (*str*) date-time of last change in PDF format + * *collection* -- *(New in v1.18.13)* (*int*) :data:`xref` of the associated PDF portfolio item if any, else zero. + * *checksum* -- *(New in v1.18.13)* (*str*) a hashcode of the stored file content as a hexadecimal string. Should be MD5 according to PDF specifications, but be prepared to see other hashing algorithms. + + .. method:: embfile_names() + + * New in v1.14.16 + + PDF only: Return a list of embedded file names. The sequence of the names equals the physical sequence in the document. + + :rtype: list + + .. index:: + pair: filename; Document.embfile_upd + pair: ufilename; Document.embfile_upd + pair: desc; Document.embfile_upd + + .. method:: embfile_upd(item, buffer=None, filename=None, ufilename=None, desc=None) + + PDF only: Change an embedded file given its entry number or name. All parameters are optional. Letting them default leads to a no-operation. + + :arg int/str item: index or name of entry. An integer must be in `range(embfile_count())`. + :arg bytes,bytearray,BytesIO buffer: the new file content. + + *(Changed in v1.14.13)* *io.BytesIO* is now also supported. + + :arg str filename: the new filename. + :arg str ufilename: the new unicode filename. + :arg str desc: the new description. + + *(Changed in v1.18.13)* The method now returns the :data:`xref` of the file object. + + :rtype: int + :returns: xref of the file object. Automatically, its `/ModDate` PDF key will be updated with the current date-time. + + + .. method:: close() + + Release objects and space allocations associated with the document. If created from a file, also closes *filename* (releasing control to the OS). Explicitly closing a document is equivalent to deleting it, `del doc`, or assigning it to something else like `doc = None`. + + .. method:: xref_object(xref, compressed=False, ascii=False) + + * New in v1.16.8 + * Changed in v1.18.10 + + PDF only: Return the definition source of a PDF object. + + :arg int xref: the object's :data`xref`. *Changed in v1.18.10:* A value of -1 returns the PDF trailer source. + :arg bool compressed: whether to generate a compact output with no line breaks or spaces. + :arg bool ascii: whether to ASCII-encode binary data. + + :rtype: str + :returns: The object definition source. + + .. method:: pdf_catalog() + + * New in v1.16.8 + + PDF only: Return the :data:`xref` number of the PDF catalog (or root) object. Use that number with :meth:`Document.xref_object` to see its source. + + + .. method:: pdf_trailer(compressed=False) + + * New in v1.16.8 + + PDF only: Return the trailer source of the PDF, which is usually located at the PDF file's end. This is :meth:`Document.xref_object` with an *xref* argument of -1. + + + .. method:: xref_stream(xref) + + * New in v1.16.8 + + PDF only: Return the **decompressed** contents of the :data:`xref` stream object. + + :arg int xref: :data:`xref` number. + + :rtype: bytes + :returns: the (decompressed) stream of the object. + + .. method:: xref_stream_raw(xref) + + * New in v1.16.8 + + PDF only: Return the **unmodified** (esp. **not decompressed**) contents of the :data:`xref` stream object. Otherwise equal to :meth:`Document.xref_stream`. + + :rtype: bytes + :returns: the (original, unmodified) stream of the object. + + .. method:: update_object(xref, obj_str, page=None) + + * New in v1.16.8 + + PDF only: Replace object definition of :data:`xref` with the provided string. The xref may also be new, in which case this instruction completes the object definition. If a page object is also given, its links and annotations will be reloaded afterwards. + + :arg int xref: :data:`xref` number. + + :arg str obj_str: a string containing a valid PDF object definition. + + :arg page: a page object. If provided, indicates, that annotations of this page should be refreshed (reloaded) to reflect changes incurred with links and / or annotations. + :type page: :ref:`Page` + + :rtype: int + :returns: zero if successful, otherwise an exception will be raised. + + + .. method:: update_stream(xref, data, new=False, compress=True) + + * New in v.1.16.8 + * Changed in v1.19.2: added parameter "compress" + * Changed in v1.19.6: deprecated parameter "new". Now confirms that the object is a PDF dictionary object. + + Replace the stream of an object identified by *xref*, which must be a PDF dictionary. If the object is no :data:`stream`, it will be turned into one. The function automatically performs a compress operation ("deflate") where beneficial. + + :arg int xref: :data:`xref` number. + + :arg bytes|bytearray|BytesIO stream: the new content of the stream. + + *(Changed in v1.14.13:)* *io.BytesIO* objects are now also supported. + + :arg bool new: *deprecated* and ignored. Will be removed some time after v1.20.0. + :arg bool compress: whether to compress the inserted stream. If `True` (default), the stream will be inserted using `/FlateDecode` compression (if beneficial), otherwise the stream will inserted as is. + + :raises ValueError: if *xref* does not represent a PDF :data:`dict`. An empty dictionary `<<>>` is accepted. So if you just created the xref and want to give it a stream, first execute `doc.update_object(xref, "<<>>")`, and then insert the stream data with this method. + + The method is primarily (but not exclusively) intended to manipulate streams containing PDF operator syntax (see pp. 643 of the :ref:`AdobeManual`) as it is the case for e.g. page content streams. + + If you update a contents stream, consider using save parameter *clean=True* to ensure consistency between PDF operator source and the object structure. + + Example: Let us assume that you no longer want a certain image appear on a page. This can be achieved by deleting the respective reference in its contents source(s) -- and indeed: the image will be gone after reloading the page. But the page's :data:`resources` object would still show the image as being referenced by the page. This save option will clean up any such mismatches. + + + .. method:: Document.xref_copy(source, target, *, keep=None) + + * New in v1.19.5 + + PDF Only: Make *target* xref an exact copy of *source*. If *source* is a :data:`stream`, then these data are also copied. + + :arg int source: the source :data:`xref`. It must be an existing **dictionary** object. + :arg int target: the target xref. Must be an existing **dictionary** object. If the xref has just been created, make sure to initialize it as a PDF dictionary with the minimum specification `<<>>`. + :arg list keep: an optional list of top-level keys in *target*, that should not be removed in preparation of the copy process. + + .. note:: + + * This method has much in common with Python's *dict* method `copy()`. + * Both xref numbers must represent existing dictionaries. + * Before data is copied from *source*, all *target* dictionary keys are deleted. You can specify exceptions from this in the *keep* list. If *source* however has a same-named key, its value will still replace the target. + * If *source* is a :data:`stream` object, then these data will also be copied over, and *target* will be converted to a stream object. + * A typical use case is to replace or remove an existing image without using redaction annotations. Example scripts can be seen `here `_. + + .. method:: Document.extract_image(xref) + + PDF Only: Extract data and meta information of an image stored in the document. The output can directly be used to be stored as an image file, as input for PIL, :ref:`Pixmap` creation, etc. This method avoids using pixmaps wherever possible to present the image in its original format (e.g. as JPEG). + + :arg int xref: :data:`xref` of an image object. If this is not in `range(1, doc.xref_length())`, or the object is no image or other errors occur, `None` is returned and no exception is raised. + + :rtype: dict + :returns: a dictionary with the following keys + + * *ext* (*str*) image type (e.g. *'jpeg'*), usable as image file extension + * *smask* (*int*) :data:`xref` number of a stencil (/SMask) image or zero + * *width* (*int*) image width + * *height* (*int*) image height + * *colorspace* (*int*) the image's *colorspace.n* number. + * *cs-name* (*str*) the image's *colorspace.name*. + * *xres* (*int*) resolution in x direction. Please also see :data:`resolution`. + * *yres* (*int*) resolution in y direction. Please also see :data:`resolution`. + * *image* (*bytes*) image data, usable as image file content + + >>> d = doc.extract_image(1373) + >>> d + {'ext': 'png', 'smask': 2934, 'width': 5, 'height': 629, 'colorspace': 3, 'xres': 96, + 'yres': 96, 'cs-name': 'DeviceRGB', + 'image': b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x05\ ...'} + >>> imgout = open(f"image.{d['ext']}", "wb") + >>> imgout.write(d["image"]) + 102 + >>> imgout.close() + + .. note:: There is a functional overlap with *pix = fitz.Pixmap(doc, xref)*, followed by a *pix.tobytes()*. Main differences are that extract_image, **(1)** does not always deliver PNG image formats, **(2)** is **very** much faster with non-PNG images, **(3)** usually results in much less disk storage for extracted images, **(4)** returns `None` in error cases (generates no exception). Look at the following example images within the same PDF. + + * xref 1268 is a PNG -- Comparable execution time and identical output:: + + In [23]: %timeit pix = fitz.Pixmap(doc, 1268);pix.tobytes() + 10.8 ms ± 52.4 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) + In [24]: len(pix.tobytes()) + Out[24]: 21462 + + In [25]: %timeit img = doc.extract_image(1268) + 10.8 ms ± 86 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) + In [26]: len(img["image"]) + Out[26]: 21462 + + * xref 1186 is a JPEG -- :meth:`Document.extract_image` is **many times faster** and produces a **much smaller** output (2.48 MB vs. 0.35 MB):: + + In [27]: %timeit pix = fitz.Pixmap(doc, 1186);pix.tobytes() + 341 ms ± 2.86 ms per loop (mean ± std. dev. of 7 runs, 1 loop each) + In [28]: len(pix.tobytes()) + Out[28]: 2599433 + + In [29]: %timeit img = doc.extract_image(1186) + 15.7 µs ± 116 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each) + In [30]: len(img["image"]) + Out[30]: 371177 + + + .. method:: Document.extract_font(xref, info_only=False, named=None) + + * Changed in v1.19.4: return a dictionary if `named == True`. + + PDF Only: Return an embedded font file's data and appropriate file extension. This can be used to store the font as an external file. The method does not throw exceptions (other than via checking for PDF and valid :data:`xref`). + + :arg int xref: PDF object number of the font to extract. + :arg bool info_only: only return font information, not the buffer. To be used for information-only purposes, avoids allocation of large buffer areas. + :arg bool named: If true, a dictionary with the following keys is returned: 'name' (font base name), 'ext' (font file extension), 'type' (font type), 'content' (font file content). + + :rtype: tuple,dict + :returns: a tuple `(basename, ext, type, content)`, where *ext* is a 3-byte suggested file extension (*str*), *basename* is the font's name (*str*), *type* is the font's type (e.g. "Type1") and *content* is a bytes object containing the font file's content (or *b""*). For possible extension values and their meaning see :ref:`FontExtensions`. Return details on error: + + * `("", "", "", b"")` -- invalid xref or xref is not a (valid) font object. + * `(basename, "n/a", "Type1", b"")` -- *basename* is not embedded and thus cannot be extracted. This is the case for e.g. the :ref:`Base-14-Fonts` and Type 3 fonts. + + Example: + + >>> # store font as an external file + >>> name, ext, _, content = doc.extract_font(4711) + >>> # assuming content is not None: + >>> ofile = open(name + "." + ext, "wb") + >>> ofile.write(content) + >>> ofile.close() + + .. warning:: The basename is returned unchanged from the PDF. So it may contain characters (such as blanks) which may disqualify it as a filename for your operating system. Take appropriate action. + + .. note:: + * The returned *basename* in general is **not** the original file name, but it probably has some similarity. + * If parameter `named == True`, a dictionary with the following keys is returned: `{'name': 'T1', 'ext': 'n/a', 'type': 'Type3', 'content': b''}`. + + + .. method:: xref_xml_metadata() + + * New in v1.16.8 + + PDF only: Return the :data:`xref` of the document's XML metadata. + + + .. method:: has_links() + + .. method:: has_annots() + + * New in v1.18.7 + + PDF only: Check whether there are links, resp. annotations anywhere in the document. + + :returns: *True* / *False*. As opposed to fields, which are also stored in a central place of a PDF document, the existence of links / annotations can only be detected by parsing each page. These methods are tuned to do this efficiently and will immediately return, if the answer is *True* for a page. For PDFs with many thousand pages however, an answer may take some time [#f6]_ if no link, resp. no annotation is found. + + + .. method:: subset_fonts() + + * New in v1.18.7, changed in v1.18.9 + + PDF only: Investigate eligible fonts for their use by text in the document. If a font is supported and a size reduction is possible, that font is replaced by a version with a character subset. + + Use this method immediately before saving the document. The following features and restrictions apply for the time being: + + * Package `fontTools `_ **must be installed**. It is required for creating the font subsets. If not installed, the method raises an `ImportError` exception. + * Supported font types only include embedded OTF, TTF and WOFF that are **not already subsets**. + * **Changed in v1.18.9:** A subset font directly replaces its original -- text remains untouched and **is not rewritten.** It thus should retain all its properties, like spacing, hiddenness, control by Optional Content, etc. + + The greatest benefit can be achieved when creating new PDFs using large fonts like is typical for Asian scripts. In these cases, the set of actually used unicodes mostly is small compared to the number of glyphs in the font. Using this feature can easily reduce the embedded font binary by two orders of magnitude -- from several megabytes to a low two-digit kilobyte amount. + + + .. method:: journal_enable() + + * New in v1.19.0 + + PDF only: Enable journalling. Use this before you start logging operations. + + .. method:: journal_start_op(name) + + * New in v1.19.0 + + PDF only: Start journalling an *"operation"* identified by a string "name". Updates will fail for a journal-enabled PDF, if no operation has been started. + + + .. method:: journal_stop_op() + + * New in v1.19.0 + + PDF only: Stop the current operation. The updates between start and stop of an operation belong to the same unit of work and will be undone / redone together. + + + .. method:: journal_position() + + * New in v1.19.0 + + PDF only: Return the numbers of the current operation and the total operation count. + + :returns: a tuple `(step, steps)` containing the current operation number and the total number of operations in the journal. If **step** is 0, we are at the top of the journal. If **step** equals **steps**, we are at the bottom. Updating the PDF with anything other than undo or redo will automatically remove all journal entries after the current one and the new update will become the new last entry in the journal. The updates corresponding to the removed journal entries will be permanently lost. + + + .. method:: journal_op_name(step) + + * New in v1.19.0 + + PDF only: Return the name of operation number *step.* + + + .. method:: journal_can_do() + + * New in v1.19.0 + + PDF only: Show whether forward ("redo") and / or backward ("undo") executions are possible from the current journal position. + + :returns: a dictionary `{"undo": bool, "redo": bool}`. The respective method is available if its value is `True`. + + + .. method:: journal_undo() + + * New in v1.19.0 + + PDF only: Revert (undo) the current step in the journal. This moves towards the journal's top. + + + .. method:: journal_redo() + + * New in v1.19.0 + + PDF only: Re-apply (redo) the current step in the journal. This moves towards the journal's bottom. + + + .. method:: journal_save(filename) + + * New in v1.19.0 + + PDF only: Save the journal to a file. + + :arg str,fp filename: either a filename as string or a file object opened as "wb" (or an `io.BytesIO()` object). + + + .. method:: journal_load(filename) + + * New in v1.19.0 + + PDF only: Load journal from a file. Enables journalling for the document. If journalling is already enabled, an exception is raised. + + :arg str,fp filename: the filename (str) of the journal or a file object opened as "rb" (or an `io.BytesIO()` object). + + + .. method:: save_snapshot() + + * New in v1.19.0 + + PDF only: Saves a "snapshot" of the document. This is a PDF document with a special, incremental-save format compatible with journalling -- therefore no save options are available. Saving a snapshot is not possible for new documents. + + This is a normal PDF document with no usage restrictions whatsoever. If it is not being changed in any way, it can be used together with its journal to undo / redo operations or continue updating. + + + .. attribute:: outline + + Contains the first :ref:`Outline` entry of the document (or `None`). Can be used as a starting point to walk through all outline items. Accessing this property for encrypted, not authenticated documents will raise an *AttributeError*. + + :type: :ref:`Outline` + + .. attribute:: is_closed + + *False* if document is still open. If closed, most other attributes and methods will have been deleted / disabled. In addition, :ref:`Page` objects referring to this document (i.e. created with :meth:`Document.load_page`) and their dependent objects will no longer be usable. For reference purposes, :attr:`Document.name` still exists and will contain the filename of the original document (if applicable). + + :type: bool + + .. attribute:: is_dirty + + *True* if this is a PDF document and contains unsaved changes, else *False*. + + :type: bool + + .. attribute:: is_pdf + + *True* if this is a PDF document, else *False*. + + :type: bool + + .. attribute:: is_form_pdf + + *False* if this is not a PDF or has no form fields, otherwise the number of root form fields (fields with no ancestors). + + *(Changed in v1.16.4)* Returns the total number of (root) form fields. + + :type: bool,int + + .. attribute:: is_reflowable + + *True* if document has a variable page layout (like e-books or HTML). In this case you can set the desired page dimensions during document creation (open) or via method :meth:`layout`. + + :type: bool + + .. attribute:: is_repaired + + * New in v1.18.2 + + *True* if PDF has been repaired during open (because of major structure issues). Always *False* for non-PDF documents. If true, more details have been stored in `TOOLS.mupdf_warnings()`, and :meth:`Document.can_save_incrementally` will return *False*. + + :type: bool + + .. attribute:: is_fast_webaccess + + * New in v1.22.2 + + *True* if PDF is in linearized format. *False* for non-PDF documents. + + :type: bool + + .. attribute:: markinfo + + * New in v1.22.2 + + A dictionary indicating the `/MarkInfo` value. If not specified, the empty dictionary is returned. If not a PDF, `None` is returned. + + :type: dict + + .. attribute:: pagemode + + * New in v1.22.2 + + A string containing the `/PageMode` value. If not specified, the default "UseNone" is returned. If not a PDF, `None` is returned. + + :type: str + + .. attribute:: pagelayout + + * New in v1.22.2 + + A string containing the `/PageLayout` value. If not specified, the default "SinglePage" is returned. If not a PDF, `None` is returned. + + :type: str + + .. attribute:: version_count + + * New in v1.22.2 + + An integer counting the number of versions present in the document. Zero if not a PDF, otherwise the number of incremental saves plus one. + + :type: int + + .. attribute:: needs_pass + + Indicates whether the document is password-protected against access. This indicator remains unchanged -- **even after the document has been authenticated**. Precludes incremental saves if true. + + :type: bool + + .. attribute:: is_encrypted + + This indicator initially equals :attr:`Document.needs_pass`. After successful authentication, it is set to *False* to reflect the situation. + + :type: bool + + .. attribute:: permissions + + * Changed in v1.16.0: This is now an integer comprised of bit indicators. Was a dictionary previously. + + Contains the permissions to access the document. This is an integer containing bool values in respective bit positions. For example, if *doc.permissions & fitz.PDF_PERM_MODIFY > 0*, you may change the document. See :ref:`PermissionCodes` for details. + + :type: int + + .. attribute:: metadata + + Contains the document's meta data as a Python dictionary or `None` (if *is_encrypted=True* and *needPass=True*). Keys are *format*, *encryption*, *title*, *author*, *subject*, *keywords*, *creator*, *producer*, *creationDate*, *modDate*, *trapped*. All item values are strings or `None`. + + Except *format* and *encryption*, for PDF documents, the key names correspond in an obvious way to the PDF keys */Creator*, */Producer*, */CreationDate*, */ModDate*, */Title*, */Author*, */Subject*, */Trapped* and */Keywords* respectively. + + - *format* contains the document format (e.g. 'PDF-1.6', 'XPS', 'EPUB'). + + - *encryption* either contains `None` (no encryption), or a string naming an encryption method (e.g. *'Standard V4 R4 128-bit RC4'*). Note that an encryption method may be specified **even if** *needs_pass=False*. In such cases not all permissions will probably have been granted. Check :attr:`Document.permissions` for details. + + - If the date fields contain valid data (which need not be the case at all!), they are strings in the PDF-specific timestamp format "D:", where + + - is the 12 character ISO timestamp *YYYYMMDDhhmmss* (*YYYY* - year, *MM* - month, *DD* - day, *hh* - hour, *mm* - minute, *ss* - second), and + + - is a time zone value (time interval relative to GMT) containing a sign ('+' or '-'), the hour (*hh*), and the minute (*'mm'*, note the apostrophes!). + + - A Paraguayan value might hence look like *D:20150415131602-04'00'*, which corresponds to the timestamp April 15, 2015, at 1:16:02 pm local time Asuncion. + + :type: dict + + .. Attribute:: name + + Contains the *filename* or *filetype* value with which *Document* was created. + + :type: str + + .. Attribute:: page_count + + Contains the number of pages of the document. May return 0 for documents with no pages. Function `len(doc)` will also deliver this result. + + :type: int + + .. Attribute:: chapter_count + + * New in v1.17.0 + + Contains the number of chapters in the document. Always at least 1. Relevant only for document types with chapter support (EPUB currently). Other documents will return 1. + + :type: int + + .. Attribute:: last_location + + * New in v1.17.0 + + Contains (chapter, pno) of the document's last page. Relevant only for document types with chapter support (EPUB currently). Other documents will return `(0, page_count - 1)` and `(0, -1)` if it has no pages. + + :type: int + + .. Attribute:: FormFonts + + A list of form field font names defined in the */AcroForm* object. `None` if not a PDF. + + :type: list + +.. NOTE:: For methods that change the structure of a PDF (:meth:`insert_pdf`, :meth:`select`, :meth:`copy_page`, :meth:`delete_page` and others), be aware that objects or properties in your program may have been invalidated or orphaned. Examples are :ref:`Page` objects and their children (links, annotations, widgets), variables holding old page counts, tables of content and the like. Remember to keep such variables up to date or delete orphaned objects. Also refer to :ref:`ReferenialIntegrity`. + +:meth:`set_metadata` Example +------------------------------- +Clear metadata information. If you do this out of privacy / data protection concerns, make sure you save the document as a new file with *garbage > 0*. Only then the old */Info* object will also be physically removed from the file. In this case, you may also want to clear any XML metadata inserted by several PDF editors: + +>>> import fitz +>>> doc=fitz.open("pymupdf.pdf") +>>> doc.metadata # look at what we currently have +{'producer': 'rst2pdf, reportlab', 'format': 'PDF 1.4', 'encryption': None, 'author': +'Jorj X. McKie', 'modDate': "D:20160611145816-04'00'", 'keywords': 'PDF, XPS, EPUB, CBZ', +'title': 'The PyMuPDF Documentation', 'creationDate': "D:20160611145816-04'00'", +'creator': 'sphinx', 'subject': 'PyMuPDF 1.9.1'} +>>> doc.set_metadata({}) # clear all fields +>>> doc.metadata # look again to show what happened +{'producer': 'none', 'format': 'PDF 1.4', 'encryption': None, 'author': 'none', +'modDate': 'none', 'keywords': 'none', 'title': 'none', 'creationDate': 'none', +'creator': 'none', 'subject': 'none'} +>>> doc._delXmlMetadata() # clear any XML metadata +>>> doc.save("anonymous.pdf", garbage = 4) # save anonymized doc + +:meth:`set_toc` Demonstration +---------------------------------- +This shows how to modify or add a table of contents. Also have a look at `import.py `_ and `export.py `_ in the examples directory. + +>>> import fitz +>>> doc = fitz.open("test.pdf") +>>> toc = doc.get_toc() +>>> for t in toc: print(t) # show what we have +[1, 'The PyMuPDF Documentation', 1] +[2, 'Introduction', 1] +[3, 'Note on the Name fitz', 1] +[3, 'License', 1] +>>> toc[1][1] += " modified by set_toc" # modify something +>>> doc.set_toc(toc) # replace outline tree +3 # number of bookmarks inserted +>>> for t in doc.get_toc(): print(t) # demonstrate it worked +[1, 'The PyMuPDF Documentation', 1] +[2, 'Introduction modified by set_toc', 1] # <<< this has changed +[3, 'Note on the Name fitz', 1] +[3, 'License', 1] + +:meth:`insert_pdf` Examples +---------------------------- +**(1) Concatenate two documents including their TOCs:** + +>>> doc1 = fitz.open("file1.pdf") # must be a PDF +>>> doc2 = fitz.open("file2.pdf") # must be a PDF +>>> pages1 = len(doc1) # save doc1's page count +>>> toc1 = doc1.get_toc(False) # save TOC 1 +>>> toc2 = doc2.get_toc(False) # save TOC 2 +>>> doc1.insert_pdf(doc2) # doc2 at end of doc1 +>>> for t in toc2: # increase toc2 page numbers + t[2] += pages1 # by old len(doc1) +>>> doc1.set_toc(toc1 + toc2) # now result has total TOC + +Obviously, similar ways can be found in more general situations. Just make sure that hierarchy levels in a row do not increase by more than one. Inserting dummy bookmarks before and after *toc2* segments would heal such cases. A ready-to-use GUI (wxPython) solution can be found in script `join.py `_ of the examples directory. + +**(2) More examples:** + +>>> # insert 5 pages of doc2, where its page 21 becomes page 15 in doc1 +>>> doc1.insert_pdf(doc2, from_page=21, to_page=25, start_at=15) + +>>> # same example, but pages are rotated and copied in reverse order +>>> doc1.insert_pdf(doc2, from_page=25, to_page=21, start_at=15, rotate=90) + +>>> # put copied pages in front of doc1 +>>> doc1.insert_pdf(doc2, from_page=21, to_page=25, start_at=0) + +Other Examples +---------------- +**Extract all page-referenced images of a PDF into separate PNG files**:: + + for i in range(doc.page_count): + imglist = doc.get_page_images(i) + for img in imglist: + xref = img[0] # xref number + pix = fitz.Pixmap(doc, xref) # make pixmap from image + if pix.n - pix.alpha < 4: # can be saved as PNG + pix.save("p%s-%s.png" % (i, xref)) + else: # CMYK: must convert first + pix0 = fitz.Pixmap(fitz.csRGB, pix) + pix0.save("p%s-%s.png" % (i, xref)) + pix0 = None # free Pixmap resources + pix = None # free Pixmap resources + +**Rotate all pages of a PDF:** + +>>> for page in doc: page.set_rotation(90) + +.. rubric:: Footnotes + +.. [#f1] Content streams describe what (e.g. text or images) appears where and how on a page. PDF uses a specialized mini language similar to PostScript to do this (pp. 643 in :ref:`AdobeManual`), which gets interpreted when a page is loaded. + +.. [#f2] However, you **can** use :meth:`Document.get_toc` and :meth:`Page.get_links` (which are available for all document types) and copy this information over to the output PDF. See demo `convert.py `_. + +.. [#f3] For applicable (EPUB) document types, loading a page via its absolute number may result in layouting a large part of the document, before the page can be accessed. To avoid this performance impact, prefer chapter-based access. Use convenience methods and attributes :meth:`Document.next_location`, :meth:`Document.prev_location` and :attr:`Document.last_location` for maintaining a high level of coding efficiency. + +.. [#f4] These parameters cause separate handling of stream categories: use it together with `expand` to restrict decompression to streams other than images / fontfiles. + +.. [#f5] Examples for "Form XObjects" are created by :meth:`Page.show_pdf_page`. + +.. [#f6] For a *False* the **complete document** must be scanned. Both methods **do not load pages,** but only scan object definitions. This makes them at least 10 times faster than application-level loops (where total response time roughly equals the time for loading all pages). For the :ref:`AdobeManual` (756 pages) and the Pandas documentation (over 3070 pages) -- both have no annotations -- the method needs about 11 ms for the answer *False*. So response times will probably become significant only well beyond this order of magnitude. + +.. [#f7] This only works under certain conditions. For example, if there is normal text covered by some image on top of it, then this is undetectable and the respective text is **not** removed. Similar is true for white text on white background, and so on. + +.. include:: footer.rst diff --git a/docs/extensions/__init__.py b/docs/extensions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/extensions/fulltoc.py b/docs/extensions/fulltoc.py new file mode 100644 index 0000000..da2aa8f --- /dev/null +++ b/docs/extensions/fulltoc.py @@ -0,0 +1,98 @@ +# -*- encoding: utf-8 -*- +# +# Copyright © 2012 New Dream Network, LLC (DreamHost) +# +# Author: Doug Hellmann +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + + +from sphinx import addnodes + + +def html_page_context(app, pagename, templatename, context, doctree): + """Event handler for the html-page-context signal. + Modifies the context directly. + - Replaces the 'toc' value created by the HTML builder with one + that shows all document titles and the local table of contents. + - Sets display_toc to True so the table of contents is always + displayed, even on empty pages. + - Replaces the 'toctree' function with one that uses the entire + document structure, ignores the maxdepth argument, and uses + only prune and collapse. + """ + rendered_toc = get_rendered_toctree(app.builder, pagename) + context["toc"] = rendered_toc + context["display_toc"] = True # force toctree to display + + if "toctree" not in context: + # json builder doesn't use toctree func, so nothing to replace + return + + def make_toctree(collapse=True, maxdepth=-1, includehidden=True): + return get_rendered_toctree( + app.builder, + pagename, + prune=False, + collapse=collapse, + ) + + context["toctree"] = make_toctree + + +def get_rendered_toctree(builder, docname, prune=False, collapse=True): + """Build the toctree relative to the named document, + with the given parameters, and then return the rendered + HTML fragment. + """ + fulltoc = build_full_toctree( + builder, + docname, + prune=prune, + collapse=collapse, + ) + rendered_toc = builder.render_partial(fulltoc)["fragment"] + return rendered_toc + + +def build_full_toctree(builder, docname, prune, collapse): + """Return a single toctree starting from docname containing all + sub-document doctrees. + """ + env = builder.env + doctree = env.get_doctree(env.config.master_doc) + toctrees = [] + for toctreenode in doctree.traverse(addnodes.toctree): + toctree = env.resolve_toctree( + docname, + builder, + toctreenode, + collapse=collapse, + prune=prune, + includehidden=True, + ) + if toctree is not None: + toctrees.append(toctree) + + if not toctrees: + return None + result = toctrees[0] + for toctree in toctrees[1:]: + if toctree: + result.extend(toctree.children) + env.resolve_references(result, docname, builder) + return result + + +def setup(app): + app.connect("html-page-context", html_page_context) diff --git a/docs/extensions/searchrepair.py b/docs/extensions/searchrepair.py new file mode 100644 index 0000000..c8e344c --- /dev/null +++ b/docs/extensions/searchrepair.py @@ -0,0 +1,23 @@ +import os + + +def modify_search_index(app, exception): + if exception is None: # build succeeded + filename = os.path.join(app.outdir, "searchindex.js") + if os.path.exists(filename): + searchfile = open(filename) + data1 = searchfile.read() + searchfile.close() + p1 = data1.find("filenames:[") + p2 = data1.find("]", p1) + s = data1[p1:p2].replace(".txt", "") + data2 = data1[:p1] + data2 += s + data2 += data1[p2:] + searchfile = open(filename, "w") + searchfile.write(data2) + searchfile.close() + + +def setup(app): + app.connect("build-finished", modify_search_index) diff --git a/docs/faq.rst b/docs/faq.rst new file mode 100644 index 0000000..8011ecd --- /dev/null +++ b/docs/faq.rst @@ -0,0 +1,17 @@ +.. include:: header.rst + +.. _FAQ: + +============================== +FAQ +============================== + +A collection of recipes in “How-To” format for using PyMuPDF. + + +Please see: + +:ref:`Recipes: Table of Contents` + + +.. include:: footer.rst diff --git a/docs/font.rst b/docs/font.rst new file mode 100644 index 0000000..355db87 --- /dev/null +++ b/docs/font.rst @@ -0,0 +1,366 @@ +.. include:: header.rst + +.. _Font: + +================ +Font +================ + +* New in v1.16.18 + +This class represents a font as defined in MuPDF (*fz_font_s* structure). It is required for the new class :ref:`TextWriter` and the new :meth:`Page.write_text`. Currently, it has no connection to how fonts are used in methods :meth:`Page.insert_text` or :meth:`Page.insert_textbox`, respectively. + +A Font object also contains useful general information, like the font bbox, the number of defined glyphs, glyph names or the bbox of a single glyph. + + +==================================== ============================================ +**Method / Attribute** **Short Description** +==================================== ============================================ +:meth:`~Font.glyph_advance` Width of a character +:meth:`~Font.glyph_bbox` Glyph rectangle +:meth:`~Font.glyph_name_to_unicode` Get unicode from glyph name +:meth:`~Font.has_glyph` Return glyph id of unicode +:meth:`~Font.text_length` Compute string length +:meth:`~Font.char_lengths` Tuple of char widths of a string +:meth:`~Font.unicode_to_glyph_name` Get glyph name of a unicode +:meth:`~Font.valid_codepoints` Array of supported unicodes +:attr:`~Font.ascender` Font ascender +:attr:`~Font.descender` Font descender +:attr:`~Font.bbox` Font rectangle +:attr:`~Font.buffer` Copy of the font's binary image +:attr:`~Font.flags` Collection of font properties +:attr:`~Font.glyph_count` Number of supported glyphs +:attr:`~Font.name` Name of font +:attr:`~Font.is_writable` Font usable with :ref:`TextWriter` +==================================== ============================================ + + +**Class API** + +.. class:: Font + + .. index:: + pair: Font, fontfile + pair: Font, fontbuffer + pair: Font, script + pair: Font, ordering + pair: Font, is_bold + pair: Font, is_italic + pair: Font, is_serif + pair: Font, fontname + pair: Font, language + + .. method:: __init__(self, fontname=None, fontfile=None, + fontbuffer=None, script=0, language=None, ordering=-1, is_bold=0, + is_italic=0, is_serif=0) + + Font constructor. The large number of parameters are used to locate font, which most closely resembles the requirements. Not all parameters are ever required -- see the below pseudo code explaining the logic how the parameters are evaluated. + + :arg str fontname: one of the :ref:`Base-14-Fonts` or CJK fontnames. Also possible are a select few other names like (watch the correct spelling): "Arial", "Times", "Times Roman". + + *(Changed in v1.17.5)* + + If you have installed `pymupdf-fonts `_, there are also new "reserved" fontnames available, which are listed in :attr:`fitz_fonts` and in the table further down. + + :arg str fontfile: the filename of a fontfile somewhere on your system [#f1]_. + :arg bytes,bytearray,io.BytesIO fontbuffer: a fontfile loaded in memory [#f1]_. + :arg in script: the number of a UCDN script. Currently supported in PyMuPDF are numbers 24, and 32 through 35. + :arg str language: one of the values "zh-Hant" (traditional Chinese), "zh-Hans" (simplified Chinese), "ja" (Japanese) and "ko" (Korean). Otherwise, all ISO 639 codes from the subsets 1, 2, 3 and 5 are also possible, but are currently documentary only. + :arg int ordering: an alternative selector for one of the CJK fonts. + :arg bool is_bold: look for a bold font. + :arg bool is_italic: look for an italic font. + :arg bool is_serif: look for a serifed font. + + :returns: a MuPDF font if successful. This is the overall sequence of checks to determine an appropriate font: + + =========== ============================================================ + Argument Action + =========== ============================================================ + fontfile? Create font from file, exception if failure. + fontbuffer? Create font from buffer, exception if failure. + ordering>=0 Create universal font, always succeeds. + fontname? Create a Base-14 font, universal font, or font + provided by `pymupdf-fonts `_. See table below. + =========== ============================================================ + + + .. note:: + + With the usual reserved names "helv", "tiro", etc., you will create fonts with the expected names "Helvetica", "Times-Roman" and so on. **However**, and in contrast to :meth:`Page.insert_font` and friends, + + * a font file will **always** be embedded in your PDF, + * Greek and Cyrillic characters are supported without needing the *encoding* parameter. + + Using *ordering >= 0*, or fontnames "cjk", "china-t", "china-s", "japan" or "korea" will **always create the same "universal"** font **"Droid Sans Fallback Regular"**. This font supports **all Chinese, Japanese, Korean and Latin characters**, including Greek and Cyrillic. This is a sans-serif font. + + Actually, you would rarely ever need another sans-serif font than **"Droid Sans Fallback Regular"**. **Except** that this font file is relatively large and adds about 1.65 MB (compressed) to your PDF file size. If you do not need CJK support, stick with specifying "helv", "tiro" etc., and you will get away with about 35 KB compressed. + + If you **know** you have a mixture of CJK and Latin text, consider just using `Font("cjk")` because this supports everything and also significantly (by a factor of up to three) speeds up execution: MuPDF will always find any character in this single font and never needs to check fallbacks. + + But if you do use some other font, you will still automatically be able to also write CJK characters: MuPDF detects this situation and silently falls back to the universal font (which will then of course also be embedded in your PDF). + + *(New in v1.17.5)* Optionally, some new "reserved" fontname codes become available if you install `pymupdf-fonts `_, `pip install pymupdf-fonts`. **"Fira Mono"** is a mono-spaced sans font set and **FiraGO** is another non-serifed "universal" font set which supports all Latin (including Cyrillic and Greek) plus Thai, Arabian, Hewbrew and Devanagari -- but none of the CJK languages. The size of a FiraGO font is only a quarter of the "Droid Sans Fallback" size (compressed 400 KB vs. 1.65 MB) -- **and** it provides the weights bold, italic, bold-italic -- which the universal font doesn't. + + **"Space Mono"** is another nice and small mono-spaced font from Google Fonts, which supports Latin Extended characters and comes with all 4 important weights. + + The following table maps a fontname code to the corresponding font. For the current content of the package please see its documentation: + + =========== =========================== ======= ============================= + Code Fontname New in Comment + =========== =========================== ======= ============================= + figo FiraGO Regular v1.0.0 narrower than Helvetica + figbo FiraGO Bold v1.0.0 + figit FiraGO Italic v1.0.0 + figbi FiraGO Bold Italic v1.0.0 + fimo Fira Mono Regular v1.0.0 + fimbo Fira Mono Bold v1.0.0 + spacemo Space Mono Regular v1.0.1 + spacembo Space Mono Bold v1.0.1 + spacemit Space Mono Italic v1.0.1 + spacembi Space Mono Bold-Italic v1.0.1 + math Noto Sans Math Regular v1.0.2 math symbols + music Noto Music Regular v1.0.2 musical symbols + symbol1 Noto Sans Symbols Regular v1.0.2 replacement for "symb" + symbol2 Noto Sans Symbols2 Regular v1.0.2 extended symbol set + notos Noto Sans Regular v1.0.3 alternative to Helvetica + notosit Noto Sans Italic v1.0.3 + notosbo Noto Sans Bold v1.0.3 + notosbi Noto Sans BoldItalic v1.0.3 + =========== =========================== ======= ============================= + + .. index:: + pair: Font.has_glyph, language + pair: Font.has_glyph, script + pair: Font.has_glyph, fallback + + .. method:: has_glyph(chr, language=None, script=0, fallback=False) + + Check whether the unicode *chr* exists in the font or (option) some fallback font. May be used to check whether any "TOFU" symbols will appear on output. + + :arg int chr: the unicode of the character (i.e. *ord()*). + :arg str language: the language -- currently unused. + :arg int script: the UCDN script number. + :arg bool fallback: *(new in v1.17.5)* perform an extended search in fallback fonts or restrict to current font (default). + :returns: *(changed in 1.17.7)* the glyph number. Zero indicates no glyph found. + + .. method:: valid_codepoints() + + * New in v1.17.5 + + Return an array of unicodes supported by this font. + + :returns: an *array.array* [#f2]_ of length at most :attr:`Font.glyph_count`. I.e. *chr()* of every item in this array has a glyph in the font without using fallbacks. This is an example display of the supported glyphs: + + >>> import fitz + >>> font = fitz.Font("math") + >>> vuc = font.valid_codepoints() + >>> for i in vuc: + print("%04X %s (%s)" % (i, chr(i), font.unicode_to_glyph_name(i))) + 0000 + 000D (CR) + 0020 (space) + 0021 ! (exclam) + 0022 " (quotedbl) + 0023 # (numbersign) + 0024 $ (dollar) + 0025 % (percent) + ... + 00AC ¬ (logicalnot) + 00B1 ± (plusminus) + ... + 21D0 ⇐ (arrowdblleft) + 21D1 ⇑ (arrowdblup) + 21D2 ⇒ (arrowdblright) + 21D3 ⇓ (arrowdbldown) + 21D4 ⇔ (arrowdblboth) + ... + 221E ∞ (infinity) + ... + + .. note:: This method only returns meaningful data for fonts having a CMAP (character map, charmap, the `/ToUnicode` PDF key). Otherwise, this array will have length 1 and contain zero only. + + .. index:: + pair: Font.glyph_advance, language + pair: Font.glyph_advance, script + pair: Font.glyph_advance, wmode + + .. method:: glyph_advance(chr, language=None, script=0, wmode=0) + + Calculate the "width" of the character's glyph (visual representation). + + :arg int chr: the unicode number of the character. Use *ord()*, not the character itself. Again, this should normally work even if a character is not supported by that font, because fallback fonts will be checked where necessary. + :arg int wmode: write mode, 0 = horizontal, 1 = vertical. + + The other parameters are not in use currently. + + :returns: a float representing the glyph's width relative to **fontsize 1**. + + .. method:: glyph_name_to_unicode(name) + + Return the unicode value for a given glyph name. Use it in conjunction with `chr()` if you want to output e.g. a certain symbol. + + :arg str name: The name of the glyph. + + :returns: The unicode integer, or 65533 = 0xFFFD if the name is unknown. Examples: `font.glyph_name_to_unicode("Sigma") = 931`, `font.glyph_name_to_unicode("sigma") = 963`. Refer to the `Adobe Glyph List `_ publication for a list of glyph names and their unicode numbers. Example: + + >>> font = fitz.Font("helv") + >>> font.has_glyph(font.glyph_name_to_unicode("infinity")) + True + + .. index:: + pair: Font.glyph_bbox, language + pair: Font.glyph_bbox, script + + .. method:: glyph_bbox(chr, language=None, script=0) + + The glyph rectangle relative to fontsize 1. + + :arg int chr: *ord()* of the character. + + :returns: a :ref:`Rect`. + + + .. method:: unicode_to_glyph_name(ch) + + Show the name of the character's glyph. + + :arg int ch: the unicode number of the character. Use *ord()*, not the character itself. + + :returns: a string representing the glyph's name. E.g. `font.glyph_name(ord("#")) = "numbersign"`. For an invalid code ".notfound" is returned. + + .. note:: *(Changed in v1.18.0)* This method and :meth:`Font.glyph_name_to_unicode` no longer depend on a font and instead retrieve information from the **Adobe Glyph List**. Also available as `fitz.unicode_to_glyph_name()` and resp. `fitz.glyph_name_to_unicode()`. + + .. index:: + pair: text_length, fontsize + + .. method:: text_length(text, fontsize=11) + + Calculate the length in points of a unicode string. + + .. note:: There is a functional overlap with :meth:`get_text_length` for Base-14 fonts only. + + :arg str text: a text string, UTF-8 encoded. + + :arg float fontsize: the fontsize. + + :rtype: float + + :returns: the length of the string in points when stored in the PDF. If a character is not contained in the font, it will automatically be looked up in a fallback font. + + .. note:: This method was originally implemented in Python, based on calling :meth:`Font.glyph_advance`. For performance reasons, it has been rewritten in C for v1.18.14. To compute the width of a single character, you can now use either of the following without performance penalty: + + 1. `font.glyph_advance(ord("Ä")) * fontsize` + 2. `font.text_length("Ä", fontsize=fontsize)` + + For multi-character strings, the method offers a huge performance advantage compared to the previous implementation: instead of about 0.5 microseconds for each character, only 12.5 nanoseconds are required for the second and subsequent ones. + + .. index:: + pair: char_lengths, fontsize + + .. method:: char_lengths(text, fontsize=11) + + *New in v1.18.14* + + Sequence of character lengths in points of a unicode string. + + :arg str text: a text string, UTF-8 encoded. + + :arg float fontsize: the fontsize. + + :rtype: tuple + + :returns: the lengths in points of the characters of a string when stored in the PDF. It works like :meth:`Font.text_length` broken down to single characters. This is a high speed method, used e.g. in :meth:`TextWriter.fill_textbox`. The following is true (allowing rounding errors): `font.text_length(text) == sum(font.char_lengths(text))`. + + >>> font = fitz.Font("helv") + >>> text = "PyMuPDF" + >>> font.text_length(text) + 50.115999937057495 + >>> fitz.get_text_length(text, fontname="helv") + 50.115999937057495 + >>> sum(font.char_lengths(text)) + 50.115999937057495 + >>> pprint(font.char_lengths(text)) + (7.336999952793121, # P + 5.5, # y + 9.163000047206879, # M + 6.115999937057495, # u + 7.336999952793121, # P + 7.942000031471252, # D + 6.721000015735626) # F + + + .. attribute:: buffer + + * New in v1.17.6 + + Copy of the binary font file content. + + :rtype: bytes + + .. attribute:: flags + + A dictionary with various font properties, each represented as bools. Example for Helvetica:: + + >>> pprint(font.flags) + {'bold': 0, + 'fake-bold': 0, + 'fake-italic': 0, + 'invalid-bbox': 0, + 'italic': 0, + 'mono': 0, + 'opentype': 0, + 'serif': 1, + 'stretch': 0, + 'substitute': 0} + + :rtype: dict + + .. attribute:: name + + :rtype: str + + Name of the font. May be "" or "(null)". + + .. attribute:: bbox + + The font bbox. This is the maximum of its glyph bboxes. + + :rtype: :ref:`Rect` + + .. attribute:: glyph_count + + :rtype: int + + The number of glyphs defined in the font. + + .. attribute:: ascender + + * New in v1.18.0 + + The ascender value of the font, see `here `_ for details. Please note that there is a difference to the strict definition: our value includes everything above the baseline -- not just the height difference between upper case "A" and and lower case "a". + + :rtype: float + + .. attribute:: descender + + * New in v1.18.0 + + The descender value of the font, see `here `_ for details. This value always is negative and is the portion that some glyphs descend below the base line, for example "g" or "y". As a consequence, the value `ascender - descender` is the total height, that every glyph of the font fits into. This is true at least for most fonts -- as always, there are exceptions, especially for calligraphic fonts, etc. + + :rtype: float + + .. attribute:: is_writable + + * New in v1.18.0 + + Indicates whether this font can be used with :ref:`TextWriter`. + + :rtype: bool + +.. rubric:: Footnotes + +.. [#f1] MuPDF does not support all fontfiles with this feature and will raise exceptions like *"mupdf: FT_New_Memory_Face((null)): unknown file format"*, if it encounters issues. The :ref:`TextWriter` methods check :attr:`Font.is_writable`. + +.. [#f2] The built-in module *array* has been chosen for its speed and its compact representation of values. + +.. include:: footer.rst diff --git a/docs/footer.rst b/docs/footer.rst new file mode 100644 index 0000000..ee36058 --- /dev/null +++ b/docs/footer.rst @@ -0,0 +1,38 @@ +.. raw:: html + + + +---- + +.. raw:: html + + + +

This software is provided AS-IS with no warranty, either express or implied. This software is distributed under license and may not be copied, modified or distributed except as expressly authorized under the terms of that license. Refer to licensing information at artifex.com or contact Artifex Software Inc., 39 Mesa Street, Suite 108A, San Francisco CA 94129, United States for further information.

+ +.. note - this ensures that the Sphinx build system will pull in the image (as it is referenced in an RST file) to _images, + we don't want to display it via rst markup due to limitations (hence width:0), however we do want it available for our raw HTML + which we use in header.rst. + +.. image:: images/discord-mark-blue.svg + :alt: Discord logo + :width: 0 + :height: 0 + :target: https://discord.gg/TSpYGBW4eq diff --git a/docs/functions.rst b/docs/functions.rst new file mode 100644 index 0000000..c5273c2 --- /dev/null +++ b/docs/functions.rst @@ -0,0 +1,817 @@ +.. include:: header.rst + +============ +Functions +============ +The following are miscellaneous functions and attributes on a fairly low-level technical detail. + +Some functions provide detail access to PDF structures. Others are stripped-down, high performance versions of other functions which provide more information. + +Yet others are handy, general-purpose utilities. + + +==================================== ============================================================== +**Function** **Short Description** +==================================== ============================================================== +:attr:`Annot.apn_bbox` PDF only: bbox of the appearance object +:attr:`Annot.apn_matrix` PDF only: the matrix of the appearance object +:attr:`Page.is_wrapped` check whether contents wrapping is present +:meth:`adobe_glyph_names` list of glyph names defined in **Adobe Glyph List** +:meth:`adobe_glyph_unicodes` list of unicodes defined in **Adobe Glyph List** +:meth:`Annot.clean_contents` PDF only: clean the annot's :data:`contents` object +:meth:`Annot.set_apn_bbox` PDF only: set the bbox of the appearance object +:meth:`Annot.set_apn_matrix` PDF only: set the matrix of the appearance object +:meth:`ConversionHeader` return header string for *get_text* methods +:meth:`ConversionTrailer` return trailer string for *get_text* methods +:meth:`Document.del_xml_metadata` PDF only: remove XML metadata +:meth:`Document.get_char_widths` PDF only: return a list of glyph widths of a font +:meth:`Document.get_new_xref` PDF only: create and return a new :data:`xref` entry +:meth:`Document.is_stream` PDF only: check whether an :data:`xref` is a stream object +:meth:`Document.xml_metadata_xref` PDF only: return XML metadata :data:`xref` number +:meth:`Document.xref_length` PDF only: return length of :data:`xref` table +:meth:`EMPTY_IRECT` return the (standard) empty / invalid rectangle +:meth:`EMPTY_QUAD` return the (standard) empty / invalid quad +:meth:`EMPTY_RECT` return the (standard) empty / invalid rectangle +:meth:`get_pdf_now` return the current timestamp in PDF format +:meth:`get_pdf_str` return PDF-compatible string +:meth:`get_text_length` return string length for a given font & fontsize +:meth:`glyph_name_to_unicode` return unicode from a glyph name +:meth:`image_profile` return a dictionary of basic image properties +:meth:`INFINITE_IRECT` return the (only existing) infinite rectangle +:meth:`INFINITE_QUAD` return the (only existing) infinite quad +:meth:`INFINITE_RECT` return the (only existing) infinite rectangle +:meth:`make_table` split rectangle in sub-rectangles +:meth:`Page.clean_contents` PDF only: clean the page's :data:`contents` objects +:meth:`Page.get_bboxlog` list of rectangles that envelop text, drawing or image objects +:meth:`Page.get_contents` PDF only: return a list of content :data:`xref` numbers +:meth:`Page.get_displaylist` create the page's display list +:meth:`Page.get_text_blocks` extract text blocks as a Python list +:meth:`Page.get_text_words` extract text words as a Python list +:meth:`Page.get_texttrace` low-level text information +:meth:`Page.read_contents` PDF only: get complete, concatenated /Contents source +:meth:`Page.run` run a page through a device +:meth:`Page.set_contents` PDF only: set page's :data:`contents` to some :data:`xref` +:meth:`Page.wrap_contents` wrap contents with stacking commands +:meth:`css_for_pymupdf_font` create CSS source for a font in package pymupdf_fonts +:meth:`paper_rect` return rectangle for a known paper format +:meth:`paper_size` return width, height for a known paper format +:meth:`paper_sizes` dictionary of pre-defined paper formats +:meth:`planish_line` matrix to map a line to the x-axis +:meth:`recover_char_quad` compute the quad of a char ("rawdict") +:meth:`recover_line_quad` compute the quad of a subset of line spans +:meth:`recover_quad` compute the quad of a span ("dict", "rawdict") +:meth:`recover_quad` return the quad for a text span ("dict" / "rawdict") +:meth:`recover_span_quad` compute the quad of a subset of span characters +:meth:`sRGB_to_pdf` return PDF RGB color tuple from an sRGB integer +:meth:`sRGB_to_rgb` return (R, G, B) color tuple from an sRGB integer +:meth:`unicode_to_glyph_name` return glyph name from a unicode +:meth:`get_tessdata` locates the language support of the Tesseract-OCR installation +:attr:`fitz_fontdescriptors` dictionary of available supplement fonts +:attr:`TESSDATA_PREFIX` a copy of `os.environ["TESSDATA_PREFIX"]` +:attr:`pdfcolor` dictionary of almost 500 RGB colors in PDF format. +==================================== ============================================================== + + .. method:: paper_size(s) + + Convenience function to return width and height of a known paper format code. These values are given in pixels for the standard resolution 72 pixels = 1 inch. + + Currently defined formats include **'A0'** through **'A10'**, **'B0'** through **'B10'**, **'C0'** through **'C10'**, **'Card-4x6'**, **'Card-5x7'**, **'Commercial'**, **'Executive'**, **'Invoice'**, **'Ledger'**, **'Legal'**, **'Legal-13'**, **'Letter'**, **'Monarch'** and **'Tabloid-Extra'**, each in either portrait or landscape format. + + A format name must be supplied as a string (case **in** \sensitive), optionally suffixed with "-L" (landscape) or "-P" (portrait). No suffix defaults to portrait. + + :arg str s: any format name from above in upper or lower case, like *"A4"* or *"letter-l"*. + + :rtype: tuple + :returns: *(width, height)* of the paper format. For an unknown format *(-1, -1)* is returned. Examples: *fitz.paper_size("A4")* returns *(595, 842)* and *fitz.paper_size("letter-l")* delivers *(792, 612)*. + +----- + + .. method:: paper_rect(s) + + Convenience function to return a :ref:`Rect` for a known paper format. + + :arg str s: any format name supported by :meth:`paper_size`. + + :rtype: :ref:`Rect` + :returns: *fitz.Rect(0, 0, width, height)* with *width, height=fitz.paper_size(s)*. + + >>> import fitz + >>> fitz.paper_rect("letter-l") + fitz.Rect(0.0, 0.0, 792.0, 612.0) + >>> + +----- + + .. method:: sRGB_to_pdf(srgb) + + *New in v1.17.4* + + Convenience function returning a PDF color triple (red, green, blue) for a given sRGB color integer as it occurs in :meth:`Page.get_text` dictionaries "dict" and "rawdict". + + :arg int srgb: an integer of format RRGGBB, where each color component is an integer in range(255). + + :returns: a tuple (red, green, blue) with float items in interval *0 <= item <= 1* representing the same color. Example `sRGB_to_pdf(0xff0000) = (1, 0, 0)` (red). + +----- + + .. method:: sRGB_to_rgb(srgb) + + *New in v1.17.4* + + Convenience function returning a color (red, green, blue) for a given *sRGB* color integer. + + :arg int srgb: an integer of format RRGGBB, where each color component is an integer in range(255). + + :returns: a tuple (red, green, blue) with integer items in `range(256)` representing the same color. Example `sRGB_to_pdf(0xff0000) = (255, 0, 0)` (red). + +----- + + .. method:: glyph_name_to_unicode(name) + + *New in v1.18.0* + + Return the unicode number of a glyph name based on the **Adobe Glyph List**. + + :arg str name: the name of some glyph. The function is based on the `Adobe Glyph List `_. + + :rtype: int + :returns: the unicode. Invalid *name* entries return `0xfffd (65533)`. + + .. note:: A similar functionality is provided by package `fontTools `_ in its *agl* sub-package. + +----- + + .. method:: unicode_to_glyph_name(ch) + + *New in v1.18.0* + + Return the glyph name of a unicode number, based on the **Adobe Glyph List**. + + :arg int ch: the unicode given by e.g. `ord("ß")`. The function is based on the `Adobe Glyph List `_. + + :rtype: str + :returns: the glyph name. E.g. `fitz.unicode_to_glyph_name(ord("Ä"))` returns `'Adieresis'`. + + .. note:: A similar functionality is provided by package `fontTools `_: in its *agl* sub-package. + +----- + + .. method:: adobe_glyph_names() + + *New in v1.18.0* + + Return a list of glyph names defined in the **Adobe Glyph List**. + + :rtype: list + :returns: list of strings. + + .. note:: A similar functionality is provided by package `fontTools `_ in its *agl* sub-package. + +----- + + .. method:: adobe_glyph_unicodes() + + *New in v1.18.0* + + Return a list of unicodes for there exists a glyph name in the **Adobe Glyph List**. + + :rtype: list + :returns: list of integers. + + .. note:: A similar functionality is provided by package `fontTools `_ in its *agl* sub-package. + +----- + + .. method:: css_for_pymupdf_font(fontcode, *, CSS=None, archive=None, name=None) + + *New in v1.21.0* + + **Utility function for use with "Story" applications.** + + Create CSS `@font-face` items for the given fontcode in pymupdf-fonts. Creates a CSS font-family for all fonts starting with string "fontcode". + + The font naming convention in package pymupdf-fonts is "fontcode", where the suffix "sf" is one of "" (empty), "it"/"i", "bo"/"b" or "bi". These suffixes thus represent the regular, italic, bold or bold-italic variants of that font. + + For example, font code "notos" refers to fonts + + * "notos" - "Noto Sans Regular" + * "notosit" - "Noto Sans Italic" + * "notosbo" - "Noto Sans Bold" + * "notosbi" - "Noto Sans Bold Italic" + + The function creates (up to) four CSS `@font-face` definitions and collectively assigns the `font-family` name "notos" to them (or the "name" value if provided). Associated font buffers are placed / added to the provided archive. + + To use the font in the Python API for :ref:`Story`, execute `.set_font(fontcode)` (or "name" if given). The correct font weight or style will automatically be selected as required. + + For example to replace the "sans-serif" HTML standard (i.e. Helvetica) with the above "notos", execute the following. Whenever "sans-serif" is used (whether explicitly or implicitly), the Noto Sans fonts will be selected. + + `CSS = fitz.css_for_pymupdf_font("notos", name="sans-serif", archive=...)` + + Expects and returns the CSS source, with the new CSS definitions appended. + + :arg str fontcode: one of the font codes present in package `pymupdf-fonts `_ (usually) representing the regular version of the font family. + :arg str CSS: any already existing CSS source, or `None`. The function will append its new definitions to this. This is the string that **must be used** as `user_css` when creating the :ref:`Story`. + :arg archive: :ref:`Archive`, **mandatory**. All font binaries (i.e. up to four) found for "fontcode" will be added to the archive. This is the archive that **must be used** as `archive` when creating the :ref:`Story`. + :arg str name: the name under which the "fontcode" fonts should be found. If omitted, "fontcode" will be used. + + :rtype: str + :returns: Modified CSS, with appended `@font-face` statements for each font variant of fontcode. Fontbuffers associated with "fontcode" will have been added to 'archive'. The function will automatically find up to 4 font variants. All pymupdf-fonts (that are no special purpose like math or music, etc.) have regular, bold, italic and bold-italic variants. To see currently available font codes check `fitz.fitz_fontdescriptors.keys()`. This will show something like `dict_keys(['cascadia', 'cascadiai', 'cascadiab', 'cascadiabi', 'figbo', 'figo', 'figbi', 'figit', 'fimbo', 'fimo', 'spacembo', 'spacembi', 'spacemit', 'spacemo', 'math', 'music', 'symbol1', 'symbol2', 'notosbo', 'notosbi', 'notosit', 'notos', 'ubuntu', 'ubuntubo', 'ubuntubi', 'ubuntuit', 'ubuntm', 'ubuntmbo', 'ubuntmbi', 'ubuntmit'])`. + + Here is a complete snippet for using the "Noto Sans" font instead of "Helvetica":: + + arch = fitz.Archive() + CSS = fitz.css_for_pymupdf_font("notos", name="sans-serif", archive=arch) + story = fitz.Story(user_css=CSS, archive=arch) + + +----- + + .. method:: recover_quad(line_dir, span) + + *New in v1.18.9* + + Convenience function returning the quadrilateral enveloping the text of a text span, as returned by :meth:`Page.get_text` using the "dict" or "rawdict" options. + + :arg tuple line_dict: the value `line["dir"]` of the span's line. + :arg dict span: the span sub-dictionary. + + :returns: the quadrilateral of the span's text. + +----- + +.. _Functions_make_table: + + .. method:: make_table(rect, cols=1, rows=1) + + *New in v1.17.4* + + Convenience function to split a rectangle into sub-rectangles. Returns a list of *rows* lists, each containing *cols* :ref:`Rect` items. Each sub-rectangle can then be addressed by its row and column index. + + :arg rect_like rect: the rectangle to split. + :arg int cols: the desired number of columns. + :arg int rows: the desired number of rows. + :returns: a list of :ref:`Rect` objects of equal size, whose union equals *rect*. Here is the layout of a 3x4 table created by `cell = fitz.make_table(rect, cols=4, rows=3)`: + + .. image:: images/img-make-table.* + :scale: 60 + + +----- + + .. method:: planish_line(p1, p2) + + * New in version 1.16.2)* + + Return a matrix which maps the line from p1 to p2 to the x-axis such that p1 will become (0,0) and p2 a point with the same distance to (0,0). + + :arg point_like p1: starting point of the line. + :arg point_like p2: end point of the line. + + :rtype: :ref:`Matrix` + :returns: a matrix which combines a rotation and a translation:: + + >>> p1 = fitz.Point(1, 1) + >>> p2 = fitz.Point(4, 5) + >>> abs(p2 - p1) # distance of points + 5.0 + >>> m = fitz.planish_line(p1, p2) + >>> p1 * m + Point(0.0, 0.0) + >>> p2 * m + Point(5.0, -5.960464477539063e-08) + >>> # distance of the resulting points + >>> abs(p2 * m - p1 * m) + 5.0 + + + .. image:: images/img-planish.png + :scale: 40 + + +----- + + .. method:: paper_sizes + + A dictionary of pre-defines paper formats. Used as basis for :meth:`paper_size`. + +----- + + .. attribute:: fitz_fontdescriptors + + * New in v1.17.5 + + A dictionary of usable fonts from repository `pymupdf-fonts `_. Items are keyed by their reserved fontname and provide information like this:: + + In [2]: fitz.fitz_fontdescriptors.keys() + Out[2]: dict_keys(['figbo', 'figo', 'figbi', 'figit', 'fimbo', 'fimo', + 'spacembo', 'spacembi', 'spacemit', 'spacemo', 'math', 'music', 'symbol1', + 'symbol2']) + In [3]: fitz.fitz_fontdescriptors["fimo"] + Out[3]: + {'name': 'Fira Mono Regular', + 'size': 125712, + 'mono': True, + 'bold': False, + 'italic': False, + 'serif': True, + 'glyphs': 1485} + + If `pymupdf-fonts` is not installed, the dictionary is empty. + + The dictionary keys can be used to define a :ref:`Font` via e.g. `font = fitz.Font("fimo")` -- just like you can do it with the builtin fonts "Helvetica" and friends. + +----- + + .. attribute:: TESSDATA_PREFIX + + * New in v1.19.4 + + Copy of `os.environ["TESSDATA_PREFIX"]` for convenient checking whether there is integrated Tesseract OCR support. + + If this attribute is `None`, Tesseract-OCR is either not installed, or the environment variable is not set to point to Tesseract's language support folder. + + .. note:: This variable is now checked before OCR functions are tried. This prevents verbose messages from MuPDF. + +----- + + .. attribute:: pdfcolor + + * New in v1.19.6 + + Contains about 500 RGB colors in PDF format with the color name as key. To see what is there, you can obviously look at `fitz.pdfcolor.keys()`. + + Examples: + + * `fitz.pdfcolor["red"] = (1.0, 0.0, 0.0)` + * `fitz.pdfcolor["skyblue"] = (0.5294117647058824, 0.807843137254902, 0.9215686274509803)` + * `fitz.pdfcolor["wheat"] = (0.9607843137254902, 0.8705882352941177, 0.7019607843137254)` + +----- + + .. method:: get_pdf_now() + + Convenience function to return the current local timestamp in PDF compatible format, e.g. *D:20170501121525-04'00'* for local datetime May 1, 2017, 12:15:25 in a timezone 4 hours westward of the UTC meridian. + + :rtype: str + :returns: current local PDF timestamp. + +----- + + .. method:: get_text_length(text, fontname="helv", fontsize=11, encoding=TEXT_ENCODING_LATIN) + + * New in version 1.14.7 + + Calculate the length of text on output with a given **builtin** font, fontsize and encoding. + + :arg str text: the text string. + :arg str fontname: the fontname. Must be one of either the :ref:`Base-14-Fonts` or the CJK fonts, identified by their "reserved" fontnames (see table in :meth.`Page.insert_font`). + :arg float fontsize: the fontsize. + :arg int encoding: the encoding to use. Besides 0 = Latin, 1 = Greek and 2 = Cyrillic (Russian) are available. Relevant for Base-14 fonts "Helvetica", "Courier" and "Times" and their variants only. Make sure to use the same value as in the corresponding text insertion. + :rtype: float + :returns: the length in points the string will have (e.g. when used in :meth:`Page.insert_text`). + + .. note:: This function will only do the calculation -- it won't insert font nor text. + + .. note:: The :ref:`Font` class offers a similar method, :meth:`Font.text_length`, which supports Base-14 fonts and any font with a character map (CMap, Type 0 fonts). + + .. warning:: If you use this function to determine the required rectangle width for the (:ref:`Page` or :ref:`Shape`) *insert_textbox* methods, be aware that they calculate on a **by-character level**. Because of rounding effects, this will mostly lead to a slightly larger number: *sum([fitz.get_text_length(c) for c in text]) > fitz.get_text_length(text)*. So either (1) do the same, or (2) use something like *fitz.get_text_length(text + "'")* for your calculation. + +----- + + .. method:: get_pdf_str(text) + + Make a PDF-compatible string: if the text contains code points *ord(c) > 255*, then it will be converted to UTF-16BE with BOM as a hexadecimal character string enclosed in "<>" brackets like **. Otherwise, it will return the string enclosed in (round) brackets, replacing any characters outside the ASCII range with some special code. Also, every "(", ")" or backslash is escaped with a backslash. + + :arg str text: the object to convert + + :rtype: str + :returns: PDF-compatible string enclosed in either *()* or *<>*. + +----- + + .. method:: image_profile(stream) + + * New in v1.16.7 + * Changed in v1.19.5: also return natural image orientation extracted from EXIF data if present. + + Show important properties of an image provided as a memory area. Its main purpose is to avoid using other Python packages just to determine them. + + :arg bytes|bytearray|BytesIO|file stream: an image either in memory or an **opened** file. A memory resident image maybe any of the formats *bytes*, *bytearray* or *io.BytesIO*. + + :rtype: dict + :returns: + No exception is ever raised: in case of error, the empty dictionary `{}` is returned. Otherwise, there are the following items:: + + In [2]: fitz.image_profile(open("nur-ruhig.jpg", "rb").read()) + Out[2]: + {'width': 439, + 'height': 501, + 'orientation': 0, # natural orientation (from EXIF) + 'transform': (1.0, 0.0, 0.0, 1.0, 0.0, 0.0), # orientation matrix + 'xres': 96, + 'yres': 96, + 'colorspace': 3, + 'bpc': 8, + 'ext': 'jpeg', + 'cs-name': 'DeviceRGB'} + + There is the following relation to *Exif* information encoded in `orientation`, and correspondingly in the `transform` matrix-like (quoted from MuPDF documentation, *ccw* = counter-clockwise): + + 0. Undefined + 1. 0 degree ccw rotation. (Exif = 1) + 2. 90 degree ccw rotation. (Exif = 8) + 3. 180 degree ccw rotation. (Exif = 3) + 4. 270 degree ccw rotation. (Exif = 6) + 5. flip on X. (Exif = 2) + 6. flip on X, then rotate ccw by 90 degrees. (Exif = 5) + 7. flip on X, then rotate ccw by 180 degrees. (Exif = 4) + 8. flip on X, then rotate ccw by 270 degrees. (Exif = 7) + + + .. note:: + + * For some "exotic" images (FAX encodings, RAW formats and the like), this method will not work and return *None*. You can however still work with such images in PyMuPDF, e.g. by using :meth:`Document.extract_image` or create pixmaps via `Pixmap(doc, xref)`. These methods will automatically convert exotic images to the PNG format before returning results. + * You can also get the properties of images embedded in a PDF, via their :data:`xref`. In this case make sure to extract the raw stream: `fitz.image_profile(doc.xref_stream_raw(xref))`. + * Images as returned by the image blocks of :meth:`Page.get_text` using "dict" or "rawdict" options are also supported. + + +----- + + .. method:: ConversionHeader("text", filename="UNKNOWN") + + Return the header string required to make a valid document out of page text outputs. + + :arg str output: type of document. Use the same as the output parameter of *get_text()*. + + :arg str filename: optional arbitrary name to use in output types "json" and "xml". + + :rtype: str + +----- + + .. method:: ConversionTrailer(output) + + Return the trailer string required to make a valid document out of page text outputs. See :meth:`Page.get_text` for an example. + + :arg str output: type of document. Use the same as the output parameter of *get_text()*. + + :rtype: str + +----- + + .. method:: Document.del_xml_metadata() + + Delete an object containing XML-based metadata from the PDF. (Py-) MuPDF does not support XML-based metadata. Use this if you want to make sure that the conventional metadata dictionary will be used exclusively. Many thirdparty PDF programs insert their own metadata in XML format and thus may override what you store in the conventional dictionary. This method deletes any such reference, and the corresponding PDF object will be deleted during next garbage collection of the file. + +----- + + .. method:: Document.xml_metadata_xref() + + Return the XML-based metadata :data:`xref` of the PDF if present -- also refer to :meth:`Document.del_xml_metadata`. You can use it to retrieve the content via :meth:`Document.xref_stream` and then work with it using some XML software. + + :rtype: int + :returns: :data:`xref` of PDF file level XML metadata -- or 0 if none exists. + +----- + + .. method:: Page.run(dev, transform) + + Run a page through a device. + + :arg dev: Device, obtained from one of the :ref:`Device` constructors. + :type dev: :ref:`Device` + + :arg transform: Transformation to apply to the page. Set it to :ref:`Identity` if no transformation is desired. + :type transform: :ref:`Matrix` + +----- + + .. method:: Page.get_bboxlog(layers=False) + + * New in v1.19.0 + * Changed in v1.22.0: optionally also return the OCG name applicable to the boundary box. + + :returns: a list of rectangles that envelop text, image or drawing objects. Each item is a tuple `(type, (x0, y0, x1, y1))` where the second tuple consists of rectangle coordinates, and *type* is one of the following values. If `layers=True`, there is a third item containing the OCG name or `None`: `(type, (x0, y0, x1, y1), None)`. + + * `"fill-text"` -- normal text (painted without character borders) + * `"stroke-text"` -- text showing character borders only + * `"ignore-text"` -- text that should not be displayed (e.g. as used by OCR text layers) + * `"fill-path"` -- drawing with fill color (and no border) + * `"stroke-path"` -- drawing with border (and no fill color) + * `"fill-image"` -- displays an image + * `"fill-shade"` -- display a shading + + The item sequence represents the **sequence in which these commands are executed** to build the page's appearance. Therefore, if an item's bbox intersects or contains that of a previous item, then the previous item may be (partially) covered / hidden. + + + So this list can be used to detect such situations. An item's index in this list equals the value of a `"seqno"` in dictionaries as returned by :meth:`Page.get_drawings` and :meth:`Page.get_texttrace`. + + +----- + + .. method:: Page.get_texttrace() + + * New in v1.18.16 + * Changed in v1.19.0: added key "seqno". + * Changed in v1.19.1: stroke and fill colors now always are either RGB or GRAY + * Changed in v1.19.3: span and character bboxes are now also correct if `dir != (1, 0)`. + * Changed in v1.22.0: add new dictionary key "layer". + + + Return low-level text information of the page. The method is available for **all** document types. The result is a list of Python dictionaries with the following content:: + + { + 'ascender': 0.83251953125, # font ascender (1) + 'bbox': (458.14019775390625, # span bbox x0 (7) + 749.4671630859375, # span bbox y0 + 467.76458740234375, # span bbox x1 + 757.5071411132812), # span bbox y1 + 'bidi': 0, # bidirectional level (1) + 'chars': ( # char information, tuple[tuple] + (45, # unicode (4) + 16, # glyph id (font dependent) + (458.14019775390625, # origin.x (1) + 755.3758544921875), # origin.y (1) + (458.14019775390625, # char bbox x0 (6) + 749.4671630859375, # char bbox y0 + 462.9649963378906, # char bbox x1 + 757.5071411132812)), # char bbox y1 + ( ... ), # more characters + ), + 'color': (0.0,), # text color, tuple[float] (1) + 'colorspace': 1, # number of colorspace components (1) + 'descender': -0.30029296875, # font descender (1) + 'dir': (1.0, 0.0), # writing direction (1) + 'flags': 12, # font flags (1) + 'font': 'CourierNewPSMT', # font name (1) + 'linewidth': 0.4019999980926514, # current line width value (3) + 'opacity': 1.0, # alpha value of the text (5) + 'layer': None, # name of Optional Content Group (9) + 'seqno': 246, # sequence number (8) + 'size': 8.039999961853027, # font size (1) + 'spacewidth': 4.824785133358091, # width of space char + 'type': 0, # span type (2) + 'wmode': 0 # writing mode (1) + } + + Details: + + 1. Information above tagged with "(1)" has the same meaning and value as explained in :ref:`TextPage`. + + - Please note that the font `flags` value will never contain a *superscript* flag bit: the detection of superscripts is done within MuPDF :ref:`TextPage` code -- it is not a property of any font. + - Also note, that the text *color* is encoded as the usual tuple of floats 0 <= f <= 1 -- not in sRGB format. Depending on `span["type"]`, interpret this as fill color or stroke color. + + 2. There are 3 text span types: + + - 0: Filled text -- equivalent to PDF text rendering mode 0 (`0 Tr`, the default in PDF), only each character's "inside" is shown. + - 1: Stroked text -- equivalent to `1 Tr`, only the character borders are shown. + - 3: Ignored text -- equivalent to `3 Tr` (hidden text). + + 3. Line width in this context is important only for processing `span["type"] != 0`: it determines the thickness of the character's border line. This value may not be provided at all with the text data. In this case, a value of 5% of the fontsize (`span["size"] * 0,05`) is generated. Often, an "artificial" bold text in PDF is created by `2 Tr`. There is no equivalent span type for this case. Instead, respective text is represented by two consecutive spans -- which are identical in every aspect, except for their types, which are 0, resp 1. It is your responsibility to handle this type of situation - in :meth:`Page.get_text`, MuPDF is doing this for you. + 4. For data compactness, the character's unicode is provided here. Use built-in function `chr()` for the character itself. + 5. The alpha / opacity value of the span's text, `0 <= opacity <= 1`, 0 is invisible text, 1 (100%) is intransparent. Depending on `span["type"]`, interpret this value as *fill* opacity or, resp. *stroke* opacity. + 6. *(Changed in v1.19.0)* This value is equal or close to `char["bbox"]` of "rawdict". In particular, the bbox **height** value is always computed as if **"small glyph heights"** had been requested. + 7. *(New in v1.19.0)* This is the union of all character bboxes. + 8. *(New in v1.19.0)* Enumerates the commands that build up the page's appearance. Can be used to find out whether text is effectively hidden by objects, which are painted "later", or *over* some object. So if there is a drawing or image with a higher sequence number, whose bbox overlaps (parts of) this text span, one may assume that such an object hides the resp. text. Different text spans have identical sequence numbers if they were created in one go. + 9. *(New in v1.22.0)* The name of the Optional Content Group (OCG) if applicable or `None`. + + Here is a list of similarities and differences of `page.get_texttrace()` compared to `page.get_text("rawdict")`: + + * The method is up to **twice as fast,** compared to "rawdict" extraction. Depends on the amount of text. + * The returned data is very **much smaller in size** -- although it provides more information. + * Additional types of text **invisibility can be detected**: opacity = 0 or type > 1 or overlapping bbox of an object with a higher sequence number. + * If MuPDF returns unicode 0xFFFD (65533) for unrecognized characters, you may still be able to deduct desired information from the glyph id. + * The `span["chars"]` **contains no spaces**, **except** the document creator has explicitly coded them. They **will never be generated** like it happens in :meth:`Page.get_text` methods. To provide some help for doing your own computations here, the width of a space character is given. This value is derived from the font where possible. Otherwise the value of a fallback font is taken. + * There is no effort to organize text like it happens for a :ref:`TextPage` (the hierarchy of blocks, lines, spans, and characters). Characters are simply extracted in sequence, one by one, and put in a span. Whenever any of the span's characteristics changes, a new span is started. So you may find characters with different `origin.y` values in the same span (which means they would appear in different lines). You cannot assume, that span characters are sorted in any particular order -- you must make sense of the info yourself, taking `span["dir"]`, `span["wmode"]`, etc. into account. + * Ligatures are represented like this: + - MuPDF handles the following ligatures: "fi", "ff", "fl", "ft", "st", "ffi", and "ffl" (only the first 3 are mostly ever used). If the page contains e.g. ligature "fi", you will find the following two character items subsequent to each other:: + + (102, glyph, (x, y), (x0, y0, x1, y1)) # 102 = ord("f") + (105, -1, (x, y), (x0, y0, x0, y1)) # 105 = ord("i"), empty bbox! + + - This means that the bbox of the first ligature character is the area containing the complete, compound glyph. Subsequent ligature components are recognizable by their glyph value -1 and a bbox of width zero. + - You may want to replace those 2 or 3 char tuples by one, that represents the ligature itself. Use the following mapping of ligatures to unicodes: + + + `"ff" -> 0xFB00` + + `"fi" -> 0xFB01` + + `"fl" -> 0xFB02` + + `"ffi" -> 0xFB03` + + `"ffl" -> 0xFB04` + + `"ft" -> 0xFB05` + + `"st" -> 0xFB06` + + So you may want to replace the two example tuples above by the following single one: `(0xFB01, glyph, (x, y), (x0, y0, x1, y1))` (there is usually no need to lookup the correct glyph id for 0xFB01 in the resp. font, but you may execute `font.has_glyph(0xFB01)` and use its return value). + + * **Changed in v1.19.3:** Similar to other text extraction methods, the character and span bboxes envelop the character quads. To recover the quads, follow the same methods :meth:`recover_quad`, :meth:`recover_char_quad` or :meth:´recover_span_quad` as explained in :ref:`textpagedict`. Use either `None` or `span["dir"]` for the writing direction. + + * **Changed in v1.21.1:** If applicable, the name of the OCG is shown in `"layer"`. + +----- + + .. method:: Page.wrap_contents() + + Put string pair "q" / "Q" before, resp. after a page's */Contents* object(s) to ensure that any "geometry" changes are **local** only. + + Use this method as an alternative, minimalist version of :meth:`Page.clean_contents`. Its advantage is a small footprint in terms of processing time and impact on the data size of incremental saves. Multiple executions of this method are no problem and have no functional impact: `b"q q contents Q Q"` is treated like `b"q contents Q"`. + +----- + + .. attribute:: Page.is_wrapped + + Indicate whether :meth:`Page.wrap_contents` may be required for object insertions in standard PDF geometry. Note that this is a quick, basic check only: a value of *False* may still be a false alarm. But nevertheless executing :meth:`Page.wrap_contents` will have no negative side effects. + + :rtype: bool + +----- + + .. method:: Page.get_text_blocks(flags=None) + + Deprecated wrapper for :meth:`TextPage.extractBLOCKS`. Use :meth:`Page.get_text` with the "blocks" option instead. + + :rtype: list[tuple] + +----- + + .. method:: Page.get_text_words(flags=None) + + Deprecated wrapper for :meth:`TextPage.extractWORDS`. Use :meth:`Page.get_text` with the "words" option instead. + + :rtype: list[tuple] + +----- + + .. method:: Page.get_displaylist() + + Run a page through a list device and return its display list. + + :rtype: :ref:`DisplayList` + :returns: the display list of the page. + +----- + + .. method:: Page.get_contents() + + PDF only: Retrieve a list of :data:`xref` of :data:`contents` objects of a page. May be empty or contain multiple integers. If the page is cleaned (:meth:`Page.clean_contents`), it will be one entry at most. The "source" of each `/Contents` object can be individually read by :meth:`Document.xref_stream` using an item of this list. Method :meth:`Page.read_contents` in contrast walks through this list and concatenates the corresponding sources into one `bytes` object. + + :rtype: list[int] + +----- + + .. method:: Page.set_contents(xref) + + PDF only: Let the page's `/Contents` key point to this xref. Any previously used contents objects will be ignored and can be removed via garbage collection. + +----- + + .. method:: Page.clean_contents(sanitize=True) + + * Changed in v1.17.6 + + PDF only: Clean and concatenate all :data:`contents` objects associated with this page. "Cleaning" includes syntactical corrections, standardizations and "pretty printing" of the contents stream. Discrepancies between :data:`contents` and :data:`resources` objects will also be corrected if sanitize is true. See :meth:`Page.get_contents` for more details. + + Changed in version 1.16.0 Annotations are no longer implicitly cleaned by this method. Use :meth:`Annot.clean_contents` separately. + + :arg bool sanitize: *(new in v1.17.6)* if true, synchronization between resources and their actual use in the contents object is snychronized. For example, if a font is not actually used for any text of the page, then it will be deleted from the `/Resources/Font` object. + + .. warning:: This is a complex function which may generate large amounts of new data and render old data unused. It is **not recommended** using it together with the **incremental save** option. Also note that the resulting singleton new */Contents* object is **uncompressed**. So you should save to a **new file** using options *"deflate=True, garbage=3"*. + +----- + + .. method:: Page.read_contents() + + *New in version 1.17.0.* + Return the concatenation of all :data:`contents` objects associated with the page -- without cleaning or otherwise modifying them. Use this method whenever you need to parse this source in its entirety without having to bother how many separate contents objects exist. + + :rtype: bytes + +----- + + .. method:: Annot.clean_contents(sanitize=True) + + Clean the :data:`contents` streams associated with the annotation. This is the same type of action which :meth:`Page.clean_contents` performs -- just restricted to this annotation. + + +----- + + .. method:: Document.get_char_widths(xref=0, limit=256) + + Return a list of character glyphs and their widths for a font that is present in the document. A font must be specified by its PDF cross reference number :data:`xref`. This function is called automatically from :meth:`Page.insert_text` and :meth:`Page.insert_textbox`. So you should rarely need to do this yourself. + + :arg int xref: cross reference number of a font embedded in the PDF. To find a font :data:`xref`, use e.g. *doc.get_page_fonts(pno)* of page number *pno* and take the first entry of one of the returned list entries. + + :arg int limit: limits the number of returned entries. The default of 256 is enforced for all fonts that only support 1-byte characters, so-called "simple fonts" (checked by this method). All :ref:`Base-14-Fonts` are simple fonts. + + :rtype: list + :returns: a list of *limit* tuples. Each character *c* has an entry *(g, w)* in this list with an index of *ord(c)*. Entry *g* (integer) of the tuple is the glyph id of the character, and float *w* is its normalized width. The actual width for some fontsize can be calculated as *w * fontsize*. For simple fonts, the *g* entry can always be safely ignored. In all other cases *g* is the basis for graphically representing *c*. + + This function calculates the pixel width of a string called *text*:: + + def pixlen(text, widthlist, fontsize): + try: + return sum([widthlist[ord(c)] for c in text]) * fontsize + except IndexError: + raise ValueError:("max. code point found: %i, increase limit" % ord(max(text))) + +----- + + .. method:: Document.is_stream(xref) + + * New in version 1.14.14 + + PDF only: Check whether the object represented by :data:`xref` is a :data:`stream` type. Return is *False* if not a PDF or if the number is outside the valid xref range. + + :arg int xref: :data:`xref` number. + + :returns: *True* if the object definition is followed by data wrapped in keyword pair *stream*, *endstream*. + +----- + + .. method:: Document.get_new_xref() + + Increase the :data:`xref` by one entry and return that number. This can then be used to insert a new object. + + :rtype: int + :returns: the number of the new :data:`xref` entry. Please note, that only a new entry in the PDF's cross reference table is created. At this point, there will not yet exist a PDF object associated with it. To create an (empty) object with this number use `doc.update_xref(xref, "<<>>")`. + +----- + + .. method:: Document.xref_length() + + Return length of :data:`xref` table. + + :rtype: int + :returns: the number of entries in the :data:`xref` table. + +----- + + .. method:: recover_quad(line_dir, span) + + Compute the quadrilateral of a text span extracted via options "dict" or "rawdict" of :meth:`Page.get_text`. + + :arg tuple line_dir: `line["dir"]` of the owning line. Use `None` for a span from :meth:`Page.get_texttrace`. + :arg dict span: the span. + :returns: the :ref:`Quad` of the span, usable for text marker annotations ('Highlight', etc.). + +----- + + .. method:: recover_char_quad(line_dir, span, char) + + Compute the quadrilateral of a text character extracted via option "rawdict" of :meth:`Page.get_text`. + + :arg tuple line_dir: `line["dir"]` of the owning line. Use `None` for a span from :meth:`Page.get_texttrace`. + :arg dict span: the span. + :arg dict char: the character. + :returns: the :ref:`Quad` of the character, usable for text marker annotations ('Highlight', etc.). + +----- + + .. method:: recover_span_quad(line_dir, span, chars=None) + + Compute the quadrilateral of a subset of characters of a span extracted via option "rawdict" of :meth:`Page.get_text`. + + :arg tuple line_dir: `line["dir"]` of the owning line. Use `None` for a span from :meth:`Page.get_texttrace`. + :arg dict span: the span. + :arg list chars: the characters to consider. If omitted, identical to :meth:`recoer_span`. If given, the selected extraction option must be "rawdict". + :returns: the :ref:`Quad` of the selected characters, usable for text marker annotations ('Highlight', etc.). + +----- + + .. method:: recover_line_quad(line, spans=None) + + Compute the quadrilateral of a subset of spans of a text line extracted via options "dict" or "rawdict" of :meth:`Page.get_text`. + + :arg dict line: the line. + :arg list spans: a sub-list of `line["spans"]`. If omitted, the full line quad will be returned. + :returns: the :ref:`Quad` of the selected line spans, usable for text marker annotations ('Highlight', etc.). + +----- + + .. method:: get_tessdata() + + Return the name of Tesseract's language support folder. Use this function if the environment variable `TESSDATA_PREFIX` has not been set. + + :returns: `os.getenv("TESSDATA_PREFIX")` if not `None`. Otherwise, if Tesseract-OCR is installed, locate the name of `tessdata`. If no installation is found, return `False`. + + The folder name can be used as parameter `tessdata` in methods :meth:`Page.get_textpage_ocr`, :meth:`Pixmap.pdfocr_save` and :meth:`Pixmap.pdfocr_tobytes`. + +----- + + .. method:: INFINITE_QUAD() + + .. method:: INFINITE_RECT() + + .. method:: INFINITE_IRECT() + + Return the (unique) infinite rectangle `Rect(-2147483648.0, -2147483648.0, 2147483520.0, 2147483520.0)`, resp. the :ref:`IRect` and :ref:`Quad` counterparts. It is the largest possible rectangle: all valid rectangles are contained in it. + +----- + + .. method:: EMPTY_QUAD() + + .. method:: EMPTY_RECT() + + .. method:: EMPTY_IRECT() + + Return the "standard" empty and invalid rectangle `Rect(2147483520.0, 2147483520.0, -2147483648.0, -2147483648.0)` resp. quad. Its top-left and bottom-right point values are reversed compared to the infinite rectangle. It will e.g. be used to indicate empty bboxes in `page.get_text("dict")` dictionaries. There are however infinitely many empty or invalid rectangles. + +.. include:: footer.rst diff --git a/docs/glossary.rst b/docs/glossary.rst new file mode 100644 index 0000000..46d8b60 --- /dev/null +++ b/docs/glossary.rst @@ -0,0 +1,167 @@ +.. include:: header.rst + +.. _Glossary: + +============== +Glossary +============== + +.. data:: matrix_like + + A Python sequence of 6 numbers. + +.. data:: rect_like + + A Python sequence of 4 numbers. + +.. data:: irect_like + + A Python sequence of 4 integers. + +.. data:: point_like + + A Python sequence of 2 numbers. + +.. data:: quad_like + + A Python sequence of 4 :data:`point_like` items. + +.. data:: inheritable + + A number of values in a PDF can inherited by objects further down in a parent-child relationship. The mediabox (physical size) of pages may for example be specified only once or in some node(s) of the :data:`pagetree` and will then be taken as value for all *kids*, that do not specify their own value. + +.. _Glossary_MediaBox: + +.. data:: MediaBox + + A PDF array of 4 floats specifying a physical page size -- (:data:`inheritable`, mandatory). This rectangle should contain all other PDF -- optional -- page rectangles, which may be specified in addition: CropBox, TrimBox, ArtBox and BleedBox. Please consult :ref:`AdobeManual` for details. The MediaBox is the only rectangle, for which there is no difference between MuPDF and PDF coordinate systems: :attr:`Page.mediabox` will always show the same coordinates as the `/MediaBox` key in a page's object definition. For all other rectangles, MuPDF transforms y coordinates such that the **top** border is the point of reference. This can sometimes be confusing -- you may for example encounter a situation like this one: + + * The page definition contains the following identical values: `/MediaBox [ 36 45 607.5 765 ]`, `/CropBox [ 36 45 607.5 765 ]`. + * PyMuPDF accordingly shows `page.mediabox = Rect(36.0, 45.0, 607.5, 765.0)`. + * **BUT:** `page.cropbox = Rect(36.0, 0.0, 607.5, 720.0)`, because the two y-coordinates have been transformed (45 subtracted from both of them). + +.. data:: CropBox + + A PDF array of 4 floats specifying a page's visible area -- (:data:`inheritable`, optional). It is the default for TrimBox, ArtBox and BleedBox. If not present, it defaults to MediaBox. This value is **not affected** if the page is rotated -- in contrast to :attr:`Page.rect`. Also, other than the page rectangle, the top-left corner of the cropbox may or may not be *(0, 0)*. + + +.. data:: catalog + + A central PDF :data:`dictionary` -- also called the "root" -- containing document-wide parameters and pointers to many other information. Its :data:`xref` is returned by :meth:`Document.pdf_catalog`. + +.. data:: trailer + + More precisely, the **PDF trailer** contains information in :data:`dictionary` format. It is usually located at the file's end. In this dictionary, you will find things like the xrefs of the catalog and the metadata, the number of :data:`xref` numbers, etc. Here is the definition of the PDF spec: + + *"The trailer of a PDF file enables an application reading the file to quickly find the cross-reference table and certain special objects. Applications should read a PDF file from its end."* + + To access the trailer in PyMuPDF, use the usual methods :meth:`Document.xref_object`, :meth:`Document.xref_get_key` and :meth:`Document.xref_get_keys` with `-1` instead of a positive xref number. + +.. data:: contents + + A **content stream** is a PDF :data:`object` with an attached :data:`stream`, whose data consists of a sequence of instructions describing the graphical elements to be painted on a page, see "Stream Objects" on page 19 of :ref:`AdobeManual`. For an overview of the mini-language used in these streams, see chapter "Operator Summary" on page 643 of the :ref:`AdobeManual`. A PDF :data:`page` can have none to many contents objects. If it has none, the page is empty (but still may show annotations). If it has several, they will be interpreted in sequence as if their instructions had been present in one such object (i.e. like in a concatenated string). It should be noted that there are more stream object types which use the same syntax: e.g. appearance dictionaries associated with annotations and Form XObjects. + + PyMuPDF provides a number of methods to deal with contents of PDF pages: + + * :meth:`Page.read_contents()` -- reads and concatenates all page contents into one `bytes` object. + * :meth:`Page.clean_contents()` -- a wrapper of a MuPDF function that reads, concatenates and syntax-cleans all page contents. After this, only one `/Contents` object will exist. In addition, page :data:`resources` will have been synchronized with it such that it will contain exactly those images, fonts and other objects that the page actually references. + * :meth:`Page.get_contents()` -- return a list of :data:`xref` numbers of a page's :data:`contents` objects. May be empty. Use :meth:`Document.xref_stream()` with one of these xrefs to read the resp. contents section. + * :meth:`Page.set_contents()` -- set a page's `/Contents` key to the provided :data:`xref` number. + +.. data:: resources + + A :data:`dictionary` containing references to any resources (like images or fonts) required by a PDF :data:`page` (required, inheritable, :ref:`AdobeManual` p. 81) and certain other objects (Form XObjects). This dictionary appears as a sub-dictionary in the object definition under the key */Resources*. Being an inheritable object type, there may exist "parent" resources for all pages or certain subsets of pages. + +.. data:: dictionary + + A PDF :data:`object` type, which is somewhat comparable to the same-named Python notion: "A dictionary object is an associative table containing pairs of objects, known as the dictionary's entries. The first element of each entry is the key and the second element is the value. The key must be a name (...). The value can be any kind of object, including another dictionary. A dictionary entry whose value is null (...) is equivalent to an absent entry." (:ref:`AdobeManual` p. 18). + + Dictionaries are the most important :data:`object` type in PDF. Here is an example (describing a :data:`page`):: + + << + /Contents 40 0 R % value: an indirect object + /Type/Page % value: a name object + /MediaBox[0 0 595.32 841.92] % value: an array object + /Rotate 0 % value: a number object + /Parent 12 0 R % value: an indirect object + /Resources<< % value: a dictionary object + /ExtGState<> + /Font<< + /R8 27 0 R/R10 21 0 R/R12 24 0 R/R14 15 0 R + /R17 4 0 R/R20 30 0 R/R23 7 0 R /R27 20 0 R + >> + /ProcSet[/PDF/Text] % value: array of two name objects + >> + /Annots[55 0 R] % value: array, one entry (indirect object) + >> + + *Contents*, *Type*, *MediaBox*, etc. are **keys**, *40 0 R*, *Page*, *[0 0 595.32 841.92]*, etc. are the respective **values**. The strings *"<<"* and *">>"* are used to enclose object definitions. + + This example also shows the syntax of **nested** dictionary values: *Resources* has an object as its value, which in turn is a dictionary with keys like *ExtGState* (with the value *<>*, which is another dictionary), etc. + +.. data:: page + + A PDF page is a :data:`dictionary` object which defines one page in a PDF, see :ref:`AdobeManual` p. 71. + +.. data:: pagetree + + The pages of a document are accessed through a structure known as the page tree, which defines the ordering of pages in the document. The tree structure allows PDF consumer applications, using only limited memory, to quickly open a document containing thousands of pages. The tree contains nodes of two types: intermediate nodes, called page tree nodes, and leaf nodes, called page objects. (:ref:`AdobeManual` p. 75). + + While it is possible to list all page references in just one array, PDFs with many pages are often created using *balanced tree* structures ("page trees") for faster access to any single page. In relation to the total number of pages, this can reduce the average page access time by page number from a linear to some logarithmic order of magnitude. + + For fast page access, MuPDF can use its own array in memory -- independently from what may or may not be present in the document file. This array is indexed by page number and therefore much faster than even the access via a perfectly balanced page tree. + +.. data:: object + + Similar to Python, PDF supports the notion *object*, which can come in eight basic types: boolean values ("true" or "false"), integer and real numbers, strings (**always** enclosed in brackets -- either "()", or "<>" to indicate hexadecimal), names (must always start with a "/", e.g. `/Contents`), arrays (enclosed in brackets "[]"), dictionaries (enclosed in brackets "<<>>"), streams (enclosed by keywords "stream" / "endstream"), and the null object ("null") (:ref:`AdobeManual` p. 13). Objects can be made identifiable by assigning a label. This label is then called *indirect* object. PyMuPDF supports retrieving definitions of indirect objects via their cross reference number via :meth:`Document.xref_object`. + +.. data:: stream + + A PDF :data:`dictionary` :data:`object` type which is followed by a sequence of bytes, similar to Python *bytes*. "However, a PDF application can read a stream incrementally, while a string must be read in its entirety. Furthermore, a stream can be of unlimited length, whereas a string is subject to an implementation limit. For this reason, objects with potentially large amounts of data, such as images and page descriptions, are represented as streams." "A stream consists of a :data:`dictionary` followed by zero or more bytes bracketed between the keywords *stream* and *endstream*":: + + nnn 0 obj + << + dictionary definition + >> + stream + (zero or more bytes) + endstream + endobj + + See :ref:`AdobeManual` p. 19. PyMuPDF supports retrieving stream content via :meth:`Document.xref_stream`. Use :meth:`Document.is_stream` to determine whether an object is of stream type. + +.. data:: unitvector + + A mathematical notion meaning a vector of norm ("length") 1 -- usually the Euclidean norm is implied. In PyMuPDF, this term is restricted to :ref:`Point` objects, see :attr:`Point.unit`. + +.. data:: xref + + Abbreviation for cross-reference number: this is an integer unique identification for objects in a PDF. There exists a cross-reference table (which may physically consist of several separate segments) in each PDF, which stores the relative position of each object for quick lookup. The cross-reference table is one entry longer than the number of existing object: item zero is reserved and must not be used in any way. Many PyMuPDF classes have an *xref* attribute (which is zero for non-PDFs), and one can find out the total number of objects in a PDF via :meth:`Document.xref_length` *- 1*. + +.. data:: resolution + + Images and :ref:`Pixmap` objects may contain resolution information provided as "dots per inch", dpi, in each direction (horizontal and vertical). When MuPDF reads an image from a file or from a PDF object, it will parse this information and put it in :attr:`Pixmap.xres`, :attr:`Pixmap.yres`, respectively. If it finds no meaningful information in the input (like non-positive values or values exceeding 4800), it will use "sane" defaults instead. The usual default value is 96, but it may also be 72 in some cases (e.g. for JPX images). + +.. data:: OCPD + + Optional content properties dictionary - a sub :data:`dictionary` of the PDF :data:`catalog`. The central place to store optional content information, which is identified by the key `/OCProperties`. This dictionary has two required and one optional entry: (1) `/OCGs`, required, an array listing all optional content groups, (2) `/D`, required, the default optional content configuration dictionary (OCCD), (3) `/Configs`, optional, an array of alternative OCCDs. + + +.. data:: OCCD + + Optional content configuration dictionary - a PDF :data:`dictionary` inside the PDF :data:`OCPD`. It stores a setting of ON / OFF states of OCGs and how they are presented to a PDF viewer program. Selecting a configuration is quick way to achieve temporary mass visibility state changes. After opening a PDF, the `/D` configuration of the :data:`OCPD` is always activated. Viewer should offer a way to switch between the `/D`, or one of the optional configurations contained in array `/Configs`. + + +.. data:: OCG + + Optional content group -- a :data:`dictionary` object used to control the visibility of other PDF objects like images or annotations. Independently on which page they are defined, objects with the same OCG can simultaneously be shown or hidden by setting their OCG to ON or OFF. This can be achieved via the user interface provided by many PDF viewers (Adobe Acrobat), or programmatically. + +.. data:: OCMD + + Optional content membership dictionary -- a :data:`dictionary` object which can be used like an :data:`OCG`: it has a visibility state. The visibility of an OCMD is **computed:** it is a logical expression, which uses the state of one or more OCGs to produce a boolean value. The expression's result is interpreted as ON (true) or OFF (false). + +.. data:: ligature + + Some frequent character combinations are represented by their own special glyphs in more advanced fonts. Typical examples are "fi", "fl", "ffi" and "ffl". These compounds are called *ligatures*. In PyMuPDF text extractions, there is the option to either return the corresponding unicode unchanged, or split ligatures up into their constituent parts: "fi" ==> "f" + "i", etc. + +.. include:: footer.rst diff --git a/docs/header.rst b/docs/header.rst new file mode 100644 index 0000000..98c0062 --- /dev/null +++ b/docs/header.rst @@ -0,0 +1,52 @@ +.. raw:: html + + + + + + + + + + + + + diff --git a/docs/identity.rst b/docs/identity.rst new file mode 100644 index 0000000..7e87ade --- /dev/null +++ b/docs/identity.rst @@ -0,0 +1,20 @@ +.. include:: header.rst + +.. _Identity: + +============ +Identity +============ + +Identity is a :ref:`Matrix` that performs no action -- to be used whenever the syntax requires a matrix, but no actual transformation should take place. It has the form *fitz.Matrix(1, 0, 0, 1, 0, 0)*. + +Identity is a constant, an "immutable" object. So, all of its matrix properties are read-only and its methods are disabled. + +If you need a **mutable** identity matrix as a starting point, use one of the following statements:: + + >>> m = fitz.Matrix(1, 0, 0, 1, 0, 0) # specify the values + >>> m = fitz.Matrix(1, 1) # use scaling by factor 1 + >>> m = fitz.Matrix(0) # use rotation by zero degrees + >>> m = fitz.Matrix(fitz.Identity) # make a copy of Identity + +.. include:: footer.rst diff --git a/docs/images/discord-mark-blue.svg b/docs/images/discord-mark-blue.svg new file mode 100644 index 0000000..ca65400 --- /dev/null +++ b/docs/images/discord-mark-blue.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/docs/images/icons/icon-cbz.svg b/docs/images/icons/icon-cbz.svg new file mode 100644 index 0000000..bb4528b --- /dev/null +++ b/docs/images/icons/icon-cbz.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/docs/images/icons/icon-epub.svg b/docs/images/icons/icon-epub.svg new file mode 100644 index 0000000..a395f5f --- /dev/null +++ b/docs/images/icons/icon-epub.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/docs/images/icons/icon-fb2.svg b/docs/images/icons/icon-fb2.svg new file mode 100644 index 0000000..a7b478a --- /dev/null +++ b/docs/images/icons/icon-fb2.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/docs/images/icons/icon-image.svg b/docs/images/icons/icon-image.svg new file mode 100644 index 0000000..2cfaed3 --- /dev/null +++ b/docs/images/icons/icon-image.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + diff --git a/docs/images/icons/icon-mobi.svg b/docs/images/icons/icon-mobi.svg new file mode 100644 index 0000000..3bb5628 --- /dev/null +++ b/docs/images/icons/icon-mobi.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + diff --git a/docs/images/icons/icon-pdf.svg b/docs/images/icons/icon-pdf.svg new file mode 100644 index 0000000..386b8cc --- /dev/null +++ b/docs/images/icons/icon-pdf.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/docs/images/icons/icon-svg.svg b/docs/images/icons/icon-svg.svg new file mode 100644 index 0000000..0863c37 --- /dev/null +++ b/docs/images/icons/icon-svg.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/docs/images/icons/icon-xps.svg b/docs/images/icons/icon-xps.svg new file mode 100644 index 0000000..cd008a7 --- /dev/null +++ b/docs/images/icons/icon-xps.svg @@ -0,0 +1,2 @@ + \ No newline at end of file diff --git a/docs/images/img-4up.png b/docs/images/img-4up.png new file mode 100644 index 0000000..f526446 Binary files /dev/null and b/docs/images/img-4up.png differ diff --git a/docs/images/img-7edges.png b/docs/images/img-7edges.png new file mode 100644 index 0000000..957433e Binary files /dev/null and b/docs/images/img-7edges.png differ diff --git a/docs/images/img-adobe.png b/docs/images/img-adobe.png new file mode 100644 index 0000000..32492f9 Binary files /dev/null and b/docs/images/img-adobe.png differ diff --git a/docs/images/img-alpha-0.png b/docs/images/img-alpha-0.png new file mode 100644 index 0000000..bde6f53 Binary files /dev/null and b/docs/images/img-alpha-0.png differ diff --git a/docs/images/img-alpha-1.png b/docs/images/img-alpha-1.png new file mode 100644 index 0000000..0f3e077 Binary files /dev/null and b/docs/images/img-alpha-1.png differ diff --git a/docs/images/img-annots.jpg b/docs/images/img-annots.jpg new file mode 100644 index 0000000..1ff17af Binary files /dev/null and b/docs/images/img-annots.jpg differ diff --git a/docs/images/img-asc-desc.png b/docs/images/img-asc-desc.png new file mode 100644 index 0000000..7e611ce Binary files /dev/null and b/docs/images/img-asc-desc.png differ diff --git a/docs/images/img-attach-result.jpg b/docs/images/img-attach-result.jpg new file mode 100644 index 0000000..9ce2fd8 Binary files /dev/null and b/docs/images/img-attach-result.jpg differ diff --git a/docs/images/img-binsetupdirs.png b/docs/images/img-binsetupdirs.png new file mode 100644 index 0000000..4cd036b Binary files /dev/null and b/docs/images/img-binsetupdirs.png differ diff --git a/docs/images/img-breadth.png b/docs/images/img-breadth.png new file mode 100644 index 0000000..6b39d2e Binary files /dev/null and b/docs/images/img-breadth.png differ diff --git a/docs/images/img-cake.png b/docs/images/img-cake.png new file mode 100644 index 0000000..f1151dd Binary files /dev/null and b/docs/images/img-cake.png differ diff --git a/docs/images/img-caret-annot.jpg b/docs/images/img-caret-annot.jpg new file mode 100644 index 0000000..5889089 Binary files /dev/null and b/docs/images/img-caret-annot.jpg differ diff --git a/docs/images/img-circle.png b/docs/images/img-circle.png new file mode 100644 index 0000000..e87aba3 Binary files /dev/null and b/docs/images/img-circle.png differ diff --git a/docs/images/img-clip.jpg b/docs/images/img-clip.jpg new file mode 100644 index 0000000..e6a1fe4 Binary files /dev/null and b/docs/images/img-clip.jpg differ diff --git a/docs/images/img-colordb.png b/docs/images/img-colordb.png new file mode 100644 index 0000000..91f72a5 Binary files /dev/null and b/docs/images/img-colordb.png differ diff --git a/docs/images/img-convexity.png b/docs/images/img-convexity.png new file mode 100644 index 0000000..8f55694 Binary files /dev/null and b/docs/images/img-convexity.png differ diff --git a/docs/images/img-copy-speed-1.png b/docs/images/img-copy-speed-1.png new file mode 100644 index 0000000..caab390 Binary files /dev/null and b/docs/images/img-copy-speed-1.png differ diff --git a/docs/images/img-copy-speed-2.png b/docs/images/img-copy-speed-2.png new file mode 100644 index 0000000..6eed76e Binary files /dev/null and b/docs/images/img-copy-speed-2.png differ diff --git a/docs/images/img-drawBezier.png b/docs/images/img-drawBezier.png new file mode 100644 index 0000000..9a6aaa5 Binary files /dev/null and b/docs/images/img-drawBezier.png differ diff --git a/docs/images/img-drawCurve.png b/docs/images/img-drawCurve.png new file mode 100644 index 0000000..74e5039 Binary files /dev/null and b/docs/images/img-drawCurve.png differ diff --git a/docs/images/img-drawSector1.png b/docs/images/img-drawSector1.png new file mode 100644 index 0000000..7ac6042 Binary files /dev/null and b/docs/images/img-drawSector1.png differ diff --git a/docs/images/img-drawSector2.png b/docs/images/img-drawSector2.png new file mode 100644 index 0000000..2e551d4 Binary files /dev/null and b/docs/images/img-drawSector2.png differ diff --git a/docs/images/img-drawcircle.jpg b/docs/images/img-drawcircle.jpg new file mode 100644 index 0000000..b8b0a8e Binary files /dev/null and b/docs/images/img-drawcircle.jpg differ diff --git a/docs/images/img-drawquad.jpg b/docs/images/img-drawquad.jpg new file mode 100644 index 0000000..2513287 Binary files /dev/null and b/docs/images/img-drawquad.jpg differ diff --git a/docs/images/img-embed-progress.jpg b/docs/images/img-embed-progress.jpg new file mode 100644 index 0000000..c37fe26 Binary files /dev/null and b/docs/images/img-embed-progress.jpg differ diff --git a/docs/images/img-encoding.jpg b/docs/images/img-encoding.jpg new file mode 100644 index 0000000..02ce105 Binary files /dev/null and b/docs/images/img-encoding.jpg differ diff --git a/docs/images/img-encrypting.jpg b/docs/images/img-encrypting.jpg new file mode 100644 index 0000000..81e747a Binary files /dev/null and b/docs/images/img-encrypting.jpg differ diff --git a/docs/images/img-even-odd.png b/docs/images/img-even-odd.png new file mode 100644 index 0000000..a959c43 Binary files /dev/null and b/docs/images/img-even-odd.png differ diff --git a/docs/images/img-extract-imga.jpg b/docs/images/img-extract-imga.jpg new file mode 100644 index 0000000..1ab90a8 Binary files /dev/null and b/docs/images/img-extract-imga.jpg differ diff --git a/docs/images/img-extract-imgb.jpg b/docs/images/img-extract-imgb.jpg new file mode 100644 index 0000000..d439250 Binary files /dev/null and b/docs/images/img-extract-imgb.jpg differ diff --git a/docs/images/img-filesizes.png b/docs/images/img-filesizes.png new file mode 100644 index 0000000..34e7f38 Binary files /dev/null and b/docs/images/img-filesizes.png differ diff --git a/docs/images/img-freetext.jpg b/docs/images/img-freetext.jpg new file mode 100644 index 0000000..1766dd4 Binary files /dev/null and b/docs/images/img-freetext.jpg differ diff --git a/docs/images/img-getdrawings.png b/docs/images/img-getdrawings.png new file mode 100644 index 0000000..d2f8677 Binary files /dev/null and b/docs/images/img-getdrawings.png differ diff --git a/docs/images/img-import-progress.jpg b/docs/images/img-import-progress.jpg new file mode 100644 index 0000000..36bc223 Binary files /dev/null and b/docs/images/img-import-progress.jpg differ diff --git a/docs/images/img-inkannot.jpg b/docs/images/img-inkannot.jpg new file mode 100644 index 0000000..0ea913c Binary files /dev/null and b/docs/images/img-inkannot.jpg differ diff --git a/docs/images/img-inserttext.jpg b/docs/images/img-inserttext.jpg new file mode 100644 index 0000000..c5bd3fd Binary files /dev/null and b/docs/images/img-inserttext.jpg differ diff --git a/docs/images/img-layout-text.jpg b/docs/images/img-layout-text.jpg new file mode 100644 index 0000000..e335645 Binary files /dev/null and b/docs/images/img-layout-text.jpg differ diff --git a/docs/images/img-line-dir.png b/docs/images/img-line-dir.png new file mode 100644 index 0000000..4ef06cb Binary files /dev/null and b/docs/images/img-line-dir.png differ diff --git a/docs/images/img-linequad.jpg b/docs/images/img-linequad.jpg new file mode 100644 index 0000000..e93d336 Binary files /dev/null and b/docs/images/img-linequad.jpg differ diff --git a/docs/images/img-make-table.jpg b/docs/images/img-make-table.jpg new file mode 100644 index 0000000..6b4bcd7 Binary files /dev/null and b/docs/images/img-make-table.jpg differ diff --git a/docs/images/img-markedpdf.jpg b/docs/images/img-markedpdf.jpg new file mode 100644 index 0000000..9860354 Binary files /dev/null and b/docs/images/img-markedpdf.jpg differ diff --git a/docs/images/img-markers.jpg b/docs/images/img-markers.jpg new file mode 100644 index 0000000..6766b5d Binary files /dev/null and b/docs/images/img-markers.jpg differ diff --git a/docs/images/img-matrix-0.png b/docs/images/img-matrix-0.png new file mode 100644 index 0000000..42a6990 Binary files /dev/null and b/docs/images/img-matrix-0.png differ diff --git a/docs/images/img-matrix-1.png b/docs/images/img-matrix-1.png new file mode 100644 index 0000000..b484392 Binary files /dev/null and b/docs/images/img-matrix-1.png differ diff --git a/docs/images/img-matrix-2.png b/docs/images/img-matrix-2.png new file mode 100644 index 0000000..8de4ecc Binary files /dev/null and b/docs/images/img-matrix-2.png differ diff --git a/docs/images/img-matrix-3.png b/docs/images/img-matrix-3.png new file mode 100644 index 0000000..8955141 Binary files /dev/null and b/docs/images/img-matrix-3.png differ diff --git a/docs/images/img-matrix-4.png b/docs/images/img-matrix-4.png new file mode 100644 index 0000000..34bd2c9 Binary files /dev/null and b/docs/images/img-matrix-4.png differ diff --git a/docs/images/img-matrix-5.png b/docs/images/img-matrix-5.png new file mode 100644 index 0000000..d84ce1e Binary files /dev/null and b/docs/images/img-matrix-5.png differ diff --git a/docs/images/img-matrix-6.png b/docs/images/img-matrix-6.png new file mode 100644 index 0000000..a034d05 Binary files /dev/null and b/docs/images/img-matrix-6.png differ diff --git a/docs/images/img-matrix-7.png b/docs/images/img-matrix-7.png new file mode 100644 index 0000000..324397e Binary files /dev/null and b/docs/images/img-matrix-7.png differ diff --git a/docs/images/img-matrix.png b/docs/images/img-matrix.png new file mode 100644 index 0000000..d10ce90 Binary files /dev/null and b/docs/images/img-matrix.png differ diff --git a/docs/images/img-opacity.jpg b/docs/images/img-opacity.jpg new file mode 100644 index 0000000..beb011e Binary files /dev/null and b/docs/images/img-opacity.jpg differ diff --git a/docs/images/img-pdfjoiner.jpg b/docs/images/img-pdfjoiner.jpg new file mode 100644 index 0000000..e16cada Binary files /dev/null and b/docs/images/img-pdfjoiner.jpg differ diff --git a/docs/images/img-pdftext.jpg b/docs/images/img-pdftext.jpg new file mode 100644 index 0000000..36b82cd Binary files /dev/null and b/docs/images/img-pdftext.jpg differ diff --git a/docs/images/img-pixmapcopy.jpg b/docs/images/img-pixmapcopy.jpg new file mode 100644 index 0000000..c52a3f1 Binary files /dev/null and b/docs/images/img-pixmapcopy.jpg differ diff --git a/docs/images/img-planish.png b/docs/images/img-planish.png new file mode 100644 index 0000000..84f32d2 Binary files /dev/null and b/docs/images/img-planish.png differ diff --git a/docs/images/img-point-unit.jpg b/docs/images/img-point-unit.jpg new file mode 100644 index 0000000..476af71 Binary files /dev/null and b/docs/images/img-point-unit.jpg differ diff --git a/docs/images/img-polyline.png b/docs/images/img-polyline.png new file mode 100644 index 0000000..ac5817d Binary files /dev/null and b/docs/images/img-polyline.png differ diff --git a/docs/images/img-posterize.png b/docs/images/img-posterize.png new file mode 100644 index 0000000..0719c96 Binary files /dev/null and b/docs/images/img-posterize.png differ diff --git a/docs/images/img-quads.jpg b/docs/images/img-quads.jpg new file mode 100644 index 0000000..78dc73c Binary files /dev/null and b/docs/images/img-quads.jpg differ diff --git a/docs/images/img-rect-contains.png b/docs/images/img-rect-contains.png new file mode 100644 index 0000000..fe25c23 Binary files /dev/null and b/docs/images/img-rect-contains.png differ diff --git a/docs/images/img-redact.jpg b/docs/images/img-redact.jpg new file mode 100644 index 0000000..ea2d0eb Binary files /dev/null and b/docs/images/img-redact.jpg differ diff --git a/docs/images/img-render-speed.png b/docs/images/img-render-speed.png new file mode 100644 index 0000000..f85b440 Binary files /dev/null and b/docs/images/img-render-speed.png differ diff --git a/docs/images/img-rendermode.jpg b/docs/images/img-rendermode.jpg new file mode 100644 index 0000000..50f00d3 Binary files /dev/null and b/docs/images/img-rendermode.jpg differ diff --git a/docs/images/img-rot+morph.png b/docs/images/img-rot+morph.png new file mode 100644 index 0000000..c2a2367 Binary files /dev/null and b/docs/images/img-rot+morph.png differ diff --git a/docs/images/img-rotate.png b/docs/images/img-rotate.png new file mode 100644 index 0000000..dcfcf5d Binary files /dev/null and b/docs/images/img-rotate.png differ diff --git a/docs/images/img-showpdfpage.jpg b/docs/images/img-showpdfpage.jpg new file mode 100644 index 0000000..b9f9fd6 Binary files /dev/null and b/docs/images/img-showpdfpage.jpg differ diff --git a/docs/images/img-sierpinski.png b/docs/images/img-sierpinski.png new file mode 100644 index 0000000..c680a17 Binary files /dev/null and b/docs/images/img-sierpinski.png differ diff --git a/docs/images/img-smallcaps.jpg b/docs/images/img-smallcaps.jpg new file mode 100644 index 0000000..e5f9cc9 Binary files /dev/null and b/docs/images/img-smallcaps.jpg differ diff --git a/docs/images/img-span-rect.png b/docs/images/img-span-rect.png new file mode 100644 index 0000000..d69bbc0 Binary files /dev/null and b/docs/images/img-span-rect.png differ diff --git a/docs/images/img-squiggly.png b/docs/images/img-squiggly.png new file mode 100644 index 0000000..c485cc8 Binary files /dev/null and b/docs/images/img-squiggly.png differ diff --git a/docs/images/img-stampannot.jpg b/docs/images/img-stampannot.jpg new file mode 100644 index 0000000..abd8c1c Binary files /dev/null and b/docs/images/img-stampannot.jpg differ diff --git a/docs/images/img-stencil.jpg b/docs/images/img-stencil.jpg new file mode 100644 index 0000000..dd842f4 Binary files /dev/null and b/docs/images/img-stencil.jpg differ diff --git a/docs/images/img-symbols.jpg b/docs/images/img-symbols.jpg new file mode 100644 index 0000000..4178a61 Binary files /dev/null and b/docs/images/img-symbols.jpg differ diff --git a/docs/images/img-target.png b/docs/images/img-target.png new file mode 100644 index 0000000..d88adb7 Binary files /dev/null and b/docs/images/img-target.png differ diff --git a/docs/images/img-textbox.jpg b/docs/images/img-textbox.jpg new file mode 100644 index 0000000..3617724 Binary files /dev/null and b/docs/images/img-textbox.jpg differ diff --git a/docs/images/img-textboxtract.png b/docs/images/img-textboxtract.png new file mode 100644 index 0000000..3ab3dfe Binary files /dev/null and b/docs/images/img-textboxtract.png differ diff --git a/docs/images/img-textmarker.jpg b/docs/images/img-textmarker.jpg new file mode 100644 index 0000000..e0af037 Binary files /dev/null and b/docs/images/img-textmarker.jpg differ diff --git a/docs/images/img-textmethods.png b/docs/images/img-textmethods.png new file mode 100644 index 0000000..1272520 Binary files /dev/null and b/docs/images/img-textmethods.png differ diff --git a/docs/images/img-textpage-char.png b/docs/images/img-textpage-char.png new file mode 100644 index 0000000..118fc75 Binary files /dev/null and b/docs/images/img-textpage-char.png differ diff --git a/docs/images/img-textpage.png b/docs/images/img-textpage.png new file mode 100644 index 0000000..46d8a70 Binary files /dev/null and b/docs/images/img-textpage.png differ diff --git a/docs/images/img-textperformance.png b/docs/images/img-textperformance.png new file mode 100644 index 0000000..0b70dc8 Binary files /dev/null and b/docs/images/img-textperformance.png differ diff --git a/docs/images/img-timings.png b/docs/images/img-timings.png new file mode 100644 index 0000000..6bc801f Binary files /dev/null and b/docs/images/img-timings.png differ diff --git a/docs/images/img-tinting.jpg b/docs/images/img-tinting.jpg new file mode 100644 index 0000000..387d9bb Binary files /dev/null and b/docs/images/img-tinting.jpg differ diff --git a/docs/images/img-warp.png b/docs/images/img-warp.png new file mode 100644 index 0000000..0f208b2 Binary files /dev/null and b/docs/images/img-warp.png differ diff --git a/docs/images/img-writeimage.png b/docs/images/img-writeimage.png new file mode 100644 index 0000000..ee66b00 Binary files /dev/null and b/docs/images/img-writeimage.png differ diff --git a/docs/images/mupdf-icons.jpg b/docs/images/mupdf-icons.jpg new file mode 100644 index 0000000..88e3137 Binary files /dev/null and b/docs/images/mupdf-icons.jpg differ diff --git a/docs/images/pymupdf-logo.png b/docs/images/pymupdf-logo.png new file mode 100644 index 0000000..500be02 Binary files /dev/null and b/docs/images/pymupdf-logo.png differ diff --git a/docs/images/pymupdf-sidebar-logo.png b/docs/images/pymupdf-sidebar-logo.png new file mode 100644 index 0000000..5a1e377 Binary files /dev/null and b/docs/images/pymupdf-sidebar-logo.png differ diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..75ca517 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,87 @@ +.. include:: header.rst + +.. This is the TOC in the sidebar! + + +Welcome to :title:`PyMuPDF` +================================ + +.. + .. image:: images/pymupdf-logo.png + :align: left + :scale: 10% + + +:title:`PyMuPDF` is an enhanced :title:`Python` binding for `MuPDF `_ -- a lightweight :title:`PDF`, :title:`XPS`, and :title:`E-book` viewer, renderer, and toolkit, which is maintained and developed by :title:`Artifex Software, Inc`. + +:title:`PyMuPDF` is hosted on `GitHub `_ and registered on `PyPI `_. + +| + +---- + +.. toctree:: + :caption: About + :maxdepth: 1 + + about.rst + + +.. toctree:: + :caption: User Guide + :maxdepth: 1 + + installation.rst + the-basics.rst + tutorial.rst + + + +.. toctree:: + :caption: How to Guide + :maxdepth: 3 + + recipes.rst + + +.. toctree:: + :caption: API Reference + :maxdepth: 2 + + module.rst + classes.rst + algebra.rst + lowlevel.rst + glossary.rst + vars.rst + colors.rst + + +.. toctree:: + :caption: Other + :maxdepth: 2 + + app1.rst + app2.rst + app3.rst + app4.rst + changes.rst + znames.rst + + + +Find out about PyMuPDF Utilities +------------------------------------------------- + +The :title:`GitHub` repository `PyMuPDF-Utilities `_ contains a full range of examples, demonstrations and use cases. + + + + + + + +.. include:: footer.rst + + + diff --git a/docs/installation.rst b/docs/installation.rst new file mode 100644 index 0000000..f31bdb6 --- /dev/null +++ b/docs/installation.rst @@ -0,0 +1,197 @@ +.. include:: header.rst + + + +Installation +============= + +Requirements +--------------- + +All the examples below assume that you are running inside a Python virtual +environment. See: https://docs.python.org/3/library/venv.html for details. + +For example:: + + python -m venv pymupdf-venv + . pymupdf-venv/bin/activate + + +PyMuPDF should be installed using pip with:: + + python -m pip install --upgrade pip + python -m pip install --upgrade pymupdf + +This will install from a Python wheel if one is available for your platform. + + +Installation when a suitable wheel is not available +--------------------------------------------------------- + +If a suitable Python wheel is not available, pip will automatically build from +source using a Python sdist. + +**This requires C/C++ development tools and SWIG to be installed**: + +* On Unix-style systems such as Linux, OpenBSD and FreeBSD, + use the system package manager to install SWIG. + + * For example on Debian Linux, do: `sudo apt install swig` + +* On Windows: + + * Install Visual Studio 2019. If not installed in a standard location, set + environmental variable `PYMUPDF_SETUP_DEVENV` to the location of the + `devenv.com` binary. + + * Having other installed versions of Visual Studio, for example Visual + Studio 2022, can cause problems because one can end up with MuPDF and + PyMuPDF code being compiled with different compiler versions. + + * Install SWIG by following the instructions at: + https://swig.org/Doc4.0/Windows.html#Windows_installation + +* On MacOS, install MacPorts using the instructions at: + https://www.macports.org/install.php + + * Then install SWIG with: `sudo port install swig` + * You may also need: `sudo port install swig-python` + +As of `PyMuPDF-1.20.0`, the required MuPDF source code is already in the +sdist and is automatically built into PyMuPDF. + + +Notes +--------------------------------------------------------- + +Wheels are available for Windows (32-bit Intel, 64-bit Intel), Linux (64-bit Intel, 64-bit ARM) and Mac OSX (64-bit Intel, 64-bit ARM), Python versions 3.7 and up. + +Wheels are not available for Python installed with `Chocolatey +`_ on Windows. Instead install Python +using the Windows installer from the python.org website, see: +http://www.python.org/downloads + +PyMuPDF does not support Python versions prior to 3.7. Older wheels can be found in `this `_ repository and on `PyPI `_. +Please note that we generally follow the official Python release schedules. For Python versions dropping out of official support this means, that generation of wheels will also be ceased for them. + +There are no **mandatory** external dependencies. However, some optional feature are available only if additional components are installed: + +* `Pillow `_ is required for :meth:`Pixmap.pil_save` and :meth:`Pixmap.pil_tobytes`. +* `fontTools `_ is required for :meth:`Document.subset_fonts`. +* `pymupdf-fonts `_ is a collection of nice fonts to be used for text output methods. +* `Tesseract-OCR `_ for optical character recognition in images and document pages. Tesseract is separate software, not a Python package. To enable OCR functions in PyMuPDF, the software must be installed and the system environment variable `"TESSDATA_PREFIX"` must be defined and contain the `tessdata` folder name of the Tesseract installation location. See below. + +.. note:: You can install these additional components at any time -- before or after installing PyMuPDF. PyMuPDF will detect their presence during import or when the respective functions are being used. + + +Installation from source without using an sdist +--------------------------------------------------------- + +* First get a PyMuPDF source tree: + + * Clone the git repository at https://github.com/pymupdf/PyMuPDF, + for example:: + + git clone https://github.com/pymupdf/PyMuPDF.git + + * Or download and extract a `.zip` or `.tar.gz` source release from + https://github.com/pymupdf/PyMuPDF/releases. + +* Install C/C++ development tools and SWIG as described above. + +* Build and install PyMuPDF:: + + cd PyMuPDF && python setup.py install + + This will automatically download a specific hard-coded MuPDF source release, + and build it into PyMuPDF. + +.. note:: When running Python scripts that use PyMuPDF, make sure that the + current directory is not the `PyMuPDF/` directory. + + Otherwise, confusingly, Python will attempt to import `fitz` from the local + `fitz/` directory, which will fail because it only contains source files. + + +Running tests +--------------------------------------------------------- + +Having a PyMuPDF tree available allows one to run PyMuPDF's `pytest` test +suite:: + + pip install pytest fontTools + pytest PyMuPDF/tests + + +Building and testing with git checkouts of PyMuPDF and MuPDF +------------------------------------------------------------------------------------------------------------------ + +Things to do: + +* Install C/C++ development tools and SWIG as described above. +* Get PyMuPDF. +* Get MuPDF. +* Create a Python virtual environment. +* Build PyMuPDF with environmental variable `PYMUPDF_SETUP_MUPDF_BUILD` set + to the path of the local MuPDF checkout. +* Run PyMuPDF tests. + +For example:: + + git clone -b 1.22 https://github.com/pymupdf/PyMuPDF.git + git clone -b 1.22.x --recursive https://ghostscript.com:/home/git/mupdf.git + python -m venv pymupdf-venv + . pymupdf-venv/bin/activate + cd PyMuPDF + PYMUPDF_SETUP_MUPDF_BUILD=../mupdf python setup.py install + cd .. + pip install pytest fontTools + pytest PyMuPDF + + +Using a non-default MuPDF +--------------------------------------------------------- + +Using a non-default build of MuPDF by setting environmental variable +`PYMUPDF_SETUP_MUPDF_BUILD` can cause various things to go wrong and so is +not generally supported: + +* If MuPDF's major version number differs from what PyMuPDF uses by default, + PyMuPDF can fail to build, because MuPDF's API can change between major + versions. + +* Runtime behaviour of PyMuPDF can change because MuPDF's runtime behaviour + changes between different minor releases. This can also break some PyMuPDF + tests. + +* If MuPDF was built with its default config instead of PyMuPDF's customised + config (for example if MuPDF is a system install), it is possible that + `tests/test_textbox.py:test_textbox3()` will fail. One can skip this + particular test by adding `-k 'not test_textbox3'` to the `pytest` + command line. + + +Enabling Integrated OCR Support +--------------------------------------------------------- + +If you do not intend to use this feature, skip this step. Otherwise, it is required for both installation paths: **from wheels and from sources.** + +PyMuPDF will already contain all the logic to support OCR functions. But it additionally does need Tesseract's language support data, so installation of Tesseract-OCR is still required. + +The language support folder location must be communicated either via storing it in the environment variable `"TESSDATA_PREFIX"`, or as a parameter in the applicable functions. + +So for a working OCR functionality, make sure to complete this checklist: + +1. Install Tesseract. + +2. Locate Tesseract's language support folder. Typically you will find it here: + - Windows: `C:/Program Files/Tesseract-OCR/tessdata` + - Unix systems: `/usr/share/tesseract-ocr/4.00/tessdata` + +3. Set the environment variable `TESSDATA_PREFIX` + - Windows: `setx TESSDATA_PREFIX "C:/Program Files/Tesseract-OCR/tessdata"` + - Unix systems: `declare -x TESSDATA_PREFIX=/usr/share/tesseract-ocr/4.00/tessdata` + +.. note:: On Windows systems, this must happen outside Python -- before starting your script. Just manipulating `os.environ` will not work! + +.. include:: footer.rst diff --git a/docs/intro.rst b/docs/intro.rst new file mode 100644 index 0000000..4d5ae70 --- /dev/null +++ b/docs/intro.rst @@ -0,0 +1,64 @@ +.. include:: header.rst + +Introduction +============== + +.. image:: images/pymupdf-logo.png + :align: center + :scale: 10% + +.. + Don't delete the bar symbol - it forces a line break beneath the image - which is required. + +| + +**PyMuPDF** is a Python binding for `MuPDF `_ -- a lightweight PDF, XPS, and E-book viewer, renderer, and toolkit, which is maintained and developed by Artifex Software, Inc + +MuPDF can access files in PDF, XPS, OpenXPS, CBZ, EPUB, MOBI and FB2 (e-books) formats, and it is known for its top performance and high rendering quality. + +MuPDF stands out among all similar products for its top rendering capability and unsurpassed processing speed. At the same time, its "light weight" makes it an excellent choice for platforms where resources are typically limited, like smartphones. + +Check this out yourself and compare the various free PDF-viewers. In terms of speed and rendering quality `SumatraPDF `_ ranges at the top (apart from MuPDF's own standalone viewer) -- since it has changed its library basis to MuPDF! + +With PyMuPDF you can access files with extensions like “.pdf”, “.xps”, “.oxps”, “.cbz”, “.fb2”, ".mobi" or “.epub”. In addition, about 10 popular image formats can also be opened and handled like documents. + +PyMuPDF provides access to many important functions of MuPDF from within a Python environment, and we are continuously seeking to expand this function set. + +PyMuPDF runs and has been tested on Mac, Linux and Windows for Python versions 3.7 [#f1]_ and up. Other platforms should work too, as long as MuPDF and Python support them. + +PyMuPDF is hosted on `GitHub `_ and registered on `PyPI `_. + +For MS Windows, Mac OSX and Linux Python wheels are available -- please see the installation chapter. + +The GitHub repository `PyMuPDF-Utilities `_ contains a full range of examples, demonstrations and use cases. + +Note on the Name *fitz* +-------------------------- +The top level Python import name for this library is **"fitz"**. This has historical reasons: + +The original rendering library for MuPDF was called *Libart*. + +*"After Artifex Software acquired the MuPDF project, the development focus shifted on writing a new modern graphics library called "Fitz". Fitz was originally intended as an R&D project to replace the aging Ghostscript graphics library, but has instead become the rendering engine powering MuPDF."* (Quoted from `Wikipedia `_). + +So PyMuPDF **cannot coexist** with packages named "fitz" in the same Python environment. + +License and Copyright +---------------------- +In order to comply with MuPDF’s dual licensing model, PyMuPDF has entered into an agreement with Artifex who has the right to sublicense PyMuPDF to third parties. + +PyMuPDF and MuPDF are now available under both, open-source AGPL and commercial license agreements. Please read the full text of the AGPL license agreement, available in the distribution material (file COPYING) and `here `_, to ensure that your use case complies with the guidelines of the license. If you determine you cannot meet the requirements of the AGPL, please contact `Artifex `_ for more information regarding a commercial license. + +Artifex is the exclusive commercial licensing agent for MuPDF. + +Artifex, the Artifex logo, MuPDF, and the MuPDF logo are registered trademarks of Artifex Software Inc. © 2022 Artifex Software, Inc. All rights reserved. + +.. include:: version.rst + +----- + +.. rubric:: Footnotes + + +.. [#f1] PyMuPDF generally only supports Python versions that are still maintained by the Python Software Foundation. Once a Python version is being retired, PyMuPDF support will also be ended. This means that wheels for a retired Python platform will no longer be provided, and that Python language features may be used that did not exist in the retired Python version. + +.. include:: footer.rst diff --git a/docs/irect.rst b/docs/irect.rst new file mode 100644 index 0000000..c611155 --- /dev/null +++ b/docs/irect.rst @@ -0,0 +1,220 @@ +.. include:: header.rst + +.. _IRect: + +========== +IRect +========== + +IRect is a rectangular bounding box, very similar to :ref:`Rect`, except that all corner coordinates are integers. IRect is used to specify an area of pixels, e.g. to receive image data during rendering. Otherwise, e.g. considerations concerning emptiness and validity of rectangles also apply to this class. Methods and attributes have the same names, and in many cases are implemented by re-using the respective :ref:`Rect` counterparts. + +============================== ============================================== +**Attribute / Method** **Short Description** +============================== ============================================== +:meth:`IRect.contains` checks containment of another object +:meth:`IRect.get_area` calculate rectangle area +:meth:`IRect.intersect` common part with another rectangle +:meth:`IRect.intersects` checks for non-empty intersection +:meth:`IRect.morph` transform with a point and a matrix +:meth:`IRect.torect` matrix that transforms to another rectangle +:meth:`IRect.norm` the Euclidean norm +:meth:`IRect.normalize` makes a rectangle finite +:attr:`IRect.bottom_left` bottom left point, synonym *bl* +:attr:`IRect.bottom_right` bottom right point, synonym *br* +:attr:`IRect.height` height of the rectangle +:attr:`IRect.is_empty` whether rectangle is empty +:attr:`IRect.is_infinite` whether rectangle is infinite +:attr:`IRect.rect` the :ref:`Rect` equivalent +:attr:`IRect.top_left` top left point, synonym *tl* +:attr:`IRect.top_right` top_right point, synonym *tr* +:attr:`IRect.quad` :ref:`Quad` made from rectangle corners +:attr:`IRect.width` width of the rectangle +:attr:`IRect.x0` X-coordinate of the top left corner +:attr:`IRect.x1` X-coordinate of the bottom right corner +:attr:`IRect.y0` Y-coordinate of the top left corner +:attr:`IRect.y1` Y-coordinate of the bottom right corner +============================== ============================================== + +**Class API** + +.. class:: IRect + + .. method:: __init__(self) + + .. method:: __init__(self, x0, y0, x1, y1) + + .. method:: __init__(self, irect) + + .. method:: __init__(self, sequence) + + Overloaded constructors. Also see examples below and those for the :ref:`Rect` class. + + If another irect is specified, a **new copy** will be made. + + If sequence is specified, it must be a Python sequence type of 4 numbers (see :ref:`SequenceTypes`). Non-integer numbers will be truncated, non-numeric values will raise an exception. + + The other parameters mean integer coordinates. + + + .. method:: get_area([unit]) + + Calculates the area of the rectangle and, with no parameter, equals *abs(IRect)*. Like an empty rectangle, the area of an infinite rectangle is also zero. + + :arg str unit: Specify required unit: respective squares of "px" (pixels, default), "in" (inches), "cm" (centimeters), or "mm" (millimeters). + + :rtype: float + + .. method:: intersect(ir) + + The intersection (common rectangular area) of the current rectangle and *ir* is calculated and replaces the current rectangle. If either rectangle is empty, the result is also empty. If either rectangle is infinite, the other one is taken as the result -- and hence also infinite if both rectangles were infinite. + + :arg rect_like ir: Second rectangle. + + .. method:: contains(x) + + Checks whether *x* is contained in the rectangle. It may be :data:`rect_like`, :data:`point_like` or a number. If *x* is an empty rectangle, this is always true. Conversely, if the rectangle is empty this is always *False*, if *x* is not an empty rectangle and not a number. If *x* is a number, it will be checked to be one of the four components. *x in irect* and *irect.contains(x)* are equivalent. + + :arg x: the object to check. + :type x: :ref:`IRect` or :ref:`Rect` or :ref:`Point` or int + + :rtype: bool + + .. method:: intersects(r) + + Checks whether the rectangle and the :data:`rect_like` "r" contain a common non-empty :ref:`IRect`. This will always be *False* if either is infinite or empty. + + :arg rect_like r: the rectangle to check. + + :rtype: bool + + .. method:: torect(rect) + + * New in version 1.19.3 + + Compute the matrix which transforms this rectangle to a given one. See :meth:`Rect.torect`. + + :arg rect_like rect: the target rectangle. Must not be empty or infinite. + :rtype: :ref:`Matrix` + :returns: a matrix `mat` such that `self * mat = rect`. Can for example be used to transform between the page and the pixmap coordinates. + + + .. method:: morph(fixpoint, matrix) + + * New in version 1.17.0 + + Return a new quad after applying a matrix to it using a fixed point. + + :arg point_like fixpoint: the fixed point. + :arg matrix_like matrix: the matrix. + :returns: a new :ref:`Quad`. This a wrapper of the same-named quad method. If infinite, the infinite quad is returned. + + .. method:: norm() + + * New in version 1.16.0 + + Return the Euclidean norm of the rectangle treated as a vector of four numbers. + + .. method:: normalize() + + Make the rectangle finite. This is done by shuffling rectangle corners. After this, the bottom right corner will indeed be south-eastern to the top left one. See :ref:`Rect` for a more details. + + .. attribute:: top_left + + .. attribute:: tl + + Equals *Point(x0, y0)*. + + :type: :ref:`Point` + + .. attribute:: top_right + + .. attribute:: tr + + Equals *Point(x1, y0)*. + + :type: :ref:`Point` + + .. attribute:: bottom_left + + .. attribute:: bl + + Equals *Point(x0, y1)*. + + :type: :ref:`Point` + + .. attribute:: bottom_right + + .. attribute:: br + + Equals *Point(x1, y1)*. + + :type: :ref:`Point` + + .. attribute:: rect + + The :ref:`Rect` with the same coordinates as floats. + + :type: :ref:`Rect` + + .. attribute:: quad + + The quadrilateral *Quad(irect.tl, irect.tr, irect.bl, irect.br)*. + + :type: :ref:`Quad` + + .. attribute:: width + + Contains the width of the bounding box. Equals *abs(x1 - x0)*. + + :type: int + + .. attribute:: height + + Contains the height of the bounding box. Equals *abs(y1 - y0)*. + + :type: int + + .. attribute:: x0 + + X-coordinate of the left corners. + + :type: int + + .. attribute:: y0 + + Y-coordinate of the top corners. + + :type: int + + .. attribute:: x1 + + X-coordinate of the right corners. + + :type: int + + .. attribute:: y1 + + Y-coordinate of the bottom corners. + + :type: int + + .. attribute:: is_infinite + + *True* if rectangle is infinite, *False* otherwise. + + :type: bool + + .. attribute:: is_empty + + *True* if rectangle is empty, *False* otherwise. + + :type: bool + + +.. note:: + + * This class adheres to the Python sequence protocol, so components can be accessed via their index, too. Also refer to :ref:`SequenceTypes`. + * Rectangles can be used with arithmetic operators -- see chapter :ref:`Algebra`. + +.. include:: footer.rst + diff --git a/docs/link.rst b/docs/link.rst new file mode 100644 index 0000000..6159db8 --- /dev/null +++ b/docs/link.rst @@ -0,0 +1,124 @@ +.. include:: header.rst + +.. _Link: + +================ +Link +================ +Represents a pointer to somewhere (this document, other documents, the internet). Links exist per document page, and they are forward-chained to each other, starting from an initial link which is accessible by the :attr:`Page.first_link` property. + +There is a parent-child relationship between a link and its page. If the page object becomes unusable (closed document, any document structure change, etc.), then so does every of its existing link objects -- an exception is raised saying that the object is "orphaned", whenever a link property or method is accessed. + +========================= ============================================ +**Attribute** **Short Description** +========================= ============================================ +:meth:`Link.set_border` modify border properties +:meth:`Link.set_colors` modify color properties +:meth:`Link.set_flags` modify link flags +:attr:`Link.border` border characteristics +:attr:`Link.colors` border line color +:attr:`Link.dest` points to destination details +:attr:`Link.is_external` external destination? +:attr:`Link.flags` link annotation flags +:attr:`Link.next` points to next link +:attr:`Link.rect` clickable area in untransformed coordinates. +:attr:`Link.uri` link destination +:attr:`Link.xref` :data:`xref` number of the entry +========================= ============================================ + +**Class API** + +.. class:: Link + + .. method:: set_border(border=None, width=0, style=None, dashes=None) + + PDF only: Change border width and dashing properties. + + *(Changed in version 1.16.9)* Allow specification without using a dictionary. The direct parameters are used if *border* is not a dictionary. + + :arg dict border: a dictionary as returned by the :attr:`border` property, with keys *"width"* (*float*), *"style"* (*str*) and *"dashes"* (*sequence*). Omitted keys will leave the resp. property unchanged. To e.g. remove dashing use: *"dashes": []*. If dashes is not an empty sequence, "style" will automatically be set to "D" (dashed). + + :arg float width: see above. + :arg str style: see above. + :arg sequence dashes: see above. + + .. method:: set_colors(colors=None, stroke=None) + + PDF only: Changes the "stroke" color. + + .. note:: In PDF, links are a subtype of annotations technically and **do not support fill colors**. However, to keep a consistent API, we do allow specifying a `fill=` parameter like with all annotations, which will be ignored with a warning. + + *(Changed in version 1.16.9)* Allow colors to be directly set. These parameters are used if *colors* is not a dictionary. + + :arg dict colors: a dictionary containing color specifications. For accepted dictionary keys and values see below. The most practical way should be to first make a copy of the *colors* property and then modify this dictionary as required. + :arg sequence stroke: see above. + + .. method:: set_flags(flags) + + *New in v1.18.16* + + Set the PDF `/F` property of the link annotation. See :meth:`Annot.set_flags` for details. If not a PDF, this method is a no-op. + + + .. attribute:: flags + + *New in v1.18.16* + + Return the link annotation flags, an integer (see :attr:`Annot.flags` for details). Zero if not a PDF. + + + .. attribute:: colors + + Meaningful for PDF only: A dictionary of two tuples of floats in range `0 <= float <= 1` specifying the *stroke* and the interior (*fill*) colors. If not a PDF, *None* is returned. As mentioned above, the fill color is always `None` for links. The stroke color is used for the border of the link rectangle. The length of the tuple implicitly determines the colorspace: 1 = GRAY, 3 = RGB, 4 = CMYK. So `(1.0, 0.0, 0.0)` stands for RGB color red. The value of each float *f* is mapped to the integer value *i* in range 0 to 255 via the computation *f = i / 255*. + + :rtype: dict + + .. attribute:: border + + Meaningful for PDF only: A dictionary containing border characteristics. It will be *None* for non-PDFs and an empty dictionary if no border information exists. The following keys can occur: + + * *width* -- a float indicating the border thickness in points. The value is -1.0 if no width is specified. + + * *dashes* -- a sequence of integers specifying a line dash pattern. *[]* means no dashes, *[n]* means equal on-off lengths of *n* points, longer lists will be interpreted as specifying alternating on-off length values. See the :ref:`AdobeManual` page 126 for more detail. + + * *style* -- 1-byte border style: *S* (Solid) = solid rectangle surrounding the annotation, *D* (Dashed) = dashed rectangle surrounding the link, the dash pattern is specified by the *dashes* entry, *B* (Beveled) = a simulated embossed rectangle that appears to be raised above the surface of the page, *I* (Inset) = a simulated engraved rectangle that appears to be recessed below the surface of the page, *U* (Underline) = a single line along the bottom of the annotation rectangle. + + :rtype: dict + + .. attribute:: rect + + The area that can be clicked in untransformed coordinates. + + :type: :ref:`Rect` + + .. attribute:: isExternal + + A bool specifying whether the link target is outside of the current document. + + :type: bool + + .. attribute:: uri + + A string specifying the link target. The meaning of this property should be evaluated in conjunction with property *isExternal*. The value may be *None*, in which case *isExternal == False*. If *uri* starts with *file://*, *mailto:*, or an internet resource name, *isExternal* is *True*. In all other cases *isExternal == False* and *uri* points to an internal location. In case of PDF documents, this should either be *#nnnn* to indicate a 1-based (!) page number *nnnn*, or a named location. The format varies for other document types, e.g. *uri = '../FixedDoc.fdoc#PG_2_LNK_1'* for page number 2 (1-based) in an XPS document. + + :type: str + + .. attribute:: xref + + An integer specifying the PDF :data:`xref`. Zero if not a PDF. + + :type: int + + .. attribute:: next + + The next link or *None*. + + :type: *Link* + + .. attribute:: dest + + The link destination details object. + + :type: :ref:`linkDest` + +.. include:: footer.rst diff --git a/docs/linkdest.rst b/docs/linkdest.rst new file mode 100644 index 0000000..e13d729 --- /dev/null +++ b/docs/linkdest.rst @@ -0,0 +1,105 @@ +.. include:: header.rst + +.. _linkDest: + +================ +linkDest +================ +Class representing the `dest` property of an outline entry or a link. Describes the destination to which such entries point. + +.. note:: Up to MuPDF v1.9.0 this class existed inside MuPDF and was dropped in version 1.10.0. For backward compatibility, PyMuPDF is still maintaining it, although some of its attributes are no longer backed by data actually available via MuPDF. + +=========================== ==================================== +**Attribute** **Short Description** +=========================== ==================================== +:attr:`linkDest.dest` destination +:attr:`linkDest.fileSpec` file specification (path, filename) +:attr:`linkDest.flags` descriptive flags +:attr:`linkDest.isMap` is this a MAP? +:attr:`linkDest.isUri` is this a URI? +:attr:`linkDest.kind` kind of destination +:attr:`linkDest.lt` top left coordinates +:attr:`linkDest.named` name if named destination +:attr:`linkDest.newWindow` name of new window +:attr:`linkDest.page` page number +:attr:`linkDest.rb` bottom right coordinates +:attr:`linkDest.uri` URI +=========================== ==================================== + +**Class API** + +.. class:: linkDest + + .. attribute:: dest + + Target destination name if :attr:`linkDest.kind` is :data:`LINK_GOTOR` and :attr:`linkDest.page` is *-1*. + + :type: str + + .. attribute:: fileSpec + + Contains the filename and path this link points to, if :attr:`linkDest.kind` is :data:`LINK_GOTOR` or :data:`LINK_LAUNCH`. + + :type: str + + .. attribute:: flags + + A bitfield describing the validity and meaning of the different aspects of the destination. As far as possible, link destinations are constructed such that e.g. :attr:`linkDest.lt` and :attr:`linkDest.rb` can be treated as defining a bounding box. But the flags indicate which of the values were actually specified, see :ref:`linkDest Flags`. + + :type: int + + .. attribute:: isMap + + This flag specifies whether to track the mouse position when the URI is resolved. Default value: False. + + :type: bool + + .. attribute:: isUri + + Specifies whether this destination is an internet resource (as opposed to e.g. a local file specification in URI format). + + :type: bool + + .. attribute:: kind + + Indicates the type of this destination, like a place in this document, a URI, a file launch, an action or a place in another file. Look at :ref:`linkDest Kinds` to see the names and numerical values. + + :type: int + + .. attribute:: lt + + The top left :ref:`Point` of the destination. + + :type: :ref:`Point` + + .. attribute:: named + + This destination refers to some named action to perform (e.g. a javascript, see :ref:`AdobeManual`). Standard actions provided are *NextPage*, *PrevPage*, *FirstPage*, and *LastPage*. + + :type: str + + .. attribute:: newWindow + + If true, the destination should be launched in a new window. + + :type: bool + + .. attribute:: page + + The page number (in this or the target document) this destination points to. Only set if :attr:`linkDest.kind` is :data:`LINK_GOTOR` or :data:`LINK_GOTO`. May be *-1* if :attr:`linkDest.kind` is :data:`LINK_GOTOR`. In this case :attr:`linkDest.dest` contains the **name** of a destination in the target document. + + :type: int + + .. attribute:: rb + + The bottom right :ref:`Point` of this destination. + + :type: :ref:`Point` + + .. attribute:: uri + + The name of the URI this destination points to. + + :type: str + +.. include:: footer.rst diff --git a/docs/lowlevel.rst b/docs/lowlevel.rst new file mode 100644 index 0000000..b2c1249 --- /dev/null +++ b/docs/lowlevel.rst @@ -0,0 +1,15 @@ +.. include:: header.rst + +================================= +Low Level Functions and Classes +================================= +Contains a number of functions and classes for the experienced user. To be used for special needs or performance requirements. + +.. toctree:: + :maxdepth: 1 + + functions + device + coop_low + +.. include:: footer.rst diff --git a/docs/matrix.rst b/docs/matrix.rst new file mode 100644 index 0000000..f07398d --- /dev/null +++ b/docs/matrix.rst @@ -0,0 +1,225 @@ +.. include:: header.rst + +.. _Matrix: + +========== +Matrix +========== + +Matrix is a row-major 3x3 matrix used by image transformations in MuPDF (which complies with the respective concepts laid down in the :ref:`AdobeManual`). With matrices you can manipulate the rendered image of a page in a variety of ways: (parts of) the page can be rotated, zoomed, flipped, sheared and shifted by setting some or all of just six float values. + + +Since all points or pixels live in a two-dimensional space, one column vector of that matrix is a constant unit vector, and only the remaining six elements are used for manipulations. These six elements are usually represented by *[a, b, c, d, e, f]*. Here is how they are positioned in the matrix: + +.. image:: images/img-matrix.* + + +Please note: + + * the below methods are just convenience functions -- everything they do, can also be achieved by directly manipulating the six numerical values + * all manipulations can be combined -- you can construct a matrix that rotates **and** shears **and** scales **and** shifts, etc. in one go. If you however choose to do this, do have a look at the **remarks** further down or at the :ref:`AdobeManual`. + +================================ ============================================== +**Method / Attribute** **Description** +================================ ============================================== +:meth:`Matrix.prerotate` perform a rotation +:meth:`Matrix.prescale` perform a scaling +:meth:`Matrix.preshear` perform a shearing (skewing) +:meth:`Matrix.pretranslate` perform a translation (shifting) +:meth:`Matrix.concat` perform a matrix multiplication +:meth:`Matrix.invert` calculate the inverted matrix +:meth:`Matrix.norm` the Euclidean norm +:attr:`Matrix.a` zoom factor X direction +:attr:`Matrix.b` shearing effect Y direction +:attr:`Matrix.c` shearing effect X direction +:attr:`Matrix.d` zoom factor Y direction +:attr:`Matrix.e` horizontal shift +:attr:`Matrix.f` vertical shift +:attr:`Matrix.is_rectilinear` true if rect corners will remain rect corners +================================ ============================================== + +**Class API** + +.. class:: Matrix + + .. method:: __init__(self) + + .. method:: __init__(self, zoom-x, zoom-y) + + .. method:: __init__(self, shear-x, shear-y, 1) + + .. method:: __init__(self, a, b, c, d, e, f) + + .. method:: __init__(self, matrix) + + .. method:: __init__(self, degree) + + .. method:: __init__(self, sequence) + + Overloaded constructors. + + Without parameters, the zero matrix *Matrix(0.0, 0.0, 0.0, 0.0, 0.0, 0.0)* will be created. + + *zoom-** and *shear-** specify zoom or shear values (float) and create a zoom or shear matrix, respectively. + + For "matrix" a **new copy** of another matrix will be made. + + Float value "degree" specifies the creation of a rotation matrix which rotates anti-clockwise. + + A "sequence" must be any Python sequence object with exactly 6 float entries (see :ref:`SequenceTypes`). + + *fitz.Matrix(1, 1)*, *fitz.Matrix(0.0 and *fitz.Matrix(fitz.Identity)* create modifiable versions of the :ref:`Identity` matrix, which looks like *[1, 0, 0, 1, 0, 0]*. + + .. method:: norm() + + * New in version 1.16.0 + + Return the Euclidean norm of the matrix as a vector. + + .. method:: prerotate(deg) + + Modify the matrix to perform a counter-clockwise rotation for positive *deg* degrees, else clockwise. The matrix elements of an identity matrix will change in the following way: + + *[1, 0, 0, 1, 0, 0] -> [cos(deg), sin(deg), -sin(deg), cos(deg), 0, 0]*. + + :arg float deg: The rotation angle in degrees (use conventional notation based on Pi = 180 degrees). + + .. method:: prescale(sx, sy) + + Modify the matrix to scale by the zoom factors sx and sy. Has effects on attributes *a* thru *d* only: *[a, b, c, d, e, f] -> [a*sx, b*sx, c*sy, d*sy, e, f]*. + + :arg float sx: Zoom factor in X direction. For the effect see description of attribute *a*. + + :arg float sy: Zoom factor in Y direction. For the effect see description of attribute *d*. + + .. method:: preshear(sx, sy) + + Modify the matrix to perform a shearing, i.e. transformation of rectangles into parallelograms (rhomboids). Has effects on attributes *a* thru *d* only: *[a, b, c, d, e, f] -> [c*sy, d*sy, a*sx, b*sx, e, f]*. + + :arg float sx: Shearing effect in X direction. See attribute *c*. + + :arg float sy: Shearing effect in Y direction. See attribute *b*. + + .. method:: pretranslate(tx, ty) + + Modify the matrix to perform a shifting / translation operation along the x and / or y axis. Has effects on attributes *e* and *f* only: *[a, b, c, d, e, f] -> [a, b, c, d, tx*a + ty*c, tx*b + ty*d]*. + + :arg float tx: Translation effect in X direction. See attribute *e*. + + :arg float ty: Translation effect in Y direction. See attribute *f*. + + .. method:: concat(m1, m2) + + Calculate the matrix product *m1 * m2* and store the result in the current matrix. Any of *m1* or *m2* may be the current matrix. Be aware that matrix multiplication is not commutative. So the sequence of *m1*, *m2* is important. + + :arg m1: First (left) matrix. + :type m1: :ref:`Matrix` + + :arg m2: Second (right) matrix. + :type m2: :ref:`Matrix` + + .. method:: invert(m = None) + + Calculate the matrix inverse of *m* and store the result in the current matrix. Returns *1* if *m* is not invertible ("degenerate"). In this case the current matrix **will not change**. Returns *0* if *m* is invertible, and the current matrix is replaced with the inverted *m*. + + :arg m: Matrix to be inverted. If not provided, the current matrix will be used. + :type m: :ref:`Matrix` + + :rtype: int + + .. attribute:: a + + Scaling in X-direction **(width)**. For example, a value of 0.5 performs a shrink of the **width** by a factor of 2. If a < 0, a left-right flip will (additionally) occur. + + :type: float + + .. attribute:: b + + Causes a shearing effect: each `Point(x, y)` will become `Point(x, y - b*x)`. Therefore, horizontal lines will be "tilt". + + :type: float + + .. attribute:: c + + Causes a shearing effect: each `Point(x, y)` will become `Point(x - c*y, y)`. Therefore, vertical lines will be "tilt". + + :type: float + + .. attribute:: d + + Scaling in Y-direction **(height)**. For example, a value of 1.5 performs a stretch of the **height** by 50%. If d < 0, an up-down flip will (additionally) occur. + + :type: float + + .. attribute:: e + + Causes a horizontal shift effect: Each *Point(x, y)* will become *Point(x + e, y)*. Positive (negative) values of *e* will shift right (left). + + :type: float + + .. attribute:: f + + Causes a vertical shift effect: Each *Point(x, y)* will become *Point(x, y - f)*. Positive (negative) values of *f* will shift down (up). + + :type: float + + .. attribute:: is_rectilinear + + Rectilinear means that no shearing is present and that any rotations are integer multiples of 90 degrees. Usually this is used to confirm that (axis-aligned) rectangles before the transformation are still axis-aligned rectangles afterwards. + + :type: bool + +.. note:: + + * This class adheres to the Python sequence protocol, so components can be accessed via their index, too. Also refer to :ref:`SequenceTypes`. + * Matrices can be used with arithmetic operators almost like ordinary numbers: they can be added, subtracted, multiplied or divided -- see chapter :ref:`Algebra`. + * Matrix multiplication is **not commutative** -- changing the sequence of the multiplicands will change the result in general. So it can quickly become unclear which result a transformation will yield. + + +Examples +------------- +Here are examples that illustrate some of the achievable effects. All pictures show some text, inserted under control of some matrix and relative to a fixed reference point (the red dot). + +1. The :ref:`Identity` matrix performs no operation. + +.. image:: images/img-matrix-0.* + :scale: 66 + +2. The scaling matrix `Matrix(2, 0.5)` stretches by a factor of 2 in horizontal, and shrinks by factor 0.5 in vertical direction. + +.. image:: images/img-matrix-1.* + :scale: 66 + +3. Attributes :attr:`Matrix.e` and :attr:`Matrix.f` shift horizontally and, respectively vertically. In the following 10 to the right and 20 down. + +.. image:: images/img-matrix-2.* + :scale: 66 + +4. A negative :attr:`Matrix.a` causes a left-right flip. + +.. image:: images/img-matrix-3.* + :scale: 66 + +5. A negative :attr:`Matrix.d` causes an up-down flip. + +.. image:: images/img-matrix-4.* + :scale: 66 + +6. Attribute :attr:`Matrix.b` tilts upwards / downwards along the x-axis. + +.. image:: images/img-matrix-5.* + :scale: 66 + +7. Attribute :attr:`Matrix.c` tilts left / right along the y-axis. + +.. image:: images/img-matrix-6.* + :scale: 66 + +8. Matrix `Matrix(beta)` performs counterclockwise rotations for positive angles `beta`. + +.. image:: images/img-matrix-7.* + :scale: 66 + + + +.. include:: footer.rst diff --git a/docs/module.rst b/docs/module.rst new file mode 100644 index 0000000..b14b60d --- /dev/null +++ b/docs/module.rst @@ -0,0 +1,483 @@ +.. include:: header.rst + +.. _Module: + +============================ +Module *fitz* +============================ + +* New in version 1.16.8 + +PyMuPDF can also be used in the command line as a **module** to perform utility functions. This feature should obsolete writing some of the most basic scripts. + +Admittedly, there is some functional overlap with the MuPDF CLI `mutool`. On the other hand, PDF embedded files are no longer supported by MuPDF, so PyMuPDF is offering something unique here. + +Invocation +----------- + +Invoke the module like this:: + + python -m fitz + +.. highlight:: python + +General remarks: + +* Request help via `"-h"`, resp. command-specific help via `"command -h"`. +* Parameters may be abbreviated where this does not introduce ambiguities. +* Several commands support parameters `-pages` and `-xrefs`. They are intended for down-selection. Please note that: + + - **page numbers** for this utility must be given **1-based**. + - valid :data:`xref` numbers start at 1. + - Specify a comma-separated list of either *single* integers or integer *ranges*. A **range** is a pair of integers separated by one hyphen "-". Integers must not exceed the maximum page, resp. xref number. To specify that maximum, the symbolic variable "N" may be used. Integers or ranges may occur several times, in any sequence and may overlap. If in a range the first number is greater than the second one, the respective items will be processed in reversed order. + +* How to use the module inside your script:: + + >>> from fitz.__main__ import main as fitz_command + >>> cmd = "clean input.pdf output.pdf -pages 1,N".split() # prepare command line + >>> saved_parms = sys.argv[1:] # save original command line + >>> sys.argv[1:] = cmd # store new command line + >>> fitz_command() # execute module + >>> sys.argv[1:] = saved_parms # restore original command line + +* Use the following 2-liner and compile it with `Nuitka `_ in standalone mode. This will give you a CLI executable with all the module's features, that can be used on all compatible platforms without Python, PyMuPDF or MuPDF being installed. + +:: + + from fitz.__main__ import main + main() + + +Cleaning and Copying +---------------------- + +.. highlight:: text + +This command will optimize the PDF and store the result in a new file. You can use it also for encryption, decryption and creating sub documents. It is mostly similar to the MuPDF command line utility *"mutool clean"*:: + + python -m fitz clean -h + usage: fitz clean [-h] [-password PASSWORD] + [-encryption {keep,none,rc4-40,rc4-128,aes-128,aes-256}] + [-owner OWNER] [-user USER] [-garbage {0,1,2,3,4}] + [-compress] [-ascii] [-linear] [-permission PERMISSION] + [-sanitize] [-pretty] [-pages PAGES] + input output + + -------------- optimize PDF or create sub-PDF if pages given -------------- + + positional arguments: + input PDF filename + output output PDF filename + + optional arguments: + -h, --help show this help message and exit + -password PASSWORD password + -encryption {keep,none,rc4-40,rc4-128,aes-128,aes-256} + encryption method + -owner OWNER owner password + -user USER user password + -garbage {0,1,2,3,4} garbage collection level + -compress compress (deflate) output + -ascii ASCII encode binary data + -linear format for fast web display + -permission PERMISSION + integer with permission levels + -sanitize sanitize / clean contents + -pretty prettify PDF structure + -pages PAGES output selected pages, format: 1,5-7,50-N + +If you specify "-pages", be aware that only page-related objects are copied, **no document-level items** like e.g. embedded files. + +Please consult :meth:`Document.save` for the parameter meanings. + + +Extracting Fonts and Images +---------------------------- +Extract fonts or images from selected PDF pages to a desired directory:: + + python -m fitz extract -h + usage: fitz extract [-h] [-images] [-fonts] [-output OUTPUT] [-password PASSWORD] + [-pages PAGES] + input + + --------------------- extract images and fonts to disk -------------------- + + positional arguments: + input PDF filename + + optional arguments: + -h, --help show this help message and exit + -images extract images + -fonts extract fonts + -output OUTPUT output directory, defaults to current + -password PASSWORD password + -pages PAGES only consider these pages, format: 1,5-7,50-N + +**Image filenames** are built according to the naming scheme: **"img-xref.ext"**, where "ext" is the extension associated with the image and "xref" the :data:`xref` of the image PDF object. + +**Font filenames** consist of the fontname and the associated extension. Any spaces in the fontname are replaced with hyphens "-". + +The output directory must already exist. + +.. note:: Except for output directory creation, this feature is **functionally equivalent** to and obsoletes `this script `_. + + +Joining PDF Documents +----------------------- +To join several PDF files specify:: + + python -m fitz join -h + usage: fitz join [-h] -output OUTPUT [input [input ...]] + + ---------------------------- join PDF documents --------------------------- + + positional arguments: + input input filenames + + optional arguments: + -h, --help show this help message and exit + -output OUTPUT output filename + + specify each input as 'filename[,password[,pages]]' + + +.. note:: + + 1. Each input must be entered as **"filename,password,pages"**. Password and pages are optional. + 2. The password entry **is required** if the "pages" entry is used. If the PDF needs no password, specify two commas. + 3. The **"pages"** format is the same as explained at the top of this section. + 4. Each input file is immediately closed after use. Therefore you can use one of them as output filename, and thus overwrite it. + + +Example: To join the following files + +1. **file1.pdf:** all pages, back to front, no password +2. **file2.pdf:** last page, first page, password: "secret" +3. **file3.pdf:** pages 5 to last, no password + +and store the result as **output.pdf** enter this command: + +*python -m fitz join -o output.pdf file1.pdf,,N-1 file2.pdf,secret,N,1 file3.pdf,,5-N* + + +Low Level Information +---------------------- + +Display PDF internal information. Again, there are similarities to *"mutool show"*:: + + python -m fitz show -h + usage: fitz show [-h] [-password PASSWORD] [-catalog] [-trailer] [-metadata] + [-xrefs XREFS] [-pages PAGES] + input + + ------------------------- display PDF information ------------------------- + + positional arguments: + input PDF filename + + optional arguments: + -h, --help show this help message and exit + -password PASSWORD password + -catalog show PDF catalog + -trailer show PDF trailer + -metadata show PDF metadata + -xrefs XREFS show selected objects, format: 1,5-7,N + -pages PAGES show selected pages, format: 1,5-7,50-N + +Examples:: + + python -m fitz show x.pdf + PDF is password protected + + python -m fitz show x.pdf -pass hugo + authentication unsuccessful + + python -m fitz show x.pdf -pass jorjmckie + authenticated as owner + file 'x.pdf', pages: 1, objects: 19, 58 MB, PDF 1.4, encryption: Standard V5 R6 256-bit AES + Document contains 15 embedded files. + + python -m fitz show FDA-1572_508_R6_FINAL.pdf -tr -m + 'FDA-1572_508_R6_FINAL.pdf', pages: 2, objects: 1645, 1.4 MB, PDF 1.6, encryption: Standard V4 R4 128-bit AES + document contains 740 root form fields and is signed + + ------------------------------- PDF metadata ------------------------------ + format: PDF 1.6 + title: FORM FDA 1572 + author: PSC Publishing Services + subject: Statement of Investigator + keywords: None + creator: PScript5.dll Version 5.2.2 + producer: Acrobat Distiller 9.0.0 (Windows) + creationDate: D:20130522104413-04'00' + modDate: D:20190718154905-07'00' + encryption: Standard V4 R4 128-bit AES + + ------------------------------- PDF trailer ------------------------------- + << + /DecodeParms << + /Columns 5 + /Predictor 12 + >> + /Encrypt 1389 0 R + /Filter /FlateDecode + /ID [ <9252E9E39183F2A0B0C51BE557B8A8FC> <85227BE9B84B724E8F678E1529BA8351> ] + /Index [ 1388 258 ] + /Info 1387 0 R + /Length 253 + /Prev 1510559 + /Root 1390 0 R + /Size 1646 + /Type /XRef + /W [ 1 3 1 ] + >> + +Embedded Files Commands +------------------------ + +The following commands deal with embedded files -- which is a feature completely removed from MuPDF after v1.14, and hence from all its command line tools. + +Information +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Show the embedded file names (long or short format):: + + python -m fitz embed-info -h + usage: fitz embed-info [-h] [-name NAME] [-detail] [-password PASSWORD] input + + --------------------------- list embedded files --------------------------- + + positional arguments: + input PDF filename + + optional arguments: + -h, --help show this help message and exit + -name NAME if given, report only this one + -detail show detail information + -password PASSWORD password + +Example:: + + python -m fitz embed-info some.pdf + 'some.pdf' contains the following 15 embedded files. + + 20110813_180956_0002.jpg + 20110813_181009_0003.jpg + 20110813_181012_0004.jpg + 20110813_181131_0005.jpg + 20110813_181144_0006.jpg + 20110813_181306_0007.jpg + 20110813_181307_0008.jpg + 20110813_181314_0009.jpg + 20110813_181315_0010.jpg + 20110813_181324_0011.jpg + 20110813_181339_0012.jpg + 20110813_181913_0013.jpg + insta-20110813_180944_0001.jpg + markiert-20110813_180944_0001.jpg + neue.datei + +Detailed output would look like this per entry:: + + name: neue.datei + filename: text-tester.pdf + ufilename: text-tester.pdf + desc: nur zum Testen! + size: 4639 + length: 1566 + +Extraction +~~~~~~~~~~~~~~~~~~~~~~~~~ + +Extract an embedded file like this:: + + python -m fitz embed-extract -h + usage: fitz embed-extract [-h] -name NAME [-password PASSWORD] [-output OUTPUT] + input + + ---------------------- extract embedded file to disk ---------------------- + + positional arguments: + input PDF filename + + optional arguments: + -h, --help show this help message and exit + -name NAME name of entry + -password PASSWORD password + -output OUTPUT output filename, default is stored name + +For details consult :meth:`Document.embfile_get`. Example (refer to previous section):: + + python -m fitz embed-extract some.pdf -name neue.datei + Saved entry 'neue.datei' as 'text-tester.pdf' + +Deletion +~~~~~~~~~~~~~~~~~~~~~~~~ +Delete an embedded file like this:: + + python -m fitz embed-del -h + usage: fitz embed-del [-h] [-password PASSWORD] [-output OUTPUT] -name NAME input + + --------------------------- delete embedded file -------------------------- + + positional arguments: + input PDF filename + + optional arguments: + -h, --help show this help message and exit + -password PASSWORD password + -output OUTPUT output PDF filename, incremental save if none + -name NAME name of entry to delete + +For details consult :meth:`Document.embfile_del`. + +Insertion +~~~~~~~~~~~~~~~~~~~~~~~~ +Add a new embedded file using this command:: + + python -m fitz embed-add -h + usage: fitz embed-add [-h] [-password PASSWORD] [-output OUTPUT] -name NAME -path + PATH [-desc DESC] + input + + ---------------------------- add embedded file ---------------------------- + + positional arguments: + input PDF filename + + optional arguments: + -h, --help show this help message and exit + -password PASSWORD password + -output OUTPUT output PDF filename, incremental save if none + -name NAME name of new entry + -path PATH path to data for new entry + -desc DESC description of new entry + +*"NAME"* **must not** already exist in the PDF. For details consult :meth:`Document.embfile_add`. + +Updates +~~~~~~~~~~~~~~~~~~~~~~~ +Update an existing embedded file using this command:: + + python -m fitz embed-upd -h + usage: fitz embed-upd [-h] -name NAME [-password PASSWORD] [-output OUTPUT] + [-path PATH] [-filename FILENAME] [-ufilename UFILENAME] + [-desc DESC] + input + + --------------------------- update embedded file -------------------------- + + positional arguments: + input PDF filename + + optional arguments: + -h, --help show this help message and exit + -name NAME name of entry + -password PASSWORD password + -output OUTPUT Output PDF filename, incremental save if none + -path PATH path to new data for entry + -filename FILENAME new filename to store in entry + -ufilename UFILENAME new unicode filename to store in entry + -desc DESC new description to store in entry + + except '-name' all parameters are optional + +Use this method to change meta-information of the file -- just omit the *"PATH"*. For details consult :meth:`Document.embfile_upd`. + + +Copying +~~~~~~~~~~~~~~~~~~~~~~~ +Copy embedded files between PDFs:: + + python -m fitz embed-copy -h + usage: fitz embed-copy [-h] [-password PASSWORD] [-output OUTPUT] -source + SOURCE [-pwdsource PWDSOURCE] + [-name [NAME [NAME ...]]] + input + + --------------------- copy embedded files between PDFs -------------------- + + positional arguments: + input PDF to receive embedded files + + optional arguments: + -h, --help show this help message and exit + -password PASSWORD password of input + -output OUTPUT output PDF, incremental save to 'input' if omitted + -source SOURCE copy embedded files from here + -pwdsource PWDSOURCE password of 'source' PDF + -name [NAME [NAME ...]] + restrict copy to these entries + + +Text Extraction +---------------- +* New in v1.18.16 + +Extract text from arbitrary :ref:`supported documents` to a textfile. Currently, there are three output formatting modes available: simple, block sorting and reproduction of physical layout. + +* **Simple** text extraction reproduces all text as it appears in the document pages -- no effort is made to rearrange in any particular reading order. +* **Block sorting** sorts text blocks (as identified by MuPDF) by ascending vertical, then horizontal coordinates. This should be sufficient to establish a "natural" reading order for basic pages of text. +* **Layout** strives to reproduce the original appearance of the input pages. You can expect results like this (produced by the command `python -m fitz gettext -pages 1 demo1.pdf`): + +.. image:: images/img-layout-text.* + :scale: 60 + +.. note:: The "gettext" command offers a functionality similar to the CLI tool `pdftotext` by XPDF software, http://www.foolabs.com/xpdf/ -- this is especially true for "layout" mode, which combines that tool's `-layout` and `-table` options. + + + +After each page of the output file, a formfeed character, `hex(12)` is written -- even if the input page has no text at all. This behavior can be controlled via options. + +.. note:: For "layout" mode, **only horizontal, left-to-right, top-to bottom** text is supported, other text is ignored. In this mode, text is also ignored, if its fontsize is too small. + + "Simple" and "blocks" mode in contrast output **all text** for any text size or orientation. + +Command:: + + python -m fitz gettext -h + usage: fitz gettext [-h] [-password PASSWORD] [-mode {simple,blocks,layout}] [-pages PAGES] [-noligatures] + [-convert-white] [-extra-spaces] [-noformfeed] [-skip-empty] [-output OUTPUT] [-grid GRID] + [-fontsize FONTSIZE] + input + + ----------------- extract text in various formatting modes ---------------- + + positional arguments: + input input document filename + + optional arguments: + -h, --help show this help message and exit + -password PASSWORD password for input document + -mode {simple,blocks,layout} + mode: simple, block sort, or layout (default) + -pages PAGES select pages, format: 1,5-7,50-N + -noligatures expand ligature characters (default False) + -convert-white convert whitespace characters to space (default False) + -extra-spaces fill gaps with spaces (default False) + -noformfeed write linefeeds, no formfeeds (default False) + -skip-empty suppress pages with no text (default False) + -output OUTPUT store text in this file (default inputfilename.txt) + -grid GRID merge lines if closer than this (default 2) + -fontsize FONTSIZE only include text with a larger fontsize (default 3) + +.. note:: Command options may be abbreviated as long as no ambiguities are introduced. So the following do the same: + + * `... -output text.txt -noligatures -noformfeed -convert-white -grid 3 -extra-spaces ...` + * `... -o text.txt -nol -nof -c -g 3 -e ...` + + The output filename defaults to the input with its extension replaced by `.txt`. As with other commands, you can select page ranges **(caution: 1-based!)** in `mutool` format, as indicated above. + +* **mode:** (str) select a formatting mode -- default is "layout". +* **noligatures:** (bool) corresponds to **not** :data:`TEXT_PRESERVE_LIGATURES`. If specified, ligatures (present in advanced fonts: glyphs combining multiple characters like "fi") are split up into their components (i.e. "f", "i"). Default is passing them through. +* **convert-white:** corresponds to **not** :data:`TEXT_PRESERVE_WHITESPACE`. If specified, all white space characters (like tabs) are replaced with one or more spaces. Default is passing them through. +* **extra-spaces:** (bool) corresponds to **not** :data:`TEXT_INHIBIT_SPACES`. If specified, large gaps between adjacent characters will be filled with one or more spaces. Default is off. +* **noformfeed:** (bool) instead of `hex(12)` (formfeed), write linebreaks `\n` at end of output pages. +* **skip-empty:** (bool) skip pages with no text. +* **grid:** lines with a vertical coordinate difference of no more than this value (in points) will be merged into the same output line. Only relevant for "layout" mode. **Use with care:** 3 or the default 2 should be adequate in most cases. If **too large**, lines that are *intended* to be different in the original may be merged and will result in garbled and / or incomplete output. If **too low**, artifact separate output lines may be generated for some spans in the input line, just because they are coded in a different font with slightly deviating properties. +* **fontsize:** include text with fontsize larger than this value only (default 3). Only relevant for "layout" option. + + +.. highlight:: python + +.. include:: footer.rst diff --git a/docs/outline.rst b/docs/outline.rst new file mode 100644 index 0000000..100dea4 --- /dev/null +++ b/docs/outline.rst @@ -0,0 +1,76 @@ +.. include:: header.rst + +.. _Outline: + +================ +Outline +================ + +*outline* (or "bookmark"), is a property of *Document*. If not *None*, it stands for the first outline item of the document. Its properties in turn define the characteristics of this item and also point to other outline items in "horizontal" or downward direction. The full tree of all outline items for e.g. a conventional table of contents (TOC) can be recovered by following these "pointers". + +============================ ================================================== +**Method / Attribute** **Short Description** +============================ ================================================== +:attr:`Outline.down` next item downwards +:attr:`Outline.next` next item same level +:attr:`Outline.page` page number (0-based) +:attr:`Outline.title` title +:attr:`Outline.uri` string further specifying outline target +:attr:`Outline.is_external` target outside document +:attr:`Outline.is_open` whether sub-outlines are open or collapsed +:attr:`Outline.dest` points to destination details object +============================ ================================================== + +**Class API** + +.. class:: Outline + + .. attribute:: down + + The next outline item on the next level down. Is *None* if the item has no kids. + + :type: :ref:`Outline` + + .. attribute:: next + + The next outline item at the same level as this item. Is *None* if this is the last one in its level. + + :type: `Outline` + + .. attribute:: page + + The page number (0-based) this bookmark points to. + + :type: int + + .. attribute:: title + + The item's title as a string or *None*. + + :type: str + + .. attribute:: is_open + + Indicator showing whether any sub-outlines should be expanded (*True*) or be collapsed (*False*). This information is interpreted by PDF reader software. + + :type: bool + + .. attribute:: is_external + + A bool specifying whether the target is outside (*True*) of the current document. + + :type: bool + + .. attribute:: uri + + A string specifying the link target. The meaning of this property should be evaluated in conjunction with *isExternal*. The value may be *None*, in which case *isExternal == False*. If *uri* starts with *file://*, *mailto:*, or an internet resource name, *isExternal* is *True*. In all other cases *isExternal == False* and *uri* points to an internal location. In case of PDF documents, this should either be *#nnnn* to indicate a 1-based (!) page number *nnnn*, or a named location. The format varies for other document types, e.g. *uri = '../FixedDoc.fdoc#PG_21_LNK_84'* for page number 21 (1-based) in an XPS document. + + :type: str + + .. attribute:: dest + + The link destination details object. + + :type: :ref:`linkDest` + +.. include:: footer.rst diff --git a/docs/page.rst b/docs/page.rst new file mode 100644 index 0000000..e259833 --- /dev/null +++ b/docs/page.rst @@ -0,0 +1,1883 @@ +.. include:: header.rst + +.. _Page: + +================ +Page +================ + +Class representing a document page. A page object is created by :meth:`Document.load_page` or, equivalently, via indexing the document like `doc[n]` - it has no independent constructor. + +There is a parent-child relationship between a document and its pages. If the document is closed or deleted, all page objects (and their respective children, too) in existence will become unusable ("orphaned"): If a page property or method is being used, an exception is raised. + +Several page methods have a :ref:`Document` counterpart for convenience. At the end of this chapter you will find a synopsis. + +Modifying Pages +--------------- +Changing page properties and adding or changing page content is available for PDF documents only. + +In a nutshell, this is what you can do with PyMuPDF: + +* Modify page rotation and the visible part ("cropbox") of the page. +* Insert images, other PDF pages, text and simple geometrical objects. +* Add annotations and form fields. + +.. note:: + + Methods require coordinates (points, rectangles) to put content in desired places. Please be aware that since v1.17.0 these coordinates **must always** be provided relative to the **unrotated** page. The reverse is also true: except :attr:`Page.rect`, resp. :meth:`Page.bound` (both *reflect* when the page is rotated), all coordinates returned by methods and attributes pertain to the unrotated page. + + So the returned value of e.g. :meth:`Page.get_image_bbox` will not change if you do a :meth:`Page.set_rotation`. The same is true for coordinates returned by :meth:`Page.get_text`, annotation rectangles, and so on. If you want to find out, where an object is located in **rotated coordinates**, multiply the coordinates with :attr:`Page.rotation_matrix`. There also is its inverse, :attr:`Page.derotation_matrix`, which you can use when interfacing with other readers, which may behave differently in this respect. + +.. note:: + + If you add or update annotations, links or form fields on the page and immediately afterwards need to work with them (i.e. **without leaving the page**), you should reload the page using :meth:`Document.reload_page` before referring to these new or updated items. + + Reloading the page is generally recommended -- although not strictly required in all cases. However, some annotation and widget types have extended features in PyMuPDF compared to MuPDF. More of these extensions may also be added in the future. + + Releoading the page ensures all your changes have been fully applied to PDF structures, so you can safely create Pixmaps or successfully iterate over annotations, links and form fields. + +================================== ======================================================= +**Method / Attribute** **Short Description** +================================== ======================================================= +:meth:`Page.add_caret_annot` PDF only: add a caret annotation +:meth:`Page.add_circle_annot` PDF only: add a circle annotation +:meth:`Page.add_file_annot` PDF only: add a file attachment annotation +:meth:`Page.add_freetext_annot` PDF only: add a text annotation +:meth:`Page.add_highlight_annot` PDF only: add a "highlight" annotation +:meth:`Page.add_ink_annot` PDF only: add an ink annotation +:meth:`Page.add_line_annot` PDF only: add a line annotation +:meth:`Page.add_polygon_annot` PDF only: add a polygon annotation +:meth:`Page.add_polyline_annot` PDF only: add a multi-line annotation +:meth:`Page.add_rect_annot` PDF only: add a rectangle annotation +:meth:`Page.add_redact_annot` PDF only: add a redaction annotation +:meth:`Page.add_squiggly_annot` PDF only: add a "squiggly" annotation +:meth:`Page.add_stamp_annot` PDF only: add a "rubber stamp" annotation +:meth:`Page.add_strikeout_annot` PDF only: add a "strike-out" annotation +:meth:`Page.add_text_annot` PDF only: add a comment +:meth:`Page.add_underline_annot` PDF only: add an "underline" annotation +:meth:`Page.add_widget` PDF only: add a PDF Form field +:meth:`Page.annot_names` PDF only: a list of annotation (and widget) names +:meth:`Page.annot_xrefs` PDF only: a list of annotation (and widget) xrefs +:meth:`Page.annots` return a generator over the annots on the page +:meth:`Page.apply_redactions` PDF only: process the redactions of the page +:meth:`Page.bound` rectangle of the page +:meth:`Page.delete_annot` PDF only: delete an annotation +:meth:`Page.delete_image` PDF only: delete an image +:meth:`Page.delete_link` PDF only: delete a link +:meth:`Page.delete_widget` PDF only: delete a widget / field +:meth:`Page.draw_bezier` PDF only: draw a cubic Bezier curve +:meth:`Page.draw_circle` PDF only: draw a circle +:meth:`Page.draw_curve` PDF only: draw a special Bezier curve +:meth:`Page.draw_line` PDF only: draw a line +:meth:`Page.draw_oval` PDF only: draw an oval / ellipse +:meth:`Page.draw_polyline` PDF only: connect a point sequence +:meth:`Page.draw_quad` PDF only: draw a quad +:meth:`Page.draw_rect` PDF only: draw a rectangle +:meth:`Page.draw_sector` PDF only: draw a circular sector +:meth:`Page.draw_squiggle` PDF only: draw a squiggly line +:meth:`Page.draw_zigzag` PDF only: draw a zig-zagged line +:meth:`Page.get_drawings` get vector graphics on page +:meth:`Page.get_fonts` PDF only: get list of referenced fonts +:meth:`Page.get_image_bbox` PDF only: get bbox and matrix of embedded image +:meth:`Page.get_image_info` get list of meta information for all used images +:meth:`Page.get_image_rects` PDF only: improved version of :meth:`Page.get_image_bbox` +:meth:`Page.get_images` PDF only: get list of referenced images +:meth:`Page.get_label` PDF only: return the label of the page +:meth:`Page.get_links` get all links +:meth:`Page.get_pixmap` create a page image in raster format +:meth:`Page.get_svg_image` create a page image in SVG format +:meth:`Page.get_text` extract the page's text +:meth:`Page.get_textbox` extract text contained in a rectangle +:meth:`Page.get_textpage_ocr` create a TextPage with OCR for the page +:meth:`Page.get_textpage` create a TextPage for the page +:meth:`Page.get_xobjects` PDF only: get list of referenced xobjects +:meth:`Page.insert_font` PDF only: insert a font for use by the page +:meth:`Page.insert_image` PDF only: insert an image +:meth:`Page.insert_link` PDF only: insert a link +:meth:`Page.insert_text` PDF only: insert text +:meth:`Page.insert_textbox` PDF only: insert a text box +:meth:`Page.links` return a generator of the links on the page +:meth:`Page.load_annot` PDF only: load a specific annotation +:meth:`Page.load_widget` PDF only: load a specific field +:meth:`Page.load_links` return the first link on a page +:meth:`Page.new_shape` PDF only: create a new :ref:`Shape` +:meth:`Page.replace_image` PDF only: replace an image +:meth:`Page.search_for` search for a string +:meth:`Page.set_artbox` PDF only: modify `/ArtBox` +:meth:`Page.set_bleedbox` PDF only: modify `/BleedBox` +:meth:`Page.set_cropbox` PDF only: modify the :data:`cropbox` (visible page) +:meth:`Page.set_mediabox` PDF only: modify `/MediaBox` +:meth:`Page.set_rotation` PDF only: set page rotation +:meth:`Page.set_trimbox` PDF only: modify `/TrimBox` +:meth:`Page.show_pdf_page` PDF only: display PDF page image +:meth:`Page.update_link` PDF only: modify a link +:meth:`Page.widgets` return a generator over the fields on the page +:meth:`Page.write_text` write one or more :ref:`Textwriter` objects +:attr:`Page.cropbox_position` displacement of the :data:`cropbox` +:attr:`Page.cropbox` the page's :data:`cropbox` +:attr:`Page.artbox` the page's `/ArtBox` +:attr:`Page.bleedbox` the page's `/BleedBox` +:attr:`Page.trimbox` the page's `/TrimBox` +:attr:`Page.derotation_matrix` PDF only: get coordinates in unrotated page space +:attr:`Page.first_annot` first :ref:`Annot` on the page +:attr:`Page.first_link` first :ref:`Link` on the page +:attr:`Page.first_widget` first widget (form field) on the page +:attr:`Page.mediabox_size` bottom-right point of :data:`mediabox` +:attr:`Page.mediabox` the page's :data:`mediabox` +:attr:`Page.number` page number +:attr:`Page.parent` owning document object +:attr:`Page.rect` rectangle of the page +:attr:`Page.rotation_matrix` PDF only: get coordinates in rotated page space +:attr:`Page.rotation` PDF only: page rotation +:attr:`Page.transformation_matrix` PDF only: translate between PDF and MuPDF space +:attr:`Page.xref` PDF only: page :data:`xref` +================================== ======================================================= + +**Class API** + +.. class:: Page + + .. method:: bound() + + Determine the rectangle of the page. Same as property :attr:`Page.rect` below. For PDF documents this **usually** also coincides with :data:`mediabox` and :data:`cropbox`, but not always. For example, if the page is rotated, then this is reflected by this method -- the :attr:`Page.cropbox` however will not change. + + :rtype: :ref:`Rect` + + .. method:: add_caret_annot(point) + + * New in v1.16.0 + + PDF only: Add a caret icon. A caret annotation is a visual symbol normally used to indicate the presence of text edits on the page. + + :arg point_like point: the top left point of a 20 x 20 rectangle containing the MuPDF-provided icon. + + :rtype: :ref:`Annot` + :returns: the created annotation. Stroke color blue = (0, 0, 1), no fill color support. + + .. image:: images/img-caret-annot.* + :scale: 70 + + .. method:: add_text_annot(point, text, icon="Note") + + PDF only: Add a comment icon ("sticky note") with accompanying text. Only the icon is visible, the accompanying text is hidden and can be visualized by many PDF viewers by hovering the mouse over the symbol. + + :arg point_like point: the top left point of a 20 x 20 rectangle containing the MuPDF-provided "note" icon. + + :arg str text: the commentary text. This will be shown on double clicking or hovering over the icon. May contain any Latin characters. + :arg str icon: *(new in v1.16.0)* choose one of "Note" (default), "Comment", "Help", "Insert", "Key", "NewParagraph", "Paragraph" as the visual symbol for the embodied text [#f4]_. + + :rtype: :ref:`Annot` + :returns: the created annotation. Stroke color yellow = (1, 1, 0), no fill color support. + + .. index:: + pair: color; add_freetext_annot + pair: fontname; add_freetext_annot + pair: fontsize; add_freetext_annot + pair: rect; add_freetext_annot + pair: rotate; add_freetext_annot + pair: align; add_freetext_annot + pair: text_color; add_freetext_annot + pair: border_color; add_freetext_annot + pair: fill_color; add_freetext_annot + + .. method:: add_freetext_annot(rect, text, fontsize=12, fontname="helv", border_color=None, text_color=0, fill_color=1, rotate=0, align=TEXT_ALIGN_LEFT) + + * Changed in v1.19.6: add border color parameter + + PDF only: Add text in a given rectangle. + + :arg rect_like rect: the rectangle into which the text should be inserted. Text is automatically wrapped to a new line at box width. Lines not fitting into the box will be invisible. + + :arg str text: the text. *(New in v1.17.0)* May contain any mixture of Latin, Greek, Cyrillic, Chinese, Japanese and Korean characters. The respective required font is automatically determined. + :arg float fontsize: the font size. Default is 12. + :arg str fontname: the font name. Default is "Helv". Accepted alternatives are "Cour", "TiRo", "ZaDb" and "Symb". The name may be abbreviated to the first two characters, like "Co" for "Cour". Lower case is also accepted. *(Changed in v1.16.0)* Bold or italic variants of the fonts are **no longer accepted**. A user-contributed script provides a circumvention for this restriction -- see section *Using Buttons and JavaScript* in chapter :ref:`FAQ`. *(New in v1.17.0)* The actual font to use is now determined on a by-character level, and all required fonts (or sub-fonts) are automatically included. Therefore, you should rarely ever need to care about this parameter and let it default (except you insist on a serifed font for your non-CJK text parts). + :arg sequence,float text_color: *(new in v1.16.0)* the text color. Default is black. + + :arg sequence,float fill_color: *(new in v1.16.0)* the fill color. Default is white. + :arg sequence,float text_color: the text color. Default is black. + :arg sequence,float border_color: *(new in v1.19.6)* the border color. Default is `None`. + :arg int align: *(new in v1.17.0)* text alignment, one of TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, TEXT_ALIGN_RIGHT - justify is **not supported**. + + + :arg int rotate: the text orientation. Accepted values are 0, 90, 270, invalid entries are set to zero. + + :rtype: :ref:`Annot` + :returns: the created annotation. Color properties **can only be changed** using special parameters of :meth:`Annot.update`. There, you can also set a border color different from the text color. + + .. method:: add_file_annot(pos, buffer, filename, ufilename=None, desc=None, icon="PushPin") + + PDF only: Add a file attachment annotation with a "PushPin" icon at the specified location. + + :arg point_like pos: the top-left point of a 18x18 rectangle containing the MuPDF-provided "PushPin" icon. + + :arg bytes,bytearray,BytesIO buffer: the data to be stored (actual file content, any data, etc.). + + Changed in v1.14.13 *io.BytesIO* is now also supported. + + :arg str filename: the filename to associate with the data. + :arg str ufilename: the optional PDF unicode version of filename. Defaults to filename. + :arg str desc: an optional description of the file. Defaults to filename. + :arg str icon: *(new in v1.16.0)* choose one of "PushPin" (default), "Graph", "Paperclip", "Tag" as the visual symbol for the attached data [#f4]_. + + :rtype: :ref:`Annot` + :returns: the created annotation. Stroke color yellow = (1, 1, 0), no fill color support. + + .. method:: add_ink_annot(list) + + PDF only: Add a "freehand" scribble annotation. + + :arg sequence list: a list of one or more lists, each containing :data:`point_like` items. Each item in these sublists is interpreted as a :ref:`Point` through which a connecting line is drawn. Separate sublists thus represent separate drawing lines. + + :rtype: :ref:`Annot` + :returns: the created annotation in default appearance black =(0, 0, 0),line width 1. No fill color support. + + .. method:: add_line_annot(p1, p2) + + PDF only: Add a line annotation. + + :arg point_like p1: the starting point of the line. + + :arg point_like p2: the end point of the line. + + :rtype: :ref:`Annot` + :returns: the created annotation. It is drawn with line (stroke) color red = (1, 0, 0) and line width 1. No fill color support. The **annot rectangle** is automatically created to contain both points, each one surrounded by a circle of radius 3 * line width to make room for any line end symbols. + + .. method:: add_rect_annot(rect) + + .. method:: add_circle_annot(rect) + + PDF only: Add a rectangle, resp. circle annotation. + + :arg rect_like rect: the rectangle in which the circle or rectangle is drawn, must be finite and not empty. If the rectangle is not equal-sided, an ellipse is drawn. + + :rtype: :ref:`Annot` + :returns: the created annotation. It is drawn with line (stroke) color red = (1, 0, 0), line width 1, fill color is supported. + + .. method:: add_redact_annot(quad, text=None, fontname=None, fontsize=11, align=TEXT_ALIGN_LEFT, fill=(1, 1, 1), text_color=(0, 0, 0), cross_out=True) + + * New in v1.16.11 + + PDF only: Add a redaction annotation. A redaction annotation identifies content to be removed from the document. Adding such an annotation is the first of two steps. It makes visible what will be removed in the subsequent step, :meth:`Page.apply_redactions`. + + :arg quad_like,rect_like quad: specifies the (rectangular) area to be removed which is always equal to the annotation rectangle. This may be a :data:`rect_like` or :data:`quad_like` object. If a quad is specified, then the enveloping rectangle is taken. + + :arg str text: *(New in v1.16.12)* text to be placed in the rectangle after applying the redaction (and thus removing old content). + + :arg str fontname: *(New in v1.16.12)* the font to use when *text* is given, otherwise ignored. The same rules apply as for :meth:`Page.insert_textbox` -- which is the method :meth:`Page.apply_redactions` internally invokes. The replacement text will be **vertically centered**, if this is one of the CJK or :ref:`Base-14-Fonts`. + + .. note:: + + * For an **existing** font of the page, use its reference name as *fontname* (this is *item[4]* of its entry in :meth:`Page.get_fonts`). + * For a **new, non-builtin** font, proceed as follows:: + + page.insert_text(point, # anywhere, but outside all redaction rectangles + "something", # some non-empty string + fontname="newname", # new, unused reference name + fontfile="...", # desired font file + render_mode=3, # makes the text invisible + ) + page.add_redact_annot(..., fontname="newname") + + :arg float fontsize: *(New in v1.16.12)* the fontsize to use for the replacing text. If the text is too large to fit, several insertion attempts will be made, gradually reducing the fontsize to no less than 4. If then the text will still not fit, no text insertion will take place at all. + + :arg int align: *(New in v1.16.12)* the horizontal alignment for the replacing text. See :meth:`insert_textbox` for available values. The vertical alignment is (approximately) centered if a PDF built-in font is used (CJK or :ref:`Base-14-Fonts`). + + :arg sequence fill: *(New in v1.16.12)* the fill color of the rectangle **after applying** the redaction. The default is *white = (1, 1, 1)*, which is also taken if *None* is specified. *(Changed in v1.16.13)* To suppress a fill color altogether, specify *False*. In this cases the rectangle remains transparent. + + :arg sequence text_color: *(New in v1.16.12)* the color of the replacing text. Default is *black = (0, 0, 0)*. + + :arg bool cross_out: *(new in v1.17.2)* add two diagonal lines to the annotation rectangle. + + :rtype: :ref:`Annot` + :returns: the created annotation. *(Changed in v1.17.2)* Its standard appearance looks like a red rectangle (no fill color), optionally showing two diagonal lines. Colors, line width, dashing, opacity and blend mode can now be set and applied via :meth:`Annot.update` like with other annotations. + + .. image:: images/img-redact.* + + .. method:: add_polyline_annot(points) + + .. method:: add_polygon_annot(points) + + PDF only: Add an annotation consisting of lines which connect the given points. A **Polygon's** first and last points are automatically connected, which does not happen for a **PolyLine**. The **rectangle** is automatically created as the smallest rectangle containing the points, each one surrounded by a circle of radius 3 (= 3 * line width). The following shows a 'PolyLine' that has been modified with colors and line ends. + + :arg list points: a list of :data:`point_like` objects. + + :rtype: :ref:`Annot` + :returns: the created annotation. It is drawn with line color black, line width 1 no fill color but fill color support. Use methods of :ref:`Annot` to make any changes to achieve something like this: + + .. image:: images/img-polyline.* + :scale: 70 + + .. method:: add_underline_annot(quads=None, start=None, stop=None, clip=None) + + .. method:: add_strikeout_annot(quads=None, start=None, stop=None, clip=None) + + .. method:: add_squiggly_annot(quads=None, start=None, stop=None, clip=None) + + .. method:: add_highlight_annot(quads=None, start=None, stop=None, clip=None) + + PDF only: These annotations are normally used for **marking text** which has previously been somehow located (for example via :meth:`Page.search_for`). But this is not required: you are free to "mark" just anything. + + Standard (stroke only -- no fill color support) colors are chosen per annotation type: **yellow** for highlighting, **red** for striking out, **green** for underlining, and **magenta** for wavy underlining. + + All these four methods convert the arguments into a list of :ref:`Quad` objects. The **annotation** rectangle is then calculated to envelop all these quadrilaterals. + + .. note:: + + :meth:`search_for` delivers a list of either :ref:`Rect` or :ref:`Quad` objects. Such a list can be directly used as an argument for these annotation types and will deliver **one common annotation** for all occurrences of the search string:: + + >>> # prefer quads=True in text searching for annotations! + >>> quads = page.search_for("pymupdf", quads=True) + >>> page.add_highlight_annot(quads) + + .. note:: + Obviously, text marker annotations need to know what is the top, the bottom, the left, and the right side of the area(s) to be marked. If the arguments are quads, this information is given by the sequence of the quad points. In contrast, a rectangle delivers much less information -- this is illustrated by the fact, that 4! = 24 different quads can be constructed with the four corners of a reactangle. + + Therefore, we **strongly recommend** to use the `quads` option for text searches, to ensure correct annotations. A similar consideration applies to marking **text spans** extracted with the "dict" / "rawdict" options of :meth:`Page.get_text`. For more details on how to compute quadrilaterals in this case, see section "How to Mark Non-horizontal Text" of :ref:`FAQ`. + + :arg rect_like,quad_like,list,tuple quads: *(Changed in v1.14.20)* the location(s) -- rectangle(s) or quad(s) -- to be marked. A list or tuple must consist of :data:`rect_like` or :data:`quad_like` items (or even a mixture of either). Every item must be finite, convex and not empty (as applicable). *(Changed in v1.16.14)* **Set this parameter to** *None* if you want to use the following arguments. And vice versa: if not *None*, the remaining parameters must be *None*. + :arg point_like start: *(New in v1.16.14)* start text marking at this point. Defaults to the top-left point of *clip*. Must be provided if `quads` is *None*. + :arg point_like stop: *(New in v1.16.14)* stop text marking at this point. Defaults to the bottom-right point of *clip*. Must be used if `quads` is *None*. + :arg rect_like clip: *(New in v1.16.14)* only consider text lines intersecting this area. Defaults to the page rectangle. Only use if `start` and `stop` are provided. + + :rtype: :ref:`Annot` or *(changed in v1.16.14)* *None* + :returns: the created annotation. *(Changed in v1.16.14)* If *quads* is an empty list, **no annotation** is created. + + .. note:: Starting with v1.16.14 you can use parameters *start*, *stop* and *clip* to highlight consecutive lines between the points *start* and *stop*. Make use of *clip* to further reduce the selected line bboxes and thus deal with e.g. multi-column pages. The following multi-line highlight on a page with three text columns was created by specifying the two red points and setting clip accordingly. + + .. image:: images/img-markers.* + :scale: 100 + + .. method:: add_stamp_annot(rect, stamp=0) + + PDF only: Add a "rubber stamp" like annotation to e.g. indicate the document's intended use ("DRAFT", "CONFIDENTIAL", etc.). + + :arg rect_like rect: rectangle where to place the annotation. + + :arg int stamp: id number of the stamp text. For available stamps see :ref:`StampIcons`. + + .. note:: + + * The stamp's text and its border line will automatically be sized and be put horizontally and vertically centered in the given rectangle. :attr:`Annot.rect` is automatically calculated to fit the given **width** and will usually be smaller than this parameter. + * The font chosen is "Times Bold" and the text will be upper case. + * The appearance can be changed using :meth:`Annot.set_opacity` and by setting the "stroke" color (no "fill" color supported). + * This can be used to create watermark images: on a temporary PDF page create a stamp annotation with a low opacity value, make a pixmap from it with *alpha=True* (and potentially also rotate it), discard the temporary PDF page and use the pixmap with :meth:`insert_image` for your target PDF. + + + .. image :: images/img-stampannot.* + :scale: 80 + + .. method:: add_widget(widget) + + PDF only: Add a PDF Form field ("widget") to a page. This also **turns the PDF into a Form PDF**. Because of the large amount of different options available for widgets, we have developed a new class :ref:`Widget`, which contains the possible PDF field attributes. It must be used for both, form field creation and updates. + + :arg widget: a :ref:`Widget` object which must have been created upfront. + :type widget: :ref:`Widget` + + :returns: a widget annotation. + + .. method:: delete_annot(annot) + + * Changed in v1.16.6: The removal will now include any bound 'Popup' or response annotations and related objects. + + PDF only: Delete annotation from the page and return the next one. + + :arg annot: the annotation to be deleted. + :type annot: :ref:`Annot` + + :rtype: :ref:`Annot` + :returns: the annotation following the deleted one. Please remember that physical removal requires saving to a new file with garbage > 0. + + .. method:: delete_widget(widget) + + * New in v1.18.4 + + PDF only: Delete field from the page and return the next one. + + :arg widget: the widget to be deleted. + :type widget: :ref:`Widget` + + :rtype: :ref:`Widget` + :returns: the widget following the deleted one. Please remember that physical removal requires saving to a new file with garbage > 0. + + .. method:: apply_redactions(images=PDF_REDACT_IMAGE_PIXELS) + + * New in v1.16.11 + * Changed in v1.16.12: The previous *mark* parameter is gone. Instead, the respective rectangles are filled with the individual *fill* color of each redaction annotation. If a *text* was given in the annotation, then :meth:`insert_textbox` is invoked to insert it, using parameters provided with the redaction. + * Changed in v1.18.0: added option for handling images that overlap redaction areas. + + PDF only: Remove all **text content** contained in any redaction rectangle. + + **This method applies and then deletes all redactions from the page.** + + :arg int images: How to redact overlapping images. The default (2) blanks out overlapping pixels. *PDF_REDACT_IMAGE_NONE* (0) ignores, and *PDF_REDACT_IMAGE_REMOVE* (1) completely removes all overlapping images. + + + :returns: *True* if at least one redaction annotation has been processed, *False* otherwise. + + .. note:: + * Text contained in a redaction rectangle will be **physically** removed from the page (assuming :meth:`Document.save` with a suitable garbage option) and will no longer appear in e.g. text extractions or anywhere else. All redaction annotations will also be removed. Other annotations are unaffected. + + * All overlapping links will be removed. If the rectangle of the link was covering text, then only the overlapping part of the text is being removed. Similar applies to images covered by link rectangles. + + * *(Changed in v1.18.0)* The overlapping parts of **images** will be blanked-out for default option `PDF_REDACT_IMAGE_PIXELS`. Option 0 does not touch any images and 1 will remove any image with an overlap. Please be aware that there is a bug for option *PDF_REDACT_IMAGE_PIXELS = 2*: transparent images will be incorrectly handled! + + * For option `images=PDF_REDACT_IMAGE_REMOVE` only this page's **references to the images** are removed - not necessarily the images themselves. Images are completely removed from the file only, if no longer referenced at all (assuming suitable garbage collection options). + + * For option `images=PDF_REDACT_IMAGE_PIXELS` a new image of format PNG is created, which the page will use in place of the original one. The original image is not deleted or replaced as part of this process, so other pages may still show the original. In addition, the new, modified PNG image currently is **stored uncompressed**. Do keep these aspects in mind when choosing the right garbage collection method and compression options during save. + + * **Text removal** is done by character: A character is removed if its bbox has a **non-empty overlap** with a redaction rectangle *(changed in MuPDF v1.17)*. Depending on the font properties and / or the chosen line height, deletion may occur for undesired text parts. Using :meth:`Tools.set_small_glyph_heights` with a *True* argument before text search may help to prevent this. + + * Redactions are a simple way to replace single words in a PDF, or to just physically remove them. Locate the word "secret" using some text extraction or search method and insert a redaction using "xxxxxx" as replacement text for each occurrence. + + - Be wary if the replacement is longer than the original -- this may lead to an awkward appearance, line breaks or no new text at all. + + - For a number of reasons, the new text may not exactly be positioned on the same line like the old one -- especially true if the replacement font was not one of CJK or :ref:`Base-14-Fonts`. + + .. method:: delete_link(linkdict) + + PDF only: Delete the specified link from the page. The parameter must be an **original item** of :meth:`get_links()` (see below). The reason for this is the dictionary's *"xref"* key, which identifies the PDF object to be deleted. + + :arg dict linkdict: the link to be deleted. + + .. method:: insert_link(linkdict) + + PDF only: Insert a new link on this page. The parameter must be a dictionary of format as provided by :meth:`get_links()` (see below). + + :arg dict linkdict: the link to be inserted. + + .. method:: update_link(linkdict) + + PDF only: Modify the specified link. The parameter must be a (modified) **original item** of :meth:`get_links()` (see below). The reason for this is the dictionary's *"xref"* key, which identifies the PDF object to be changed. + + :arg dict linkdict: the link to be modified. + + .. warning:: If updating / inserting a URI link (`"kind": LINK_URI`), please make sure to start the value for the `"uri"` key with a disambiguating string like `"http://"`, `"https://"`, `"file://"`, `"ftp://"`, `"mailto:"`, etc. Otherwise -- depending on your browser or other "consumer" software -- unexpected default assumptions may lead to unwanted behaviours. + + + .. method:: get_label() + + * New in v1.18.6 + + PDF only: Return the label for the page. + + :rtype: str + + :returns: the label string like "vii" for Roman numbering or "" if not defined. + + + + .. method:: get_links() + + Retrieves **all** links of a page. + + :rtype: list + :returns: A list of dictionaries. For a description of the dictionary entries see below. Always use this or the :meth:`Page.links` method if you intend to make changes to the links of a page. + + .. method:: links(kinds=None) + + * New in v1.16.4 + + Return a generator over the page's links. The results equal the entries of :meth:`Page.get_links`. + + :arg sequence kinds: a sequence of integers to down-select to one or more link kinds. Default is all links. Example: *kinds=(fitz.LINK_GOTO,)* will only return internal links. + + :rtype: generator + :returns: an entry of :meth:`Page.get_links()` for each iteration. + + .. method:: annots(types=None) + + * New in v1.16.4 + + Return a generator over the page's annotations. + + :arg sequence types: a sequence of integers to down-select to one or more annotation types. Default is all annotations. Example: *types=(fitz.PDF_ANNOT_FREETEXT, fitz.PDF_ANNOT_TEXT)* will only return 'FreeText' and 'Text' annotations. + + :rtype: generator + :returns: an :ref:`Annot` for each iteration. + + .. caution:: + You **cannot safely update annotations** from within this generator. This is because most annotation updates require reloading the page via `page = doc.reload_page(page)`. To circumvent this restriction, make a list of annotations xref numbers first and then iterate over these numbers:: + + In [4]: xrefs = [annot.xref for annot in page.annots(types=[...])] + In [5]: for xref in xrefs: + ...: annot = page.load_annot(xref) + ...: annot.update() + ...: page = doc.reload_page(page) + In [6]: + + .. method:: widgets(types=None) + + * New in v1.16.4 + + Return a generator over the page's form fields. + + :arg sequence types: a sequence of integers to down-select to one or more widget types. Default is all form fields. Example: `types=(fitz.PDF_WIDGET_TYPE_TEXT,)` will only return 'Text' fields. + + :rtype: generator + :returns: a :ref:`Widget` for each iteration. + + + .. method:: write_text(rect=None, writers=None, overlay=True, color=None, opacity=None, keep_proportion=True, rotate=0, oc=0) + + * New in v1.16.18 + + PDF only: Write the text of one or more :ref:`Textwriter` objects to the page. + + :arg rect_like rect: where to place the text. If omitted, the rectangle union of the text writers is used. + :arg sequence writers: a non-empty tuple / list of :ref:`TextWriter` objects or a single :ref:`TextWriter`. + :arg float opacity: set transparency, overwrites resp. value in the text writers. + :arg sequ color: set the text color, overwrites resp. value in the text writers. + :arg bool overlay: put the text in foreground or background. + :arg bool keep_proportion: maintain the aspect ratio. + :arg float rotate: rotate the text by an arbitrary angle. + :arg int oc: *(new in v1.18.4)* the :data:`xref` of an :data:`OCG` or :data:`OCMD`. + + .. note:: Parameters *overlay, keep_proportion, rotate* and *oc* have the same meaning as in :meth:`Page.show_pdf_page`. + + + .. index:: + pair: border_width; insert_text + pair: color; insert_text + pair: encoding; insert_text + pair: fill; insert_text + pair: fontfile; insert_text + pair: fontname; insert_text + pair: fontsize; insert_text + pair: morph; insert_text + pair: overlay; insert_text + pair: render_mode; insert_text + pair: rotate; insert_text + pair: stroke_opacity; insert_text + pair: fill_opacity; insert_text + pair: oc; insert_text + + .. method:: insert_text(point, text, fontsize=11, fontname="helv", fontfile=None, idx=0, color=None, fill=None, render_mode=0, border_width=1, encoding=TEXT_ENCODING_LATIN, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, overlay=True, oc=0) + + * Changed in v1.18.4 + + PDF only: Insert text starting at :data:`point_like` *point*. See :meth:`Shape.insert_text`. + + .. index:: + pair: align; insert_textbox + pair: border_width; insert_textbox + pair: color; insert_textbox + pair: encoding; insert_textbox + pair: expandtabs; insert_textbox + pair: fill; insert_textbox + pair: fontfile; insert_textbox + pair: fontname; insert_textbox + pair: fontsize; insert_textbox + pair: morph; insert_textbox + pair: overlay; insert_textbox + pair: render_mode; insert_textbox + pair: rotate; insert_textbox + pair: stroke_opacity; insert_textbox + pair: fill_opacity; insert_textbox + pair: oc; insert_textbox + + .. method:: insert_textbox(rect, buffer, fontsize=11, fontname="helv", fontfile=None, idx=0, color=None, fill=None, render_mode=0, border_width=1, encoding=TEXT_ENCODING_LATIN, expandtabs=8, align=TEXT_ALIGN_LEFT, charwidths=None, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, oc=0, overlay=True) + + * Changed in v1.18.4 + + PDF only: Insert text into the specified :data:`rect_like` *rect*. See :meth:`Shape.insert_textbox`. + + .. index:: + pair: closePath; draw_line + pair: color; draw_line + pair: dashes; draw_line + pair: fill; draw_line + pair: lineCap; draw_line + pair: lineJoin; draw_line + pair: lineJoin; draw_line + pair: morph; draw_line + pair: overlay; draw_line + pair: width; draw_line + pair: stroke_opacity; draw_line + pair: fill_opacity; draw_line + pair: oc; draw_line + + .. method:: draw_line(p1, p2, color=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: Draw a line from *p1* to *p2* (:data:`point_like` \s). See :meth:`Shape.draw_line`. + + .. index:: + pair: breadth; draw_zigzag + pair: closePath; draw_zigzag + pair: color; draw_zigzag + pair: dashes; draw_zigzag + pair: fill; draw_zigzag + pair: lineCap; draw_zigzag + pair: lineJoin; draw_zigzag + pair: morph; draw_zigzag + pair: overlay; draw_zigzag + pair: width; draw_zigzag + pair: stroke_opacity; draw_zigzag + pair: fill_opacity; draw_zigzag + pair: oc; draw_zigzag + + .. method:: draw_zigzag(p1, p2, breadth=2, color=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: Draw a zigzag line from *p1* to *p2* (:data:`point_like` \s). See :meth:`Shape.draw_zigzag`. + + .. index:: + pair: breadth; draw_squiggle + pair: closePath; draw_squiggle + pair: color; draw_squiggle + pair: dashes; draw_squiggle + pair: fill; draw_squiggle + pair: lineCap; draw_squiggle + pair: lineJoin; draw_squiggle + pair: morph; draw_squiggle + pair: overlay; draw_squiggle + pair: width; draw_squiggle + pair: stroke_opacity; draw_squiggle + pair: fill_opacity; draw_squiggle + pair: oc; draw_squiggle + + .. method:: draw_squiggle(p1, p2, breadth=2, color=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: Draw a squiggly (wavy, undulated) line from *p1* to *p2* (:data:`point_like` \s). See :meth:`Shape.draw_squiggle`. + + .. index:: + pair: closePath; draw_circle + pair: color; draw_circle + pair: dashes; draw_circle + pair: fill; draw_circle + pair: lineCap; draw_circle + pair: lineJoin; draw_circle + pair: morph; draw_circle + pair: overlay; draw_circle + pair: width; draw_circle + pair: stroke_opacity; draw_circle + pair: fill_opacity; draw_circle + pair: oc; draw_circle + + .. method:: draw_circle(center, radius, color=None, fill=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: Draw a circle around *center* (:data:`point_like`) with a radius of *radius*. See :meth:`Shape.draw_circle`. + + .. index:: + pair: closePath; draw_oval + pair: color; draw_oval + pair: dashes; draw_oval + pair: fill; draw_oval + pair: lineCap; draw_oval + pair: lineJoin; draw_oval + pair: morph; draw_oval + pair: overlay; draw_oval + pair: width; draw_oval + pair: stroke_opacity; draw_oval + pair: fill_opacity; draw_oval + pair: oc; draw_oval + + .. method:: draw_oval(quad, color=None, fill=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: Draw an oval (ellipse) within the given :data:`rect_like` or :data:`quad_like`. See :meth:`Shape.draw_oval`. + + .. index:: + pair: closePath; draw_sector + pair: color; draw_sector + pair: dashes; draw_sector + pair: fill; draw_sector + pair: fullSector; draw_sector + pair: lineCap; draw_sector + pair: lineJoin; draw_sector + pair: morph; draw_sector + pair: overlay; draw_sector + pair: width; draw_sector + pair: stroke_opacity; draw_sector + pair: fill_opacity; draw_sector + pair: oc; draw_sector + + .. method:: draw_sector(center, point, angle, color=None, fill=None, width=1, dashes=None, lineCap=0, lineJoin=0, fullSector=True, overlay=True, closePath=False, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: Draw a circular sector, optionally connecting the arc to the circle's center (like a piece of pie). See :meth:`Shape.draw_sector`. + + .. index:: + pair: closePath; draw_polyline + pair: color; draw_polyline + pair: dashes; draw_polyline + pair: fill; draw_polyline + pair: lineCap; draw_polyline + pair: lineJoin; draw_polyline + pair: morph; draw_polyline + pair: overlay; draw_polyline + pair: width; draw_polyline + pair: stroke_opacity; draw_polyline + pair: fill_opacity; draw_polyline + pair: oc; draw_polyline + + .. method:: draw_polyline(points, color=None, fill=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, closePath=False, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: Draw several connected lines defined by a sequence of :data:`point_like` \s. See :meth:`Shape.draw_polyline`. + + + .. index:: + pair: closePath; draw_bezier + pair: color; draw_bezier + pair: dashes; draw_bezier + pair: fill; draw_bezier + pair: lineCap; draw_bezier + pair: lineJoin; draw_bezier + pair: morph; draw_bezier + pair: overlay; draw_bezier + pair: width; draw_bezier + pair: stroke_opacity; draw_bezier + pair: fill_opacity; draw_bezier + pair: oc; draw_bezier + + .. method:: draw_bezier(p1, p2, p3, p4, color=None, fill=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, closePath=False, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: Draw a cubic Bézier curve from *p1* to *p4* with the control points *p2* and *p3* (all are :data:`point_like` \s). See :meth:`Shape.draw_bezier`. + + .. index:: + pair: closePath; draw_curve + pair: color; draw_curve + pair: dashes; draw_curve + pair: fill; draw_curve + pair: lineCap; draw_curve + pair: lineJoin; draw_curve + pair: morph; draw_curve + pair: overlay; draw_curve + pair: width; draw_curve + pair: stroke_opacity; draw_curve + pair: fill_opacity; draw_curve + pair: oc; draw_curve + + .. method:: draw_curve(p1, p2, p3, color=None, fill=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, closePath=False, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: This is a special case of *draw_bezier()*. See :meth:`Shape.draw_curve`. + + .. index:: + pair: closePath; draw_rect + pair: color; draw_rect + pair: dashes; draw_rect + pair: fill; draw_rect + pair: lineCap; draw_rect + pair: lineJoin; draw_rect + pair: morph; draw_rect + pair: overlay; draw_rect + pair: width; draw_rect + pair: stroke_opacity; draw_rect + pair: fill_opacity; draw_rect + pair: radius; draw_rect + pair: oc; draw_rect + + .. method:: draw_rect(rect, color=None, fill=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, morph=None, stroke_opacity=1, fill_opacity=1, radius=None, oc=0) + + * Changed in v1.18.4 + * Changed in v1.22.0: Added parameter *radius*. + + PDF only: Draw a rectangle. See :meth:`Shape.draw_rect`. + + .. index:: + pair: closePath; draw_quad + pair: color; draw_quad + pair: dashes; draw_quad + pair: fill; draw_quad + pair: lineCap; draw_quad + pair: lineJoin; draw_quad + pair: morph; draw_quad + pair: overlay; draw_quad + pair: width; draw_quad + pair: stroke_opacity; draw_quad + pair: fill_opacity; draw_quad + pair: oc; draw_quad + + .. method:: draw_quad(quad, color=None, fill=None, width=1, dashes=None, lineCap=0, lineJoin=0, overlay=True, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + * Changed in v1.18.4 + + PDF only: Draw a quadrilateral. See :meth:`Shape.draw_quad`. + + + .. index:: + pair: encoding; insert_font + pair: fontbuffer; insert_font + pair: fontfile; insert_font + pair: fontname; insert_font + pair: set_simple; insert_font + + .. method:: insert_font(fontname="helv", fontfile=None, fontbuffer=None, set_simple=False, encoding=TEXT_ENCODING_LATIN) + + PDF only: Add a new font to be used by text output methods and return its :data:`xref`. If not already present in the file, the font definition will be added. Supported are the built-in :data:`Base14_Fonts` and the CJK fonts via **"reserved"** fontnames. Fonts can also be provided as a file path or a memory area containing the image of a font file. + + :arg str fontname: The name by which this font shall be referenced when outputting text on this page. In general, you have a "free" choice here (but consult the :ref:`AdobeManual`, page 16, section 7.3.5 for a formal description of building legal PDF names). However, if it matches one of the :data:`Base14_Fonts` or one of the CJK fonts, *fontfile* and *fontbuffer* **are ignored**. + + In other words, you cannot insert a font via *fontfile* / *fontbuffer* and also give it a reserved *fontname*. + + .. note:: A reserved fontname can be specified in any mixture of upper or lower case and still match the right built-in font definition: fontnames "helv", "Helv", "HELV", "Helvetica", etc. all lead to the same font definition "Helvetica". But from a :ref:`Page` perspective, these are **different references**. You can exploit this fact when using different *encoding* variants (Latin, Greek, Cyrillic) of the same font on a page. + + :arg str fontfile: a path to a font file. If used, *fontname* must be **different from all reserved names**. + + :arg bytes/bytearray fontbuffer: the memory image of a font file. If used, *fontname* must be **different from all reserved names**. This parameter would typically be used with :attr:`Font.buffer` for fonts supported / available via :ref:`Font`. + + :arg int set_simple: applicable for *fontfile* / *fontbuffer* cases only: enforce treatment as a "simple" font, i.e. one that only uses character codes up to 255. + + :arg int encoding: applicable for the "Helvetica", "Courier" and "Times" sets of :data:`Base14_Fonts` only. Select one of the available encodings Latin (0), Cyrillic (2) or Greek (1). Only use the default (0 = Latin) for "Symbol" and "ZapfDingBats". + + :rytpe: int + :returns: the :data:`xref` of the installed font. + + .. note:: Built-in fonts will not lead to the inclusion of a font file. So the resulting PDF file will remain small. However, your PDF viewer software is responsible for generating an appropriate appearance -- and there **exist** differences on whether or how each one of them does this. This is especially true for the CJK fonts. But also Symbol and ZapfDingbats are incorrectly handled in some cases. Following are the **Font Names** and their correspondingly installed **Base Font** names: + + **Base-14 Fonts** [#f1]_ + + ============= ============================ ========================================= + **Font Name** **Installed Base Font** **Comments** + ============= ============================ ========================================= + helv Helvetica normal + heit Helvetica-Oblique italic + hebo Helvetica-Bold bold + hebi Helvetica-BoldOblique bold-italic + cour Courier normal + coit Courier-Oblique italic + cobo Courier-Bold bold + cobi Courier-BoldOblique bold-italic + tiro Times-Roman normal + tiit Times-Italic italic + tibo Times-Bold bold + tibi Times-BoldItalic bold-italic + symb Symbol [#f3]_ + zadb ZapfDingbats [#f3]_ + ============= ============================ ========================================= + + **CJK Fonts** [#f2]_ (China, Japan, Korea) + + ============= ============================ ========================================= + **Font Name** **Installed Base Font** **Comments** + ============= ============================ ========================================= + china-s Heiti simplified Chinese + china-ss Song simplified Chinese (serif) + china-t Fangti traditional Chinese + china-ts Ming traditional Chinese (serif) + japan Gothic Japanese + japan-s Mincho Japanese (serif) + korea Dotum Korean + korea-s Batang Korean (serif) + ============= ============================ ========================================= + + .. index:: + pair: filename; insert_image + pair: keep_proportion; insert_image + pair: overlay; insert_image + pair: pixmap; insert_image + pair: rotate; insert_image + pair: stream; insert_image + pair: mask; insert_image + pair: oc; insert_image + pair: xref; insert_image + + .. method:: insert_image(rect, filename=None, pixmap=None, stream=None, mask=None, rotate=0, alpha=-1, oc=0, xref=0, keep_proportion=True, overlay=True) + + PDF only: Put an image inside the given rectangle. The image may already exist in the PDF or be taken from a pixmap, a file, or a memory area. + + * Changed in v1.14.1: By default, the image keeps its aspect ratio. + * Changed in v1.14.13: The image is now always placed **centered** in the rectangle, i.e. the centers of image and rectangle are equal. + * Changed in v1.17.6: Insertion rectangle no longer needs to have a non-empty intersection with the page's :attr:`Page.cropbox` [#f5]_. + * Changed in v1.18.13: Allow providing the image as the xref of an existing one. + + :arg rect_like rect: where to put the image. Must be finite and not empty. + :arg str filename: name of an image file (all formats supported by MuPDF -- see :ref:`ImageFiles`). + :arg bytes,bytearray,io.BytesIO stream: image in memory (all formats supported by MuPDF -- see :ref:`ImageFiles`). + + Changed in v1.14.13: *io.BytesIO* is now also supported. + + :arg pixmap: a pixmap containing the image. + :type pixmap: :ref:`Pixmap` + + :arg bytes,bytearray,io.BytesIO mask: *(new in version v1.18.1)* image in memory -- to be used as image mask (alpha values) for the base image. When specified, the base image must be provided as a filename or a stream -- and must not be an image that already has a mask. + + :arg int xref: *(New in v1.18.13)* the :data:`xref` of an image already present in the PDF. If given, parameters `filename`, `pixmap`, `stream`, `alpha` and `mask` are ignored. The page will simply receive a reference to the existing image. + + :arg int alpha: *(Changed in v1.19.3)* deprecated. No longer needed -- ignored when given. + + :arg int rotate: *(new in version v1.14.11)* rotate the image. + Must be an integer multiple of 90 degrees. + Positive values rotate anti-clockwise. + If you need a rotation by an arbitrary angle, + consider converting the image to a PDF + (:meth:`Document.convert_to_pdf`) + first and then use :meth:`Page.show_pdf_page` instead. + + :arg int oc: *(new in v1.18.3)* (:data:`xref`) make image visibility dependent on this :data:`OCG` or :data:`OCMD`. Ignored after the first of multiple insertions. The property is stored with the generated PDF image object and therefore controls the image's visibility throughout the PDF. + :arg bool keep_proportion: *(new in version v1.14.11)* maintain the aspect ratio of the image. + + For a description of *overlay* see :ref:`CommonParms`. + + *Changed in v1.18.13:* Return xref of stored image. + + :rtype: int + :returns: The xref of the embedded image. This can be used as the `xref` argument for very significant performance boosts, if the image is inserted again. + + This example puts the same image on every page of a document:: + + >>> doc = fitz.open(...) + >>> rect = fitz.Rect(0, 0, 50, 50) # put thumbnail in upper left corner + >>> img = open("some.jpg", "rb").read() # an image file + >>> img_xref = 0 # first execution embeds the image + >>> for page in doc: + img_xref = page.insert_image(rect, stream=img, + xref=img_xref, 2nd time reuses existing image + ) + >>> doc.save(...) + + .. note:: + + 1. The method detects multiple insertions of the same image (like in above example) and will store its data only on the first execution. This is even true (although less performant), if using the default `xref=0`. + + 2. The method cannot detect if the same image had already been part of the file before opening it. + + 3. You can use this method to provide a background or foreground image for the page, like a copyright or a watermark. Please remember, that watermarks require a transparent image if put in foreground ... + + 4. The image may be inserted uncompressed, e.g. if a *Pixmap* is used or if the image has an alpha channel. Therefore, consider using *deflate=True* when saving the file. In addition, there exist effective ways to control the image size -- even if transparency comes into play. Have a look at `this `_ section of the documentation. + + 5. The image is stored in the PDF in its original quality. This may be much better than what you ever need for your display. Consider **decreasing the image size** before insertion -- e.g. by using the pixmap option and then shrinking it or scaling it down (see :ref:`Pixmap` chapter). The PIL method *Image.thumbnail()* can also be used for that purpose. The file size savings can be very significant. + + 6. Another efficient way to display the same image on multiple pages is another method: :meth:`show_pdf_page`. Consult :meth:`Document.convert_to_pdf` for how to obtain intermediary PDFs usable for that method. Demo script `fitz-logo.py `_ implements a fairly complete approach. + + + .. index:: + pair: filename; replace_image + pair: pixmap; replace_image + pair: stream; replace_image + pair: xref; replace_image + + .. method:: replace_image(xref, filename=None, pixmap=None, stream=None) + + * New in v1.21.0 + + Replace the image at xref with another one. + + :arg int xref: the :data:`xref` of the image. + :arg filename: the filename of the new image. + :arg pixmap: the :ref:`Pixmap` of the new image. + :arg stream: the memory area containing the new image. + + Arguments `filename`, `pixmap`, `stream` have the same meaning as in :meth:`Page.insert_image`, especially exactly one of these must be provided. + + This is a **global replacement:** the new image will also be shown wherever the old one has been displayed throughout the file. + + This method mainly exists for technical purposes. Typical uses include replacing large images by smaller versions, like a lower resolution, graylevel instead of colored, etc., or changing transparency. + + + .. index:: + pair: xref; delete_image + + .. method:: delete_image(xref) + + * New in v1.21.0 + + Delete the image at xref. This is slightly misleading: actually the image is being replaced with a small transparent :ref:`Pixmap` using above :meth:`Page.replace_image`. The visible effect however is equivalent. + + :arg int xref: the :data:`xref` of the image. + + This is a **global replacement:** the image will disappear wherever the old one has been displayed throughout the file. + + If you inspect / extract a page's images by methods like :meth:`Page.get_images`, + :meth:`Page.get_image_info` or :meth:`Page.get_text`, + the replacing "dummy" image will be detected like so + `(45, 47, 1, 1, 8, 'DeviceGray', '', 'Im1', 'FlateDecode')` + and also seem to "cover" the same boundary box on the page. + + + .. index:: + pair: blocks; Page.get_text + pair: dict; Page.get_text + pair: clip; Page.get_text + pair: flags; Page.get_text + pair: html; Page.get_text + pair: json; Page.get_text + pair: rawdict; Page.get_text + pair: text; Page.get_text + pair: words; Page.get_text + pair: xhtml; Page.get_text + pair: xml; Page.get_text + pair: textpage; Page.get_text + pair: sort; Page.get_text + + .. method:: get_text(opt,*, clip=None, flags=None, textpage=None, sort=False) + + * Changed in v1.19.0: added `textpage` parameter + * Changed in v1.19.1: added `sort` parameter + * Changed in v1.19.6: added new constants for defining default flags per method. + + Retrieves the content of a page in a variety of formats. This is a wrapper for :ref:`TextPage` methods by choosing the output option as follows: + + * "text" -- :meth:`TextPage.extractTEXT`, default + * "blocks" -- :meth:`TextPage.extractBLOCKS` + * "words" -- :meth:`TextPage.extractWORDS` + * "html" -- :meth:`TextPage.extractHTML` + * "xhtml" -- :meth:`TextPage.extractXHTML` + * "xml" -- :meth:`TextPage.extractXML` + * "dict" -- :meth:`TextPage.extractDICT` + * "json" -- :meth:`TextPage.extractJSON` + * "rawdict" -- :meth:`TextPage.extractRAWDICT` + * "rawjson" -- :meth:`TextPage.extractRAWJSON` + + :arg str opt: A string indicating the requested format, one of the above. A mixture of upper and lower case is supported. + + Changed in v1.16.3 Values "words" and "blocks" are now also accepted. + + :arg rect-like clip: *(new in v1.17.7)* restrict extracted text to this rectangle. If None, the full page is taken. Has **no effect** for options "html", "xhtml" and "xml". + + :arg int flags: *(new in v1.16.2)* indicator bits to control whether to include images or how text should be handled with respect to white spaces and :data:`ligatures`. See :ref:`TextPreserve` for available indicators and :ref:`text_extraction_flags` for default settings. + + :arg textpage: (new in v1.19.0) use a previously created :ref:`TextPage`. This reduces execution time **very significantly:** by more than 50% and up to 95%, depending on the extraction option. If specified, the 'flags' and 'clip' arguments are ignored, because they are textpage-only properties. If omitted, a new, temporary textpage will be created. + + :arg bool sort: (new in v1.19.1) sort the output by vertical, then horizontal coordinates. In many cases, this should suffice to generate a "natural" reading order. Has no effect on (X)HTML and XML. Output option **"words"** sorts by `(y1, x0)` of the words' bboxes. Similar is true for "blocks", "dict", "json", "rawdict", "rawjson": they all are sorted by `(y1, x0)` of the resp. block bbox. If specified for "text", then internally "blocks" is used. + + :rtype: *str, list, dict* + :returns: The page's content as a string, a list or a dictionary. Refer to the corresponding :ref:`TextPage` method for details. + + .. note:: + + 1. You can use this method as a **document conversion tool** from :ref:`any supported document type` to one of TEXT, HTML, XHTML or XML documents. + 2. The inclusion of text via the *clip* parameter is decided on a by-character level: **(changed in v1.18.2)** a character becomes part of the output, if its bbox is contained in *clip*. This **deviates** from the algorithm used in redaction annotations: a character will be **removed if its bbox intersects** any redaction annotation. + + .. index:: + pair: rect; get_textbox + pair: textpage; get_textbox + + .. method:: get_textbox(rect, textpage=None) + + * New in v1.17.7 + * Changed in v1.19.0: add `textpage` parameter + + Retrieve the text contained in a rectangle. + + :arg rect-like rect: rect-like. + :arg textpage: a :ref:`TextPage` to use. If omitted, a new, temporary textpage will be created. + + :returns: a string with interspersed linebreaks where necessary. Changed in v1.19.0: It is based on dedicated code. A tyical use is checking the result of :meth:`Page.search_for`: + + >>> rl = page.search_for("currency:") + >>> page.get_textbox(rl[0]) + 'Currency:' + >>> + + + .. index:: + pair: flags; get_textpage + pair: clip; get_textpage + + .. method:: get_textpage(clip=None, flags=3) + + * New in v1.16.5 + * Changed in v1.17.7: introduced `clip` parameter. + + Create a :ref:`TextPage` for the page. + + :arg in flags: indicator bits controlling the content available for subsequent text extractions and searches -- see the parameter of :meth:`Page.get_text`. + + :arg rect-like clip: *(new in v1.17.7)* restrict extracted text to this area. + + :returns: :ref:`TextPage` + + + .. index:: + pair: flags; get_textpage_ocr + pair: language; get_textpage_ocr + pair: dpi; get_textpage_ocr + pair: full; get_textpage_ocr + pair: tessdata; get_textpage_ocr + + .. method:: get_textpage_ocr(flags=3, language="eng", dpi=72, full=False, tessdata=None) + + * New in v.1.19.0 + * Changed in v1.19.1: support full and partial OCRing a page. + + Create a :ref:`TextPage` for the page that includes OCRed text. MuPDF will invoke Tesseract-OCR if this method is used. Otherwise this is a normal :ref:`TextPage` object. + + :arg in flags: indicator bits controlling the content available for subsequent test extractions and searches -- see the parameter of :meth:`Page.get_text`. + :arg str language: the expected language(s). Use "+"-separated values if multiple languages are expected, "eng+spa" for English and Spanish. + :arg int dpi: the desired resolution in dots per inch. Influences recognition quality (and execution time). + :arg bool full: whether to OCR the full page, or just the displayed images. + :arg str tessdata: The name of Tesseract's language support folder `tessdata`. If omitted, this information must be present as environment variable `TESSDATA_PREFIX`. Can be determined by function :meth:`get_tessdata`. + + .. note:: This method does **not** support a clip parameter -- OCR will always happen for the complete page rectangle. + + :returns: + + a :ref:`TextPage`. Execution may be significantly longer than :meth:`Page.get_textpage`. + + For a full page OCR, **all text** will have the font "GlyphlessFont" from Tesseract. In case of partial OCR, normal text will keep its properties, and only text coming from images will have the GlyphlessFont. + + .. note:: + + **OCRed text is only available** to PyMuPDF's text extractions and searches if their `textpage` parameter specifies the output of this method. + + `This `_ Jupyter notebook walks through an example for using OCR textpages. + + + .. method:: get_drawings(extended=False) + + * New in v1.18.0 + * Changed in v1.18.17 + * Changed in v1.19.0: add "seqno" key, remove "clippings" key + * Changed in v1.19.1: "color" / "fill" keys now always are either are RGB tuples or `None`. This resolves issues caused by exotic colorspaces. + * Changed in v1.19.2: add an indicator for the *"orientation"* of the area covered by an "re" item. + * Changed in v1.22.0: add new key `"layer"` which contains the name of the Optional Content Group of the path (or `None`). + * Changed in v1.22.0: add parameter `extended` to also return clipping and group paths. + + Return the vector graphics of the page. These are instructions which draw lines, rectangles, quadruples or curves, including properties like colors, transparency, line width and dashing, etc. Alternative terms are "line art" and "drawings". + + :returns: a list of dictionaries. Each dictionary item contains one or more single draw commands belonging together: they have the same properties (colors, dashing, etc.). This is called a **"path"** in PDF, so we adopted that name here, but the method **works for all document types**. + + The path dictionary for fill, stroke and fill-stroke paths has been designed to be compatible with class :ref:`Shape`. There are the following keys: + + ============== ============================================================================ + Key Value + ============== ============================================================================ + closePath Same as the parameter in :ref:`Shape`. + color Stroke color (see :ref:`Shape`). + dashes Dashed line specification (see :ref:`Shape`). + even_odd Fill colors of area overlaps -- same as the parameter in :ref:`Shape`. + fill Fill color (see :ref:`Shape`). + items List of draw commands: lines, rectangles, quads or curves. + lineCap Number 3-tuple, use its max value on output with :ref:`Shape`. + lineJoin Same as the parameter in :ref:`Shape`. + fill_opacity (new in v1.18.17) fill color transparency (see :ref:`Shape`). + stroke_opacity (new in v1.18.17) stroke color transparency (see :ref:`Shape`). + rect Page area covered by this path. Information only. + layer (new in v1.22.0) name of applicable Optional Content Group + level (new in v1.22.0) the hierarchy level if `extended=True` + seqno (new in v1.19.0) command number when building page appearance + type (new in v1.18.17) type of this path. + width Stroke line width (see :ref:`Shape`). + ============== ============================================================================ + + * *(Changed in v1.18.17)* Key `"opacity"` has been replaced by the new keys `"fill_opacity"` and `"stroke_opacity"`. This is now compatible with the corresponding parameters of :meth:`Shape.finish`. + + + For paths other than groups or clips, key `"type"` takes one of the following values: + + * **"f"** -- this is a *fill-only* path. Only key-values relevant for this operation have a meaning, not applicable ones are present with a value of *None*: `"color"`, `"lineCap"`, `"lineJoin"`, `"width"`, `"closePath"`, `"dashes"` and should be ignored. + * **"s"** -- this is a *stroke-only* path. Similar to previous, key `"fill"` is present with value *None*. + * **"fs"** -- this is a path performing combined *fill* and *stroke* operations. + + Each item in `path["items"]` is one of the following: + + * `("l", p1, p2)` - a line from p1 to p2 (:ref:`Point` objects). + * `("c", p1, p2, p3, p4)` - cubic Bézier curve **from p1 to p4** (p2 and p3 are the control points). All objects are of type :ref:`Point`. + * `("re", rect, orientation)` - a :ref:`Rect`. *Changed in v1.18.17:* Multiple rectangles within the same path are now detected. *Changed in v1.19.2:* added integer `orientation` which is 1 resp. -1 indicating whether the enclosed area is rotated left (1 = anti-clockwise), or resp. right [#f7]_. + * `("qu", quad)` - a :ref:`Quad`. *New in v1.18.17, changed in v1.19.2:* 3 or 4 consecutive lines are detected to actually represent a :ref:`Quad`. + + .. note:: Starting with v1.19.2, quads and rectangles are more reliably recognized as such. + + Using class :ref:`Shape`, you should be able to recreate the original drawings on a separate (PDF) page with high fidelity under normal, not too sophisticated circumstances. Please see the following comments on restrictions. A coding draft can be found in section "Extractings Drawings" of chapter :ref:`FAQ`. + + **New in v1.22.0:** Specifying `extended=True` significantly alters the output. Most importantly, new dictionary types are present: "clip" and "group". All paths will now be organized in a hierarchic structure which is encoded by the new integer key "level", the hierarchy level. Each group or clip establishes a new hierarchy, which applies to all subsequent paths having a *larger* level value. + + Any path with a smaller level value than its predecessor will end the scope of (at least) the preceeding hierarchy level. A "clip" path with the same level as the preceding clip will end the scope of that clip. Same is true for groups. This is best explained by an example:: + + +------+------+--------+------+--------+ + | line | lvl0 | lvl1 | lvl2 | lvl3 | + +------+------+--------+------+--------+ + | 0 | clip | | | | + | 1 | | fill | | | + | 2 | | group | | | + | 3 | | | clip | | + | 4 | | | | stroke | + | 5 | | | fill | | ends scope of clip in line 3 + | 6 | | stroke | | | ends scope of group in line 2 + | 7 | | clip | | | + | 8 | fill | | | | ends scope of line 0 + +------+------+--------+------+--------+ + + The clip in line 0 applies to line including line 7. Group in line 2 applies to lines 3 to 5, clip in line 3 only applies to line 4. + + "stroke" in line 4 is under control of "group" in line 2 and "clip" in line 3 (which in turn is a subset of line 0 clip). + + * **"clip"** dictionary. Its values (most importantly "scissor") remain valid / apply as long as following dictionaries have a **larger "level"** value. + + ============== ============================================================================ + Key Value + ============== ============================================================================ + closePath Same as in "stroke" or "fill" dictionaries + even_odd Same as in "stroke" or "fill" dictionaries + items Same as in "stroke" or "fill" dictionaries + rect Same as in "stroke" or "fill" dictionaries + layer Same as in "stroke" or "fill" dictionaries + level Same as in "stroke" or "fill" dictionaries + scissor the clip rectangle + type "clip" + ============== ============================================================================ + + * "group" dictionary. Its values remain valid (apply) as long as following dictionaries have a **larger "level"** value. Any dictionary with an equal or lower level end this group. + + ============== ============================================================================ + Key Value + ============== ============================================================================ + rect Same as in "stroke" or "fill" dictionaries + layer Same as in "stroke" or "fill" dictionaries + level Same as in "stroke" or "fill" dictionaries + isolated (bool) Whether this group is isolated + knockout (bool) Whether this is a "Knockout Group" + blendmode Name of the BlendMode, default is "Normal" + opacity Float value in range [0, 1]. + type "group" + ============== ============================================================================ + + + + * *(Changed in v1.18.17)* Key `"opacity"` has been replaced by the new keys `"fill_opacity"` and `"stroke_opacity"`. This is now compatible with the corresponding parameters of :meth:`Shape.finish`. + + + Key `"type"` takes one of the following values: + + * **"f"** -- this is a *fill-only* path. Only key-values relevant for this operation have a meaning, irrelevant ones have been added with default values for backward compatibility: `"color"`, `"lineCap"`, `"lineJoin"`, `"width"`, `"closePath"`, `"dashes"` and should be ignored. + * **"s"** -- this is a *stroke-only* path. Similar to previous, key `"fill"` is present with value `None`. + * **"fs"** -- this is a path performing combined *fill* and *stroke* operations. + + Each item in `path["items"]` is one of the following: + + * `("l", p1, p2)` - a line from p1 to p2 (:ref:`Point` objects). + * `("c", p1, p2, p3, p4)` - cubic Bézier curve **from p1 to p4** (p2 and p3 are the control points). All objects are of type :ref:`Point`. + * `("re", rect, orientation)` - a :ref:`Rect`. *Changed in v1.18.17:* Multiple rectangles within the same path are now detected. *Changed in v1.19.2:* added integer `orientation` which is 1 resp. -1 indicating whether the enclosed area is rotated left (1 = anti-clockwise), or resp. right [#f7]_. + * `("qu", quad)` - a :ref:`Quad`. *New in v1.18.17, changed in v1.19.2:* 3 or 4 consecutive lines are detected to actually represent a :ref:`Quad`. + + .. note:: Starting with v1.19.2, quads and rectangles are more reliably recognized as such. + + Using class :ref:`Shape`, you should be able to recreate the original drawings on a separate (PDF) page with high fidelity under normal, not too sophisticated circumstances. Please see the following comments on restrictions. A coding draft can be found in section "How to Extract Drawings" of chapter :ref:`FAQ`. + + + .. note:: The method is based on the output of :meth:`Page.get_cdrawings` -- which is much faster, but requires somewhat more attention processing its output. + + + + .. method:: get_cdrawings(extended=False) + + * New in v1.18.17 + * Changed in v1.19.0: removed "clippings" key, added "seqno" key. + * Changed in v1.19.1: always generate RGB color tuples. + * Changed in v1.22.0: added new key `"layer"` which contains the name of the Optional Content Group of the path (or `None`). + * Changed in v1.22.0 added parameter `extended` to also return clipping paths. + + Extract the vector graphics on the page. Apart from following technical differences, functionally equivalent to :meth:`Page.get_drawings`, but much faster: + + * Every path type only contains the relevant keys, e.g. a stroke path has no `"fill"` color key. See comment in method :meth:`Page.get_drawings`. + * Coordinates are given as :data:`point_like`, :data:`rect_like` and :data:`quad_like` **tuples** -- not as :ref:`Point`, :ref:`Rect`, :ref:`Quad` objects. + + If performance is a concern, consider using this method: Compared to versions earlier than 1.18.17, you should see much shorter response times. We have seen pages that required 2 seconds then, now only need 200 ms with this method. + + + .. method:: get_fonts(full=False) + + PDF only: Return a list of fonts referenced by the page. Wrapper for :meth:`Document.get_page_fonts`. + + + .. method:: get_images(full=False) + + PDF only: Return a list of images referenced by the page. Wrapper for :meth:`Document.get_page_images`. + + + .. index:: + pair: hashes; get_image_info + pair: xrefs; get_image_info + + .. method:: get_image_info(hashes=False, xrefs=False) + + * *New in v1.18.11* + * *Changed in v1.18.13:* added image MD5 hashcode computation and :data:`xref` search. + + Return a list of meta information dictionaries for all images shown on the page. This works for all document types. Technically, this is a subset of the dictionary output of :meth:`Page.get_text`: the image binary content and any text on the page are ignored. + + :arg bool hashes: *New in v1.18.13:* Compute the MD5 hashcode for each encountered image, which allows identifying image duplicates. This adds the key `"digest"` to the output, whose value is a 16 byte `bytes` object. + + :arg bool xrefs: *New in v1.18.13:* **PDF only.** Try to find the :data:`xref` for each image. Implies `hashes=True`. Adds the `"xref"` key to the dictionary. If not found, the value is 0, which means, the image is either "inline" or otherwise undetectable. Please note that this option has an extended response time, because the MD5 hashcode will be computed at least two times for each image with an xref. + + :rtype: list[dict] + :returns: A list of dictionaries. This includes information for **exactly those** images, that are shown on the page -- including *"inline images"*. In contrast to images included in :meth:`Page.get_text`, image **binary content** is not loaded, which drastically reduces memory usage. The dictionary layout is similar to that of image blocks in `page.get_text("dict")`. + + =============== =============================================================== + **Key** **Value** + =============== =============================================================== + number block number *(int)* + bbox image bbox on page, :data:`rect_like` + width original image width *(int)* + height original image height *(int)* + cs-name colorspace name *(str)* + colorspace colorspace.n *(int)* + xres resolution in x-direction *(int)* + yres resolution in y-direction *(int)* + bpc bits per component *(int)* + size storage occupied by image *(int)* + digest MD5 hashcode *(bytes)*, if *hashes* is true + xref image :data:`xref` or 0, if *xrefs* is true + transform matrix transforming image rect to bbox, :data:`matrix_like` + =============== =============================================================== + + Multiple occurrences of the same image are always reported. You can detect duplicates by comparing their `digest` values. + + + .. method:: get_xobjects() + + PDF only: Return a list of Form XObjects referenced by the page. Wrapper for :meth:`Document.get_page_xobjects`. + + + .. index:: + pair: transform; get_image_rects + + .. method:: get_image_rects(item, transform=False) + + *New in v1.18.13* + + PDF only: Return boundary boxes and transformation matrices of an embedded image. This is an improved version of :meth:`Page.get_image_bbox` with the following differences: + + * There is no restriction on **how** the image is invoked (by the page or one of its Form XObjects). The result is always complete and correct. + * The result is a list of :ref:`Rect` or (:ref:`Rect`, :ref:`Matrix`) objects -- depending on *transform*. Each list item represents one location of the image on the page. Multiple occurrences might not be detectable by :meth:`Page.get_image_bbox`. + * The method invokes :meth:`Page.get_image_info` with `xrefs=True` and therefore has a noticeably longer response time than :meth:`Page.get_image_bbox`. + + :arg list,str,int item: an item of the list :meth:`Page.get_images`, or the reference **name** entry of such an item (item[7]), or the image :data:`xref`. + :arg bool transform: also return the matrix used to transform the image rectangle to the bbox on the page. If true, then tuples `(bbox, matrix)` are returned. + + :rtype: list + :returns: Boundary boxes and respective transformation matrices for each image occurrence on the page. If the item is not on the page, an empty list `[]` is returned. + + + .. index:: + pair: transform; get_image_bbox + + .. method:: get_image_bbox(item, transform=False) + + * Changed in v1.18.11: return image transformation matrix + + PDF only: Return boundary box and transformation matrix of an embedded image. + + :arg list,str item: an item of the list :meth:`Page.get_images` with *full=True* specified, or the reference **name** entry of such an item, which is item[-3] (or item[7] respectively). + :arg bool transform: *(new in v1.18.11)* also return the matrix used to transform the image rectangle to the bbox on the page. Default is just the bbox. If true, then a tuple `(bbox, matrix)` is returned. + + :rtype: :ref:`Rect` or (:ref:`Rect`, :ref:`Matrix`) + :returns: the boundary box of the image -- optionally also its transformation matrix. + + * *(Changed in v1.16.7)* -- If the page in fact does not display this image, an infinite rectangle is returned now. In previous versions, an exception was raised. Formally invalid parameters still raise exceptions. + * *(Changed in v1.17.0)* -- Only images referenced directly by the page are considered. This means that images occurring in embedded PDF pages are ignored and an exception is raised. + * *(Changed in v1.18.5)* -- Removed the restriction introduced in v1.17.0: any item of the page's image list may be specified. + * *(Changed in v1.18.11)* -- Partially re-instated a restriction: only those images are considered, that are either directly referenced by the page or by a Form XObject directly referenced by the page. + * *(Changed in v1.18.11)* -- Optionally also return the transformation matrix together with the bbox as the tuple `(bbox, transform)`. + + .. note:: + + 1. Be aware that :meth:`Page.get_images` may contain "dead" entries i.e. images, which the page **does not display**. This is no error, but intended by the PDF creator. No exception will be raised in this case, but an infinite rectangle is returned. You can avoid this from happening by executing :meth:`Page.clean_contents` before this method. + 2. The image's "transformation matrix" is defined as the matrix, for which the expression `bbox / transform == fitz.Rect(0, 0, 1, 1)` is true, lookup details here: :ref:`ImageTransformation`. + + .. index:: + pair: matrix; get_svg_image + + .. method:: get_svg_image(matrix=fitz.Identity, text_as_path=True) + + Create an SVG image from the page. Only full page images are currently supported. + + :arg matrix_like matrix: a matrix, default is :ref:`Identity`. + :arg bool text_as_path: *(new in v1.17.5)* -- controls how text is represented. *True* outputs each character as a series of elementary draw commands, which leads to a more precise text display in browsers, but a **very much larger** output for text-oriented pages. Display quality for *False* relies on the presence of the referenced fonts on the current system. For missing fonts, the internet browser will fall back to some default -- leading to unpleasant appearances. Choose *False* if you want to parse the text of the SVG. + + :returns: a UTF-8 encoded string that contains the image. Because SVG has XML syntax it can be saved in a text file, the standard extension is `.svg`. + + .. note:: In case of a PDF, you can circumvent the "full page image only" restriction by modifying the page's CropBox before using the method. + + .. index:: + pair: alpha; get_pixmap + pair: annots; get_pixmap + pair: clip; get_pixmap + pair: colorspace; get_pixmap + pair: matrix; get_pixmap + pair: dpi; get_pixmap + + .. method:: get_pixmap(*, matrix=fitz.Identity, dpi=None, colorspace=fitz.csRGB, clip=None, alpha=False, annots=True) + + * Changed in v1.19.2: added support of parameter dpi. + + Create a pixmap from the page. This is probably the most often used method to create a :ref:`Pixmap`. + + All parameters are *keyword-only.* + + :arg matrix_like matrix: default is :ref:`Identity`. + :arg int dpi: (new in v1.19.2) desired resolution in x and y direction. If not `None`, the `"matrix"` parameter is ignored. + :arg colorspace: The desired colorspace, one of "GRAY", "RGB" or "CMYK" (case insensitive). Or specify a :ref:`Colorspace`, ie. one of the predefined ones: :data:`csGRAY`, :data:`csRGB` or :data:`csCMYK`. + :type colorspace: str or :ref:`Colorspace` + :arg irect_like clip: restrict rendering to the intersection of this area with the page's rectangle. + :arg bool alpha: whether to add an alpha channel. Always accept the default *False* if you do not really need transparency. This will save a lot of memory (25% in case of RGB ... and pixmaps are typically **large**!), and also processing time. Also note an **important difference** in how the image will be rendered: with *True* the pixmap's samples area will be pre-cleared with *0x00*. This results in **transparent** areas where the page is empty. With *False* the pixmap's samples will be pre-cleared with *0xff*. This results in **white** where the page has nothing to show. + + Changed in v1.14.17 + The default alpha value is now *False*. + + * Generated with *alpha=True* + + .. image:: images/img-alpha-1.* + + + * Generated with *alpha=False* + + .. image:: images/img-alpha-0.* + + :arg bool annots: *(new in version 1.16.0)* whether to also render annotations or to suppress them. You can create pixmaps for annotations separately. + + :rtype: :ref:`Pixmap` + :returns: Pixmap of the page. For fine-controlling the generated image, the by far most important parameter is **matrix**. E.g. you can increase or decrease the image resolution by using **Matrix(xzoom, yzoom)**. If zoom > 1, you will get a higher resolution: zoom=2 will double the number of pixels in that direction and thus generate a 2 times larger image. Non-positive values will flip horizontally, resp. vertically. Similarly, matrices also let you rotate or shear, and you can combine effects via e.g. matrix multiplication. See the :ref:`Matrix` section to learn more. + + .. note:: + The method will respect any page rotation and will not exceed the intersection of `clip` and :attr:`Page.cropbox`. If you need the page's mediabox (and if this is a different rectangle), you can use a snippet like the following to achieve this:: + + In [1]: import fitz + In [2]: doc=fitz.open("demo1.pdf") + In [3]: page=doc[0] + In [4]: rotation = page.rotation + In [5]: cropbox = page.cropbox + In [6]: page.set_cropbox(page.mediabox) + In [7]: page.set_rotation(0) + In [8]: pix = page.get_pixmap() + In [9]: page.set_cropbox(cropbox) + In [10]: if rotation != 0: + ...: page.set_rotation(rotation) + ...: + In [11]: + + + + .. method:: annot_names() + + * New in v1.16.10 + + PDF only: return a list of the names of annotations, widgets and links. Technically, these are the */NM* values of every PDF object found in the page's */Annots* array. + + :rtype: list + + + .. method:: annot_xrefs() + + * New in v1.17.1 + + PDF only: return a list of the :data`xref` numbers of annotations, widgets and links -- technically of all entries found in the page's */Annots* array. + + :rtype: list + :returns: a list of items *(xref, type)* where type is the annotation type. Use the type to tell apart links, fields and annotations, see :ref:`AnnotationTypes`. + + + .. method:: load_annot(ident) + + * New in v1.17.1 + + PDF only: return the annotation identified by *ident*. This may be its unique name (PDF `/NM` key), or its :data:`xref`. + + :arg str,int ident: the annotation name or xref. + + :rtype: :ref:`Annot` + :returns: the annotation or *None*. + + .. note:: Methods :meth:`Page.annot_names`, :meth:`Page.annot_xrefs` provide lists of names or xrefs, respectively, from where an item may be picked and loaded via this method. + + .. method:: load_widget(xref) + + * New in v1.19.6 + + PDF only: return the field identified by *xref*. + + :arg int xref: the field's xref. + + :rtype: :ref:`Widget` + :returns: the field or *None*. + + .. note:: This is similar to the analogous method :meth:`Page.load_annot` -- except that here only the xref is supported as identifier. + + .. method:: load_links() + + Return the first link on a page. Synonym of property :attr:`first_link`. + + :rtype: :ref:`Link` + :returns: first link on the page (or *None*). + + .. index:: + pair: rotate; set_rotation + + .. method:: set_rotation(rotate) + + PDF only: Set the rotation of the page. + + :arg int rotate: An integer specifying the required rotation in degrees. Must be an integer multiple of 90. Values will be converted to one of 0, 90, 180, 270. + + .. index:: + pair: clip; show_pdf_page + pair: keep_proportion; show_pdf_page + pair: overlay; show_pdf_page + pair: rotate; show_pdf_page + + .. method:: show_pdf_page(rect, docsrc, pno=0, keep_proportion=True, overlay=True, oc=0, rotate=0, clip=None) + + * Changed in v1.14.11: Parameter *reuse_xref* has been deprecated. Position the source rectangle centered in target rectangle. Any rotation angle is now supported. + * Changed in v1.18.3: New parameter `oc`. + + PDF only: Display a page of another PDF as a **vector image** (otherwise similar to :meth:`Page.insert_image`). This is a multi-purpose method. For example, you can use it to + + * create "n-up" versions of existing PDF files, combining several input pages into **one output page** (see example `4-up.py `_), + * create "posterized" PDF files, i.e. every input page is split up in parts which each create a separate output page (see `posterize.py `_), + * include PDF-based vector images like company logos, watermarks, etc., see `svg-logo.py `_, which puts an SVG-based logo on each page (requires additional packages to deal with SVG-to-PDF conversions). + + :arg rect_like rect: where to place the image on current page. Must be finite and its intersection with the page must not be empty. + :arg docsrc: source PDF document containing the page. Must be a different document object, but may be the same file. + :type docsrc: :ref:`Document` + + :arg int pno: page number (0-based, in `-∞ < pno < docsrc.page_count`) to be shown. + + :arg bool keep_proportion: whether to maintain the width-height-ratio (default). If false, all 4 corners are always positioned on the border of the target rectangle -- whatever the rotation value. In general, this will deliver distorted and /or non-rectangular images. + + :arg bool overlay: put image in foreground (default) or background. + + :arg int oc: *(new in v1.18.3)* (:data:`xref`) make visibility dependent on this OCG (optional content group). + :arg float rotate: *(new in v1.14.10)* show the source rectangle rotated by some angle. *Changed in v1.14.11:* Any angle is now supported. + + :arg rect_like clip: choose which part of the source page to show. Default is the full page, else must be finite and its intersection with the source page must not be empty. + + .. note:: In contrast to method :meth:`Document.insert_pdf`, this method does not copy annotations, widgets or links, so these are not included in the target [#f6]_. But all its **other resources (text, images, fonts, etc.)** will be imported into the current PDF. They will therefore appear in text extractions and in :meth:`get_fonts` and :meth:`get_images` lists -- even if they are not contained in the visible area given by *clip*. + + Example: Show the same source page, rotated by 90 and by -90 degrees: + + >>> doc = fitz.open() # new empty PDF + >>> page=doc.new_page() # new page in A4 format + >>> + >>> # upper half page + >>> r1 = fitz.Rect(0, 0, page.rect.width, page.rect.height/2) + >>> + >>> # lower half page + >>> r2 = r1 + (0, page.rect.height/2, 0, page.rect.height/2) + >>> + >>> src = fitz.open("PyMuPDF.pdf") # show page 0 of this + >>> + >>> page.show_pdf_page(r1, src, 0, rotate=90) + >>> page.show_pdf_page(r2, src, 0, rotate=-90) + >>> doc.save("show.pdf") + + .. image:: images/img-showpdfpage.* + :scale: 70 + + .. method:: new_shape() + + PDF only: Create a new :ref:`Shape` object for the page. + + :rtype: :ref:`Shape` + :returns: a new :ref:`Shape` to use for compound drawings. See description there. + + + .. index:: + pair: flags; search_for + pair: quads; search_for + pair: clip; search_for + pair: textpage; search_for + + .. method:: search_for(needle, *, clip=clip, quads=False, flags=TEXT_DEHYPHENATE | TEXT_PRESERVE_WHITESPACE | TEXT_PRESERVE_LIGATURES, textpage=None) + + * Changed in v1.18.2: added `clip` parameter. Remove `hit_max` parameter. Add default "dehyphenate". + * Changed in v1.19.0: added `textpage` parameter. + + Search for *needle* on a page. Wrapper for :meth:`TextPage.search`. + + :arg str needle: Text to search for. May contain spaces. Upper / lower case is ignored, but only works for ASCII characters: For example, "COMPÉTENCES" will not be found if needle is "compétences" -- "compÉtences" however will. Similar is true for German umlauts and the like. + :arg rect_like clip: *(New in v1.18.2)* only search within this area. + :arg bool quads: Return object type :ref:`Quad` instead of :ref:`Rect`. + :arg int flags: Control the data extracted by the underlying :ref:`TextPage`. By default, ligatures and white spaces are kept, and hyphenation [#f8]_ is detected. + :arg textpage: (new in v1.19.0) use a previously created :ref:`TextPage`. This reduces execution time **significantly.** If specified, the 'flags' and 'clip' arguments are ignored. If omitted, a temporary textpage will be created. + + :rtype: list + + :returns: + + A list of :ref:`Rect` or :ref:`Quad` objects, each of which -- **normally!** -- surrounds one occurrence of *needle*. **However:** if parts of *needle* occur on more than one line, then a separate item is generated for each these parts. So, if `needle = "search string"`, two rectangles may be generated. + + **Changes in v1.18.2:** + + * There no longer is a limit on the list length (removal of the `hit_max` parameter). + * If a word is **hyphenated** at a line break, it will still be found. E.g. the needle "method" will be found even if hyphenated as "meth-od" at a line break, and two rectangles will be returned: one surrounding "meth" (without the hyphen) and another one surrounding "od". + + .. note:: The method supports multi-line text marker annotations: you can use the full returned list as **one single** parameter for creating the annotation. + + .. caution:: + + * There is a tricky aspect: the search logic regards **contiguous multiple occurrences** of *needle* as one: assuming *needle* is "abc", and the page contains "abc" and "abcabc", then only **two** rectangles will be returned, one for "abc", and a second one for "abcabc". + * You can always use :meth:`Page.get_textbox` to check what text actually is being surrounded by each rectangle. + + .. note:: A feature repeatedly asked for is supporting **regular expressions** when specifying the `"needle"` string: **There is no way to do this.** If you need something in that direction, first extract text in the desired format and then subselect the result by matching with some regex pattern. Here is an example for matching words:: + + >>> pattern = re.compile(r"...") # the regex pattern + >>> words = page.get_text("words") # extract words on page + >>> matches = [w for w in words if pattern.search(w[4])] + + The `matches` list will contain the words matching the given pattern. In the same way you can select `span["text"]` from the output of `page.get_text("dict")`. + + + .. method:: set_mediabox(r) + + * New in v1.16.13 + * Changed in v1.19.4: remove all other rectangle definitions. + + PDF only: Change the physical page dimension by setting :data:`mediabox` in the page's object definition. + + :arg rect-like r: the new :data:`mediabox` value. + + .. note:: This method also removes the page's other (optional) rectangles (:data:`cropbox`, ArtBox, TrimBox and Bleedbox) to prevent inconsistent situations. This will cause those to assume their default values. + + .. caution:: For non-empty pages this may have undesired effects, because the location of all content depends on this value and will therefore change position or even disappear. + + + .. method:: set_cropbox(r) + + PDF only: change the visible part of the page. + + :arg rect_like r: the new visible area of the page. Note that this **must** be specified in **unrotated coordinates**, not empty, nor infinite and be completely contained in the :attr:`Page.mediabox`. + + After execution **(if the page is not rotated)**, :attr:`Page.rect` will equal this rectangle, but be shifted to the top-left position (0, 0) if necessary. Example session: + + >>> page = doc.new_page() + >>> page.rect + fitz.Rect(0.0, 0.0, 595.0, 842.0) + >>> + >>> page.cropbox # cropbox and mediabox still equal + fitz.Rect(0.0, 0.0, 595.0, 842.0) + >>> + >>> # now set cropbox to a part of the page + >>> page.set_cropbox(fitz.Rect(100, 100, 400, 400)) + >>> # this will also change the "rect" property: + >>> page.rect + fitz.Rect(0.0, 0.0, 300.0, 300.0) + >>> + >>> # but mediabox remains unaffected + >>> page.mediabox + fitz.Rect(0.0, 0.0, 595.0, 842.0) + >>> + >>> # revert CropBox change + >>> # either set it to MediaBox + >>> page.set_cropbox(page.mediabox) + >>> # or 'refresh' MediaBox: will remove all other rectangles + >>> page.set_mediabox(page.mediabox) + + .. method:: set_artbox(r) + + .. method:: set_bleedbox(r) + + .. method:: set_trimbox(r) + + * New in v1.19.4 + + PDF only: Set the resp. rectangle in the page object. For the meaning of these objects see :ref:`AdobeManual`, page 77. Parameter and restrictions are the same as for :meth:`Page.set_cropbox`. + + + .. attribute:: rotation + + Contains the rotation of the page in degrees (always 0 for non-PDF types). + + :type: int + + .. attribute:: cropbox_position + + Contains the top-left point of the page's `/CropBox` for a PDF, otherwise *Point(0, 0)*. + + :type: :ref:`Point` + + .. attribute:: cropbox + + The page's `/CropBox` for a PDF. Always the **unrotated** page rectangle is returned. For a non-PDF this will always equal the page rectangle. + + .. note:: In PDF, the relationship between `/MediaBox`, `/CropBox` and page rectangle may sometimes be confusing, please do lookup the glossary for :data:`MediaBox`. + + :type: :ref:`Rect` + + .. attribute:: artbox + + .. attribute:: bleedbox + + .. attribute:: trimbox + + The page's `/ArtBox`, `/BleedBox`, `/TrimBox`, respectively. If not provided, defaulting to :attr:`Page.cropbox`. + + :type: :ref:`Rect` + + .. attribute:: mediabox_size + + Contains the width and height of the page's :attr:`Page.mediabox` for a PDF, otherwise the bottom-right coordinates of :attr:`Page.rect`. + + :type: :ref:`Point` + + .. attribute:: mediabox + + The page's :data:`mediabox` for a PDF, otherwise :attr:`Page.rect`. + + :type: :ref:`Rect` + + .. note:: For most PDF documents and for **all other document types**, `page.rect == page.cropbox == page.mediabox` is true. However, for some PDFs the visible page is a true subset of :data:`mediabox`. Also, if the page is rotated, its `Page.rect` may not equal `Page.cropbox`. In these cases the above attributes help to correctly locate page elements. + + .. attribute:: transformation_matrix + + This matrix translates coordinates from the PDF space to the MuPDF space. For example, in PDF `/Rect [x0 y0 x1 y1]` the pair (x0, y0) specifies the **bottom-left** point of the rectangle -- in contrast to MuPDF's system, where (x0, y0) specify top-left. Multiplying the PDF coordinates with this matrix will deliver the (Py-) MuPDF rectangle version. Obviously, the inverse matrix will again yield the PDF rectangle. + + :type: :ref:`Matrix` + + .. attribute:: rotation_matrix + + .. attribute:: derotation_matrix + + These matrices may be used for dealing with rotated PDF pages. When adding / inserting anything to a PDF page, the coordinates of the **unrotated** page are always used. These matrices help translating between the two states. Example: if a page is rotated by 90 degrees -- what would then be the coordinates of the top-left Point(0, 0) of an A4 page? + + >>> page.set_rotation(90) # rotate an ISO A4 page + >>> page.rect + Rect(0.0, 0.0, 842.0, 595.0) + >>> p = fitz.Point(0, 0) # where did top-left point land? + >>> p * page.rotation_matrix + Point(842.0, 0.0) + >>> + + :type: :ref:`Matrix` + + .. attribute:: first_link + + Contains the first :ref:`Link` of a page (or *None*). + + :type: :ref:`Link` + + .. attribute:: first_annot + + Contains the first :ref:`Annot` of a page (or *None*). + + :type: :ref:`Annot` + + .. attribute:: first_widget + + Contains the first :ref:`Widget` of a page (or *None*). + + :type: :ref:`Widget` + + .. attribute:: number + + The page number. + + :type: int + + .. attribute:: parent + + The owning document object. + + :type: :ref:`Document` + + + .. attribute:: rect + + Contains the rectangle of the page. Same as result of :meth:`Page.bound()`. + + :type: :ref:`Rect` + + .. attribute:: xref + + The page's PDF :data:`xref`. Zero if not a PDF. + + :type: :ref:`Rect` + +----- + +Description of *get_links()* Entries +---------------------------------------- +Each entry of the :meth:`Page.get_links` list is a dictionary with the following keys: + +* *kind*: (required) an integer indicating the kind of link. This is one of *LINK_NONE*, *LINK_GOTO*, *LINK_GOTOR*, *LINK_LAUNCH*, or *LINK_URI*. For values and meaning of these names refer to :ref:`linkDest Kinds`. + +* *from*: (required) a :ref:`Rect` describing the "hot spot" location on the page's visible representation (where the cursor changes to a hand image, usually). + +* *page*: a 0-based integer indicating the destination page. Required for *LINK_GOTO* and *LINK_GOTOR*, else ignored. + +* *to*: either a *fitz.Point*, specifying the destination location on the provided page, default is *fitz.Point(0, 0)*, or a symbolic (indirect) name. If an indirect name is specified, *page = -1* is required and the name must be defined in the PDF in order for this to work. Required for *LINK_GOTO* and *LINK_GOTOR*, else ignored. + +* *file*: a string specifying the destination file. Required for *LINK_GOTOR* and *LINK_LAUNCH*, else ignored. + +* *uri*: a string specifying the destination internet resource. Required for *LINK_URI*, else ignored. You should make sure to start this string with an unambiguous substring, that classifies the subtype of the URL, like `"http://"`, `"https://"`, `"file://"`, `"ftp://"`, `"mailto:"`, etc. Otherwise your browser will try to interpret the text and come to unwanted / unexpected conclusions about the intended URL type. + +* *xref*: an integer specifying the PDF :data:`xref` of the link object. Do not change this entry in any way. Required for link deletion and update, otherwise ignored. For non-PDF documents, this entry contains *-1*. It is also *-1* for **all** entries in the *get_links()* list, if **any** of the links is not supported by MuPDF - see the note below. + +Notes on Supporting Links +--------------------------- +MuPDF's support for links has changed in **v1.10a**. These changes affect link types :data:`LINK_GOTO` and :data:`LINK_GOTOR`. + +Reading (pertains to method *get_links()* and the *first_link* property chain) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If MuPDF detects a link to another file, it will supply either a *LINK_GOTOR* or a *LINK_LAUNCH* link kind. In case of *LINK_GOTOR* destination details may either be given as page number (eventually including position information), or as an indirect destination. + +If an indirect destination is given, then this is indicated by *page = -1*, and *link.dest.dest* will contain this name. The dictionaries in the *get_links()* list will contain this information as the *to* value. + +**Internal links are always** of kind *LINK_GOTO*. If an internal link specifies an indirect destination, it **will always be resolved** and the resulting direct destination will be returned. Names are **never returned for internal links**, and undefined destinations will cause the link to be ignored. + +Writing +~~~~~~~~~ + +PyMuPDF writes (updates, inserts) links by constructing and writing the appropriate PDF object **source**. This makes it possible to specify indirect destinations for *LINK_GOTOR* **and** *LINK_GOTO* link kinds (pre *PDF 1.2* file formats are **not supported**). + +.. warning:: If a *LINK_GOTO* indirect destination specifies an undefined name, this link can later on not be found / read again with MuPDF / PyMuPDF. Other readers however **will** detect it, but flag it as erroneous. + +Indirect *LINK_GOTOR* destinations can in general of course not be checked for validity and are therefore **always accepted**. + +**Example: How to insert a link pointing to another page in the same document** + +1. Determine the rectangle on the current page, where the link should be placed. This may be the bbox of an image or some text. + +2. Determine the target page number ("pno", 0-based) and a :ref:`Point` on it, where the link should be directed to. + +3. Create a dictionary `d = {"kind": fitz.LINK_GOTO, "page": pno, "from": bbox, "to": point}`. + +4. Execute `page.insert_link(d)`. + + +Homologous Methods of :ref:`Document` and :ref:`Page` +-------------------------------------------------------- +This is an overview of homologous methods on the :ref:`Document` and on the :ref:`Page` level. + +====================================== ===================================== +**Document Level** **Page Level** +====================================== ===================================== +*Document.get_page_fonts(pno)* :meth:`Page.get_fonts` +*Document.get_page_images(pno)* :meth:`Page.get_images` +*Document.get_page_pixmap(pno, ...)* :meth:`Page.get_pixmap` +*Document.get_page_text(pno, ...)* :meth:`Page.get_text` +*Document.search_page_for(pno, ...)* :meth:`Page.search_for` +====================================== ===================================== + +The page number "pno" is a 0-based integer `-∞ < pno < page_count`. + +.. note:: + + Most document methods (left column) exist for convenience reasons, and are just wrappers for: *Document[pno].*. So they **load and discard the page** on each execution. + + However, the first two methods work differently. They only need a page's object definition statement - the page itself will **not** be loaded. So e.g. :meth:`Page.get_fonts` is a wrapper the other way round and defined as follows: *page.get_fonts == page.parent.get_page_fonts(page.number)*. + +.. rubric:: Footnotes + +.. [#f1] If your existing code already uses the installed base name as a font reference (as it was supported by PyMuPDF versions earlier than 1.14), this will continue to work. + +.. [#f2] Not all PDF reader software (including internet browsers and office software) display all of these fonts. And if they do, the difference between the **serifed** and the **non-serifed** version may hardly be noticeable. But serifed and non-serifed versions lead to different installed base fonts, thus providing an option to be displayable with your specific PDF viewer. + +.. [#f3] Not all PDF readers display these fonts at all. Some others do, but use a wrong character spacing, etc. + +.. [#f4] You are generally free to choose any of the :ref:`mupdficons` you consider adequate. + +.. [#f5] The previous algorithm caused images to be **shrunk** to this intersection. Now the image can be anywhere on :attr:`Page.mediabox`, potentially being invisible or only partially visible if the cropbox (representing the visible page part) is smaller. + +.. [#f6] If you need to also see annotations or fields in the target page, you can try and convert the source PDF to another PDF using :meth:`Document.convert_to_pdf`. The underlying MuPDF function of that method will convert these objects to normal page content. Then use :meth:`Page.show_pdf_page` with the converted PDF page. + +.. [#f7] In PDF, an area enclosed by some lines or curves can have a property called "orientation". This is significant for switching on or off the fill color of that area when there exist multiple area overlaps - see discussion in method :meth:`Shape.finish` using the "non-zero winding number" rule. While orientation of curves, quads, triangles and other shapes enclosed by lines always was detectable, this has been impossible for "re" (rectangle) items in the past. Adding the orientation parameter now delivers the missing information. + +.. [#f8] Hyphenation detection simply means that if the last character of a line is "-", it will be assumed to be a continuation character. That character will not be found by text searching with its default flag setting. Please take note, that a MuPDF *line* may not always be what you expect: words separated by overly large gaps (e.g. caused by text justification) may constitute separate MuPDF lines. If then any of these words ends with a hyphen, it will only be found by text searching if hyphenation is switched off. + +.. include:: footer.rst diff --git a/docs/pixmap.rst b/docs/pixmap.rst new file mode 100644 index 0000000..6ed9f39 --- /dev/null +++ b/docs/pixmap.rst @@ -0,0 +1,664 @@ +.. include:: header.rst + +.. _Pixmap: + +================ +Pixmap +================ + +Pixmaps ("pixel maps") are objects at the heart of MuPDF's rendering capabilities. They represent plane rectangular sets of pixels. Each pixel is described by a number of bytes ("components") defining its color, plus an optional alpha byte defining its transparency. + +In PyMuPDF, there exist several ways to create a pixmap. Except the first one, all of them are available as overloaded constructors. A pixmap can be created ... + +1. from a document page (method :meth:`Page.get_pixmap`) +2. empty, based on :ref:`Colorspace` and :ref:`IRect` information +3. from a file +4. from an in-memory image +5. from a memory area of plain pixels +6. from an image inside a PDF document +7. as a copy of another pixmap + +.. note:: A number of image formats is supported as input for points 3. and 4. above. See section :ref:`ImageFiles`. + +Have a look at the :ref:`FAQ` section to see some pixmap usage "at work". + +================================ =================================================== +**Method / Attribute** **Short Description** +================================ =================================================== +:meth:`Pixmap.clear_with` clear parts of the pixmap +:meth:`Pixmap.color_count` determine used colors +:meth:`Pixmap.color_topusage` determine share of most used color +:meth:`Pixmap.copy` copy parts of another pixmap +:meth:`Pixmap.gamma_with` apply a gamma factor to the pixmap +:meth:`Pixmap.invert_irect` invert the pixels of a given area +:meth:`Pixmap.pdfocr_save` save the pixmap as an OCRed 1-page PDF +:meth:`Pixmap.pdfocr_tobytes` save the pixmap as an OCRed 1-page PDF +:meth:`Pixmap.pil_save` save as image using pillow +:meth:`Pixmap.pil_tobytes` write to `bytes` object using pillow +:meth:`Pixmap.pixel` return the value of a pixel +:meth:`Pixmap.save` save the pixmap in a variety of formats +:meth:`Pixmap.set_alpha` set alpha values +:meth:`Pixmap.set_dpi` set the image resolution +:meth:`Pixmap.set_origin` set pixmap x,y values +:meth:`Pixmap.set_pixel` set color and alpha of a pixel +:meth:`Pixmap.set_rect` set color and alpha of all pixels in a rectangle +:meth:`Pixmap.shrink` reduce size keeping proportions +:meth:`Pixmap.tint_with` tint the pixmap +:meth:`Pixmap.tobytes` return a memory area in a variety of formats +:meth:`Pixmap.warp` return a pixmap made from a quad inside +:attr:`Pixmap.alpha` transparency indicator +:attr:`Pixmap.colorspace` pixmap's :ref:`Colorspace` +:attr:`Pixmap.digest` MD5 hashcode of the pixmap +:attr:`Pixmap.height` pixmap height +:attr:`Pixmap.interpolate` interpolation method indicator +:attr:`Pixmap.is_monochrome` check if only black and white occur +:attr:`Pixmap.is_unicolor` check if only one color occurs +:attr:`Pixmap.irect` :ref:`IRect` of the pixmap +:attr:`Pixmap.n` bytes per pixel +:attr:`Pixmap.samples_mv` `memoryview` of pixel area +:attr:`Pixmap.samples_ptr` Python pointer to pixel area +:attr:`Pixmap.samples` `bytes` copy of pixel area +:attr:`Pixmap.size` pixmap's total length +:attr:`Pixmap.stride` size of one image row +:attr:`Pixmap.width` pixmap width +:attr:`Pixmap.x` X-coordinate of top-left corner +:attr:`Pixmap.xres` resolution in X-direction +:attr:`Pixmap.y` Y-coordinate of top-left corner +:attr:`Pixmap.yres` resolution in Y-direction +================================ =================================================== + +**Class API** + +.. class:: Pixmap + + .. method:: __init__(self, colorspace, irect, alpha) + + **New empty pixmap:** Create an empty pixmap of size and origin given by the rectangle. So, *irect.top_left* designates the top left corner of the pixmap, and its width and height are *irect.width* resp. *irect.height*. Note that the image area is **not initialized** and will contain crap data -- use eg. :meth:`clear_with` or :meth:`set_rect` to be sure. + + :arg colorspace: colorspace. + :type colorspace: :ref:`Colorspace` + + :arg irect_like irect: The pixmap's position and dimension. + + :arg bool alpha: Specifies whether transparency bytes should be included. Default is *False*. + + .. method:: __init__(self, colorspace, source) + + **Copy and set colorspace:** Copy *source* pixmap converting colorspace. Any colorspace combination is possible, but source colorspace must not be *None*. + + :arg colorspace: desired **target** colorspace. This **may also be** *None*. In this case, a "masking" pixmap is created: its :attr:`Pixmap.samples` will consist of the source's alpha bytes only. + :type colorspace: :ref:`Colorspace` + + :arg source: the source pixmap. + :type source: *Pixmap* + + .. method:: __init__(self, source, mask) + + * New in v1.18.18 + + **Copy and add image mask:** Copy *source* pixmap, add an alpha channel with transparency data from a mask pixmap. + + :arg source: pixmap without alpha channel. + :type source: :ref:`Pixmap` + + :arg mask: a mask pixmap. Must be a graysale pixmap. + :type mask: :ref:`Pixmap` + + .. method:: __init__(self, source, width, height, [clip]) + + **Copy and scale:** Copy *source* pixmap, scaling new width and height values -- the image will appear stretched or shrunk accordingly. Supports partial copying. The source colorspace may be *None*. + + :arg source: the source pixmap. + :type source: *Pixmap* + + :arg float width: desired target width. + + :arg float height: desired target height. + + :arg irect_like clip: restrict the resulting pixmap to this region of the **scaled** pixmap. + + .. note:: If width or height do not *represent* integers (i.e. `value.is_integer() != True`), then the resulting pixmap **will have an alpha channel**. + + .. method:: __init__(self, source, alpha=1) + + **Copy and add or drop alpha:** Copy *source* and add or drop its alpha channel. Identical copy if *alpha* equals *source.alpha*. If an alpha channel is added, its values will be set to 255. + + :arg source: source pixmap. + :type source: *Pixmap* + + :arg bool alpha: whether the target will have an alpha channel, default and mandatory if source colorspace is *None*. + + .. note:: A typical use includes separation of color and transparency bytes in separate pixmaps. Some applications require this like e.g. *wx.Bitmap.FromBufferAndAlpha()* of *wxPython*: + + >>> # 'pix' is an RGBA pixmap + >>> pixcolors = fitz.Pixmap(pix, 0) # extract the RGB part (drop alpha) + >>> pixalpha = fitz.Pixmap(None, pix) # extract the alpha part + >>> bm = wx.Bitmap.FromBufferAndAlpha(pix.width, pix.height, pixcolors.samples, pixalpha.samples) + + + .. method:: __init__(self, filename) + + **From a file:** Create a pixmap from *filename*. All properties are inferred from the input. The origin of the resulting pixmap is *(0, 0)*. + + :arg str filename: Path of the image file. + + .. method:: __init__(self, stream) + + **From memory:** Create a pixmap from a memory area. All properties are inferred from the input. The origin of the resulting pixmap is *(0, 0)*. + + :arg bytes,bytearray,BytesIO stream: Data containing a complete, valid image. Could have been created by e.g. *stream = bytearray(open('image.file', 'rb').read())*. Type *bytes* is supported in **Python 3 only**, because *bytes == str* in Python 2 and the method will interpret the stream as a filename. + + *Changed in version 1.14.13:* *io.BytesIO* is now also supported. + + + .. method:: __init__(self, colorspace, width, height, samples, alpha) + + **From plain pixels:** Create a pixmap from *samples*. Each pixel must be represented by a number of bytes as controlled by the *colorspace* and *alpha* parameters. The origin of the resulting pixmap is *(0, 0)*. This method is useful when raw image data are provided by some other program -- see :ref:`FAQ`. + + :arg colorspace: Colorspace of image. + :type colorspace: :ref:`Colorspace` + + :arg int width: image width + + :arg int height: image height + + :arg bytes,bytearray,BytesIO samples: an area containing all pixels of the image. Must include alpha values if specified. + + *Changed in version 1.14.13:* (1) *io.BytesIO* can now also be used. (2) Data are now **copied** to the pixmap, so may safely be deleted or become unavailable. + + :arg bool alpha: whether a transparency channel is included. + + .. note:: + + 1. The following equation **must be true**: *(colorspace.n + alpha) * width * height == len(samples)*. + 2. Starting with version 1.14.13, the samples data are **copied** to the pixmap. + + + .. method:: __init__(self, doc, xref) + + **From a PDF image:** Create a pixmap from an image **contained in PDF** *doc* identified by its :data:`xref`. All pimap properties are set by the image. Have a look at `extract-img1.py `_ and `extract-img2.py `_ to see how this can be used to recover all of a PDF's images. + + :arg doc: an opened **PDF** document. + :type doc: :ref:`Document` + + :arg int xref: the :data:`xref` of an image object. For example, you can make a list of images used on a particular page with :meth:`Document.get_page_images`, which also shows the :data:`xref` numbers of each image. + + .. method:: clear_with([value [, irect]]) + + Initialize the samples area. + + :arg int value: if specified, values from 0 to 255 are valid. Each color byte of each pixel will be set to this value, while alpha will be set to 255 (non-transparent) if present. If omitted, then all bytes (including any alpha) are cleared to *0x00*. + + :arg irect_like irect: the area to be cleared. Omit to clear the whole pixmap. Can only be specified, if *value* is also specified. + + .. method:: tint_with(black, white) + + Colorize a pixmap by replacing black and / or white with colors given as **sRGB integer** values. Only colorspaces :data:`CS_GRAY` and :data:`CS_RGB` are supported, others are ignored with a warning. + + If the colorspace is :data:`CS_GRAY`, the average *(red + green + blue)/3* will be taken. The pixmap will be changed in place. + + :arg int black: replace black with this value. Specifying 0x000000 makes no changes. + :arg int white: replace white with this value. Specifying 0xFFFFFF makes no changes. + + Examples: + + * `tint_with(0x000000, 0xFFFFFF)` is a no-op. + * `tint_with(0x00FF00, 0xFFFFFF)` changes black to green, leaves white intact. + * `tint_with(0xFF0000, 0x0000FF)` changes black to red and white to blue. + + + .. method:: gamma_with(gamma) + + Apply a gamma factor to a pixmap, i.e. lighten or darken it. Pixmaps with colorspace *None* are ignored with a warning. + + :arg float gamma: *gamma = 1.0* does nothing, *gamma < 1.0* lightens, *gamma > 1.0* darkens the image. + + .. method:: shrink(n) + + Shrink the pixmap by dividing both, its width and height by 2\ :sup:`n`. + + :arg int n: determines the new pixmap (samples) size. For example, a value of 2 divides width and height by 4 and thus results in a size of one 16\ :sup:`th` of the original. Values less than 1 are ignored with a warning. + + .. note:: Use this methods to reduce a pixmap's size retaining its proportion. The pixmap is changed "in place". If you want to keep original and also have more granular choices, use the resp. copy constructor above. + + .. method:: pixel(x, y) + + *New in version:: 1.14.5:* Return the value of the pixel at location (x, y) (column, line). + + :arg int x: the column number of the pixel. Must be in `range(pix.width)`. + :arg int y: the line number of the pixel, Must be in `range(pix.height)`. + + :rtype: list + :returns: a list of color values and, potentially the alpha value. Its length and content depend on the pixmap's colorspace and the presence of an alpha. For RGBA pixmaps the result would e.g. be *[r, g, b, a]*. All items are integers in `range(256)`. + + .. method:: set_pixel(x, y, color) + + *New in version 1.14.7:* Manipulate the pixel at location (x, y) (column, line). + + :arg int x: the column number of the pixel. Must be in `range(pix.width)`. + :arg int y: the line number of the pixel. Must be in `range(pix.height)`. + :arg sequence color: the desired pixel value given as a sequence of integers in `range(256)`. The length of the sequence must equal :attr:`Pixmap.n`, which includes any alpha byte. + + .. method:: set_rect(irect, color) + + *New in version 1.14.8:* Set the pixels of a rectangle to a value. + + :arg irect_like irect: the rectangle to be filled with the value. The actual area is the intersection of this parameter and :attr:`Pixmap.irect`. For an empty intersection (or an invalid parameter), no change will happen. + :arg sequence color: the desired value, given as a sequence of integers in `range(256)`. The length of the sequence must equal :attr:`Pixmap.n`, which includes any alpha byte. + + :rtype: bool + :returns: *False* if the rectangle was invalid or had an empty intersection with :attr:`Pixmap.irect`, else *True*. + + .. note:: + + 1. This method is equivalent to :meth:`Pixmap.set_pixel` executed for each pixel in the rectangle, but is obviously **very much faster** if many pixels are involved. + 2. This method can be used similar to :meth:`Pixmap.clear_with` to initialize a pixmap with a certain color like this: *pix.set_rect(pix.irect, (255, 255, 0))* (RGB example, colors the complete pixmap with yellow). + + .. method:: set_origin(x, y) + + * New in v1.17.7 + + Set the x and y values of the pixmap's top-left point. + + :arg int x: x coordinate + :arg int y: y coordinate + + + .. method:: set_dpi(xres, yres) + + * New in v1.16.17 + + * Changed in v1.18.0: When saving as a PNG image, these values will be stored now. + + Set the resolution (dpi) in x and y direction. + + :arg int xres: resolution in x direction. + :arg int yres: resolution in y direction. + + + .. method:: set_alpha(alphavalues, premultiply=1, opaque=None) + + * Changed in v 1.18.13 + + Change the alpha values. The pixmap must have an alpha channel. + + :arg bytes,bytearray,BytesIO alphavalues: the new alpha values. If provided, its length must be at least *width * height*. If omitted (`None`), all alpha values are set to 255 (no transparency). *Changed in version 1.14.13:* *io.BytesIO* is now also accepted. + :arg bool premultiply: *New in v1.18.13:* whether to premultiply color components with the alpha value. + :arg list,tuple opaque: ignore the alpha value and set this color to fully transparent. A sequence of integers in `range(256)` with a length of :attr:`Pixmap.n`. Default is *None*. For example, a typical choice for RGB would be `opaque=(255, 255, 255)` (white). + + + .. method:: invert_irect([irect]) + + Invert the color of all pixels in :ref:`IRect` *irect*. Will have no effect if colorspace is *None*. + + :arg irect_like irect: The area to be inverted. Omit to invert everything. + + .. method:: copy(source, irect) + + Copy the *irect* part of the *source* pixmap into the corresponding area of this one. The two pixmaps may have different dimensions and can each have :data:`CS_GRAY` or :data:`CS_RGB` colorspaces, but they currently **must** have the same alpha property [#f2]_. The copy mechanism automatically adjusts discrepancies between source and target like so: + + If copying from :data:`CS_GRAY` to :data:`CS_RGB`, the source gray-shade value will be put into each of the three rgb component bytes. If the other way round, *(r + g + b) / 3* will be taken as the gray-shade value of the target. + + Between *irect* and the target pixmap's rectangle, an "intersection" is calculated at first. This takes into account the rectangle coordinates and the current attribute values :attr:`Pixmap.x` and :attr:`Pixmap.y` (which you are free to modify for this purpose via :meth:`Pixmap.set_origin`). Then the corresponding data of this intersection are copied. If the intersection is empty, nothing will happen. + + :arg source: source pixmap. + :type source: :ref:`Pixmap` + + :arg irect_like irect: The area to be copied. + + .. note:: Example: Suppose you have two pixmaps, `pix1` and `pix2` and you want to copy the lower right quarter of `pix2` to `pix1` such that it starts at the top-left point of `pix1`. Use the following snippet:: + + >>> # safeguard: set top-left of pix1 and pix2 to (0, 0) + >>> pix1.set_origin(0, 0) + >>> pix2.set_origin(0, 0) + >>> # compute top-left coordinates of pix2 region to copy + >>> x1 = int(pix2.width / 2) + >>> y1 = int(pix2.height / 2) + >>> # shift top-left of pix2 such, that the to-be-copied + >>> # area starts at (0, 0): + >>> pix2.set_origin(-x1, -y1) + >>> # now copy ... + >>> pix1.copy(pix2, (0, 0, x1, y1)) + + .. image:: images/img-pixmapcopy.* + :scale: 20 + + .. method:: save(filename, output=None, jpg_quality=95) + + * Changed in v1.22.0: Added **direct support of JPEG** images. Image quality can be controlled via parameter "jpg_quality". + + Save pixmap as an image file. Depending on the output chosen, only some or all colorspaces are supported and different file extensions can be chosen. Please see the table below. + + :arg str,Path,file filename: The file to save to. May be provided as a string, as a ``pathlib.Path`` or as a Python file object. In the latter two cases, the filename is taken from the resp. object. The filename's extension determines the image format, which can be overruled by the output parameter. + + :arg str output: The desired image format. The default is the filename's extension. If both, this value and the file extension are unsupported, an exception is raised. For possible values see :ref:`PixmapOutput`. + :arg int jpg_quality: The desired image quality, default 95. Only applies to JPEG images, else ignored. This parameter trades quality against file size. A value of 98 is close to lossless. Higher values should not lead to better quality. + + :raises ValueError: For unsupported image formats. + + .. method:: tobytes(output="png", jpg_quality=95) + + * New in version 1.14.5: Return the pixmap as a *bytes* memory object of the specified format -- similar to :meth:`save`. + * Changed in v1.22.0: Added **direct JPEG support**. Image quality can be influenced via new parameter "jpg_quality". + + :arg str output: The desired image format. The default is "png". For possible values see :ref:`PixmapOutput`. + :arg int jpg_quality: The desired image quality, default 95. Only applies to JPEG images, else ignored. This parameter trades quality against file size. A value of 98 is close to lossless. Higher values should not lead to better quality. + + :raises ValueError: For unsupported image formats. + :rtype: bytes + + :arg str output: The requested image format. The default is "png". For other possible values see :ref:`PixmapOutput`. + + .. method:: pdfocr_save(filename, compress=True, language="eng", tessdata=None) + + * New in v1.19.0 + + * Changed in v1.22.4: Support of new parameter for Tesseract's tessdata. + + Perform text recognition using Tesseract and save the image as a 1-page PDF with an OCR text layer. + + :arg str,fp filename: identifies the file to save to. May be either a string or a pointer to a file opened with "wb" (includes `io.BytesIO()` objects). + :arg bool compress: whether to compress the resulting PDF, default is `True`. + :arg str language: the languages occurring in the image. This must be specified in Tesseract format. Default is "eng" for English. Use "+"-separated Tesseract language codes for multiple languages, like "eng+spa" for English and Spanish. + : arg str tessdata: folder name of Tesseract's language support. If omitted, this information must be present as environment variable `TESSDATA_PREFIX`. + + .. note:: **Will fail** if Tesseract is not installed or if the environment variable "TESSDATA_PREFIX" is not set to the `tessdata` folder name and not provided as parameter. + + .. method:: pdfocr_tobytes(compress=True, language="eng", tessdata=None) + + * New in v1.19.0 + + * Changed in v1.22.4: Support of new parameter for Tesseract's tessdata. + + Perform text recognition using Tesseract and convert the image to a 1-page PDF with an OCR text layer. Internally invokes :meth:`Pixmap.pdfocr_save`. + + :returns: A 1-page PDF file in memory. Could be opened like `doc=fitz.open("pdf", pix.pdfocr_tobytes())`, and text extractions could be performed on its `page=doc[0]`. + + .. note:: + + Another possible use is insertion into some pdf. The following snippet reads the images of a folder and stores them as pages in a new PDF that contain an OCR text layer:: + + doc = fitz.open() + for imgfile in os.listdir(folder): + pix = fitz.Pixmap(imgfile) + imgpdf = fitz.open("pdf", pix.pdfocr_tobytes()) + doc.insert_pdf(imgpdf) + pix = None + imgpdf.close() + doc.save("ocr-images.pdf") + + + .. method:: pil_save(*args, **kwargs) + + * New in v1.17.3 + + Write the pixmap as an image file using Pillow. Use this method for output unsupported by MuPDF. Examples are + + * Formats JPX, J2K, WebP, etc. + * Storing EXIF information. + * If you do not provide dpi information, the values *xres*, *yres* stored with the pixmap are automatically used. + + A simple example: `pix.pil_save("some.webp", optimize=True, dpi=(150, 150))`. For details on other parameters see the Pillow documentation. + + Since v1.22.0, PyMuPDF supports JPEG output directly. For both, performance reasons and for reducing external dependencies, the use of this method is no longer recommended when outputting JPEG images. + + :raises ImportError: if Pillow is not installed. + + .. method:: pil_tobytes(*args, **kwargs) + + * New in v1.17.3 + + Return an image as a bytes object in the specified format using Pillow. For example `stream = pix.pil_tobytes(format="WEBP", optimize=True)`. Also see above. For details on other parameters see the Pillow documentation. + + .raises ImportError: if Pillow is not installed. + + :rtype: bytes + + + .. method:: warp(quad, width, height) + + * New in v1.19.3 + + Return a new pixmap by "warping" the quad such that the quad corners become the new pixmap's corners. The target pixmap's `irect` will be `(0, 0, width, height)`. + + :arg quad_like quad: a convex quad with coordinates inside :attr:`Pixmap.irect` (including the border points). + :arg int width: desired resulting width. + :arg int height: desired resulting height. + :returns: A new pixmap where the quad corners are mapped to the pixmap corners in a clockwise fashion: `quad.ul -> irect.tl`, `quad.ur -> irect.tr`, etc. + :rtype: :ref:`Pixmap` + + .. image:: images/img-warp.* + :scale: 40 + :align: center + + + .. method:: color_count(colors=False, clip=None) + + * New in v1.19.2 + * Changed in v1.19.3 + + Determine the pixmap's unique colors and their count. + + :arg bool colors: *(changed in v1.19.3)* If `True` return a dictionary of color pixels and their usage count, else just the number of unique colors. + :arg rect_like clip: a rectangle inside :attr:`Pixmap.irect`. If provided, only those pixels are considered. This allows inspecting sub-rectangles of a given pixmap directly -- instead of building sub-pixmaps. + :rtype: dict or int + :returns: either the number of colors, or a dictionary with the items `pixel: count`. The pixel key is a `bytes` object of length :attr:`Pixmap.n`. + + .. note:: To recover the **tuple** of a pixel, use `tuple(colors.keys()[i])` for the i-th item. + + * The response time depends on the pixmap's samples size and may be more than a second for very large pixmaps. + * Where applicable, pixels with different alpha values will be treated as different colors. + + + .. method:: color_topusage(clip=None) + + * New in v1.19.3 + + Return the most frequently used color and its relative frequency. + + :arg rect_like clip: A rectangle inside :attr:`Pixmap.irect`. If provided, only those pixels are considered. This allows inspecting sub-rectangles of a given pixmap directly -- instead of building sub-pixmaps. + :rtype: tuple + :returns: A tuple `(ratio, pixel)` where `0 < ratio <= 1` and *pixel* is the pixel value of the color. Use this to decide if the image is "almost" unicolor: a response `(0.95, b"\x00\x00\x00")` means that 95% of all pixels are black. See an example here :ref:`RecipesImages_P`. + + + .. attribute:: alpha + + Indicates whether the pixmap contains transparency information. + + :type: bool + + .. attribute:: digest + + The MD5 hashcode (16 bytes) of the pixmap. This is a technical value used for unique identifications. + + :type: bytes + + .. attribute:: colorspace + + The colorspace of the pixmap. This value may be *None* if the image is to be treated as a so-called *image mask* or *stencil mask* (currently happens for extracted PDF document images only). + + :type: :ref:`Colorspace` + + .. attribute:: stride + + Contains the length of one row of image data in :attr:`Pixmap.samples`. This is primarily used for calculation purposes. The following expressions are true: + + * `len(samples) == height * stride` + * `width * n == stride` + + :type: int + + + .. attribute:: is_monochrome + + * New in v1.19.2 + + Is `True` for a gray pixmap which only has the colors black and white. + + :type: bool + + + .. attribute:: is_unicolor + + * New in v1.19.2 + + Is `True` if all pixels are identical (any colorspace). Where applicable, pixels with different alpha values will be treated as different colors. + + :type: bool + + + .. attribute:: irect + + Contains the :ref:`IRect` of the pixmap. + + :type: :ref:`IRect` + + .. attribute:: samples + + The color and (if :attr:`Pixmap.alpha` is true) transparency values for all pixels. It is an area of `width * height * n` bytes. Each n bytes define one pixel. Each successive n bytes yield another pixel in scanline order. Subsequent scanlines follow each other with no padding. E.g. for an RGBA colorspace this means, *samples* is a sequence of bytes like *..., R, G, B, A, ...*, and the four byte values R, G, B, A define one pixel. + + This area can be passed to other graphics libraries like PIL (Python Imaging Library) to do additional processing like saving the pixmap in other image formats. + + .. note:: + * The underlying data is typically a **large** memory area, from which a `bytes` copy is made for this attribute ... each time you access it: for example an RGB-rendered letter page has a samples size of almost 1.4 MB. So consider assigning a new variable to it or use the `memoryview` version :attr:`Pixmap.samples_mv` (new in v1.18.17). + * Any changes to the underlying data are available only after accessing this attribute again. This is different from using the memoryview version. + + :type: bytes + + .. attribute:: samples_mv + + * New in v1.18.17 + + Like :attr:`Pixmap.samples`, but in Python `memoryview` format. It is built pointing to the memory in the pixmap -- not from a copy of it. So its creation speed is independent from the pixmap size, and any changes to pixels will be available immediately. + + Copies like `bytearray(pix.samples_mv)`, or `bytes(pixmap.samples_mv)` are equivalent to and can be used in place of `pix.samples`. + + We also have `len(pix.samples) == len(pix.samples_mv)`. + + Look at this example from a 2 MB JPEG: the memoryview is **ten thousand times faster**:: + + In [3]: %timeit len(pix.samples_mv) + 367 ns ± 1.75 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each) + In [4]: %timeit len(pix.samples) + 3.52 ms ± 57.5 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) + + :type: memoryview + + .. attribute:: samples_ptr + + * New in v1.18.17 + + Python pointer to the pixel area. This is a special integer format, which can be used by supporting applications (such as PyQt) to directly address the samples area and thus build their images extremely fast. For example:: + + img = QtGui.QImage(pix.samples, pix.width, pix.height, format) # (1) + img = QtGui.QImage(pix.samples_ptr, pix.width, pix.height, format) # (2) + + Both of the above lead to the same Qt image, but (2) can be **many hundred times faster**, because it avoids an additional copy of the pixel area. + + :type: int + + .. attribute:: size + + Contains *len(pixmap)*. This will generally equal *len(pix.samples)* plus some platform-specific value for defining other attributes of the object. + + :type: int + + .. attribute:: width + + .. attribute:: w + + Width of the region in pixels. + + :type: int + + .. attribute:: height + + .. attribute:: h + + Height of the region in pixels. + + :type: int + + .. attribute:: x + + X-coordinate of top-left corner in pixels. Cannot directly be changed -- use :meth:`Pixmap.set_origin`. + + :type: int + + .. attribute:: y + + Y-coordinate of top-left corner in pixels. Cannot directly be changed -- use :meth:`Pixmap.set_origin`. + + :type: int + + .. attribute:: n + + Number of components per pixel. This number depends on colorspace and alpha. If colorspace is not *None* (stencil masks), then *Pixmap.n - Pixmap.aslpha == pixmap.colorspace.n* is true. If colorspace is *None*, then *n == alpha == 1*. + + :type: int + + .. attribute:: xres + + Horizontal resolution in dpi (dots per inch). Please also see :data:`resolution`. Cannot directly be changed -- use :meth:`Pixmap.set_dpi`. + + :type: int + + .. attribute:: yres + + Vertical resolution in dpi (dots per inch). Please also see :data:`resolution`. Cannot directly be changed -- use :meth:`Pixmap.set_dpi`. + + :type: int + + .. attribute:: interpolate + + An information-only boolean flag set to *True* if the image will be drawn using "linear interpolation". If *False* "nearest neighbour sampling" will be used. + + :type: bool + +.. _ImageFiles: + +Supported Input Image Formats +----------------------------------------------- +The following file types are supported as **input** to construct pixmaps: **BMP, JPEG, GIF, TIFF, JXR, JPX**, **PNG**, **PAM** and all of the **Portable Anymap** family (**PBM, PGM, PNM, PPM**). This support is two-fold: + +1. Directly create a pixmap with *Pixmap(filename)* or *Pixmap(byterray)*. The pixmap will then have properties as determined by the image. + +2. Open such files with *fitz.open(...)*. The result will then appear as a document containing one single page. Creating a pixmap of this page offers all the options available in this context: apply a matrix, choose colorspace and alpha, confine the pixmap to a clip area, etc. + +**SVG images** are only supported via method 2 above, not directly as pixmaps. But remember: the result of this is a **raster image** as is always the case with pixmaps [#f1]_. + +.. _PixmapOutput: + +Supported Output Image Formats +--------------------------------------------------------------------------- +A number of image **output** formats are supported. You have the option to either write an image directly to a file (:meth:`Pixmap.save`), or to generate a bytes object (:meth:`Pixmap.tobytes`). Both methods accept a string identifying the desired format (**Format** column below). Please note that not all combinations of pixmap colorspace, transparency support (alpha) and image format are possible. + +========== =============== ========= ============== ================================= +**Format** **Colorspaces** **alpha** **Extensions** **Description** +========== =============== ========= ============== ================================= +jpg, jpeg gray, rgb, cmyk no .jpg, .jpeg Joint Photographic Experts Group +pam gray, rgb, cmyk yes .pam Portable Arbitrary Map +pbm gray, rgb no .pbm Portable Bitmap +pgm gray, rgb no .pgm Portable Graymap +png gray, rgb yes .png Portable Network Graphics +pnm gray, rgb no .pnm Portable Anymap +ppm gray, rgb no .ppm Portable Pixmap +ps gray, rgb, cmyk no .ps Adobe PostScript Image +psd gray, rgb, cmyk yes .psd Adobe Photoshop Document +========== =============== ========= ============== ================================= + +.. note:: + * Not all image file types are supported (or at least common) on all OS platforms. E.g. PAM and the Portable Anymap formats are rare or even unknown on Windows. + * Especially pertaining to CMYK colorspaces, you can always convert a CMYK pixmap to an RGB pixmap with *rgb_pix = fitz.Pixmap(fitz.csRGB, cmyk_pix)* and then save that in the desired format. + * As can be seen, MuPDF's image support range is different for input and output. Among those supported both ways, PNG and JPEG are probably the most popular. + * We also recommend using "ppm" formats as input to tkinter's *PhotoImage* method like this: *tkimg = tkinter.PhotoImage(data=pix.tobytes("ppm"))* (also see the tutorial). This is **very** fast (**60 times** faster than PNG). + + + +.. rubric:: Footnotes + +.. [#f1] If you need a **vector image** from the SVG, you must first convert it to a PDF. Try :meth:`Document.convert_to_pdf`. If this is not good enough, look for other SVG-to-PDF conversion tools like the Python packages `svglib `_, `CairoSVG `_, `Uniconvertor `_ or the Java solution `Apache Batik `_. Have a look at our Wiki for more examples. + +.. [#f2] To also set the alpha property, add an additional step to this method by dropping or adding an alpha channel to the result. + +.. include:: footer.rst diff --git a/docs/point.rst b/docs/point.rst new file mode 100644 index 0000000..39ad508 --- /dev/null +++ b/docs/point.rst @@ -0,0 +1,105 @@ +.. include:: header.rst + +.. _Point: + +================ +Point +================ + +*Point* represents a point in the plane, defined by its x and y coordinates. + +============================ ============================================ +**Attribute / Method** **Description** +============================ ============================================ +:meth:`Point.distance_to` calculate distance to point or rect +:meth:`Point.norm` the Euclidean norm +:meth:`Point.transform` transform point with a matrix +:attr:`Point.abs_unit` same as unit, but positive coordinates +:attr:`Point.unit` point coordinates divided by *abs(point)* +:attr:`Point.x` the X-coordinate +:attr:`Point.y` the Y-coordinate +============================ ============================================ + +**Class API** + +.. class:: Point + + .. method:: __init__(self) + + .. method:: __init__(self, x, y) + + .. method:: __init__(self, point) + + .. method:: __init__(self, sequence) + + Overloaded constructors. + + Without parameters, *Point(0, 0)* will be created. + + With another point specified, a **new copy** will be created, "sequence" is a Python sequence of 2 numbers (see :ref:`SequenceTypes`). + + :arg float x: x coordinate of the point + + :arg float y: y coordinate of the point + + .. method:: distance_to(x [, unit]) + + Calculate the distance to *x*, which may be :data:`point_like` or :data:`rect_like`. The distance is given in units of either pixels (default), inches, centimeters or millimeters. + + :arg point_like,rect_like x: to which to compute the distance. + + :arg str unit: the unit to be measured in. One of "px", "in", "cm", "mm". + + :rtype: float + :returns: the distance to *x*. If this is :data:`rect_like`, then the distance + + * is the length of the shortest line connecting to one of the rectangle sides + * is calculated to the **finite version** of it + * is zero if it **contains** the point + + .. method:: norm() + + * New in version 1.16.0 + + Return the Euclidean norm (the length) of the point as a vector. Equals result of function *abs()*. + + .. method:: transform(m) + + Apply a matrix to the point and replace it with the result. + + :arg matrix_like m: The matrix to be applied. + + :rtype: :ref:`Point` + + .. attribute:: unit + + Result of dividing each coordinate by *norm(point)*, the distance of the point to (0,0). This is a vector of length 1 pointing in the same direction as the point does. Its x, resp. y values are equal to the cosine, resp. sine of the angle this vector (and the point itself) has with the x axis. + + .. image:: images/img-point-unit.* + + :type: :ref:`Point` + + .. attribute:: abs_unit + + Same as :attr:`unit` above, replacing the coordinates with their absolute values. + + :type: :ref:`Point` + + .. attribute:: x + + The x coordinate + + :type: float + + .. attribute:: y + + The y coordinate + + :type: float + +.. note:: + + * This class adheres to the Python sequence protocol, so components can be accessed via their index, too. Also refer to :ref:`SequenceTypes`. + * Rectangles can be used with arithmetic operators -- see chapter :ref:`Algebra`. + +.. include:: footer.rst diff --git a/docs/quad.rst b/docs/quad.rst new file mode 100644 index 0000000..3a19674 --- /dev/null +++ b/docs/quad.rst @@ -0,0 +1,160 @@ +.. include:: header.rst + +.. _Quad: + +========== +Quad +========== + +Represents a four-sided mathematical shape (also called "quadrilateral" or "tetragon") in the plane, defined as a sequence of four :ref:`Point` objects ul, ur, ll, lr (conveniently called upper left, upper right, lower left, lower right). + +Quads can **be obtained** as results of text search methods (:meth:`Page.search_for`), and they **are used** to define text marker annotations (see e.g. :meth:`Page.add_squiggly_annot` and friends), and in several draw methods (like :meth:`Page.draw_quad` / :meth:`Shape.draw_quad`, :meth:`Page.draw_oval`/ :meth:`Shape.draw_quad`). + +.. note:: + + * If the corners of a rectangle are transformed with a **rotation**, **scale** or **translation** :ref:`Matrix`, then the resulting quad is **rectangular** (= congruent to a rectangle), i.e. all of its corners again enclose angles of 90 degrees. Property :attr:`Quad.is_rectangular` checks whether a quad can be thought of being the result of such an operation. + + * This is not true for all matrices: e.g. shear matrices produce parallelograms, and non-invertible matrices deliver "degenerate" tetragons like triangles or lines. + + * Attribute :attr:`Quad.rect` obtains the enveloping rectangle. Vice versa, rectangles now have attributes :attr:`Rect.quad`, resp. :attr:`IRect.quad` to obtain their respective tetragon versions. + + +============================= ======================================================= +**Methods / Attributes** **Short Description** +============================= ======================================================= +:meth:`Quad.transform` transform with a matrix +:meth:`Quad.morph` transform with a point and matrix +:attr:`Quad.ul` upper left point +:attr:`Quad.ur` upper right point +:attr:`Quad.ll` lower left point +:attr:`Quad.lr` lower right point +:attr:`Quad.is_convex` true if quad is a convex set +:attr:`Quad.is_empty` true if quad is an empty set +:attr:`Quad.is_rectangular` true if quad is congruent to a rectangle +:attr:`Quad.rect` smallest containing :ref:`Rect` +:attr:`Quad.width` the longest width value +:attr:`Quad.height` the longest height value +============================= ======================================================= + +**Class API** + +.. class:: Quad + + .. method:: __init__(self) + + .. method:: __init__(self, ul, ur, ll, lr) + + .. method:: __init__(self, quad) + + .. method:: __init__(self, sequence) + + Overloaded constructors: "ul", "ur", "ll", "lr" stand for :data:`point_like` objects (the four corners), "sequence" is a Python sequence with four :data:`point_like` objects. + + If "quad" is specified, the constructor creates a **new copy** of it. + + Without parameters, a quad consisting of 4 copies of *Point(0, 0)* is created. + + + .. method:: transform(matrix) + + Modify the quadrilateral by transforming each of its corners with a matrix. + + :arg matrix_like matrix: the matrix. + + .. method:: morph(fixpoint, matrix) + + *(New in version 1.17.0)* "Morph" the quad with a matrix-like using a point-like as fixed point. + + :arg point_like fixpoint: the point. + :arg matrix_like matrix: the matrix. + :returns: a new quad (no operation if this is the infinite quad). + + + .. attribute:: rect + + The smallest rectangle containing the quad, represented by the blue area in the following picture. + + .. image:: images/img-quads.* + + :type: :ref:`Rect` + + .. attribute:: ul + + Upper left point. + + :type: :ref:`Point` + + .. attribute:: ur + + Upper right point. + + :type: :ref:`Point` + + .. attribute:: ll + + Lower left point. + + :type: :ref:`Point` + + .. attribute:: lr + + Lower right point. + + :type: :ref:`Point` + + .. attribute:: is_convex + + * New in version 1.16.1 + + Checks if for any two points of the quad, all points on their connecting line also belong to the quad. + + .. image:: images/img-convexity.* + :scale: 30 + + :type: bool + + .. attribute:: is_empty + + True if enclosed area is zero, which means that at least three of the four corners are on the same line. If this is false, the quad may still be degenerate or not look like a tetragon at all (triangles, parallelograms, trapezoids, ...). + + :type: bool + + .. attribute:: is_rectangular + + True if all corner angles are 90 degrees. This implies that the quad is **convex and not empty**. + + :type: bool + + .. attribute:: width + + The maximum length of the top and the bottom side. + + :type: float + + .. attribute:: height + + The maximum length of the left and the right side. + + :type: float + +Remark +------ +This class adheres to the sequence protocol, so components can be dealt with via their indices, too. Also refer to :ref:`SequenceTypes`. + +Algebra and Containment Checks +------------------------------- +Starting with v1.19.6, quads can be used in algebraic expressions like the other geometry object -- the respective restrictions have been lifted. In particular, all the following combinations of containment checking are now possible: + +`{Point | IRect | Rect | Quad} in {IRect | Rect | Quad}` + +Please note the following interesting detail: + +For a rectangle, only its top-left point belongs to it. Since v1.19.0, rectangles are defined to be "open", such that its bottom and its right edge do not belong to it -- including the respective corners. But for quads there exists no such notion like "openness", so we have the following somewhat surprising implication: + + >>> rect.br in rect + False + >>> # but: + >>> rect.br in rect.quad + True + +.. include:: footer.rst diff --git a/docs/recipes-annotations.rst b/docs/recipes-annotations.rst new file mode 100644 index 0000000..f1ef6cf --- /dev/null +++ b/docs/recipes-annotations.rst @@ -0,0 +1,162 @@ +.. include:: header.rst + +.. _RecipesAnnotations: + +============================== +Annotations +============================== + + +In v1.14.0, annotation handling has been considerably extended: + +* New annotation type support for 'Ink', 'Rubber Stamp' and 'Squiggly' annotations. Ink annots simulate handwriting by combining one or more lists of interconnected points. Stamps are intended to visually inform about a document's status or intended usage (like "draft", "confidential", etc.). 'Squiggly' is a text marker annot, which underlines selected text with a zig-zagged line. + +* Extended 'FreeText' support: + 1. all characters from the *Latin* character set are now available, + 2. colors of text, rectangle background and rectangle border can be independently set + 3. text in rectangle can be rotated by either +90 or -90 degrees + 4. text is automatically wrapped (made multi-line) in available rectangle + 5. all Base-14 fonts are now available (*normal* variants only, i.e. no bold, no italic). +* MuPDF now supports line end icons for 'Line' annots (only). PyMuPDF supported that in v1.13.x already -- and for (almost) the full range of applicable types. So we adjusted the appearance of 'Polygon' and 'PolyLine' annots to closely resemble the one of MuPDF for 'Line'. +* MuPDF now provides its own annotation icons where relevant. PyMuPDF switched to using them (for 'FileAttachment' and 'Text' ["sticky note"] so far). +* MuPDF now also supports 'Caret', 'Movie', 'Sound' and 'Signature' annotations, which we may include in PyMuPDF at some later time. + + +.. _RecipesAnnotations_A: + +How to Add and Modify Annotations +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In PyMuPDF, new annotations can be added via :ref:`Page` methods. Once an annotation exists, it can be modified to a large extent using methods of the :ref:`Annot` class. + +In contrast to many other tools, initial insert of annotations happens with a minimum number of properties. We leave it to the programmer to e.g. set attributes like author, creation date or subject. + +As an overview for these capabilities, look at the following script that fills a PDF page with most of the available annotations. Look in the next sections for more special situations: + +.. literalinclude:: samples/new-annots.py + :language: python + + +This script should lead to the following output: + +.. image:: images/img-annots.* + :scale: 80 + +------------------------------ + +.. _RecipesAnnotations_B: + +How to Use FreeText +~~~~~~~~~~~~~~~~~~~~~ +This script shows a couple of ways to deal with 'FreeText' annotations:: + + # -*- coding: utf-8 -*- + import fitz + + # some colors + blue = (0,0,1) + green = (0,1,0) + red = (1,0,0) + gold = (1,1,0) + + # a new PDF with 1 page + doc = fitz.open() + page = doc.new_page() + + # 3 rectangles, same size, above each other + r1 = fitz.Rect(100,100,200,150) + r2 = r1 + (0,75,0,75) + r3 = r2 + (0,75,0,75) + + # the text, Latin alphabet + t = "¡Un pequeño texto para practicar!" + + # add 3 annots, modify the last one somewhat + a1 = page.add_freetext_annot(r1, t, color=red) + a2 = page.add_freetext_annot(r2, t, fontname="Ti", color=blue) + a3 = page.add_freetext_annot(r3, t, fontname="Co", color=blue, rotate=90) + a3.set_border(width=0) + a3.update(fontsize=8, fill_color=gold) + + # save the PDF + doc.save("a-freetext.pdf") + +The result looks like this: + +.. image:: images/img-freetext.* + :scale: 80 + +------------------------------ + + + + +Using Buttons and JavaScript +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Since MuPDF v1.16, 'FreeText' annotations no longer support bold or italic versions of the Times-Roman, Helvetica or Courier fonts. + +A big **thank you** to our user `@kurokawaikki `_, who contributed the following script to **circumvent this restriction**. + +.. literalinclude:: samples/make-bold.py + :language: python + +-------------------------- + + +.. _RecipesAnnotations_C: + +How to Use Ink Annotations +~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Ink annotations are used to contain freehand scribbling. A typical example may be an image of your signature consisting of first name and last name. Technically an ink annotation is implemented as a **list of lists of points**. Each point list is regarded as a continuous line connecting the points. Different point lists represent independent line segments of the annotation. + +The following script creates an ink annotation with two mathematical curves (sine and cosine function graphs) as line segments:: + + import math + import fitz + + #------------------------------------------------------------------------------ + # preliminary stuff: create function value lists for sine and cosine + #------------------------------------------------------------------------------ + w360 = math.pi * 2 # go through full circle + deg = w360 / 360 # 1 degree as radians + rect = fitz.Rect(100,200, 300, 300) # use this rectangle + first_x = rect.x0 # x starts from left + first_y = rect.y0 + rect.height / 2. # rect middle means y = 0 + x_step = rect.width / 360 # rect width means 360 degrees + y_scale = rect.height / 2. # rect height means 2 + sin_points = [] # sine values go here + cos_points = [] # cosine values go here + for x in range(362): # now fill in the values + x_coord = x * x_step + first_x # current x coordinate + y = -math.sin(x * deg) # sine + p = (x_coord, y * y_scale + first_y) # corresponding point + sin_points.append(p) # append + y = -math.cos(x * deg) # cosine + p = (x_coord, y * y_scale + first_y) # corresponding point + cos_points.append(p) # append + + #------------------------------------------------------------------------------ + # create the document with one page + #------------------------------------------------------------------------------ + doc = fitz.open() # make new PDF + page = doc.new_page() # give it a page + + #------------------------------------------------------------------------------ + # add the Ink annotation, consisting of 2 curve segments + #------------------------------------------------------------------------------ + annot = page.addInkAnnot((sin_points, cos_points)) + # let it look a little nicer + annot.set_border(width=0.3, dashes=[1,]) # line thickness, some dashing + annot.set_colors(stroke=(0,0,1)) # make the lines blue + annot.update() # update the appearance + + page.draw_rect(rect, width=0.3) # only to demonstrate we did OK + + doc.save("a-inktest.pdf") + +This is the result: + +.. image:: images/img-inkannot.* + :scale: 50 + +.. include:: footer.rst diff --git a/docs/recipes-common-issues-and-their-solutions.rst b/docs/recipes-common-issues-and-their-solutions.rst new file mode 100644 index 0000000..616a982 --- /dev/null +++ b/docs/recipes-common-issues-and-their-solutions.rst @@ -0,0 +1,323 @@ +.. include:: header.rst + +.. _RecipesCommonIssuesAndTheirSolutions: + +========================================== +Common Issues and their Solutions +========================================== + +How To Dynamically Clean Up Corrupt :title:`PDFs` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This shows a potential use of :title:`PyMuPDF` with another Python PDF library (the excellent pure Python package `pdfrw `_ is used here as an example). + +If a clean, non-corrupt / decompressed PDF is needed, one could dynamically invoke PyMuPDF to recover from many problems like so:: + + import sys + from io import BytesIO + from pdfrw import PdfReader + import fitz + + #--------------------------------------- + # 'Tolerant' PDF reader + #--------------------------------------- + def reader(fname, password = None): + idata = open(fname, "rb").read() # read the PDF into memory and + ibuffer = BytesIO(idata) # convert to stream + if password is None: + try: + return PdfReader(ibuffer) # if this works: fine! + except: + pass + + # either we need a password or it is a problem-PDF + # create a repaired / decompressed / decrypted version + doc = fitz.open("pdf", ibuffer) + if password is not None: # decrypt if password provided + rc = doc.authenticate(password) + if not rc > 0: + raise ValueError("wrong password") + c = doc.tobytes(garbage=3, deflate=True) + del doc # close & delete doc + return PdfReader(BytesIO(c)) # let pdfrw retry + #--------------------------------------- + # Main program + #--------------------------------------- + pdf = reader("pymupdf.pdf", password = None) # include a password if necessary + print pdf.Info + # do further processing + +With the command line utility *pdftk* (`available `_ for Windows only, but reported to also run under `Wine `_) a similar result can be achieved, see `here `_. However, you must invoke it as a separate process via *subprocess.Popen*, using stdin and stdout as communication vehicles. + + + +How to Convert Any Document to :title:`PDF` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here is a script that converts any :title:`PyMuPDF` :ref:`supported document` to a :title:`PDF`. These include XPS, EPUB, FB2, CBZ and image formats, including multi-page TIFF images. + +It features maintaining any metadata, table of contents and links contained in the source document:: + + """ + Demo script: Convert input file to a PDF + ----------------------------------------- + Intended for multi-page input files like XPS, EPUB etc. + + Features: + --------- + Recovery of table of contents and links of input file. + While this works well for bookmarks (outlines, table of contents), + links will only work if they are not of type "LINK_NAMED". + This link type is skipped by the script. + + For XPS and EPUB input, internal links however **are** of type "LINK_NAMED". + Base library MuPDF does not resolve them to page numbers. + + So, for anyone expert enough to know the internal structure of these + document types, can further interpret and resolve these link types. + + Dependencies + -------------- + PyMuPDF v1.14.0+ + """ + import sys + import fitz + if not (list(map(int, fitz.VersionBind.split("."))) >= [1,14,0]): + raise SystemExit("need PyMuPDF v1.14.0+") + fn = sys.argv[1] + + print("Converting '%s' to '%s.pdf'" % (fn, fn)) + + doc = fitz.open(fn) + + b = doc.convert_to_pdf() # convert to pdf + pdf = fitz.open("pdf", b) # open as pdf + + toc= doc.het_toc() # table of contents of input + pdf.set_toc(toc) # simply set it for output + meta = doc.metadata # read and set metadata + if not meta["producer"]: + meta["producer"] = "PyMuPDF v" + fitz.VersionBind + + if not meta["creator"]: + meta["creator"] = "PyMuPDF PDF converter" + meta["modDate"] = fitz.get_pdf_now() + meta["creationDate"] = meta["modDate"] + pdf.set_metadata(meta) + + # now process the links + link_cnti = 0 + link_skip = 0 + for pinput in doc: # iterate through input pages + links = pinput.get_links() # get list of links + link_cnti += len(links) # count how many + pout = pdf[pinput.number] # read corresp. output page + for l in links: # iterate though the links + if l["kind"] == fitz.LINK_NAMED: # we do not handle named links + print("named link page", pinput.number, l) + link_skip += 1 # count them + continue + pout.insert_link(l) # simply output the others + + # save the conversion result + pdf.save(fn + ".pdf", garbage=4, deflate=True) + # say how many named links we skipped + if link_cnti > 0: + print("Skipped %i named links of a total of %i in input." % (link_skip, link_cnti)) + + + +How to Deal with Messages Issued by :title:`MuPDF` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Since :title:`PyMuPDF` v1.16.0, **error messages** issued by the underlying :title:`MuPDF` library are being redirected to the Python standard device *sys.stderr*. So you can handle them like any other output going to this devices. + +In addition, these messages go to the internal buffer together with any :title:`MuPDF` warnings -- see below. + +We always prefix these messages with an identifying string *"mupdf:"*. +If you prefer to not see recoverable MuPDF errors at all, issue the command `fitz.TOOLS.mupdf_display_errors(False)`. + +MuPDF warnings continue to be stored in an internal buffer and can be viewed using :meth:`Tools.mupdf_warnings`. + +Please note that MuPDF errors may or may not lead to Python exceptions. In other words, you may see error messages from which MuPDF can recover and continue processing. + +Example output for a **recoverable error**. We are opening a damaged PDF, but MuPDF is able to repair it and gives us a little information on what happened. Then we illustrate how to find out whether the document can later be saved incrementally. Checking the :attr:`Document.is_dirty` attribute at this point also indicates that during `fitz.open` the document had to be repaired: + +>>> import fitz +>>> doc = fitz.open("damaged-file.pdf") # leads to a sys.stderr message: +mupdf: cannot find startxref +>>> print(fitz.TOOLS.mupdf_warnings()) # check if there is more info: +cannot find startxref +trying to repair broken xref +repairing PDF document +object missing 'endobj' token +>>> doc.can_save_incrementally() # this is to be expected: +False +>>> # the following indicates whether there are updates so far +>>> # this is the case because of the repair actions: +>>> doc.is_dirty +True +>>> # the document has nevertheless been created: +>>> doc +fitz.Document('damaged-file.pdf') +>>> # we now know that any save must occur to a new file + +Example output for an **unrecoverable error**: + +>>> import fitz +>>> doc = fitz.open("does-not-exist.pdf") +mupdf: cannot open does-not-exist.pdf: No such file or directory +Traceback (most recent call last): + File "", line 1, in + doc = fitz.open("does-not-exist.pdf") + File "C:\Users\Jorj\AppData\Local\Programs\Python\Python37\lib\site-packages\fitz\fitz.py", line 2200, in __init__ + _fitz.Document_swiginit(self, _fitz.new_Document(filename, stream, filetype, rect, width, height, fontsize)) +RuntimeError: cannot open does-not-exist.pdf: No such file or directory +>>> + + + +Changing Annotations: Unexpected Behaviour +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Problem +^^^^^^^^^ +There are two scenarios: + +1. **Updating** an annotation with PyMuPDF which was created by some other software. +2. **Creating** an annotation with PyMuPDF and later changing it with some other software. + +In both cases you may experience unintended changes, like a different annotation icon or text font, the fill color or line dashing have disappeared, line end symbols have changed their size or even have disappeared too, etc. + +Cause +^^^^^^ +Annotation maintenance is handled differently by each PDF maintenance application. Some annotation types may not be supported, or not be supported fully or some details may be handled in a different way than in another application. **There is no standard.** + +Almost always a PDF application also comes with its own icons (file attachments, sticky notes and stamps) and its own set of supported text fonts. For example: + +* (Py-) MuPDF only supports these 5 basic fonts for 'FreeText' annotations: Helvetica, Times-Roman, Courier, ZapfDingbats and Symbol -- no italics / no bold variations. When changing a 'FreeText' annotation created by some other app, its font will probably not be recognized nor accepted and be replaced by Helvetica. + +* PyMuPDF supports all PDF text markers (highlight, underline, strikeout, squiggly), but these types cannot be updated with Adobe Acrobat Reader. + +In most cases there also exists limited support for line dashing which causes existing dashes to be replaced by straight lines. For example: + +* PyMuPDF fully supports all line dashing forms, while other viewers only accept a limited subset. + + +Solutions +^^^^^^^^^^ +Unfortunately there is not much you can do in most of these cases. + +1. Stay with the same software for **creating and changing** an annotation. +2. When using PyMuPDF to change an "alien" annotation, try to **avoid** :meth:`Annot.update`. The following methods **can be used without it,** so that the original appearance should be maintained: + + * :meth:`Annot.set_rect` (location changes) + * :meth:`Annot.set_flags` (annotation behaviour) + * :meth:`Annot.set_info` (meta information, except changes to *content*) + * :meth:`Annot.set_popup` (create popup or change its rect) + * :meth:`Annot.set_optional_content` (add / remove reference to optional content information) + * :meth:`Annot.set_open` + * :meth:`Annot.update_file` (file attachment changes) + +Misplaced Item Insertions on PDF Pages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Problem +^^^^^^^^^ + +You inserted an item (like an image, an annotation or some text) on an existing PDF page, but later you find it being placed at a different location than intended. For example an image should be inserted at the top, but it unexpectedly appears near the bottom of the page. + +Cause +^^^^^^ + +The creator of the PDF has established a non-standard page geometry without keeping it "local" (as they should!). Most commonly, the PDF standard point (0,0) at *bottom-left* has been changed to the *top-left* point. So top and bottom are reversed -- causing your insertion to be misplaced. + +The visible image of a PDF page is controlled by commands coded in a special mini-language. For an overview of this language consult "Operator Summary" on pp. 643 of the :ref:`AdobeManual`. These commands are stored in :data:`contents` objects as strings (*bytes* in PyMuPDF). + +There are commands in that language, which change the coordinate system of the page for all the following commands. In order to limit the scope of such commands to "local", they must be wrapped by the command pair *q* ("save graphics state", or "stack") and *Q* ("restore graphics state", or "unstack"). + +.. highlight:: text + +So the PDF creator did this:: + + stream + 1 0 0 -1 0 792 cm % <=== change of coordinate system: + ... % letter page, top / bottom reversed + ... % remains active beyond these lines + endstream + +where they should have done this:: + + stream + q % put the following in a stack + 1 0 0 -1 0 792 cm % <=== scope of this is limited by Q command + ... % here, a different geometry exists + Q % after this line, geometry of outer scope prevails + endstream + +.. note:: + + * In the mini-language's syntax, spaces and line breaks are equally accepted token delimiters. + * Multiple consecutive delimiters are treated as one. + * Keywords "stream" and "endstream" are inserted automatically -- not by the programmer. + +.. highlight:: python + +Solutions +^^^^^^^^^^ + +Since v1.16.0, there is the property :attr:`Page.is_wrapped`, which lets you check whether a page's contents are wrapped in that string pair. + +If it is *False* or if you want to be on the safe side, pick one of the following: + +1. The easiest way: in your script, do a :meth:`Page.clean_contents` before you do your first item insertion. +2. Pre-process your PDF with the MuPDF command line utility *mutool clean -c ...* and work with its output file instead. +3. Directly wrap the page's :data:`contents` with the stacking commands before you do your first item insertion. + +**Solutions 1. and 2.** use the same technical basis and **do a lot more** than what is required in this context: they also clean up other inconsistencies or redundancies that may exist, multiple */Contents* objects will be concatenated into one, and much more. + +.. note:: For **incremental saves,** solution 1. has an unpleasant implication: it will bloat the update delta, because it changes so many things and, in addition, stores the **cleaned contents uncompressed**. So, if you use :meth:`Page.clean_contents` you should consider **saving to a new file** with (at least) *garbage=3* and *deflate=True*. + +**Solution 3.** is completely under your control and only does the minimum corrective action. There is a handy utility method :meth:`Page.wrap_contents` which -- as twe name suggests -- **wraps** the page's :data:`contents` object(s) by the PDF commands `q` and `Q`. + +This solution is extremely fast and the changes to the PDF are minimal. This is useful in situations where incrementally saving the file is desirable -- or even a must when the PDF has been digitally signed and you cannot change this status. + +We recommend the following snippet to get the situation under control: + + >>> if not page.is_wrapped: + page.wrap_contents() + >>> # start inserting text, images and other objects here + + +Missing or Unreadable Extracted Text +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Fairly often, text extraction does not work text as you would expect: text may be missing, or may not appear in the reading sequence visible on your screen, or contain garbled characters (like a ? or a "TOFU" symbol), etc. This can be caused by a number of different problems. + +Problem: no text is extracted +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Your PDF viewer does display text, but you cannot select it with your cursor, and text extraction delivers nothing. + +Cause +^^^^^^ +1. You may be looking at an image embedded in the PDF page (e.g. a scanned PDF). +2. The PDF creator used no font, but **simulated** text by painting it, using little lines and curves. E.g. a capital "D" could be painted by a line "|" and a left-open semi-circle, an "o" by an ellipse, and so on. + +Solution +^^^^^^^^^^ +Use an OCR software like `OCRmyPDF `_ to insert a hidden text layer underneath the visible page. The resulting PDF should behave as expected. + +Problem: unreadable text +^^^^^^^^^^^^^^^^^^^^^^^^ +Text extraction does not deliver the text in readable order, duplicates some text, or is otherwise garbled. + +Cause +^^^^^^ +1. The single characters are readable as such (no "" symbols), but the sequence in which the text is **coded in the file** deviates from the reading order. The motivation behind may be technical or protection of data against unwanted copies. +2. Many "" symbols occur, indicating MuPDF could not interpret these characters. The font may indeed be unsupported by MuPDF, or the PDF creator may haved used a font that displays readable text, but on purpose obfuscates the originating corresponding unicode character. + +Solution +^^^^^^^^ +1. Use layout preserving text extraction: `python -m fitz gettext file.pdf`. +2. If other text extraction tools also don't work, then the only solution again is OCRing the page. + +.. include:: footer.rst diff --git a/docs/recipes-drawing-and-graphics.rst b/docs/recipes-drawing-and-graphics.rst new file mode 100644 index 0000000..1819dbb --- /dev/null +++ b/docs/recipes-drawing-and-graphics.rst @@ -0,0 +1,198 @@ +.. include:: header.rst + +.. _RecipesDrawingAndGraphics: + +============================== +Drawing and Graphics +============================== + + +PDF files support elementary drawing operations as part of their syntax. This includes basic geometrical objects like lines, curves, circles, rectangles including specifying colors. + +The syntax for such operations is defined in "A Operator Summary" on page 643 of the :ref:`AdobeManual`. Specifying these operators for a PDF page happens in its :data:`contents` objects. + +PyMuPDF implements a large part of the available features via its :ref:`Shape` class, which is comparable to notions like "canvas" in other packages (e.g. `reportlab `_). + +A shape is always created as a **child of a page**, usually with an instruction like *shape = page.new_shape()*. The class defines numerous methods that perform drawing operations on the page's area. For example, *last_point = shape.draw_rect(rect)* draws a rectangle along the borders of a suitably defined *rect = fitz.Rect(...)*. + +The returned *last_point* **always** is the :ref:`Point` where drawing operation ended ("last point"). Every such elementary drawing requires a subsequent :meth:`Shape.finish` to "close" it, but there may be multiple drawings which have one common *finish()* method. + +In fact, :meth:`Shape.finish` *defines* a group of preceding draw operations to form one -- potentially rather complex -- graphics object. PyMuPDF provides several predefined graphics in `shapes_and_symbols.py `_ which demonstrate how this works. + +If you import this script, you can also directly use its graphics as in the following example:: + + # -*- coding: utf-8 -*- + """ + Created on Sun Dec 9 08:34:06 2018 + + @author: Jorj + @license: GNU AFFERO GPL V3 + + Create a list of available symbols defined in shapes_and_symbols.py + + This also demonstrates an example usage: how these symbols could be used + as bullet-point symbols in some text. + + """ + + import fitz + import shapes_and_symbols as sas + + # list of available symbol functions and their descriptions + tlist = [ + (sas.arrow, "arrow (easy)"), + (sas.caro, "caro (easy)"), + (sas.clover, "clover (easy)"), + (sas.diamond, "diamond (easy)"), + (sas.dontenter, "do not enter (medium)"), + (sas.frowney, "frowney (medium)"), + (sas.hand, "hand (complex)"), + (sas.heart, "heart (easy)"), + (sas.pencil, "pencil (very complex)"), + (sas.smiley, "smiley (easy)"), + ] + + r = fitz.Rect(50, 50, 100, 100) # first rect to contain a symbol + d = fitz.Rect(0, r.height + 10, 0, r.height + 10) # displacement to next rect + p = (15, -r.height * 0.2) # starting point of explanation text + rlist = [r] # rectangle list + + for i in range(1, len(tlist)): # fill in all the rectangles + rlist.append(rlist[i-1] + d) + + doc = fitz.open() # create empty PDF + page = doc.new_page() # create an empty page + shape = page.new_shape() # start a Shape (canvas) + + for i, r in enumerate(rlist): + tlist[i][0](shape, rlist[i]) # execute symbol creation + shape.insert_text(rlist[i].br + p, # insert description text + tlist[i][1], fontsize=r.height/1.2) + + # store everything to the page's /Contents object + shape.commit() + + import os + scriptdir = os.path.dirname(__file__) + doc.save(os.path.join(scriptdir, "symbol-list.pdf")) # save the PDF + + +This is the script's outcome: + +.. image:: images/img-symbols.* + :scale: 50 + +------------------------------ + +How to Extract Drawings +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +* New in v1.18.0 + +The drawing commands issued by a page can be extracted. Interestingly, this is possible for :ref:`all supported document types` -- not just PDF: so you can use it for XPS, EPUB and others as well. + +Page method, :meth:`Page.get_drawings()` accesses draw commands and converts them into a list of Python dictionaries. Each dictionary -- called a "path" -- represents a separate drawing -- it may be simple like a single line, or a complex combination of lines and curves representing one of the shapes of the previous section. + +The *path* dictionary has been designed such that it can easily be used by the :ref:`Shape` class and its methods. Here is an example for a page with one path, that draws a red-bordered yellow circle inside rectangle `Rect(100, 100, 200, 200)`:: + + >>> pprint(page.get_drawings()) + [{'closePath': True, + 'color': [1.0, 0.0, 0.0], + 'dashes': '[] 0', + 'even_odd': False, + 'fill': [1.0, 1.0, 0.0], + 'items': [('c', + Point(100.0, 150.0), + Point(100.0, 177.614013671875), + Point(122.38600158691406, 200.0), + Point(150.0, 200.0)), + ('c', + Point(150.0, 200.0), + Point(177.61399841308594, 200.0), + Point(200.0, 177.614013671875), + Point(200.0, 150.0)), + ('c', + Point(200.0, 150.0), + Point(200.0, 122.385986328125), + Point(177.61399841308594, 100.0), + Point(150.0, 100.0)), + ('c', + Point(150.0, 100.0), + Point(122.38600158691406, 100.0), + Point(100.0, 122.385986328125), + Point(100.0, 150.0))], + 'lineCap': (0, 0, 0), + 'lineJoin': 0, + 'opacity': 1.0, + 'rect': Rect(100.0, 100.0, 200.0, 200.0), + 'width': 1.0}] + >>> + +.. note:: You need (at least) 4 Bézier curves (of 3rd order) to draw a circle with acceptable precision. See this `Wikipedia article `_ for some background. + + +The following is a code snippet which extracts the drawings of a page and re-draws them on a new page:: + + import fitz + doc = fitz.open("some.file") + page = doc[0] + paths = page.get_drawings() # extract existing drawings + # this is a list of "paths", which can directly be drawn again using Shape + # ------------------------------------------------------------------------- + # + # define some output page with the same dimensions + outpdf = fitz.open() + outpage = outpdf.new_page(width=page.rect.width, height=page.rect.height) + shape = outpage.new_shape() # make a drawing canvas for the output page + # -------------------------------------- + # loop through the paths and draw them + # -------------------------------------- + for path in paths: + # ------------------------------------ + # draw each entry of the 'items' list + # ------------------------------------ + for item in path["items"]: # these are the draw commands + if item[0] == "l": # line + shape.draw_line(item[1], item[2]) + elif item[0] == "re": # rectangle + shape.draw_rect(item[1]) + elif item[0] == "qu": # quad + shape.draw_quad(item[1]) + elif item[0] == "c": # curve + shape.draw_bezier(item[1], item[2], item[3], item[4]) + else: + raise ValueError("unhandled drawing", item) + # ------------------------------------------------------ + # all items are drawn, now apply the common properties + # to finish the path + # ------------------------------------------------------ + shape.finish( + fill=path["fill"], # fill color + color=path["color"], # line color + dashes=path["dashes"], # line dashing + even_odd=path.get("even_odd", True), # control color of overlaps + closePath=path["closePath"], # whether to connect last and first point + lineJoin=path["lineJoin"], # how line joins should look like + lineCap=max(path["lineCap"]), # how line ends should look like + width=path["width"], # line width + stroke_opacity=path.get("stroke_opacity", 1), # same value for both + fill_opacity=path.get("fill_opacity", 1), # opacity parameters + ) + # all paths processed - commit the shape to its page + shape.commit() + outpdf.save("drawings-page-0.pdf") + +As can be seen, there is a high congruence level with the :ref:`Shape` class. With one exception: For technical reasons `lineCap` is a tuple of 3 numbers here, whereas it is an integer in :ref:`Shape` (and in PDF). So we simply take the maximum value of that tuple. + +Here is a comparison between input and output of an example page, created by the previous script: + +.. image:: images/img-getdrawings.png + :scale: 50 + +.. note:: The reconstruction of graphics, like shown here, is not perfect. The following aspects will not be reproduced as of this version: + + * Page definitions can be complex and include instructions for not showing / hiding certain areas to keep them invisible. Things like this are ignored by :meth:`Page.get_drawings` - it will always return all paths. + +.. note:: You can use the path list to make your own lists of e.g. all lines or all rectangles on the page and subselect them by criteria, like color or position on the page etc. + +.. include:: footer.rst diff --git a/docs/recipes-images.rst b/docs/recipes-images.rst new file mode 100644 index 0000000..38b6926 --- /dev/null +++ b/docs/recipes-images.rst @@ -0,0 +1,650 @@ +.. include:: header.rst + +.. _RecipesImages: + +============================== +Images +============================== + + + +.. _RecipesImages_A: + +How to Make Images from Document Pages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This little script will take a document filename and generate a PNG file from each of its pages. + +The document can be any :ref:`supported type`. + +The script works as a command line tool which expects the filename being supplied as a parameter. The generated image files (1 per page) are stored in the directory of the script:: + + import sys, fitz # import the bindings + fname = sys.argv[1] # get filename from command line + doc = fitz.open(fname) # open document + for page in doc: # iterate through the pages + pix = page.get_pixmap() # render page to an image + pix.save("page-%i.png" % page.number) # store image as a PNG + +The script directory will now contain PNG image files named *page-0.png*, *page-1.png*, etc. Pictures have the dimension of their pages with width and height rounded to integers, e.g. 595 x 842 pixels for an A4 portrait sized page. They will have a resolution of 96 dpi in x and y dimension and have no transparency. You can change all that -- for how to do this, read the next sections. + +---------- + + +.. _RecipesImages_B: + +How to Increase :index:`Image Resolution ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The image of a document page is represented by a :ref:`Pixmap`, and the simplest way to create a pixmap is via method :meth:`Page.get_pixmap`. + +This method has many options to influence the result. The most important among them is the :ref:`Matrix`, which lets you :index:`zoom`, rotate, distort or mirror the outcome. + +:meth:`Page.get_pixmap` by default will use the :ref:`Identity` matrix, which does nothing. + +In the following, we apply a :index:`zoom factor ` of 2 to each dimension, which will generate an image with a four times better resolution for us (and also about 4 times the size):: + + zoom_x = 2.0 # horizontal zoom + zoom_y = 2.0 # vertical zoom + mat = fitz.Matrix(zoom_x, zoom_y) # zoom factor 2 in each dimension + pix = page.get_pixmap(matrix=mat) # use 'mat' instead of the identity matrix + + +Since version 1.19.2 there is a more direct way to set the resolution: Parameter `"dpi"` (dots per inch) can be used in place of `"matrix"`. To create a 300 dpi image of a page specify `pix = page.get_pixmap(dpi=300)`. Apart from notation brevity, this approach has the additional advantage that the **dpi value is saved with the image** file -- which does not happen automatically when using the Matrix notation. + +---------- + + +.. _RecipesImages_C: + +How to Create :index:`Partial Pixmaps` (Clips) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +You do not always need or want the full image of a page. This is the case e.g. when you display the image in a GUI and would like to fill the respective window with a zoomed part of the page. + +Let's assume your GUI window has room to display a full document page, but you now want to fill this room with the bottom right quarter of your page, thus using a four times better resolution. + +To achieve this, define a rectangle equal to the area you want to appear in the GUI and call it "clip". One way of constructing rectangles in PyMuPDF is by providing two diagonally opposite corners, which is what we are doing here. + +.. image:: images/img-clip.* + :width: 50% + +:: + + mat = fitz.Matrix(2, 2) # zoom factor 2 in each direction + rect = page.rect # the page rectangle + mp = (rect.tl + rect.br) / 2 # its middle point, becomes top-left of clip + clip = fitz.Rect(mp, rect.br) # the area we want + pix = page.get_pixmap(matrix=mat, clip=clip) + +In the above we construct *clip* by specifying two diagonally opposite points: the middle point *mp* of the page rectangle, and its bottom right, *rect.br*. + +---------- + + +.. _RecipesImages_D: + +How to Zoom a Clip to a GUI Window +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Please also read the previous section. This time we want to **compute the zoom factor** for a clip, such that its image best fits a given GUI window. This means, that the image's width or height (or both) will equal the window dimension. For the following code snippet you need to provide the WIDTH and HEIGHT of your GUI's window that should receive the page's clip rectangle. + +:: + + # WIDTH: width of the GUI window + # HEIGHT: height of the GUI window + # clip: a subrectangle of the document page + # compare width/height ratios of image and window + + if clip.width / clip.height < WIDTH / HEIGHT: + # clip is narrower: zoom to window HEIGHT + zoom = HEIGHT / clip.height + else: # clip is broader: zoom to window WIDTH + zoom = WIDTH / clip.width + mat = fitz.Matrix(zoom, zoom) + pix = page.get_pixmap(matrix=mat, clip=clip) + +For the other way round, now assume you **have** the zoom factor and need to **compute the fitting clip**. + +In this case we have `zoom = HEIGHT/clip.height = WIDTH/clip.width`, so we must set `clip.height = HEIGHT/zoom` and, `clip.width = WIDTH/zoom`. Choose the top-left point `tl` of the clip on the page to compute the right pixmap:: + + width = WIDTH / zoom + height = HEIGHT / zoom + clip = fitz.Rect(tl, tl.x + width, tl.y + height) + # ensure we still are inside the page + clip &= page.rect + mat = fitz.Matrix(zoom, zoom) + pix = fitz.Pixmap(matrix=mat, clip=clip) + + +---------- + + +.. _RecipesImages_E: + +How to Create or Suppress Annotation Images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Normally, the pixmap of a page also shows the page's annotations. Occasionally, this may not be desirable. + +To suppress the annotation images on a rendered page, just specify `annots=False` in :meth:`Page.get_pixmap`. + +You can also render annotations separately: they have their own :meth:`Annot.get_pixmap` method. The resulting pixmap has the same dimensions as the annotation rectangle. + +---------- + +.. index:: + triple: extract;image;non-PDF + pair: convert_to_pdf;examples + + +.. _RecipesImages_F: + +How to Extract Images: Non-PDF Documents +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In contrast to the previous sections, this section deals with **extracting** images **contained** in documents, so they can be displayed as part of one or more pages. + +If you want to recreate the original image in file form or as a memory area, you have basically two options: + +1. Convert your document to a PDF, and then use one of the PDF-only extraction methods. This snippet will convert a document to PDF:: + + >>> pdfbytes = doc.convert_to_pdf() # this a bytes object + >>> pdf = fitz.open("pdf", pdfbytes) # open it as a PDF document + >>> # now use 'pdf' like any PDF document + +2. Use :meth:`Page.get_text` with the "dict" parameter. This works for all document types. It will extract all text and images shown on the page, formatted as a Python dictionary. Every image will occur in an image block, containing meta information and **the binary image data**. For details of the dictionary's structure, see :ref:`TextPage`. The method works equally well for PDF files. This creates a list of all images shown on a page:: + + >>> d = page.get_text("dict") + >>> blocks = d["blocks"] # the list of block dictionaries + >>> imgblocks = [b for b in blocks if b["type"] == 1] + >>> pprint(imgblocks[0]) + {'bbox': (100.0, 135.8769989013672, 300.0, 364.1230163574219), + 'bpc': 8, + 'colorspace': 3, + 'ext': 'jpeg', + 'height': 501, + 'image': b'\xff\xd8\xff\xe0\x00\x10JFIF\...', # CAUTION: LARGE! + 'size': 80518, + 'transform': (200.0, 0.0, -0.0, 228.2460174560547, 100.0, 135.8769989013672), + 'type': 1, + 'width': 439, + 'xres': 96, + 'yres': 96} + +---------- + +.. index:: + triple: extract;image;PDF + pair: extract_image;examples + + +.. _RecipesImages_G: + +How to Extract Images: PDF Documents +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Like any other "object" in a PDF, images are identified by a cross reference number (:data:`xref`, an integer). If you know this number, you have two ways to access the image's data: + +1. **Create** a :ref:`Pixmap` of the image with instruction *pix = fitz.Pixmap(doc, xref)*. This method is **very** fast (single digit micro-seconds). The pixmap's properties (width, height, ...) will reflect the ones of the image. In this case there is no way to tell which image format the embedded original has. + +2. **Extract** the image with *img = doc.extract_image(xref)*. This is a dictionary containing the binary image data as *img["image"]*. A number of meta data are also provided -- mostly the same as you would find in the pixmap of the image. The major difference is string *img["ext"]*, which specifies the image format: apart from "png", strings like "jpeg", "bmp", "tiff", etc. can also occur. Use this string as the file extension if you want to store to disk. The execution speed of this method should be compared to the combined speed of the statements *pix = fitz.Pixmap(doc, xref);pix.tobytes()*. If the embedded image is in PNG format, the speed of :meth:`Document.extract_image` is about the same (and the binary image data are identical). Otherwise, this method is **thousands of times faster**, and the **image data is much smaller**. + +The question remains: **"How do I know those 'xref' numbers of images?"**. There are two answers to this: + +a. **"Inspect the page objects:"** Loop through the items of :meth:`Page.get_images`. It is a list of list, and its items look like *[xref, smask, ...]*, containing the :data:`xref` of an image. This :data:`xref` can then be used with one of the above methods. Use this method for **valid (undamaged)** documents. Be wary however, that the same image may be referenced multiple times (by different pages), so you might want to provide a mechanism avoiding multiple extracts. +b. **"No need to know:"** Loop through the list of **all xrefs** of the document and perform a :meth:`Document.extract_image` for each one. If the returned dictionary is empty, then continue -- this :data:`xref` is no image. Use this method if the PDF is **damaged (unusable pages)**. Note that a PDF often contains "pseudo-images" ("stencil masks") with the special purpose of defining the transparency of some other image. You may want to provide logic to exclude those from extraction. Also have a look at the next section. + +For both extraction approaches, there exist ready-to-use general purpose scripts: + +`extract-from-pages.py `_ extracts images page by page: + +.. image:: images/img-extract-imga.* + :scale: 80 + +and `extract-from-xref.py `_ extracts images by xref table: + +.. image:: images/img-extract-imgb.* + :scale: 80 + +---------- + + +.. _RecipesImages_H: + +How to Handle Image Masks +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Some images in PDFs are accompanied by **image masks**. In their simplest form, masks represent alpha (transparency) bytes stored as separate images. In order to reconstruct the original of an image, which has a mask, it must be "enriched" with transparency bytes taken from its mask. + +Whether an image does have such a mask can be recognized in one of two ways in PyMuPDF: + +1. An item of :meth:`Document.get_page_images` has the general format `(xref, smask, ...)`, where *xref* is the image's :data:`xref` and *smask*, if positive, then it is the :data:`xref` of a mask. +2. The (dictionary) results of :meth:`Document.extract_image` have a key *"smask"*, which also contains any mask's :data:`xref` if positive. + +If *smask == 0* then the image encountered via :data:`xref` can be processed as it is. + +To recover the original image using PyMuPDF, the procedure depicted as follows must be executed: + +.. image:: images/img-stencil.* + :scale: 60 + +>>> pix1 = fitz.Pixmap(doc.extract_image(xref)["image"]) # (1) pixmap of image w/o alpha +>>> mask = fitz.Pixmap(doc.extract_image(smask)["image"]) # (2) mask pixmap +>>> pix = fitz.Pixmap(pix1, mask) # (3) copy of pix1, image mask added + +Step (1) creates a pixmap of the basic image. Step (2) does the same with the image mask. Step (3) adds an alpha channel and fills it with transparency information. + +The scripts `extract-from-pages.py `_, and `extract-from-xref.py `_ above also contain this logic. + +---------- + +.. index:: + triple: picture;embed;PDF + pair: show_pdf_page;examples + pair: insert_image;examples + pair: embfile_add;examples + pair: add_file_annot;examples + + +.. _RecipesImages_I: + + +How to Make one PDF of all your Pictures (or Files) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +We show here **three scripts** that take a list of (image and other) files and put them all in one PDF. + +**Method 1: Inserting Images as Pages** + +The first one converts each image to a PDF page with the same dimensions. The result will be a PDF with one page per image. It will only work for :ref:`supported image` file formats:: + + import os, fitz + import PySimpleGUI as psg # for showing a progress bar + doc = fitz.open() # PDF with the pictures + imgdir = "D:/2012_10_05" # where the pics are + imglist = os.listdir(imgdir) # list of them + imgcount = len(imglist) # pic count + + for i, f in enumerate(imglist): + img = fitz.open(os.path.join(imgdir, f)) # open pic as document + rect = img[0].rect # pic dimension + pdfbytes = img.convert_to_pdf() # make a PDF stream + img.close() # no longer needed + imgPDF = fitz.open("pdf", pdfbytes) # open stream as PDF + page = doc.new_page(width = rect.width, # new page with ... + height = rect.height) # pic dimension + page.show_pdf_page(rect, imgPDF, 0) # image fills the page + psg.EasyProgressMeter("Import Images", # show our progress + i+1, imgcount) + + doc.save("all-my-pics.pdf") + +This will generate a PDF only marginally larger than the combined pictures' size. Some numbers on performance: + +The above script needed about 1 minute on my machine for 149 pictures with a total size of 514 MB (and about the same resulting PDF size). + +.. image:: images/img-import-progress.* + :scale: 80 + +Look `here `_ for a more complete source code: it offers a directory selection dialog and skips unsupported files and non-file entries. + +.. note:: We might have used :meth:`Page.insert_image` instead of :meth:`Page.show_pdf_page`, and the result would have been a similar looking file. However, depending on the image type, it may store **images uncompressed**. Therefore, the save option *deflate = True* must be used to achieve a reasonable file size, which hugely increases the runtime for large numbers of images. So this alternative **cannot be recommended** here. + +**Method 2: Embedding Files** + +The second script **embeds** arbitrary files -- not only images. The resulting PDF will have just one (empty) page, required for technical reasons. To later access the embedded files again, you would need a suitable PDF viewer that can display and / or extract embedded files:: + + import os, fitz + import PySimpleGUI as psg # for showing progress bar + doc = fitz.open() # PDF with the pictures + imgdir = "D:/2012_10_05" # where my files are + + imglist = os.listdir(imgdir) # list of pictures + imgcount = len(imglist) # pic count + imglist.sort() # nicely sort them + + for i, f in enumerate(imglist): + img = open(os.path.join(imgdir,f), "rb").read() # make pic stream + doc.embfile_add(img, f, filename=f, # and embed it + ufilename=f, desc=f) + psg.EasyProgressMeter("Embedding Files", # show our progress + i+1, imgcount) + + page = doc.new_page() # at least 1 page is needed + + doc.save("all-my-pics-embedded.pdf") + +.. image:: images/img-embed-progress.* + :scale: 80 + +This is by far the fastest method, and it also produces the smallest possible output file size. The above pictures needed 20 seconds on my machine and yielded a PDF size of 510 MB. Look `here `_ for a more complete source code: it offers a directory selection dialog and skips non-file entries. + +**Method 3: Attaching Files** + +A third way to achieve this task is **attaching files** via page annotations see `here `_ for the complete source code. + +This has a similar performance as the previous script and it also produces a similar file size. It will produce PDF pages which show a 'FileAttachment' icon for each attached file. + +.. image:: images/img-attach-result.* + +.. note:: Both, the **embed** and the **attach** methods can be used for **arbitrary files** -- not just images. + +.. note:: We strongly recommend using the awesome package `PySimpleGUI `_ to display a progress meter for tasks that may run for an extended time span. It's pure Python, uses Tkinter (no additional GUI package) and requires just one more line of code! + +---------- + +.. index:: + triple: vector;image;SVG + pair: show_pdf_page;examples + pair: insert_image;examples + pair: embfile_add;examples + + +.. _RecipesImages_J: + +How to Create Vector Images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The usual way to create an image from a document page is :meth:`Page.get_pixmap`. A pixmap represents a raster image, so you must decide on its quality (i.e. resolution) at creation time. It cannot be changed later. + +PyMuPDF also offers a way to create a **vector image** of a page in SVG format (scalable vector graphics, defined in XML syntax). SVG images remain precise across zooming levels (of course with the exception of any raster graphic elements embedded therein). + +Instruction *svg = page.get_svg_image(matrix=fitz.Identity)* delivers a UTF-8 string *svg* which can be stored with extension ".svg". + +---------- + +.. index:: + pair: save;examples + pair: tobytes;examples + pair: Photoshop;examples + pair: Postscript;examples + pair: JPEG;examples + pair: PhotoImage;examples + + +.. _RecipesImages_K: + +How to Convert Images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Just as a feature among others, PyMuPDF's image conversion is easy. It may avoid using other graphics packages like PIL/Pillow in many cases. + +Notwithstanding that interfacing with Pillow is almost trivial. + +================= ================== ========================================= +**Input Formats** **Output Formats** **Description** +================= ================== ========================================= +BMP . Windows Bitmap +JPEG JPEG Joint Photographic Experts Group +JXR . JPEG Extended Range +JPX/JP2 . JPEG 2000 +GIF . Graphics Interchange Format +TIFF . Tagged Image File Format +PNG PNG Portable Network Graphics +PNM PNM Portable Anymap +PGM PGM Portable Graymap +PBM PBM Portable Bitmap +PPM PPM Portable Pixmap +PAM PAM Portable Arbitrary Map +. PSD Adobe Photoshop Document +. PS Adobe Postscript +================= ================== ========================================= + +The general scheme is just the following two lines:: + + pix = fitz.Pixmap("input.xxx") # any supported input format + pix.save("output.yyy") # any supported output format + +**Remarks** + +1. The **input** argument of *fitz.Pixmap(arg)* can be a file or a bytes / io.BytesIO object containing an image. +2. Instead of an output **file**, you can also create a bytes object via *pix.tobytes("yyy")* and pass this around. +3. As a matter of course, input and output formats must be compatible in terms of colorspace and transparency. The *Pixmap* class has batteries included if adjustments are needed. + +.. note:: + **Convert JPEG to Photoshop**:: + + pix = fitz.Pixmap("myfamily.jpg") + pix.save("myfamily.psd") + +.. note:: + Convert **JPEG to Tkinter PhotoImage**. Any **RGB / no-alpha** image works exactly the same. Conversion to one of the **Portable Anymap** formats (PPM, PGM, etc.) does the trick, because they have **very fast** support by all Tkinter versions:: + + import tkinter as tk + pix = fitz.Pixmap("input.jpg") # or any RGB / no-alpha image + tkimg = tk.PhotoImage(data=pix.tobytes("ppm")) + +.. note:: + Convert **PNG with alpha** to Tkinter PhotoImage. This requires **removing the alpha bytes**, before we can do the PPM conversion:: + + import tkinter as tk + pix = fitz.Pixmap("input.png") # may have an alpha channel + if pix.alpha: # we have an alpha channel! + pix = fitz.Pixmap(pix, 0) # remove it + tkimg = tk.PhotoImage(data=pix.tobytes("ppm")) + +---------- + +.. index:: + pair: copy;examples + + +.. _RecipesImages_L: + +How to Use Pixmaps: Gluing Images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This shows how pixmaps can be used for purely graphical, non-document purposes. The script reads an image file and creates a new image which consist of 3 * 4 tiles of the original:: + + import fitz + src = fitz.Pixmap("img-7edges.png") # create pixmap from a picture + col = 3 # tiles per row + lin = 4 # tiles per column + tar_w = src.width * col # width of target + tar_h = src.height * lin # height of target + + # create target pixmap + tar_pix = fitz.Pixmap(src.colorspace, (0, 0, tar_w, tar_h), src.alpha) + + # now fill target with the tiles + for i in range(col): + for j in range(lin): + src.set_origin(src.width * i, src.height * j) + tar_pix.copy(src, src.irect) # copy input to new loc + + tar_pix.save("tar.png") + +This is the input picture: + +.. image:: images/img-7edges.png + :scale: 33 + +Here is the output: + +.. image:: images/img-target.png + :scale: 33 + +---------- + +.. index:: + pair: set_rect;examples + pair: invert_irect;examples + pair: copy;examples + pair: save;examples + + +.. _RecipesImages_M: + +How to Use Pixmaps: Making a Fractal +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here is another Pixmap example that creates **Sierpinski's Carpet** -- a fractal generalizing the **Cantor Set** to two dimensions. Given a square carpet, mark its 9 sub-suqares (3 times 3) and cut out the one in the center. Treat each of the remaining eight sub-squares in the same way, and continue *ad infinitum*. The end result is a set with area zero and fractal dimension 1.8928... + +This script creates an approximate image of it as a PNG, by going down to one-pixel granularity. To increase the image precision, change the value of n (precision):: + + import fitz, time + if not list(map(int, fitz.VersionBind.split("."))) >= [1, 14, 8]: + raise SystemExit("need PyMuPDF v1.14.8 for this script") + n = 6 # depth (precision) + d = 3**n # edge length + + t0 = time.perf_counter() + ir = (0, 0, d, d) # the pixmap rectangle + + pm = fitz.Pixmap(fitz.csRGB, ir, False) + pm.set_rect(pm.irect, (255,255,0)) # fill it with some background color + + color = (0, 0, 255) # color to fill the punch holes + + # alternatively, define a 'fill' pixmap for the punch holes + # this could be anything, e.g. some photo image ... + fill = fitz.Pixmap(fitz.csRGB, ir, False) # same size as 'pm' + fill.set_rect(fill.irect, (0, 255, 255)) # put some color in + + def punch(x, y, step): + """Recursively "punch a hole" in the central square of a pixmap. + + Arguments are top-left coords and the step width. + + Some alternative punching methods are commented out. + """ + s = step // 3 # the new step + # iterate through the 9 sub-squares + # the central one will be filled with the color + for i in range(3): + for j in range(3): + if i != j or i != 1: # this is not the central cube + if s >= 3: # recursing needed? + punch(x+i*s, y+j*s, s) # recurse + else: # punching alternatives are: + pm.set_rect((x+s, y+s, x+2*s, y+2*s), color) # fill with a color + #pm.copy(fill, (x+s, y+s, x+2*s, y+2*s)) # copy from fill + #pm.invert_irect((x+s, y+s, x+2*s, y+2*s)) # invert colors + + return + + #============================================================================== + # main program + #============================================================================== + # now start punching holes into the pixmap + punch(0, 0, d) + t1 = time.perf_counter() + pm.save("sierpinski-punch.png") + t2 = time.perf_counter() + print ("%g sec to create / fill the pixmap" % round(t1-t0,3)) + print ("%g sec to save the image" % round(t2-t1,3)) + +The result should look something like this: + +.. image:: images/img-sierpinski.png + :scale: 33 + +---------- + +.. _RecipesImages_N: + +How to Interface with NumPy +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This shows how to create a PNG file from a numpy array (several times faster than most other methods):: + + import numpy as np + import fitz + #============================================================================== + # create a fun-colored width * height PNG with fitz and numpy + #============================================================================== + height = 150 + width = 100 + bild = np.ndarray((height, width, 3), dtype=np.uint8) + + for i in range(height): + for j in range(width): + # one pixel (some fun coloring) + bild[i, j] = [(i+j)%256, i%256, j%256] + + samples = bytearray(bild.tostring()) # get plain pixel data from numpy array + pix = fitz.Pixmap(fitz.csRGB, width, height, samples, alpha=False) + pix.save("test.png") + + +---------- + + +.. _RecipesImages_O: + +How to Add Images to a PDF Page +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +There are two methods to add images to a PDF page: :meth:`Page.insert_image` and :meth:`Page.show_pdf_page`. Both methods have things in common, but there are also differences. + +============================== ===================================== ========================================= +**Criterion** :meth:`Page.insert_image` :meth:`Page.show_pdf_page` +============================== ===================================== ========================================= +displayable content image file, image in memory, pixmap PDF page +display resolution image resolution vectorized (except raster page content) +rotation 0, 90, 180 or 270 degrees any angle +clipping no (full image only) yes +keep aspect ratio yes (default option) yes (default option) +transparency (water marking) depends on the image depends on the page +location / placement scaled to fit target rectangle scaled to fit target rectangle +performance automatic prevention of duplicates; automatic prevention of duplicates; +multi-page image support no yes +ease of use simple, intuitive; simple, intuitive; + **usable for all document types** + (including images!) after conversion to + PDF via :meth:`Document.convert_to_pdf` +============================== ===================================== ========================================= + +Basic code pattern for :meth:`Page.insert_image`. **Exactly one** of the parameters **filename / stream / pixmap** must be given, if not re-inserting an existing image:: + + page.insert_image( + rect, # where to place the image (rect-like) + filename=None, # image in a file + stream=None, # image in memory (bytes) + pixmap=None, # image from pixmap + mask=None, # specify alpha channel separately + rotate=0, # rotate (int, multiple of 90) + xref=0, # re-use existing image + oc=0, # control visibility via OCG / OCMD + keep_proportion=True, # keep aspect ratio + overlay=True, # put in foreground + ) + +Basic code pattern for :meth:`Page.show_pdf_page`. Source and target PDF must be different :ref:`Document` objects (but may be opened from the same file):: + + page.show_pdf_page( + rect, # where to place the image (rect-like) + src, # source PDF + pno=0, # page number in source PDF + clip=None, # only display this area (rect-like) + rotate=0, # rotate (float, any value) + oc=0, # control visibility via OCG / OCMD + keep_proportion=True, # keep aspect ratio + overlay=True, # put in foreground + ) + +.. _RecipesImages_P: + +How to Use Pixmaps: Checking Text Visibility +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Whether or not a given piece of text is actually visible on a page depends on a number of factors: + +1. Text is not covered by another object but may have the same color as the background i.e., white-on-white etc. +2. Text may be covered by an image or vector graphics. Detecting this is an important capability, for example to uncover badly anonymized legal documents. +3. Text is created hidden. This technique is usually used by OCR tools to store the recognized text in an invisible layer on the page. + +The following shows how to detect situation 1. above, or situation 2. if the covering object is unicolor:: + + pix = page.get_pixmap(dpi=150) # make page image with a decent resolution + + # the following matrix transforms page to pixmap coordinates + mat = page.rect.torect(pix.irect) + + # search for some string "needle" + rlist = page.search_for("needle") + # check the visibility for each hit rectangle + for rect in rlist: + if pix.color_topusage(clip=rect * mat)[0] > 0.95: + print("'needle' is invisible here:", rect) + +Method :meth:`Pixmap.color_topusage` returns a tuple `(ratio, pixel)` where 0 < ratio <= 1 and *pixel* is the pixel value of the color. Please note that we create a **pixmap only once**. This can save a lot of processing time if there are multiple hit rectangles. + +The logic of the above code is: If the needle's rectangle is ("almost": > 95%) unicolor, then the text cannot be visible. A typical result for visible text returns the color of the background (mostly white) and a ratio around 0.7 to 0.8, for example `(0.685, b'\xff\xff\xff')`. + + +.. include:: footer.rst diff --git a/docs/recipes-journalling.rst b/docs/recipes-journalling.rst new file mode 100644 index 0000000..ba0c023 --- /dev/null +++ b/docs/recipes-journalling.rst @@ -0,0 +1,140 @@ +.. include:: header.rst + +.. _RecipesJournalling: + +========================================= +Journalling +========================================= + + +Starting with version 1.19.0, journalling is possible when updating PDF documents. + +Journalling is a logging mechanism which permits either **reverting** or **re-applying** changes to a PDF. Similar to LUWs "Logical Units of Work" in modern database systems, one can group a set of updates into an "operation". In MuPDF journalling, an operation plays the role of a LUW. + +.. note:: In contrast to LUW implementations found in database systems, MuPDF journalling happens on a **per document level**. There is no support for simultaneous updates across multiple PDFs: one would have to establish one's own logic here. + +* Journalling must be *enabled* via a document method. Journalling is possible for existing or new documents. Journalling **can be disabled only** by closing the file. +* Once enabled, every change must happen inside an *operation* -- otherwise an exception is raised. An operation is started and stopped via document methods. Updates happening between these two calls form an LUW and can thus collectively be rolled back or re-applied, or, in MuPDF terminology "undone" resp. "redone". +* At any point, the journalling status can be queried: whether journalling is active, how many operations have been recorded, whether "undo" or "redo" is possible, the current position inside the journal, etc. +* The journal can be **saved to** or **loaded from** a file. These are document methods. +* When loading a journal file, compatibility with the document is checked and journalling is automatically enabled upon success. +* For an **existing** PDF being journalled, a special new save method is available: :meth:`Document.save_snapshot`. This performs a special incremental save that includes all journalled updates so far. If its journal is saved at the same time (immediately after the document snapshot), then document and journal are in sync and can later on be used together to undo or redo operations or to continue journalled updates -- just as if there had been no interruption. +* The snapshot PDF is a valid PDF in every aspect and fully usable. If the document is however changed in any way without using its journal file, then a desynchronization will take place and the journal is rendered unusable. +* Snapshot files are structured like incremental updates. Nevertheless, the internal journalling logic requires, that saving **must happen to a new file**. So the user should develop a file naming convention to support recognizable relationships between an original PDF, like `original.pdf` and its snapshot sets, like `original-snap1.pdf` / `original-snap1.log`, `original-snap2.pdf` / `original-snap2.log`, etc. + +Example Session 1 +~~~~~~~~~~~~~~~~~~ +Description: + +* Make a new PDF and enable journalling. Then add a page and some text lines -- each as a separate operation. +* Navigate within the journal, undoing and redoing these updates and displaying status and file results:: + + >>> import fitz + >>> doc=fitz.open() + >>> doc.journal_enable() + + >>> # try update without an operation: + >>> page = doc.new_page() + mupdf: No journalling operation started + ... omitted lines + RuntimeError: No journalling operation started + + >>> doc.journal_start_op("op1") + >>> page = doc.new_page() + >>> doc.journal_stop_op() + + >>> doc.journal_start_op("op2") + >>> page.insert_text((100,100), "Line 1") + >>> doc.journal_stop_op() + + >>> doc.journal_start_op("op3") + >>> page.insert_text((100,120), "Line 2") + >>> doc.journal_stop_op() + + >>> doc.journal_start_op("op4") + >>> page.insert_text((100,140), "Line 3") + >>> doc.journal_stop_op() + + >>> # show position in journal + >>> doc.journal_position() + (4, 4) + >>> # 4 operations recorded - positioned at bottom + >>> # what can we do? + >>> doc.journal_can_do() + {'undo': True, 'redo': False} + >>> # currently only undos are possible. Print page content: + >>> print(page.get_text()) + Line 1 + Line 2 + Line 3 + + >>> # undo last insert: + >>> doc.journal_undo() + >>> # show combined status again: + >>> doc.journal_position();doc.journal_can_do() + (3, 4) + {'undo': True, 'redo': True} + >>> print(page.get_text()) + Line 1 + Line 2 + + >>> # our position is now second to last + >>> # last text insertion was reverted + >>> # but we can redo / move forward as well: + >>> doc.journal_redo() + >>> # our combined status: + >>> doc.journal_position();doc.journal_can_do() + (4, 4) + {'undo': True, 'redo': False} + >>> print(page.get_text()) + Line 1 + Line 2 + Line 3 + >>> # line 3 has appeared again! + + +Example Session 2 +~~~~~~~~~~~~~~~~~~ +Description: + +* Similar to previous, but after undoing some operations, we now add a different update. This will cause: + + - permanent removal of the undone journal entries + - the new update operation will become the new last entry. + + + >>> doc=fitz.open() + >>> doc.journal_enable() + >>> doc.journal_start_op("Page insert") + >>> page=doc.new_page() + >>> doc.journal_stop_op() + >>> for i in range(5): + doc.journal_start_op("insert-%i" % i) + page.insert_text((100, 100 + 20*i), "text line %i" %i) + doc.journal_stop_op() + + >>> # combined status info: + >>> doc.journal_position();doc.journal_can_do() + (6, 6) + {'undo': True, 'redo': False} + + >>> for i in range(3): # revert last three operations + doc.journal_undo() + >>> doc.journal_position();doc.journal_can_do() + (3, 6) + {'undo': True, 'redo': True} + + >>> # now do a different update: + >>> doc.journal_start_op("Draw some line") + >>> page.draw_line((100,150), (300,150)) + Point(300.0, 150.0) + >>> doc.journal_stop_op() + >>> doc.journal_position();doc.journal_can_do() + (4, 4) + {'undo': True, 'redo': False} + + >>> # this has changed the journal: + >>> # previous last 3 text line operations were removed, and + >>> # we have only 4 operations: drawing the line is the new last one + +.. include:: footer.rst diff --git a/docs/recipes-low-level-interfaces.rst b/docs/recipes-low-level-interfaces.rst new file mode 100644 index 0000000..f649fe9 --- /dev/null +++ b/docs/recipes-low-level-interfaces.rst @@ -0,0 +1,444 @@ +.. include:: header.rst + +.. _RecipesLowLevelInterfaces: + +========================================= +Low-Level Interfaces +========================================= + + +Numerous methods are available to access and manipulate PDF files on a fairly low level. Admittedly, a clear distinction between "low level" and "normal" functionality is not always possible or subject to personal taste. + +It also may happen, that functionality previously deemed low-level is later on assessed as being part of the normal interface. This has happened in v1.14.0 for the class :ref:`Tools` - you now find it as an item in the Classes chapter. + +It is a matter of documentation only in which chapter of the documentation you find what you are looking for. Everything is available and always via the same interface. + +---------------------------------- + +How to Iterate through the :data:`xref` Table +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A PDF's :data:`xref` table is a list of all objects defined in the file. This table may easily contain many thousands of entries -- the manual :ref:`AdobeManual` for example has 127,000 objects. Table entry "0" is reserved and must not be touched. +The following script loops through the :data:`xref` table and prints each object's definition:: + + >>> xreflen = doc.xref_length() # length of objects table + >>> for xref in range(1, xreflen): # skip item 0! + print("") + print("object %i (stream: %s)" % (xref, doc.xref_is_stream(xref))) + print(doc.xref_object(xref, compressed=False)) + + +.. highlight:: text + +This produces the following output:: + + object 1 (stream: False) + << + /ModDate (D:20170314122233-04'00') + /PXCViewerInfo (PDF-XChange Viewer;2.5.312.1;Feb 9 2015;12:00:06;D:20170314122233-04'00') + >> + + object 2 (stream: False) + << + /Type /Catalog + /Pages 3 0 R + >> + + object 3 (stream: False) + << + /Kids [ 4 0 R 5 0 R ] + /Type /Pages + /Count 2 + >> + + object 4 (stream: False) + << + /Type /Page + /Annots [ 6 0 R ] + /Parent 3 0 R + /Contents 7 0 R + /MediaBox [ 0 0 595 842 ] + /Resources 8 0 R + >> + ... + object 7 (stream: True) + << + /Length 494 + /Filter /FlateDecode + >> + ... + +.. highlight:: python + +A PDF object definition is an ordinary ASCII string. + +---------------------------------- + +How to Handle Object Streams +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Some object types contain additional data apart from their object definition. Examples are images, fonts, embedded files or commands describing the appearance of a page. + +Objects of these types are called "stream objects". PyMuPDF allows reading an object's stream via method :meth:`Document.xref_stream` with the object's :data:`xref` as an argument. It is also possible to write back a modified version of a stream using :meth:`Document.update_stream`. + +Assume that the following snippet wants to read all streams of a PDF for whatever reason:: + + >>> xreflen = doc.xref_length() # number of objects in file + >>> for xref in range(1, xreflen): # skip item 0! + if stream := doc.xref_stream(xref): + # do something with it (it is a bytes object or None) + # e.g. just write it back: + doc.update_stream(xref, stream) + +:meth:`Document.xref_stream` automatically returns a stream decompressed as a bytes object -- and :meth:`Document.update_stream` automatically compresses it if beneficial. + +---------------------------------- + +How to Handle Page Contents +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A PDF page can have zero or multiple :data:`contents` objects. These are stream objects describing **what** appears **where** and **how** on a page (like text and images). They are written in a special mini-language described e.g. in chapter "APPENDIX A - Operator Summary" on page 643 of the :ref:`AdobeManual`. + +Every PDF reader application must be able to interpret the contents syntax to reproduce the intended appearance of the page. + +If multiple :data:`contents` objects are provided, they must be interpreted in the specified sequence in exactly the same way as if they were provided as a concatenation of the several. + +There are good technical arguments for having multiple :data:`contents` objects: + +* It is a lot easier and faster to just add new :data:`contents` objects than maintaining a single big one (which entails reading, decompressing, modifying, recompressing, and rewriting it for each change). +* When working with incremental updates, a modified big :data:`contents` object will bloat the update delta and can thus easily negate the efficiency of incremental saves. + +For example, PyMuPDF adds new, small :data:`contents` objects in methods :meth:`Page.insert_image`, :meth:`Page.show_pdf_page` and the :ref:`Shape` methods. + +However, there are also situations when a **single** :data:`contents` object is beneficial: it is easier to interpret and more compressible than multiple smaller ones. + +Here are two ways of combining multiple contents of a page:: + + >>> # method 1: use the MuPDF clean function + >>> page.clean_contents() # cleans and combines multiple Contents + >>> xref = page.get_contents()[0] # only one /Contents now! + >>> cont = doc.xref_stream(xref) + >>> # this has also reformatted the PDF commands + + >>> # method 2: extract concatenated contents + >>> cont = page.read_contents() + >>> # the /Contents source itself is unmodified + +The clean function :meth:`Page.clean_contents` does a lot more than just glueing :data:`contents` objects: it also corrects and optimizes the PDF operator syntax of the page and removes any inconsistencies with the page's object definition. + +---------------------------------- + +How to Access the PDF Catalog +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This is a central ("root") object of a PDF. It serves as a starting point to reach important other objects and it also contains some global options for the PDF:: + + >>> import fitz + >>> doc=fitz.open("PyMuPDF.pdf") + >>> cat = doc.pdf_catalog() # get xref of the /Catalog + >>> print(doc.xref_object(cat)) # print object definition + << + /Type/Catalog % object type + /Pages 3593 0 R % points to page tree + /OpenAction 225 0 R % action to perform on open + /Names 3832 0 R % points to global names tree + /PageMode /UseOutlines % initially show the TOC + /PageLabels<>2<>8<>]>> % labels given to pages + /Outlines 3835 0 R % points to outline tree + >> + +.. note:: Indentation, line breaks and comments are inserted here for clarification purposes only and will not normally appear. For more information on the PDF catalog see section 7.7.2 on page 71 of the :ref:`AdobeManual`. + +---------------------------------- + +How to Access the PDF File Trailer +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The trailer of a PDF file is a :data:`dictionary` located towards the end of the file. It contains special objects, and pointers to important other information. See :ref:`AdobeManual` p. 42. Here is an overview: + +======= =========== =================================================================================== +**Key** **Type** **Value** +======= =========== =================================================================================== +Size int Number of entries in the cross-reference table + 1. +Prev int Offset to previous :data:`xref` section (indicates incremental updates). +Root dictionary (indirect) Pointer to the catalog. See previous section. +Encrypt dictionary Pointer to encryption object (encrypted files only). +Info dictionary (indirect) Pointer to information (metadata). +ID array File identifier consisting of two byte strings. +XRefStm int Offset of a cross-reference stream. See :ref:`AdobeManual` p. 49. +======= =========== =================================================================================== + +Access this information via PyMuPDF with :meth:`Document.pdf_trailer` or, equivalently, via :meth:`Document.xref_object` using -1 instead of a valid :data:`xref` number. + + >>> import fitz + >>> doc=fitz.open("PyMuPDF.pdf") + >>> print(doc.xref_object(-1)) # or: print(doc.pdf_trailer()) + << + /Type /XRef + /Index [ 0 8263 ] + /Size 8263 + /W [ 1 3 1 ] + /Root 8260 0 R + /Info 8261 0 R + /ID [ <4339B9CEE46C2CD28A79EBDDD67CC9B3> <4339B9CEE46C2CD28A79EBDDD67CC9B3> ] + /Length 19883 + /Filter /FlateDecode + >> + >>> + +---------------------------------- + +How to Access XML Metadata +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +A PDF may contain XML metadata in addition to the standard metadata format. In fact, most PDF viewer or modification software adds this type of information when saving the PDF (Adobe, Nitro PDF, PDF-XChange, etc.). + +PyMuPDF has no way to **interpret or change** this information directly, because it contains no XML features. XML metadata is however stored as a :data:`stream` object, so it can be read, modified with appropriate software and written back. + + >>> xmlmetadata = doc.get_xml_metadata() + >>> print(xmlmetadata) + + + + ... + omitted data + ... + + +Using some XML package, the XML data can be interpreted and / or modified and then stored back. The following also works, if the PDF previously had no XML metadata:: + + >>> # write back modified XML metadata: + >>> doc.set_xml_metadata(xmlmetadata) + >>> + >>> # XML metadata can be deleted like this: + >>> doc.del_xml_metadata() + +---------------------------------- + +How to Extend PDF Metadata +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Attribute :attr:`Document.metadata` is designed so it works for all :ref:`supported document types` in the same way: it is a Python dictionary with a **fixed set of key-value pairs**. Correspondingly, :meth:`Document.set_metadata` only accepts standard keys. + +However, PDFs may contain items not accessible like this. Also, there may be reasons to store additional information, like copyrights. Here is a way to handle **arbitrary metadata items** by using PyMuPDF low-level functions. + +As an example, look at this standard metadata output of some PDF:: + + # --------------------- + # standard metadata + # --------------------- + pprint(doc.metadata) + {'author': 'PRINCE', + 'creationDate': "D:2010102417034406'-30'", + 'creator': 'PrimoPDF http://www.primopdf.com/', + 'encryption': None, + 'format': 'PDF 1.4', + 'keywords': '', + 'modDate': "D:20200725062431-04'00'", + 'producer': 'macOS Version 10.15.6 (Build 19G71a) Quartz PDFContext, ' + 'AppendMode 1.1', + 'subject': '', + 'title': 'Full page fax print', + 'trapped': ''} + +Use the following code to see **all items** stored in the metadata object:: + + # ---------------------------------- + # metadata including private items + # ---------------------------------- + metadata = {} # make my own metadata dict + what, value = doc.xref_get_key(-1, "Info") # /Info key in the trailer + if what != "xref": + pass # PDF has no metadata + else: + xref = int(value.replace("0 R", "")) # extract the metadata xref + for key in doc.xref_get_keys(xref): + metadata[key] = doc.xref_get_key(xref, key)[1] + pprint(metadata) + {'Author': 'PRINCE', + 'CreationDate': "D:2010102417034406'-30'", + 'Creator': 'PrimoPDF http://www.primopdf.com/', + 'ModDate': "D:20200725062431-04'00'", + 'PXCViewerInfo': 'PDF-XChange Viewer;2.5.312.1;Feb 9 ' + "2015;12:00:06;D:20200725062431-04'00'", + 'Producer': 'macOS Version 10.15.6 (Build 19G71a) Quartz PDFContext, ' + 'AppendMode 1.1', + 'Title': 'Full page fax print'} + # --------------------------------------------------------------- + # note the additional 'PXCViewerInfo' key - ignored in standard! + # --------------------------------------------------------------- + + +*Vice versa*, you can also **store private metadata items** in a PDF. It is your responsibility to make sure that these items conform to PDF specifications - especially they must be (unicode) strings. Consult section 14.3 (p. 548) of the :ref:`AdobeManual` for details and caveats:: + + what, value = doc.xref_get_key(-1, "Info") # /Info key in the trailer + if what != "xref": + raise ValueError("PDF has no metadata") + xref = int(value.replace("0 R", "")) # extract the metadata xref + # add some private information + doc.xref_set_key(xref, "mykey", fitz.get_pdf_str("北京 is Beijing")) + # + # after executing the previous code snippet, we will see this: + pprint(metadata) + {'Author': 'PRINCE', + 'CreationDate': "D:2010102417034406'-30'", + 'Creator': 'PrimoPDF http://www.primopdf.com/', + 'ModDate': "D:20200725062431-04'00'", + 'PXCViewerInfo': 'PDF-XChange Viewer;2.5.312.1;Feb 9 ' + "2015;12:00:06;D:20200725062431-04'00'", + 'Producer': 'macOS Version 10.15.6 (Build 19G71a) Quartz PDFContext, ' + 'AppendMode 1.1', + 'Title': 'Full page fax print', + 'mykey': '北京 is Beijing'} + +To delete selected keys, use `doc.xref_set_key(xref, "mykey", "null")`. As explained in the next section, string "null" is the PDF equivalent to Python's `None`. A key with that value will be treated as not being specified -- and physically removed in garbage collections. + +---------------------------------- + +How to Read and Update PDF Objects +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. highlight:: python + + +There also exist granular, elegant ways to access and manipulate selected PDF :data:`dictionary` keys. + +* :meth:`Document.xref_get_keys` returns the PDF keys of the object at :data:`xref`:: + + In [1]: import fitz + In [2]: doc = fitz.open("pymupdf.pdf") + In [3]: page = doc[0] + In [4]: from pprint import pprint + In [5]: pprint(doc.xref_get_keys(page.xref)) + ('Type', 'Contents', 'Resources', 'MediaBox', 'Parent') + +* Compare with the full object definition:: + + In [6]: print(doc.xref_object(page.xref)) + << + /Type /Page + /Contents 1297 0 R + /Resources 1296 0 R + /MediaBox [ 0 0 612 792 ] + /Parent 1301 0 R + >> + +* Single keys can also be accessed directly via :meth:`Document.xref_get_key`. The value **always is a string** together with type information, that helps with interpreting it:: + + In [7]: doc.xref_get_key(page.xref, "MediaBox") + Out[7]: ('array', '[0 0 612 792]') + +* Here is a full listing of the above page keys:: + + In [9]: for key in doc.xref_get_keys(page.xref): + ...: print("%s = %s" % (key, doc.xref_get_key(page.xref, key))) + ...: + Type = ('name', '/Page') + Contents = ('xref', '1297 0 R') + Resources = ('xref', '1296 0 R') + MediaBox = ('array', '[0 0 612 792]') + Parent = ('xref', '1301 0 R') + +* An undefined key inquiry returns `('null', 'null')` -- PDF object type `null` corresponds to `None` in Python. Similar for the booleans `true` and `false`. +* Let us add a new key to the page definition that sets its rotation to 90 degrees (you are aware that there actually exists :meth:`Page.set_rotation` for this?):: + + In [11]: doc.xref_get_key(page.xref, "Rotate") # no rotation set: + Out[11]: ('null', 'null') + In [12]: doc.xref_set_key(page.xref, "Rotate", "90") # insert a new key + In [13]: print(doc.xref_object(page.xref)) # confirm success + << + /Type /Page + /Contents 1297 0 R + /Resources 1296 0 R + /MediaBox [ 0 0 612 792 ] + /Parent 1301 0 R + /Rotate 90 + >> + +* This method can also be used to remove a key from the :data:`xref` dictionary by setting its value to `null`: The following will remove the rotation specification from the page: `doc.xref_set_key(page.xref, "Rotate", "null")`. Similarly, to remove all links, annotations and fields from a page, use `doc.xref_set_key(page.xref, "Annots", "null")`. Because `Annots` by definition is an array, setting en empty array with the statement `doc.xref_set_key(page.xref, "Annots", "[]")` would do the same job in this case. + +* PDF dictionaries can be hierarchically nested. In the following page object definition both, `Font` and `XObject` are subdictionaries of `Resources`:: + + In [15]: print(doc.xref_object(page.xref)) + << + /Type /Page + /Contents 1297 0 R + /Resources << + /XObject << + /Im1 1291 0 R + >> + /Font << + /F39 1299 0 R + /F40 1300 0 R + >> + >> + /MediaBox [ 0 0 612 792 ] + /Parent 1301 0 R + /Rotate 90 + >> + +* The above situation **is supported** by methods :meth:`Document.xref_set_key` and :meth:`Document.xref_get_key`: use a path-like notation to point at the required key. For example, to retrieve the value of key `Im1` above, specify the complete chain of dictionaries "above" it in the key argument: `"Resources/XObject/Im1"`:: + + In [16]: doc.xref_get_key(page.xref, "Resources/XObject/Im1") + Out[16]: ('xref', '1291 0 R') + +* The path notation can also be used to **directly set a value**: use the following to let `Im1` point to a different object:: + + In [17]: doc.xref_set_key(page.xref, "Resources/XObject/Im1", "9999 0 R") + In [18]: print(doc.xref_object(page.xref)) # confirm success: + << + /Type /Page + /Contents 1297 0 R + /Resources << + /XObject << + /Im1 9999 0 R + >> + /Font << + /F39 1299 0 R + /F40 1300 0 R + >> + >> + /MediaBox [ 0 0 612 792 ] + /Parent 1301 0 R + /Rotate 90 + >> + + Be aware, that **no semantic checks** whatsoever will take place here: if the PDF has no xref 9999, it won't be detected at this point. + +* If a key does not exist, it will be created by setting its value. Moreover, if any intermediate keys do not exist either, they will also be created as necessary. The following creates an array `D` several levels below the existing dictionary `A`. Intermediate dictionaries `B` and `C` are automatically created:: + + In [5]: print(doc.xref_object(xref)) # some existing PDF object: + << + /A << + >> + >> + In [6]: # the following will create 'B', 'C' and 'D' + In [7]: doc.xref_set_key(xref, "A/B/C/D", "[1 2 3 4]") + In [8]: print(doc.xref_object(xref)) # check out what happened: + << + /A << + /B << + /C << + /D [ 1 2 3 4 ] + >> + >> + >> + >> + +* When setting key values, basic **PDF syntax checking** will be done by MuPDF. For example, new keys can only be created **below a dictionary**. The following tries to create some new string item `E` below the previously created array `D`:: + + In [9]: # 'D' is an array, no dictionary! + In [10]: doc.xref_set_key(xref, "A/B/C/D/E", "(hello)") + mupdf: not a dict (array) + --- ... --- + RuntimeError: not a dict (array) + +* It is also **not possible**, to create a key if some higher level key is an **"indirect"** object, i.e. an xref. In other words, xrefs can only be modified directly and not implicitly via other objects referencing them:: + + In [13]: # the following object points to an xref + In [14]: print(doc.xref_object(4)) + << + /E 3 0 R + >> + In [15]: # 'E' is an indirect object and cannot be modified here! + In [16]: doc.xref_set_key(4, "E/F", "90") + mupdf: path to 'F' has indirects + --- ... --- + RuntimeError: path to 'F' has indirects + +.. caution:: These are expert functions! There are no validations as to whether valid PDF objects, xrefs, etc. are specified. As with other low-level methods there is the risk to render the PDF, or parts of it unusable. + +.. include:: footer.rst diff --git a/docs/recipes-multiprocessing.rst b/docs/recipes-multiprocessing.rst new file mode 100644 index 0000000..8c4712c --- /dev/null +++ b/docs/recipes-multiprocessing.rst @@ -0,0 +1,49 @@ +.. include:: header.rst + +.. _RecipesMultiprocessing: + + +.. |toggleStart| raw:: html + +
+ See code + +.. |toggleEnd| raw:: html + +
+ +============================== +Multiprocessing +============================== + +:title:`MuPDF` has no integrated support for threading - calling itself "thread-agnostic". While there do exist tricky possibilities to still use threading with :title:`MuPDF`, the baseline consequence for :title:`PyMuPDF` is: + +**No Python threading support**. + +Using :title:`PyMuPDF` in a :title:`Python` threading environment will lead to blocking effects for the main thread. + +However, there is the option to use :title:`Python's` *multiprocessing* module in a variety of ways. + +If you are looking to speed up page-oriented processing for a large document, use this script as a starting point. It should be at least twice as fast as the corresponding sequential processing. + + +|toggleStart| + +.. literalinclude:: samples/multiprocess-render.py + :language: python + +|toggleEnd| + + +Here is a more complex example involving inter-process communication between a main process (showing a GUI) and a child process doing :title:`PyMuPDF` access to a document. + + +|toggleStart| + +.. literalinclude:: samples/multiprocess-gui.py + :language: python + +|toggleEnd| + + +.. include:: footer.rst diff --git a/docs/recipes-optional-content.rst b/docs/recipes-optional-content.rst new file mode 100644 index 0000000..17b5fae --- /dev/null +++ b/docs/recipes-optional-content.rst @@ -0,0 +1,65 @@ +.. include:: header.rst + +.. _RecipesOptionalContent: + +============================== +Optional Content Support +============================== + +This document explains PyMuPDF's support of the PDF concept **"Optional Content"**. + +Introduction: The Optional Content Concept +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +*Optional Content* in PDF is a way to show or hide parts of a document based on certain conditions: Parameters that can be set to ON or to OFF when using a supporting PDF consumer (viewer), or programmatically. + +This capability is useful in items such as CAD drawings, layered artwork, maps, and multi-language documents. Typical uses include showing or hiding details of complex vector graphics like geographical maps, technical devices, architectural designs and similar, including automatically switching between different zooming levels. Other use cases may be to automatically show different detail levels when displaying a document on screen as opposed to printing it. + +Special PDF objects, so-called **Optional Content Groups** (OCGs) are used to define these different *layers* of content. + +Assigning an OCG to a "normal" PDF object (like a text or an image) causes that object to be visible or hidden, depending on the current state of the assigned OCG. + +To ease definition of the overall configuration of a PDF's Optional Content, OCGs can be organized in higher level groupings, called **OC Configurations**. Each configuration being a collection of OCGs, together with each OCG's desired initial visibility state. Selecting one of these configurations (via the PDF viewer or programmatically) causes a corresponding visibility change of all affected PDF objects throughout the document. + +Except for the default one, OC Configurations are optional. + +For more explanations and additional background please refer to PDF specification manuals. + +PyMuPDF Support for PDF Optional Content +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +PyMuPDF offers full support for viewing, defining, changing and deleting Option Content Groups, Configurations, maintaining the assignment of OCGs to PDF objects and programmatically switching between OC Configurations and the visibility states of each single OCG. + +How to Add Optional Content +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +This is as simple as adding an Optional Content Group, OCG, to a PDF: :meth:`Document.add_ocg`. + +If previously the PDF had no OC support at all, the required setup (like defining the default OC Configuration) will be done at this point automatically. + +The method returns an :data:`xref` of the created OCG. Use this xref to associate (mark) any PDF object with it, that you want to make dependent on this OCG's state. For example, you can insert an image on a page and refer to the xref like this:: + + img_xref = page.insert_image(rect, filename="image.file", oc=xref) + +If you want to put an **existing** image under the control of an OCG, you must first find out the image's xref number (called `img_xref` here) and then do `doc.set_oc(img_xref, xref)`. After this, the image will be (in-) visible everywhere throughout the document if the OCG's state is "ON", respectively "OFF". You can also assign a different OCG with this method. + +To **remove** an OCG from an image, do `doc.set_oc(img_xref, 0)`. + +One single OCG can be assigned to mutiple PDF objects to control their visibility. + +How to Define Complex Optional Content Conditions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Sophisticated logical conditions can be established to address complex visibility needs. + +For example, you might want to create a multi-language document, so the user may switch between languages as required. + +Please have a look at `this Jupyter Notebook`_ and execute it as desired. + +Certainly, your requirements may even be more complex and involve multiple OCGs with ON/OFF states that are connected by some kind of logical relationship -- but it should give you an impression of what is possible and how to plan your next steps. + +.. include:: footer.rst + +.. External Links: + +.. _this Jupyter Notebook: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/jupyter-notebooks/optional-content.ipynb + + + diff --git a/docs/recipes-stories.rst b/docs/recipes-stories.rst new file mode 100644 index 0000000..244ecd5 --- /dev/null +++ b/docs/recipes-stories.rst @@ -0,0 +1,567 @@ +.. include:: header.rst + +.. _RecipesStories: + + +.. |toggleStart| raw:: html + +
+ See recipe + +.. |toggleEnd| raw:: html + +
+ +============================== +Stories +============================== + +This document showcases some typical use cases for :ref:`Stories`. + +As mentioned in the :ref:`tutorial`, stories may be created using up to three input sources: HTML, CSS and Archives -- all of which are optional and which, respectively, can be provided programmatically. + +The following examples will showcase combinations for using these inputs. + +.. note:: + + Many of these recipe's source code are included as examples in the `docs` folder. + + +.. _RecipesStories_A: + +How to Add a Line of Text with Some Formatting +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Here is the inevitable "Hello World" example. We will show two variants: + +1. Create using existing HTML source [#f1]_, that may come from anywhere. +2. Create using the Python API. + +----- + +Variant using an existing HTML source [#f1]_ -- which in this case is defined as a constant in the script:: + + import fitz + + HTML = """ +

Hello World!

+ """ + + MEDIABOX = fitz.paper_rect("letter") # output page format: Letter + WHERE = MEDIABOX + (36, 36, -36, -36) # leave borders of 0.5 inches + + story = fitz.Story(html=HTML) # create story from HTML + writer = fitz.DocumentWriter("output.pdf") # create the writer + + more = 1 # will indicate end of input once it is set to 0 + + while more: # loop outputting the story + device = writer.begin_page(MEDIABOX) # make new page + more, _ = story.place(WHERE) # layout into allowed rectangle + story.draw(device) # write on page + writer.end_page() # finish page + + writer.close() # close output file + +.. note:: + + The above effect (sans-serif and blue text) could have been achieved by using a separate CSS source like so:: + + import fitz + + CSS = """ + body { + font-family: sans-serif; + color: blue; + } + """ + + HTML = """ +

Hello World!

+ """ + + # the story would then be created like this: + story = fitz.Story(html=HTML, user_css=CSS) + + +----- + +The Python API variant -- everything is created programmatically:: + + import fitz + + MEDIABOX = fitz.paper_rect("letter") + WHERE = MEDIABOX + (36, 36, -36, -36) + + story = fitz.Story() # create an empty story + body = story.body # access the body of its DOM + with body.add_paragraph() as para: # store desired content + para.set_font("sans-serif").set_color("blue").add_text("Hello World!") + + writer = fitz.DocumentWriter("output.pdf") + + more = 1 + + while more: + device = writer.begin_page(MEDIABOX) + more, _ = story.place(WHERE) + story.draw(device) + writer.end_page() + + writer.close() + +Both variants will produce the same output PDF. + +----- + +.. _RecipesStories_B: + + +How to use Images +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Images can be referenced in the provided HTML source, or the reference to a desired image can also be stored via the Python API. In any case, this requires using an :ref:`Archive`, which refers to the place where the image can be found. + +.. note:: Images with the binary content embedded in the HTML source are **not supported** by stories. + +We extend our "Hello World" example from above and display an image of our planet right after the text. Assuming the image has the name "world.jpg" and is present in the script's folder, then this is the modified version of the above Python API variant:: + + import fitz + + MEDIABOX = fitz.paper_rect("letter") + WHERE = MEDIABOX + (36, 36, -36, -36) + + # create story, let it look at script folder for resources + story = fitz.Story(archive=".") + body = story.body # access the body of its DOM + + with body.add_paragraph() as para: + # store desired content + para.set_font("sans-serif").set_color("blue").add_text("Hello World!") + + # another paragraph for our image: + with body.add_paragraph() as para: + # store image in another paragraph + para.add_image("world.jpg") + + writer = fitz.DocumentWriter("output.pdf") + + more = 1 + + while more: + device = writer.begin_page(MEDIABOX) + more, _ = story.place(WHERE) + story.draw(device) + writer.end_page() + + writer.close() + + +----- + + +.. _RecipesStories_C: + + +How to Read External HTML and CSS for a Story +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These cases are fairly straightforward. + +As a general recommendation, HTML and CSS sources should be **read as binary files** and decoded before using them in a story. The Python `pathlib.Path` provides convenient ways to do this:: + + import pathlib + import fitz + + htmlpath = pathlib.Path("myhtml.html") + csspath = pathlib.Path("mycss.css") + + HTML = htmlpath.read_bytes().decode() + CSS = csspath.read_bytes().decode() + + story = fitz.Story(html=HTML, user_css=CSS) + + +----- + + +.. _RecipesStories_D: + + +How to Output Database Content with Story Templates +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This script demonstrates how to report SQL database content using an **HTML template**. + + + +The example SQL database contains two tables: + +1. Table "films" contains one row per film with the fields **"title"**, **"director"** and (release) **"year"**. +2. Table "actors" contains one row per actor and film title (fields (actor) **"name"** and (film) **"title"**). + +The story DOM consists of a template for one film, which reports film data together with a list of casted actors. + +**Files:** + +* `docs/samples/filmfestival-sql.py` +* `docs/samples/filmfestival-sql.db` + + +|toggleStart| + +.. literalinclude:: samples/filmfestival-sql.py + +|toggleEnd| + + +----- + + +.. _RecipesStories_E: + +How to Integrate with Existing PDFs +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Because a :ref:`DocumentWriter` can only write to a new file, stories cannot be placed on existing pages. This script demonstrates a circumvention of this restriction. + +The basic idea is letting :ref:`DocumentWriter` output to a PDF in memory. Once the story has finished, we re-open this memory PDF and put its pages to desired locations on **existing** pages via method :meth:`Page.show_pdf_page`. + +**Files:** + +* `docs/samples/showpdf-page.py` + +|toggleStart| + +.. literalinclude:: samples/showpdf-page.py + +|toggleEnd| + + +----- + + +.. _RecipesStories_F: + +How to Make Multi-Columned Layouts and Access Fonts from Package `pymupdf-fonts`_ +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This script outputs an article (taken from Wikipedia) that contains text and multiple images and uses a 2-column page layout. + +In addition, two "Ubuntu" font families from package `pymupdf-fonts`_ are used instead of defaulting to Base-14 fonts. + +Yet another feature used here is that all data -- the images and the article HTML -- are jointly stored in a ZIP file. + + +**Files:** + +* `docs/samples/quickfox.py` +* `docs/samples/quickfox.zip` + + +|toggleStart| + +.. literalinclude:: samples/quickfox.py + +|toggleEnd| + + +----- + + +.. _RecipesStories_G: + +How to Make a Layout which Wraps Around a Predefined "no go area" Layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +This is a demo script using PyMuPDF's Story class to output text as a PDF with +a two-column page layout. + +The script demonstrates the following features: + +* Layout text around images of an existing ("target") PDF. +* Based on a few global parameters, areas on each page are identified, that + can be used to receive text layouted by a Story. +* These global parameters are not stored anywhere in the target PDF and + must therefore be provided in some way: + + - The width of the border(s) on each page. + - The fontsize to use for text. This value determines whether the provided + text will fit in the empty spaces of the (fixed) pages of target PDF. It + cannot be predicted in any way. The script ends with an exception if + target PDF has not enough pages, and prints a warning message if not all + pages receive at least some text. In both cases, the FONTSIZE value + can be changed (a float value). + - Use of a 2-column page layout for the text. +* The layout creates a temporary (memory) PDF. Its produced page content + (the text) is used to overlay the corresponding target page. If text + requires more pages than are available in target PDF, an exception is raised. + If not all target pages receive at least some text, a warning is printed. +* The script reads "image-no-go.pdf" in its own folder. This is the "target" PDF. + It contains 2 pages with each 2 images (from the original article), which are + positioned at places that create a broad overall test coverage. Otherwise the + pages are empty. +* The script produces "quickfox-image-no-go.pdf" which contains the original pages + and image positions, but with the original article text laid out around them. + +**Files:** + +* `docs/samples/quickfox-image-no-go.py` +* `docs/samples/quickfox-image-no-go.pdf` +* `docs/samples/quickfox.zip` + + +|toggleStart| + +.. literalinclude:: samples/quickfox-image-no-go.py + +|toggleEnd| + + +----- + + +.. _RecipesStories_H: + +How to Output an HTML Table +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Outputting HTML tables is supported as follows: + +* Flat table layouts are supported ("rows x columns"), no support of the "colspan" / "rowspan" attributes. +* Table header tag :htmlTag:`th` supports attribute "scope" with values "row" or "col". Applicable text will be bold by default. +* Column widths are computed automatically based on column content. They cannot be directly set. +* Table **cells may contain images** which will be considered in the column width calculation magic. +* Row heights are computed automatically based on row content - leading to multi-line rows where needed. +* The potentially multiple lines of a table row will always be kept together on one page (respectively "where" rectangle) and not be splitted. +* Table header rows are only **shown on the first page / "where" rectangle.** +* The "style" attribute is ignored when given directly in HTML table elements. Styling for a table and its elements must happen separately, in CSS source or within the :htmlTag:`style` tag. +* Styling for :htmlTag:`tr` elements is not supported and ignored. Therefore, a table-wide grid or alternating row background colors are not supported. One of the following example scripts however shows an easy way to deal with this limitation. + +**Files:** + +* `docs/samples/table01.py` This script reflects basic features. + +|toggleStart| + +.. literalinclude:: samples/table01.py + +|toggleEnd| + +* `docs/samples/national-capitals.py` Advanced script extending table output options using simple additional code: + + - Multi-page output simulating **repeating header rows** + - Alternating table row background colors + - Table rows and columns delimited by gridlines + - Table rows dynamically generated / filled with data from an SQL database + +|toggleStart| + +.. literalinclude:: samples/national-capitals.py + +|toggleEnd| + + +----- + + +.. _RecipesStories_I: + +How to Create a Simple Grid Layout +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +By creating a sequence of :ref:`Story` objects within a grid created via the :ref:`make_table` function a developer can create grid layouts as required. + +**Files:** + +* `docs/samples/simple-grid.py` + +|toggleStart| + +.. literalinclude:: samples/simple-grid.py + +|toggleEnd| + + +----- + + +.. _RecipesStories_J: + +How to Generate a Table of Contents +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This script lists the source code of all Python scripts that live in the script's directory. + +**Files:** + +* `docs/samples/code-printer.py` + +|toggleStart| + +.. literalinclude:: samples/code-printer.py + +|toggleEnd| + + +It features the following capabilities: + +* Automatic generation of a Table of Contents (TOC) on separately numbered pages at the start of the document - using a specialized :ref:`Story`. + +* Use of 3 separate :ref:`Story` objects per page: header story, footer story and the story for printing the Python sources. + + - The page **footer is automatically changed** to show the name of the current Python file. + +* Use of :meth:`Story.element_positions` to collect the data for the TOC and for the dynamic adjustment of page footers. This is an example of a **bidirectional communication** between the story output process and the script. + +* The main PDF with the Python sources is being written to memory by its :ref:`DocumentWriter`. Another :ref:`Story` / :ref:`DocumentWriter` pair is then used to create a (memory) PDF for the TOC pages. Finally, both these PDFs are joined and the result stored to disk. + + +----- + + +.. _RecipesStories_K: + +How to Display a List from JSON Data +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This example takes some JSON data input which it uses to populate a :ref:`Story`. It also contains some visual text formatting and shows how to add links. + + +**Files:** + +* `docs/samples/json-example.py` + +|toggleStart| + +.. literalinclude:: samples/json-example.py + +|toggleEnd| + + + +----- + + +.. _RecipesStories_L: + +Using the Alternative :meth:`Story.write*()` functions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The :meth:`Story.write*()` functions provide a different way to use the +:ref:`Story` functionality, removing the need for calling code to implement +a loop that calls :meth:`Story.place()` and :meth:`Story.draw()` etc, at the +expense of having to provide at least a `rectfn()` callback. + + +.. _RecipesStories_L_a: + +How to do Basic Layout with :meth:`Story.write()` +------------------------------------------------- + +This script lays out multiple copies of its own source code, into four +rectangles per page. + +**Files:** + +* `docs/samples/story-write.py` + +|toggleStart| + +.. literalinclude:: samples/story-write.py + +|toggleEnd| + + +----- + + +.. _RecipesStories_L_b: + +How to do Iterative Layout for a Table of Contents with :meth:`Story.write_stabilized()` +---------------------------------------------------------------------------------------- + +This script creates html content dynamically, adding a contents section based +on :ref:`ElementPosition` items that have non-zero `.heading` values. + +The contents section is at the start of the document, so modifications to the +contents can change page numbers in the rest of the document, which in turn can +cause page numbers in the contents section to be incorrect. + +So the script uses :meth:`Story.write_stabilized()` to repeatedly lay things +out until things are stable. + + +**Files:** + +* `docs/samples/story-write-stabilized.py` + +|toggleStart| + +.. literalinclude:: samples/story-write-stabilized.py + +|toggleEnd| + + + +----- + +.. _RecipesStories_L_c: + +How to do Iterative Layout and Create PDF Links with :meth:`Story.write_stabilized_links()` +------------------------------------------------------------------------------------------- + +This script is similar to the one described in "How to use +:meth:`Story.write_stabilized()`" above, except that the generated PDF also +contains links that correspond to the internal links in the original html. + +This is done by using :meth:`Story.write_stabilized_links()`; this is slightly +different from :meth:`Story.write_stabilized()`: + +* It does not take a :ref:`DocumentWriter` `writer` arg. +* It returns a PDF :ref:`Document` instance. + +[The reasons for this are a little involved; for example a +:ref:`DocumentWriter` is not necessarily a PDF writer, so doesn't really work +in a PDF-specific API.] + + +**Files:** + +* `docs/samples/story-write-stabilized-links.py` + +|toggleStart| + +.. literalinclude:: samples/story-write-stabilized-links.py + +|toggleEnd| + + + +----- + + +.. rubric:: Footnotes + +.. [#f1] HTML & CSS support + + .. note:: + + At the time of writing the HTML engine for Stories is fairly basic and supports a subset of CSS2 attributes. + + Some important CSS support to consider: + + - The only available layout is relative layout. + - `background` is unavailable, use `background-color` instead. + - `float` is unavailable. + + +.. include:: footer.rst + +.. External Links: + +.. _pymupdf-fonts: https://github.com/pymupdf/pymupdf-fonts + + + diff --git a/docs/recipes-text.rst b/docs/recipes-text.rst new file mode 100644 index 0000000..667ebff --- /dev/null +++ b/docs/recipes-text.rst @@ -0,0 +1,456 @@ +.. include:: header.rst + +.. _RecipesText: + +============================== +Text +============================== + + +.. _RecipesText_A: + +How to Extract all Document Text +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This script will take a document filename and generate a text file from all of its text. + +The document can be any :ref:`supported type`. + +The script works as a command line tool which expects the document filename supplied as a parameter. It generates one text file named "filename.txt" in the script directory. Text of pages is separated by a form feed character:: + + import sys, pathlib, fitz + fname = sys.argv[1] # get document filename + with fitz.open(fname) as doc: # open document + text = chr(12).join([page.get_text() for page in doc]) + # write as a binary file to support non-ASCII characters + pathlib.Path(fname + ".txt").write_bytes(text.encode()) + +The output will be plain text as it is coded in the document. No effort is made to prettify in any way. Specifically for PDF, this may mean output not in usual reading order, unexpected line breaks and so forth. + +You have many options to rectify this -- see chapter :ref:`Appendix2`. Among them are: + +1. Extract text in HTML format and store it as a HTML document, so it can be viewed in any browser. +2. Extract text as a list of text blocks via *Page.get_text("blocks")*. Each item of this list contains position information for its text, which can be used to establish a convenient reading order. +3. Extract a list of single words via *Page.get_text("words")*. Its items are words with position information. Use it to determine text contained in a given rectangle -- see next section. + +See the following two sections for examples and further explanations. + + +.. index:: + triple: lookup;text;key-value + + +.. _RecipesText_A1: + +How to Extract Key-Value Pairs from a Page +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If the layout of a page is *"predictable"* in some sense, then there is a simple way to find the values for a given set of keywords fast and easily -- without using regular expressions. Please see `this `_ example script. + +"Predictable" in this context means: + +* Every keyword is followed by its value -- no other text is present in between them. +* The bottom of the value's boundary box is **not above** the one of the keyword. +* There are **no other restrictions**: the page layout may or may not be fixed, and the text may also have been stored as one string. Key and value may have any distance from each other. + +For example, the following five key-value pairs will be correctly identified:: + + key1 value1 + key2 + value2 + key3 + value3 blah, blah, blah key4 value4 some other text key5 value5 ... + + +.. index:: + triple: extract;text;rectangle + + +.. _RecipesText_B: + +How to Extract Text from within a Rectangle +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +There is now (v1.18.0) more than one way to achieve this. We therefore have created a `folder `_ in the PyMuPDF-Utilities repository specifically dealing with this topic. + +---------- + +.. index:: + pair: text;reading order + +.. _RecipesText_C: + +How to Extract Text in Natural Reading Order +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +One of the common issues with PDF text extraction is, that text may not appear in any particular reading order. + +This is the responsibility of the PDF creator (software or a human). For example, page headers may have been inserted in a separate step -- after the document had been produced. In such a case, the header text will appear at the end of a page text extraction (although it will be correctly shown by PDF viewer software). For example, the following snippet will add some header and footer lines to an existing PDF:: + + doc = fitz.open("some.pdf") + header = "Header" # text in header + footer = "Page %i of %i" # text in footer + for page in doc: + page.insert_text((50, 50), header) # insert header + page.insert_text( # insert footer 50 points above page bottom + (50, page.rect.height - 50), + footer % (page.number + 1, doc.page_count), + ) + +The text sequence extracted from a page modified in this way will look like this: + +1. original text +2. header line +3. footer line + +PyMuPDF has several means to re-establish some reading sequence or even to re-generate a layout close to the original: + +1. Use `sort` parameter of :meth:`Page.get_text`. It will sort the output from top-left to bottom-right (ignored for XHTML, HTML and XML output). +2. Use the `fitz` module in CLI: `python -m fitz gettext ...`, which produces a text file where text has been re-arranged in layout-preserving mode. Many options are available to control the output. + +You can also use the above mentioned `script `_ with your modifications. + +---------- + +.. _RecipesText_D: + +How to :index:`Extract Table Content ` from Documents +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +If you see a table in a document, you are not normally looking at something like an embedded Excel or other identifiable object. It usually is just text, formatted to appear as appropriate. + +Extracting a tabular data from such a page area therefore means that you must find a way to **(1)** graphically indicate table and column borders, and **(2)** then extract text based on this information. + +The wxPython GUI script `extract.py `_ strives to exactly do that. You may want to have a look at it and adjust it to your liking. + +---------- + +.. _RecipesText_E: + +How to Mark Extracted Text +~~~~~~~~~~~~~~~~~~~~~~~~~~ +There is a standard search function to search for arbitrary text on a page: :meth:`Page.search_for`. It returns a list of :ref:`Rect` objects which surround a found occurrence. These rectangles can for example be used to automatically insert annotations which visibly mark the found text. + +This method has advantages and drawbacks. Pros are: + +* The search string can contain blanks and wrap across lines +* Upper or lower case characters are treated equal +* Word hyphenation at line ends is detected and resolved +* Return may also be a list of :ref:`Quad` objects to precisely locate text that is **not parallel** to either axis -- using :ref:`Quad` output is also recommended, when page rotation is not zero. + +But you also have other options:: + + import sys + import fitz + + def mark_word(page, text): + """Underline each word that contains 'text'. + """ + found = 0 + wlist = page.get_text("words") # make the word list + for w in wlist: # scan through all words on page + if text in w[4]: # w[4] is the word's string + found += 1 # count + r = fitz.Rect(w[:4]) # make rect from word bbox + page.add_underline_annot(r) # underline + return found + + fname = sys.argv[1] # filename + text = sys.argv[2] # search string + doc = fitz.open(fname) + + print("underlining words containing '%s' in document '%s'" % (word, doc.name)) + + new_doc = False # indicator if anything found at all + + for page in doc: # scan through the pages + found = mark_word(page, text) # mark the page's words + if found: # if anything found ... + new_doc = True + print("found '%s' %i times on page %i" % (text, found, page.number + 1)) + + if new_doc: + doc.save("marked-" + doc.name) + +This script uses `Page.get_text("words")` to look for a string, handed in via cli parameter. This method separates a page's text into "words" using spaces and line breaks as delimiters. Further remarks: + +* If found, the **complete word containing the string** is marked (underlined) -- not only the search string. +* The search string may **not contain spaces** or other white space. +* As shown here, upper / lower cases are **respected**. But this can be changed by using the string method *lower()* (or even regular expressions) in function *mark_word*. +* There is **no upper limit**: all occurrences will be detected. +* You can use **anything** to mark the word: 'Underline', 'Highlight', 'StrikeThrough' or 'Square' annotations, etc. +* Here is an example snippet of a page of this manual, where "MuPDF" has been used as the search string. Note that all strings **containing "MuPDF"** have been completely underlined (not just the search string). + +.. image:: images/img-markedpdf.* + :scale: 60 + +---------------------------------------------- + + +.. _RecipesText_F: + +How to Mark Searched Text +~~~~~~~~~~~~~~~~~~~~~~~~~~ +This script searches for text and marks it:: + + # -*- coding: utf-8 -*- + import fitz + + # the document to annotate + doc = fitz.open("tilted-text.pdf") + + # the text to be marked + t = "¡La práctica hace el campeón!" + + # work with first page only + page = doc[0] + + # get list of text locations + # we use "quads", not rectangles because text may be tilted! + rl = page.search_for(t, quads = True) + + # mark all found quads with one annotation + page.add_squiggly_annot(rl) + + # save to a new PDF + doc.save("a-squiggly.pdf") + +The result looks like this: + +.. image:: images/img-textmarker.* + :scale: 80 + +---------------------------------------------- + + +.. _RecipesText_G: + +How to Mark Non-horizontal Text +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +The previous section already shows an example for marking non-horizontal text, that was detected by text **searching**. + +But text **extraction** with the "dict" / "rawdict" options of :meth:`Page.get_text` may also return text with a non-zero angle to the x-axis. This is indicated by the value of the line dictionary's `"dir"` key: it is the tuple `(cosine, sine)` for that angle. If `line["dir"] != (1, 0)`, then the text of all its spans is rotated by (the same) angle != 0. + +The "bboxes" returned by the method however are rectangles only -- not quads. So, to mark span text correctly, its quad must be recovered from the data contained in the line and span dictionary. Do this with the following utility function (new in v1.18.9):: + + span_quad = fitz.recover_quad(line["dir"], span) + annot = page.add_highlight_annot(span_quad) # this will mark the complete span text + +If you want to **mark the complete line** or a subset of its spans in one go, use the following snippet (works for v1.18.10 or later):: + + line_quad = fitz.recover_line_quad(line, spans=line["spans"][1:-1]) + page.add_highlight_annot(line_quad) + +.. image:: images/img-linequad.* + +The `spans` argument above may specify any sub-list of `line["spans"]`. In the example above, the second to second-to-last span are marked. If omitted, the complete line is taken. + +------------------------------ + +.. _RecipesText_H: + +How to Analyze Font Characteristics +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +To analyze the characteristics of text in a PDF use this elementary script as a starting point: + +.. literalinclude:: samples/text-lister.py + :language: python + +Here is the PDF page and the script output: + +.. image:: images/img-pdftext.* + :scale: 80 + +----------------------------------------- + + +.. _RecipesText_I: + +How to Insert Text +~~~~~~~~~~~~~~~~~~~~ +PyMuPDF provides ways to insert text on new or existing PDF pages with the following features: + +* choose the font, including built-in fonts and fonts that are available as files +* choose text characteristics like bold, italic, font size, font color, etc. +* position the text in multiple ways: + + - either as simple line-oriented output starting at a certain point, + - or fitting text in a box provided as a rectangle, in which case text alignment choices are also available, + - choose whether text should be put in foreground (overlay existing content), + - all text can be arbitrarily "morphed", i.e. its appearance can be changed via a :ref:`Matrix`, to achieve effects like scaling, shearing or mirroring, + - independently from morphing and in addition to that, text can be rotated by integer multiples of 90 degrees. + +All of the above is provided by three basic :ref:`Page`, resp. :ref:`Shape` methods: + +* :meth:`Page.insert_font` -- install a font for the page for later reference. The result is reflected in the output of :meth:`Document.get_page_fonts`. The font can be: + + - provided as a file, + - via :ref:`Font` (then use :attr:`Font.buffer`) + - already present somewhere in **this or another** PDF, or + - be a **built-in** font. + +* :meth:`Page.insert_text` -- write some lines of text. Internally, this uses :meth:`Shape.insert_text`. + +* :meth:`Page.insert_textbox` -- fit text in a given rectangle. Here you can choose text alignment features (left, right, centered, justified) and you keep control as to whether text actually fits. Internally, this uses :meth:`Shape.insert_textbox`. + +.. note:: Both text insertion methods automatically install the font as necessary. + + +.. _RecipesText_I_a: + +How to Write Text Lines +^^^^^^^^^^^^^^^^^^^^^^^^^^ +Output some text lines on a page:: + + import fitz + doc = fitz.open(...) # new or existing PDF + page = doc.new_page() # new or existing page via doc[n] + p = fitz.Point(50, 72) # start point of 1st line + + text = "Some text,\nspread across\nseveral lines." + # the same result is achievable by + # text = ["Some text", "spread across", "several lines."] + + rc = page.insert_text(p, # bottom-left of 1st char + text, # the text (honors '\n') + fontname = "helv", # the default font + fontsize = 11, # the default font size + rotate = 0, # also available: 90, 180, 270 + ) + print("%i lines printed on page %i." % (rc, page.number)) + + doc.save("text.pdf") + +With this method, only the **number of lines** will be controlled to not go beyond page height. Surplus lines will not be written and the number of actual lines will be returned. The calculation uses a line height calculated from the fontsize and 36 points (0.5 inches) as bottom margin. + +Line **width is ignored**. The surplus part of a line will simply be invisible. + +However, for built-in fonts there are ways to calculate the line width beforehand - see :meth:`get_text_length`. + +Here is another example. It inserts 4 text strings using the four different rotation options, and thereby explains, how the text insertion point must be chosen to achieve the desired result:: + + import fitz + doc = fitz.open() + page = doc.new_page() + # the text strings, each having 3 lines + text1 = "rotate=0\nLine 2\nLine 3" + text2 = "rotate=90\nLine 2\nLine 3" + text3 = "rotate=-90\nLine 2\nLine 3" + text4 = "rotate=180\nLine 2\nLine 3" + red = (1, 0, 0) # the color for the red dots + # the insertion points, each with a 25 pix distance from the corners + p1 = fitz.Point(25, 25) + p2 = fitz.Point(page.rect.width - 25, 25) + p3 = fitz.Point(25, page.rect.height - 25) + p4 = fitz.Point(page.rect.width - 25, page.rect.height - 25) + # create a Shape to draw on + shape = page.new_shape() + + # draw the insertion points as red, filled dots + shape.draw_circle(p1,1) + shape.draw_circle(p2,1) + shape.draw_circle(p3,1) + shape.draw_circle(p4,1) + shape.finish(width=0.3, color=red, fill=red) + + # insert the text strings + shape.insert_text(p1, text1) + shape.insert_text(p3, text2, rotate=90) + shape.insert_text(p2, text3, rotate=-90) + shape.insert_text(p4, text4, rotate=180) + + # store our work to the page + shape.commit() + doc.save(...) + +This is the result: + +.. image:: images/img-inserttext.* + :scale: 33 + + + +------------------------------------------ + +.. _RecipesText_I_b: + +How to Fill a Text Box +^^^^^^^^^^^^^^^^^^^^^^^^^^ +This script fills 4 different rectangles with text, each time choosing a different rotation value:: + + import fitz + doc = fitz.open(...) # new or existing PDF + page = doc.new_page() # new page, or choose doc[n] + r1 = fitz.Rect(50,100,100,150) # a 50x50 rectangle + disp = fitz.Rect(55, 0, 55, 0) # add this to get more rects + r2 = r1 + disp # 2nd rect + r3 = r1 + disp * 2 # 3rd rect + r4 = r1 + disp * 3 # 4th rect + t1 = "text with rotate = 0." # the texts we will put in + t2 = "text with rotate = 90." + t3 = "text with rotate = -90." + t4 = "text with rotate = 180." + red = (1,0,0) # some colors + gold = (1,1,0) + blue = (0,0,1) + """We use a Shape object (something like a canvas) to output the text and + the rectangles surrounding it for demonstration. + """ + shape = page.new_shape() # create Shape + shape.draw_rect(r1) # draw rectangles + shape.draw_rect(r2) # giving them + shape.draw_rect(r3) # a yellow background + shape.draw_rect(r4) # and a red border + shape.finish(width = 0.3, color = red, fill = gold) + # Now insert text in the rectangles. Font "Helvetica" will be used + # by default. A return code rc < 0 indicates insufficient space (not checked here). + rc = shape.insert_textbox(r1, t1, color = blue) + rc = shape.insert_textbox(r2, t2, color = blue, rotate = 90) + rc = shape.insert_textbox(r3, t3, color = blue, rotate = -90) + rc = shape.insert_textbox(r4, t4, color = blue, rotate = 180) + shape.commit() # write all stuff to page /Contents + doc.save("...") + +Several default values were used above: font "Helvetica", font size 11 and text alignment "left". The result will look like this: + +.. image:: images/img-textbox.* + :scale: 50 + +------------------------------------------ + +.. _RecipesText_I_c: + +How to Use Non-Standard Encoding +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Since v1.14, MuPDF allows Greek and Russian encoding variants for the :data:`Base14_Fonts`. In PyMuPDF this is supported via an additional *encoding* argument. Effectively, this is relevant for Helvetica, Times-Roman and Courier (and their bold / italic forms) and characters outside the ASCII code range only. Elsewhere, the argument is ignored. Here is how to request Russian encoding with the standard font Helvetica:: + + page.insert_text(point, russian_text, encoding=fitz.TEXT_ENCODING_CYRILLIC) + +The valid encoding values are TEXT_ENCODING_LATIN (0), TEXT_ENCODING_GREEK (1), and TEXT_ENCODING_CYRILLIC (2, Russian) with Latin being the default. Encoding can be specified by all relevant font and text insertion methods. + +By the above statement, the fontname *helv* is automatically connected to the Russian font variant of Helvetica. Any subsequent text insertion with **this fontname** will use the Russian Helvetica encoding. + +If you change the fontname just slightly, you can also achieve an **encoding "mixture"** for the **same base font** on the same page:: + + import fitz + doc=fitz.open() + page = doc.new_page() + shape = page.new_shape() + t="Sômé tèxt wìth nöñ-Lâtîn characterß." + shape.insert_text((50,70), t, fontname="helv", encoding=fitz.TEXT_ENCODING_LATIN) + shape.insert_text((50,90), t, fontname="HElv", encoding=fitz.TEXT_ENCODING_GREEK) + shape.insert_text((50,110), t, fontname="HELV", encoding=fitz.TEXT_ENCODING_CYRILLIC) + shape.commit() + doc.save("t.pdf") + +The result: + +.. image:: images/img-encoding.* + :scale: 50 + +The snippet above indeed leads to three different copies of the Helvetica font in the PDF. Each copy is uniquely identified (and referenceable) by using the correct upper-lower case spelling of the reserved word "helv":: + + for f in doc.get_page_fonts(0): print(f) + + [6, 'n/a', 'Type1', 'Helvetica', 'helv', 'WinAnsiEncoding'] + [7, 'n/a', 'Type1', 'Helvetica', 'HElv', 'WinAnsiEncoding'] + [8, 'n/a', 'Type1', 'Helvetica', 'HELV', 'WinAnsiEncoding'] + +.. include:: footer.rst diff --git a/docs/recipes.rst b/docs/recipes.rst new file mode 100644 index 0000000..54bd4ee --- /dev/null +++ b/docs/recipes.rst @@ -0,0 +1,71 @@ +.. include:: header.rst + +.. title:: PyMuPDF: How to Guide + + +.. _RecipesTOC: + + +.. toctree:: + + recipes-text.rst + + +---- + +.. toctree:: + + recipes-images.rst + + +---- + +.. toctree:: + + recipes-annotations.rst + + +---- + +.. toctree:: + + recipes-drawing-and-graphics.rst + +---- + +.. toctree:: + + recipes-stories.rst + +---- + +.. toctree:: + + recipes-journalling.rst + +---- + +.. toctree:: + + recipes-multiprocessing.rst + +---- + +.. toctree:: + + recipes-optional-content.rst + +---- + +.. toctree:: + + recipes-low-level-interfaces.rst + +---- + +.. toctree:: + + recipes-common-issues-and-their-solutions.rst + + +.. include:: footer.rst diff --git a/docs/rect.rst b/docs/rect.rst new file mode 100644 index 0000000..d3fd55c --- /dev/null +++ b/docs/rect.rst @@ -0,0 +1,315 @@ +.. include:: header.rst + +.. _Rect: + +========== +Rect +========== + +*Rect* represents a rectangle defined by four floating point numbers x0, y0, x1, y1. They are treated as being coordinates of two diagonally opposite points. The first two numbers are regarded as the "top left" corner P\ :sub:`(x0,y0)` and P\ :sub:`(x1,y1)` as the "bottom right" one. However, these two properties need not coincide with their intuitive meanings -- read on. + +The following remarks are also valid for :ref:`IRect` objects: + +* A rectangle in the sense of (Py-) MuPDF **(and PDF)** always has **borders parallel to the x- resp. y-axis**. A general orthogonal tetragon **is not a rectangle** -- in contrast to the mathematical definition. +* The constructing points can be (almost! -- see below) anywhere in the plane -- they need not even be different, and e.g. "top left" need not be the geometrical "north-western" point. +* For any given quadruple of numbers, the geometrically "same" rectangle can be defined in four different ways: + 1. Rect(P\ :sub:`(x0,y0)`, P\ :sub:`(x1,y1)`\ ) + 2. Rect(P\ :sub:`(x1,y1)`, P\ :sub:`(x0,y0)`\ ) + 3. Rect(P\ :sub:`(x0,y1)`, P\ :sub:`(x1,y0)`\ ) + 4. Rect(P\ :sub:`(x1,y0)`, P\ :sub:`(x0,y1)`\ ) + +**(Changed in v1.19.0)** Hence some classification: + +* A rectangle is called **valid** if `x0 <= x1` and `y0 <= y1` (i.e. the bottom right point is "south-eastern" to the top left one), otherwise **invalid**. Of the four alternatives above, **only the first** is valid. Please take into account, that in MuPDF's coordinate system, the y-axis is oriented from **top to bottom**. Invalid rectangles have been called infinite in earlier versions. + +* A rectangle is called **empty** if `x0 >= x1` or `y0 >= y1`. This implies, that **invalid rectangles are also always empty.** And `width` (resp. `height`) is **set to zero** if `x0 > x1` (resp. `y0 > y1`). In previous versions, a rectangle was empty only if one of width or height was zero. + +* Rectangle coordinates **cannot be outside** the number range from `FZ_MIN_INF_RECT = -2147483648` to `FZ_MAX_INF_RECT = 2147483520`. Both values have been chosen, because they are the smallest / largest 32bit integers that survive C float conversion roundtrips. In previous versions there was no limit for coordinate values. + +* There is **exactly one "infinite" rectangle**, defined by `x0 = y0 = FZ_MIN_INF_RECT` and `x1 = y1 = FZ_MAX_INF_RECT`. It contains every other rectangle. It is mainly used for technical purposes -- e.g. when a function call should ignore a formally required rectangle argument. This rectangle is not empty. + +* **Rectangles are (semi-) open:** The right and the bottom edges (including the resp. corners) are not considered part of the rectangle. This implies, that only the top-left corner `(x0, y0)` can ever belong to the rectangle - the other three corners never do. An empty rectangle contains no corners at all. + + .. image:: images/img-rect-contains.* + :scale: 30 + :align: center + +* Here is an overview of the changes. + + ================= =================================== ================================================== + Notion Versions < 1.19.0 Versions 1.19.* + ================= =================================== ================================================== + empty x0 = x1 or y0 = y1 x0 >= x1 or y0 >= y1 -- includes invalid rects + valid n/a x0 <= x1 and y0 <= y1 + infinite all rects where x0 > x1 or y1 > y0 **exactly one infinite rect / irect!** + coordinate values all numbers `FZ_MIN_INF_RECT <= number <= FZ_MAX_INF_RECT` + borders, corners are parts of the rectangle right and bottom corners and edges **are outside** + ================= =================================== ================================================== + +* There are new top level functions defining infinite and standard empty rectangles and quads, see :meth:`INFINITE_RECT` and friends. + + +============================= ======================================================= +**Methods / Attributes** **Short Description** +============================= ======================================================= +:meth:`Rect.contains` checks containment of point_likes and rect_likes +:meth:`Rect.get_area` calculate rectangle area +:meth:`Rect.include_point` enlarge rectangle to also contain a point +:meth:`Rect.include_rect` enlarge rectangle to also contain another one +:meth:`Rect.intersect` common part with another rectangle +:meth:`Rect.intersects` checks for non-empty intersections +:meth:`Rect.morph` transform with a point and a matrix +:meth:`Rect.torect` the matrix that transforms to another rectangle +:meth:`Rect.norm` the Euclidean norm +:meth:`Rect.normalize` makes a rectangle valid +:meth:`Rect.round` create smallest :ref:`Irect` containing rectangle +:meth:`Rect.transform` transform rectangle with a matrix +:attr:`Rect.bottom_left` bottom left point, synonym *bl* +:attr:`Rect.bottom_right` bottom right point, synonym *br* +:attr:`Rect.height` rectangle height +:attr:`Rect.irect` equals result of method *round()* +:attr:`Rect.is_empty` whether rectangle is empty +:attr:`Rect.is_valid` whether rectangle is valid +:attr:`Rect.is_infinite` whether rectangle is infinite +:attr:`Rect.top_left` top left point, synonym *tl* +:attr:`Rect.top_right` top_right point, synonym *tr* +:attr:`Rect.quad` :ref:`Quad` made from rectangle corners +:attr:`Rect.width` rectangle width +:attr:`Rect.x0` left corners' x coordinate +:attr:`Rect.x1` right corners' x -coordinate +:attr:`Rect.y0` top corners' y coordinate +:attr:`Rect.y1` bottom corners' y coordinate +============================= ======================================================= + +**Class API** + +.. class:: Rect + + .. method:: __init__(self) + + .. method:: __init__(self, x0, y0, x1, y1) + + .. method:: __init__(self, top_left, bottom_right) + + .. method:: __init__(self, top_left, x1, y1) + + .. method:: __init__(self, x0, y0, bottom_right) + + .. method:: __init__(self, rect) + + .. method:: __init__(self, sequence) + + Overloaded constructors: *top_left*, *bottom_right* stand for :data:`point_like` objects, "sequence" is a Python sequence type of 4 numbers (see :ref:`SequenceTypes`), "rect" means another :data:`rect_like`, while the other parameters mean coordinates. + + If "rect" is specified, the constructor creates a **new copy** of it. + + Without parameters, the empty rectangle *Rect(0.0, 0.0, 0.0, 0.0)* is created. + + .. method:: round() + + Creates the smallest containing :ref:`IRect`. This is **not** the same as simply rounding the rectangle's edges: The top left corner is rounded upwards and to the left while the bottom right corner is rounded downwards and to the right. + + >>> fitz.Rect(0.5, -0.01, 123.88, 455.123456).round() + IRect(0, -1, 124, 456) + + 1. If the rectangle is **empty**, the result is also empty. + 2. **Possible paradox:** The result may be empty, **even if** the rectangle is **not** empty! In such cases, the result obviously does **not** contain the rectangle. This is because MuPDF's algorithm allows for a small tolerance (1e-3). Example: + + >>> r = fitz.Rect(100, 100, 200, 100.001) + >>> r.is_empty # rect is NOT empty + False + >>> r.round() # but its irect IS empty! + fitz.IRect(100, 100, 200, 100) + >>> r.round().is_empty + True + + :rtype: :ref:`IRect` + + .. method:: transform(m) + + Transforms the rectangle with a matrix and **replaces the original**. If the rectangle is empty or infinite, this is a no-operation. + + :arg m: The matrix for the transformation. + :type m: :ref:`Matrix` + + :rtype: *Rect* + :returns: the smallest rectangle that contains the transformed original. + + .. method:: intersect(r) + + The intersection (common rectangular area, largest rectangle contained in both) of the current rectangle and *r* is calculated and **replaces the current** rectangle. If either rectangle is empty, the result is also empty. If *r* is infinite, this is a no-operation. If the rectangles are (mathematically) disjoint sets, then the result is invalid. If the result is valid but empty, then the rectangles touch each other in a corner or (part of) a side. + + :arg r: Second rectangle + :type r: :ref:`Rect` + + .. method:: include_rect(r) + + The smallest rectangle containing the current one and *r* is calculated and **replaces the current** one. If either rectangle is infinite, the result is also infinite. If one is empty, the other one will be taken as the result. + + :arg r: Second rectangle + :type r: :ref:`Rect` + + .. method:: include_point(p) + + The smallest rectangle containing the current one and point *p* is calculated and **replaces the current** one. **The infinite rectangle remains unchanged.** To create a rectangle containing a series of points, start with (the empty) *fitz.Rect(p1, p1)* and successively include the remaining points. + + :arg p: Point to include. + :type p: :ref:`Point` + + + .. method:: get_area([unit]) + + Calculate the area of the rectangle and, with no parameter, equals *abs(rect)*. Like an empty rectangle, the area of an infinite rectangle is also zero. So, at least one of *fitz.Rect(p1, p2)* and *fitz.Rect(p2, p1)* has a zero area. + + :arg str unit: Specify required unit: respective squares of *px* (pixels, default), *in* (inches), *cm* (centimeters), or *mm* (millimeters). + :rtype: float + + .. method:: contains(x) + + Checks whether *x* is contained in the rectangle. It may be an *IRect*, *Rect*, *Point* or number. If *x* is an empty rectangle, this is always true. If the rectangle is empty this is always *False* for all non-empty rectangles and for all points. `x in rect` and `rect.contains(x)` are equivalent. + + :arg x: the object to check. + :type x: :data:`rect_like` or :data:`point_like`. + + :rtype: bool + + .. method:: intersects(r) + + Checks whether the rectangle and a :data:`rect_like` "r" contain a common non-empty :ref:`Rect`. This will always be *False* if either is infinite or empty. + + :arg rect_like r: the rectangle to check. + + :rtype: bool + + .. method:: torect(rect) + + * New in version 1.19.3 + + Compute the matrix which transforms this rectangle to a given one. + + :arg rect_like rect: the target rectangle. Must not be empty or infinite. + :rtype: :ref:`Matrix` + :returns: a matrix `mat` such that `self * mat = rect`. Can for example be used to transform between the page and the pixmap coordinates. See an example use here :ref:`RecipesImages_P`. + + .. method:: morph(fixpoint, matrix) + + * New in version 1.17.0 + + Return a new quad after applying a matrix to the rectangle using the fixed point `fixpoint`. + + :arg point_like fixpoint: the fixed point. + :arg matrix_like matrix: the matrix. + :returns: a new :ref:`Quad`. This a wrapper for the same-named quad method. If infinite, the infinite quad is returned. + + .. method:: norm() + + * New in version 1.16.0 + + Return the Euclidean norm of the rectangle treated as a vector of four numbers. + + .. method:: normalize() + + **Replace** the rectangle with its valid version. This is done by shuffling the rectangle corners. After completion of this method, the bottom right corner will indeed be south-eastern to the top left one (but may still be empty). + + .. attribute:: irect + + Equals result of method *round()*. + + .. attribute:: top_left + + .. attribute:: tl + + Equals *Point(x0, y0)*. + + :type: :ref:`Point` + + .. attribute:: top_right + + .. attribute:: tr + + Equals `Point(x1, y0)`. + + :type: :ref:`Point` + + .. attribute:: bottom_left + + .. attribute:: bl + + Equals `Point(x0, y1)`. + + :type: :ref:`Point` + + .. attribute:: bottom_right + + .. attribute:: br + + Equals `Point(x1, y1)`. + + :type: :ref:`Point` + + .. attribute:: quad + + The quadrilateral `Quad(rect.tl, rect.tr, rect.bl, rect.br)`. + + :type: :ref:`Quad` + + .. attribute:: width + + Width of the rectangle. Equals `max(x1 - x0, 0)`. + + :rtype: float + + .. attribute:: height + + Height of the rectangle. Equals `max(y1 - y0, 0)`. + + :rtype: float + + .. attribute:: x0 + + X-coordinate of the left corners. + + :type: float + + .. attribute:: y0 + + Y-coordinate of the top corners. + + :type: float + + .. attribute:: x1 + + X-coordinate of the right corners. + + :type: float + + .. attribute:: y1 + + Y-coordinate of the bottom corners. + + :type: float + + .. attribute:: is_infinite + + `True` if this is the infinite rectangle. + + :type: bool + + .. attribute:: is_empty + + `True` if rectangle is empty. + + :type: bool + + .. attribute:: is_valid + + `True` if rectangle is valid. + + :type: bool + +.. note:: + + * This class adheres to the Python sequence protocol, so components can be accessed via their index, too. Also refer to :ref:`SequenceTypes`. + * Rectangles can be used with arithmetic operators -- see chapter :ref:`Algebra`. + +.. include:: footer.rst diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000..15bf523 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,6 @@ +rst2pdf + +# define sphinx versioning +sphinx==5.3.0 +furo +readthedocs-sphinx-search==0.1.1 diff --git a/docs/samples/code-printer.py b/docs/samples/code-printer.py new file mode 100644 index 0000000..8aa54b4 --- /dev/null +++ b/docs/samples/code-printer.py @@ -0,0 +1,247 @@ +""" +Demo script PyMuPDF Story class +------------------------------- + +Read the Python sources in the script directory and create a PDF of all their +source codes. + +The following features are included as a specialty: +1. HTML source for fitz.Story created via Python API exclusively +2. Separate Story objects for page headers and footers +3. Use of HTML "id" elements for identifying source start pages +4. Generate a Table of Contents pointing to source file starts. This + - uses the new Stoy callback feature + - uses Story also for making the TOC page(s) + +""" +import io +import os +import time + +import fitz + +THISDIR = os.path.dirname(os.path.abspath(__file__)) +TOC = [] # this will contain the TOC list items +CURRENT_ID = "" # currently processed filename - stored by recorder func +MEDIABOX = fitz.paper_rect("a4-l") # chosen page size +WHERE = MEDIABOX + (36, 50, -36, -36) # sub rectangle for source content +# location of the header rectangle +HDR_WHERE = (36, 5, MEDIABOX.width - 36, 40) +# location of the footer rectangle +FTR_WHERE = (36, MEDIABOX.height - 36, MEDIABOX.width - 36, MEDIABOX.height) + + +def recorder(elpos): + """Callback function invoked during story.place(). + This function generates / collects all TOC items and updates the value of + CURRENT_ID - which is used to update the footer line of each page. + """ + global TOC, CURRENT_ID + if not elpos.open_close & 1: # only consider "open" items + return + level = elpos.heading + y0 = elpos.rect[1] # top of written rectangle (use for TOC) + if level > 0: # this is a header (h1 - h6) + pno = elpos.page + 1 # the page number + TOC.append( + ( + level, + elpos.text, + elpos.page + 1, + y0, + ) + ) + return + + CURRENT_ID = elpos.id if elpos.id else "" # update for footer line + return + + +def header_story(text): + """Make the page header""" + header = fitz.Story() + hdr_body = header.body + hdr_body.add_paragraph().set_properties( + align=fitz.fitz.TEXT_ALIGN_CENTER, + bgcolor="#eee", + font="sans-serif", + bold=True, + fontsize=12, + color="green", + ).add_text(text) + return header + + +def footer_story(text): + """Make the page footer""" + footer = fitz.Story() + ftr_body = footer.body + ftr_body.add_paragraph().set_properties( + bgcolor="#eee", + align=fitz.TEXT_ALIGN_CENTER, + color="blue", + fontsize=10, + font="sans-serif", + ).add_text(text) + return footer + + +def code_printer(outfile): + """Output the generated PDF to outfile.""" + global MAX_TITLE_LEN + where = +WHERE + writer = fitz.DocumentWriter(outfile, "") + print_time = time.strftime("%Y-%m-%d %H:%M:%S (%z)") + thispath = os.path.abspath(os.curdir) + basename = os.path.basename(thispath) + + story = fitz.Story() + body = story.body + body.set_properties(font="sans-serif") + + text = f"Python sources in folder '{THISDIR}'" + + body.add_header(1).add_text(text) # the only h1 item in the story + + files = os.listdir(THISDIR) # list / select Python files in our directory + i = 1 + for code_file in files: + if not code_file.endswith(".py"): + continue + + # read Python file source + fileinput = open(os.path.join(THISDIR, code_file), "rb") + text = fileinput.read().decode() + fileinput.close() + + # make level 2 header + hdr = body.add_header(2) + if i > 1: + hdr.set_pagebreak_before() + hdr.add_text(f"{i}. Listing of file '{code_file}'") + + # Write the file code + body.add_codeblock().set_bgcolor((240, 255, 210)).set_color("blue").set_id( + code_file + ).set_fontsize(10).add_text(text) + + # Indicate end of a source file + body.add_paragraph().set_align(fitz.TEXT_ALIGN_CENTER).add_text( + f"---------- End of File '{code_file}' ----------" + ) + i += 1 # update file counter + + i = 0 + while True: + i += 1 + device = writer.begin_page(MEDIABOX) + # create Story objects for header, footer and the rest. + header = header_story(f"Python Files in '{THISDIR}'") + hdr_ok, _ = header.place(HDR_WHERE) + if hdr_ok != 0: + raise ValueError("header does not fit") + header.draw(device, None) + + # -------------------------------------------------------------- + # Write the file content. + # -------------------------------------------------------------- + more, filled = story.place(where) + # Inform the callback function + # Args: + # recorder: the Python function to call + # {}: dictionary containing anything - we pass the page number + story.element_positions(recorder, {"page": i - 1}) + story.draw(device, None) + + # -------------------------------------------------------------- + # Make / write page footer. + # We MUST have a paragraph b/o background color / alignment + # -------------------------------------------------------------- + if CURRENT_ID: + text = f"File '{CURRENT_ID}' printed at {print_time}{chr(160)*5}{'-'*10}{chr(160)*5}Page {i}" + else: + text = f"Printed at {print_time}{chr(160)*5}{'-'*10}{chr(160)*5}Page {i}" + footer = footer_story(text) + # write the page footer + ftr_ok, _ = footer.place(FTR_WHERE) + if ftr_ok != 0: + raise ValueError("footer does not fit") + footer.draw(device, None) + + writer.end_page() + if more == 0: + break + writer.close() + + +if __name__ == "__main__" or os.environ.get('PYTEST_CURRENT_TEST'): + fileptr1 = io.BytesIO() + t0 = time.perf_counter() + code_printer(fileptr1) # make the PDF + t1 = time.perf_counter() + doc = fitz.open("pdf", fileptr1) + old_count = doc.page_count + # ----------------------------------------------------------------------------- + # Post-processing step to make / insert the toc + # This also works using fitz.Story: + # - make a new PDF in memory which contains pages with the TOC text + # - add these TOC pages to the end of the original file + # - search item text on the inserted pages and cover each with a PDF link + # - move the TOC pages to the front of the document + # ----------------------------------------------------------------------------- + story = fitz.Story() + body = story.body + body.add_header(1).set_font("sans-serif").add_text("Table of Contents") + # prefix TOC with an entry pointing to this page + TOC.insert(0, [1, "Table of Contents", old_count + 1, 36]) + + for item in TOC[1:]: # write the file name headers as TOC lines + body.add_paragraph().set_font("sans-serif").add_text( + item[1] + f" - ({item[2]})" + ) + fileptr2 = io.BytesIO() # put TOC pages to a separate PDF initially + writer = fitz.DocumentWriter(fileptr2) + i = 1 + more = 1 + while more: + device = writer.begin_page(MEDIABOX) + header = header_story(f"Python Files in '{THISDIR}'") + # write the page header + hdr_ok, _ = header.place(HDR_WHERE) + header.draw(device, None) + + more, filled = story.place(WHERE) + story.draw(device, None) + + footer = footer_story(f"TOC-{i}") # separate page numbering scheme + # write the page footer + ftr_ok, _ = footer.place(FTR_WHERE) + footer.draw(device, None) + writer.end_page() + i += 1 + + writer.close() + doc2 = fitz.open("pdf", fileptr2) # open TOC pages as another PDF + doc.insert_pdf(doc2) # and append to the main PDF + new_range = range(old_count, doc.page_count) # the TOC page numbers + pages = [doc[i] for i in new_range] # these are the TOC pages within main PDF + for item in TOC: # search for TOC item text to get its rectangle + for page in pages: + rl = page.search_for(item[1], flags=~fitz.TEXT_PRESERVE_LIGATURES) + if rl != []: # this text must be on next page + break + rect = rl[0] # rectangle of TOC item text + link = { # make a link from it + "kind": fitz.LINK_GOTO, + "from": rect, + "to": fitz.Point(0, item[3]), + "page": item[2] - 1, + } + page.insert_link(link) + + # insert the TOC in the main PDF + doc.set_toc(TOC) + # move all the TOC pages to the desired place (1st page here) + for i in new_range: + doc.move_page(doc.page_count - 1, 0) + doc.ez_save(__file__.replace(".py", ".pdf")) diff --git a/docs/samples/filmfestival-sql.db b/docs/samples/filmfestival-sql.db new file mode 100644 index 0000000..249f9a7 Binary files /dev/null and b/docs/samples/filmfestival-sql.db differ diff --git a/docs/samples/filmfestival-sql.py b/docs/samples/filmfestival-sql.py new file mode 100644 index 0000000..66bce9d --- /dev/null +++ b/docs/samples/filmfestival-sql.py @@ -0,0 +1,115 @@ +""" +This is a demo script for using PyMuPDF with its "Story" feature. + +The following aspects are being covered here: + +* The script produces a report of films that are stored in an SQL database +* The report format is provided as a HTML template + +The SQL database contains two tables: +1. Table "films" which has the columns "title" (film title, str), "director" + (str) and "year" (year of release, int). +2. Table "actors" which has the columns "name" (actor name, str) and "title" + (the film title where the actor had been casted, str). + +The script reads all content of the "films" table. For each film title it +reads all rows from table "actors" which took part in that film. + +Comment 1 +--------- +To keep things easy and free from pesky technical detail, the relevant file +names inherit the name of this script: +- the database's filename is the script name with ".py" extension replaced + by ".db". +- the output PDF similarly has script file name with extension ".pdf". + +Comment 2 +--------- +The SQLITE database has been created using https://sqlitebrowser.org/, a free +multi-platform tool to maintain or manipulate SQLITE databases. +""" +import os +import sqlite3 + +import fitz + +# ---------------------------------------------------------------------- +# HTML template for the film report +# There are four placeholders coded as "id" attributes. +# One "id" allows locating the template part itself, the other three +# indicate where database text should be inserted. +# ---------------------------------------------------------------------- +festival_template = ( + "Just some arbitrary text" + '

Hook Norton Film Festival

' + "
    " + '
  1. ' + '' + "
    " + '
    Director
    ' + '
    Release Year
    ' + '
    Cast
    ' + "
    " + "
  2. " + "
" + "= num_pages! + seg_size = int(num_pages / cpu + 1) + seg_from = idx * seg_size # our first page number + seg_to = min(seg_from + seg_size, num_pages) # last page number + + for i in range(seg_from, seg_to): # work through our page segment + page = doc[i] + # page.get_text("rawdict") # use any page-related type of work here, eg + pix = page.get_pixmap(alpha=False, matrix=mat) + # store away the result somewhere ... + # pix.save("p-%i.png" % i) + print("Processed page numbers %i through %i" % (seg_from, seg_to - 1)) + + +if __name__ == "__main__": + t0 = mytime() # start a timer + filename = sys.argv[1] + mat = fitz.Matrix(0.2, 0.2) # the rendering matrix: scale down to 20% + cpu = cpu_count() + + # make vectors of arguments for the processes + vectors = [(i, cpu, filename, mat) for i in range(cpu)] + print("Starting %i processes for '%s'." % (cpu, filename)) + + pool = Pool() # make pool of 'cpu_count()' processes + pool.map(render_page, vectors, 1) # start processes passing each a vector + + t1 = mytime() # stop the timer + print("Total time %g seconds" % round(t1 - t0, 2)) diff --git a/docs/samples/mupdf-title.pdf b/docs/samples/mupdf-title.pdf new file mode 100644 index 0000000..07f5afc Binary files /dev/null and b/docs/samples/mupdf-title.pdf differ diff --git a/docs/samples/national-capitals.py b/docs/samples/national-capitals.py new file mode 100644 index 0000000..0b7ae52 --- /dev/null +++ b/docs/samples/national-capitals.py @@ -0,0 +1,449 @@ +""" +Demo script using (Py-) MuPDF "Story" feature. + +The following features are implemented: + +* Use of Story "template" feature to provide row content +* Use database access (SQLITE) to fetch row content +* Use ElementPosition feature to locate cell positions on page +* Simulate feature "Table Header Repeat" +* Simulate feature "Cell Grid Lines" + +""" +import io +import sqlite3 +import sys + +import fitz + +""" +Table data. Used to populate a temporary SQL database, which will be processed by the script. +Its only purpose is to avoid carrying around a separate database file. +""" +table_data = """China;Beijing;21542000;1.5%;2018 +Japan;Tokyo;13921000;11.2%;2019 +DR Congo;Kinshasa;12691000;13.2%;2017 +Russia;Moscow;12655050;8.7%;2021 +Indonesia;Jakarta;10562088;3.9%;2020 +Egypt;Cairo;10107125;9.3%;2022 +South Korea;Seoul;9508451;18.3%;2022 +Mexico;Mexico City;9209944;7.3%;2020 +United Kingdom;London;9002488;13.4%;2020 +Bangladesh;Dhaka;8906039;5.3%;2011 +Peru;Lima;8852000;26.3%;2012 +Iran;Tehran;8693706;9.9%;2016 +Thailand;Bangkok;8305218;11.6%;2010 +Vietnam;Hanoi;8053663;8.3%;2019 +Iraq;Baghdad;7682136;17.6%;2021 +Saudi Arabia;Riyadh;7676654;21.4%;2018 +Hong Kong;Hong Kong;7291600;100%;2022 +Colombia;Bogotá;7181469;13.9%;2011 +Chile;Santiago;6310000;32.4%;2012 +Turkey;Ankara;5747325;6.8%;2021 +Singapore;Singapore;5453600;91.8%;2021 +Afghanistan;Kabul;4601789;11.5%;2021 +Kenya;Nairobi;4397073;8.3%;2019 +Jordan;Amman;4061150;36.4%;2021 +Algeria;Algiers;3915811;8.9%;2011 +Germany;Berlin;3677472;4.4%;2021 +Spain;Madrid;3305408;7.0%;2021 +Ethiopia;Addis Ababa;3040740;2.5%;2012 +Kuwait;Kuwait City;2989000;70.3%;2018 +Guatemala;Guatemala City;2934841;16.7%;2020 +South Africa;Pretoria;2921488;4.9%;2011 +Ukraine;Kyiv;2920873;6.7%;2021 +Argentina;Buenos Aires;2891082;6.4%;2010 +North Korea;Pyongyang;2870000;11.1%;2016 +Uzbekistan;Tashkent;2860600;8.4%;2022 +Italy;Rome;2761632;4.7%;2022 +Ecuador;Quito;2800388;15.7%;2020 +Cameroon;Yaoundé;2765568;10.2%;2015 +Zambia;Lusaka;2731696;14.0%;2020 +Sudan;Khartoum;2682431;5.9%;2012 +Brazil;Brasília;2648532;1.2%;2012 +Taiwan;Taipei (de facto);2608332;10.9%;2020 +Yemen;Sanaa;2575347;7.8%;2012 +Angola;Luanda;2571861;7.5%;2020 +Burkina Faso;Ouagadougou;2453496;11.1%;2019 +Ghana;Accra;2388000;7.3%;2017 +Somalia;Mogadishu;2388000;14.0%;2021 +Azerbaijan;Baku;2303100;22.3%;2022 +Cambodia;Phnom Penh;2281951;13.8%;2019 +Venezuela;Caracas;2245744;8.0%;2016 +France;Paris;2139907;3.3%;2022 +Cuba;Havana;2132183;18.9%;2020 +Zimbabwe;Harare;2123132;13.3%;2012 +Syria;Damascus;2079000;9.7%;2019 +Belarus;Minsk;1996553;20.8%;2022 +Austria;Vienna;1962779;22.0%;2022 +Poland;Warsaw;1863056;4.9%;2021 +Philippines;Manila;1846513;1.6%;2020 +Mali;Bamako;1809106;8.3%;2009 +Malaysia;Kuala Lumpur;1782500;5.3%;2019 +Romania;Bucharest;1716983;8.9%;2021 +Hungary;Budapest;1706851;17.6%;2022 +Congo;Brazzaville;1696392;29.1%;2015 +Serbia;Belgrade;1688667;23.1%;2021 +Uganda;Kampala;1680600;3.7%;2019 +Guinea;Conakry;1660973;12.3%;2014 +Mongolia;Ulaanbaatar;1466125;43.8%;2020 +Honduras;Tegucigalpa;1444085;14.0%;2021 +Senegal;Dakar;1438725;8.5%;2021 +Niger;Niamey;1334984;5.3%;2020 +Uruguay;Montevideo;1319108;38.5%;2011 +Bulgaria;Sofia;1307439;19.0%;2021 +Oman;Muscat;1294101;28.6%;2021 +Czech Republic;Prague;1275406;12.1%;2022 +Madagascar;Antananarivo;1275207;4.4%;2018 +Kazakhstan;Astana;1239900;6.5%;2022 +Nigeria;Abuja;1235880;0.6%;2011 +Georgia;Tbilisi;1201769;32.0%;2022 +Mauritania;Nouakchott;1195600;25.9%;2019 +Qatar;Doha;1186023;44.1%;2020 +Libya;Tripoli;1170000;17.4%;2019 +Myanmar;Naypyidaw;1160242;2.2%;2014 +Rwanda;Kigali;1132686;8.4%;2012 +Mozambique;Maputo;1124988;3.5%;2020 +Dominican Republic;Santo Domingo;1111838;10.0%;2010 +Armenia;Yerevan;1096100;39.3%;2021 +Kyrgyzstan;Bishkek;1074075;16.5%;2021 +Sierra Leone;Freetown;1055964;12.5%;2015 +Nicaragua;Managua;1055247;15.4%;2020 +Canada;Ottawa;1017449;2.7%;2021 +Pakistan;Islamabad;1014825;0.4%;2017 +Liberia;Monrovia;1010970;19.5%;2008 +United Arab Emirates;Abu Dhabi;1010092;10.8%;2020 +Malawi;Lilongwe;989318;5.0%;2018 +Haiti;Port-au-Prince;987310;8.6%;2015 +Sweden;Stockholm;978770;9.4%;2021 +Eritrea;Asmara;963000;26.6%;2020 +Israel;Jerusalem;936425;10.5%;2019 +Laos;Vientiane;927724;12.5%;2019 +Chad;N'Djamena;916000;5.3%;2009 +Netherlands;Amsterdam;905234;5.2%;2022 +Central African Republic;Bangui;889231;16.3%;2020 +Panama;Panama City;880691;20.2%;2013 +Tajikistan;Dushanbe;863400;8.9%;2020 +Nepal;Kathmandu;845767;2.8%;2021 +Togo;Lomé;837437;9.7%;2010 +Turkmenistan;Ashgabat;791000;12.5%;2017 +Moldova;Chişinău;779300;25.5%;2019 +Croatia;Zagreb;769944;19.0%;2021 +Gabon;Libreville;703904;30.1%;2013 +Norway;Oslo;697010;12.9%;2021 +Macau;Macau;671900;97.9%;2022 +United States;Washington D.C.;670050;0.2%;2021 +Jamaica;Kingston;662491;23.4%;2019 +Finland;Helsinki;658864;11.9%;2021 +Tunisia;Tunis;638845;5.2%;2014 +Denmark;Copenhagen;638117;10.9%;2021 +Greece;Athens;637798;6.1%;2021 +Latvia;Riga;605802;32.3%;2021 +Djibouti;Djibouti (city);604013;54.6%;2012 +Ireland;Dublin;588233;11.8%;2022 +Morocco;Rabat;577827;1.6%;2014 +Lithuania;Vilnius;576195;20.7%;2022 +El Salvador;San Salvador;570459;9.0%;2019 +Albania;Tirana;557422;19.5%;2011 +North Macedonia;Skopje;544086;25.9%;2015 +South Sudan;Juba;525953;4.9%;2017 +Paraguay;Asunción;521559;7.8%;2020 +Portugal;Lisbon;509614;5.0%;2020 +Guinea-Bissau;Bissau;492004;23.9%;2015 +Slovakia;Bratislava;440948;8.1%;2020 +Estonia;Tallinn;438341;33.0%;2021 +Australia;Canberra;431380;1.7%;2020 +Namibia;Windhoek;431000;17.0%;2020 +Tanzania;Dodoma;410956;0.6%;2012 +Papua New Guinea;Port Moresby;364145;3.7%;2011 +Ivory Coast;Yamoussoukro;361893;1.3%;2020 +Lebanon;Beirut;361366;6.5%;2014 +Bolivia;Sucre;360544;3.0%;2022 +Puerto Rico (US);San Juan;342259;10.5%;2020 +Costa Rica;San José;342188;6.6%;2018 +Lesotho;Maseru;330760;14.5%;2016 +Cyprus;Nicosia;326739;26.3%;2016 +Equatorial Guinea;Malabo;297000;18.2%;2018 +Slovenia;Ljubljana;285604;13.5%;2021 +East Timor;Dili;277279;21.0%;2015 +Bosnia and Herzegovina;Sarajevo;275524;8.4%;2013 +Bahamas;Nassau;274400;67.3%;2016 +Botswana;Gaborone;273602;10.6%;2020 +Benin;Porto-Novo;264320;2.0%;2013 +Suriname;Paramaribo;240924;39.3%;2012 +India;New Delhi;249998;0.0%;2011 +Sahrawi Arab Democratic Republic;Laayoune (claimed) - Tifariti (de facto);217732 - 3000;—;2014 +New Zealand;Wellington;217000;4.2%;2021 +Bahrain;Manama;200000;13.7%;2020 +Kosovo;Pristina;198897;12.0%;2011 +Montenegro;Podgorica;190488;30.3%;2020 +Belgium;Brussels;187686;1.6%;2022 +Cape Verde;Praia;159050;27.1%;2017 +Mauritius;Port Louis;147066;11.3%;2018 +Curaçao (Netherlands);Willemstad;136660;71.8%;2011 +Burundi;Gitega;135467;1.1%;2020 +Switzerland;Bern (de facto);134591;1.5%;2020 +Transnistria;Tiraspol;133807;38.5%;2015 +Maldives;Malé;133412;25.6%;2014 +Iceland;Reykjavík;133262;36.0%;2021 +Luxembourg;Luxembourg City;124509;19.5%;2021 +Guyana;Georgetown;118363;14.7%;2012 +Bhutan;Thimphu;114551;14.7%;2017 +Comoros;Moroni;111326;13.5%;2016 +Barbados;Bridgetown;110000;39.1%;2014 +Sri Lanka;Sri Jayawardenepura Kotte;107925;0.5%;2012 +Brunei;Bandar Seri Begawan;100700;22.6%;2007 +Eswatini;Mbabane;94874;8.0%;2010 +New Caledonia (France);Nouméa;94285;32.8%;2019 +Fiji;Suva;93970;10.2%;2017 +Solomon Islands;Honiara;92344;13.0%;2021 +Republic of Artsakh;Stepanakert;75000;62.5%;2021 +Gambia;Banjul;73000;2.8%;2013 +São Tomé and Príncipe;São Tomé;71868;32.2%;2015 +Kiribati;Tarawa;70480;54.7%;2020 +Vanuatu;Port Vila;51437;16.1%;2016 +Northern Mariana Islands (USA);Saipan;47565;96.1%;2017 +Samoa;Apia;41611;19.0%;2021 +Palestine;Ramallah (de facto);38998;0.8%;2017 +Monaco;Monaco;38350;104.5%;2020 +Jersey (UK);Saint Helier;37540;34.2%;2018 +Trinidad and Tobago;Port of Spain;37074;2.4%;2011 +Cayman Islands (UK);George Town;34399;50.5%;2021 +Gibraltar (UK);Gibraltar;34003;104.1%;2020 +Grenada;St. George's;33734;27.1%;2012 +Aruba (Netherlands);Oranjestad;28294;26.6%;2010 +Isle of Man (UK);Douglas;27938;33.2%;2011 +Marshall Islands;Majuro;27797;66.1%;2011 +Tonga;Nukuʻalofa;27600;26.0%;2022 +Seychelles;Victoria;26450;24.8%;2010 +French Polynesia (France);Papeete;26926;8.9%;2017 +Andorra;Andorra la Vella;22873;28.9%;2022 +Faroe Islands (Denmark);Tórshavn;22738;43.0%;2022 +Antigua and Barbuda;St. John's;22219;23.8%;2011 +Belize;Belmopan;20621;5.2%;2016 +Saint Lucia;Castries;20000;11.1%;2013 +Guernsey (UK);Saint Peter Port;18958;30.1%;2019 +Greenland (Denmark);Nuuk;18800;33.4%;2021 +Dominica;Roseau;14725;20.3%;2011 +Saint Kitts and Nevis;Basseterre;14000;29.4%;2018 +Saint Vincent and the Grenadines;Kingstown;12909;12.4%;2012 +British Virgin Islands (UK);Road Town;12603;40.5%;2012 +Åland (Finland);Mariehamn;11736;39.0%;2021 +U.S. Virgin Islands (US);Charlotte Amalie;14477;14.5%;2020 +Micronesia;Palikir;6647;5.9%;2010 +Tuvalu;Funafuti;6320;56.4%;2017 +Malta;Valletta;5827;1.1%;2019 +Liechtenstein;Vaduz;5774;14.8%;2021 +Saint Pierre and Miquelon (France);Saint-Pierre;5394;91.7%;2019 +Cook Islands (NZ);Avarua;4906;28.9%;2016 +San Marino;City of San Marino;4061;12.0%;2021 +Turks and Caicos Islands (UK);Cockburn Town;3720;8.2%;2016 +American Samoa (USA);Pago Pago;3656;8.1%;2010 +Saint Martin (France);Marigot;3229;10.1%;2017 +Saint Barthélemy (France);Gustavia;2615;24.1%;2010 +Falkland Islands (UK);Stanley;2460;65.4%;2016 +Svalbard (Norway);Longyearbyen;2417;82.2%;2020 +Sint Maarten (Netherlands);Philipsburg;1894;4.3%;2011 +Christmas Island (Australia);Flying Fish Cove;1599;86.8%;2016 +Anguilla (UK);The Valley;1067;6.8%;2011 +Guam (US);Hagåtña;1051;0.6%;2010 +Wallis and Futuna (France);Mata Utu;1029;8.9%;2018 +Bermuda (UK);Hamilton;854;1.3%;2016 +Nauru;Yaren (de facto);747;6.0%;2011 +Saint Helena (UK);Jamestown;629;11.6%;2016 +Niue (NZ);Alofi;597;30.8%;2017 +Tokelau (NZ);Atafu;541;29.3%;2016 +Vatican City;Vatican City (city-state);453;100%;2019 +Montserrat (UK);Brades (de facto) - Plymouth (de jure);449 - 0;-;2011 +Norfolk Island (Australia);Kingston;341;-;2015 +Palau;Ngerulmud;271;1.5%;2010 +Cocos (Keeling) Islands (Australia);West Island;134;24.6%;2011 +Pitcairn Islands (UK);Adamstown;40;100.0%;2021 +South Georgia and the South Sandwich Islands (UK);King Edward Point;22;73.3%;2018""" + +# ------------------------------------------------------------------- +# HTML template for the report. We define no table header items +# because this is done in post processing. +# The actual template part is the table row, identified by id "row". +# The content of each cell will be filled using the respective id. +# ------------------------------------------------------------------- +HTML = """ +

World Capital Cities

+

Percent "%" is city population as a percentage of the country, as of "Year". +

+ + + + + + + + +
+""" + +# ------------------------------------------------------------------- +# Sets font-family globally to sans-serif, and text-align to right +# for the numerical table columns. +# ------------------------------------------------------------------- +CSS = """ +body { + font-family: sans-serif; +} +td[id="population"], td[id="percent"], td[id="year"] { + text-align: right; + padding-right: 2px; +}""" + +# ------------------------------------------------------------------- +# recorder function for cell positions +# ------------------------------------------------------------------- +coords = {} # stores cell gridline coordinates + + +def recorder(elpos): + """We only record positions of table rows and cells. + + Information is stored in "coords" with page number as key. + """ + global coords # dictionary of row and cell coordinates per page + if elpos.open_close != 2: # only consider coordinates provided at "close" + return + if elpos.id not in ("row", "country", "capital", "population", "percent", "year"): + return # only look at row / cell content + + rect = fitz.Rect(elpos.rect) # cell rectangle + if rect.y1 > elpos.filled: # ignore stuff below the filled rectangle + return + + # per page, we store the floats top-most y, right-most x, column left + # and row bottom borders. + x, y, x1, y0 = coords.get(elpos.page, (set(), set(), 0, sys.maxsize)) + + if elpos.id != "row": + x.add(rect.x0) # add cell left border coordinate + if rect.x1 > x1: # store right-most cell border on page + x1 = rect.x1 + else: + y.add(rect.y1) # add row bottom border coordinate + if rect.y0 < y0: # store top-most cell border per page + y0 = rect.y0 + + coords[elpos.page] = (x, y, x1, y0) # write back info per page + return + + +# ------------------------------------------------------------------- +# define database access: make an intermediate memory database for +# our demo purposes. +# ------------------------------------------------------------------- +dbfilename = ":memory:" # the SQLITE database file name +database = sqlite3.connect(dbfilename) # open database +cursor = database.cursor() # multi-purpose database cursor + +# Define and fill the SQLITE database +cursor.execute( + """CREATE TABLE capitals (Country text, Capital text, Population text, Percent text, Year text)""" +) + +for value in table_data.splitlines(): + cursor.execute("INSERT INTO capitals VALUES (?,?,?,?,?)", value.split(";")) + +# select statement for the rows - let SQL also sort it for us +select = """SELECT * FROM capitals ORDER BY "Country" """ + +# ------------------------------------------------------------------- +# define the HTML Story and fill it with database data +# ------------------------------------------------------------------- +story = fitz.Story(HTML, user_css=CSS) +body = story.body # access the HTML body detail + +template = body.find(None, "id", "row") # find the template part +table = body.find("table", None, None) # find start of table + +# read the rows from the database and put them all in one Python list +# NOTE: instead, we might fetch rows one by one (advisable for large volumes) + +cursor.execute(select) # execute cursor, and ... +rows = cursor.fetchall() # read out what was found +database.close() # no longer needed + +for country, capital, population, percent, year in rows: # iterate through the row + row = template.clone() # clone the template to report each row + row.find(None, "id", "country").add_text(country) + row.find(None, "id", "capital").add_text(capital) + row.find(None, "id", "population").add_text(population) + row.find(None, "id", "percent").add_text(percent) + row.find(None, "id", "year").add_text(year) + + table.append_child(row) + +template.remove() # remove the template + +# ------------------------------------------------------------------- +# generate the PDF and write it to memory +# ------------------------------------------------------------------- +fp = io.BytesIO() +writer = fitz.DocumentWriter(fp) +mediabox = fitz.paper_rect("letter") # use pages in Letter format +where = mediabox + (36, 36, -36, -72) # leave page borders +more = True +page = 0 +while more: + dev = writer.begin_page(mediabox) # make a new page + if page > 0: # leave room above the cells for inserting header row + delta = (0, 20, 0, 0) + else: + delta = (0, 0, 0, 0) + more, filled = story.place(where + delta) # arrange content on this rectangle + story.element_positions(recorder, {"page": page, "filled": where.y1}) + story.draw(dev) # write content to page + writer.end_page() # finish the page + page += 1 +writer.close() # close the PDF + +# ------------------------------------------------------------------- +# re-open memory PDF for inserting gridlines and header rows +# ------------------------------------------------------------------- +doc = fitz.open("pdf", fp) +for page in doc: + page.wrap_contents() # ensure all "cm" commands are properly wrapped + x, y, x1, y0 = coords[page.number] # read coordinates of the page + x = sorted(list(x)) + [x1] # list of cell left-right borders + y = [y0] + sorted(list(y)) # list of cell top-bottom borders + shape = page.new_shape() # make a canvas to draw upon + + for item in y: # draw horizontal lines (one under each row) + shape.draw_line((x[0] - 2, item), (x[-1] + 2, item)) + + for i in range(len(y)): # alternating row coloring + if i % 2: + rect = (x[0] - 2, y[i - 1], x[-1] + 2, y[i]) + shape.draw_rect(rect) + + for i in range(len(x)): # draw vertical lines + d = 2 if i == len(x) - 1 else -2 + shape.draw_line((x[i] + d, y[0]), (x[i] + d, y[-1])) + + # Write header row above table content + y0 -= 5 # bottom coord for header row text + shape.insert_text((x[0], y0), "Country", fontname="hebo", fontsize=12) + shape.insert_text((x[1], y0), "Capital", fontname="hebo", fontsize=12) + shape.insert_text((x[2], y0), "Population", fontname="hebo", fontsize=12) + shape.insert_text((x[3], y0), " %", fontname="hebo", fontsize=12) + shape.insert_text((x[4], y0), "Year", fontname="hebo", fontsize=12) + + # Write page footer + y0 = page.rect.height - 50 # top coordinate of footer bbox + bbox = fitz.Rect(0, y0, page.rect.width, y0 + 20) # footer bbox + page.insert_textbox( + bbox, + f"World Capital Cities, Page {page.number+1} of {doc.page_count}", + align=fitz.TEXT_ALIGN_CENTER, + ) + shape.finish(width=0.3, color=0.5, fill=0.9) # rectangles and gray lines + shape.commit(overlay=False) # put the drawings in background + +doc.subset_fonts() +doc.save(__file__.replace(".py", ".pdf"), deflate=True, garbage=4, pretty=True) +doc.close() diff --git a/docs/samples/new-annots.py b/docs/samples/new-annots.py new file mode 100644 index 0000000..74d459d --- /dev/null +++ b/docs/samples/new-annots.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +""" +------------------------------------------------------------------------------- +Demo script showing how annotations can be added to a PDF using PyMuPDF. + +It contains the following annotation types: +Caret, Text, FreeText, text markers (underline, strike-out, highlight, +squiggle), Circle, Square, Line, PolyLine, Polygon, FileAttachment, Stamp +and Redaction. +There is some effort to vary appearances by adding colors, line ends, +opacity, rotation, dashed lines, etc. + +Dependencies +------------ +PyMuPDF v1.17.0 +------------------------------------------------------------------------------- +""" +from __future__ import print_function + +import gc +import sys + +import fitz + +print(fitz.__doc__) +if fitz.VersionBind.split(".") < ["1", "17", "0"]: + sys.exit("PyMuPDF v1.17.0+ is needed.") + +gc.set_debug(gc.DEBUG_UNCOLLECTABLE) + +highlight = "this text is highlighted" +underline = "this text is underlined" +strikeout = "this text is striked out" +squiggled = "this text is zigzag-underlined" +red = (1, 0, 0) +blue = (0, 0, 1) +gold = (1, 1, 0) +green = (0, 1, 0) + +displ = fitz.Rect(0, 50, 0, 50) +r = fitz.Rect(72, 72, 220, 100) +t1 = u"têxt üsès Lätiñ charß,\nEUR: €, mu: µ, super scripts: ²³!" + + +def print_descr(annot): + """Print a short description to the right of each annot rect.""" + annot.parent.insert_text( + annot.rect.br + (10, -5), "%s annotation" % annot.type[1], color=red + ) + + +doc = fitz.open() +page = doc.new_page() + +page.set_rotation(0) + +annot = page.add_caret_annot(r.tl) +print_descr(annot) + +r = r + displ +annot = page.add_freetext_annot( + r, + t1, + fontsize=10, + rotate=90, + text_color=blue, + fill_color=gold, + align=fitz.TEXT_ALIGN_CENTER, +) +annot.set_border(width=0.3, dashes=[2]) +annot.update(text_color=blue, fill_color=gold) +print_descr(annot) + +r = annot.rect + displ +annot = page.add_text_annot(r.tl, t1) +print_descr(annot) + +# Adding text marker annotations: +# first insert a unique text, then search for it, then mark it +pos = annot.rect.tl + displ.tl +page.insert_text( + pos, # insertion point + highlight, # inserted text + morph=(pos, fitz.Matrix(-5)), # rotate around insertion point +) +rl = page.search_for(highlight, quads=True) # need a quad b/o tilted text +annot = page.add_highlight_annot(rl[0]) +print_descr(annot) + +pos = annot.rect.bl # next insertion point +page.insert_text(pos, underline, morph=(pos, fitz.Matrix(-10))) +rl = page.search_for(underline, quads=True) +annot = page.add_underline_annot(rl[0]) +print_descr(annot) + +pos = annot.rect.bl +page.insert_text(pos, strikeout, morph=(pos, fitz.Matrix(-15))) +rl = page.search_for(strikeout, quads=True) +annot = page.add_strikeout_annot(rl[0]) +print_descr(annot) + +pos = annot.rect.bl +page.insert_text(pos, squiggled, morph=(pos, fitz.Matrix(-20))) +rl = page.search_for(squiggled, quads=True) +annot = page.add_squiggly_annot(rl[0]) +print_descr(annot) + +pos = annot.rect.bl +r = fitz.Rect(pos, pos.x + 75, pos.y + 35) + (0, 20, 0, 20) +annot = page.add_polyline_annot([r.bl, r.tr, r.br, r.tl]) # 'Polyline' +annot.set_border(width=0.3, dashes=[2]) +annot.set_colors(stroke=blue, fill=green) +annot.set_line_ends(fitz.PDF_ANNOT_LE_CLOSED_ARROW, fitz.PDF_ANNOT_LE_R_CLOSED_ARROW) +annot.update(fill_color=(1, 1, 0)) +print_descr(annot) + +r += displ +annot = page.add_polygon_annot([r.bl, r.tr, r.br, r.tl]) # 'Polygon' +annot.set_border(width=0.3, dashes=[2]) +annot.set_colors(stroke=blue, fill=gold) +annot.set_line_ends(fitz.PDF_ANNOT_LE_DIAMOND, fitz.PDF_ANNOT_LE_CIRCLE) +annot.update() +print_descr(annot) + +r += displ +annot = page.add_line_annot(r.tr, r.bl) # 'Line' +annot.set_border(width=0.3, dashes=[2]) +annot.set_colors(stroke=blue, fill=gold) +annot.set_line_ends(fitz.PDF_ANNOT_LE_DIAMOND, fitz.PDF_ANNOT_LE_CIRCLE) +annot.update() +print_descr(annot) + +r += displ +annot = page.add_rect_annot(r) # 'Square' +annot.set_border(width=1, dashes=[1, 2]) +annot.set_colors(stroke=blue, fill=gold) +annot.update(opacity=0.5) +print_descr(annot) + +r += displ +annot = page.add_circle_annot(r) # 'Circle' +annot.set_border(width=0.3, dashes=[2]) +annot.set_colors(stroke=blue, fill=gold) +annot.update() +print_descr(annot) + +r += displ +annot = page.add_file_annot( + r.tl, b"just anything for testing", "testdata.txt" # 'FileAttachment' +) +print_descr(annot) # annot.rect + +r += displ +annot = page.add_stamp_annot(r, stamp=10) # 'Stamp' +annot.set_colors(stroke=green) +annot.update() +print_descr(annot) + +r += displ + (0, 0, 50, 10) +rc = page.insert_textbox( + r, + "This content will be removed upon applying the redaction.", + color=blue, + align=fitz.TEXT_ALIGN_CENTER, +) +annot = page.add_redact_annot(r) +print_descr(annot) + +doc.save(__file__.replace(".py", "-%i.pdf" % page.rotation), deflate=True) diff --git a/docs/samples/quickfox-image-no-go.py b/docs/samples/quickfox-image-no-go.py new file mode 100644 index 0000000..c370dfb --- /dev/null +++ b/docs/samples/quickfox-image-no-go.py @@ -0,0 +1,188 @@ +""" +This is a demo script using PyMuPDF's Story class to output text as a PDF with +a two-column page layout. + +The script demonstrates the following features: +* Layout text around images of an existing ("target") PDF. +* Based on a few global parameters, areas on each page are identified, that + can be used to receive text layouted by a Story. +* These global parameters are not stored anywhere in the target PDF and + must therefore be provided in some way. + - The width of the border(s) on each page. + - The fontsize to use for text. This value determines whether the provided + text will fit in the empty spaces of the (fixed) pages of target PDF. It + cannot be predicted in any way. The script ends with an exception if + target PDF has not enough pages, and prints a warning message if not all + pages receive at least some text. In both cases, the FONTSIZE value + can be changed (a float value). + - Use of a 2-column page layout for the text. +* The layout creates a temporary (memory) PDF. Its produced page content + (the text) is used to overlay the corresponding target page. If text + requires more pages than are available in target PDF, an exception is raised. + If not all target pages receive at least some text, a warning is printed. +* The script reads "image-no-go.pdf" in its own folder. This is the "target" PDF. + It contains 2 pages with each 2 images (from the original article), which are + positioned at places that create a broad overall test coverage. Otherwise the + pages are empty. +* The script produces "quickfox-image-no-go.pdf" which contains the original pages + and image positions, but with the original article text laid out around them. + +Note: +-------------- +This script version uses just image positions to derive "No-Go areas" for +layouting the text. Other PDF objects types are detectable by PyMuPDF and may +be taken instead or in addition, without influencing the layouting. +The following are candidates for other such "No-Go areas". Each can be detected +and located by PyMuPDF: +* Annotations +* Drawings +* Existing text + +-------------- +The text and images are taken from the somewhat modified Wikipedia article +https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog. +-------------- +""" + +import io +import os +import zipfile +import fitz + + +thisdir = os.path.dirname(os.path.abspath(__file__)) +myzip = zipfile.ZipFile(os.path.join(thisdir, "quickfox.zip")) + +docname = os.path.join(thisdir, "image-no-go.pdf") # "no go" input PDF file name +outname = os.path.join(thisdir, "quickfox-image-no-go.pdf") # output PDF file name +BORDER = 36 # global parameter +FONTSIZE = 12.5 # global parameter +COLS = 2 # number of text columns, global parameter + + +def analyze_page(page): + """Compute MediaBox and rectangles on page that are free to receive text. + + Notes: + Assume a BORDER around the page, make 2 columns of the resulting + sub-rectangle and extract the rectangles of all images on page. + For demo purposes, the image rectangles are taken as "NO-GO areas" + on the page when writing text with the Story. + The function returns free areas for each of the columns. + + Returns: + (page.number, mediabox, CELLS), where CELLS is a list of free cells. + """ + prect = page.rect # page rectangle - will be our MEDIABOX later + where = prect + (BORDER, BORDER, -BORDER, -BORDER) + TABLE = fitz.make_table(where, rows=1, cols=COLS) + + # extract rectangles covered by images on this page + IMG_RECTS = sorted( # image rects on page (sort top-left to bottom-right) + [fitz.Rect(item["bbox"]) for item in page.get_image_info()], + key=lambda b: (b.y1, b.x0), + ) + + def free_cells(column): + """Return free areas in this column.""" + free_stripes = [] # y-value pairs wrapping a free area stripe + # intersecting images: block complete intersecting column stripe + col_imgs = [(b.y0, b.y1) for b in IMG_RECTS if abs(b & column) > 0] + s_y0 = column.y0 # top y-value of column + for y0, y1 in col_imgs: # an image stripe + if y0 > s_y0 + FONTSIZE: # image starts below last free btm value + free_stripes.append((s_y0, y0)) # store as free stripe + s_y0 = y1 # start of next free stripe + + if s_y0 + FONTSIZE < column.y1: # enough room to column bottom + free_stripes.append((s_y0, column.y1)) + + if free_stripes == []: # covers "no image in this column" + free_stripes.append((column.y0, column.y1)) + + # make available cells of this column + CELLS = [fitz.Rect(column.x0, y0, column.x1, y1) for (y0, y1) in free_stripes] + return CELLS + + # collection of available Story rectangles on page + CELLS = [] + for i in range(COLS): + CELLS.extend(free_cells(TABLE[0][i])) + + return page.number, prect, CELLS + + +HTML = myzip.read("quickfox.html").decode() + +# -------------------------------------------------------------- +# Make the Story object +# -------------------------------------------------------------- +story = fitz.Story(HTML) + +# modify the DOM somewhat +body = story.body # access HTML body +body.set_properties(font="sans-serif") # and give it our font globally + +# modify certain nodes +para = body.find("p", None, None) # find relevant nodes (here: paragraphs) +while para != None: + para.set_properties( # method MUST be used for existing nodes + indent=15, + fontsize=FONTSIZE, + ) + para = para.find_next("p", None, None) + +# we remove all image references, because the target PDF already has them +img = body.find("img", None, None) +while img != None: + next_img = img.find_next("img", None, None) + img.remove() + img = next_img + +page_info = {} # contains MEDIABOX and free CELLS per page +doc = fitz.open(docname) +for page in doc: + pno, mediabox, cells = analyze_page(page) + page_info[pno] = (mediabox, cells) +doc.close() # close target PDF for now - re-open later + +fileobject = io.BytesIO() # let DocumentWriter write to memory +writer = fitz.DocumentWriter(fileobject) # define output writer + +more = 1 # stop if this ever becomes zero +pno = 0 # count output pages +while more: # loop until all HTML text has been written + try: + MEDIABOX, CELLS = page_info[pno] + except KeyError: # too much text space required: reduce fontsize? + raise ValueError("text does not fit on target PDF") + dev = writer.begin_page(MEDIABOX) # prepare a new output page + for cell in CELLS: # iterate over free cells on this page + if not more: # need to check this for every cell + continue + more, _ = story.place(cell) + story.draw(dev) + writer.end_page() # finish the PDF page + pno += 1 + +writer.close() # close DocumentWriter output + +# Re-open writer output, read its pages and overlay target pages with them. +# The generated pages have same dimension as their targets. +src = fitz.open("pdf", fileobject) +doc = fitz.open(doc.name) +for page in doc: # overlay every target page with the prepared text + if page.number >= src.page_count: + print(f"Text only uses {src.page_count} target pages!") + continue # story did not need all target pages? + + # overlay target page + page.show_pdf_page(page.rect, src, page.number) + + # DEBUG start --- draw the text rectangles + # mb, cells = page_info[page.number] + # for cell in cells: + # page.draw_rect(cell, color=(1, 0, 0)) + # DEBUG stop --- + +doc.ez_save(outname) diff --git a/docs/samples/quickfox.py b/docs/samples/quickfox.py new file mode 100644 index 0000000..27ceab2 --- /dev/null +++ b/docs/samples/quickfox.py @@ -0,0 +1,89 @@ +""" +This is a demo script using PyMuPDF's Story class to output text as a PDF with +a two-column page layout. + +The script demonstrates the following features: +* How to fill columns or table cells of complex page layouts +* How to embed images +* How to modify existing, given HTML sources for output (text indent, font size) +* How to use fonts defined in package "pymupdf-fonts" +* How to use ZIP files as Archive + +-------------- +The example is taken from the somewhat modified Wikipedia article +https://en.wikipedia.org/wiki/The_quick_brown_fox_jumps_over_the_lazy_dog. +-------------- +""" + +import io +import os +import zipfile +import fitz + + +thisdir = os.path.dirname(os.path.abspath(__file__)) +myzip = zipfile.ZipFile(os.path.join(thisdir, "quickfox.zip")) +arch = fitz.Archive(myzip) + +if fitz.fitz_fontdescriptors: + # we want to use the Ubuntu fonts for sans-serif and for monospace + CSS = fitz.css_for_pymupdf_font("ubuntu", archive=arch, name="sans-serif") + CSS = fitz.css_for_pymupdf_font("ubuntm", CSS=CSS, archive=arch, name="monospace") +else: + # No pymupdf-fonts available. + CSS="" + +docname = __file__.replace(".py", ".pdf") # output PDF file name + +HTML = myzip.read("quickfox.html").decode() + +# make the Story object +story = fitz.Story(HTML, user_css=CSS, archive=arch) + +# -------------------------------------------------------------- +# modify the DOM somewhat +# -------------------------------------------------------------- +body = story.body # access HTML body +body.set_properties(font="sans-serif") # and give it our font globally + +# modify certain nodes +para = body.find("p", None, None) # find relevant nodes (here: paragraphs) +while para != None: + para.set_properties( # method MUST be used for existing nodes + indent=15, + fontsize=13, + ) + para = para.find_next("p", None, None) + +# choose PDF page size +MEDIABOX = fitz.paper_rect("letter") +# text appears only within this subrectangle +WHERE = MEDIABOX + (36, 36, -36, -36) + +# -------------------------------------------------------------- +# define page layout within the WHERE rectangle +# -------------------------------------------------------------- +COLS = 2 # layout: 2 cols 1 row +ROWS = 1 +TABLE = fitz.make_table(WHERE, cols=COLS, rows=ROWS) +# fill the cells of each page in this sequence: +CELLS = [TABLE[i][j] for i in range(ROWS) for j in range(COLS)] + +fileobject = io.BytesIO() # let DocumentWriter write to memory +writer = fitz.DocumentWriter(fileobject) # define the writer + +more = 1 +while more: # loop until all input text has been written out + dev = writer.begin_page(MEDIABOX) # prepare a new output page + for cell in CELLS: + # content may be complete after any cell, ... + if more: # so check this status first + more, _ = story.place(cell) + story.draw(dev) + writer.end_page() # finish the PDF page + +writer.close() # close DocumentWriter output + +# for housekeeping work re-open from memory +doc = fitz.open("pdf", fileobject) +doc.ez_save(docname) diff --git a/docs/samples/quickfox.zip b/docs/samples/quickfox.zip new file mode 100644 index 0000000..0c75e4f Binary files /dev/null and b/docs/samples/quickfox.zip differ diff --git a/docs/samples/showpdf-page.py b/docs/samples/showpdf-page.py new file mode 100644 index 0000000..44400eb --- /dev/null +++ b/docs/samples/showpdf-page.py @@ -0,0 +1,82 @@ +""" +Demo of Story class in PyMuPDF +------------------------------- + +This script demonstrates how to the results of a fitz.Story output can be +placed in a rectangle of an existing (!) PDF page. + +""" +import io +import os + +import fitz + + +def make_pdf(fileptr, text, rect, font="sans-serif", archive=None): + """Make a memory DocumentWriter from HTML text and a rect. + + Args: + fileptr: a Python file object. For example an io.BytesIO(). + text: the text to output (HTML format) + rect: the target rectangle. Will use its width / height as mediabox + font: (str) font family name, default sans-serif + archive: fitz.Archive parameter. To be used if e.g. images or special + fonts should be used. + Returns: + The matrix to convert page rectangles of the created PDF back + to rectangle coordinates in the parameter "rect". + Normal use will expect to fit all the text in the given rect. + However, if an overflow occurs, this function will output multiple + pages, and the caller may decide to either accept or retry with + changed parameters. + """ + # use input rectangle as the page dimension + mediabox = fitz.Rect(0, 0, rect.width, rect.height) + # this matrix converts mediabox back to input rect + matrix = mediabox.torect(rect) + + story = fitz.Story(text, archive=archive) + body = story.body + body.set_properties(font=font) + writer = fitz.DocumentWriter(fileptr) + while True: + device = writer.begin_page(mediabox) + more, _ = story.place(mediabox) + story.draw(device) + writer.end_page() + if not more: + break + writer.close() + return matrix + + +# ------------------------------------------------------------- +# We want to put this in a given rectangle of an existing page +# ------------------------------------------------------------- +HTML = """ +

PyMuPDF is a great package! And it still improves significantly from one version to the next one!

+

It is a Python binding for MuPDF, a lightweight PDF, XPS, and E-book viewer, renderer, and toolkit.
Both are maintained and developed by Artifex Software, Inc.

+

Via MuPDF it can access files in PDF, XPS, OpenXPS, CBZ, EPUB, MOBI and FB2 (e-books) formats,
and it is known for its top +performance and rendering quality.

""" + +# Make a PDF page for demo purposes +root = os.path.abspath( f"{__file__}/..") +doc = fitz.open(f"{root}/mupdf-title.pdf") +page = doc[0] + +WHERE = fitz.Rect(50, 100, 250, 500) # target rectangle on existing page + +fileptr = io.BytesIO() # let DocumentWriter use this as its file + +# ------------------------------------------------------------------- +# call DocumentWriter and Story to fill our rectangle +matrix = make_pdf(fileptr, HTML, WHERE) +# ------------------------------------------------------------------- +src = fitz.open("pdf", fileptr) # open DocumentWriter output PDF +if src.page_count > 1: # target rect was too small + raise ValueError("target WHERE too small") + +# its page 0 contains our result +page.show_pdf_page(WHERE, src, 0) + +doc.ez_save(f"{root}/mupdf-title-after.pdf") diff --git a/docs/samples/simple-grid.py b/docs/samples/simple-grid.py new file mode 100644 index 0000000..84dbd27 --- /dev/null +++ b/docs/samples/simple-grid.py @@ -0,0 +1,26 @@ +import fitz + +MEDIABOX = fitz.paper_rect("letter") # output page format: Letter +GRIDSPACE = fitz.Rect(100, 100, 400, 400) +GRID = fitz.make_table(GRIDSPACE, rows=2, cols=2) +CELLS = [GRID[i][j] for i in range(2) for j in range(2)] +text_table = ("A", "B", "C", "D") +writer = fitz.DocumentWriter(__file__.replace(".py", ".pdf")) # create the writer + +device = writer.begin_page(MEDIABOX) # make new page +for i, text in enumerate(text_table): + story = fitz.Story(em=1) + body = story.body + with body.add_paragraph() as para: + para.set_bgcolor("#ecc") + para.set_pagebreak_after() # fills whole cell with bgcolor + para.set_align("center") + para.set_fontsize(16) + para.add_text(f"\n\n\n{text}") + story.place(CELLS[i]) + story.draw(device) + del story + +writer.end_page() # finish page + +writer.close() # close output file diff --git a/docs/samples/story-write-stabilized-links.py b/docs/samples/story-write-stabilized-links.py new file mode 100644 index 0000000..6532d16 --- /dev/null +++ b/docs/samples/story-write-stabilized-links.py @@ -0,0 +1,89 @@ +""" +Demo script for PyMuPDF's `fitz.Story.write_stabilized_with_links()`. + +`fitz.Story.write_stabilized_links()` is similar to +`fitz.Story.write_stabilized()` except that it creates a PDF `fitz.Document` +that contains PDF links generated from all internal links in the original html. +""" + +import textwrap + +import fitz + + +def rectfn(rect_num, filled): + ''' + We return one rect per page. + ''' + rect = fitz.Rect(10, 20, 290, 380) + mediabox = fitz.Rect(0, 0, 300, 400) + #print(f'rectfn(): rect_num={rect_num} filled={filled}') + return mediabox, rect, None + + +def contentfn(positions): + ''' + Returns html content, with a table of contents derived from `positions`. + ''' + ret = '' + ret += textwrap.dedent(''' + + +

Contents

+
    + ''') + + # Create table of contents with links to all sections in the + # document. + for position in positions: + if position.heading and (position.open_close & 1): + text = position.text if position.text else '' + if position.id: + ret += f"
  • {text}\n" + else: + ret += f"
  • {text}\n" + ret += f"
      \n" + ret += f"
    • page={position.page_num}\n" + ret += f"
    • depth={position.depth}\n" + ret += f"
    • heading={position.heading}\n" + ret += f"
    • id={position.id!r}\n" + ret += f"
    • href={position.href!r}\n" + ret += f"
    • rect={position.rect}\n" + ret += f"
    • text={text!r}\n" + ret += f"
    • open_close={position.open_close}\n" + ret += f"
    \n" + + ret += '
\n' + + # Main content. + ret += textwrap.dedent(f''' + +

First section

+

Contents of first section. +

+ +

Second section

+

Contents of second section. +

Second section first subsection

+ +

Contents of second section first subsection. +

IDTEST + +

Third section

+

Contents of third section. +

NAMETEST. + + + ''') + ret = ret.strip() + with open(__file__.replace('.py', '.html'), 'w') as f: + f.write(ret) + return ret; + + +out_path = __file__.replace('.py', '.pdf') +document = fitz.Story.write_stabilized_with_links(contentfn, rectfn) +document.save(out_path) diff --git a/docs/samples/story-write-stabilized.py b/docs/samples/story-write-stabilized.py new file mode 100644 index 0000000..205ab05 --- /dev/null +++ b/docs/samples/story-write-stabilized.py @@ -0,0 +1,88 @@ +""" +Demo script for PyMuPDF's `fitz.Story.write_stabilized()`. + +`fitz.Story.write_stabilized()` is similar to `fitz.Story.write()`, +except instead of taking a fixed html document, it does iterative layout +of dynamically-generated html content (provided by a callback) to a +`fitz.DocumentWriter`. + +For example this allows one to add a dynamically-generated table of contents +section while ensuring that page numbers are patched up until stable. +""" + +import textwrap + +import fitz + + +def rectfn(rect_num, filled): + ''' + We return one rect per page. + ''' + rect = fitz.Rect(10, 20, 290, 380) + mediabox = fitz.Rect(0, 0, 300, 400) + #print(f'rectfn(): rect_num={rect_num} filled={filled}') + return mediabox, rect, None + + +def contentfn(positions): + ''' + Returns html content, with a table of contents derived from `positions`. + ''' + ret = '' + ret += textwrap.dedent(''' + + +

Contents

+
    + ''') + + # Create table of contents with links to all sections in the + # document. + for position in positions: + if position.heading and (position.open_close & 1): + text = position.text if position.text else '' + if position.id: + ret += f"
  • {text}\n" + else: + ret += f"
  • {text}\n" + ret += f"
      \n" + ret += f"
    • page={position.page_num}\n" + ret += f"
    • depth={position.depth}\n" + ret += f"
    • heading={position.heading}\n" + ret += f"
    • id={position.id!r}\n" + ret += f"
    • href={position.href!r}\n" + ret += f"
    • rect={position.rect}\n" + ret += f"
    • text={text!r}\n" + ret += f"
    • open_close={position.open_close}\n" + ret += f"
    \n" + + ret += '
\n' + + # Main content. + ret += textwrap.dedent(f''' + +

First section

+

Contents of first section. + +

Second section

+

Contents of second section. +

Second section first subsection

+ +

Contents of second section first subsection. + +

Third section

+

Contents of third section. + + + ''') + ret = ret.strip() + with open(__file__.replace('.py', '.html'), 'w') as f: + f.write(ret) + return ret; + + +out_path = __file__.replace('.py', '.pdf') +writer = fitz.DocumentWriter(out_path) +fitz.Story.write_stabilized(writer, contentfn, rectfn) +writer.close() diff --git a/docs/samples/story-write.py b/docs/samples/story-write.py new file mode 100644 index 0000000..005853c --- /dev/null +++ b/docs/samples/story-write.py @@ -0,0 +1,81 @@ +""" +Demo script for PyMuPDF's `Story.write()` method. + +This is a way of laying out a story into a PDF document, that avoids the need +to write a loop that calls `story.place()` and `story.draw()`. + +Instead just a single function call is required, albeit with a `rectfn()` +callback that returns the rectangles into which the story is placed. +""" + +import html + +import fitz + + +# Create html containing multiple copies of our own source code. +# +with open(__file__) as f: + text = f.read() +text = html.escape(text) +html = f''' + + + +

Contents of {__file__}

+ +

Normal

+
+{text}
+
+ +

Strong

+ +
+{text}
+
+
+ +

Em

+ +
+{text}
+
+
+ + +''' + + +def rectfn(rect_num, filled): + ''' + We return four rectangles per page in this order: + + 1 3 + 2 4 + ''' + page_w = 800 + page_h = 600 + margin = 50 + rect_w = (page_w - 3*margin) / 2 + rect_h = (page_h - 3*margin) / 2 + + if rect_num % 4 == 0: + # New page. + mediabox = fitz.Rect(0, 0, page_w, page_h) + else: + mediabox = None + # Return one of four rects in turn. + rect_x = margin + (rect_w+margin) * ((rect_num // 2) % 2) + rect_y = margin + (rect_h+margin) * (rect_num % 2) + rect = fitz.Rect(rect_x, rect_y, rect_x + rect_w, rect_y + rect_h) + #print(f'rectfn(): rect_num={rect_num} filled={filled}. Returning: rect={rect}') + return mediabox, rect, None + +story = fitz.Story(html, em=8) + +out_path = __file__.replace('.py', '.pdf') +writer = fitz.DocumentWriter(out_path) + +story.write(writer, rectfn) +writer.close() diff --git a/docs/samples/table01.py b/docs/samples/table01.py new file mode 100644 index 0000000..4faa861 --- /dev/null +++ b/docs/samples/table01.py @@ -0,0 +1,124 @@ +""" +Demo script for basic HTML table support in Story objects + +Outputs a table with three columns that fits on one Letter page. +The content of each row is filled via the Story's template mechanism. +Column widths and row heights are automatically computed by MuPDF. +Some styling via a CSS source is also demonstrated: + +- The table header row has a gray background +- Each cell shows a border at its top +- The Story's body uses the sans-serif font family +- The text of one of the columns is set to blue + +Dependencies +------------- +PyMuPDF v1.22.0 or later +""" +import fitz + +table_text = ( # the content of each table row + ( + "Length", + "integer", + """(Required) The number of bytes from the beginning of the line following the keyword stream to the last byte just before the keyword endstream. (There may be an additional EOL marker, preceding endstream, that is not included in the count and is not logically part of the stream data.) See “Stream Extent,” above, for further discussion.""", + ), + ( + "Filter", + "name or array", + """(Optional) The name of a filter to be applied in processing the stream data found between the keywords stream and endstream, or an array of such names. Multiple filters should be specified in the order in which they are to be applied.""", + ), + ( + "FFilter", + "name or array", + """(Optional; PDF 1.2) The name of a filter to be applied in processing the data found in the stream's external file, or an array of such names. The same rules apply as for Filter.""", + ), + ( + "FDecodeParms", + "dictionary or array", + """(Optional; PDF 1.2) A parameter dictionary, or an array of such dictionaries, used by the filters specified by FFilter. The same rules apply as for DecodeParms.""", + ), + ( + "DecodeParms", + "dictionary or array", + """(Optional) A parameter dictionary or an array of such dictionaries, used by the filters specified by Filter. If there is only one filter and that filter has parameters, DecodeParms must be set to the filter's parameter dictionary unless all the filter's parameters have their default values, in which case the DecodeParms entry may be omitted. If there are multiple filters and any of the filters has parameters set to nondefault values, DecodeParms must be an array with one entry for each filter: either the parameter dictionary for that filter, or the null object if that filter has no parameters (or if all of its parameters have their default values). If none of the filters have parameters, or if all their parameters have default values, the DecodeParms entry may be omitted. (See implementation note 7 in Appendix H.)""", + ), + ( + "DL", + "integer", + """(Optional; PDF 1.5) A non-negative integer representing the number of bytes in the decoded (defiltered) stream. It can be used to determine, for example, whether enough disk space is available to write a stream to a file.\nThis value should be considered a hint only; for some stream filters, it may not be possible to determine this value precisely.""", + ), + ( + "F", + "file specification", + """(Optional; PDF 1.2) The file containing the stream data. If this entry is present, the bytes between stream and endstream are ignored, the filters are specified by FFilter rather than Filter, and the filter parameters are specified by FDecodeParms rather than DecodeParms. However, the Length entry should still specify the number of those bytes. (Usually, there are no bytes and Length is 0.) (See implementation note 46 in Appendix H.)""", + ), +) + +# Only a minimal HTML source is required to provide the Story's working +HTML = """ + +

TABLE 3.4 Entries common to all stream dictionaries

+ + + + + + + +""" + +""" +--------------------------------------------------------------------- +Just for demo purposes, set: +- header cell background to gray +- text color in col1 to blue +- a border line at the top of all table cells +- all text to the sans-serif font +--------------------------------------------------------------------- +""" +CSS = """th { + background-color: #aaa; +} + +td[id="col1"] { + color: blue; +} + +td, tr { + border: 1px solid black; + border-right-width: 0px; + border-left-width: 0px; + border-bottom-width: 0px; +} +body { + font-family: sans-serif; +} +""" + +story = fitz.Story(HTML, user_css=CSS) # define the Story +body = story.body # access the HTML of it +template = body.find(None, "id", "row") # find the template with name "row" +parent = template.parent # access its parent i.e., the
KEYTYPEVALUE
+ +for col0, col1, col2 in table_text: + row = template.clone() # make a clone of the row template + # add text to each cell in the duplicated row + row.find(None, "id", "col0").add_text(col0) + row.find(None, "id", "col1").add_text(col1) + row.find(None, "id", "col2").add_text(col2) + parent.append_child(row) # add new row to
+template.remove() # remove the template + +# Story is ready - output it via a writer +writer = fitz.DocumentWriter(__file__.replace(".py", ".pdf"), "compress") +mediabox = fitz.paper_rect("letter") # size of one output page +where = mediabox + (36, 36, -36, -36) # use this sub-area for the content + +more = True # detects end of output +while more: + dev = writer.begin_page(mediabox) # start a page, returning a device + more, filled = story.place(where) # compute content fitting into "where" + story.draw(dev) # output it to the page + writer.end_page() # finalize the page +writer.close() # close the output diff --git a/docs/samples/text-lister.py b/docs/samples/text-lister.py new file mode 100644 index 0000000..2b6dbe1 --- /dev/null +++ b/docs/samples/text-lister.py @@ -0,0 +1,42 @@ +import sys + +import fitz + + +def flags_decomposer(flags): + """Make font flags human readable.""" + l = [] + if flags & 2 ** 0: + l.append("superscript") + if flags & 2 ** 1: + l.append("italic") + if flags & 2 ** 2: + l.append("serifed") + else: + l.append("sans") + if flags & 2 ** 3: + l.append("monospaced") + else: + l.append("proportional") + if flags & 2 ** 4: + l.append("bold") + return ", ".join(l) + + +doc = fitz.open(sys.argv[1]) +page = doc[0] + +# read page text as a dictionary, suppressing extra spaces in CJK fonts +blocks = page.get_text("dict", flags=11)["blocks"] +for b in blocks: # iterate through the text blocks + for l in b["lines"]: # iterate through the text lines + for s in l["spans"]: # iterate through the text spans + print("") + font_properties = "Font: '%s' (%s), size %g, color #%06x" % ( + s["font"], # font name + flags_decomposer(s["flags"]), # readable font flags + s["size"], # font size + s["color"], # font color + ) + print("Text: '%s'" % s["text"]) # simple print of text + print(font_properties) diff --git a/docs/shape.rst b/docs/shape.rst new file mode 100644 index 0000000..1e895e4 --- /dev/null +++ b/docs/shape.rst @@ -0,0 +1,637 @@ +.. include:: header.rst + +.. _Shape: + +Shape +================ + +This class allows creating interconnected graphical elements on a PDF page. Its methods have the same meaning and name as the corresponding :ref:`Page` methods. + +In fact, each :ref:`Page` draw method is just a convenience wrapper for (1) one shape draw method, (2) the :meth:`Shape.finish` method, and (3) the :meth:`Shape.commit` method. For page text insertion, only the :meth:`Shape.commit` method is invoked. If many draw and text operations are executed for a page, you should always consider using a Shape object. + +Several draw methods can be executed in a row and each one of them will contribute to one drawing. Once the drawing is complete, the :meth:`Shape.finish` method must be invoked to apply color, dashing, width, morphing and other attributes. + +**Draw** methods of this class (and :meth:`Shape.insert_textbox`) are logging the area they are covering in a rectangle (:attr:`Shape.rect`). This property can for instance be used to set :attr:`Page.cropbox_position`. + +**Text insertions** :meth:`Shape.insert_text` and :meth:`Shape.insert_textbox` implicitly execute a "finish" and therefore only require :meth:`Shape.commit` to become effective. As a consequence, both include parameters for controlling properties like colors, etc. + +================================ ===================================================== +**Method / Attribute** **Description** +================================ ===================================================== +:meth:`Shape.commit` update the page's contents +:meth:`Shape.draw_bezier` draw a cubic Bezier curve +:meth:`Shape.draw_circle` draw a circle around a point +:meth:`Shape.draw_curve` draw a cubic Bezier using one helper point +:meth:`Shape.draw_line` draw a line +:meth:`Shape.draw_oval` draw an ellipse +:meth:`Shape.draw_polyline` connect a sequence of points +:meth:`Shape.draw_quad` draw a quadrilateral +:meth:`Shape.draw_rect` draw a rectangle +:meth:`Shape.draw_sector` draw a circular sector or piece of pie +:meth:`Shape.draw_squiggle` draw a squiggly line +:meth:`Shape.draw_zigzag` draw a zigzag line +:meth:`Shape.finish` finish a set of draw commands +:meth:`Shape.insert_text` insert text lines +:meth:`Shape.insert_textbox` fit text into a rectangle +:attr:`Shape.doc` stores the page's document +:attr:`Shape.draw_cont` draw commands since last :meth:`Shape.finish` +:attr:`Shape.height` stores the page's height +:attr:`Shape.lastPoint` stores the current point +:attr:`Shape.page` stores the owning page +:attr:`Shape.rect` rectangle surrounding drawings +:attr:`Shape.text_cont` accumulated text insertions +:attr:`Shape.totalcont` accumulated string to be stored in :data:`contents` +:attr:`Shape.width` stores the page's width +================================ ===================================================== + +**Class API** + +.. class:: Shape + + .. method:: __init__(self, page) + + Create a new drawing. During importing PyMuPDF, the *fitz.Page* object is being given the convenience method *new_shape()* to construct a *Shape* object. During instantiation, a check will be made whether we do have a PDF page. An exception is otherwise raised. + + :arg page: an existing page of a PDF document. + :type page: :ref:`Page` + + .. method:: draw_line(p1, p2) + + Draw a line from :data:`point_like` objects *p1* to *p2*. + + :arg point_like p1: starting point + + :arg point_like p2: end point + + :rtype: :ref:`Point` + :returns: the end point, *p2*. + + .. index:: + pair: breadth; draw_squiggle + + .. method:: draw_squiggle(p1, p2, breadth=2) + + Draw a squiggly (wavy, undulated) line from :data:`point_like` objects *p1* to *p2*. An integer number of full wave periods will always be drawn, one period having a length of *4 * breadth*. The breadth parameter will be adjusted as necessary to meet this condition. The drawn line will always turn "left" when leaving *p1* and always join *p2* from the "right". + + :arg point_like p1: starting point + + :arg point_like p2: end point + + :arg float breadth: the amplitude of each wave. The condition *2 * breadth < abs(p2 - p1)* must be true to fit in at least one wave. See the following picture, which shows two points connected by one full period. + + :rtype: :ref:`Point` + :returns: the end point, *p2*. + + .. image:: images/img-breadth.* + + Here is an example of three connected lines, forming a closed, filled triangle. Little arrows indicate the stroking direction. + + >>> import fitz + >>> doc=fitz.open() + >>> page=doc.new_page() + >>> r = fitz.Rect(100, 100, 300, 200) + >>> shape=page.new_shape() + >>> shape.draw_squiggle(r.tl, r.tr) + >>> shape.draw_squiggle(r.tr, r.br) + >>> shape.draw_squiggle(r.br, r.tl) + >>> shape.finish(color=(0, 0, 1), fill=(1, 1, 0)) + >>> shape.commit() + >>> doc.save("x.pdf") + + .. image:: images/img-squiggly.* + + .. note:: Waves drawn are **not** trigonometric (sine / cosine). If you need that, have a look at `draw.py `_. + + .. index:: + pair: breadth; draw_zigzag + + .. method:: draw_zigzag(p1, p2, breadth=2) + + Draw a zigzag line from :data:`point_like` objects *p1* to *p2*. Otherwise works exactly like :meth:`Shape.draw_squiggle`. + + :arg point_like p1: starting point + + :arg point_like p2: end point + + :arg float breadth: the amplitude of the movement. The condition *2 * breadth < abs(p2 - p1)* must be true to fit in at least one period. + + :rtype: :ref:`Point` + :returns: the end point, *p2*. + + .. method:: draw_polyline(points) + + Draw several connected lines between points contained in the sequence *points*. This can be used for creating arbitrary polygons by setting the last item equal to the first one. + + :arg sequence points: a sequence of :data:`point_like` objects. Its length must at least be 2 (in which case it is equivalent to *draw_line()*). + + :rtype: :ref:`Point` + :returns: *points[-1]* -- the last point in the argument sequence. + + .. method:: draw_bezier(p1, p2, p3, p4) + + Draw a standard cubic Bézier curve from *p1* to *p4*, using *p2* and *p3* as control points. + + All arguments are :data:`point_like` \s. + + :rtype: :ref:`Point` + :returns: the end point, *p4*. + + .. note:: The points do not need to be different -- experiment a bit with some of them being equal! + + Example: + + .. image:: images/img-drawBezier.* + + .. method:: draw_oval(tetra) + + Draw an "ellipse" inside the given tetragon (quadrilateral). If it is a square, a regular circle is drawn, a general rectangle will result in an ellipse. If a quadrilateral is used instead, a plethora of shapes can be the result. + + The drawing starts and ends at the middle point of the line `bottom-left -> top-left` corners in an anti-clockwise movement. + + :arg rect_like,quad_like tetra: :data:`rect_like` or :data:`quad_like`. + + *Changed in version 1.14.5:* Quads are now also supported. + + :rtype: :ref:`Point` + :returns: the middle point of line `rect.bl -> rect.tl`, or resp. `quad.ll -> quad.ul`. Look at just a few examples here, or at the *quad-show?.py* scripts in the PyMuPDF-Utilities repository. + + .. image:: images/img-drawquad.* + :scale: 50 + + .. method:: draw_circle(center, radius) + + Draw a circle given its center and radius. The drawing starts and ends at point `center - (radius, 0)` in an **anti-clockwise** movement. This point is the middle of the enclosing square's left side. + + This is a shortcut for `draw_sector(center, start, 360, fullSector=False)`. To draw the same circle in a **clockwise** movement, use `-360` as degrees. + + :arg point_like center: the center of the circle. + + :arg float radius: the radius of the circle. Must be positive. + + :rtype: :ref:`Point` + :returns: `Point(center.x - radius, center.y)`. + + .. image:: images/img-drawcircle.* + :scale: 60 + + .. method:: draw_curve(p1, p2, p3) + + A special case of *draw_bezier()*: Draw a cubic Bezier curve from *p1* to *p3*. On each of the two lines `p1 -> p2` and `p3 -> p2` one control point is generated. Both control points will therefore be on the same side of the line `p1 -> p3`. This guaranties that the curve's curvature does not change its sign. If the lines to p2 intersect with an angle of 90 degrees, then the resulting curve is a quarter ellipse (resp. quarter circle, if of same length). + + All arguments are :data:`point_like`. + + :rtype: :ref:`Point` + :returns: the end point, *p3*. The following is a filled quarter ellipse segment. The yellow area is oriented **clockwise:** + + .. image:: images/img-drawCurve.png + :align: center + + + .. index:: + pair: fullSector; draw_sector + + .. method:: draw_sector(center, point, angle, fullSector=True) + + Draw a circular sector, optionally connecting the arc to the circle's center (like a piece of pie). + + :arg point_like center: the center of the circle. + + :arg point_like point: one of the two end points of the pie's arc segment. The other one is calculated from the *angle*. + + :arg float angle: the angle of the sector in degrees. Used to calculate the other end point of the arc. Depending on its sign, the arc is drawn anti-clockwise (positive) or clockwise. + + :arg bool fullSector: whether to draw connecting lines from the ends of the arc to the circle center. If a fill color is specified, the full "pie" is colored, otherwise just the sector. + + :rtype: :ref:`Point` + :returns: the other end point of the arc. Can be used as starting point for a following invocation to create logically connected pies charts. Examples: + + .. image:: images/img-drawSector1.* + + .. image:: images/img-drawSector2.* + + + .. method:: draw_rect(rect, *, radius=None) + + * Changed in v1.22.0: Added parameter *radius*. + + Draw a rectangle. The drawing starts and ends at the top-left corner in an anti-clockwise movement. + + :arg rect_like rect: where to put the rectangle on the page. + :arg multiple radius: draw rounded rectangle corners. If not `None`, specifies the radius of the curvature as a percentage of a rectangle side length. This must one or (a tuple of) two floats `0 < radius <= 0.5`, where 0.5 corresponds to 50% of the respective side. If a float, the radius of the curvature is computed as `radius * min(width, height)`, drawing the corner's perimeter as a quarter circle. If a tuple `(rx, ry)` is given, then the curvature is asymmetric with respect to the horizontal and vertical directions. A value of `radius=(0.5, 0.5)` draws an ellipse. + + :rtype: :ref:`Point` + :returns: top-left corner of the rectangle. + + .. method:: draw_quad(quad) + + Draw a quadrilateral. The drawing starts and ends at the top-left corner (:attr:`Quad.ul`) in an anti-clockwise movement. It is a shortcut of :meth:`Shape.draw_polyline` with the argument `(ul, ll, lr, ur, ul)`. + + :arg quad_like quad: where to put the tetragon on the page. + + :rtype: :ref:`Point` + :returns: :attr:`Quad.ul`. + + .. index:: + pair: border_width; insert_text + pair: color; insert_text + pair: encoding; insert_text + pair: fill; insert_text + pair: fontfile; insert_text + pair: fontname; insert_text + pair: fontsize; insert_text + pair: morph; insert_text + pair: render_mode; insert_text + pair: rotate; insert_text + pair: stroke_opacity; insert_text + pair: fill_opacity; insert_text + pair: oc; insert_text + + .. index:: + pair: closePath; finish + pair: color; finish + pair: dashes; finish + pair: even_odd; finish + pair: fill; finish + pair: lineCap; finish + pair: lineJoin; finish + pair: morph; finish + pair: width; finish + pair: stroke_opacity; finish + pair: fill_opacity; finish + pair: oc; finish + + + .. method:: finish(width=1, color=None, fill=None, lineCap=0, lineJoin=0, dashes=None, closePath=True, even_odd=False, morph=(fixpoint, matrix), stroke_opacity=1, fill_opacity=1, oc=0) + + Finish a set of *draw*()* methods by applying :ref:`CommonParms` to all of them. + + It has **no effect on** :meth:`Shape.insert_text` and :meth:`Shape.insert_textbox`. + + The method also supports **morphing the compound drawing** using :ref:`Point` *fixpoint* and :ref:`matrix` *matrix*. + + :arg sequence morph: morph the text or the compound drawing around some arbitrary :ref:`Point` *fixpoint* by applying :ref:`Matrix` *matrix* to it. This implies that *fixpoint* is a **fixed point** of this operation: it will not change its position. Default is no morphing (*None*). The matrix can contain any values in its first 4 components, *matrix.e == matrix.f == 0* must be true, however. This means that any combination of scaling, shearing, rotating, flipping, etc. is possible, but translations are not. + + :arg float stroke_opacity: *(new in v1.18.1)* set transparency for stroke colors. Value < 0 or > 1 will be ignored. Default is 1 (intransparent). + :arg float fill_opacity: *(new in v1.18.1)* set transparency for fill colors. Default is 1 (intransparent). + + :arg bool even_odd: request the **"even-odd rule"** for filling operations. Default is *False*, so that the **"nonzero winding number rule"** is used. These rules are alternative methods to apply the fill color where areas overlap. Only with fairly complex shapes a different behavior is to be expected with these rules. For an in-depth explanation, see :ref:`AdobeManual`, pp. 137 ff. Here is an example to demonstrate the difference. + + :arg int oc: *(new in v1.18.4)* the :data:`xref` number of an :data:`OCG` or :data:`OCMD` to make this drawing conditionally displayable. + + .. image:: images/img-even-odd.* + + .. note:: For each pixel in a shape, the following will happen: + + 1. Rule **"even-odd"** counts, how many areas contain the pixel. If this count is **odd,** the pixel is regarded **inside** the shape, if it is **even**, the pixel is **outside**. + + 2. The default rule **"nonzero winding"** in addition looks at the *"orientation"* of each area containing the pixel: it **adds 1** if an area is drawn anti-clockwise and it **subtracts 1** for clockwise areas. If the result is zero, the pixel is regarded **outside,** pixels with a non-zero count are **inside** the shape. + + Of the four shapes in above image, the top two each show three circles drawn in standard manner (anti-clockwise, look at the arrows). The lower two shapes contain one (the top-left) circle drawn clockwise. As can be seen, area orientation is irrelevant for the right column (even-odd rule). + + + .. method:: insert_text(point, text, fontsize=11, fontname="helv", fontfile=None, set_simple=False, encoding=TEXT_ENCODING_LATIN, color=None, lineheight=None, fill=None, render_mode=0, border_width=1, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + Insert text lines start at *point*. + + :arg point_like point: the bottom-left position of the first character of *text* in pixels. It is important to understand, how this works in conjunction with the *rotate* parameter. Please have a look at the following picture. The small red dots indicate the positions of *point* in each of the four possible cases. + + .. image:: images/img-inserttext.* + :scale: 33 + + :arg str/sequence text: the text to be inserted. May be specified as either a string type or as a sequence type. For sequences, or strings containing line breaks *\n*, several lines will be inserted. No care will be taken if lines are too wide, but the number of inserted lines will be limited by "vertical" space on the page (in the sense of reading direction as established by the *rotate* parameter). Any rest of *text* is discarded -- the return code however contains the number of inserted lines. + + :arg float lineheight: a factor to override the line height calculated from font properties. If not *None*, a line height of `fontsize * lineheight` will be used. + :arg float stroke_opacity: *(new in v1.18.1)* set transparency for stroke colors. Negative values and values > 1 will be ignored. Default is 1 (intransparent). + :arg float fill_opacity: *(new in v1.18.1)* set transparency for fill colors. Default is 1 (intransparent). Use this value to control transparency of the text color. Stroke opacity **only** affects the border line of characters. + + :arg int rotate: determines whether to rotate the text. Acceptable values are multiples of 90 degrees. Default is 0 (no rotation), meaning horizontal text lines oriented from left to right. 180 means text is shown upside down from **right to left**. 90 means anti-clockwise rotation, text running **upwards**. 270 (or -90) means clockwise rotation, text running **downwards**. In any case, *point* specifies the bottom-left coordinates of the first character's rectangle. Multiple lines, if present, always follow the reading direction established by this parameter. So line 2 is located **above** line 1 in case of *rotate = 180*, etc. + + :arg int oc: *(new in v1.18.4)* the :data:`xref` number of an :data:`OCG` or :data:`OCMD` to make this text conditionally displayable. + + :rtype: int + :returns: number of lines inserted. + + For a description of the other parameters see :ref:`CommonParms`. + + .. index:: + pair: align; insert_textbox + pair: border_width; insert_textbox + pair: color; insert_textbox + pair: encoding; insert_textbox + pair: expandtabs; insert_textbox + pair: fill; insert_textbox + pair: fontfile; insert_textbox + pair: fontname; insert_textbox + pair: fontsize; insert_textbox + pair: morph; insert_textbox + pair: render_mode; insert_textbox + pair: rotate; insert_textbox + pair: oc; insert_textbox + + .. method:: insert_textbox(rect, buffer, fontsize=11, fontname="helv", fontfile=None, set_simple=False, encoding=TEXT_ENCODING_LATIN, color=None, fill=None, render_mode=0, border_width=1, expandtabs=8, align=TEXT_ALIGN_LEFT, rotate=0, morph=None, stroke_opacity=1, fill_opacity=1, oc=0) + + PDF only: Insert text into the specified rectangle. The text will be split into lines and words and then filled into the available space, starting from one of the four rectangle corners, which depends on *rotate*. Line feeds and multiple space will be respected. + + :arg rect_like rect: the area to use. It must be finite and not empty. + + :arg str/sequence buffer: the text to be inserted. Must be specified as a string or a sequence of strings. Line breaks are respected also when occurring in a sequence entry. + + :arg int align: align each text line. Default is 0 (left). Centered, right and justified are the other supported options, see :ref:`TextAlign`. Please note that the effect of parameter value *TEXT_ALIGN_JUSTIFY* is only achievable with "simple" (single-byte) fonts (including the :ref:`Base-14-Fonts`). + + :arg int expandtabs: controls handling of tab characters *\t* using the *string.expandtabs()* method **per each line**. + + :arg float stroke_opacity: *(new in v1.18.1)* set transparency for stroke colors. Negative values and values > 1 will be ignored. Default is 1 (intransparent). + :arg float fill_opacity: *(new in v1.18.1)* set transparency for fill colors. Default is 1 (intransparent). Use this value to control transparency of the text color. Stroke opacity **only** affects the border line of characters. + + :arg int rotate: requests text to be rotated in the rectangle. This value must be a multiple of 90 degrees. Default is 0 (no rotation). Effectively, four different values are processed: 0, 90, 180 and 270 (= -90), each causing the text to start in a different rectangle corner. Bottom-left is 90, bottom-right is 180, and -90 / 270 is top-right. See the example how text is filled in a rectangle. This argument takes precedence over morphing. See the second example, which shows text first rotated left by 90 degrees and then the whole rectangle rotated clockwise around is lower left corner. + + :arg int oc: *(new in v1.18.4)* the :data:`xref` number of an :data:`OCG` or :data:`OCMD` to make this text conditionally displayable. + + :rtype: float + :returns: + **If positive or zero**: successful execution. The value returned is the unused rectangle line space in pixels. This may safely be ignored -- or be used to optimize the rectangle, position subsequent items, etc. + + **If negative**: no execution. The value returned is the space deficit to store text lines. Enlarge rectangle, decrease *fontsize*, decrease text amount, etc. + + .. image:: images/img-rotate.* + + .. image:: images/img-rot+morph.* + + For a description of the other parameters see :ref:`CommonParms`. + + + .. index:: + pair: overlay; commit + + .. method:: commit(overlay=True) + + Update the page's :data:`contents` with the accumulated drawings, followed by any text insertions. If text overlaps drawings, it will be written on top of the drawings. + + .. warning:: **Do not forget to execute this method:** + + If a shape is **not committed, it will be ignored and the page will not be changed!** + + The method will reset attributes :attr:`Shape.rect`, :attr:`lastPoint`, :attr:`draw_cont`, :attr:`text_cont` and :attr:`totalcont`. Afterwards, the shape object can be reused for the **same page**. + + :arg bool overlay: determine whether to put content in foreground (default) or background. Relevant only, if the page already has a non-empty :data:`contents` object. + + **---------- Attributes ----------** + + .. attribute:: doc + + For reference only: the page's document. + + :type: :ref:`Document` + + .. attribute:: page + + For reference only: the owning page. + + :type: :ref:`Page` + + .. attribute:: height + + Copy of the page's height + + :type: float + + .. attribute:: width + + Copy of the page's width. + + :type: float + + .. attribute:: draw_cont + + Accumulated command buffer for **draw methods** since last finish. Every finish method will append its commands to :attr:`Shape.totalcont`. + + :type: str + + .. attribute:: text_cont + + Accumulated text buffer. All **text insertions** go here. This buffer will be appended to :attr:`totalcont` :meth:`Shape.commit`, so that text will never be covered by drawings in the same Shape. + + :type: str + + .. attribute:: rect + + Rectangle surrounding drawings. This attribute is at your disposal and may be changed at any time. Its value is set to *None* when a shape is created or committed. Every *draw** method, and :meth:`Shape.insert_textbox` update this property (i.e. **enlarge** the rectangle as needed). **Morphing** operations, however (:meth:`Shape.finish`, :meth:`Shape.insert_textbox`) are ignored. + + A typical use of this attribute would be setting :attr:`Page.cropbox_position` to this value, when you are creating shapes for later or external use. If you have not manipulated the attribute yourself, it should reflect a rectangle that contains all drawings so far. + + If you have used morphing and need a rectangle containing the morphed objects, use the following code:: + + >>> # assuming ... + >>> morph = (point, matrix) + >>> # ... recalculate the shape rectangle like so: + >>> shape.rect = (shape.rect - fitz.Rect(point, point)) * ~matrix + fitz.Rect(point, point) + + :type: :ref:`Rect` + + .. attribute:: totalcont + + Total accumulated command buffer for draws and text insertions. This will be used by :meth:`Shape.commit`. + + :type: str + + .. attribute:: lastPoint + + For reference only: the current point of the drawing path. It is *None* at *Shape* creation and after each *finish()* and *commit()*. + + :type: :ref:`Point` + +Usage +------ +A drawing object is constructed by *shape = page.new_shape()*. After this, as many draw, finish and text insertions methods as required may follow. Each sequence of draws must be finished before the drawing is committed. The overall coding pattern looks like this:: + + >>> shape = page.new_shape() + >>> shape.draw1(...) + >>> shape.draw2(...) + >>> ... + >>> shape.finish(width=..., color=..., fill=..., morph=...) + >>> shape.draw3(...) + >>> shape.draw4(...) + >>> ... + >>> shape.finish(width=..., color=..., fill=..., morph=...) + >>> ... + >>> shape.insert_text* + >>> ... + >>> shape.commit() + >>> .... + +.. note:: + + 1. Each *finish()* combines the preceding draws into one logical shape, giving it common colors, line width, morphing, etc. If *closePath* is specified, it will also connect the end point of the last draw with the starting point of the first one. + + 2. To successfully create compound graphics, let each draw method use the end point of the previous one as its starting point. In the above pseudo code, *draw2* should hence use the returned :ref:`Point` of *draw1* as its starting point. Failing to do so, would automatically start a new path and *finish()* may not work as expected (but it won't complain either). + + 3. Text insertions may occur anywhere before the commit (they neither touch :attr:`Shape.draw_cont` nor :attr:`Shape.lastPoint`). They are appended to *Shape.totalcont* directly, whereas draws will be appended by *Shape.finish*. + + 4. Each *commit* takes all text insertions and shapes and places them in foreground or background on the page -- thus providing a way to control graphical layers. + + 5. **Only** *commit* **will update** the page's contents, the other methods are basically string manipulations. + +Examples +--------- +1. Create a full circle of pieces of pie in different colors:: + + shape = page.new_shape() # start a new shape + cols = (...) # a sequence of RGB color triples + pieces = len(cols) # number of pieces to draw + beta = 360. / pieces # angle of each piece of pie + center = fitz.Point(...) # center of the pie + p0 = fitz.Point(...) # starting point + for i in range(pieces): + p0 = shape.draw_sector(center, p0, beta, + fullSector=True) # draw piece + # now fill it but do not connect ends of the arc + shape.finish(fill=cols[i], closePath=False) + shape.commit() # update the page + +Here is an example for 5 colors: + +.. image:: images/img-cake.* + +2. Create a regular n-edged polygon (fill yellow, red border). We use *draw_sector()* only to calculate the points on the circumference, and empty the draw command buffer again before drawing the polygon:: + + shape = page.new_shape() # start a new shape + beta = -360.0 / n # our angle, drawn clockwise + center = fitz.Point(...) # center of circle + p0 = fitz.Point(...) # start here (1st edge) + points = [p0] # store polygon edges + for i in range(n): # calculate the edges + p0 = shape.draw_sector(center, p0, beta) + points.append(p0) + shape.draw_cont = "" # do not draw the circle sectors + shape.draw_polyline(points) # draw the polygon + shape.finish(color=(1,0,0), fill=(1,1,0), closePath=False) + shape.commit() + +Here is the polygon for n = 7: + +.. image:: images/img-7edges.* + +.. _CommonParms: + +Common Parameters +------------------- + +**fontname** (*str*) + + In general, there are three options: + + 1. Use one of the standard :ref:`Base-14-Fonts`. In this case, *fontfile* **must not** be specified and *"Helvetica"* is used if this parameter is omitted, too. + 2. Choose a font already in use by the page. Then specify its **reference** name prefixed with a slash "/", see example below. + 3. Specify a font file present on your system. In this case choose an arbitrary, but new name for this parameter (without "/" prefix). + + If inserted text should re-use one of the page's fonts, use its reference name appearing in :meth:`Page.get_fonts` like so: + + Suppose the font list has the item *[1024, 0, 'Type1', 'NimbusMonL-Bold', 'R366']*, then specify *fontname = "/R366", fontfile = None* to use font *NimbusMonL-Bold*. + +---- + +**fontfile** (*str*) + + File path of a font existing on your computer. If you specify *fontfile*, make sure you use a *fontname* **not occurring** in the above list. This new font will be embedded in the PDF upon *doc.save()*. Similar to new images, a font file will be embedded only once. A table of MD5 codes for the binary font contents is used to ensure this. + +---- + +**set_simple** (*bool*) + + Fonts installed from files are installed as **Type0** fonts by default. If you want to use 1-byte characters only, set this to true. This setting cannot be reverted. Subsequent changes are ignored. + +---- + +**fontsize** (*float*) + + Font size of text. + +---- + +**dashes** (*str*) + + Causes lines to be drawn dashed. The general format is `"[n m] p"` of (up to) 3 floats denoting pixel lengths. `n` is the dash length, `m` (optional) is the subsequent gap length, and `p` (the "phase" - **required**, even if 0!) specifies how many pixels should be skipped before the dashing starts. If `m` is omitted, it defaults to `n`. + + A continuous line (no dashes) is drawn with `"[] 0"` or *None* or `""`. Examples: + + * Specifying `"[3 4] 0"` means dashes of 3 and gaps of 4 pixels following each other. + * `"[3 3] 0"` and `"[3] 0"` do the same thing. + + For (the rather complex) details on how to achieve sophisticated dashing effects, see :ref:`AdobeManual`, page 217. + +---- + +**color / fill** (*list, tuple*) + + Stroke and fill colors can be specified as tuples or list of of floats from 0 to 1. These sequences must have a length of 1 (GRAY), 3 (RGB) or 4 (CMYK). For GRAY colorspace, a single float instead of the unwieldy *(float,)* or *[float]* is also accepted. Accept (default) or use `None` to not use the parameter. + + To simplify color specification, method *getColor()* in *fitz.utils* may be used to get predefined RGB color triples by name. It accepts a string as the name of the color and returns the corresponding triple. The method knows over 540 color names -- see section :ref:`ColorDatabase`. + + Please note that the term *color* usually means "stroke" color when used in conjunction with fill color. + + If letting default a color parameter to `None`, then no resp. color selection command will be generated. If *fill* and *color* are both `None`, then the drawing will contain no color specification. But it will still be "stroked", which causes PDF's default color "black" be used by Adobe Acrobat and all other viewers. + +---- + +**stroke_opacity / fill_opacity** (*floats*) + + Both values are floats in range [0, 1]. Negative values or values > 1 will ignored (in most cases). Both set the transparency such that a value 0.5 corresponds to 50% transparency, 0 means invisible and 1 means intransparent. For e.g. a rectangle the stroke opacity applies to its border and fill opacity to its interior. + + For text insertions (:meth:`Shape.insert_text` and :meth:`Shape.insert_textbox`), use *fill_opacity* for the text. At first sight this seems surprising, but it becomes obvious when you look further down to *render_mode*: *fill_opacity* applies to the yellow and *stroke_opacity* applies to the blue color. + +---- + +**border_width** (*float*) + + Set the border width for text insertions. New in v1.14.9. Relevant only if the render mode argument is used with a value greater zero. + +---- + +**render_mode** (*int*) + + *New in version 1.14.9:* Integer in `range(8)` which controls the text appearance (:meth:`Shape.insert_text` and :meth:`Shape.insert_textbox`). See page 246 in :ref:`AdobeManual`. New in v1.14.9. These methods now also differentiate between fill and stroke colors. + + * For default 0, only the text fill color is used to paint the text. For backward compatibility, using the *color* parameter instead also works. + * For render mode 1, only the border of each glyph (i.e. text character) is drawn with a thickness as set in argument *border_width*. The color chosen in the *color* argument is taken for this, the *fill* parameter is ignored. + * For render mode 2, the glyphs are filled and stroked, using both color parameters and the specified border width. You can use this value to simulate **bold text** without using another font: choose the same value for *fill* and *color* and an appropriate value for *border_width*. + * For render mode 3, the glyphs are neither stroked nor filled: the text becomes invisible. + + The following examples use border_width=0.3, together with a fontsize of 15. Stroke color is blue and fill color is some yellow. + + .. image:: images/img-rendermode.* + +---- + +**overlay** (*bool*) + + Causes the item to appear in foreground (default) or background. + +---- + +**morph** (*sequence*) + + Causes "morphing" of either a shape, created by the *draw*()* methods, or the text inserted by page methods *insert_textbox()* / *insert_text()*. If not *None*, it must be a pair *(fixpoint, matrix)*, where *fixpoint* is a :ref:`Point` and *matrix* is a :ref:`Matrix`. The matrix can be anything except translations, i.e. *matrix.e == matrix.f == 0* must be true. The point is used as a fixed point for the matrix operation. For example, if *matrix* is a rotation or scaling, then *fixpoint* is its center. Similarly, if *matrix* is a left-right or up-down flip, then the mirroring axis will be the vertical, respectively horizontal line going through *fixpoint*, etc. + + .. note:: Several methods contain checks whether the to be inserted items will actually fit into the page (like :meth:`Shape.insert_text`, or :meth:`Shape.draw_rect`). For the result of a morphing operation there is however no such guaranty: this is entirely the programmer's responsibility. + +---- + +**lineCap (deprecated: "roundCap")** (*int*) + + Controls the look of line ends. The default value 0 lets each line end at exactly the given coordinate in a sharp edge. A value of 1 adds a semi-circle to the ends, whose center is the end point and whose diameter is the line width. Value 2 adds a semi-square with an edge length of line width and a center of the line end. + + *Changed in version 1.14.15* + +---- + +**lineJoin** (*int*) + + *New in version 1.14.15:* Controls the way how line connections look like. This may be either as a sharp edge (0), a rounded join (1), or a cut-off edge (2, "butt"). + +---- + +**closePath** (*bool*) + + Causes the end point of a drawing to be automatically connected with the starting point (by a straight line). + +.. include:: footer.rst diff --git a/docs/story-class.rst b/docs/story-class.rst new file mode 100644 index 0000000..c3d0e18 --- /dev/null +++ b/docs/story-class.rst @@ -0,0 +1,293 @@ +.. include:: header.rst + +.. _Story: + +================ +Story +================ + +.. role:: htmlTag(emphasis) + +* New in v1.21.0 + +=========================================== ============================================================= +**Method / Attribute** **Short Description** +=========================================== ============================================================= +:meth:`Story.reset` "rewind" story output to its beginning +:meth:`Story.place` compute story content to fit in provided rectangle +:meth:`Story.draw` write the computed content to current page +:meth:`Story.element_positions` callback function logging currently processed story content +:attr:`Story.body` the story's underlying :htmlTag:`body` +:meth:`Story.write` places and draws Story to a DocumentWriter +:meth:`Story.write_stabilized` iterative layout of html content to a DocumentWriter +:meth:`Story.write_with_links` like `write()` but also creates PDF links +:meth:`Story.write_stabilized_with_links` like `write_stabilized()` but also creates PDF links +=========================================== ============================================================= + +**Class API** + +.. class:: Story + + .. method:: __init__(self, html=None, user_css=None, em=12, archive=None) + + Create a **story**, optionally providing HTML and CSS source. + The HTML is parsed, and held within the Story as a DOM (Document Object Model). + + This structure may be modified: content (text, images) may be added, + copied, modified or removed by using methods of the :ref:`Xml` class. + + When finished, the **story** can be written to any device; + in typical usage the device may be provided by a :ref:`DocumentWriter` to make new pages. + + Here are some general remarks: + + * The :ref:`Story` constructor parses and validates the provided HTML to create the DOM. + * PyMuPDF provides a number of ways to manipulate the HTML source by + providing access to the *nodes* of the underlying DOM. + Documents can be completely built from ground up programmatically, + or the existing DOM can be modified pretty arbitrarily. + For details of this interface, please see the :ref:`Xml` class. + * If no (or no more) changes to the DOM are required, + the story is ready to be laid out and to be fed to a series of devices + (typically devices provided by a :ref:`DocumentWriter` to produce new pages). + * The next step is to place the story and write it out. + This can either be done directly, by looping around calling `place()` and `draw()`, + or alternatively, + the looping can handled for you using the `write()` or `write_stabilised()` methods. + Which method you choose is largely a matter of taste. + + * To work in the first of these styles, the following loop should be used: + + 1. Obtain a suitable device to write to; + typically by requesting a new, + empty page from a :ref:`DocumentWriter`. + 2. Determine one or more rectangles on the page, + that should receive **story** data. + Note that not every page needs to have the same set of rectangles. + 3. Pass each rectangle to the **story** to place it, + learning what part of that rectangle has been filled, + and whether there is more story data that did not fit. + This step can be repeated several times with adjusted rectangles + until the caller is happy with the results. + 4. Optionally, at this point, + we can request details of where interesting items have been placed, + by calling the `element_positions()` method. + Items are deemed to be interesting if their integer `heading` attribute is a non-zero + (corresponding to HTML tags :htmlTag:`h1` - :htmlTag:`h6`), + if their `id` attribute is not `None` (corresponding to HTML tag :htmlTag:`id`), + or if their `href` attribute is not `None` (responding to HTML tag :htmlTag:`href`). + This can conveniently be used for automatic generation of a Table of Contents, + an index of images or the like. + 5. Next, draw that rectangle out to the device with the `draw()` method. + 6. If the most recent call to `place()` indicated that all the story data had fitted, + stop now. + 7. Otherwise, we can loop back. + If there are more rectangles to be placed on the current device (page), + we jump back to step 3 - if not, we jump back to step 1 to get a new device. + * Alternatively, in the case where you are using a :ref:`DocumentWriter`, + the `write()` or `write_stabilized()` methods can be used. + These handle all the looping for you, + in exchange for being provided with callbacks that control the behaviour + (notably a callback that enumerates the rectangles/pages to use). + * Which part of the **story** will land on which rectangle / which page, + is fully under control of the :ref:`Story` object and cannot be predicted. + * Images may be part of a **story**. They will be placed together with any surrounding text. + * Multiple stories may - independently from each other - write to the same page. + For example, one may have separate stories for page header, + page footer, regular text, comment boxes, etc. + + :arg str html: HTML source code. If omitted, a basic minimum is generated (see below). + If provided, not a complete HTML document is needed. + The in-built source parser will forgive (many / most) + HTML syntax errors and also accepts HTML fragments like + `"Hello, World!"`. + :arg str user_css: CSS source code. If provided, must contain valid CSS specifications. + :arg float em: the default text font size. + :arg archive: an :ref:`Archive` from which to load resources for rendering. Currently supported resource types are images and text fonts. If omitted, the story will not try to look up any such data and may thus produce incomplete output. + + .. note:: Instead of an actual archive, valid arguments for **creating** an :ref:`Archive` can also be provided -- in which case an archive will temporarily be constructed. So, instead of `story = fitz.Story(archive=fitz.Archive("myfolder"))`, one can also shorter write `story = fitz.Story(archive="myfolder")`. + + .. method:: place(where) + + Calculate that part of the story's content, that will fit in the provided rectangle. The method maintains a pointer which part of the story's content has already been written and upon the next invocation resumes from that pointer's position. + + :arg rect_like where: layout the current part of the content to fit into this rectangle. This must be a sub-rectangle of the page's :ref:`MediaBox`. + + :rtype: tuple[bool, rect_like] + :returns: a bool (int) `more` and a rectangle `filled`. If `more == 0`, all content of the story has been written, otherwise more is waiting to be written to subsequent rectangles / pages. Rectangle `filled` is the part of `where` that has actually been filled. + + .. method:: draw(dev, matrix=None) + + Write the content part prepared by :meth:`Story.place` to the page. + + :arg dev: the :ref:`Device` created by `dev = writer.begin_page(mediabox)`. The device knows how to call all MuPDF functions needed to write the content. + :arg matrix_like matrix: a matrix for transforming content when writing to the page. An example may be writing rotated text. The default means no transformation (i.e. the :ref:`Identity` matrix). + + .. method:: element_positions(function, args=None) + + Let the Story provide positioning information about certain HTML elements once their place on the current page has been computed - i.e. invoke this method **directly after** :meth:`Story.place`. + + *Story* will pass position information to *function*. This information can for example be used to generate a Table of Contents. + + :arg callable function: a Python function accepting an :class:`ElementPosition` object. It will be invoked by the Story object to process positioning information. The function **must** be a callable accepting exactly one argument. + :arg dict args: an optional dictionary with any **additional** information + that should be added to the :class:`ElementPosition` instance passed to `function`. + Like for example the current output page number. + Every key in this dictionary must be a string that conforms to the rules for a valid Python identifier. + The complete set of information is explained below. + + + .. method:: reset() + + Rewind the story's document to the beginning for starting over its output. + + .. attribute:: body + + The :htmlTag:`body` part of the story's DOM. This attribute contains the :ref:`Xml` node of :htmlTag:`body`. All relevant content for PDF production is contained between "" and "". + + .. method:: write(writer, rectfn, positionfn=None, pagefn=None) + + Places and draws Story to a `DocumentWriter`. Avoids the need for + calling code to implement a loop that calls `Story.place()` and + `Story.draw()` etc, at the expense of having to provide at least the + `rectfn()` callback. + + :arg writer: a `DocumentWriter` or None. + :arg rectfn: a callable taking `(rect_num: int, filled: Rect)` and + returning `(mediabox, rect, ctm)`: + mediabox: + None or rect for new page. + rect: + The next rect into which content should be placed. + ctm: + None or a `Matrix`. + :arg positionfn: None, or a callable taking `(position: ElementPosition)`: + position: + An `ElementPosition` with an extra `.page_num` member. + Typically called multiple times as we generate elements that + are headings or have an id. + :arg pagefn: + None, or a callable taking `(page_num, mediabox, dev, after)`; + called at start (`after=0`) and end (`after=1`) of each page. + + .. staticmethod:: write_stabilized(writer, contentfn, rectfn, user_css=None, em=12, positionfn=None, pagefn=None, archive=None, add_header_ids=True) + + Static method that does iterative layout of html content to a + `DocumentWriter`. + + For example this allows one to add a table of contents section + while ensuring that page numbers are patched up until stable. + + Repeatedly creates a new `Story` from `(contentfn(), + user_css, em, archive)` and lays it out with internal call + to `Story.write()`; uses a None writer and extracts the list + of `ElementPosition`'s which is passed to the next call of + `contentfn()`. + + When the html from `contentfn()` becomes unchanged, we do a + final iteration using `writer`. + + :arg writer: + A `DocumentWriter`. + :arg contentfn: + A function taking a list of `ElementPositions` and + returning a string containing html. The returned html + can depend on the list of positions, for example with a + table of contents near the start. + :arg rectfn: + A callable taking `(rect_num: int, filled: Rect)` and + returning `(mediabox, rect, ctm)`: + mediabox: + None or rect for new page. + rect: + The next rect into which content should be + placed. + ctm: + A `Matrix`. + :arg pagefn: + None, or a callable taking `(page_num, medibox, + dev, after)`; called at start (`after=0`) and end + (`after=1`) of each page. + :arg archive: + . + :arg add_header_ids: + If true, we add unique ids to all header tags that + don't already have an id. This can help automatic + generation of tables of contents. + Returns: + None. + + .. method:: write_with_links(rectfn, positionfn=None, pagefn=None) + + Similar to `write()` except that we don't have a `writer` arg + and we return a PDF `Document` in which links have been created + for each internal html link. + + .. staticmethod:: write_stabilized_with_links(contentfn, rectfn, user_css=None, em=12, positionfn=None, pagefn=None, archive=None, add_header_ids=True) + + Similar to `write_stabilized()` except that we don't have a `writer` + arg and instead return a PDF `Document` in which links have been + created for each internal html link. + + +Element Positioning CallBack function +-------------------------------------- + +The callback function can be used to log information about story output. The function's access to the information is read-only: it has no way to influence the story's output. + +A typical loop for executing a story with using this method would look like this:: + + HTML = """ + + + +

Header level 1

+

Header level 2

+

Hello MuPDF!

+ + + """ + MEDIABOX = fitz.paper_rect("letter") # size of a page + WHERE = MEDIABOX + (36, 36, -36, -36) # leave borders of 0.5 inches + story = fitz.Story(html=HTML) # make the story + writer = fitz.DocumentWriter("test.pdf") # make the writer + pno = 0 # current page number + more = 1 # will be set to 0 when done + while more: # loop until all story content is processed + dev = writer.begin_page(MEDIABOX) # make a device to write on the page + more, filled = story.place(WHERE) # compute content positions on page + story.element_positions(recorder, {"page": pno}) # provide page number in addition + story.draw(dev) + writer.end_page() + pno += 1 # increase page number + writer.close() # close output file + + def recorder(elpos): + pass + + +Attributes of the ElementPosition class +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Exactly one parameter must be passed to the function provided by :meth:`Story.element_positions`. It is an object with the following attributes: + +The parameter passed to the `recorder` function is an object with the following attributes: + +* `elpos.depth` (int) -- depth of this element in the box structure. + +* `elpos.heading` (int) -- the header level, 0 if no header, 1-6 for :htmlTag:`h1` - :htmlTag:`h6`. + +* `elpos.href` (str) -- value of the `href`attribute, or None if not defined. + +* `elpos.id` (str) -- value of the `id` attribute, or None if not defined. + +* `elpos.rect` (tuple) -- element position on page. + +* `elpos.text` (str) -- immediate text of the element. + +* `elpos.open_close` (int bit field) -- bit 0 set: opens element, bit 1 set: closes element. Relevant for elements that may contain other elements and thus may not immediately be closed after being created / opened. + +* `elpos.rect_num` (int) -- count of rectangles filled by the story so far. + +* `elpos.page_num` (int) -- page number; only present when using `fitz.Story.write*()` functions. + +.. include:: footer.rst diff --git a/docs/textpage.rst b/docs/textpage.rst new file mode 100644 index 0000000..8b8ac3a --- /dev/null +++ b/docs/textpage.rst @@ -0,0 +1,344 @@ +.. include:: header.rst + +.. _TextPage: + +================ +TextPage +================ + +This class represents text and images shown on a document page. All :ref:`MuPDF document types` are supported. + +The usual ways to create a textpage are :meth:`DisplayList.get_textpage` and :meth:`Page.get_textpage`. Because there is a limited set of methods in this class, there exist wrappers in :ref:`Page` which are handier to use. The last column of this table shows these corresponding :ref:`Page` methods. + +For a description of what this class is all about, see Appendix 2. + +======================== ================================ ============================== +**Method** **Description** page get_text or search method +======================== ================================ ============================== +:meth:`~.extractText` extract plain text "text" +:meth:`~.extractTEXT` synonym of previous "text" +:meth:`~.extractBLOCKS` plain text grouped in blocks "blocks" +:meth:`~.extractWORDS` all words with their bbox "words" +:meth:`~.extractHTML` page content in HTML format "html" +:meth:`~.extractXHTML` page content in XHTML format "xhtml" +:meth:`~.extractXML` page text in XML format "xml" +:meth:`~.extractDICT` page content in *dict* format "dict" +:meth:`~.extractJSON` page content in JSON format "json" +:meth:`~.extractRAWDICT` page content in *dict* format "rawdict" +:meth:`~.extractRAWJSON` page content in JSON format "rawjson" +:meth:`~.search` Search for a string in the page :meth:`Page.search_for` +======================== ================================ ============================== + +**Class API** + +.. class:: TextPage + + .. method:: extractText + + .. method:: extractTEXT + + Return a string of the page's complete text. The text is UTF-8 unicode and in the same sequence as specified at the time of document creation. + + :rtype: str + + + .. method:: extractBLOCKS + + Textpage content as a list of text lines grouped by block. Each list items looks like this:: + + (x0, y0, x1, y1, "lines in the block", block_no, block_type) + + The first four entries are the block's bbox coordinates, *block_type* is 1 for an image block, 0 for text. *block_no* is the block sequence number. Multiple text lines are joined via line breaks. + + For an image block, its bbox and a text line with some image meta information is included -- **not the image content**. + + This is a high-speed method with just enough information to output plain text in desired reading sequence. + + :rtype: list + + .. method:: extractWORDS + + Textpage content as a list of single words with bbox information. An item of this list looks like this:: + + (x0, y0, x1, y1, "word", block_no, line_no, word_no) + + Everything delimited by spaces is treated as a *"word"*. This is a high-speed method which e.g. allows extracting text from within given areas or recovering the text reading sequence. + + :rtype: list + + .. method:: extractHTML + + Textpage content as a string in HTML format. This version contains complete formatting and positioning information. Images are included (encoded as base64 strings). You need an HTML package to interpret the output in Python. Your internet browser should be able to adequately display this information, but see :ref:`HTMLQuality`. + + :rtype: str + + .. method:: extractDICT + + Textpage content as a Python dictionary. Provides same information detail as HTML. See below for the structure. + + :rtype: dict + + .. method:: extractJSON + + Textpage content as a JSON string. Created by `json.dumps(TextPage.extractDICT())`. It is included for backlevel compatibility. You will probably use this method ever only for outputting the result to some file. The method detects binary image data and converts them to base64 encoded strings. + + :rtype: str + + .. method:: extractXHTML + + Textpage content as a string in XHTML format. Text information detail is comparable with :meth:`extractTEXT`, but also contains images (base64 encoded). This method makes no attempt to re-create the original visual appearance. + + :rtype: str + + .. method:: extractXML + + Textpage content as a string in XML format. This contains complete formatting information about every single character on the page: font, size, line, paragraph, location, color, etc. Contains no images. You need an XML package to interpret the output in Python. + + :rtype: str + + .. method:: extractRAWDICT + + Textpage content as a Python dictionary -- technically similar to :meth:`extractDICT`, and it contains that information as a subset (including any images). It provides additional detail down to each character, which makes using XML obsolete in many cases. See below for the structure. + + :rtype: dict + + .. method:: extractRAWJSON + + Textpage content as a JSON string. Created by `json.dumps(TextPage.extractRAWDICT())`. You will probably use this method ever only for outputting the result to some file. The method detects binary image data and converts them to base64 encoded strings. + + :rtype: str + + .. method:: search(needle, quads=False) + + * Changed in v1.18.2 + + Search for *string* and return a list of found locations. + + :arg str needle: the string to search for. Upper and lower cases will all match if needle consists of ASCII letters only -- it does not yet work for "Ä" versus "ä", etc. + :arg bool quads: return quadrilaterals instead of rectangles. + :rtype: list + :returns: a list of :ref:`Rect` or :ref:`Quad` objects, each surrounding a found *needle* occurrence. As the search string may contain spaces, its parts may be found on different lines. In this case, more than one rectangle (resp. quadrilateral) are returned. **(Changed in v1.18.2)** The method **now supports dehyphenation**, so it will find e.g. "method", even if it was hyphenated in two parts "meth-" and "od" across two lines. The two returned rectangles will contain "meth" (no hyphen) and "od". + + .. note:: **Overview of changes in v1.18.2:** + + 1. The `hit_max` parameter has been removed: all hits are always returned. + 2. The `rect` parameter of the :ref:`TextPage` is now respected: only text inside this area is examined. Only characters with fully contained bboxes are considered. The wrapper method :meth:`Page.search_for` correspondingly supports a *clip* parameter. + 3. **Hyphenated words** are now found. + 4. **Overlapping rectangles** in the same line are now automatically joined. We assume that such separations are an artifact created by multiple marked content groups, containing parts of the same search needle. + + Example Quad versus Rect: when searching for needle "pymupdf", then the corresponding entry will either be the blue rectangle, or, if *quads* was specified, the quad *Quad(ul, ur, ll, lr)*. + + .. image:: images/img-quads.* + + .. attribute:: rect + + The rectangle associated with the text page. This either equals the rectangle of the creating page or the `clip` parameter of :meth:`Page.get_textpage` and text extraction / searching methods. + + .. note:: The output of text searching and most text extractions **is restricted to this rectangle**. (X)HTML and XML output will however always extract the full page. + +.. _textpagedict: + +Structure of Dictionary Outputs +-------------------------------- +Methods :meth:`TextPage.extractDICT`, :meth:`TextPage.extractJSON`, :meth:`TextPage.extractRAWDICT`, and :meth:`TextPage.extractRAWJSON` return dictionaries, containing the page's text and image content. The dictionary structures of all four methods are almost equal. They strive to map the text page's information hierarchy of blocks, lines, spans and characters as precisely as possible, by representing each of these by its own sub-dictionary: + +* A **page** consists of a list of **block dictionaries**. +* A (text) **block** consists of a list of **line dictionaries**. +* A **line** consists of a list of **span dictionaries**. +* A **span** either consists of the text itself or, for the RAW variants, a list of **character dictionaries**. +* RAW variants: a **character** is a dictionary of its origin, bbox and unicode. + +All PyMuPDF geometry objects herein (points, rectangles, matrices) are represented by there **"like"** formats: a :data:`rect_like` *tuple* is used instead of a :ref:`Rect`, etc. The reasons for this are performance and memory considerations: + +* This code is written in C, where Python tuples can easily be generated. The geometry objects on the other hand are defined in Python source only. A conversion of each Python tuple into its corresponding geometry object would add significant -- and largely unnecessary -- execution time. +* A 4-tuple needs about 168 bytes, the corresponding :ref:`Rect` 472 bytes - almost three times the size. A "dict" dictionary for a text-heavy page contains 300+ bbox objects -- which thus require about 50 KB storage as 4-tuples versus 140 KB as :ref:`Rect` objects. A "rawdict" output for such a page will however contain **4 to 5 thousand** bboxes, so in this case we talk about 750 KB versus 2 MB. + +Please also note, that only **bboxes** (= :data:`rect_like` 4-tuples) are returned, whereas a :ref:`TextPage` actually has the **full position information** -- in :ref:`Quad` format. The reason for this decision is again a memory consideration: a :data:`quad_like` needs 488 bytes (3 times the size of a :data:`rect_like`). Given the mentioned amounts of generated bboxes, returning :data:`quad_like` information would have a significant impact. + +In the vast majority of cases, we are dealing with **horizontal text only**, where bboxes provide entirely sufficient information. + +In addition, **the full quad information is not lost**: it can be recovered as needed for lines, spans, and characters by using the appropriate function from the following list: + +* :meth:`recover_quad` -- the quad of a complete span +* :meth:`recover_span_quad` -- the quad of a character subset of a span +* :meth:`recover_line_quad` -- the quad of a line +* :meth:`recover_char_quad` -- the quad of a character + +As mentioned, using these functions is ever only needed, if the text is **not written horizontally** -- `line["dir"] != (1, 0)` -- and you need the quad for text marker annotations (:meth:`Page.add_highlight_annot` and friends). + + +.. image:: images/img-textpage.* + :scale: 66 + + +Page Dictionary +~~~~~~~~~~~~~~~~~ + +=============== ============================================ +**Key** **Value** +=============== ============================================ +width width of the `clip` rectangle *(float)* +height height of the `clip` rectangle *(float)* +blocks *list* of block dictionaries +=============== ============================================ + +Block Dictionaries +~~~~~~~~~~~~~~~~~~ +Block dictionaries come in two different formats for **image blocks** and for **text blocks**. + +* *(Changed in v1.18.0)* -- new dict key *number*, the block number. +* *(Changed in v1.18.11)* -- new dict key *transform*, the image transformation matrix for image blocks. +* *(Changed in v1.18.11)* -- new dict key *size*, the size of the image in bytes for image blocks. + +**Image block:** + +=============== =============================================================== +**Key** **Value** +=============== =============================================================== +type 1 = image *(int)* +bbox image bbox on page (:data:`rect_like`) +number block count *(int)* +ext image type *(str)*, as file extension, see below +width original image width *(int)* +height original image height *(int)* +colorspace colorspace component count *(int)* +xres resolution in x-direction *(int)* +yres resolution in y-direction *(int)* +bpc bits per component *(int)* +transform matrix transforming image rect to bbox (:data:`matrix_like`) +size size of the image in bytes *(int)* +image image content *(bytes)* +=============== =============================================================== + +Possible values of the "ext" key are "bmp", "gif", "jpeg", "jpx" (JPEG 2000), "jxr" (JPEG XR), "png", "pnm", and "tiff". + +.. note:: + + 1. An image block is generated for **all and every image occurrence** on the page. Hence there may be duplicates, if an image is shown at different locations. + + 2. :ref:`TextPage` and corresponding method :meth:`Page.get_text` are **available for all document types**. Only for PDF documents, methods :meth:`Document.get_page_images` / :meth:`Page.get_images` offer some overlapping functionality as far as image lists are concerned. But both lists **may or may not** contain the same items. Any differences are most probably caused by one of the following: + + - "Inline" images (see page 214 of the :ref:`AdobeManual`) of a PDF page are contained in a textpage, but **do not appear** in :meth:`Page.get_images`. + - Annotations may also contain images -- these will **not appear** in :meth:`Page.get_images`. + - Image blocks in a textpage are generated for **every** image location -- whether or not there are any duplicates. This is in contrast to :meth:`Page.get_images`, which will list each image only once (per reference name). + - Images mentioned in the page's :data:`object` definition will **always** appear in :meth:`Page.get_images` [#f1]_. But it may happen, that there is no "display" command in the page's :data:`contents` (erroneously or on purpose). In this case the image will **not appear** in the textpage. + + 3. The image's "transformation matrix" is defined as the matrix, for which the expression `bbox / transform == fitz.Rect(0, 0, 1, 1)` is true, lookup details here: :ref:`ImageTransformation`. + + +**Text block:** + +=============== ==================================================== +**Key** **Value** +=============== ==================================================== +type 0 = text *(int)* +bbox block rectangle, :data:`rect_like` +number block count *(int)* +lines *list* of text line dictionaries +=============== ==================================================== + +Line Dictionary +~~~~~~~~~~~~~~~~~ + +=============== ===================================================== +**Key** **Value** +=============== ===================================================== +bbox line rectangle, :data:`rect_like` +wmode writing mode *(int)*: 0 = horizontal, 1 = vertical +dir writing direction, :data:`point_like` +spans *list* of span dictionaries +=============== ===================================================== + +The value of key *"dir"* is the **unit vector** `dir = (cosine, sine)` of the angle, which the text has relative to the x-axis. See the following picture: The word in each quadrant (counter-clockwise from top-right to bottom-right) is rotated by 30, 120, 210 and 300 degrees respectively. + +.. image:: images/img-line-dir.* + :scale: 100 + +Span Dictionary +~~~~~~~~~~~~~~~~~ + +Spans contain the actual text. A line contains **more than one span only**, if it contains text with different font properties. + +* Changed in version 1.14.17 Spans now also have a *bbox* key (again). +* Changed in version 1.17.6 Spans now also have an *origin* key. + +=============== ===================================================================== +**Key** **Value** +=============== ===================================================================== +bbox span rectangle, :data:`rect_like` +origin the first character's origin, :data:`point_like` +font font name *(str)* +ascender ascender of the font *(float)* +descender descender of the font *(float)* +size font size *(float)* +flags font characteristics *(int)* +color text color in sRGB format *(int)* +text (only for :meth:`extractDICT`) text *(str)* +chars (only for :meth:`extractRAWDICT`) *list* of character dictionaries +=============== ===================================================================== + +*(New in version 1.16.0):* *"color"* is the text color encoded in sRGB (int) format, e.g. 0xFF0000 for red. There are functions for converting this integer back to formats (r, g, b) (PDF with float values from 0 to 1) :meth:`sRGB_to_pdf`, or (R, G, B), :meth:`sRGB_to_rgb` (with integer values from 0 to 255). + +*(New in v1.18.5):* *"ascender"* and *"descender"* are font properties, provided relative to fontsize 1. Note that descender is a negative value. The following picture shows the relationship to other values and properties. + +.. image:: images/img-asc-desc.* + :scale: 60 + +These numbers may be used to compute the minimum height of a character (or span) -- as opposed to the standard height provided in the "bbox" values (which actually represents the **line height**). The following code recalculates the span bbox to have a height of **fontsize** exactly fitting the text inside: + +>>> a = span["ascender"] +>>> d = span["descender"] +>>> r = fitz.Rect(span["bbox"]) +>>> o = fitz.Point(span["origin"]) # its y-value is the baseline +>>> r.y1 = o.y - span["size"] * d / (a - d) +>>> r.y0 = r.y1 - span["size"] +>>> # r now is a rectangle of height 'fontsize' + +.. caution:: The above calculation may deliver a **larger** height! This may e.g. happen for OCRed documents, where the risk of all sorts of text artifacts is high. MuPDF tries to come up with a reasonable bbox height, independently from the fontsize found in the PDF. So please ensure that the height of `span["bbox"]` is **larger** than `span["size"]`. + +.. note:: You may request PyMuPDF to do all of the above automatically by executing `fitz.TOOLS.set_small_glyph_heights(True)`. This sets a global parameter so that all subsequent text searches and text extractions are based on reduced glyph heights, where meaningful. + +The following shows the original span rectangle in red and the rectangle with re-computed height in blue. + +.. image:: images/img-span-rect.* + :scale: 200 + + +*"flags"* is an integer, which represents font properties except for the first bit 0. They are to be interpreted like this: + +* bit 0: superscripted (2\ :sup:`0`) -- not a font property, detected by MuPDF code. +* bit 1: italic (2\ :sup:`1`) +* bit 2: serifed (2\ :sup:`2`) +* bit 3: monospaced (2\ :sup:`3`) +* bit 4: bold (2\ :sup:`4`) + +Test these characteristics like so: + +>>> if flags & 2**1: print("italic") +>>> # etc. + +Bits 1 thru 4 are font properties, i.e. encoded in the font program. Please note, that this information is not necessarily correct or complete: fonts quite often contain wrong data here. + +Character Dictionary for :meth:`extractRAWDICT` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +=============== =========================================================== +**Key** **Value** +=============== =========================================================== +origin character's left baseline point, :data:`point_like` +bbox character rectangle, :data:`rect_like` +c the character (unicode) +=============== =========================================================== + +This image shows the relationship between a character's bbox and its quad: |textpagechar| + +.. |textpagechar| image:: images/img-textpage-char.* + :align: top + :scale: 66 + + +.. rubric:: Footnotes + +.. [#f1] Image specifications for a PDF page are done in a page's (sub-) :data:`dictionary`, called *"/Resources"*. Resource dictionaries can be **inherited** from the page's parent object (usually the :data:`catalog`). The PDF creator may e.g. define one */Resources* on file level, naming all images and all fonts ever used by any page. In these cases, :meth:`Page.get_images` and :meth:`Page.get_fonts` will return the same lists for all pages. + +.. include:: footer.rst diff --git a/docs/textwriter.rst b/docs/textwriter.rst new file mode 100644 index 0000000..6cc326c --- /dev/null +++ b/docs/textwriter.rst @@ -0,0 +1,194 @@ +.. include:: header.rst + +.. _TextWriter: + +================ +TextWriter +================ + +* New in v1.16.18 + +This class represents a MuPDF *text* object. The basic idea is to **decouple (1) text preparation, and (2) text output** to PDF pages. + +During **preparation**, a text writer stores any number of text pieces ("spans") together with their positions and individual font information. The **output** of the writer's prepared content may happen multiple times to any PDF page with a compatible page size. + +A text writer is an elegant alternative to methods :meth:`Page.insert_text` and friends: + +* **Improved text positioning:** Choose any point where insertion of text should start. Storing text returns the "cursor position" after the *last character* of the span. +* **Free font choice:** Each text span has its own font and fontsize. This lets you easily switch when composing a larger text. +* **Automatic fallback fonts:** If a character is not supported by the chosen font, alternative fonts are automatically searched. This significantly reduces the risk of seeing unprintable symbols in the output ("TOFUs" -- looking like a small rectangle). PyMuPDF now also comes with the **universal font "Droid Sans Fallback Regular"**, which supports **all Latin** characters (including Cyrillic and Greek), and **all CJK** characters (Chinese, Japanese, Korean). +* **Cyrillic and Greek Support:** The :ref:`Base-14-fonts` have integrated support of Cyrillic and Greek characters **without specifying encoding.** Your text may be a mixture of Latin, Greek and Cyrillic. +* **Transparency support:** Parameter *opacity* is supported. This offers a handy way to create watermark-style text. +* **Justified text:** Supported for any font -- not just simple fonts as in :meth:`Page.insert_textbox`. +* **Reusability:** A TextWriter object exists independent from PDF pages. It can be written multiple times, either to the same or to other pages, in the same or in different PDFs, choosing different colors or transparency. + +Using this object entails three steps: + +1. When **created**, a TextWriter requires a fixed **page rectangle** in relation to which it calculates text positions. A text writer can write to pages of this size only. +2. Store text in the TextWriter using methods :meth:`TextWriter.append`, :meth:`TextWriter.appendv` and :meth:`TextWriter.fill_textbox` as often as is desired. +3. Output the TextWriter object on some PDF page(s). + +.. note:: + + * Starting with version 1.17.0, TextWriters **do support** text rotation via the *morph* parameter of :meth:`TextWriter.write_text`. + + * There also exists :meth:`Page.write_text` which combines one or more TextWriters and jointly writes them to a given rectangle and with a given rotation angle -- much like :meth:`Page.show_pdf_page`. + + +================================ ============================================ +**Method / Attribute** **Short Description** +================================ ============================================ +:meth:`~TextWriter.append` Add text in horizontal write mode +:meth:`~TextWriter.appendv` Add text in vertical write mode +:meth:`~TextWriter.fill_textbox` Fill rectangle (horizontal write mode) +:meth:`~TextWriter.write_text` Output TextWriter to a PDF page +:attr:`~TextWriter.color` Text color (can be changed) +:attr:`~TextWriter.last_point` Last written character ends here +:attr:`~TextWriter.opacity` Text opacity (can be changed) +:attr:`~TextWriter.rect` Page rectangle used by this TextWriter +:attr:`~TextWriter.text_rect` Area occupied so far +================================ ============================================ + + +**Class API** + +.. class:: TextWriter + + .. method:: __init__(self, rect, opacity=1, color=None) + + :arg rect-like rect: rectangle internally used for text positioning computations. + :arg float opacity: sets the transparency for the text to store here. Values outside the interval `[0, 1)` will be ignored. A value of e.g. 0.5 means 50% transparency. + :arg float,sequ color: the color of the text. All colors are specified as floats *0 <= color <= 1*. A single float represents some gray level, a sequence implies the colorspace via its length. + + + .. method:: append(pos, text, font=None, fontsize=11, language=None, right_to_left=False, small_caps=0) + + * *Changed in v1.18.9* + * *Changed in v1.18.15* + + Add some new text in horizontal writing. + + :arg point_like pos: start position of the text, the bottom left point of the first character. + :arg str text: a string of arbitrary length. It will be written starting at position "pos". + :arg font: a :ref:`Font`. If omitted, `fitz.Font("helv")` will be used. + :arg float fontsize: the fontsize, a positive number, default 11. + :arg str language: the language to use, e.g. "en" for English. Meaningful values should be compliant with the ISO 639 standards 1, 2, 3 or 5. Reserved for future use: currently has no effect as far as we know. + :arg bool right_to_left: *(New in v1.18.9)* whether the text should be written from right to left. Applicable for languages like Arabian or Hebrew. Default is *False*. If *True*, any Latin parts within the text will automatically converted. There are no other consequences, i.e. :attr:`TextWriter.last_point` will still be the rightmost character, and there neither is any alignment taking place. Hence you may want to use :meth:`TextWriter.fill_textbox` instead. + :arg bool small_caps: *(New in v1.18.15)* look for the character's Small Capital version in the font. If present, take that value instead. Otherwise the original character (this font or the fallback font) will be taken. The fallback font will never return small caps. For example, this snippet:: + + >>> doc = fitz.open() + >>> page = doc.new_page() + >>> text = "PyMuPDF: the Python bindings for MuPDF" + >>> font = fitz.Font("figo") # choose a font with small caps + >>> tw = fitz.TextWriter(page.rect) + >>> tw.append((50,100), text, font=font, small_caps=True) + >>> tw.write_text(page) + >>> doc.ez_save("x.pdf") + + will produce this PDF text: + + .. image:: images/img-smallcaps.* + + + :returns: :attr:`text_rect` and :attr:`last_point`. *(Changed in v1.18.0:)* Raises an exception for an unsupported font -- checked via :attr:`Font.is_writable`. + + + .. method:: appendv(pos, text, font=None, fontsize=11, language=None, small_caps=0) + + *Changed in v1.18.15* + + Add some new text in vertical, top-to-bottom writing. + + :arg point_like pos: start position of the text, the bottom left point of the first character. + :arg str text: a string. It will be written starting at position "pos". + :arg font: a :ref:`Font`. If omitted, `fitz.Font("helv")` will be used. + :arg float fontsize: the fontsize, a positive float, default 11. + :arg str language: the language to use, e.g. "en" for English. Meaningful values should be compliant with the ISO 639 standards 1, 2, 3 or 5. Reserved for future use: currently has no effect as far as we know. + :arg bool small_caps: *(New in v1.18.15)* see :meth:`append`. + + :returns: :attr:`text_rect` and :attr:`last_point`. *(Changed in v1.18.0:)* Raises an exception for an unsupported font -- checked via :attr:`Font.is_writable`. + + .. method:: fill_textbox(rect, text, *, pos=None, font=None, fontsize=11, align=0, right_to_left=False, warn=None, small_caps=0) + + * Changed in 1.17.3: New parameter `pos` to specify where to start writing within rectangle. + * Changed in v1.18.9: Return list of lines which do not fit in rectangle. Support writing right-to-left (e.g. Arabian, Hebrew). + * Changed in v1.18.15: Prefer small caps if supported by the font. + + Fill a given rectangle with text in horizontal writing mode. This is a convenience method to use as an alternative for :meth:`append`. + + :arg rect_like rect: the area to fill. No part of the text will appear outside of this. + :arg str,sequ text: the text. Can be specified as a (UTF-8) string or a list / tuple of strings. A string will first be converted to a list using *splitlines()*. Every list item will begin on a new line (forced line breaks). + :arg point_like pos: *(new in v1.17.3)* start storing at this point. Default is a point near rectangle top-left. + :arg font: the :ref:`Font`, default `fitz.Font("helv")`. + :arg float fontsize: the fontsize. + :arg int align: text alignment. Use one of TEXT_ALIGN_LEFT, TEXT_ALIGN_CENTER, TEXT_ALIGN_RIGHT or TEXT_ALIGN_JUSTIFY. + :arg bool right_to_left: *(New in v1.18.9)* whether the text should be written from right to left. Applicable for languages like Arabian or Hebrew. Default is *False*. If *True*, any Latin parts are automatically reverted. You must still set the alignment (if you want right alignment), it does not happen automatically -- the other alignment options remain available as well. + :arg bool warn: on text overflow do nothing, warn, or raise an exception. Overflow text will never be written. **Changed in v1.18.9:** + + * Default is *None*. + * The list of overflow lines will be returned. + + :arg bool small_caps: *(New in v1.18.15)* see :meth:`append`. + + :rtype: list + :returns: *New in v1.18.9* -- List of lines that did not fit in the rectangle. Each item is a tuple `(text, length)` containing a string and its length (on the page). + + .. note:: Use these methods as often as is required -- there is no technical limit (except memory constraints of your system). You can also mix :meth:`append` and text boxes and have multiple of both. Text positioning is exclusively controlled by the insertion point. Therefore there is no need to adhere to any order. *(Changed in v1.18.0:)* Raise an exception for an unsupported font -- checked via :attr:`Font.is_writable`. + + + .. method:: write_text(page, opacity=None, color=None, morph=None, overlay=True, oc=0, render_mode=0) + + Write the TextWriter text to a page, which is the only mandatory parameter. The other parameters can be used to temporarily override the values used when the TextWriter was created. + + :arg page: write to this :ref:`Page`. + :arg float opacity: override the value of the TextWriter for this output. + :arg sequ color: override the value of the TextWriter for this output. + :arg sequ morph: modify the text appearance by applying a matrix to it. If provided, this must be a sequence *(fixpoint, matrix)* with a point-like *fixpoint* and a matrix-like *matrix*. A typical example is rotating the text around *fixpoint*. + :arg bool overlay: put in foreground (default) or background. + :arg int oc: *(new in v1.18.4)* the :data:`xref` of an :data:`OCG` or :data:`OCMD`. + :arg int render_mode: The PDF `Tr` operator value. Values: 0 (default), 1, 2, 3 (invisible). + + .. image:: images/img-rendermode.* + + + .. attribute:: text_rect + + The area currently occupied. + + :rtype: :ref:`Rect` + + .. attribute:: last_point + + The "cursor position" -- a :ref:`Point` -- after the last written character (its bottom-right). + + :rtype: :ref:`Point` + + .. attribute:: opacity + + The text opacity (modifiable). + + :rtype: float + + .. attribute:: color + + The text color (modifiable). + + :rtype: float,tuple + + .. attribute:: rect + + The page rectangle for which this TextWriter was created. Must not be modified. + + :rtype: :ref:`Rect` + + +.. note:: To see some demo scripts dealing with TextWriter, have a look at `this `_ repository. + + 1. Opacity and color apply to **all the text** in this object. + 2. If you need different colors / transparency, you must create a separate TextWriter. Whenever you determine the color should change, simply append the text to the respective TextWriter using the previously returned :attr:`last_point` as position for the new text span. + 3. Appending items or text boxes can occur in arbitrary order: only the position parameter controls where text appears. + 4. Font and fontsize can freely vary within the same TextWriter. This can be used to let text with different properties appear on the same displayed line: just specify *pos* accordingly, and e.g. set it to :attr:`last_point` of the previously added item. + 5. You can use the *pos* argument of :meth:`TextWriter.fill_textbox` to set the position of the first text character. This allows filling the same textbox with contents from different :ref:`TextWriter` objects, thus allowing for multiple colors, opacities, etc. + 6. MuPDF does not support all fonts with this feature, e.g. no Type3 fonts. Starting with v1.18.0 this can be checked via the font attribute :attr:`Font.is_writable`. This attribute is also checked when using :ref:`TextWriter` methods. + +.. include:: footer.rst diff --git a/docs/the-basics.rst b/docs/the-basics.rst new file mode 100644 index 0000000..e878f2e --- /dev/null +++ b/docs/the-basics.rst @@ -0,0 +1,1114 @@ +.. include:: header.rst + +.. _TheBasics: + + +============================== +The Basics +============================== + +.. _Supported_File_Types: + +Supported File Types +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +:title:`PyMuPDF` supports the following file types: + +.. raw:: html + + + +
+ + + + + + + + + + + + + + + +
File type
Document Formats + PDF + XPS + EPUB + MOBI + FB2 + CBZ + SVG +
Image Formats + +
Input formats JPG/JPEG, PNG, BMP, GIF, TIFF, PNM, PGM, PBM, PPM, PAM, JXR, JPX/JP2, PSD
+
Output formats JPG/JPEG, PNG, PNM, PGM, PBM, PPM, PAM, PSD, PS
+
+ + +.. _The_Basics_Opening_Files: + +Opening a File +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +To open a file, do the following: + +.. raw:: html + +
+        
+            import fitz
+
+            doc = fitz.open("a.pdf") # open a document
+        
+    
+ + +Opening with :index:`a Wrong File Extension ` +"""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""" + +If you have a document with a wrong file extension for its type, you can still correctly open it. + +Assume that *"some.file"* is actually an XPS. Open it like so: + +.. raw:: html + +
+        
+            import fitz
+
+            doc = fitz.open("some.file", filetype="xps")
+        
+    
+ + + +.. note:: + + **Taking it further** + + There are many file types beyond :title:`PDF` which can be opened by :title:`PyMuPDF`, for more details see the list of :ref:`supported file types`. + + :title:`PyMuPDF` itself does not try to determine the file type from the file contents. **You** are responsible for supplying the filetype info in some way -- either implicitly via the file extension, or explicitly as shown. There are pure :title:`Python` packages like `filetype `_ that help you doing this. Also consult the :ref:`Document` chapter for a full description. + + If :title:`PyMuPDF` encounters a file with an unknown / missing extension, it will try to open it as a :title:`PDF`. So in these cases there is no need for additional precautions. Similarly, for memory documents, you can just specify `doc=fitz.open(stream=mem_area)` to open it as a :title:`PDF` document. + + If you attempt to open an unsupported file then :title:`PyMuPDF` will throw a file data error. + +---------- + + +.. _The_Basics_Extracting_Text: + +Extract text from a :title:`PDF` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To extract all the text from a :title:`PDF` file, do the following: + +.. raw:: html + +
+        
+            import fitz
+
+            doc = fitz.open("a.pdf") # open a document
+            out = open("output.txt", "wb") # create a text output
+            for page in doc: # iterate the document pages
+                text = page.get_text().encode("utf8") # get plain text (is in UTF-8)
+                out.write(text) # write text of page
+                out.write(bytes((12,))) # write page delimiter (form feed 0x0C)
+            out.close()
+        
+    
+ +.. note:: + + **Taking it further** + + There are many more examples which explain how to extract text from specific areas or how to extract tables from documents. Please refer to the :ref:`How to Guide for Text`. + + **API reference** + + - :meth:`Page.get_text` + +---------- + + +.. _The_Basics_Extracting_Images: + +Extract images from a :title:`PDF` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To extract all the images from a :title:`PDF` file, do the following: + +.. raw:: html + +
+        
+            import fitz
+
+            doc = fitz.open("test.pdf") # open a document
+
+            for page_index in range(len(doc)): # iterate over pdf pages
+                page = doc[page_index] # get the page
+                image_list = page.get_images()
+
+                # print the number of images found on the page
+                if image_list:
+                    print(f"Found {len(image_list)} images on page {page_index}")
+                else:
+                    print("No images found on page", page_index)
+
+                for image_index, img in enumerate(image_list, start=1): # enumerate the image list
+                    xref = img[0] # get the XREF of the image
+                    pix = fitz.Pixmap(doc, xref) # create a Pixmap
+
+                    if pix.n - pix.alpha > 3: # CMYK: convert to RGB first
+                        pix = fitz.Pixmap(fitz.csRGB, pix)
+
+                    pix.save("page_%s-image_%s.png" % (page_index, image_index)) # save the image as png
+                    pix = None
+        
+    
+ + +.. note:: + + **Taking it further** + + There are many more examples which explain how to extract text from specific areas or how to extract tables from documents. Please refer to the :ref:`How to Guide for Text`. + + **API reference** + + - :meth:`Page.get_images` + - :ref:`Pixmap` + + +---------- + +.. _The_Basics_Merging_PDF: +.. _merge PDF: +.. _join PDF: + +Merging :title:`PDF` files +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To merge :title:`PDF` files, do the following: + +.. raw:: html + +
+        
+            import fitz
+
+            doc_a = fitz.open("a.pdf") # open the 1st document
+            doc_b = fitz.open("b.pdf") # open the 2nd document
+
+            doc_a.insert_pdf(doc_b) # merge the docs
+            doc_a.save("a+b.pdf") # save the merged document with a new filename
+        
+    
+ + +Merging :title:`PDF` files with other types of file +""""""""""""""""""""""""""""""""""""""""""""""""""""" + +With :meth:`Document.insert_file` you can invoke the method to merge :ref:`supported files` with :title:`PDF`. For example: + +.. raw:: html + +
+        
+            import fitz
+
+            doc_a = fitz.open("a.pdf") # open the 1st document
+            doc_b = fitz.open("b.svg") # open the 2nd document
+
+            doc_a.insert_file(doc_b) # merge the docs
+            doc_a.save("a+b.pdf") # save the merged document with a new filename
+        
+    
+ + + +.. note:: + + **Taking it further** + + It is easy to join PDFs with :meth:`Document.insert_pdf` & :meth:`Document.insert_file`. Given open :title:`PDF` documents, you can copy page ranges from one to the other. You can select the point where the copied pages should be placed, you can revert the page sequence and also change page rotation. This Wiki `article `_ contains a full description. + + The GUI script `join.py `_ uses this method to join a list of files while also joining the respective table of contents segments. It looks like this: + + .. image:: images/img-pdfjoiner.* + :scale: 60 + + **API reference** + + - :meth:`Document.insert_pdf` + - :meth:`Document.insert_file` + + +---------- + + +.. _The_Basics_Watermarks: + +Adding a watermark to a :title:`PDF` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To add a watermark to a :title:`PDF` file, do the following: + +.. raw:: html + +
+        
+            import fitz
+
+            doc = fitz.open("document.pdf") # open a document
+
+            for page_index in range(len(doc)): # iterate over pdf pages
+                page = doc[page_index] # get the page
+
+                # insert an image watermark from a file name to fit the page bounds
+                page.insert_image(page.bound(),filename="watermark.png", overlay=False)
+
+            doc.save("watermarked-document.pdf") # save the document with a new filename
+        
+    
+ +.. note:: + + **Taking it further** + + Adding watermarks is essentially as simple as adding an image at the base of each :title:`PDF` page. You should ensure that the image has the required opacity and aspect ratio to make it look the way you need it to. + + In the example above a new image is created from each file reference, but to be more performant (by saving memory and file size) this image data should be referenced only once - see the code example and explanation on :meth:`Page.insert_image` for the implemetation. + + **API reference** + + - :meth:`Page.bound` + - :meth:`Page.insert_image` + + +---------- + + +.. _The_Basics_Images: + +Adding an image to a :title:`PDF` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To add an image to a :title:`PDF` file, for example a logo, do the following: + +.. raw:: html + +
+        
+            import fitz
+
+            doc = fitz.open("document.pdf") # open a document
+
+            for page_index in range(len(doc)): # iterate over pdf pages
+                page = doc[page_index] # get the page
+
+                # insert an image logo from a file name at the top left of the document
+                page.insert_image(fitz.Rect(0,0,50,50),filename="my-logo.png")
+
+            doc.save("logo-document.pdf") # save the document with a new filename
+        
+    
+ +.. note:: + + **Taking it further** + + As with the watermark example you should ensure to be more performant by only referencing the image once if possible - see the code example and explanation on :meth:`Page.insert_image`. + + **API reference** + + - :ref:`Rect` + - :meth:`Page.insert_image` + + +---------- + + +.. _The_Basics_Rotating: + +Rotating a :title:`PDF` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To add a rotation to a page, do the following: + +.. raw:: html + +
+        
+            import fitz
+
+            doc = fitz.open("test.pdf") # open document
+            page = doc[0] # get the 1st page of the document
+            page.set_rotation(90) # rotate the page
+            doc.save("rotated-page-1.pdf")
+        
+    
+ + +.. note:: + + **API reference** + + - :meth:`Page.set_rotation` + + +---------- + +.. _The_Basics_Cropping: + +Cropping a :title:`PDF` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To crop a page to a defined :ref:`Rect`, do the following: + +.. raw:: html + +
+        
+            import fitz
+
+            doc = fitz.open("test.pdf") # open document
+            page = doc[0] # get the 1st page of the document
+            page.set_cropbox(fitz.Rect(100, 100, 400, 400)) # set a cropbox for the page
+            doc.save("cropped-page-1.pdf")
+        
+    
+ + +.. note:: + + **API reference** + + - :meth:`Page.set_cropbox` + + +---------- + + +.. _The_Basics_Attaching_Files: + +:index:`Attaching Files ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To attach another file to a page, do the following: + +.. raw:: html + +
+    
+        import fitz
+
+        doc = fitz.open("test.pdf") # open main document
+        attachment = fitz.open("my-attachment.pdf") # open document you want to attach
+
+        page = doc[0] # get the 1st page of the document
+        point = fitz.Point(100, 100) # create the point where you want to add the attachment
+        attachment_data = attachment.tobytes() # get the document byte data as a buffer
+
+        # add the file annotation with the point, data and the file name
+        file_annotation = page.add_file_annot(point, attachment_data, "attachment.pdf")
+
+        doc.save("document-with-attachment.pdf") # save the document
+    
+  
+ +.. note:: + + **Taking it further** + + When adding the file with :meth:`Page.add_file_annot` note that the third parameter for the `filename` should include the actual file extension. Without this the attachment possibly will not be able to be recognized as being something which can be opened. For example, if the `filename` is just *"attachment"* when view the resulting PDF and attempting to open the attachment you may well get an error. However, with *"attachment.pdf"* this can be recognized and opened by PDF viewers as a valid file type. + + The default icon for the attachment is by default a "push pin", however you can change this by setting the `icon` parameter. + + **API reference** + + - :ref:`Point` + - :meth:`Document.tobytes` + - :meth:`Page.add_file_annot` + + +---------- + + +.. _The_Basics_Embedding_Files: + +:index:`Embedding Files ` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To embed a file to a document, do the following: + +.. raw:: html + +
+    
+        import fitz
+
+        doc = fitz.open("test.pdf") # open main document
+        embedded_doc = fitz.open("my-embed.pdf") # open document you want to embed
+
+        embedded_data = embedded_doc.tobytes() # get the document byte data as a buffer
+
+        # embed with the file name and the data
+        doc.embfile_add("my-embedded_file.pdf", embedded_data)
+
+        doc.save("document-with-embed.pdf") # save the document
+    
+  
+ + + + +.. note:: + + **Taking it further** + + As with :ref:`attaching files`, when adding the file with :meth:`Document.embfile_add` note that the first parameter for the `filename` should include the actual file extension. + + **API reference** + + - :meth:`Document.tobytes` + - :meth:`Document.embfile_add` + + +---------- + + + +.. _The_Basics_Deleting_Pages: + +Deleting Pages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To delete a page from a document, do the following: + +.. raw:: html + +
+    
+        import fitz
+
+        doc = fitz.open("test.pdf") # open a document
+        doc.delete_page(0) # delete the 1st page of the document
+        doc.save("test-deleted-page-one.pdf") # save the document
+
+    
+  
+ + +To delete a multiple pages from a document, do the following: + +.. raw:: html + +
+    
+        import fitz
+
+        doc = fitz.open("test.pdf") # open a document
+        doc.delete_pages(from_page=9, to_page=14) # delete a page range from the document
+        doc.save("test-deleted-pages.pdf") # save the document
+
+    
+  
+ + +.. note:: + + **Taking it further** + + The page index is zero-based, so to delete page 10 of a document you would do the following `doc.delete_page(9)`. + + Similarly, `doc.delete_pages(from_page=9, to_page=14)` will delete pages 10 - 15 inclusive. + + + **API reference** + + - :meth:`Document.delete_page` + - :meth:`Document.delete_pages` + +---------- + + +.. _The_Basics_Rearrange_Pages: + +Re-Arranging Pages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To re-arrange pages, do the following: + +.. raw:: html + +
+    
+        import fitz
+
+        doc = fitz.open("test.pdf") # open a document
+        doc.move_page(1,0) # move the 2nd page of the document to the start of the document
+        doc.save("test-page-moved.pdf") # save the document
+    
+  
+ +.. note:: + + **API reference** + + - :meth:`Document.move_page` + +---------- + + + +.. _The_Basics_Copying_Pages: + +Copying Pages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +To copy pages, do the following: + + +.. raw:: html + +
+    
+        import fitz
+
+        doc = fitz.open("test.pdf") # open a document
+        doc.copy_page(0) # copy the 1st page and puts it at the end of the document
+        doc.save("test-page-copied.pdf") # save the document
+    
+  
+ +.. note:: + + **API reference** + + - :meth:`Document.copy_page` + + +---------- + +.. _The_Basics_Selecting_Pages: + +Selecting Pages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +To select pages, do the following: + +.. raw:: html + +
+    
+        import fitz
+
+        doc = fitz.open("test.pdf") # open a document
+        doc.select([0, 1]) # select the 1st & 2nd page of the document
+        doc.save("just-page-one-and-two.pdf") # save the document
+    
+  
+ +.. note:: + + **Taking it further** + + With :title:`PyMuPDF` you have all options to copy, move, delete or re-arrange the pages of a :title:`PDF`. Intuitive methods exist that allow you to do this on a page-by-page level, like the :meth:`Document.copy_page` method. + + Or you alternatively prepare a complete new page layout in form of a :title:`Python` sequence, that contains the page numbers you want, in the sequence you want, and as many times as you want each page. The following may illustrate what can be done with :meth:`Document.select` + + .. raw:: html + +
+        
+            doc.select([1, 1, 1, 5, 4, 9, 9, 9, 0, 2, 2, 2])
+        
+      
+ + + Now let's prepare a PDF for double-sided printing (on a printer not directly supporting this): + + The number of pages is given by `len(doc)` (equal to `doc.page_count`). The following lists represent the even and the odd page numbers, respectively: + + .. raw:: html + +
+        
+            p_even = [p in range(doc.page_count) if p % 2 == 0]
+            p_odd  = [p in range(doc.page_count) if p % 2 == 1]
+        
+      
+ + + This snippet creates the respective sub documents which can then be used to print the document: + + .. raw:: html + +
+        
+            doc.select(p_even) # only the even pages left over
+            doc.save("even.pdf") # save the "even" PDF
+            doc.close() # recycle the file
+            doc = fitz.open(doc.name) # re-open
+            doc.select(p_odd) # and do the same with the odd pages
+            doc.save("odd.pdf")
+        
+      
+ + For more information also have a look at this Wiki `article `_. + + + The following example will reverse the order of all pages (**extremely fast:** sub-second time for the 756 pages of the :ref:`AdobeManual`): + + .. raw:: html + +
+        
+            lastPage = doc.page_count - 1
+            for i in range(lastPage):
+                doc.move_page(lastPage, i) # move current last page to the front
+        
+      
+ + + This snippet duplicates the PDF with itself so that it will contain the pages *0, 1, ..., n, 0, 1, ..., n* **(extremely fast and without noticeably increasing the file size!)**: + + .. raw:: html + +
+        
+            page_count = len(doc)
+            for i in range(page_count):
+                doc.copy_page(i) # copy this page to after last page
+        
+      
+ + + **API reference** + + - :meth:`Document.select` + +---------- + + +.. _The_Basics_Adding_Blank_Pages: + + + + +Adding Blank Pages +~~~~~~~~~~~~~~~~~~~~~ + +To add a blank page, do the following: + +.. raw:: html + +
+    
+        import fitz
+
+        doc = fitz.open(...) # some new or existing PDF document
+        page = doc.new_page(-1, # insertion point: end of document
+                            width = 595, # page dimension: A4 portrait
+                            height = 842)
+        doc.save("doc-with-new-blank-page.pdf") # save the document
+    
+  
+ +.. note:: + + **Taking it further** + + Use this to create the page with another pre-defined paper format: + + .. raw:: html +
+            
+                w, h = fitz.paper_size("letter-l")  # 'Letter' landscape
+                page = doc.new_page(width = w, height = h)
+            
+        
+ + The convenience function :meth:`paper_size` knows over 40 industry standard paper formats to choose from. To see them, inspect dictionary :attr:`paperSizes`. Pass the desired dictionary key to :meth:`paper_size` to retrieve the paper dimensions. Upper and lower case is supported. If you append "-L" to the format name, the landscape version is returned. + + Here is a 3-liner that creates a :title:`PDF`: with one empty page. Its file size is 460 bytes: + + .. raw:: html +
+            
+                doc = fitz.open()
+                doc.new_page()
+                doc.save("A4.pdf")
+            
+        
+ + + **API reference** + + - :meth:`Document.new_page` + - :attr:`paperSizes` + + +---------- + + +.. _The_Basics_Inserting_Pages: + +Inserting Pages with Text Content +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Using the :meth:`Document.insert_page` method also inserts a new page and accepts the same `width` and `height` parameters. But it lets you also insert arbitrary text into the new page and returns the number of inserted lines. + +.. raw:: html + +
+    
+        import fitz
+
+        doc = fitz.open(...)  # some new or existing PDF document
+        n = doc.insert_page(-1, # default insertion point
+                            text = "The quick brown fox jumped over the lazy dog",
+                            fontsize = 11,
+                            width = 595,
+                            height = 842,
+                            fontname = "Helvetica", # default font
+                            fontfile = None, # any font file name
+                            color = (0, 0, 0)) # text color (RGB)
+    
+  
+ + + +.. note:: + + **Taking it further** + + The text parameter can be a (sequence of) string (assuming UTF-8 encoding). Insertion will start at :ref:`Point` (50, 72), which is one inch below top of page and 50 points from the left. The number of inserted text lines is returned. + + **API reference** + + - :meth:`Document.insert_page` + + + +---------- + + + +.. _The_Basics_Spliting_Single_Pages: + +Splitting Single Pages +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This deals with splitting up pages of a :title:`PDF` in arbitrary pieces. For example, you may have a :title:`PDF` with *Letter* format pages which you want to print with a magnification factor of four: each page is split up in 4 pieces which each going to a separate :title:`PDF` page in *Letter* format again. + + + +.. raw:: html + +
+    
+        import fitz
+
+        src = fitz.open("test.pdf")
+        doc = fitz.open()  # empty output PDF
+
+        for spage in src:  # for each page in input
+            r = spage.rect  # input page rectangle
+            d = fitz.Rect(spage.cropbox_position,  # CropBox displacement if not
+                          spage.cropbox_position)  # starting at (0, 0)
+            #--------------------------------------------------------------------------
+            # example: cut input page into 2 x 2 parts
+            #--------------------------------------------------------------------------
+            r1 = r / 2  # top left rect
+            r2 = r1 + (r1.width, 0, r1.width, 0)  # top right rect
+            r3 = r1 + (0, r1.height, 0, r1.height)  # bottom left rect
+            r4 = fitz.Rect(r1.br, r.br)  # bottom right rect
+            rect_list = [r1, r2, r3, r4]  # put them in a list
+
+            for rx in rect_list:  # run thru rect list
+                rx += d  # add the CropBox displacement
+                page = doc.new_page(-1,  # new output page with rx dimensions
+                                   width = rx.width,
+                                   height = rx.height)
+                page.show_pdf_page(
+                        page.rect,  # fill all new page with the image
+                        src,  # input document
+                        spage.number,  # input page number
+                        clip = rx,  # which part to use of input page
+                    )
+
+        # that's it, save output file
+        doc.save("poster-" + src.name,
+                 garbage=3,  # eliminate duplicate objects
+                 deflate=True,  # compress stuff where possible
+        )
+    
+  
+ + +Example: + +.. image:: images/img-posterize.png + +.. note:: + + **API reference** + + - :meth:`Page.cropbox_position` + - :meth:`Page.show_pdf_page` + + +-------------------------- + + +.. _The_Basics_Combining_Single_Pages: + + +Combining Single Pages +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This deals with joining :title:`PDF` pages to form a new :title:`PDF` with pages each combining two or four original ones (also called "2-up", "4-up", etc.). This could be used to create booklets or thumbnail-like overviews. + + +.. raw:: html + +
+    
+        import fitz
+
+        src = fitz.open("test.pdf")
+        doc = fitz.open()  # empty output PDF
+
+        width, height = fitz.paper_size("a4")  # A4 portrait output page format
+        r = fitz.Rect(0, 0, width, height)
+
+        # define the 4 rectangles per page
+        r1 = r / 2  # top left rect
+        r2 = r1 + (r1.width, 0, r1.width, 0)  # top right
+        r3 = r1 + (0, r1.height, 0, r1.height)  # bottom left
+        r4 = fitz.Rect(r1.br, r.br)  # bottom right
+
+        # put them in a list
+        r_tab = [r1, r2, r3, r4]
+
+        # now copy input pages to output
+        for spage in src:
+            if spage.number % 4 == 0:  # create new output page
+                page = doc.new_page(-1,
+                              width = width,
+                              height = height)
+            # insert input page into the correct rectangle
+            page.show_pdf_page(r_tab[spage.number % 4],  # select output rect
+                             src,  # input document
+                             spage.number)  # input page number
+
+        # by all means, save new file using garbage collection and compression
+        doc.save("4up.pdf", garbage=3, deflate=True)
+    
+  
+ +Example: + +.. image:: images/img-4up.png + + +.. note:: + + **API reference** + + - :meth:`Page.cropbox_position` + - :meth:`Page.show_pdf_page` + +-------------------------- + + +.. _The_Basics_Encryption_and_Decryption: + + +:title:`PDF` Encryption & Decryption +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + + +Starting with version 1.16.0, :title:`PDF` decryption and encryption (using passwords) are fully supported. You can do the following: + +* Check whether a document is password protected / (still) encrypted (:attr:`Document.needs_pass`, :attr:`Document.is_encrypted`). +* Gain access authorization to a document (:meth:`Document.authenticate`). +* Set encryption details for PDF files using :meth:`Document.save` or :meth:`Document.write` and + + - decrypt or encrypt the content + - set password(s) + - set the encryption method + - set permission details + +.. note:: A PDF document may have two different passwords: + + * The **owner password** provides full access rights, including changing passwords, encryption method, or permission detail. + * The **user password** provides access to document content according to the established permission details. If present, opening the :title:`PDF` in a viewer will require providing it. + + Method :meth:`Document.authenticate` will automatically establish access rights according to the password used. + +The following snippet creates a new :title:`PDF` and encrypts it with separate user and owner passwords. Permissions are granted to print, copy and annotate, but no changes are allowed to someone authenticating with the user password. + + +.. raw:: html + +
+    
+        import fitz
+
+        text = "some secret information" # keep this data secret
+        perm = int(
+            fitz.PDF_PERM_ACCESSIBILITY # always use this
+            | fitz.PDF_PERM_PRINT # permit printing
+            | fitz.PDF_PERM_COPY # permit copying
+            | fitz.PDF_PERM_ANNOTATE # permit annotations
+        )
+        owner_pass = "owner" # owner password
+        user_pass = "user" # user password
+        encrypt_meth = fitz.PDF_ENCRYPT_AES_256 # strongest algorithm
+        doc = fitz.open() # empty pdf
+        page = doc.new_page() # empty page
+        page.insert_text((50, 72), text) # insert the data
+        doc.save(
+            "secret.pdf",
+            encryption=encrypt_meth, # set the encryption method
+            owner_pw=owner_pass, # set the owner password
+            user_pw=user_pass, # set the user password
+            permissions=perm, # set permissions
+        )
+    
+  
+ + +.. note:: + + **Taking it further** + + Opening this document with some viewer (Nitro Reader 5) reflects these settings: + + .. image:: images/img-encrypting.* + + **Decrypting** will automatically happen on save as before when no encryption parameters are provided. + + To **keep the encryption method** of a PDF save it using `encryption=fitz.PDF_ENCRYPT_KEEP`. If `doc.can_save_incrementally() == True`, an incremental save is also possible. + + To **change the encryption method** specify the full range of options above (`encryption`, `owner_pw`, `user_pw`, `permissions`). An incremental save is **not possible** in this case. + + **API reference** + + - :meth:`Document.save` + + + + + + +.. include:: footer.rst diff --git a/docs/tools.rst b/docs/tools.rst new file mode 100644 index 0000000..0d6f723 --- /dev/null +++ b/docs/tools.rst @@ -0,0 +1,272 @@ +.. include:: header.rst + +.. _Tools: + +Tools +================ + +This class is a collection of utility methods and attributes, mainly around memory management. To simplify and speed up its use, it is automatically instantiated under the name *TOOLS* when PyMuPDF is imported. + +====================================== ================================================= +**Method / Attribute** **Description** +====================================== ================================================= +:meth:`Tools.gen_id` generate a unique identifyer +:meth:`Tools.store_shrink` shrink the storables cache [#f1]_ +:meth:`Tools.mupdf_warnings` return the accumulated MuPDF warnings +:meth:`Tools.mupdf_display_errors` return the accumulated MuPDF warnings +:meth:`Tools.reset_mupdf_warnings` empty MuPDF messages on STDOUT +:meth:`Tools.set_aa_level` set the anti-aliasing values +:meth:`Tools.set_annot_stem` set the prefix of new annotation / link ids +:meth:`Tools.set_small_glyph_heights` search and extract using small bbox heights +:meth:`Tools.set_subset_fontnames` control suppression of subset fontname tags +:meth:`Tools.show_aa_level` return the anti-aliasing values +:meth:`Tools.unset_quad_corrections` disable PyMuPDF-specific code +:attr:`Tools.fitz_config` configuration settings of PyMuPDF +:attr:`Tools.store_maxsize` maximum storables cache size +:attr:`Tools.store_size` current storables cache size +====================================== ================================================= + +**Class API** + +.. class:: Tools + + .. method:: gen_id() + + A convenience method returning a unique positive integer which will increase by 1 on every invocation. Example usages include creating unique keys in databases - its creation should be faster than using timestamps by an order of magnitude. + + .. note:: MuPDF has dropped support for this in v1.14.0, so we have re-implemented a similar function with the following differences: + + * It is not part of MuPDF's global context and not threadsafe (not an issue because we do not support threads in PyMuPDF anyway). + * It is implemented as *int*. This means that the maximum number is *sys.maxsize*. Should this number ever be exceeded, the counter starts over again at 1. + + :rtype: int + :returns: a unique positive integer. + + + .. method:: set_annot_stem(stem=None) + + * New in v1.18.6 + + Set or inquire the prefix for the id of new annotations, fields or links. + + :arg str stem: if omitted, the current value is returned, default is "fitz". Annotations, fields / widgets and links technically are subtypes of the same type of object (`/Annot`) in PDF documents. An `/Annot` object may be given a unique identifier within a page. For each of the applicable subtypes, PyMuPDF generates identifiers "stem-Annn", "stem-Wnnn" or "stem-Lnnn" respectively. The number "nnn" is used to enforce the required uniqueness. + + :rtype: str + :returns: the current value. + + + .. method:: set_small_glyph_heights(on=None) + + * New in v1.18.5 + + Set or inquire reduced bbox heights in text extract and text search methods. + + :arg bool on: if omitted or `None`, the current setting is returned. For other values the *bool()* function is applied to set a global variable. If `True`, :meth:`Page.search_for` and :meth:`Page.get_text` methods return character, span, line or block bboxes that have a height of *font size*. If `False` (standard setting when PyMuPDF is imported), bbox height will be based on font properties and normally equal *line height*. + + :rtype: bool + :returns: *True* or *False*. + + .. note:: Text extraction options "xml", "xhtml" and "html", which directly wrap MuPDF code, are not influenced by this. + + .. method:: set_subset_fontnames(on=None) + + * New in v1.18.9 + + Control suppression of subset fontname tags in text extractions. + + :arg bool on: if omitted / `None`, the current setting is returned. Arguments evaluating to `True` or `False` set a global variable. If `True`, options "dict", "json", "rawdict" and "rawjson" will return e.g. `"NOHSJV+Calibri-Light"`, otherwise only `"Calibri-Light"` (the default). The setting remains in effect until changed again. + + :rtype: bool + :returns: *True* or *False*. + + .. note:: Except mentioned above, no other text extraction variants are influenced by this. This is especially true for the options "xml", "xhtml" and "html", which are based on MuPDF code. They extract the font name `"Calibri-Light"`, or even just the **family** name -- `Calibri` in this example. + + + .. method:: unset_quad_corrections(on=None) + + * New in v1.18.10 + + Enable / disable PyMuPDF-specific code, that tries to rebuild valid character quads when encountering nonsense in :meth:`Page.get_text` text extractions. This code depends on certain font properties (ascender and descender), which do not exist in rare situations and cause segmentation faults when trying to access them. This method sets a global parameter in PyMuPDF, which suppresses execution of this code. + + :arg bool on: if omitted or `None`, the current setting is returned. For other values the *bool()* function is applied to set a global variable. If `True`, PyMuPDF will not try to access the resp. font properties and use values `ascender=0.8` and `descender=-0.2` instead. + + :rtype: bool + :returns: *True* or *False*. + + + .. method:: store_shrink(percent) + + Reduce the storables cache by a percentage of its current size. + + :arg int percent: the percentage of current size to free. If 100+ the store will be emptied, if zero, nothing will happen. MuPDF's caching strategy is "least recently used", so low-usage elements get deleted first. + + :rtype: int + :returns: the new current store size. Depending on the situation, the size reduction may be larger than the requested percentage. + + .. method:: show_aa_level() + + * New in version 1.16.14 + + Return the current anti-aliasing values. These values control the rendering quality of graphics and text elements. + + :rtype: dict + :returns: A dictionary with the following initial content: `{'graphics': 8, 'text': 8, 'graphics_min_line_width': 0.0}`. + + + .. method:: set_aa_level(level) + + * New in version 1.16.14 + + Set the new number of bits to use for anti-aliasing. The same value is taken currently for graphics and text rendering. This might change in a future MuPDF release. + + :arg int level: an integer ranging between 0 and 8. Value outside this range will be silently changed to valid values. The value will remain in effect throughout the current session or until changed again. + + + .. method:: reset_mupdf_warnings() + + * New in version 1.16.0 + + Empty MuPDF warnings message buffer. + + + .. method:: mupdf_display_errors(value=None) + + * New in version 1.16.8 + + Show or set whether MuPDF errors should be displayed. + + :arg bool value: if not a bool, the current setting is returned. If true, MuPDF errors will be shown on *sys.stderr*, otherwise suppressed. In any case, messages continue to be stored in the warnings store. Upon import of PyMuPDF this value is *True*. + + :returns: *True* or *False* + + + .. method:: mupdf_warnings(reset=True) + + * New in version 1.16.0 + + Return all stored MuPDF messages as a string with interspersed line-breaks. + + :arg bool reset: *(new in version 1.16.7)* whether to automatically empty the store. + + + .. attribute:: fitz_config + + A dictionary containing the actual values used for configuring PyMuPDF and MuPDF. Also refer to the installation chapter. This is an overview of the keys, each of which describes the status of a support aspect. + + ================= =================================================== + **Key** **Support included for ...** + ================= =================================================== + plotter-g Gray colorspace rendering + plotter-rgb RGB colorspace rendering + plotter-cmyk CMYK colorspcae rendering + plotter-n overprint rendering + pdf PDF documents + xps XPS documents + svg SVG documents + cbz CBZ documents + img IMG documents + html HTML documents + epub EPUB documents + jpx JPEG2000 images + js JavaScript + tofu all TOFU fonts + tofu-cjk CJK font subset (China, Japan, Korea) + tofu-cjk-ext CJK font extensions + tofu-cjk-lang CJK font language extensions + tofu-emoji TOFU emoji fonts + tofu-historic TOFU historic fonts + tofu-symbol TOFU symbol fonts + tofu-sil TOFU SIL fonts + icc ICC profiles + py-memory using Python memory management [#f2]_ + base14 Base-14 fonts (should always be true) + ================= =================================================== + + For an explanation of the term "TOFU" see `this Wikipedia article `_.:: + + In [1]: import fitz + In [2]: TOOLS.fitz_config + Out[2]: + {'plotter-g': True, + 'plotter-rgb': True, + 'plotter-cmyk': True, + 'plotter-n': True, + 'pdf': True, + 'xps': True, + 'svg': True, + 'cbz': True, + 'img': True, + 'html': True, + 'epub': True, + 'jpx': True, + 'js': True, + 'tofu': False, + 'tofu-cjk': True, + 'tofu-cjk-ext': False, + 'tofu-cjk-lang': False, + 'tofu-emoji': False, + 'tofu-historic': False, + 'tofu-symbol': False, + 'tofu-sil': False, + 'icc': True, + 'py-memory': False, + 'base14': True} + + :rtype: dict + + .. attribute:: store_maxsize + + Maximum storables cache size in bytes. PyMuPDF is generated with a value of 268'435'456 (256 MB, the default value), which you should therefore always see here. If this value is zero, then an "unlimited" growth is permitted. + + :rtype: int + + .. attribute:: store_size + + Current storables cache size in bytes. This value may change (and will usually increase) with every use of a PyMuPDF function. It will (automatically) decrease only when :attr:`Tools.store_maxize` is going to be exceeded: in this case, MuPDF will evict low-usage objects until the value is again in range. + + :rtype: int + +Example Session +---------------- + +.. highlight:: python + +:: + >>> import fitz + # print the maximum and current cache sizes + >>> fitz.TOOLS.store_maxsize + 268435456 + >>> fitz.TOOLS.store_size + 0 + >>> doc = fitz.open("demo1.pdf") + # pixmap creation puts lots of object in cache (text, images, fonts), + # apart from the pixmap itself + >>> pix = doc[0].get_pixmap(alpha=False) + >>> fitz.TOOLS.store_size + 454519 + # release (at least) 50% of the storage + >>> fitz.TOOLS.store_shrink(50) + 13471 + >>> fitz.TOOLS.store_size + 13471 + # get a few unique numbers + >>> fitz.TOOLS.gen_id() + 1 + >>> fitz.TOOLS.gen_id() + 2 + >>> fitz.TOOLS.gen_id() + 3 + # close document and see how much cache is still in use + >>> doc.close() + >>> fitz.TOOLS.store_size + 0 + >>> + + +.. rubric:: Footnotes + +.. [#f1] This memory area is internally used by MuPDF, and it serves as a cache for objects that have already been read and interpreted, thus improving performance. The most bulky object types are images and also fonts. When an application starts up the MuPDF library (in our case this happens as part of *import fitz*), it must specify a maximum size for this area. PyMuPDF's uses the default value (256 MB) to limit memory consumption. Use the methods here to control or investigate store usage. For example: even after a document has been closed and all related objects have been deleted, the store usage may still not drop down to zero. So you might want to enforce that before opening another document. + +.. [#f2] By default PyMuPDF and MuPDF use `malloc()`/`free()` for dynamic memory management. One can instead force them to use the Python allocation functions `PyMem_New()`/`PyMem_Del()`, by modifying *fitz/fitz.i* to do `#define JM_MEMORY 1` and rebuilding PyMuPDF. + +.. include:: footer.rst diff --git a/docs/tutorial.rst b/docs/tutorial.rst new file mode 100644 index 0000000..64da92c --- /dev/null +++ b/docs/tutorial.rst @@ -0,0 +1,449 @@ +.. include:: header.rst + +.. _Tutorial: + +========= +Tutorial +========= + +.. highlight:: python + +This tutorial will show you the use of :title:`PyMuPDF`, :title:`MuPDF` in :title:`Python`, step by step. + +Because :title:`MuPDF` supports not only PDF, but also XPS, OpenXPS, CBZ, CBR, FB2 and EPUB formats, so does PyMuPDF [#f1]_. Nevertheless, for the sake of brevity we will only talk about PDF files. At places where indeed only PDF files are supported, this will be mentioned explicitly. + +Importing the Bindings +========================== +The Python bindings to MuPDF are made available by this import statement. We also show here how your version can be checked:: + + >>> import fitz + >>> print(fitz.__doc__) + PyMuPDF 1.16.0: Python bindings for the MuPDF 1.16.0 library. + Version date: 2019-07-28 07:30:14. + Built for Python 3.7 on win32 (64-bit). + + +Note on the Name *fitz* +-------------------------- +The top level Python import name for this library is **"fitz"**. This has historical reasons: + +The original rendering library for MuPDF was called *Libart*. + +*"After Artifex Software acquired the MuPDF project, the development focus shifted on writing a new modern graphics library called "Fitz". Fitz was originally intended as an R&D project to replace the aging Ghostscript graphics library, but has instead become the rendering engine powering MuPDF."* (Quoted from `Wikipedia `_). + + +.. note:: + + So :title:`PyMuPDF` **cannot coexist** with packages named "fitz" in the same Python environment. + + +.. _Tutorial_Opening_a_Document: + +Opening a Document +====================== + +To access a :ref:`supported document`, it must be opened with the following statement:: + + doc = fitz.open(filename) # or fitz.Document(filename) + +This creates the :ref:`Document` object *doc*. *filename* must be a Python string (or a `pathlib.Path`) specifying the name of an existing file. + +It is also possible to open a document from memory data, or to create a new, empty PDF. See :ref:`Document` for details. You can also use :ref:`Document` as a *context manager*. + +A document contains many attributes and functions. Among them are meta information (like "author" or "subject"), number of total pages, outline and encryption information. + +Some :ref:`Document` Methods and Attributes +============================================= + +=========================== ========================================== +**Method / Attribute** **Description** +=========================== ========================================== +:attr:`Document.page_count` the number of pages (*int*) +:attr:`Document.metadata` the metadata (*dict*) +:meth:`Document.get_toc` get the table of contents (*list*) +:meth:`Document.load_page` read a :ref:`Page` +=========================== ========================================== + +Accessing Meta Data +======================== +PyMuPDF fully supports standard metadata. :attr:`Document.metadata` is a Python dictionary with the following keys. It is available for **all document types**, though not all entries may always contain data. For details of their meanings and formats consult the respective manuals, e.g. :ref:`AdobeManual` for PDF. Further information can also be found in chapter :ref:`Document`. The meta data fields are strings or *None* if not otherwise indicated. Also be aware that not all of them always contain meaningful data -- even if they are not *None*. + +============== ================================= +Key Value +============== ================================= +producer producer (producing software) +format format: 'PDF-1.4', 'EPUB', etc. +encryption encryption method used if any +author author +modDate date of last modification +keywords keywords +title title +creationDate date of creation +creator creating application +subject subject +============== ================================= + +.. note:: Apart from these standard metadata, **PDF documents** starting from PDF version 1.4 may also contain so-called *"metadata streams"* (see also :data:`stream`). Information in such streams is coded in XML. PyMuPDF deliberately contains no XML components for this purpose (the :ref:`PyMuPDF Xml class` is a helper class intended to access the DOM content of a :ref:`Story` object), so we do not directly support access to information contained therein. But you can extract the stream as a whole, inspect or modify it using a package like `lxml`_ and then store the result back into the PDF. If you want, you can also delete this data altogether. + +.. note:: There are two utility scripts in the repository that `metadata import (PDF only)`_ resp. `metadata export`_ metadata from resp. to CSV files. + +Working with Outlines +========================= +The easiest way to get all outlines (also called "bookmarks") of a document, is by loading its *table of contents*:: + + toc = doc.get_toc() + +This will return a Python list of lists *[[lvl, title, page, ...], ...]* which looks much like a conventional table of contents found in books. + +*lvl* is the hierarchy level of the entry (starting from 1), *title* is the entry's title, and *page* the page number (1-based!). Other parameters describe details of the bookmark target. + +.. note:: There are two utility scripts in the repository that `toc import (PDF only)`_ resp. `toc export`_ table of contents from resp. to CSV files. + +Working with Pages +====================== +:ref:`Page` handling is at the core of MuPDF's functionality. + +* You can render a page into a raster or vector (SVG) image, optionally zooming, rotating, shifting or shearing it. +* You can extract a page's text and images in many formats and search for text strings. +* For PDF documents many more methods are available to add text or images to pages. + +First, a :ref:`Page` must be created. This is a method of :ref:`Document`:: + + page = doc.load_page(pno) # loads page number 'pno' of the document (0-based) + page = doc[pno] # the short form + +Any integer `-∞ < pno < page_count` is possible here. Negative numbers count backwards from the end, so *doc[-1]* is the last page, like with Python sequences. + +Some more advanced way would be using the document as an **iterator** over its pages:: + + for page in doc: + # do something with 'page' + + # ... or read backwards + for page in reversed(doc): + # do something with 'page' + + # ... or even use 'slicing' + for page in doc.pages(start, stop, step): + # do something with 'page' + + +Once you have your page, here is what you would typically do with it: + +Inspecting the Links, Annotations or Form Fields of a Page +----------------------------------------------------------- +Links are shown as "hot areas" when a document is displayed with some viewer software. If you click while your cursor shows a hand symbol, you will usually be taken to the target that is encoded in that hot area. Here is how to get all links:: + + # get all links on a page + links = page.get_links() + +*links* is a Python list of dictionaries. For details see :meth:`Page.get_links`. + +You can also use an iterator which emits one link at a time:: + + for link in page.links(): + # do something with 'link' + +If dealing with a PDF document page, there may also exist annotations (:ref:`Annot`) or form fields (:ref:`Widget`), each of which have their own iterators:: + + for annot in page.annots(): + # do something with 'annot' + + for field in page.widgets(): + # do something with 'field' + + +Rendering a Page +----------------------- +This example creates a **raster** image of a page's content:: + + pix = page.get_pixmap() + +*pix* is a :ref:`Pixmap` object which (in this case) contains an **RGB** image of the page, ready to be used for many purposes. Method :meth:`Page.get_pixmap` offers lots of variations for controlling the image: resolution / DPI, colorspace (e.g. to produce a grayscale image or an image with a subtractive color scheme), transparency, rotation, mirroring, shifting, shearing, etc. For example: to create an **RGBA** image (i.e. containing an alpha channel), specify *pix = page.get_pixmap(alpha=True)*. + +A :ref:`Pixmap` contains a number of methods and attributes which are referenced below. Among them are the integers *width*, *height* (each in pixels) and *stride* (number of bytes of one horizontal image line). Attribute *samples* represents a rectangular area of bytes representing the image data (a Python *bytes* object). + +.. note:: You can also create a **vector** image of a page by using :meth:`Page.get_svg_image`. Refer to this `Vector Image Support page`_ for details. + +Saving the Page Image in a File +----------------------------------- +We can simply store the image in a PNG file:: + + pix.save("page-%i.png" % page.number) + +Displaying the Image in GUIs +------------------------------------------- +We can also use it in GUI dialog managers. :attr:`Pixmap.samples` represents an area of bytes of all the pixels as a Python bytes object. Here are some examples, find more in the `examples`_ directory. + +wxPython +~~~~~~~~~~~~~ +Consult their documentation for adjustments to RGB(A) pixmaps and, potentially, specifics for your wxPython release:: + + if pix.alpha: + bitmap = wx.Bitmap.FromBufferRGBA(pix.width, pix.height, pix.samples) + else: + bitmap = wx.Bitmap.FromBuffer(pix.width, pix.height, pix.samples) + +Tkinter +~~~~~~~~~~ +Please also see section 3.19 of the `Pillow documentation`_:: + + from PIL import Image, ImageTk + + # set the mode depending on alpha + mode = "RGBA" if pix.alpha else "RGB" + img = Image.frombytes(mode, [pix.width, pix.height], pix.samples) + tkimg = ImageTk.PhotoImage(img) + +The following **avoids using Pillow**:: + + # remove alpha if present + pix1 = fitz.Pixmap(pix, 0) if pix.alpha else pix # PPM does not support transparency + imgdata = pix1.tobytes("ppm") # extremely fast! + tkimg = tkinter.PhotoImage(data = imgdata) + +If you are looking for a complete Tkinter script paging through **any supported** document, `here it is!`_. It can also zoom into pages, and it runs under Python 2 or 3. It requires the extremely handy `PySimpleGUI`_ pure Python package. + +PyQt4, PyQt5, PySide +~~~~~~~~~~~~~~~~~~~~~ +Please also see section 3.16 of the `Pillow documentation`_:: + + from PIL import Image, ImageQt + + # set the mode depending on alpha + mode = "RGBA" if pix.alpha else "RGB" + img = Image.frombytes(mode, [pix.width, pix.height], pix.samples) + qtimg = ImageQt.ImageQt(img) + +Again, you also can get along **without using Pillow.** Qt's `QImage` luckily supports native Python pointers, so the following is the recommended way to create Qt images:: + + from PyQt5.QtGui import QImage + + # set the correct QImage format depending on alpha + fmt = QImage.Format_RGBA8888 if pix.alpha else QImage.Format_RGB888 + qtimg = QImage(pix.samples_ptr, pix.width, pix.height, fmt) + + +Extracting Text and Images +--------------------------- +We can also extract all text, images and other information of a page in many different forms, and levels of detail:: + + text = page.get_text(opt) + +Use one of the following strings for *opt* to obtain different formats [#f2]_: + +* **"text"**: (default) plain text with line breaks. No formatting, no text position details, no images. + +* **"blocks"**: generate a list of text blocks (= paragraphs). + +* **"words"**: generate a list of words (strings not containing spaces). + +* **"html"**: creates a full visual version of the page including any images. This can be displayed with your internet browser. + +* **"dict"** / **"json"**: same information level as HTML, but provided as a Python dictionary or resp. JSON string. See :meth:`TextPage.extractDICT` for details of its structure. + +* **"rawdict"** / **"rawjson"**: a super-set of **"dict"** / **"json"**. It additionally provides character detail information like XML. See :meth:`TextPage.extractRAWDICT` for details of its structure. + +* **"xhtml"**: text information level as the TEXT version but includes images. Can also be displayed by internet browsers. + +* **"xml"**: contains no images, but full position and font information down to each single text character. Use an XML module to interpret. + +To give you an idea about the output of these alternatives, we did text example extracts. See :ref:`Appendix2`. + +Searching for Text +------------------- +You can find out, exactly where on a page a certain text string appears:: + + areas = page.search_for("mupdf") + +This delivers a list of rectangles (see :ref:`Rect`), each of which surrounds one occurrence of the string "mupdf" (case insensitive). You could use this information to e.g. highlight those areas (PDF only) or create a cross reference of the document. + +Please also do have a look at chapter :ref:`cooperation` and at demo programs `demo.py`_ and `demo-lowlevel.py`_. Among other things they contain details on how the :ref:`TextPage`, :ref:`Device` and :ref:`DisplayList` classes can be used for a more direct control, e.g. when performance considerations suggest it. + + + +.. _WorkingWithStories: + +Stories: Generating PDF from HTML Source +========================================= + +The :ref:`Story` class is a new feature of PyMuPDF version 1.21.0. It represents support for MuPDF's **"story"** interface. + +The following is a quote from the book `"MuPDF Explored"`_ by Robin Watts from `Artifex`_: + +----- + +*Stories provide a way to easily layout styled content for use with devices, such as those offered by Document Writers (...). The concept of a story comes from desktop publishing, which in turn (...) gets it from newspapers. If you consider a traditional newspaper layout, it will consist of various news articles (stories) that are laid out into multiple columns, possibly across multiple pages.* + +*Accordingly, MuPDF uses a story to represent a flow of text with styling information. The user of the story can then supply a sequence of rectangles into which the story will be laid out, and the positioned text can then be drawn to an output device. This keeps the concept of the text itself (the story) to be separated from the areas into which the text should be flowed (the layout).* + +----- + +.. note:: A Story works somewhat similar to an internet browser: It faithfully parses and renders HTML hypertext and also optional stylesheets (CSS). But its **output is a PDF** -- not web pages. + + +When creating a :ref:`Story`, the input from up to three different information sources is taken into account. All these items are optional. + +1. HTML source code, either a Python string or **created by the script** using methods of :ref:`Xml`. + +2. CSS (Cascaded Style Sheet) source code, provided as a Python string. CSS can be used to provide styling information (text font size, color, etc.) like it would happen for web pages. Obviously, this string may also be read from a file. + +3. An :ref:`Archive` **must be used** whenever the DOM references images, or uses text fonts except the standard :ref:`Base-14-Fonts`, CJK fonts and the NOTO fonts generated into the PyMuPDF binary. + + +The :ref:`API` allows creating DOMs completely from scratch, including desired styling information. It can also be used to modify or extend **provided** HTML: text can be deleted or replaced, or its styling can be changed. Text -- for example extracted from databases -- can also be added and fill template-like HTML documents. + +It is **not required** to provide syntactically complete HTML documents: snippets like `Hello World!` are fully accepted, and many / most syntax errors are automatically corrected. + +After the HTML is considered complete, it can be used to create a PDF document. This happens via the new :ref:`DocumentWriter` class. The programmer calls its methods to create a new empty page, and passes rectangles to the Story to fill them. + +The story in turn will return completion codes indicating whether or not more content is waiting to be written. Which part of the content will land in which rectangle or on which page is automatically determined by the story itself -- it cannot be influenced other than by providing the rectangles. + +Please see the :ref:`Stories recipes` for a number of typical use cases. + + +PDF Maintenance +================== +PDFs are the only document type that can be **modified** using PyMuPDF. Other file types are read-only. + +However, you can convert **any document** (including images) to a PDF and then apply all PyMuPDF features to the conversion result. Find out more here :meth:`Document.convert_to_pdf`, and also look at the demo script `pdf-converter.py`_ which can convert any :ref:`supported document` to PDF. + +:meth:`Document.save()` always stores a PDF in its current (potentially modified) state on disk. + +You normally can choose whether to save to a new file, or just append your modifications to the existing one ("incremental save"), which often is very much faster. + +The following describes ways how you can manipulate PDF documents. This description is by no means complete: much more can be found in the following chapters. + +Modifying, Creating, Re-arranging and Deleting Pages +------------------------------------------------------- +There are several ways to manipulate the so-called **page tree** (a structure describing all the pages) of a PDF: + +:meth:`Document.delete_page` and :meth:`Document.delete_pages` delete pages. + +:meth:`Document.copy_page`, :meth:`Document.fullcopy_page` and :meth:`Document.move_page` copy or move a page to other locations within the same document. + +:meth:`Document.select` shrinks a PDF down to selected pages. Parameter is a sequence [#f3]_ of the page numbers that you want to keep. These integers must all be in range *0 <= i < page_count*. When executed, all pages **missing** in this list will be deleted. Remaining pages will occur **in the sequence and as many times (!) as you specify them**. + +So you can easily create new PDFs with + +* the first or last 10 pages, +* only the odd or only the even pages (for doing double-sided printing), +* pages that **do** or **don't** contain a given text, +* reverse the page sequence, ... + +... whatever you can think of. + +The saved new document will contain links, annotations and bookmarks that are still valid (i.a.w. either pointing to a selected page or to some external resource). + +:meth:`Document.insert_page` and :meth:`Document.new_page` insert new pages. + +Pages themselves can moreover be modified by a range of methods (e.g. page rotation, annotation and link maintenance, text and image insertion). + +Joining and Splitting PDF Documents +------------------------------------ + +Method :meth:`Document.insert_pdf` copies pages **between different** PDF documents. Here is a simple **joiner** example (*doc1* and *doc2* being opened PDFs):: + + # append complete doc2 to the end of doc1 + doc1.insert_pdf(doc2) + +Here is a snippet that **splits** *doc1*. It creates a new document of its first and its last 10 pages:: + + doc2 = fitz.open() # new empty PDF + doc2.insert_pdf(doc1, to_page = 9) # first 10 pages + doc2.insert_pdf(doc1, from_page = len(doc1) - 10) # last 10 pages + doc2.save("first-and-last-10.pdf") + +More can be found in the :ref:`Document` chapter. Also have a look at `PDFjoiner.py`_. + +Embedding Data +--------------- + +PDFs can be used as containers for arbitrary data (executables, other PDFs, text or binary files, etc.) much like ZIP archives. + +PyMuPDF fully supports this feature via :ref:`Document` *embfile_** methods and attributes. For some detail read :ref:`Appendix 3`, consult the Wiki on `dealing with embedding files`_, or the example scripts `embedded-copy.py`_, `embedded-export.py`_, `embedded-import.py`_, and `embedded-list.py`_. + + +Saving +------- + +As mentioned above, :meth:`Document.save` will **always** save the document in its current state. + +You can write changes back to the **original PDF** by specifying option *incremental=True*. This process is (usually) **extremely fast**, since changes are **appended to the original file** without completely rewriting it. + +:meth:`Document.save` options correspond to options of MuPDF's command line utility *mutool clean*, see the following table. + +=================== =========== ================================================== +**Save Option** **mutool** **Effect** +=================== =========== ================================================== +garbage=1 g garbage collect unused objects +garbage=2 gg in addition to 1, compact :data:`xref` tables +garbage=3 ggg in addition to 2, merge duplicate objects +garbage=4 gggg in addition to 3, merge duplicate stream content +clean=True cs clean and sanitize content streams +deflate=True z deflate uncompressed streams +deflate_images=True i deflate image streams +deflate_fonts=True f deflate fontfile streams +ascii=True a convert binary data to ASCII format +linear=True l create a linearized version +expand=True d decompress all streams +=================== =========== ================================================== + +.. note:: For an explanation of terms like *object, stream, xref* consult the :ref:`Glossary` chapter. + +For example, *mutool clean -ggggz file.pdf* yields excellent compression results. It corresponds to *doc.save(filename, garbage=4, deflate=True)*. + +Closing +========= +It is often desirable to "close" a document to relinquish control of the underlying file to the OS, while your program continues. + +This can be achieved by the :meth:`Document.close` method. Apart from closing the underlying file, buffer areas associated with the document will be freed. + +Further Reading +================ +Also have a look at PyMuPDF's `Wiki`_ pages. Especially those named in the sidebar under title **"Recipes"** cover over 15 topics written in "How-To" style. + +This document also contains a :ref:`FAQ`. This chapter has close connection to the aforementioned recipes, and it will be extended with more content over time. + + +----- + + +.. rubric:: Footnotes + +.. [#f1] PyMuPDF lets you also open several image file types just like normal documents. See section :ref:`ImageFiles` in chapter :ref:`Pixmap` for more comments. + +.. [#f2] :meth:`Page.get_text` is a convenience wrapper for several methods of another PyMuPDF class, :ref:`TextPage`. The names of these methods correspond to the argument string passed to :meth:`Page.get_text` \: *Page.get_text("dict")* is equivalent to *TextPage.extractDICT()* \. + +.. [#f3] "Sequences" are Python objects conforming to the sequence protocol. These objects implement a method named *__getitem__()*. Best known examples are Python tuples and lists. But *array.array*, *numpy.array* and PyMuPDF's "geometry" objects (:ref:`Algebra`) are sequences, too. Refer to :ref:`SequenceTypes` for details. + + +.. include:: footer.rst + +.. External links: + + +.. _lxml: https://pypi.org/project/lxml/ +.. _metadata import (PDF only): https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/import-metadata/import.py +.. _metadata export: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/export-metadata/export.py +.. _toc import (PDF only): https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/import-toc/import.py +.. _toc export: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/export-toc/export.py +.. _Vector Image Support page: https://github.com/pymupdf/PyMuPDF/wiki/Vector-Image-Support +.. _examples: https://github.com/pymupdf/PyMuPDF/tree/master/examples +.. _Pillow documentation: https://Pillow.readthedocs.io +.. _here it is!: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/browse-document/browse.py +.. _PySimpleGUI: https://pypi.org/project/PySimpleGUI/ +.. _demo.py: https://github.com/pymupdf/PyMuPDF-Utilities/tree/master/demo/demo.py +.. _demo-lowlevel.py: https://github.com/pymupdf/PyMuPDF-Utilities/tree/master/demo/demo-lowlevel.py +.. _"MuPDF Explored": https://mupdf.com/docs/mupdf-explored.html +.. _Artifex: https://www.artifex.com +.. _pdf-converter.py: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/convert-document/convert.py +.. _PDFjoiner.py: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/join-documents/join.py +.. _dealing with embedding files: https://github.com/pymupdf/PyMuPDF/wiki/Dealing-with-Embedded-Files +.. _embedded-copy.py: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/copy-embedded/copy.py +.. _embedded-export.py: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/export-embedded/export.py +.. _embedded-import.py: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/import-embedded/import.py +.. _embedded-list.py: https://github.com/pymupdf/PyMuPDF-Utilities/blob/master/examples/list-embedded/list.py +.. _Wiki: https://github.com/pymupdf/PyMuPDF/wiki + + diff --git a/docs/vars.rst b/docs/vars.rst new file mode 100644 index 0000000..cd86c6c --- /dev/null +++ b/docs/vars.rst @@ -0,0 +1,520 @@ +.. include:: header.rst + +=============================== +Constants and Enumerations +=============================== +Constants and enumerations of :title:`MuPDF` as implemented by :title:`PyMuPDF`. Each of the following variables is accessible as *fitz.variable*. + + +Constants +--------- + +.. py:data:: Base14_Fonts + + Predefined Python list of valid :ref:`Base-14-Fonts`. + + :rtype: list + +.. py:data:: csRGB + + Predefined RGB colorspace *fitz.Colorspace(fitz.CS_RGB)*. + + :rtype: :ref:`Colorspace` + +.. py:data:: csGRAY + + Predefined GRAY colorspace *fitz.Colorspace(fitz.CS_GRAY)*. + + :rtype: :ref:`Colorspace` + +.. py:data:: csCMYK + + Predefined CMYK colorspace *fitz.Colorspace(fitz.CS_CMYK)*. + + :rtype: :ref:`Colorspace` + +.. py:data:: CS_RGB + + 1 -- Type of :ref:`Colorspace` is RGBA + + :rtype: int + +.. py:data:: CS_GRAY + + 2 -- Type of :ref:`Colorspace` is GRAY + + :rtype: int + +.. py:data:: CS_CMYK + + 3 -- Type of :ref:`Colorspace` is CMYK + + :rtype: int + +.. py:data:: VersionBind + + 'x.xx.x' -- version of PyMuPDF (these bindings) + + :rtype: string + +.. py:data:: VersionFitz + + 'x.xxx' -- version of MuPDF + + :rtype: string + +.. py:data:: VersionDate + + ISO timestamp *YYYY-MM-DD HH:MM:SS* when these bindings were built. + + :rtype: string + +.. Note:: The docstring of *fitz* contains information of the above which can be retrieved like so: *print(fitz.__doc__)*, and should look like: *PyMuPDF 1.10.0: Python bindings for the MuPDF 1.10 library, built on 2016-11-30 13:09:13*. + +.. py:data:: version + + (VersionBind, VersionFitz, timestamp) -- combined version information where *timestamp* is the generation point in time formatted as "YYYYMMDDhhmmss". + + :rtype: tuple + + +.. _PermissionCodes: + +Document Permissions +---------------------------- + +====================== ======================================================================= +Code Permitted Action +====================== ======================================================================= +PDF_PERM_PRINT Print the document +PDF_PERM_MODIFY Modify the document's contents +PDF_PERM_COPY Copy or otherwise extract text and graphics +PDF_PERM_ANNOTATE Add or modify text annotations and interactive form fields +PDF_PERM_FORM Fill in forms and sign the document +PDF_PERM_ACCESSIBILITY Obsolete, always permitted +PDF_PERM_ASSEMBLE Insert, rotate, or delete pages, bookmarks, thumbnail images +PDF_PERM_PRINT_HQ High quality printing +====================== ======================================================================= + +.. _OptionalContentCodes: + +PDF Optional Content Codes +---------------------------- + +====================== ======================================================================= +Code Meaning +====================== ======================================================================= +PDF_OC_ON Set an OCG to ON temporarily +PDF_OC_TOGGLE Toggle OCG status temporarily +PDF_OC_OFF Set an OCG to OFF temporarily +====================== ======================================================================= + +.. _EncryptionMethods: + +PDF encryption method codes +---------------------------- + +=================== ==================================================== +Code Meaning +=================== ==================================================== +PDF_ENCRYPT_KEEP do not change +PDF_ENCRYPT_NONE remove any encryption +PDF_ENCRYPT_RC4_40 RC4 40 bit +PDF_ENCRYPT_RC4_128 RC4 128 bit +PDF_ENCRYPT_AES_128 *Advanced Encryption Standard* 128 bit +PDF_ENCRYPT_AES_256 *Advanced Encryption Standard* 256 bit +PDF_ENCRYPT_UNKNOWN unknown +=================== ==================================================== + +.. _FontExtensions: + +Font File Extensions +----------------------- +The table show file extensions you should use when saving fontfile buffers extracted from a PDF. This string is returned by :meth:`Document.get_page_fonts`, :meth:`Page.get_fonts` and :meth:`Document.extract_font`. + +==== ============================================================================ +Ext Description +==== ============================================================================ +ttf TrueType font +pfa Postscript for ASCII font (various subtypes) +cff Type1C font (compressed font equivalent to Type1) +cid character identifier font (postscript format) +otf OpenType font +n/a not extractable, e.g. :ref:`Base-14-Fonts`, Type 3 fonts and others +==== ============================================================================ + +.. _TextAlign: + +Text Alignment +----------------------- +.. py:data:: TEXT_ALIGN_LEFT + + 0 -- align left. + +.. py:data:: TEXT_ALIGN_CENTER + + 1 -- align center. + +.. py:data:: TEXT_ALIGN_RIGHT + + 2 -- align right. + +.. py:data:: TEXT_ALIGN_JUSTIFY + + 3 -- align justify. + +.. _TextPreserve: + +Text Extraction Flags +--------------------- +Option bits controlling the amount of data, that are parsed into a :ref:`TextPage` -- this class is mainly used only internally in PyMuPDF. + +For the PyMuPDF programmer, some combination (using Python's `|` operator, or simply use `+`) of these values are aggregated in the `flags` integer, a parameter of all text search and text extraction methods. Depending on the individual method, different default combinations of the values are used. Please use a value that meets your situation. Especially make sure to switch off image extraction unless you really need them. The impact on performance and memory is significant! + +.. py:data:: TEXT_PRESERVE_LIGATURES + + 1 -- If set, ligatures are passed through to the application in their original form. Otherwise ligatures are expanded into their constituent parts, e.g. the ligature "ffi" is expanded into three eparate characters f, f and i. Default is "on" in PyMuPDF. MuPDF supports the following 7 ligatures: "ff", "fi", "fl", "ffi", "ffl", , "ft", "st". + +.. py:data:: TEXT_PRESERVE_WHITESPACE + + 2 -- If set, whitespace is passed through. Otherwise any type of horizontal whitespace (including horizontal tabs) will be replaced with space characters of variable width. Default is "on" in PyMuPDF. + +.. py:data:: TEXT_PRESERVE_IMAGES + + 4 -- If set, then images will be stored in the :ref:`TextPage`. This causes the presence of (usually large!) binary image content in the output of text extractions of types "blocks", "dict", "json", "rawdict", "rawjson", "html", and "xhtml" and is the default there. If used with "blocks" however, only image metadata will be returned, not the image itself. + +.. py:data:: TEXT_INHIBIT_SPACES + + 8 -- If set, Mupdf will not try to add missing space characters where there are large gaps between characters. In PDF, the creator often does not insert spaces to point to the next character's position, but will provide the direct location address. The default in PyMuPDF is "off" -- so spaces **will be generated**. + +.. py:data:: TEXT_DEHYPHENATE + + 16 -- Ignore hyphens at line ends and join with next line. Used internally with the text search functions. However, it is generally available: if on, text extractions will return joined text lines (or spans) with the ending hyphen of the first line eliminated. So two separate spans **"first meth-"** and **"od leads to wrong results"** on different lines will be joined to one span **"first method leads to wrong results"** and correspondingly updated bboxes: the characters of the resulting span will no longer have identical y-coordinates. + +.. py:data:: TEXT_PRESERVE_SPANS + + 32 -- Generate a new line for every span. Not used ("off") in PyMuPDF, but available for your use. Every line in "dict", "json", "rawdict", "rawjson" will contain exactly one span. + +.. py:data:: TEXT_MEDIABOX_CLIP + + 64 -- If set, characters entirely outside a page's **mediabox** will be ignored. This is default in PyMuPDF. + +The following constants represent the default combinations of the above for text extraction and searching: + +.. py:data:: TEXTFLAGS_TEXT + + `TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_MEDIABOX_CLIP` + +.. py:data:: TEXTFLAGS_WORDS + + `TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_MEDIABOX_CLIP` + +.. py:data:: TEXTFLAGS_BLOCKS + + `TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_MEDIABOX_CLIP` + +.. py:data:: TEXTFLAGS_DICT + + `TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_MEDIABOX_CLIP | TEXT_PRESERVE_IMAGES` + +.. py:data:: TEXTFLAGS_RAWDICT + + `TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_MEDIABOX_CLIP | TEXT_PRESERVE_IMAGES` + +.. py:data:: TEXTFLAGS_HTML + + `TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_MEDIABOX_CLIP | TEXT_PRESERVE_IMAGES` + +.. py:data:: TEXTFLAGS_XHTML + + `TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_MEDIABOX_CLIP | TEXT_PRESERVE_IMAGES` + +.. py:data:: TEXTFLAGS_XML + + `TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_MEDIABOX_CLIP` + +.. py:data:: TEXTFLAGS_SEARCH + + `TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE | TEXT_MEDIABOX_CLIP | TEXT_DEHYPHENATE` + + +.. _linkDest Kinds: + +Link Destination Kinds +----------------------- +Possible values of :attr:`linkDest.kind` (link destination kind). + +.. py:data:: LINK_NONE + + 0 -- No destination. Indicates a dummy link. + + :rtype: int + +.. py:data:: LINK_GOTO + + 1 -- Points to a place in this document. + + :rtype: int + +.. py:data:: LINK_URI + + 2 -- Points to a URI -- typically a resource specified with internet syntax. + + :rtype: int + +.. py:data:: LINK_LAUNCH + + 3 -- Launch (open) another file (of any "executable" type). + + :rtype: int + +.. py:data:: LINK_NAMED + + 4 -- points to a named location. + + :rtype: int + +.. py:data:: LINK_GOTOR + + 5 -- Points to a place in another PDF document. + + :rtype: int + +.. _linkDest Flags: + +Link Destination Flags +------------------------- + +.. Note:: The rightmost byte of this integer is a bit field, so test the truth of these bits with the *&* operator. + +.. py:data:: LINK_FLAG_L_VALID + + 1 (bit 0) Top left x value is valid + + :rtype: bool + +.. py:data:: LINK_FLAG_T_VALID + + 2 (bit 1) Top left y value is valid + + :rtype: bool + +.. py:data:: LINK_FLAG_R_VALID + + 4 (bit 2) Bottom right x value is valid + + :rtype: bool + +.. py:data:: LINK_FLAG_B_VALID + + 8 (bit 3) Bottom right y value is valid + + :rtype: bool + +.. py:data:: LINK_FLAG_FIT_H + + 16 (bit 4) Horizontal fit + + :rtype: bool + +.. py:data:: LINK_FLAG_FIT_V + + 32 (bit 5) Vertical fit + + :rtype: bool + +.. py:data:: LINK_FLAG_R_IS_ZOOM + + 64 (bit 6) Bottom right x is a zoom figure + + :rtype: bool + + +Annotation Related Constants +----------------------------- +See chapter 8.4.5, pp. 615 of the :ref:`AdobeManual` for details. + +.. _AnnotationTypes: + +Annotation Types +~~~~~~~~~~~~~~~~~ +These identifiers also cover **links** and **widgets**: the PDF specification technically handles them all in the same way, whereas **MuPDF** (and PyMuPDF) treats them as three basically different types of objects. + +:: + + PDF_ANNOT_TEXT 0 + PDF_ANNOT_LINK 1 # <=== Link object in PyMuPDF + PDF_ANNOT_FREE_TEXT 2 + PDF_ANNOT_LINE 3 + PDF_ANNOT_SQUARE 4 + PDF_ANNOT_CIRCLE 5 + PDF_ANNOT_POLYGON 6 + PDF_ANNOT_POLY_LINE 7 + PDF_ANNOT_HIGHLIGHT 8 + PDF_ANNOT_UNDERLINE 9 + PDF_ANNOT_SQUIGGLY 10 + PDF_ANNOT_STRIKE_OUT 11 + PDF_ANNOT_REDACT 12 + PDF_ANNOT_STAMP 13 + PDF_ANNOT_CARET 14 + PDF_ANNOT_INK 15 + PDF_ANNOT_POPUP 16 + PDF_ANNOT_FILE_ATTACHMENT 17 + PDF_ANNOT_SOUND 18 + PDF_ANNOT_MOVIE 19 + PDF_ANNOT_RICH_MEDIA 20 + PDF_ANNOT_WIDGET 21 # <=== Widget object in PyMuPDF + PDF_ANNOT_SCREEN 22 + PDF_ANNOT_PRINTER_MARK 23 + PDF_ANNOT_TRAP_NET 24 + PDF_ANNOT_WATERMARK 25 + PDF_ANNOT_3D 26 + PDF_ANNOT_PROJECTION 27 + PDF_ANNOT_UNKNOWN -1 + +.. _AnnotationFlags: + +Annotation Flag Bits +~~~~~~~~~~~~~~~~~~~~~ +:: + + PDF_ANNOT_IS_INVISIBLE 1 << (1-1) + PDF_ANNOT_IS_HIDDEN 1 << (2-1) + PDF_ANNOT_IS_PRINT 1 << (3-1) + PDF_ANNOT_IS_NO_ZOOM 1 << (4-1) + PDF_ANNOT_IS_NO_ROTATE 1 << (5-1) + PDF_ANNOT_IS_NO_VIEW 1 << (6-1) + PDF_ANNOT_IS_READ_ONLY 1 << (7-1) + PDF_ANNOT_IS_LOCKED 1 << (8-1) + PDF_ANNOT_IS_TOGGLE_NO_VIEW 1 << (9-1) + PDF_ANNOT_IS_LOCKED_CONTENTS 1 << (10-1) + +.. _AnnotationLineEnds: + +Annotation Line Ending Styles +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:: + + PDF_ANNOT_LE_NONE 0 + PDF_ANNOT_LE_SQUARE 1 + PDF_ANNOT_LE_CIRCLE 2 + PDF_ANNOT_LE_DIAMOND 3 + PDF_ANNOT_LE_OPEN_ARROW 4 + PDF_ANNOT_LE_CLOSED_ARROW 5 + PDF_ANNOT_LE_BUTT 6 + PDF_ANNOT_LE_R_OPEN_ARROW 7 + PDF_ANNOT_LE_R_CLOSED_ARROW 8 + PDF_ANNOT_LE_SLASH 9 + + +Widget Constants +----------------- + +.. _WidgetTypes: + +Widget Types (*field_type*) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:: + + PDF_WIDGET_TYPE_UNKNOWN 0 + PDF_WIDGET_TYPE_BUTTON 1 + PDF_WIDGET_TYPE_CHECKBOX 2 + PDF_WIDGET_TYPE_COMBOBOX 3 + PDF_WIDGET_TYPE_LISTBOX 4 + PDF_WIDGET_TYPE_RADIOBUTTON 5 + PDF_WIDGET_TYPE_SIGNATURE 6 + PDF_WIDGET_TYPE_TEXT 7 + +Text Widget Subtypes (*text_format*) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +:: + + PDF_WIDGET_TX_FORMAT_NONE 0 + PDF_WIDGET_TX_FORMAT_NUMBER 1 + PDF_WIDGET_TX_FORMAT_SPECIAL 2 + PDF_WIDGET_TX_FORMAT_DATE 3 + PDF_WIDGET_TX_FORMAT_TIME 4 + + +Widget flags (*field_flags*) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +**Common to all field types**:: + + PDF_FIELD_IS_READ_ONLY 1 + PDF_FIELD_IS_REQUIRED 1 << 1 + PDF_FIELD_IS_NO_EXPORT 1 << 2 + +**Text widgets**:: + + PDF_TX_FIELD_IS_MULTILINE 1 << 12 + PDF_TX_FIELD_IS_PASSWORD 1 << 13 + PDF_TX_FIELD_IS_FILE_SELECT 1 << 20 + PDF_TX_FIELD_IS_DO_NOT_SPELL_CHECK 1 << 22 + PDF_TX_FIELD_IS_DO_NOT_SCROLL 1 << 23 + PDF_TX_FIELD_IS_COMB 1 << 24 + PDF_TX_FIELD_IS_RICH_TEXT 1 << 25 + +**Button widgets**:: + + PDF_BTN_FIELD_IS_NO_TOGGLE_TO_OFF 1 << 14 + PDF_BTN_FIELD_IS_RADIO 1 << 15 + PDF_BTN_FIELD_IS_PUSHBUTTON 1 << 16 + PDF_BTN_FIELD_IS_RADIOS_IN_UNISON 1 << 25 + +**Choice widgets**:: + + PDF_CH_FIELD_IS_COMBO 1 << 17 + PDF_CH_FIELD_IS_EDIT 1 << 18 + PDF_CH_FIELD_IS_SORT 1 << 19 + PDF_CH_FIELD_IS_MULTI_SELECT 1 << 21 + PDF_CH_FIELD_IS_DO_NOT_SPELL_CHECK 1 << 22 + PDF_CH_FIELD_IS_COMMIT_ON_SEL_CHANGE 1 << 26 + + +.. _BlendModes: + +PDF Standard Blend Modes +---------------------------- + +For an explanation see :ref:`AdobeManual`, page 324:: + + PDF_BM_Color "Color" + PDF_BM_ColorBurn "ColorBurn" + PDF_BM_ColorDodge "ColorDodge" + PDF_BM_Darken "Darken" + PDF_BM_Difference "Difference" + PDF_BM_Exclusion "Exclusion" + PDF_BM_HardLight "HardLight" + PDF_BM_Hue "Hue" + PDF_BM_Lighten "Lighten" + PDF_BM_Luminosity "Luminosity" + PDF_BM_Multiply "Multiply" + PDF_BM_Normal "Normal" + PDF_BM_Overlay "Overlay" + PDF_BM_Saturation "Saturation" + PDF_BM_Screen "Screen" + PDF_BM_SoftLight "Softlight" + + +.. _StampIcons: + +Stamp Annotation Icons +---------------------------- +MuPDF has defined the following icons for **rubber stamp** annotations:: + + STAMP_Approved 0 + STAMP_AsIs 1 + STAMP_Confidential 2 + STAMP_Departmental 3 + STAMP_Experimental 4 + STAMP_Expired 5 + STAMP_Final 6 + STAMP_ForComment 7 + STAMP_ForPublicRelease 8 + STAMP_NotApproved 9 + STAMP_NotForPublicRelease 10 + STAMP_Sold 11 + STAMP_TopSecret 12 + STAMP_Draft 13 + +.. include:: footer.rst diff --git a/docs/version.rst b/docs/version.rst new file mode 100644 index 0000000..a9616dc --- /dev/null +++ b/docs/version.rst @@ -0,0 +1,6 @@ +---- + +This documentation covers **PyMuPDF v1.22.5** features as of **2023-06-21 00:00:01**. + +The major and minor versions of **PyMuPDF** and **MuPDF** will always be the same. Only the third qualifier (patch level) may deviate from that of **MuPDF**. + diff --git a/docs/widget.rst b/docs/widget.rst new file mode 100644 index 0000000..76c29a3 --- /dev/null +++ b/docs/widget.rst @@ -0,0 +1,243 @@ +.. include:: header.rst + +.. _Widget: + +================ +Widget +================ + +This class represents a PDF Form field, also called a "widget". Throughout this documentation, we are using these terms synonymously. Fields technically are a special case of PDF annotations, which allow users with limited permissions to enter information in a PDF. This is primarily used for filling out forms. + +Like annotations, widgets live on PDF pages. Similar to annotations, the first widget on a page is accessible via :attr:`Page.first_widget` and subsequent widgets can be accessed via the :attr:`Widget.next` property. + +*(Changed in version 1.16.0)* MuPDF no longer treats widgets as a subset of general annotations. Consequently, :attr:`Page.first_annot` and :meth:`Annot.next` will deliver **non-widget annotations exclusively**, and be *None* if only form fields exist on a page. Vice versa, :attr:`Page.first_widget` and :meth:`Widget.next` will only show widgets. This design decision is purely internal to MuPDF; technically, links, annotations and fields have a lot in common and also continue to share the better part of their code within (Py-) MuPDF. + + +**Class API** + +.. class:: Widget + + .. method:: button_states + + *New in version 1.18.15* + + Return the names of On / Off (i.e. selected / clicked or not) states a button field may have. While the 'Off' state usually is also named like so, the 'On' state is often given a name relating to the functional context, for example 'Yes', 'Female', etc. + + This method helps finding out the possible values of :attr:`field_value` in these cases. + + :returns: a dictionary with the names of 'On' and 'Off' for the *normal* and the *pressed-down* appearance of button widgets. The following example shows that the "selected" value is "Male": + + >>> print(field.field_name, field.button_states()) + Gender Second person {'down': ['Male', 'Off'], 'normal': ['Male', 'Off']} + + + .. method:: on_state + + * New in version 1.22.2 + + Return the value of the "ON" state of check boxes and radio buttons. For check boxes this is always the value "Yes". For radio buttons, this is the value to select / activate the button. + + :returns: the value that sets the button to "selected". For non-checkbox, non-radiobutton fields, always `None` is returned. For check boxes the return is `True`. For radio buttons this is the value "Male" in the following example: + + >>> print(field.field_name, field.button_states()) + Gender Second person {'down': ['Male', 'Off'], 'normal': ['Male', 'Off']} + >>> print(field.on_state()) + Male + + So for check boxes and radio buttons, the recommended method to set them to "selected", or to check the state is the following: + + >>> field.field_value = field.on_state() + >>> field.field_value == field.on_state() + True + + + .. method:: update + + After any changes to a widget, this method **must be used** to store them in the PDF [#f1]_. + + .. method:: reset + + Reset the field's value to its default -- if defined -- or remove it. Do not forget to issue :meth:`update` afterwards. + + .. attribute:: next + + Point to the next form field on the page. The last widget returns *None*. + + .. attribute:: border_color + + A list of up to 4 floats defining the field's border color. Default value is *None* which causes border style and border width to be ignored. + + .. attribute:: border_style + + A string defining the line style of the field's border. See :attr:`Annot.border`. Default is "s" ("Solid") -- a continuous line. Only the first character (upper or lower case) will be regarded when creating a widget. + + .. attribute:: border_width + + A float defining the width of the border line. Default is 1. + + .. attribute:: border_dashes + + A list/tuple of integers defining the dash properties of the border line. This is only meaningful if *border_style == "D"* and :attr:`border_color` is provided. + + .. attribute:: choice_values + + Python sequence of strings defining the valid choices of list boxes and combo boxes. For these widget types, this property is mandatory and must contain at least two items. Ignored for other types. + + .. attribute:: field_name + + A mandatory string defining the field's name. No checking for duplicates takes place. + + .. attribute:: field_label + + An optional string containing an "alternate" field name. Typically used for any notes, help on field usage, etc. Default is the field name. + + .. attribute:: field_value + + The value of the field. + + .. attribute:: field_flags + + An integer defining a large amount of properties of a field. Be careful when changing this attribute as this may change the field type. + + .. attribute:: field_type + + A mandatory integer defining the field type. This is a value in the range of 0 to 6. It cannot be changed when updating the widget. + + .. attribute:: field_type_string + + A string describing (and derived from) the field type. + + .. attribute:: fill_color + + A list of up to 4 floats defining the field's background color. + + .. attribute:: button_caption + + The caption string of a button-type field. + + .. attribute:: is_signed + + A bool indicating the signing status of a signature field, else *None*. + + .. attribute:: rect + + The rectangle containing the field. + + .. attribute:: text_color + + A list of **1, 3 or 4 floats** defining the text color. Default value is black (`[0, 0, 0]`). + + .. attribute:: text_font + + A string defining the font to be used. Default and replacement for invalid values is *"Helv"*. For valid font reference names see the table below. + + .. attribute:: text_fontsize + + A float defining the text fontsize. Default value is zero, which causes PDF viewer software to dynamically choose a size suitable for the annotation's rectangle and text amount. + + .. attribute:: text_maxlen + + An integer defining the maximum number of text characters. PDF viewers will (should) not accept a longer text. + + .. attribute:: text_type + + An integer defining acceptable text types (e.g. numeric, date, time, etc.). For reference only for the time being -- will be ignored when creating or updating widgets. + + .. attribute:: xref + + The PDF :data:`xref` of the widget. + + .. attribute:: script + + * New in version 1.16.12 + + JavaScript text (unicode) for an action associated with the widget, or *None*. This is the only script action supported for **button type** widgets. + + .. attribute:: script_stroke + + * New in version 1.16.12 + + JavaScript text (unicode) to be performed when the user types a key-stroke into a text field or combo box or modifies the selection in a scrollable list box. This action can check the keystroke for validity and reject or modify it. *None* if not present. + + .. attribute:: script_format + + * New in version 1.16.12 + + JavaScript text (unicode) to be performed before the field is formatted to display its current value. This action can modify the field’s value before formatting. *None* if not present. + + .. attribute:: script_change + + * New in version 1.16.12 + + JavaScript text (unicode) to be performed when the field’s value is changed. This action can check the new value for validity. *None* if not present. + + .. attribute:: script_calc + + * New in version 1.16.12 + + JavaScript text (unicode) to be performed to recalculate the value of this field when that of another field changes. *None* if not present. + + .. note:: + + 1. For **adding** or **changing** one of the above scripts, + just put the appropriate JavaScript source code in the widget attribute. + To **remove** a script, set the respective attribute to *None*. + + 2. Button fields only support :attr:`script`. + Other script entries will automatically be set to *None*. + + 3. It is worthwhile to look at + `this `_ + manual with lots of information about Adobe's standard scripts for various field types. + For example, if you want to add a text field representing a date, + you may want to store the following scripts. + They will ensure pattern-compatible date formats and display date pickers in supporting viewers:: + + widget.script_format = 'AFDate_FormatEx("mm/dd/yyyy");' + widget.script_stroke = 'AFDate_KeystrokeEx("mm/dd/yyyy");' + + +Standard Fonts for Widgets +---------------------------------- +Widgets use their own resources object */DR*. A widget resources object must at least contain a */Font* object. Widget fonts are independent from page fonts. We currently support the 14 PDF base fonts using the following fixed reference names, or any name of an already existing field font. When specifying a text font for new or changed widgets, **either** choose one in the first table column (upper and lower case supported), **or** one of the already existing form fonts. In the latter case, spelling must exactly match. + +To find out already existing field fonts, inspect the list :attr:`Document.FormFonts`. + +============= ======================= +**Reference** **Base14 Fontname** +============= ======================= +CoBI Courier-BoldOblique +CoBo Courier-Bold +CoIt Courier-Oblique +Cour Courier +HeBI Helvetica-BoldOblique +HeBo Helvetica-Bold +HeIt Helvetica-Oblique +Helv Helvetica **(default)** +Symb Symbol +TiBI Times-BoldItalic +TiBo Times-Bold +TiIt Times-Italic +TiRo Times-Roman +ZaDb ZapfDingbats +============= ======================= + +You are generally free to use any font for every widget. However, we recommend using *ZaDb* ("ZapfDingbats") and fontsize 0 for check boxes: typical viewers will put a correctly sized tickmark in the field's rectangle, when it is clicked. + +Supported Widget Types +----------------------- +PyMuPDF supports the creation and update of many, but not all widget types. + +* text (`PDF_WIDGET_TYPE_TEXT`) +* push button (`PDF_WIDGET_TYPE_BUTTON`) +* check box (`PDF_WIDGET_TYPE_CHECKBOX`) +* combo box (`PDF_WIDGET_TYPE_COMBOBOX`) +* list box (`PDF_WIDGET_TYPE_LISTBOX`) +* radio button (`PDF_WIDGET_TYPE_RADIOBUTTON`): PyMuPDF does not currently support the **creation** of groups of (interconnected) radio buttons, where setting one automatically unsets the other buttons in the group. The widget object also does not reflect the presence of a button group. However: consistently selecting (or unselecting) a radio button is supported. This includes correctly setting the value maintained in the owning button group. Selecting a radio button may be done by either assigning `True` or `field.on_sate()` to the field value. **De-selecting** the button should be done assigning `False`. +* signature (`PDF_WIDGET_TYPE_SIGNATURE`) **read only**. + +.. rubric:: Footnotes + +.. [#f1] If you intend to re-access a new or updated field (e.g. for making a pixmap), make sure to reload the page first. Either close and re-open the document, or load another page first, or simply do `page = doc.reload_page(page)`. + +.. include:: footer.rst diff --git a/docs/xml-class.rst b/docs/xml-class.rst new file mode 100644 index 0000000..da65c9b --- /dev/null +++ b/docs/xml-class.rst @@ -0,0 +1,440 @@ +.. include:: header.rst + +.. _Xml: + +================ +Xml +================ + +.. role:: htmlTag(emphasis) + +* New in v1.21.0 + +This represents an HTML or an XML node. It is a helper class intended to access the DOM (Document Object Model) content of a :ref:`Story` object. + +There is no need to ever directly construct an :ref:`Xml` object: after creating a :ref:`Story`, simply take :attr:`Story.body` -- which is an Xml node -- and use it to navigate your way through the story's DOM. + + +================================ =========================================================================================== +**Method / Attribute** **Description** +================================ =========================================================================================== +:meth:`~.add_bullet_list` Add a :htmlTag:`ul` tag - bulleted list, context manager. +:meth:`~.add_codeblock` Add a :htmlTag:`pre` tag, context manager. +:meth:`~.add_description_list` Add a :htmlTag:`dl` tag, context manager. +:meth:`~.add_division` add a :htmlTag:`div` tag (renamed from “section”), context manager. +:meth:`~.add_header` Add a header tag (one of :htmlTag:`h1` to :htmlTag:`h6`), context manager. +:meth:`~.add_horizontal_line` Add a :htmlTag:`hr` tag. +:meth:`~.add_image` Add a :htmlTag:`img` tag. +:meth:`~.add_link` Add a :htmlTag:`a` tag. +:meth:`~.add_number_list` Add a :htmlTag:`ol` tag, context manager. +:meth:`~.add_paragraph` Add a :htmlTag:`p` tag. +:meth:`~.add_span` Add a :htmlTag:`span` tag, context manager. +:meth:`~.add_subscript` Add subscript text(:htmlTag:`sub` tag) - inline element, treated like text. +:meth:`~.add_superscript` Add subscript text (:htmlTag:`sup` tag) - inline element, treated like text. +:meth:`~.add_code` Add code text (:htmlTag:`code` tag) - inline element, treated like text. +:meth:`~.add_var` Add code text (:htmlTag:`code` tag) - inline element, treated like text. +:meth:`~.add_samp` Add code text (:htmlTag:`code` tag) - inline element, treated like text. +:meth:`~.add_kbd` Add code text (:htmlTag:`code` tag) - inline element, treated like text. +:meth:`~.add_text` Add a text string. Line breaks `\n` are honored as :htmlTag:`br` tags. +:meth:`~.append_child` Append a child node. +:meth:`~.clone` Make a copy if this node. +:meth:`~.create_element` Make a new node with a given tag name. +:meth:`~.create_text_node` Create direct text for the current node. +:meth:`~.find` Find a sub-node with given properties. +:meth:`~.find_next` Repeat previous "find" with the same criteria. +:meth:`~.insert_after` Insert an element after current node. +:meth:`~.insert_before` Insert an element before current node. +:meth:`~.remove` Remove this node. +:meth:`~.set_align` Set the alignment using a CSS style spec. Only works for block-level tags. +:meth:`~.set_attribute` Set an arbitrary key to some value (which may be empty). +:meth:`~.set_bgcolor` Set the background color. Only works for block-level tags. +:meth:`~.set_bold` Set bold on or off or to some string value. +:meth:`~.set_color` Set text color. +:meth:`~.set_columns` Set the number of columns. Argument may be any valid number or string. +:meth:`~.set_font` Set the font-family, e.g. “sans-serif”. +:meth:`~.set_fontsize` Set the font size. Either a float or a valid HTML/CSS string. +:meth:`~.set_id` Set a :htmlTag:`id`. A check for uniqueness is performed. +:meth:`~.set_italic` Set italic on or off or to some string value. +:meth:`~.set_leading` Set inter-block text distance (`-mupdf-leading`), only works on block-level nodes. +:meth:`~.set_lineheight` Set height of a line. Float like 1.5, which sets to `1.5 * fontsize`. +:meth:`~.set_margins` Set the margin(s), float or string with up to 4 values. +:meth:`~.set_pagebreak_after` Insert a page break after this node. +:meth:`~.set_pagebreak_before` Insert a page break before this node. +:meth:`~.set_properties` Set any or all desired properties in one call. +:meth:`~.add_style` Set (add) a “style” that is not supported by its own `set_` method. +:meth:`~.add_class` Set (add) a “class” attribute. +:meth:`~.set_text_indent` Set indentation for first textblock line. Only works for block-level nodes. +:attr:`~.tagname` Either the HTML tag name like :htmlTag:`p` or `None` if a text node. +:attr:`~.text` Either the node's text or `None` if a tag node. +:attr:`~.is_text` Check if the node is a text. +:attr:`~.first_child` Contains the first node one level below this one (or `None`). +:attr:`~.last_child` Contains the last node one level below this one (or `None`). +:attr:`~.next` The next node at the same level (or `None`). +:attr:`~.previous` The previous node at the same level. +:attr:`~.root` The top node of the DOM, which hence has the tagname :htmlTag:`html`. +================================ =========================================================================================== + + + +**Class API** + +.. class:: Xml + + .. method:: add_bullet_list + + Add an :htmlTag:`ul` tag - bulleted list, context manager. See `ul `_. + + .. method:: add_codeblock + + Add a :htmlTag:`pre` tag, context manager. See `pre `_. + + .. method:: add_description_list + + Add a :htmlTag:`dl` tag, context manager. See `dl `_. + + .. method:: add_division + + Add a :htmlTag:`div` tag, context manager. See `div `_. + + .. method:: add_header(value) + + Add a header tag (one of :htmlTag:`h1` to :htmlTag:`h6`), context manager. See `headings `_. + + :arg int value: a value 1 - 6. + + .. method:: add_horizontal_line + + Add a :htmlTag:`hr` tag. See `hr `_. + + .. method:: add_image(name, width=None, height=None) + + Add an :htmlTag:`img` tag. This causes the inclusion of the named image in the DOM. + + :arg str name: the filename of the image. This **must be the member name** of some entry of the :ref:`Archive` parameter of the :ref:`Story` constructor. + :arg width: if provided, either an absolute (int) value, or a percentage string like "30%". A percentage value refers to the width of the specified `where` rectangle in :meth:`Story.place`. If this value is provided and `height` is omitted, the image will be included keeping its aspect ratio. + :arg height: if provided, either an absolute (int) value, or a percentage string like "30%". A percentage value refers to the height of the specified `where` rectangle in :meth:`Story.place`. If this value is provided and `width` is omitted, the image's aspect ratio will be honored. + + .. method:: add_link(href, text=None) + + Add an :htmlTag:`a` tag - inline element, treated like text. + + :arg str href: the URL target. + :arg str text: the text to display. If omitted, the `href` text is shown instead. + + .. method:: add_number_list + + Add an :htmlTag:`ol` tag, context manager. + + .. method:: add_paragraph + + Add a :htmlTag:`p` tag, context manager. + + .. method:: add_span + + Add a :htmlTag:`span` tag, context manager. See `span`_ + + .. method:: add_subscript(text) + + Add "subscript" text(:htmlTag:`sub` tag) - inline element, treated like text. + + .. method:: add_superscript(text) + + Add "superscript" text (:htmlTag:`sup` tag) - inline element, treated like text. + + .. method:: add_code(text) + + Add "code" text (:htmlTag:`code` tag) - inline element, treated like text. + + .. method:: add_var(text) + + Add "variable" text (:htmlTag:`var` tag) - inline element, treated like text. + + .. method:: add_samp(text) + + Add "sample output" text (:htmlTag:`samp` tag) - inline element, treated like text. + + .. method:: add_kbd(text) + + Add "keyboard input" text (:htmlTag:`kbd` tag) - inline element, treated like text. + + .. method:: add_text(text) + + Add a text string. Line breaks `\n` are honored as :htmlTag:`br` tags. + + .. method:: set_align(value) + + Set the text alignment. Only works for block-level tags. + + :arg value: either one of the :ref:`TextAlign` or the `text-align `_ values. + + .. method:: set_attribute(key, value=None) + + Set an arbitrary key to some value (which may be empty). + + :arg str key: the name of the attribute. + :arg str value: the (optional) value of the attribute. + + .. method:: get_attributes() + + Retrieve all attributes of the current nodes as a dictionary. + + :returns: a dictionary with the attributes and their values of the node. + + .. method:: get_attribute_value(key) + + Get the attribute value of `key`. + + :arg str key: the name of the attribute. + + :returns: a string with the value of `key`. + + .. method:: remove_attribute(key) + + Remove the attribute `key` from the node. + + :arg str key: the name of the attribute. + + .. method:: set_bgcolor(value) + + Set the background color. Only works for block-level tags. + + :arg value: either an RGB value like (255, 0, 0) (for "red") or a valid `background-color `_ value. + + .. method:: set_bold(value) + + Set bold on or off or to some string value. + + :arg value: `True`, `False` or a valid `font-weight `_ value. + + .. method:: set_color(value) + + Set the color of the text following. + + :arg value: either an RGB value like (255, 0, 0) (for "red") or a valid `color `_ value. + + .. method:: set_columns(value) + + Set the number of columns. + + :arg value: a valid `columns `_ value. + + .. note:: Currently ignored - supported in a future MuPDF version. + + .. method:: set_font(value) + + Set the font-family. + + :arg str value: e.g. "sans-serif". + + .. method:: set_fontsize(value) + + Set the font size for text following. + + :arg value: a float or a valid `font-size `_ value. + + .. method:: set_id(unqid) + + Set a :htmlTag:`id`. This serves as a unique identification of the node within the DOM. Use it to easily locate the node to inspect or modify it. A check for uniqueness is performed. + + :arg str unqid: id string of the node. + + .. method:: set_italic(value) + + Set italic on or off or to some string value for the text following it. + + :arg value: `True`, `False` or some valid `font-style `_ value. + + .. method:: set_leading(value) + + Set inter-block text distance (`-mupdf-leading`), only works on block-level nodes. + + :arg float value: the distance in points to the previous block. + + .. method:: set_lineheight(value) + + Set height of a line. + + :arg value: a float like 1.5 (which sets to `1.5 * fontsize`), or some valid `line-height `_ value. + + .. method:: set_margins(value) + + Set the margin(s). + + :arg value: float or string with up to 4 values. See `CSS documentation `_. + + .. method:: set_pagebreak_after + + Insert a page break after this node. + + .. method:: set_pagebreak_before + + Insert a page break before this node. + + .. method:: set_properties(align=None, bgcolor=None, bold=None, color=None, columns=None, font=None, fontsize=None, indent=None, italic=None, leading=None, lineheight=None, margins=None, pagebreak_after=False, pagebreak_before=False, unqid=None, cls=None) + + Set any or all desired properties in one call. The meaning of argument values equal the values of the corresponding `set_` methods. + + .. note:: The properties set by this method are directly attached to the node, whereas every `set_` method generates a new :htmlTag:`span` below the current node that has the respective property. So to e.g. "globally" set some property for the :htmlTag:`body`, this method must be used. + + .. method:: add_style(value) + + Set (add) some style attribute not supported by its own `set_` method. + + :arg str value: any valid CSS style value. + + .. method:: add_class(value) + + Set (add) some "class" attribute. + + :arg str value: the name of the class. Must have been defined in either the HTML or the CSS source of the DOM. + + .. method:: set_text_indent(value) + + Set indentation for the first textblock line. Only works for block-level nodes. + + :arg value: a valid `text-indent `_ value. Please note that negative values do not work. + + + .. method:: append_child(node) + + Append a child node. This is a low-level method used by other methods like :meth:`Xml.add_paragraph`. + + :arg node: the :ref:`Xml` node to append. + + .. method:: create_text_node(text) + + Create direct text for the current node. + + :arg str text: the text to append. + + :rtype: :ref:`Xml` + :returns: the created element. + + .. method:: create_element(tag) + + Create a new node with a given tag. This a low-level method used by other methods like :meth:`Xml.add_paragraph`. + + :arg str tag: the element tag. + + :rtype: :ref:`Xml` + :returns: the created element. To actually bind it to the DOM, use :meth:`Xml.append_child`. + + .. method:: insert_before(elem) + + Insert the given element `elem` before this node. + + :arg elem: some :ref:`Xml` element. + + .. method:: insert_after(elem) + + Insert the given element `elem` after this node. + + :arg elem: some :ref:`Xml` element. + + .. method:: clone() + + Make a copy of this node, which then may be appended (using :meth:`Xml.append_child`) or inserted (using one of :meth:`Xml.insert_before`, :meth:`Xml.insert_after`) in this DOM. + + :returns: the clone (:ref:`Xml`) of the current node. + + .. method:: remove() + + Remove this node from the DOM. + + + .. method:: debug() + + For debugging purposes, print this node's structure in a simplified form. + + .. method:: find(tag, att, match) + + Under the current node, find the first node with the given `tag`, attribute `att` and value `match`. + + :arg str tag: restrict search to this tag. May be `None` for unrestricted searches. + :arg str att: check this attribute. May be `None`. + :arg str match: the desired attribute value to match. May be `None`. + + :rtype: :ref:`Xml`. + :returns: `None` if nothing found, otherwise the first matching node. + + .. method:: find_next( tag, att, match) + + Continue a previous :meth:`Xml.find` (or ::meth:`find_next`) with the same values. + + :rtype: :ref:`Xml`. + :returns: `None` if none more found, otherwise the next matching node. + + + .. attribute:: tagname + + Either the HTML tag name like :htmlTag:`p` or `None` if a text node. + + .. attribute:: text + + Either the node's text or `None` if a tag node. + + .. attribute:: is_text + + Check if a text node. + + .. attribute:: first_child + + Contains the first node one level below this one (or `None`). + + .. attribute:: last_child + + Contains the last node one level below this one (or `None`). + + .. attribute:: next + + The next node at the same level (or `None`). + + .. attribute:: previous + + The previous node at the same level. + + .. attribute:: root + + The top node of the DOM, which hence has the tagname :htmlTag:`html`. + + +Setting Text properties +------------------------ + +In HTML tags can be nested such that innermost text **inherits properties** from the tag enveloping its parent tag. For example `

some bold textthis is bold and italicregular text

`. + +To achieve the same effect, methods like :meth:`Xml.set_bold` and :meth:`Xml.set_italic` each open a temporary :htmlTag:`span` with the desired property underneath the current node. + +In addition, these methods return there parent node, so they can be concatenated with each other. + + + +Context Manager support +------------------------ +The standard way to add nodes to a DOM is this:: + + body = story.body + para = body.add_paragraph() # add a paragraph + para.set_bold() # text that follows will be bold + para.add_text("some bold text") + para.set_italic() # text that follows will additionally be italic + para.add_txt("this is bold and italic") + para.set_italic(False).set_bold(False) # all following text will be regular + para.add_text("regular text") + + + +Methods that are flagged as "context managers" can conveniently be used in this way:: + + body = story.body + with body.add_paragraph() as para: + para.set_bold().add_text("some bold text") + para.set_italic().add_text("this is bold and italic") + para.set_italic(False).set_bold(False).add_text("regular text") + para.add_text("more regular text") + +.. include:: footer.rst + +.. External links: + +.. _span: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/span diff --git a/docs/znames.rst b/docs/znames.rst new file mode 100644 index 0000000..e822d2f --- /dev/null +++ b/docs/znames.rst @@ -0,0 +1,47 @@ +.. include:: header.rst + +.. _Deprecated: + +================ +Deprecated Names +================ + +The original naming convention for methods and properties has been "camelCase". Since its creation around 2013, a tremendous increase of functionality has happened in PyMuPDF -- and with it a corresponding increase in classes, methods and properties. In too many cases, this has led to non-intuitive, illogical and ugly names, difficult to memorize or guess. + +A few versions ago, I therefore decided to shift gears and switch to a "snake_cased" naming standard. +This was a major effort, which needed a step-wise approach. I think am done with it now (version 1.18.14). + +The following list maps deprecated names to their new versions. For example, property `pageCount` became `page_count` in the :ref:`Document` class. There also are less obvious name changes, e.g. method `getPNGdata` was renamed to `tobytes` in the :ref:`Pixmap` class. + +Names of classes (camel case) and package-wide constants (the majority is upper case) remain untouched. + +Old names will remain available as deprecated aliases through MuPDF version 1.19.0 and **be removed** in the version that follows it - probably version 1.20.0, but this depends on upstream decisions (MuPDF). + +Starting with version 1.19.0, we will issue deprecation warnings on `sys.stderr` like `Deprecation: 'newPage' removed from class 'Document' after v1.19.0 - use 'new_page'.` when aliased methods are being used. Using a deprecated property will not cause this type of warning. + +Starting immediately, all deprecated objects (methods and properties) will show a copy of the original's docstring, **prefixed** with the deprecation message, for example:: + + >>> print(fitz.Document.pageCount.__doc__) + *** Deprecated and removed in version following 1.19.0 - use 'page_count'. *** + Number of pages. + >>> print(fitz.Document.newPage.__doc__) + *** Deprecated and removed in version following 1.19.0 - use 'new_page'. *** + Create and return a new page object. + + Args: + pno: (int) insert before this page. Default: after last page. + width: (float) page width in points. Default: 595 (ISO A4 width). + height: (float) page height in points. Default 842 (ISO A4 height). + Returns: + A Page object. + + +There is a utility script `alias-changer.py `_ which can be used to do mass-renames in your scripts. It accepts either a single file or a folder as argument. If a folder is supplied, all its Python files and those of its subfolders are changed. Optionally, backups of the scripts can be taken. + +Deprecated names are not separately documented. The following list will help you find the documentation of the original. + +.. note:: This is automatically generated. One or two items refer to yet undocumented methods - please simply ignore them. + +.. include:: deprecated.rst + +.. include:: footer.rst diff --git a/fitz/__init__.py b/fitz/__init__.py new file mode 100644 index 0000000..090233e --- /dev/null +++ b/fitz/__init__.py @@ -0,0 +1,509 @@ +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +import sys + +import glob +import os +if os.path.exists( 'fitz/__init__.py'): + if not glob.glob( 'fitz/_fitz*'): + print( '#' * 40) + print( '# Warning: current directory appears to contain an incomplete') + print( '# fitz/ installation directory so "import fitz" may fail.') + print( '# This can happen if current directory is a PyMuPDF source tree.') + print( '# Suggest changing to a different current directory.') + print( '#' * 40) + +from fitz.fitz import * + +# define the supported colorspaces for convenience +fitz.csRGB = fitz.Colorspace(fitz.CS_RGB) +fitz.csGRAY = fitz.Colorspace(fitz.CS_GRAY) +fitz.csCMYK = fitz.Colorspace(fitz.CS_CMYK) +csRGB = fitz.csRGB +csGRAY = fitz.csGRAY +csCMYK = fitz.csCMYK + +# create the TOOLS object. +# +# Unfortunately it seems that this is never be destructed even if we use an +# atexit() handler, which makes MuPDF's Memento list it as a leak. In fitz.i +# we use Memento_startLeaking()/Memento_stopLeaking() when allocating +# the Tools instance so at least the leak is marked as known. +# +TOOLS = fitz.Tools() +TOOLS.thisown = True +fitz.TOOLS = TOOLS + +# This atexit handler runs, but doesn't cause ~Tools() to be run. +# +import atexit + + +def cleanup_tools(TOOLS): + # print(f'cleanup_tools: TOOLS={TOOLS} id(TOOLS)={id(TOOLS)}') + # print(f'TOOLS.thisown={TOOLS.thisown}') + del TOOLS + del fitz.TOOLS + + +atexit.register(cleanup_tools, TOOLS) + + +# Require that MuPDF matches fitz.TOOLS.mupdf_version(); also allow use with +# next minor version (e.g. 1.21.2 => 1.22), so we can test with mupdf master. +# +def v_str_to_tuple(s): + return tuple(map(int, s.split('.'))) + +def v_tuple_to_string(t): + return '.'.join(map(str, t)) + +mupdf_version_tuple = v_str_to_tuple(fitz.TOOLS.mupdf_version()) +mupdf_version_tuple_required = v_str_to_tuple(fitz.VersionFitz) +mupdf_version_tuple_required_prev = (mupdf_version_tuple_required[0], mupdf_version_tuple_required[1]-1) +mupdf_version_tuple_required_next = (mupdf_version_tuple_required[0], mupdf_version_tuple_required[1]+1) + +if mupdf_version_tuple[:2] not in ( + mupdf_version_tuple_required_prev[:2], + mupdf_version_tuple_required[:2], + mupdf_version_tuple_required_next[:2], + ): + raise ValueError( + f'MuPDF library {v_tuple_to_string(mupdf_version_tuple)!r} mismatch:' + f' require' + f' {v_tuple_to_string(mupdf_version_tuple_required_prev)!r}' + f' or {v_tuple_to_string(mupdf_version_tuple_required)!r}' + f' or {v_tuple_to_string(mupdf_version_tuple_required_next)!r}' + f'.' + ) + +# copy functions in 'utils' to their respective fitz classes +import fitz.utils + +# ------------------------------------------------------------------------------ +# General +# ------------------------------------------------------------------------------ +fitz.recover_quad = fitz.utils.recover_quad +fitz.recover_bbox_quad = fitz.utils.recover_bbox_quad +fitz.recover_line_quad = fitz.utils.recover_line_quad +fitz.recover_span_quad = fitz.utils.recover_span_quad +fitz.recover_char_quad = fitz.utils.recover_char_quad + +# ------------------------------------------------------------------------------ +# Document +# ------------------------------------------------------------------------------ +fitz.open = fitz.Document +fitz.Document._do_links = fitz.utils.do_links +fitz.Document.del_toc_item = fitz.utils.del_toc_item +fitz.Document.get_char_widths = fitz.utils.get_char_widths +fitz.Document.get_ocmd = fitz.utils.get_ocmd +fitz.Document.get_page_labels = fitz.utils.get_page_labels +fitz.Document.get_page_numbers = fitz.utils.get_page_numbers +fitz.Document.get_page_pixmap = fitz.utils.get_page_pixmap +fitz.Document.get_page_text = fitz.utils.get_page_text +fitz.Document.get_toc = fitz.utils.get_toc +fitz.Document.has_annots = fitz.utils.has_annots +fitz.Document.has_links = fitz.utils.has_links +fitz.Document.insert_page = fitz.utils.insert_page +fitz.Document.new_page = fitz.utils.new_page +fitz.Document.scrub = fitz.utils.scrub +fitz.Document.search_page_for = fitz.utils.search_page_for +fitz.Document.set_metadata = fitz.utils.set_metadata +fitz.Document.set_ocmd = fitz.utils.set_ocmd +fitz.Document.set_page_labels = fitz.utils.set_page_labels +fitz.Document.set_toc = fitz.utils.set_toc +fitz.Document.set_toc_item = fitz.utils.set_toc_item +fitz.Document.tobytes = fitz.Document.write +fitz.Document.subset_fonts = fitz.utils.subset_fonts +fitz.Document.get_oc = fitz.utils.get_oc +fitz.Document.set_oc = fitz.utils.set_oc +fitz.Document.xref_copy = fitz.utils.xref_copy + + +# ------------------------------------------------------------------------------ +# Page +# ------------------------------------------------------------------------------ +fitz.Page.apply_redactions = fitz.utils.apply_redactions +fitz.Page.delete_widget = fitz.utils.delete_widget +fitz.Page.draw_bezier = fitz.utils.draw_bezier +fitz.Page.draw_circle = fitz.utils.draw_circle +fitz.Page.draw_curve = fitz.utils.draw_curve +fitz.Page.draw_line = fitz.utils.draw_line +fitz.Page.draw_oval = fitz.utils.draw_oval +fitz.Page.draw_polyline = fitz.utils.draw_polyline +fitz.Page.draw_quad = fitz.utils.draw_quad +fitz.Page.draw_rect = fitz.utils.draw_rect +fitz.Page.draw_sector = fitz.utils.draw_sector +fitz.Page.draw_squiggle = fitz.utils.draw_squiggle +fitz.Page.draw_zigzag = fitz.utils.draw_zigzag +fitz.Page.get_links = fitz.utils.get_links +fitz.Page.get_pixmap = fitz.utils.get_pixmap +fitz.Page.get_text = fitz.utils.get_text +fitz.Page.get_image_info = fitz.utils.get_image_info +fitz.Page.get_text_blocks = fitz.utils.get_text_blocks +fitz.Page.get_text_selection = fitz.utils.get_text_selection +fitz.Page.get_text_words = fitz.utils.get_text_words +fitz.Page.get_textbox = fitz.utils.get_textbox +fitz.Page.insert_image = fitz.utils.insert_image +fitz.Page.insert_link = fitz.utils.insert_link +fitz.Page.insert_text = fitz.utils.insert_text +fitz.Page.insert_textbox = fitz.utils.insert_textbox +fitz.Page.new_shape = lambda x: fitz.utils.Shape(x) +fitz.Page.search_for = fitz.utils.search_for +fitz.Page.show_pdf_page = fitz.utils.show_pdf_page +fitz.Page.update_link = fitz.utils.update_link +fitz.Page.write_text = fitz.utils.write_text +fitz.Page.get_label = fitz.utils.get_label +fitz.Page.get_image_rects = fitz.utils.get_image_rects +fitz.Page.get_textpage_ocr = fitz.utils.get_textpage_ocr +fitz.Page.delete_image = fitz.utils.delete_image +fitz.Page.replace_image = fitz.utils.replace_image + +# ------------------------------------------------------------------------ +# Annot +# ------------------------------------------------------------------------ +fitz.Annot.get_text = fitz.utils.get_text +fitz.Annot.get_textbox = fitz.utils.get_textbox + +# ------------------------------------------------------------------------ +# Rect and IRect +# ------------------------------------------------------------------------ +fitz.Rect.get_area = fitz.utils.get_area +fitz.IRect.get_area = fitz.utils.get_area + +# ------------------------------------------------------------------------ +# TextWriter +# ------------------------------------------------------------------------ +fitz.TextWriter.fill_textbox = fitz.utils.fill_textbox + + +class FitzDeprecation(DeprecationWarning): + pass + + +def restore_aliases(): + import warnings + + warnings.filterwarnings( + "once", + category=FitzDeprecation, + ) + + def showthis(msg, cat, filename, lineno, file=None, line=None): + text = warnings.formatwarning(msg, cat, filename, lineno, line=line) + s = text.find("FitzDeprecation") + if s < 0: + print(text, file=sys.stderr) + return + text = text[s:].splitlines()[0][4:] + print(text, file=sys.stderr) + + warnings.showwarning = showthis + + def _alias(fitz_class, old, new): + fname = getattr(fitz_class, new) + r = str(fitz_class)[1:-1] + objname = " ".join(r.split()[:2]) + objname = objname.replace("fitz.fitz.", "") + objname = objname.replace("fitz.utils.", "") + if callable(fname): + + def deprecated_function(*args, **kw): + msg = "'%s' removed from %s after v1.19 - use '%s'." % ( + old, + objname, + new, + ) + if not VersionBind.startswith("1.18"): + warnings.warn(msg, category=FitzDeprecation) + return fname(*args, **kw) + + setattr(fitz_class, old, deprecated_function) + else: + if type(fname) is property: + setattr(fitz_class, old, property(fname.fget)) + else: + setattr(fitz_class, old, fname) + + eigen = getattr(fitz_class, old) + x = fname.__doc__ + if not x: + x = "" + try: + if callable(fname) or type(fname) is property: + eigen.__doc__ = ( + "*** Deprecated and removed after v1.19 - use '%s'. ***\n" % new + x + ) + except: + pass + + # deprecated Document aliases + _alias(fitz.Document, "chapterCount", "chapter_count") + _alias(fitz.Document, "chapterPageCount", "chapter_page_count") + _alias(fitz.Document, "convertToPDF", "convert_to_pdf") + _alias(fitz.Document, "copyPage", "copy_page") + _alias(fitz.Document, "deletePage", "delete_page") + _alias(fitz.Document, "deletePageRange", "delete_pages") + _alias(fitz.Document, "embeddedFileAdd", "embfile_add") + _alias(fitz.Document, "embeddedFileCount", "embfile_count") + _alias(fitz.Document, "embeddedFileDel", "embfile_del") + _alias(fitz.Document, "embeddedFileGet", "embfile_get") + _alias(fitz.Document, "embeddedFileInfo", "embfile_info") + _alias(fitz.Document, "embeddedFileNames", "embfile_names") + _alias(fitz.Document, "embeddedFileUpd", "embfile_upd") + _alias(fitz.Document, "extractFont", "extract_font") + _alias(fitz.Document, "extractImage", "extract_image") + _alias(fitz.Document, "findBookmark", "find_bookmark") + _alias(fitz.Document, "fullcopyPage", "fullcopy_page") + _alias(fitz.Document, "getCharWidths", "get_char_widths") + _alias(fitz.Document, "getOCGs", "get_ocgs") + _alias(fitz.Document, "getPageFontList", "get_page_fonts") + _alias(fitz.Document, "getPageImageList", "get_page_images") + _alias(fitz.Document, "getPagePixmap", "get_page_pixmap") + _alias(fitz.Document, "getPageText", "get_page_text") + _alias(fitz.Document, "getPageXObjectList", "get_page_xobjects") + _alias(fitz.Document, "getSigFlags", "get_sigflags") + _alias(fitz.Document, "getToC", "get_toc") + _alias(fitz.Document, "getXmlMetadata", "get_xml_metadata") + _alias(fitz.Document, "insertPage", "insert_page") + _alias(fitz.Document, "insertPDF", "insert_pdf") + _alias(fitz.Document, "isDirty", "is_dirty") + _alias(fitz.Document, "isFormPDF", "is_form_pdf") + _alias(fitz.Document, "isPDF", "is_pdf") + _alias(fitz.Document, "isReflowable", "is_reflowable") + _alias(fitz.Document, "isRepaired", "is_repaired") + _alias(fitz.Document, "isStream", "xref_is_stream") + _alias(fitz.Document, "is_stream", "xref_is_stream") + _alias(fitz.Document, "lastLocation", "last_location") + _alias(fitz.Document, "loadPage", "load_page") + _alias(fitz.Document, "makeBookmark", "make_bookmark") + _alias(fitz.Document, "metadataXML", "xref_xml_metadata") + _alias(fitz.Document, "movePage", "move_page") + _alias(fitz.Document, "needsPass", "needs_pass") + _alias(fitz.Document, "newPage", "new_page") + _alias(fitz.Document, "nextLocation", "next_location") + _alias(fitz.Document, "pageCount", "page_count") + _alias(fitz.Document, "pageCropBox", "page_cropbox") + _alias(fitz.Document, "pageXref", "page_xref") + _alias(fitz.Document, "PDFCatalog", "pdf_catalog") + _alias(fitz.Document, "PDFTrailer", "pdf_trailer") + _alias(fitz.Document, "previousLocation", "prev_location") + _alias(fitz.Document, "resolveLink", "resolve_link") + _alias(fitz.Document, "searchPageFor", "search_page_for") + _alias(fitz.Document, "setLanguage", "set_language") + _alias(fitz.Document, "setMetadata", "set_metadata") + _alias(fitz.Document, "setToC", "set_toc") + _alias(fitz.Document, "setXmlMetadata", "set_xml_metadata") + _alias(fitz.Document, "updateObject", "update_object") + _alias(fitz.Document, "updateStream", "update_stream") + _alias(fitz.Document, "xrefLength", "xref_length") + _alias(fitz.Document, "xrefObject", "xref_object") + _alias(fitz.Document, "xrefStream", "xref_stream") + _alias(fitz.Document, "xrefStreamRaw", "xref_stream_raw") + + # deprecated Page aliases + _alias(fitz.Page, "_isWrapped", "is_wrapped") + _alias(fitz.Page, "addCaretAnnot", "add_caret_annot") + _alias(fitz.Page, "addCircleAnnot", "add_circle_annot") + _alias(fitz.Page, "addFileAnnot", "add_file_annot") + _alias(fitz.Page, "addFreetextAnnot", "add_freetext_annot") + _alias(fitz.Page, "addHighlightAnnot", "add_highlight_annot") + _alias(fitz.Page, "addInkAnnot", "add_ink_annot") + _alias(fitz.Page, "addLineAnnot", "add_line_annot") + _alias(fitz.Page, "addPolygonAnnot", "add_polygon_annot") + _alias(fitz.Page, "addPolylineAnnot", "add_polyline_annot") + _alias(fitz.Page, "addRectAnnot", "add_rect_annot") + _alias(fitz.Page, "addRedactAnnot", "add_redact_annot") + _alias(fitz.Page, "addSquigglyAnnot", "add_squiggly_annot") + _alias(fitz.Page, "addStampAnnot", "add_stamp_annot") + _alias(fitz.Page, "addStrikeoutAnnot", "add_strikeout_annot") + _alias(fitz.Page, "addTextAnnot", "add_text_annot") + _alias(fitz.Page, "addUnderlineAnnot", "add_underline_annot") + _alias(fitz.Page, "addWidget", "add_widget") + _alias(fitz.Page, "cleanContents", "clean_contents") + _alias(fitz.Page, "CropBox", "cropbox") + _alias(fitz.Page, "CropBoxPosition", "cropbox_position") + _alias(fitz.Page, "deleteAnnot", "delete_annot") + _alias(fitz.Page, "deleteLink", "delete_link") + _alias(fitz.Page, "deleteWidget", "delete_widget") + _alias(fitz.Page, "derotationMatrix", "derotation_matrix") + _alias(fitz.Page, "drawBezier", "draw_bezier") + _alias(fitz.Page, "drawCircle", "draw_circle") + _alias(fitz.Page, "drawCurve", "draw_curve") + _alias(fitz.Page, "drawLine", "draw_line") + _alias(fitz.Page, "drawOval", "draw_oval") + _alias(fitz.Page, "drawPolyline", "draw_polyline") + _alias(fitz.Page, "drawQuad", "draw_quad") + _alias(fitz.Page, "drawRect", "draw_rect") + _alias(fitz.Page, "drawSector", "draw_sector") + _alias(fitz.Page, "drawSquiggle", "draw_squiggle") + _alias(fitz.Page, "drawZigzag", "draw_zigzag") + _alias(fitz.Page, "firstAnnot", "first_annot") + _alias(fitz.Page, "firstLink", "first_link") + _alias(fitz.Page, "firstWidget", "first_widget") + _alias(fitz.Page, "getContents", "get_contents") + _alias(fitz.Page, "getDisplayList", "get_displaylist") + _alias(fitz.Page, "getDrawings", "get_drawings") + _alias(fitz.Page, "getFontList", "get_fonts") + _alias(fitz.Page, "getImageBbox", "get_image_bbox") + _alias(fitz.Page, "getImageList", "get_images") + _alias(fitz.Page, "getLinks", "get_links") + _alias(fitz.Page, "getPixmap", "get_pixmap") + _alias(fitz.Page, "getSVGimage", "get_svg_image") + _alias(fitz.Page, "getText", "get_text") + _alias(fitz.Page, "getTextBlocks", "get_text_blocks") + _alias(fitz.Page, "getTextbox", "get_textbox") + _alias(fitz.Page, "getTextPage", "get_textpage") + _alias(fitz.Page, "getTextWords", "get_text_words") + _alias(fitz.Page, "insertFont", "insert_font") + _alias(fitz.Page, "insertImage", "insert_image") + _alias(fitz.Page, "insertLink", "insert_link") + _alias(fitz.Page, "insertText", "insert_text") + _alias(fitz.Page, "insertTextbox", "insert_textbox") + _alias(fitz.Page, "loadAnnot", "load_annot") + _alias(fitz.Page, "loadLinks", "load_links") + _alias(fitz.Page, "MediaBox", "mediabox") + _alias(fitz.Page, "MediaBoxSize", "mediabox_size") + _alias(fitz.Page, "newShape", "new_shape") + _alias(fitz.Page, "readContents", "read_contents") + _alias(fitz.Page, "rotationMatrix", "rotation_matrix") + _alias(fitz.Page, "searchFor", "search_for") + _alias(fitz.Page, "setCropBox", "set_cropbox") + _alias(fitz.Page, "setMediaBox", "set_mediabox") + _alias(fitz.Page, "setRotation", "set_rotation") + _alias(fitz.Page, "showPDFpage", "show_pdf_page") + _alias(fitz.Page, "transformationMatrix", "transformation_matrix") + _alias(fitz.Page, "updateLink", "update_link") + _alias(fitz.Page, "wrapContents", "wrap_contents") + _alias(fitz.Page, "writeText", "write_text") + + # deprecated Shape aliases + _alias(fitz.utils.Shape, "drawBezier", "draw_bezier") + _alias(fitz.utils.Shape, "drawCircle", "draw_circle") + _alias(fitz.utils.Shape, "drawCurve", "draw_curve") + _alias(fitz.utils.Shape, "drawLine", "draw_line") + _alias(fitz.utils.Shape, "drawOval", "draw_oval") + _alias(fitz.utils.Shape, "drawPolyline", "draw_polyline") + _alias(fitz.utils.Shape, "drawQuad", "draw_quad") + _alias(fitz.utils.Shape, "drawRect", "draw_rect") + _alias(fitz.utils.Shape, "drawSector", "draw_sector") + _alias(fitz.utils.Shape, "drawSquiggle", "draw_squiggle") + _alias(fitz.utils.Shape, "drawZigzag", "draw_zigzag") + _alias(fitz.utils.Shape, "insertText", "insert_text") + _alias(fitz.utils.Shape, "insertTextbox", "insert_textbox") + + # deprecated Annot aliases + _alias(fitz.Annot, "getText", "get_text") + _alias(fitz.Annot, "getTextbox", "get_textbox") + _alias(fitz.Annot, "fileGet", "get_file") + _alias(fitz.Annot, "fileUpd", "update_file") + _alias(fitz.Annot, "getPixmap", "get_pixmap") + _alias(fitz.Annot, "getTextPage", "get_textpage") + _alias(fitz.Annot, "lineEnds", "line_ends") + _alias(fitz.Annot, "setBlendMode", "set_blendmode") + _alias(fitz.Annot, "setBorder", "set_border") + _alias(fitz.Annot, "setColors", "set_colors") + _alias(fitz.Annot, "setFlags", "set_flags") + _alias(fitz.Annot, "setInfo", "set_info") + _alias(fitz.Annot, "setLineEnds", "set_line_ends") + _alias(fitz.Annot, "setName", "set_name") + _alias(fitz.Annot, "setOpacity", "set_opacity") + _alias(fitz.Annot, "setRect", "set_rect") + _alias(fitz.Annot, "setOC", "set_oc") + _alias(fitz.Annot, "soundGet", "get_sound") + + # deprecated TextWriter aliases + _alias(fitz.TextWriter, "writeText", "write_text") + _alias(fitz.TextWriter, "fillTextbox", "fill_textbox") + + # deprecated DisplayList aliases + _alias(fitz.DisplayList, "getPixmap", "get_pixmap") + _alias(fitz.DisplayList, "getTextPage", "get_textpage") + + # deprecated Pixmap aliases + _alias(fitz.Pixmap, "setAlpha", "set_alpha") + _alias(fitz.Pixmap, "gammaWith", "gamma_with") + _alias(fitz.Pixmap, "tintWith", "tint_with") + _alias(fitz.Pixmap, "clearWith", "clear_with") + _alias(fitz.Pixmap, "copyPixmap", "copy") + _alias(fitz.Pixmap, "getImageData", "tobytes") + _alias(fitz.Pixmap, "getPNGData", "tobytes") + _alias(fitz.Pixmap, "getPNGdata", "tobytes") + _alias(fitz.Pixmap, "writeImage", "save") + _alias(fitz.Pixmap, "writePNG", "save") + _alias(fitz.Pixmap, "pillowWrite", "pil_save") + _alias(fitz.Pixmap, "pillowData", "pil_tobytes") + _alias(fitz.Pixmap, "invertIRect", "invert_irect") + _alias(fitz.Pixmap, "setPixel", "set_pixel") + _alias(fitz.Pixmap, "setOrigin", "set_origin") + _alias(fitz.Pixmap, "setRect", "set_rect") + _alias(fitz.Pixmap, "setResolution", "set_dpi") + + # deprecated geometry aliases + _alias(fitz.Rect, "getArea", "get_area") + _alias(fitz.IRect, "getArea", "get_area") + _alias(fitz.Rect, "getRectArea", "get_area") + _alias(fitz.IRect, "getRectArea", "get_area") + _alias(fitz.Rect, "includePoint", "include_point") + _alias(fitz.IRect, "includePoint", "include_point") + _alias(fitz.Rect, "includeRect", "include_rect") + _alias(fitz.IRect, "includeRect", "include_rect") + _alias(fitz.Rect, "isInfinite", "is_infinite") + _alias(fitz.IRect, "isInfinite", "is_infinite") + _alias(fitz.Rect, "isEmpty", "is_empty") + _alias(fitz.IRect, "isEmpty", "is_empty") + _alias(fitz.Quad, "isEmpty", "is_empty") + _alias(fitz.Quad, "isRectangular", "is_rectangular") + _alias(fitz.Quad, "isConvex", "is_convex") + _alias(fitz.Matrix, "isRectilinear", "is_rectilinear") + _alias(fitz.Matrix, "preRotate", "prerotate") + _alias(fitz.Matrix, "preScale", "prescale") + _alias(fitz.Matrix, "preShear", "preshear") + _alias(fitz.Matrix, "preTranslate", "pretranslate") + + # deprecated other aliases + _alias(fitz.Outline, "isExternal", "is_external") + _alias(fitz.Outline, "isOpen", "is_open") + _alias(fitz.Link, "isExternal", "is_external") + _alias(fitz.Link, "setBorder", "set_border") + _alias(fitz.Link, "setColors", "set_colors") + _alias(fitz, "getPDFstr", "get_pdf_str") + _alias(fitz, "getPDFnow", "get_pdf_now") + _alias(fitz, "PaperSize", "paper_size") + _alias(fitz, "PaperRect", "paper_rect") + _alias(fitz, "paperSizes", "paper_sizes") + _alias(fitz, "ImageProperties", "image_profile") + _alias(fitz, "planishLine", "planish_line") + _alias(fitz, "getTextLength", "get_text_length") + _alias(fitz, "getTextlength", "get_text_length") + + +fitz.__doc__ = """ +PyMuPDF %s: Python bindings for the MuPDF %s library. +Version date: %s. +Built for Python %i.%i on %s (%i-bit). +""" % ( + fitz.VersionBind, + fitz.VersionFitz, + fitz.VersionDate, + sys.version_info[0], + sys.version_info[1], + sys.platform, + 64 if sys.maxsize > 2**32 else 32, +) + +if VersionBind.startswith("1.19"): # don't generate aliases after v1.19.* + restore_aliases() + +pdfcolor = dict( + [ + (k, (r / 255, g / 255, b / 255)) + for k, (r, g, b) in fitz.utils.getColorInfoDict().items() + ] +) diff --git a/fitz/__main__.py b/fitz/__main__.py new file mode 100644 index 0000000..1930594 --- /dev/null +++ b/fitz/__main__.py @@ -0,0 +1,1136 @@ +# ----------------------------------------------------------------------------- +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# Part of "PyMuPDF", Python bindings for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ----------------------------------------------------------------------------- +import argparse +import bisect +import os +import sys +import statistics +from typing import Dict, List, Set, Tuple + +import fitz +from fitz.fitz import ( + TEXT_INHIBIT_SPACES, + TEXT_PRESERVE_LIGATURES, + TEXT_PRESERVE_WHITESPACE, +) + +mycenter = lambda x: (" %s " % x).center(75, "-") + + +def recoverpix(doc, item): + """Return image for a given XREF.""" + x = item[0] # xref of PDF image + s = item[1] # xref of its /SMask + if s == 0: # no smask: use direct image output + return doc.extract_image(x) + + def getimage(pix): + if pix.colorspace.n != 4: + return pix + tpix = fitz.Pixmap(fitz.csRGB, pix) + return tpix + + # we need to reconstruct the alpha channel with the smask + pix1 = fitz.Pixmap(doc, x) + pix2 = fitz.Pixmap(doc, s) # create pixmap of the /SMask entry + + """Sanity check: + - both pixmaps must have the same rectangle + - both pixmaps must have alpha=0 + - pix2 must consist of 1 byte per pixel + """ + if not (pix1.irect == pix2.irect and pix1.alpha == pix2.alpha == 0 and pix2.n == 1): + print("Warning: unsupported /SMask %i for %i:" % (s, x)) + print(pix2) + pix2 = None + return getimage(pix1) # return the pixmap as is + + pix = fitz.Pixmap(pix1) # copy of pix1, with an alpha channel added + pix.set_alpha(pix2.samples) # treat pix2.samples as the alpha values + pix1 = pix2 = None # free temp pixmaps + + # we may need to adjust something for CMYK pixmaps here: + return getimage(pix) + + +def open_file(filename, password, show=False, pdf=True): + """Open and authenticate a document.""" + doc = fitz.open(filename) + if not doc.is_pdf and pdf is True: + sys.exit("this command supports PDF files only") + rc = -1 + if not doc.needs_pass: + return doc + if password: + rc = doc.authenticate(password) + if not rc: + sys.exit("authentication unsuccessful") + if show is True: + print("authenticated as %s" % "owner" if rc > 2 else "user") + else: + sys.exit("'%s' requires a password" % doc.name) + return doc + + +def print_dict(item): + """Print a Python dictionary.""" + l = max([len(k) for k in item.keys()]) + 1 + for k, v in item.items(): + msg = "%s: %s" % (k.rjust(l), v) + print(msg) + return + + +def print_xref(doc, xref): + """Print an object given by XREF number. + + Simulate the PDF source in "pretty" format. + For a stream also print its size. + """ + print("%i 0 obj" % xref) + xref_str = doc.xref_object(xref) + print(xref_str) + if doc.xref_is_stream(xref): + temp = xref_str.split() + try: + idx = temp.index("/Length") + 1 + size = temp[idx] + if size.endswith("0 R"): + size = "unknown" + except: + size = "unknown" + print("stream\n...%s bytes" % size) + print("endstream") + print("endobj") + + +def get_list(rlist, limit, what="page"): + """Transform a page / xref specification into a list of integers. + + Args + ---- + rlist: (str) the specification + limit: maximum number, i.e. number of pages, number of objects + what: a string to be used in error messages + Returns + ------- + A list of integers representing the specification. + """ + N = str(limit - 1) + rlist = rlist.replace("N", N).replace(" ", "") + rlist_arr = rlist.split(",") + out_list = [] + for seq, item in enumerate(rlist_arr): + n = seq + 1 + if item.isdecimal(): # a single integer + i = int(item) + if 1 <= i < limit: + out_list.append(int(item)) + else: + sys.exit("bad %s specification at item %i" % (what, n)) + continue + try: # this must be a range now, and all of the following must work: + i1, i2 = item.split("-") # will fail if not 2 items produced + i1 = int(i1) # will fail on non-integers + i2 = int(i2) + except: + sys.exit("bad %s range specification at item %i" % (what, n)) + + if not (1 <= i1 < limit and 1 <= i2 < limit): + sys.exit("bad %s range specification at item %i" % (what, n)) + + if i1 == i2: # just in case: a range of equal numbers + out_list.append(i1) + continue + + if i1 < i2: # first less than second + out_list += list(range(i1, i2 + 1)) + else: # first larger than second + out_list += list(range(i1, i2 - 1, -1)) + + return out_list + + +def show(args): + doc = open_file(args.input, args.password, True) + size = os.path.getsize(args.input) / 1024 + flag = "KB" + if size > 1000: + size /= 1024 + flag = "MB" + size = round(size, 1) + meta = doc.metadata + print( + "'%s', pages: %i, objects: %i, %g %s, %s, encryption: %s" + % ( + args.input, + doc.page_count, + doc.xref_length() - 1, + size, + flag, + meta["format"], + meta["encryption"], + ) + ) + n = doc.is_form_pdf + if n > 0: + s = doc.get_sigflags() + print( + "document contains %i root form fields and is %ssigned" + % (n, "not " if s != 3 else "") + ) + n = doc.embfile_count() + if n > 0: + print("document contains %i embedded files" % n) + print() + if args.catalog: + print(mycenter("PDF catalog")) + xref = doc.pdf_catalog() + print_xref(doc, xref) + print() + if args.metadata: + print(mycenter("PDF metadata")) + print_dict(doc.metadata) + print() + if args.xrefs: + print(mycenter("object information")) + xrefl = get_list(args.xrefs, doc.xref_length(), what="xref") + for xref in xrefl: + print_xref(doc, xref) + print() + if args.pages: + print(mycenter("page information")) + pagel = get_list(args.pages, doc.page_count + 1) + for pno in pagel: + n = pno - 1 + xref = doc.page_xref(n) + print("Page %i:" % pno) + print_xref(doc, xref) + print() + if args.trailer: + print(mycenter("PDF trailer")) + print(doc.pdf_trailer()) + print() + doc.close() + + +def clean(args): + doc = open_file(args.input, args.password, pdf=True) + encryption = args.encryption + encrypt = ("keep", "none", "rc4-40", "rc4-128", "aes-128", "aes-256").index( + encryption + ) + + if not args.pages: # simple cleaning + doc.save( + args.output, + garbage=args.garbage, + deflate=args.compress, + pretty=args.pretty, + clean=args.sanitize, + ascii=args.ascii, + linear=args.linear, + encryption=encrypt, + owner_pw=args.owner, + user_pw=args.user, + permissions=args.permission, + ) + return + + # create sub document from page numbers + pages = get_list(args.pages, doc.page_count + 1) + outdoc = fitz.open() + for pno in pages: + n = pno - 1 + outdoc.insert_pdf(doc, from_page=n, to_page=n) + outdoc.save( + args.output, + garbage=args.garbage, + deflate=args.compress, + pretty=args.pretty, + clean=args.sanitize, + ascii=args.ascii, + linear=args.linear, + encryption=encrypt, + owner_pw=args.owner, + user_pw=args.user, + permissions=args.permission, + ) + doc.close() + outdoc.close() + return + + +def doc_join(args): + """Join pages from several PDF documents.""" + doc_list = args.input # a list of input PDFs + doc = fitz.open() # output PDF + for src_item in doc_list: # process one input PDF + src_list = src_item.split(",") + password = src_list[1] if len(src_list) > 1 else None + src = open_file(src_list[0], password, pdf=True) + pages = ",".join(src_list[2:]) # get 'pages' specifications + if pages: # if anything there, retrieve a list of desired pages + page_list = get_list(",".join(src_list[2:]), src.page_count + 1) + else: # take all pages + page_list = range(1, src.page_count + 1) + for i in page_list: + doc.insert_pdf(src, from_page=i - 1, to_page=i - 1) # copy each source page + src.close() + + doc.save(args.output, garbage=4, deflate=True) + doc.close() + + +def embedded_copy(args): + """Copy embedded files between PDFs.""" + doc = open_file(args.input, args.password, pdf=True) + if not doc.can_save_incrementally() and ( + not args.output or args.output == args.input + ): + sys.exit("cannot save PDF incrementally") + src = open_file(args.source, args.pwdsource) + names = set(args.name) if args.name else set() + src_names = set(src.embfile_names()) + if names: + if not names <= src_names: + sys.exit("not all names are contained in source") + else: + names = src_names + if not names: + sys.exit("nothing to copy") + intersect = names & set(doc.embfile_names()) # any equal name already in target? + if intersect: + sys.exit("following names already exist in receiving PDF: %s" % str(intersect)) + + for item in names: + info = src.embfile_info(item) + buff = src.embfile_get(item) + doc.embfile_add( + item, + buff, + filename=info["filename"], + ufilename=info["ufilename"], + desc=info["desc"], + ) + print("copied entry '%s' from '%s'" % (item, src.name)) + src.close() + if args.output and args.output != args.input: + doc.save(args.output, garbage=3) + else: + doc.saveIncr() + doc.close() + + +def embedded_del(args): + """Delete an embedded file entry.""" + doc = open_file(args.input, args.password, pdf=True) + if not doc.can_save_incrementally() and ( + not args.output or args.output == args.input + ): + sys.exit("cannot save PDF incrementally") + + try: + doc.embfile_del(args.name) + except ValueError: + sys.exit("no such embedded file '%s'" % args.name) + if not args.output or args.output == args.input: + doc.save_incr() + else: + doc.save(args.output, garbage=1) + doc.close() + + +def embedded_get(args): + """Retrieve contents of an embedded file.""" + doc = open_file(args.input, args.password, pdf=True) + try: + stream = doc.embfile_get(args.name) + d = doc.embfile_info(args.name) + except ValueError: + sys.exit("no such embedded file '%s'" % args.name) + filename = args.output if args.output else d["filename"] + output = open(filename, "wb") + output.write(stream) + output.close() + print("saved entry '%s' as '%s'" % (args.name, filename)) + doc.close() + + +def embedded_add(args): + """Insert a new embedded file.""" + doc = open_file(args.input, args.password, pdf=True) + if not doc.can_save_incrementally() and ( + args.output is None or args.output == args.input + ): + sys.exit("cannot save PDF incrementally") + + try: + doc.embfile_del(args.name) + sys.exit("entry '%s' already exists" % args.name) + except: + pass + + if not os.path.exists(args.path) or not os.path.isfile(args.path): + sys.exit("no such file '%s'" % args.path) + stream = open(args.path, "rb").read() + filename = args.path + ufilename = filename + if not args.desc: + desc = filename + else: + desc = args.desc + doc.embfile_add( + args.name, stream, filename=filename, ufilename=ufilename, desc=desc + ) + if not args.output or args.output == args.input: + doc.saveIncr() + else: + doc.save(args.output, garbage=3) + doc.close() + + +def embedded_upd(args): + """Update contents or metadata of an embedded file.""" + doc = open_file(args.input, args.password, pdf=True) + if not doc.can_save_incrementally() and ( + args.output is None or args.output == args.input + ): + sys.exit("cannot save PDF incrementally") + + try: + doc.embfile_info(args.name) + except: + sys.exit("no such embedded file '%s'" % args.name) + + if ( + args.path is not None + and os.path.exists(args.path) + and os.path.isfile(args.path) + ): + stream = open(args.path, "rb").read() + else: + stream = None + + if args.filename: + filename = args.filename + else: + filename = None + + if args.ufilename: + ufilename = args.ufilename + elif args.filename: + ufilename = args.filename + else: + ufilename = None + + if args.desc: + desc = args.desc + else: + desc = None + + doc.embfile_upd( + args.name, stream, filename=filename, ufilename=ufilename, desc=desc + ) + if args.output is None or args.output == args.input: + doc.saveIncr() + else: + doc.save(args.output, garbage=3) + doc.close() + + +def embedded_list(args): + """List embedded files.""" + doc = open_file(args.input, args.password, pdf=True) + names = doc.embfile_names() + if args.name is not None: + if args.name not in names: + sys.exit("no such embedded file '%s'" % args.name) + else: + print() + print( + "printing 1 of %i embedded file%s:" + % (len(names), "s" if len(names) > 1 else "") + ) + print() + print_dict(doc.embfile_info(args.name)) + print() + return + if not names: + print("'%s' contains no embedded files" % doc.name) + return + if len(names) > 1: + msg = "'%s' contains the following %i embedded files" % (doc.name, len(names)) + else: + msg = "'%s' contains the following embedded file" % doc.name + print(msg) + print() + for name in names: + if not args.detail: + print(name) + continue + _ = doc.embfile_info(name) + print_dict(doc.embfile_info(name)) + print() + doc.close() + + +def extract_objects(args): + """Extract images and / or fonts from a PDF.""" + if not args.fonts and not args.images: + sys.exit("neither fonts nor images requested") + doc = open_file(args.input, args.password, pdf=True) + + if args.pages: + pages = get_list(args.pages, doc.page_count + 1) + else: + pages = range(1, doc.page_count + 1) + + if not args.output: + out_dir = os.path.abspath(os.curdir) + else: + out_dir = args.output + if not (os.path.exists(out_dir) and os.path.isdir(out_dir)): + sys.exit("output directory %s does not exist" % out_dir) + + font_xrefs = set() # already saved fonts + image_xrefs = set() # already saved images + + for pno in pages: + if args.fonts: + itemlist = doc.get_page_fonts(pno - 1) + for item in itemlist: + xref = item[0] + if xref not in font_xrefs: + font_xrefs.add(xref) + fontname, ext, _, buffer = doc.extract_font(xref) + if ext == "n/a" or not buffer: + continue + outname = os.path.join( + out_dir, f"{fontname.replace(' ', '-')}-{xref}.{ext}" + ) + outfile = open(outname, "wb") + outfile.write(buffer) + outfile.close() + buffer = None + if args.images: + itemlist = doc.get_page_images(pno - 1) + for item in itemlist: + xref = item[0] + if xref not in image_xrefs: + image_xrefs.add(xref) + pix = recoverpix(doc, item) + if type(pix) is dict: + ext = pix["ext"] + imgdata = pix["image"] + outname = os.path.join(out_dir, "img-%i.%s" % (xref, ext)) + outfile = open(outname, "wb") + outfile.write(imgdata) + outfile.close() + else: + outname = os.path.join(out_dir, "img-%i.png" % xref) + pix2 = ( + pix + if pix.colorspace.n < 4 + else fitz.Pixmap(fitz.csRGB, pix) + ) + pix2.save(outname) + + if args.fonts: + print("saved %i fonts to '%s'" % (len(font_xrefs), out_dir)) + if args.images: + print("saved %i images to '%s'" % (len(image_xrefs), out_dir)) + doc.close() + + +def page_simple(page, textout, GRID, fontsize, noformfeed, skip_empty, flags): + eop = b"\n" if noformfeed else bytes([12]) + text = page.get_text("text", flags=flags) + if not text: + if not skip_empty: + textout.write(eop) # write formfeed + return + textout.write(text.encode("utf8", errors="surrogatepass")) + textout.write(eop) + return + + +def page_blocksort(page, textout, GRID, fontsize, noformfeed, skip_empty, flags): + eop = b"\n" if noformfeed else bytes([12]) + blocks = page.get_text("blocks", flags=flags) + if blocks == []: + if not skip_empty: + textout.write(eop) # write formfeed + return + blocks.sort(key=lambda b: (b[3], b[0])) + for b in blocks: + textout.write(b[4].encode("utf8", errors="surrogatepass")) + textout.write(eop) + return + + +def page_layout(page, textout, GRID, fontsize, noformfeed, skip_empty, flags): + eop = b"\n" if noformfeed else bytes([12]) + + # -------------------------------------------------------------------- + def find_line_index(values: List[int], value: int) -> int: + """Find the right row coordinate. + + Args: + values: (list) y-coordinates of rows. + value: (int) lookup for this value (y-origin of char). + Returns: + y-ccordinate of appropriate line for value. + """ + i = bisect.bisect_right(values, value) + if i: + return values[i - 1] + raise RuntimeError("Line for %g not found in %s" % (value, values)) + + # -------------------------------------------------------------------- + def curate_rows(rows: Set[int], GRID) -> List: + rows = list(rows) + rows.sort() # sort ascending + nrows = [rows[0]] + for h in rows[1:]: + if h >= nrows[-1] + GRID: # only keep significant differences + nrows.append(h) + return nrows # curated list of line bottom coordinates + + def process_blocks(blocks: List[Dict], page: fitz.Page): + rows = set() + page_width = page.rect.width + page_height = page.rect.height + rowheight = page_height + left = page_width + right = 0 + chars = [] + for block in blocks: + for line in block["lines"]: + if line["dir"] != (1, 0): # ignore non-horizontal text + continue + x0, y0, x1, y1 = line["bbox"] + if y1 < 0 or y0 > page.rect.height: # ignore if outside CropBox + continue + # upd row height + height = y1 - y0 + + if rowheight > height: + rowheight = height + for span in line["spans"]: + if span["size"] <= fontsize: + continue + for c in span["chars"]: + x0, _, x1, _ = c["bbox"] + cwidth = x1 - x0 + ox, oy = c["origin"] + oy = int(round(oy)) + rows.add(oy) + ch = c["c"] + if left > ox and ch != " ": + left = ox # update left coordinate + if right < x1: + right = x1 # update right coordinate + # handle ligatures: + if cwidth == 0 and chars != []: # potential ligature + old_ch, old_ox, old_oy, old_cwidth = chars[-1] + if old_oy == oy: # ligature + if old_ch != chr(0xFB00): # previous "ff" char lig? + lig = joinligature(old_ch + ch) # no + # convert to one of the 3-char ligatures: + elif ch == "i": + lig = chr(0xFB03) # "ffi" + elif ch == "l": + lig = chr(0xFB04) # "ffl" + else: # something wrong, leave old char in place + lig = old_ch + chars[-1] = (lig, old_ox, old_oy, old_cwidth) + continue + chars.append((ch, ox, oy, cwidth)) # all chars on page + return chars, rows, left, right, rowheight + + def joinligature(lig: str) -> str: + """Return ligature character for a given pair / triple of characters. + + Args: + lig: (str) 2/3 characters, e.g. "ff" + Returns: + Ligature, e.g. "ff" -> chr(0xFB00) + """ + + if lig == "ff": + return chr(0xFB00) + elif lig == "fi": + return chr(0xFB01) + elif lig == "fl": + return chr(0xFB02) + elif lig == "ffi": + return chr(0xFB03) + elif lig == "ffl": + return chr(0xFB04) + elif lig == "ft": + return chr(0xFB05) + elif lig == "st": + return chr(0xFB06) + return lig + + # -------------------------------------------------------------------- + def make_textline(left, slot, minslot, lchars): + """Produce the text of one output line. + + Args: + left: (float) left most coordinate used on page + slot: (float) avg width of one character in any font in use. + minslot: (float) min width for the characters in this line. + chars: (list[tuple]) characters of this line. + Returns: + text: (str) text string for this line + """ + text = "" # we output this + old_char = "" + old_x1 = 0 # end coordinate of last char + old_ox = 0 # x-origin of last char + if minslot <= fitz.EPSILON: + raise RuntimeError("program error: minslot too small = %g" % minslot) + + for c in lchars: # loop over characters + char, ox, _, cwidth = c + ox = ox - left # its (relative) start coordinate + x1 = ox + cwidth # ending coordinate + + # eliminate overprint effect + if old_char == char and ox - old_ox <= cwidth * 0.2: + continue + + # omit spaces overlapping previous char + if char == " " and (old_x1 - ox) / cwidth > 0.8: + continue + + old_char = char + # close enough to previous? + if ox < old_x1 + minslot: # assume char adjacent to previous + text += char # append to output + old_x1 = x1 # new end coord + old_ox = ox # new origin.x + continue + + # else next char starts after some gap: + # fill in right number of spaces, so char is positioned + # in the right slot of the line + if char == " ": # rest relevant for non-space only + continue + delta = int(ox / slot) - len(text) + if ox > old_x1 and delta > 1: + text += " " * delta + # now append char + text += char + old_x1 = x1 # new end coordinate + old_ox = ox # new origin + return text.rstrip() + + # extract page text by single characters ("rawdict") + blocks = page.get_text("rawdict", flags=flags)["blocks"] + chars, rows, left, right, rowheight = process_blocks(blocks, page) + + if chars == []: + if not skip_empty: + textout.write(eop) # write formfeed + return + # compute list of line coordinates - ignoring small (GRID) differences + rows = curate_rows(rows, GRID) + + # sort all chars by x-coordinates, so every line will receive char info, + # sorted from left to right. + chars.sort(key=lambda c: c[1]) + + # populate the lines with their char info + lines = {} # key: y1-ccordinate, value: char list + for c in chars: + _, _, oy, _ = c + y = find_line_index(rows, oy) # y-coord of the right line + lchars = lines.get(y, []) # read line chars so far + lchars.append(c) # append this char + lines[y] = lchars # write back to line + + # ensure line coordinates are ascending + keys = list(lines.keys()) + keys.sort() + + # ------------------------------------------------------------------------- + # Compute "char resolution" for the page: the char width corresponding to + # 1 text char position on output - call it 'slot'. + # For each line, compute median of its char widths. The minimum across all + # lines is 'slot'. + # The minimum char width of each line is used to determine if spaces must + # be inserted in between two characters. + # ------------------------------------------------------------------------- + slot = right - left + minslots = {} + for k in keys: + lchars = lines[k] + ccount = len(lchars) + if ccount < 2: + minslots[k] = 1 + continue + widths = [c[3] for c in lchars] + widths.sort() + this_slot = statistics.median(widths) # take median value + if this_slot < slot: + slot = this_slot + minslots[k] = widths[0] + + # compute line advance in text output + rowheight = rowheight * (rows[-1] - rows[0]) / (rowheight * len(rows)) * 1.2 + rowpos = rows[0] # first line positioned here + textout.write(b"\n") + for k in keys: # walk through the lines + while rowpos < k: # honor distance between lines + textout.write(b"\n") + rowpos += rowheight + text = make_textline(left, slot, minslots[k], lines[k]) + textout.write((text + "\n").encode("utf8", errors="surrogatepass")) + rowpos = k + rowheight + + textout.write(eop) # write formfeed + + +def gettext(args): + doc = open_file(args.input, args.password, pdf=False) + pagel = get_list(args.pages, doc.page_count + 1) + output = args.output + if output == None: + filename, _ = os.path.splitext(doc.name) + output = filename + ".txt" + textout = open(output, "wb") + flags = TEXT_PRESERVE_LIGATURES | TEXT_PRESERVE_WHITESPACE + if args.convert_white: + flags ^= TEXT_PRESERVE_WHITESPACE + if args.noligatures: + flags ^= TEXT_PRESERVE_LIGATURES + if args.extra_spaces: + flags ^= TEXT_INHIBIT_SPACES + func = { + "simple": page_simple, + "blocks": page_blocksort, + "layout": page_layout, + } + for pno in pagel: + page = doc[pno - 1] + func[args.mode]( + page, + textout, + args.grid, + args.fontsize, + args.noformfeed, + args.skip_empty, + flags=flags, + ) + + textout.close() + + +def main(): + """Define command configurations.""" + parser = argparse.ArgumentParser( + prog="fitz", + description=mycenter("Basic PyMuPDF Functions"), + ) + subps = parser.add_subparsers( + title="Subcommands", help="Enter 'command -h' for subcommand specific help" + ) + + # ------------------------------------------------------------------------- + # 'show' command + # ------------------------------------------------------------------------- + ps_show = subps.add_parser("show", description=mycenter("display PDF information")) + ps_show.add_argument("input", type=str, help="PDF filename") + ps_show.add_argument("-password", help="password") + ps_show.add_argument("-catalog", action="store_true", help="show PDF catalog") + ps_show.add_argument("-trailer", action="store_true", help="show PDF trailer") + ps_show.add_argument("-metadata", action="store_true", help="show PDF metadata") + ps_show.add_argument( + "-xrefs", type=str, help="show selected objects, format: 1,5-7,N" + ) + ps_show.add_argument( + "-pages", type=str, help="show selected pages, format: 1,5-7,50-N" + ) + ps_show.set_defaults(func=show) + + # ------------------------------------------------------------------------- + # 'clean' command + # ------------------------------------------------------------------------- + ps_clean = subps.add_parser( + "clean", description=mycenter("optimize PDF, or create sub-PDF if pages given") + ) + ps_clean.add_argument("input", type=str, help="PDF filename") + ps_clean.add_argument("output", type=str, help="output PDF filename") + ps_clean.add_argument("-password", help="password") + + ps_clean.add_argument( + "-encryption", + help="encryption method", + choices=("keep", "none", "rc4-40", "rc4-128", "aes-128", "aes-256"), + default="none", + ) + + ps_clean.add_argument("-owner", type=str, help="owner password") + ps_clean.add_argument("-user", type=str, help="user password") + + ps_clean.add_argument( + "-garbage", + type=int, + help="garbage collection level", + choices=range(5), + default=0, + ) + + ps_clean.add_argument( + "-compress", + action="store_true", + default=False, + help="compress (deflate) output", + ) + + ps_clean.add_argument( + "-ascii", action="store_true", default=False, help="ASCII encode binary data" + ) + + ps_clean.add_argument( + "-linear", + action="store_true", + default=False, + help="format for fast web display", + ) + + ps_clean.add_argument( + "-permission", type=int, default=-1, help="integer with permission levels" + ) + + ps_clean.add_argument( + "-sanitize", + action="store_true", + default=False, + help="sanitize / clean contents", + ) + ps_clean.add_argument( + "-pretty", action="store_true", default=False, help="prettify PDF structure" + ) + ps_clean.add_argument( + "-pages", help="output selected pages pages, format: 1,5-7,50-N" + ) + ps_clean.set_defaults(func=clean) + + # ------------------------------------------------------------------------- + # 'join' command + # ------------------------------------------------------------------------- + ps_join = subps.add_parser( + "join", + description=mycenter("join PDF documents"), + epilog="specify each input as 'filename[,password[,pages]]'", + ) + ps_join.add_argument("input", nargs="*", help="input filenames") + ps_join.add_argument("-output", required=True, help="output filename") + ps_join.set_defaults(func=doc_join) + + # ------------------------------------------------------------------------- + # 'extract' command + # ------------------------------------------------------------------------- + ps_extract = subps.add_parser( + "extract", description=mycenter("extract images and fonts to disk") + ) + ps_extract.add_argument("input", type=str, help="PDF filename") + ps_extract.add_argument("-images", action="store_true", help="extract images") + ps_extract.add_argument("-fonts", action="store_true", help="extract fonts") + ps_extract.add_argument( + "-output", help="folder to receive output, defaults to current" + ) + ps_extract.add_argument("-password", help="password") + ps_extract.add_argument( + "-pages", type=str, help="consider these pages only, format: 1,5-7,50-N" + ) + ps_extract.set_defaults(func=extract_objects) + + # ------------------------------------------------------------------------- + # 'embed-info' + # ------------------------------------------------------------------------- + ps_show = subps.add_parser( + "embed-info", description=mycenter("list embedded files") + ) + ps_show.add_argument("input", help="PDF filename") + ps_show.add_argument("-name", help="if given, report only this one") + ps_show.add_argument("-detail", action="store_true", help="detail information") + ps_show.add_argument("-password", help="password") + ps_show.set_defaults(func=embedded_list) + + # ------------------------------------------------------------------------- + # 'embed-add' command + # ------------------------------------------------------------------------- + ps_embed_add = subps.add_parser( + "embed-add", description=mycenter("add embedded file") + ) + ps_embed_add.add_argument("input", help="PDF filename") + ps_embed_add.add_argument("-password", help="password") + ps_embed_add.add_argument( + "-output", help="output PDF filename, incremental save if none" + ) + ps_embed_add.add_argument("-name", required=True, help="name of new entry") + ps_embed_add.add_argument("-path", required=True, help="path to data for new entry") + ps_embed_add.add_argument("-desc", help="description of new entry") + ps_embed_add.set_defaults(func=embedded_add) + + # ------------------------------------------------------------------------- + # 'embed-del' command + # ------------------------------------------------------------------------- + ps_embed_del = subps.add_parser( + "embed-del", description=mycenter("delete embedded file") + ) + ps_embed_del.add_argument("input", help="PDF filename") + ps_embed_del.add_argument("-password", help="password") + ps_embed_del.add_argument( + "-output", help="output PDF filename, incremental save if none" + ) + ps_embed_del.add_argument("-name", required=True, help="name of entry to delete") + ps_embed_del.set_defaults(func=embedded_del) + + # ------------------------------------------------------------------------- + # 'embed-upd' command + # ------------------------------------------------------------------------- + ps_embed_upd = subps.add_parser( + "embed-upd", + description=mycenter("update embedded file"), + epilog="except '-name' all parameters are optional", + ) + ps_embed_upd.add_argument("input", help="PDF filename") + ps_embed_upd.add_argument("-name", required=True, help="name of entry") + ps_embed_upd.add_argument("-password", help="password") + ps_embed_upd.add_argument( + "-output", help="Output PDF filename, incremental save if none" + ) + ps_embed_upd.add_argument("-path", help="path to new data for entry") + ps_embed_upd.add_argument("-filename", help="new filename to store in entry") + ps_embed_upd.add_argument( + "-ufilename", help="new unicode filename to store in entry" + ) + ps_embed_upd.add_argument("-desc", help="new description to store in entry") + ps_embed_upd.set_defaults(func=embedded_upd) + + # ------------------------------------------------------------------------- + # 'embed-extract' command + # ------------------------------------------------------------------------- + ps_embed_extract = subps.add_parser( + "embed-extract", description=mycenter("extract embedded file to disk") + ) + ps_embed_extract.add_argument("input", type=str, help="PDF filename") + ps_embed_extract.add_argument("-name", required=True, help="name of entry") + ps_embed_extract.add_argument("-password", help="password") + ps_embed_extract.add_argument( + "-output", help="output filename, default is stored name" + ) + ps_embed_extract.set_defaults(func=embedded_get) + + # ------------------------------------------------------------------------- + # 'embed-copy' command + # ------------------------------------------------------------------------- + ps_embed_copy = subps.add_parser( + "embed-copy", description=mycenter("copy embedded files between PDFs") + ) + ps_embed_copy.add_argument("input", type=str, help="PDF to receive embedded files") + ps_embed_copy.add_argument("-password", help="password of input") + ps_embed_copy.add_argument( + "-output", help="output PDF, incremental save to 'input' if omitted" + ) + ps_embed_copy.add_argument( + "-source", required=True, help="copy embedded files from here" + ) + ps_embed_copy.add_argument("-pwdsource", help="password of 'source' PDF") + ps_embed_copy.add_argument( + "-name", nargs="*", help="restrict copy to these entries" + ) + ps_embed_copy.set_defaults(func=embedded_copy) + + # ------------------------------------------------------------------------- + # 'textlayout' command + # ------------------------------------------------------------------------- + ps_gettext = subps.add_parser( + "gettext", description=mycenter("extract text in various formatting modes") + ) + ps_gettext.add_argument("input", type=str, help="input document filename") + ps_gettext.add_argument("-password", help="password for input document") + ps_gettext.add_argument( + "-mode", + type=str, + help="mode: simple, block sort, or layout (default)", + choices=("simple", "blocks", "layout"), + default="layout", + ) + ps_gettext.add_argument( + "-pages", + type=str, + help="select pages, format: 1,5-7,50-N", + default="1-N", + ) + ps_gettext.add_argument( + "-noligatures", + action="store_true", + help="expand ligature characters (default False)", + default=False, + ) + ps_gettext.add_argument( + "-convert-white", + action="store_true", + help="convert whitespace characters to white (default False)", + default=False, + ) + ps_gettext.add_argument( + "-extra-spaces", + action="store_true", + help="fill gaps with spaces (default False)", + default=False, + ) + ps_gettext.add_argument( + "-noformfeed", + action="store_true", + help="write linefeeds, no formfeeds (default False)", + default=False, + ) + ps_gettext.add_argument( + "-skip-empty", + action="store_true", + help="suppress pages with no text (default False)", + default=False, + ) + ps_gettext.add_argument( + "-output", + help="store text in this file (default inputfilename.txt)", + ) + ps_gettext.add_argument( + "-grid", + type=float, + help="merge lines if closer than this (default 2)", + default=2, + ) + ps_gettext.add_argument( + "-fontsize", + type=float, + help="only include text with a larger fontsize (default 3)", + default=3, + ) + ps_gettext.set_defaults(func=gettext) + + # ------------------------------------------------------------------------- + # start program + # ------------------------------------------------------------------------- + args = parser.parse_args() # create parameter arguments class + if not hasattr(args, "func"): # no function selected + parser.print_help() # so print top level help + else: + args.func(args) # execute requested command + + +if __name__ == "__main__": + main() diff --git a/fitz/fitz.i b/fitz/fitz.i new file mode 100644 index 0000000..c342ce5 --- /dev/null +++ b/fitz/fitz.i @@ -0,0 +1,15116 @@ +%module fitz +%pythonbegin %{ +%} +//------------------------------------------------------------------------ +// SWIG macros: handle fitz exceptions +//------------------------------------------------------------------------ +%define FITZEXCEPTION(meth, cond) +%exception meth +{ + $action + if (cond) { + return JM_ReturnException(gctx); + } +} +%enddef + + +%define FITZEXCEPTION2(meth, cond) +%exception meth +{ + $action + if (cond) { + const char *msg = fz_caught_message(gctx); + if (strcmp(msg, MSG_BAD_FILETYPE) == 0) { + PyErr_SetString(PyExc_ValueError, msg); + } else { + PyErr_SetString(JM_Exc_FileDataError, MSG_BAD_DOCUMENT); + } + return NULL; + } +} +%enddef + +//------------------------------------------------------------------------ +// SWIG macro: check that a document is not closed / encrypted +//------------------------------------------------------------------------ +%define CLOSECHECK(meth, doc) +%pythonprepend meth %{doc +if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted")%} +%enddef + +%define CLOSECHECK0(meth, doc) +%pythonprepend meth%{doc +if self.is_closed: + raise ValueError("document closed")%} +%enddef + +//------------------------------------------------------------------------ +// SWIG macro: check if object has a valid parent +//------------------------------------------------------------------------ +%define PARENTCHECK(meth, doc) +%pythonprepend meth %{doc +CheckParent(self)%} +%enddef + + +//------------------------------------------------------------------------ +// SWIG macro: ensure object still exists +//------------------------------------------------------------------------ +%define ENSURE_OWNERSHIP(meth, doc) +%pythonprepend meth %{doc +EnsureOwnership(self)%} +%enddef + +%include "mupdf/fitz/version.h" + +%{ +#define MEMDEBUG 0 +#if MEMDEBUG == 1 + #define DEBUGMSG1(x) PySys_WriteStderr("[DEBUG] free %s ", x) + #define DEBUGMSG2 PySys_WriteStderr("... done!\n") +#else + #define DEBUGMSG1(x) + #define DEBUGMSG2 +#endif + +#ifndef FLT_EPSILON + #define FLT_EPSILON 1e-5 +#endif + +#define SWIG_FILE_WITH_INIT + +// JM_MEMORY controls what allocators we tell MuPDF to use when we call +// fz_new_context(): +// +// JM_MEMORY=0: MuPDF uses malloc()/free(). +// JM_MEMORY=1: MuPDF uses PyMem_Malloc()/PyMem_Free(). +// +// There are also a small number of places where we call malloc() or +// PyMem_Malloc() ourselves, depending on JM_MEMORY. +// +#define JM_MEMORY 0 + +#if JM_MEMORY == 1 + #define JM_Alloc(type, len) PyMem_New(type, len) + #define JM_Free(x) PyMem_Del(x) +#else + #define JM_Alloc(type, len) (type *) malloc(sizeof(type)*len) + #define JM_Free(x) free(x) +#endif + +#define EMPTY_STRING PyUnicode_FromString("") +#define EXISTS(x) (x != NULL && PyObject_IsTrue(x)==1) +#define RAISEPY(context, msg, exc) {JM_Exc_CurrentException=exc; fz_throw(context, FZ_ERROR_GENERIC, msg);} +#define ASSERT_PDF(cond) if (cond == NULL) RAISEPY(gctx, MSG_IS_NO_PDF, PyExc_RuntimeError) +#define ENSURE_OPERATION(ctx, pdf) if (!JM_have_operation(ctx, pdf)) RAISEPY(ctx, "No journalling operation started", PyExc_RuntimeError) +#define INRANGE(v, low, high) ((low) <= v && v <= (high)) +#define JM_BOOL(x) PyBool_FromLong((long) (x)) +#define JM_PyErr_Clear if (PyErr_Occurred()) PyErr_Clear() + +#define JM_StrAsChar(x) (char *)PyUnicode_AsUTF8(x) +#define JM_BinFromChar(x) PyBytes_FromString(x) +#define JM_BinFromCharSize(x, y) PyBytes_FromStringAndSize(x, (Py_ssize_t) y) + +#include +#include +#include +// freetype includes >> -------------------------------------------------- +#include +#include FT_FREETYPE_H +#ifdef FT_FONT_FORMATS_H +#include FT_FONT_FORMATS_H +#else +#include FT_XFREE86_H +#endif +#include FT_TRUETYPE_TABLES_H + +#ifndef FT_SFNT_HEAD +#define FT_SFNT_HEAD ft_sfnt_head +#endif +// << freetype includes -------------------------------------------------- + +void JM_delete_widget(fz_context *ctx, pdf_page *page, pdf_annot *annot); +static void JM_get_page_labels(fz_context *ctx, PyObject *liste, pdf_obj *nums); +static int DICT_SETITEMSTR_DROP(PyObject *dict, const char *key, PyObject *value); +static int LIST_APPEND_DROP(PyObject *list, PyObject *item); +static int LIST_APPEND_DROP(PyObject *list, PyObject *item); +static fz_irect JM_irect_from_py(PyObject *r); +static fz_matrix JM_matrix_from_py(PyObject *m); +static fz_point JM_normalize_vector(float x, float y); +static fz_point JM_point_from_py(PyObject *p); +static fz_quad JM_quad_from_py(PyObject *r); +static fz_rect JM_rect_from_py(PyObject *r); +static int JM_FLOAT_ITEM(PyObject *obj, Py_ssize_t idx, double *result); +static int JM_INT_ITEM(PyObject *obj, Py_ssize_t idx, int *result); +static PyObject *JM_py_from_irect(fz_irect r); +static PyObject *JM_py_from_matrix(fz_matrix m); +static PyObject *JM_py_from_point(fz_point p); +static PyObject *JM_py_from_quad(fz_quad q); +static PyObject *JM_py_from_rect(fz_rect r); +static void show(const char* prefix, PyObject* obj); + + +// additional headers ---------------------------------------------- +pdf_obj *pdf_lookup_page_loc(fz_context *ctx, pdf_document *doc, int needle, pdf_obj **parentp, int *indexp); +fz_pixmap *fz_scale_pixmap(fz_context *ctx, fz_pixmap *src, float x, float y, float w, float h, const fz_irect *clip); +int fz_pixmap_size(fz_context *ctx, fz_pixmap *src); +void fz_subsample_pixmap(fz_context *ctx, fz_pixmap *tile, int factor); +void fz_copy_pixmap_rect(fz_context *ctx, fz_pixmap *dest, fz_pixmap *src, fz_irect b, const fz_default_colorspaces *default_cs); +static const float JM_font_ascender(fz_context *ctx, fz_font *font); +static const float JM_font_descender(fz_context *ctx, fz_font *font); +void fz_write_pixmap_as_jpeg(fz_context *ctx, fz_output *out, fz_pixmap *pix, int jpg_quality); +// end of additional headers -------------------------------------------- + +static PyObject *JM_mupdf_warnings_store; +static int JM_mupdf_show_errors; +static int JM_mupdf_show_warnings; +static PyObject *JM_Exc_FileDataError; +static PyObject *JM_Exc_CurrentException; +%} + +//------------------------------------------------------------------------ +// global context +//------------------------------------------------------------------------ +%init %{ + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + /* Stop Memento backtraces if we reach the Python interpreter. + `cfunction_call()` isn't the only way that Python calls C though, so we + might need extra calls to Memento_addBacktraceLimitFnname(). */ + Memento_addBacktraceLimitFnname("cfunction_call"); + #endif + + /* + We end up with Memento leaks from fz_new_context()'s allocs even when our + atexit handler calls fz_drop_context(), so remove these from Memento's + accounting. + */ + Memento_startLeaking(); +#if JM_MEMORY == 1 + gctx = fz_new_context(&JM_Alloc_Context, NULL, FZ_STORE_DEFAULT); +#else + gctx = fz_new_context(NULL, NULL, FZ_STORE_DEFAULT); +#endif + Memento_stopLeaking(); + if(!gctx) + { + PyErr_SetString(PyExc_RuntimeError, "Fatal error: cannot create global context."); + return NULL; + } + fz_register_document_handlers(gctx); + +//------------------------------------------------------------------------ +// START redirect stdout/stderr +//------------------------------------------------------------------------ +JM_mupdf_warnings_store = PyList_New(0); +JM_mupdf_show_errors = 1; +JM_mupdf_show_warnings = 0; +char user[] = "PyMuPDF"; +fz_set_warning_callback(gctx, JM_mupdf_warning, &user); +fz_set_error_callback(gctx, JM_mupdf_error, &user); +JM_Exc_FileDataError = NULL; +JM_Exc_CurrentException = PyExc_RuntimeError; +//------------------------------------------------------------------------ +// STOP redirect stdout/stderr +//------------------------------------------------------------------------ +// init global constants +//------------------------------------------------------------------------ +dictkey_align = PyUnicode_InternFromString("align"); +dictkey_ascender = PyUnicode_InternFromString("ascender"); +dictkey_bbox = PyUnicode_InternFromString("bbox"); +dictkey_blocks = PyUnicode_InternFromString("blocks"); +dictkey_bpc = PyUnicode_InternFromString("bpc"); +dictkey_c = PyUnicode_InternFromString("c"); +dictkey_chars = PyUnicode_InternFromString("chars"); +dictkey_color = PyUnicode_InternFromString("color"); +dictkey_colorspace = PyUnicode_InternFromString("colorspace"); +dictkey_content = PyUnicode_InternFromString("content"); +dictkey_creationDate = PyUnicode_InternFromString("creationDate"); +dictkey_cs_name = PyUnicode_InternFromString("cs-name"); +dictkey_da = PyUnicode_InternFromString("da"); +dictkey_dashes = PyUnicode_InternFromString("dashes"); +dictkey_desc = PyUnicode_InternFromString("desc"); +dictkey_desc = PyUnicode_InternFromString("descender"); +dictkey_descender = PyUnicode_InternFromString("descender"); +dictkey_dir = PyUnicode_InternFromString("dir"); +dictkey_effect = PyUnicode_InternFromString("effect"); +dictkey_ext = PyUnicode_InternFromString("ext"); +dictkey_filename = PyUnicode_InternFromString("filename"); +dictkey_fill = PyUnicode_InternFromString("fill"); +dictkey_flags = PyUnicode_InternFromString("flags"); +dictkey_font = PyUnicode_InternFromString("font"); +dictkey_glyph = PyUnicode_InternFromString("glyph"); +dictkey_height = PyUnicode_InternFromString("height"); +dictkey_id = PyUnicode_InternFromString("id"); +dictkey_image = PyUnicode_InternFromString("image"); +dictkey_items = PyUnicode_InternFromString("items"); +dictkey_length = PyUnicode_InternFromString("length"); +dictkey_lines = PyUnicode_InternFromString("lines"); +dictkey_matrix = PyUnicode_InternFromString("transform"); +dictkey_modDate = PyUnicode_InternFromString("modDate"); +dictkey_name = PyUnicode_InternFromString("name"); +dictkey_number = PyUnicode_InternFromString("number"); +dictkey_origin = PyUnicode_InternFromString("origin"); +dictkey_rect = PyUnicode_InternFromString("rect"); +dictkey_size = PyUnicode_InternFromString("size"); +dictkey_smask = PyUnicode_InternFromString("smask"); +dictkey_spans = PyUnicode_InternFromString("spans"); +dictkey_stroke = PyUnicode_InternFromString("stroke"); +dictkey_style = PyUnicode_InternFromString("style"); +dictkey_subject = PyUnicode_InternFromString("subject"); +dictkey_text = PyUnicode_InternFromString("text"); +dictkey_title = PyUnicode_InternFromString("title"); +dictkey_type = PyUnicode_InternFromString("type"); +dictkey_ufilename = PyUnicode_InternFromString("ufilename"); +dictkey_width = PyUnicode_InternFromString("width"); +dictkey_wmode = PyUnicode_InternFromString("wmode"); +dictkey_xref = PyUnicode_InternFromString("xref"); +dictkey_xres = PyUnicode_InternFromString("xres"); +dictkey_yres = PyUnicode_InternFromString("yres"); + +atexit( cleanup); +%} + +%header %{ +fz_context *gctx; + +static void cleanup() +{ + fz_drop_context( gctx); +} + +static int JM_UNIQUE_ID = 0; + +struct DeviceWrapper { + fz_device *device; + fz_display_list *list; +}; +%} + +//------------------------------------------------------------------------ +// include version information and several other helpers +//------------------------------------------------------------------------ +%pythoncode %{ +import sys +import io +import math +import os +import weakref +import hashlib +import typing +import binascii +import re +import tarfile +import zipfile +import pathlib + +TESSDATA_PREFIX = os.getenv("TESSDATA_PREFIX") +point_like = "point_like" +rect_like = "rect_like" +matrix_like = "matrix_like" +quad_like = "quad_like" +AnyType = typing.Any +OptInt = typing.Union[int, None] +OptFloat = typing.Optional[float] +OptStr = typing.Optional[str] +OptDict = typing.Optional[dict] +OptBytes = typing.Optional[typing.ByteString] +OptSeq = typing.Optional[typing.Sequence] + +try: + from pymupdf_fonts import fontdescriptors, fontbuffers + + fitz_fontdescriptors = fontdescriptors.copy() + for k in fitz_fontdescriptors.keys(): + fitz_fontdescriptors[k]["loader"] = fontbuffers[k] + del fontdescriptors, fontbuffers +except ImportError: + fitz_fontdescriptors = {} +%} +%include version.i +%include helper-git-versions.i +%include helper-defines.i +%include helper-globals.i +%include helper-geo-c.i +%include helper-other.i +%include helper-pixmap.i +%include helper-geo-py.i +%include helper-annot.i +%include helper-fields.i +%include helper-python.i +%include helper-portfolio.i +%include helper-select.i +%include helper-stext.i +%include helper-xobject.i +%include helper-pdfinfo.i +%include helper-convert.i +%include helper-fileobj.i +%include helper-devices.i + +%{ +// Declaring these structs here prevents gcc from generating warnings like: +// +// warning: 'struct Document' declared inside parameter list will not be visible outside of this definition or declaration +// +struct Colorspace; +struct Document; +struct Font; +struct Graftmap; +struct TextPage; +struct TextWriter; +struct DocumentWriter; +struct Xml; +struct Archive; +struct Story; +%} + +//------------------------------------------------------------------------ +// fz_document +//------------------------------------------------------------------------ +struct Document +{ + %extend + { + ~Document() + { + DEBUGMSG1("Document"); + fz_document *this_doc = (fz_document *) $self; + fz_drop_document(gctx, this_doc); + DEBUGMSG2; + } + FITZEXCEPTION2(Document, !result) + + %pythonprepend Document %{ + """Creates a document. Use 'open' as a synonym. + + Notes: + Basic usages: + open() - new PDF document + open(filename) - string, pathlib.Path, or file object. + open(filename, fileype=type) - overwrite filename extension. + open(type, buffer) - type: extension, buffer: bytes object. + open(stream=buffer, filetype=type) - keyword version of previous. + Parameters rect, width, height, fontsize: layout reflowable + document on open (e.g. EPUB). Ignored if n/a. + """ + self.is_closed = False + self.is_encrypted = False + self.isEncrypted = False + self.metadata = None + self.FontInfos = [] + self.Graftmaps = {} + self.ShownPages = {} + self.InsertedImages = {} + self._page_refs = weakref.WeakValueDictionary() + + if not filename or type(filename) is str: + pass + elif hasattr(filename, "absolute"): + filename = str(filename) + elif hasattr(filename, "name"): + filename = filename.name + else: + msg = "bad filename" + raise TypeError(msg) + + if stream != None: + if type(stream) is bytes: + self.stream = stream + elif type(stream) is bytearray: + self.stream = bytes(stream) + elif type(stream) is io.BytesIO: + self.stream = stream.getvalue() + else: + msg = "bad type: 'stream'" + raise TypeError(msg) + stream = self.stream + if not (filename or filetype): + filename = "pdf" + else: + self.stream = None + + if filename and self.stream == None: + self.name = filename + from_file = True + else: + from_file = False + self.name = "" + + if from_file: + if not os.path.exists(filename): + msg = f"no such file: '{filename}'" + raise FileNotFoundError(msg) + elif not os.path.isfile(filename): + msg = f"'{filename}' is no file" + raise FileDataError(msg) + if from_file and os.path.getsize(filename) == 0 or type(self.stream) is bytes and len(self.stream) == 0: + msg = "cannot open empty document" + raise EmptyFileError(msg) + %} + %pythonappend Document %{ + if self.thisown: + self._graft_id = TOOLS.gen_id() + if self.needs_pass is True: + self.is_encrypted = True + self.isEncrypted = True + else: # we won't init until doc is decrypted + self.init_doc() + # the following hack detects invalid/empty SVG files, which else may lead + # to interpreter crashes + if filename and filename.lower().endswith("svg") or filetype and "svg" in filetype.lower(): + try: + _ = self.convert_to_pdf() # this seems to always work + except: + raise FileDataError("cannot open broken document") from None + %} + + Document(const char *filename=NULL, PyObject *stream=NULL, + const char *filetype=NULL, PyObject *rect=NULL, + float width=0, float height=0, + float fontsize=11) + { + int old_msg_option = JM_mupdf_show_errors; + JM_mupdf_show_errors = 0; + fz_document *doc = NULL; + const fz_document_handler *handler; + char *c = NULL; + char *magic = NULL; + size_t len = 0; + fz_stream *data = NULL; + float w = width, h = height; + fz_rect r = JM_rect_from_py(rect); + if (!fz_is_infinite_rect(r)) { + w = r.x1 - r.x0; + h = r.y1 - r.y0; + } + + fz_try(gctx) { + if (stream != Py_None) { // stream given, **MUST** be bytes! + c = PyBytes_AS_STRING(stream); // just a pointer, no new obj + len = (size_t) PyBytes_Size(stream); + data = fz_open_memory(gctx, (const unsigned char *) c, len); + magic = (char *)filename; + if (!magic) magic = (char *)filetype; + handler = fz_recognize_document(gctx, magic); + if (!handler) { + RAISEPY(gctx, MSG_BAD_FILETYPE, PyExc_ValueError); + } + doc = fz_open_document_with_stream(gctx, magic, data); + } else { + if (filename && strlen(filename)) { + if (!filetype || strlen(filetype) == 0) { + doc = fz_open_document(gctx, filename); + } else { + handler = fz_recognize_document(gctx, filetype); + if (!handler) { + RAISEPY(gctx, MSG_BAD_FILETYPE, PyExc_ValueError); + } + if (handler->open) { + doc = handler->open(gctx, filename); + } else if (handler->open_with_stream) { + data = fz_open_file(gctx, filename); + doc = handler->open_with_stream(gctx, data); + } + } + } else { + pdf_document *pdf = pdf_create_document(gctx); + doc = (fz_document *) pdf; + } + } + } + fz_always(gctx) { + fz_drop_stream(gctx, data); + } + fz_catch(gctx) { + JM_mupdf_show_errors = old_msg_option; + return NULL; + } + if (w > 0 && h > 0) { + fz_layout_document(gctx, doc, w, h, fontsize); + } else if (fz_is_document_reflowable(gctx, doc)) { + fz_layout_document(gctx, doc, 400, 600, 11); + } + return (struct Document *) doc; + } + + + FITZEXCEPTION(load_page, !result) + %pythonprepend load_page %{ + """Load a page. + + 'page_id' is either a 0-based page number or a tuple (chapter, pno), + with chapter number and page number within that chapter. + """ + + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if page_id is None: + page_id = 0 + if page_id not in self: + raise ValueError("page not in document") + if type(page_id) is int and page_id < 0: + np = self.page_count + while page_id < 0: + page_id += np + %} + %pythonappend load_page %{ + val.thisown = True + val.parent = weakref.proxy(self) + self._page_refs[id(val)] = val + val._annot_refs = weakref.WeakValueDictionary() + val.number = page_id + %} + struct Page * + load_page(PyObject *page_id) + { + fz_page *page = NULL; + fz_document *doc = (fz_document *) $self; + int pno = 0, chapter = 0; + fz_try(gctx) { + if (PySequence_Check(page_id)) { + if (JM_INT_ITEM(page_id, 0, &chapter) == 1) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + if (JM_INT_ITEM(page_id, 1, &pno) == 1) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + page = fz_load_chapter_page(gctx, doc, chapter, pno); + } else { + pno = (int) PyLong_AsLong(page_id); + if (PyErr_Occurred()) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + page = fz_load_page(gctx, doc, pno); + } + } + fz_catch(gctx) { + PyErr_Clear(); + return NULL; + } + PyErr_Clear(); + return (struct Page *) page; + } + + + FITZEXCEPTION(_remove_links_to, !result) + PyObject *_remove_links_to(PyObject *numbers) + { + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + remove_dest_range(gctx, pdf, numbers); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + CLOSECHECK0(_loadOutline, """Load first outline.""") + struct Outline *_loadOutline() + { + fz_outline *ol = NULL; + fz_document *doc = (fz_document *) $self; + fz_try(gctx) { + ol = fz_load_outline(gctx, doc); + } + fz_catch(gctx) { + return NULL; + } + return (struct Outline *) ol; + } + + void _dropOutline(struct Outline *ol) { + DEBUGMSG1("Outline"); + fz_outline *this_ol = (fz_outline *) ol; + fz_drop_outline(gctx, this_ol); + DEBUGMSG2; + } + + FITZEXCEPTION(_insert_font, !result) + CLOSECHECK0(_insert_font, """Utility: insert font from file or binary.""") + PyObject * + _insert_font(char *fontfile=NULL, PyObject *fontbuffer=NULL) + { + PyObject *value=NULL; + pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self); + + fz_try(gctx) { + ASSERT_PDF(pdf); + if (!fontfile && !EXISTS(fontbuffer)) { + RAISEPY(gctx, MSG_FILE_OR_BUFFER, PyExc_ValueError); + } + value = JM_insert_font(gctx, pdf, NULL, fontfile, fontbuffer, + 0, 0, 0, 0, 0, -1); + } + fz_catch(gctx) { + return NULL; + } + return value; + } + + + FITZEXCEPTION(get_outline_xrefs, !result) + CLOSECHECK0(get_outline_xrefs, """Get list of outline xref numbers.""") + PyObject * + get_outline_xrefs() + { + PyObject *xrefs = PyList_New(0); + pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self); + if (!pdf) { + return xrefs; + } + fz_try(gctx) { + pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root)); + if (!root) goto finished; + pdf_obj *olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines)); + if (!olroot) goto finished; + pdf_obj *first = pdf_dict_get(gctx, olroot, PDF_NAME(First)); + if (!first) goto finished; + xrefs = JM_outline_xrefs(gctx, first, xrefs); + finished:; + } + fz_catch(gctx) { + Py_DECREF(xrefs); + return NULL; + } + return xrefs; + } + + + FITZEXCEPTION(xref_get_keys, !result) + CLOSECHECK0(xref_get_keys, """Get the keys of PDF dict object at 'xref'. Use -1 for the PDF trailer.""") + PyObject * + xref_get_keys(int xref) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self); + pdf_obj *obj=NULL; + PyObject *rc = NULL; + int i, n; + fz_try(gctx) { + ASSERT_PDF(pdf); + int xreflen = pdf_xref_len(gctx, pdf); + if (!INRANGE(xref, 1, xreflen-1) && xref != -1) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + if (xref > 0) { + obj = pdf_load_object(gctx, pdf, xref); + } else { + obj = pdf_trailer(gctx, pdf); + } + n = pdf_dict_len(gctx, obj); + rc = PyTuple_New(n); + if (!n) goto finished; + for (i = 0; i < n; i++) { + const char *key = pdf_to_name(gctx, pdf_dict_get_key(gctx, obj, i)); + PyTuple_SET_ITEM(rc, i, Py_BuildValue("s", key)); + } + finished:; + } + fz_always(gctx) { + if (xref > 0) { + pdf_drop_obj(gctx, obj); + } + } + fz_catch(gctx) { + return NULL; + } + return rc; + } + + + FITZEXCEPTION(xref_get_key, !result) + CLOSECHECK0(xref_get_key, """Get PDF dict key value of object at 'xref'.""") + PyObject * + xref_get_key(int xref, const char *key) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self); + pdf_obj *obj=NULL, *subobj=NULL; + PyObject *rc = NULL; + fz_buffer *res = NULL; + PyObject *text = NULL; + fz_try(gctx) { + ASSERT_PDF(pdf); + int xreflen = pdf_xref_len(gctx, pdf); + if (!INRANGE(xref, 1, xreflen-1) && xref != -1) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + if (xref > 0) { + obj = pdf_load_object(gctx, pdf, xref); + } else { + obj = pdf_trailer(gctx, pdf); + } + if (!obj) { + goto not_found; + } + subobj = pdf_dict_getp(gctx, obj, key); + if (!subobj) { + goto not_found; + } + char *type; + if (pdf_is_indirect(gctx, subobj)) { + type = "xref"; + text = PyUnicode_FromFormat("%i 0 R", pdf_to_num(gctx, subobj)); + } else if (pdf_is_array(gctx, subobj)) { + type = "array"; + } else if (pdf_is_dict(gctx, subobj)) { + type = "dict"; + } else if (pdf_is_int(gctx, subobj)) { + type = "int"; + text = PyUnicode_FromFormat("%i", pdf_to_int(gctx, subobj)); + } else if (pdf_is_real(gctx, subobj)) { + type = "float"; + } else if (pdf_is_null(gctx, subobj)) { + type = "null"; + text = PyUnicode_FromString("null"); + } else if (pdf_is_bool(gctx, subobj)) { + type = "bool"; + if (pdf_to_bool(gctx, subobj)) { + text = PyUnicode_FromString("true"); + } else { + text = PyUnicode_FromString("false"); + } + } else if (pdf_is_name(gctx, subobj)) { + type = "name"; + text = PyUnicode_FromFormat("/%s", pdf_to_name(gctx, subobj)); + } else if (pdf_is_string(gctx, subobj)) { + type = "string"; + text = JM_UnicodeFromStr(pdf_to_text_string(gctx, subobj)); + } else { + type = "unknown"; + } + if (!text) { + res = JM_object_to_buffer(gctx, subobj, 1, 0); + text = JM_UnicodeFromBuffer(gctx, res); + } + rc = Py_BuildValue("sO", type, text); + Py_DECREF(text); + goto finished; + + not_found:; + rc = Py_BuildValue("ss", "null", "null"); + finished:; + } + fz_always(gctx) { + if (xref > 0) { + pdf_drop_obj(gctx, obj); + } + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + return rc; + } + + + FITZEXCEPTION(xref_set_key, !result) + CLOSECHECK0(xref_set_key, """Set the value of a PDF dictionary key.""") + PyObject * + xref_set_key(int xref, const char *key, char *value) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self); + pdf_obj *obj = NULL, *new_obj = NULL; + int i, n; + fz_try(gctx) { + ASSERT_PDF(pdf); + if (!key || strlen(key) == 0) { + RAISEPY(gctx, "bad 'key'", PyExc_ValueError); + } + if (!value || strlen(value) == 0) { + RAISEPY(gctx, "bad 'value'", PyExc_ValueError); + } + int xreflen = pdf_xref_len(gctx, pdf); + if (!INRANGE(xref, 1, xreflen-1) && xref != -1) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + if (xref != -1) { + obj = pdf_load_object(gctx, pdf, xref); + } else { + obj = pdf_trailer(gctx, pdf); + } + // if val=="null" and no path hierarchy, delete "key" from object + // chr(47) = "/" + if (strcmp(value, "null") == 0 && strchr(key, 47) == NULL) { + pdf_dict_dels(gctx, obj, key); + goto finished; + } + new_obj = JM_set_object_value(gctx, obj, key, value); + if (!new_obj) { + goto finished; // did not work: skip update + } + if (xref != -1) { + pdf_drop_obj(gctx, obj); + obj = NULL; + pdf_update_object(gctx, pdf, xref, new_obj); + } else { + n = pdf_dict_len(gctx, new_obj); + for (i = 0; i < n; i++) { + pdf_dict_put(gctx, obj, pdf_dict_get_key(gctx, new_obj, i), pdf_dict_get_val(gctx, new_obj, i)); + } + } + finished:; + } + fz_always(gctx) { + if (xref != -1) { + pdf_drop_obj(gctx, obj); + } + pdf_drop_obj(gctx, new_obj); + PyErr_Clear(); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(_extend_toc_items, !result) + CLOSECHECK0(_extend_toc_items, """Add color info to all items of an extended TOC list.""") + PyObject * + _extend_toc_items(PyObject *items) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *)$self); + pdf_obj *bm, *col, *obj; + int count, flags; + PyObject *item=NULL, *itemdict=NULL, *xrefs, *bold, *italic, *collapse, *zoom; + zoom = PyUnicode_FromString("zoom"); + bold = PyUnicode_FromString("bold"); + italic = PyUnicode_FromString("italic"); + collapse = PyUnicode_FromString("collapse"); + fz_try(gctx) { + pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root)); + if (!root) goto finished; + pdf_obj *olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines)); + if (!olroot) goto finished; + pdf_obj *first = pdf_dict_get(gctx, olroot, PDF_NAME(First)); + if (!first) goto finished; + xrefs = PyList_New(0); // pre-allocate an empty list + xrefs = JM_outline_xrefs(gctx, first, xrefs); + Py_ssize_t i, n = PySequence_Size(xrefs), m = PySequence_Size(items); + if (!n) goto finished; + if (n != m) { + RAISEPY(gctx, "internal error finding outline xrefs", PyExc_IndexError); + } + int xref; + + // update all TOC item dictionaries + for (i = 0; i < n; i++) { + JM_INT_ITEM(xrefs, i, &xref); + item = PySequence_ITEM(items, i); + itemdict = PySequence_ITEM(item, 3); + if (!itemdict || !PyDict_Check(itemdict)) { + RAISEPY(gctx, "need non-simple TOC format", PyExc_ValueError); + } + PyDict_SetItem(itemdict, dictkey_xref, PySequence_ITEM(xrefs, i)); + bm = pdf_load_object(gctx, pdf, xref); + flags = pdf_to_int(gctx, (pdf_dict_get(gctx, bm, PDF_NAME(F)))); + if (flags == 1) { + PyDict_SetItem(itemdict, italic, Py_True); + } else if (flags == 2) { + PyDict_SetItem(itemdict, bold, Py_True); + } else if (flags == 3) { + PyDict_SetItem(itemdict, italic, Py_True); + PyDict_SetItem(itemdict, bold, Py_True); + } + count = pdf_to_int(gctx, (pdf_dict_get(gctx, bm, PDF_NAME(Count)))); + if (count < 0) { + PyDict_SetItem(itemdict, collapse, Py_True); + } else if (count > 0) { + PyDict_SetItem(itemdict, collapse, Py_False); + } + col = pdf_dict_get(gctx, bm, PDF_NAME(C)); + if (pdf_is_array(gctx, col) && pdf_array_len(gctx, col) == 3) { + PyObject *color = PyTuple_New(3); + PyTuple_SET_ITEM(color, 0, Py_BuildValue("f", pdf_to_real(gctx, pdf_array_get(gctx, col, 0)))); + PyTuple_SET_ITEM(color, 1, Py_BuildValue("f", pdf_to_real(gctx, pdf_array_get(gctx, col, 1)))); + PyTuple_SET_ITEM(color, 2, Py_BuildValue("f", pdf_to_real(gctx, pdf_array_get(gctx, col, 2)))); + DICT_SETITEM_DROP(itemdict, dictkey_color, color); + } + float z=0; + obj = pdf_dict_get(gctx, bm, PDF_NAME(Dest)); + if (!obj || !pdf_is_array(gctx, obj)) { + obj = pdf_dict_getl(gctx, bm, PDF_NAME(A), PDF_NAME(D), NULL); + } + if (pdf_is_array(gctx, obj) && pdf_array_len(gctx, obj) == 5) { + z = pdf_to_real(gctx, pdf_array_get(gctx, obj, 4)); + } + DICT_SETITEM_DROP(itemdict, zoom, Py_BuildValue("f", z)); + PyList_SetItem(item, 3, itemdict); + PyList_SetItem(items, i, item); + pdf_drop_obj(gctx, bm); + bm = NULL; + } + finished:; + } + fz_always(gctx) { + Py_CLEAR(xrefs); + Py_CLEAR(bold); + Py_CLEAR(italic); + Py_CLEAR(collapse); + Py_CLEAR(zoom); + pdf_drop_obj(gctx, bm); + PyErr_Clear(); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------------------------------------- + // EmbeddedFiles utility functions + //---------------------------------------------------------------- + FITZEXCEPTION(_embfile_names, !result) + CLOSECHECK0(_embfile_names, """Get list of embedded file names.""") + PyObject *_embfile_names(PyObject *namelist) + { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_specifics(gctx, doc); + fz_try(gctx) { + ASSERT_PDF(pdf); + PyObject *val; + pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(Names), + PDF_NAME(EmbeddedFiles), + PDF_NAME(Names), + NULL); + if (pdf_is_array(gctx, names)) { + int i, n = pdf_array_len(gctx, names); + for (i=0; i < n; i+=2) { + val = JM_EscapeStrFromStr(pdf_to_text_string(gctx, + pdf_array_get(gctx, names, i))); + LIST_APPEND_DROP(namelist, val); + } + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION(_embfile_del, !result) + PyObject *_embfile_del(int idx) + { + fz_try(gctx) { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_document_from_fz_document(gctx, doc); + pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(Names), + PDF_NAME(EmbeddedFiles), + PDF_NAME(Names), + NULL); + pdf_array_delete(gctx, names, idx + 1); + pdf_array_delete(gctx, names, idx); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION(_embfile_info, !result) + PyObject *_embfile_info(int idx, PyObject *infodict) + { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_document_from_fz_document(gctx, doc); + char *name; + int xref = 0, ci_xref=0; + fz_try(gctx) { + pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(Names), + PDF_NAME(EmbeddedFiles), + PDF_NAME(Names), + NULL); + + pdf_obj *o = pdf_array_get(gctx, names, 2*idx+1); + pdf_obj *ci = pdf_dict_get(gctx, o, PDF_NAME(CI)); + if (ci) { + ci_xref = pdf_to_num(gctx, ci); + } + DICT_SETITEMSTR_DROP(infodict, "collection", Py_BuildValue("i", ci_xref)); + name = (char *) pdf_to_text_string(gctx, + pdf_dict_get(gctx, o, PDF_NAME(F))); + DICT_SETITEM_DROP(infodict, dictkey_filename, JM_EscapeStrFromStr(name)); + + name = (char *) pdf_to_text_string(gctx, + pdf_dict_get(gctx, o, PDF_NAME(UF))); + DICT_SETITEM_DROP(infodict, dictkey_ufilename, JM_EscapeStrFromStr(name)); + + name = (char *) pdf_to_text_string(gctx, + pdf_dict_get(gctx, o, PDF_NAME(Desc))); + DICT_SETITEM_DROP(infodict, dictkey_desc, JM_UnicodeFromStr(name)); + + int len = -1, DL = -1; + pdf_obj *fileentry = pdf_dict_getl(gctx, o, PDF_NAME(EF), PDF_NAME(F), NULL); + xref = pdf_to_num(gctx, fileentry); + o = pdf_dict_get(gctx, fileentry, PDF_NAME(Length)); + if (o) len = pdf_to_int(gctx, o); + + o = pdf_dict_get(gctx, fileentry, PDF_NAME(DL)); + if (o) { + DL = pdf_to_int(gctx, o); + } else { + o = pdf_dict_getl(gctx, fileentry, PDF_NAME(Params), + PDF_NAME(Size), NULL); + if (o) DL = pdf_to_int(gctx, o); + } + DICT_SETITEM_DROP(infodict, dictkey_size, Py_BuildValue("i", DL)); + DICT_SETITEM_DROP(infodict, dictkey_length, Py_BuildValue("i", len)); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", xref); + } + + FITZEXCEPTION(_embfile_upd, !result) + PyObject *_embfile_upd(int idx, PyObject *buffer = NULL, char *filename = NULL, char *ufilename = NULL, char *desc = NULL) + { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_document_from_fz_document(gctx, doc); + fz_buffer *res = NULL; + fz_var(res); + int xref = 0; + fz_try(gctx) { + pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(Names), + PDF_NAME(EmbeddedFiles), + PDF_NAME(Names), + NULL); + + pdf_obj *entry = pdf_array_get(gctx, names, 2*idx+1); + + pdf_obj *filespec = pdf_dict_getl(gctx, entry, PDF_NAME(EF), + PDF_NAME(F), NULL); + if (!filespec) { + RAISEPY(gctx, "bad PDF: no /EF object", JM_Exc_FileDataError); + } + res = JM_BufferFromBytes(gctx, buffer); + if (EXISTS(buffer) && !res) { + RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_TypeError); + } + if (res && buffer != Py_None) + { + JM_update_stream(gctx, pdf, filespec, res, 1); + // adjust /DL and /Size parameters + int64_t len = (int64_t) fz_buffer_storage(gctx, res, NULL); + pdf_obj *l = pdf_new_int(gctx, len); + pdf_dict_put(gctx, filespec, PDF_NAME(DL), l); + pdf_dict_putl(gctx, filespec, l, PDF_NAME(Params), PDF_NAME(Size), NULL); + } + xref = pdf_to_num(gctx, filespec); + if (filename) + pdf_dict_put_text_string(gctx, entry, PDF_NAME(F), filename); + + if (ufilename) + pdf_dict_put_text_string(gctx, entry, PDF_NAME(UF), ufilename); + + if (desc) + pdf_dict_put_text_string(gctx, entry, PDF_NAME(Desc), desc); + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) + return NULL; + + return Py_BuildValue("i", xref); + } + + FITZEXCEPTION(_embeddedFileGet, !result) + PyObject *_embeddedFileGet(int idx) + { + fz_document *doc = (fz_document *) $self; + PyObject *cont = NULL; + pdf_document *pdf = pdf_document_from_fz_document(gctx, doc); + fz_buffer *buf = NULL; + fz_var(buf); + fz_try(gctx) { + pdf_obj *names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(Names), + PDF_NAME(EmbeddedFiles), + PDF_NAME(Names), + NULL); + + pdf_obj *entry = pdf_array_get(gctx, names, 2*idx+1); + pdf_obj *filespec = pdf_dict_getl(gctx, entry, PDF_NAME(EF), + PDF_NAME(F), NULL); + buf = pdf_load_stream(gctx, filespec); + cont = JM_BinFromBuffer(gctx, buf); + } + fz_always(gctx) { + fz_drop_buffer(gctx, buf); + } + fz_catch(gctx) { + return NULL; + } + return cont; + } + + FITZEXCEPTION(_embfile_add, !result) + PyObject *_embfile_add(const char *name, PyObject *buffer, char *filename=NULL, char *ufilename=NULL, char *desc=NULL) + { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_document_from_fz_document(gctx, doc); + fz_buffer *data = NULL; + fz_var(data); + pdf_obj *names = NULL; + int xref = 0; // xref of file entry + fz_try(gctx) { + ASSERT_PDF(pdf); + data = JM_BufferFromBytes(gctx, buffer); + if (!data) { + RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_TypeError); + } + + names = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(Names), + PDF_NAME(EmbeddedFiles), + PDF_NAME(Names), + NULL); + if (!pdf_is_array(gctx, names)) { + pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root)); + names = pdf_new_array(gctx, pdf, 6); // an even number! + pdf_dict_putl_drop(gctx, root, names, + PDF_NAME(Names), + PDF_NAME(EmbeddedFiles), + PDF_NAME(Names), + NULL); + } + + pdf_obj *fileentry = JM_embed_file(gctx, pdf, data, + filename, + ufilename, + desc, 1); + xref = pdf_to_num(gctx, pdf_dict_getl(gctx, fileentry, + PDF_NAME(EF), PDF_NAME(F), NULL)); + pdf_array_push_drop(gctx, names, pdf_new_text_string(gctx, name)); + pdf_array_push_drop(gctx, names, fileentry); + } + fz_always(gctx) { + fz_drop_buffer(gctx, data); + } + fz_catch(gctx) { + return NULL; + } + + return Py_BuildValue("i", xref); + } + + %pythoncode %{ + def embfile_names(self) -> list: + """Get list of names of EmbeddedFiles.""" + filenames = [] + self._embfile_names(filenames) + return filenames + + def _embeddedFileIndex(self, item: typing.Union[int, str]) -> int: + filenames = self.embfile_names() + msg = "'%s' not in EmbeddedFiles array." % str(item) + if item in filenames: + idx = filenames.index(item) + elif item in range(len(filenames)): + idx = item + else: + raise ValueError(msg) + return idx + + def embfile_count(self) -> int: + """Get number of EmbeddedFiles.""" + return len(self.embfile_names()) + + def embfile_del(self, item: typing.Union[int, str]): + """Delete an entry from EmbeddedFiles. + + Notes: + The argument must be name or index of an EmbeddedFiles item. + Physical deletion of data will happen on save to a new + file with appropriate garbage option. + Args: + item: name or number of item. + Returns: + None + """ + idx = self._embeddedFileIndex(item) + return self._embfile_del(idx) + + def embfile_info(self, item: typing.Union[int, str]) -> dict: + """Get information of an item in the EmbeddedFiles array. + + Args: + item: number or name of item. + Returns: + Information dictionary. + """ + idx = self._embeddedFileIndex(item) + infodict = {"name": self.embfile_names()[idx]} + xref = self._embfile_info(idx, infodict) + t, date = self.xref_get_key(xref, "Params/CreationDate") + if t != "null": + infodict["creationDate"] = date + t, date = self.xref_get_key(xref, "Params/ModDate") + if t != "null": + infodict["modDate"] = date + t, md5 = self.xref_get_key(xref, "Params/CheckSum") + if t != "null": + infodict["checksum"] = binascii.hexlify(md5.encode()).decode() + return infodict + + def embfile_get(self, item: typing.Union[int, str]) -> bytes: + """Get the content of an item in the EmbeddedFiles array. + + Args: + item: number or name of item. + Returns: + (bytes) The file content. + """ + idx = self._embeddedFileIndex(item) + return self._embeddedFileGet(idx) + + def embfile_upd(self, item: typing.Union[int, str], + buffer: OptBytes =None, + filename: OptStr =None, + ufilename: OptStr =None, + desc: OptStr =None,) -> None: + """Change an item of the EmbeddedFiles array. + + Notes: + Only provided parameters are changed. If all are omitted, + the method is a no-op. + Args: + item: number or name of item. + buffer: (binary data) the new file content. + filename: (str) the new file name. + ufilename: (unicode) the new filen ame. + desc: (str) the new description. + """ + idx = self._embeddedFileIndex(item) + xref = self._embfile_upd(idx, buffer=buffer, + filename=filename, + ufilename=ufilename, + desc=desc) + date = get_pdf_now() + self.xref_set_key(xref, "Params/ModDate", get_pdf_str(date)) + return xref + + def embfile_add(self, name: str, buffer: typing.ByteString, + filename: OptStr =None, + ufilename: OptStr =None, + desc: OptStr =None,) -> None: + """Add an item to the EmbeddedFiles array. + + Args: + name: name of the new item, must not already exist. + buffer: (binary data) the file content. + filename: (str) the file name, default: the name + ufilename: (unicode) the file name, default: filename + desc: (str) the description. + """ + filenames = self.embfile_names() + msg = "Name '%s' already exists." % str(name) + if name in filenames: + raise ValueError(msg) + + if filename is None: + filename = name + if ufilename is None: + ufilename = unicode(filename, "utf8") if str is bytes else filename + if desc is None: + desc = name + xref = self._embfile_add(name, buffer=buffer, + filename=filename, + ufilename=ufilename, + desc=desc) + date = get_pdf_now() + self.xref_set_key(xref, "Type", "/EmbeddedFile") + self.xref_set_key(xref, "Params/CreationDate", get_pdf_str(date)) + self.xref_set_key(xref, "Params/ModDate", get_pdf_str(date)) + return xref + %} + + FITZEXCEPTION(convert_to_pdf, !result) + %pythonprepend convert_to_pdf %{ + """Convert document to a PDF, selecting page range and optional rotation. Output bytes object.""" + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + %} + PyObject *convert_to_pdf(int from_page=0, int to_page=-1, int rotate=0) + { + PyObject *doc = NULL; + fz_document *fz_doc = (fz_document *) $self; + fz_try(gctx) { + int fp = from_page, tp = to_page, srcCount = fz_count_pages(gctx, fz_doc); + if (fp < 0) fp = 0; + if (fp > srcCount - 1) fp = srcCount - 1; + if (tp < 0) tp = srcCount - 1; + if (tp > srcCount - 1) tp = srcCount - 1; + Py_ssize_t len0 = PyList_Size(JM_mupdf_warnings_store); + doc = JM_convert_to_pdf(gctx, fz_doc, fp, tp, rotate); + Py_ssize_t len1 = PyList_Size(JM_mupdf_warnings_store); + Py_ssize_t i = len0; + while (i < len1) { + PySys_WriteStderr("%s\n", JM_StrAsChar(PyList_GetItem(JM_mupdf_warnings_store, i))); + i++; + } + } + fz_catch(gctx) { + return NULL; + } + if (doc) { + return doc; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(page_count, !result) + CLOSECHECK0(page_count, """Number of pages.""") + %pythoncode%{@property%} + PyObject *page_count() + { + PyObject *ret; + fz_try(gctx) { + ret = PyLong_FromLong((long) fz_count_pages(gctx, (fz_document *) $self)); + } + fz_catch(gctx) { + PyErr_Clear(); + return NULL; + } + return ret; + } + + FITZEXCEPTION(chapter_count, !result) + CLOSECHECK0(chapter_count, """Number of chapters.""") + %pythoncode%{@property%} + PyObject *chapter_count() + { + PyObject *ret; + fz_try(gctx) { + ret = PyLong_FromLong((long) fz_count_chapters(gctx, (fz_document *) $self)); + } + fz_catch(gctx) { + return NULL; + } + return ret; + } + + FITZEXCEPTION(last_location, !result) + CLOSECHECK0(last_location, """Id (chapter, page) of last page.""") + %pythoncode%{@property%} + PyObject *last_location() + { + fz_document *this_doc = (fz_document *) $self; + fz_location last_loc; + fz_try(gctx) { + last_loc = fz_last_page(gctx, this_doc); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("ii", last_loc.chapter, last_loc.page); + } + + + FITZEXCEPTION(chapter_page_count, !result) + CLOSECHECK0(chapter_page_count, """Page count of chapter.""") + PyObject *chapter_page_count(int chapter) + { + long pages = 0; + fz_try(gctx) { + int chapters = fz_count_chapters(gctx, (fz_document *) $self); + if (chapter < 0 || chapter >= chapters) { + RAISEPY(gctx, "bad chapter number", PyExc_ValueError); + } + pages = (long) fz_count_chapter_pages(gctx, (fz_document *) $self, chapter); + } + fz_catch(gctx) { + return NULL; + } + return PyLong_FromLong(pages); + } + + FITZEXCEPTION(prev_location, !result) + %pythonprepend prev_location %{ + """Get (chapter, page) of previous page.""" + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if type(page_id) is int: + page_id = (0, page_id) + if page_id not in self: + raise ValueError("page id not in document") + if page_id == (0, 0): + return () + %} + PyObject *prev_location(PyObject *page_id) + { + fz_document *this_doc = (fz_document *) $self; + fz_location prev_loc, loc; + PyObject *val; + int pno; + fz_try(gctx) { + val = PySequence_GetItem(page_id, 0); + if (!val) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + int chapter = (int) PyLong_AsLong(val); + Py_DECREF(val); + if (PyErr_Occurred()) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + + val = PySequence_GetItem(page_id, 1); + if (!val) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + pno = (int) PyLong_AsLong(val); + Py_DECREF(val); + if (PyErr_Occurred()) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + loc = fz_make_location(chapter, pno); + prev_loc = fz_previous_page(gctx, this_doc, loc); + } + fz_catch(gctx) { + PyErr_Clear(); + return NULL; + } + return Py_BuildValue("ii", prev_loc.chapter, prev_loc.page); + } + + + FITZEXCEPTION(next_location, !result) + %pythonprepend next_location %{ + """Get (chapter, page) of next page.""" + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if type(page_id) is int: + page_id = (0, page_id) + if page_id not in self: + raise ValueError("page id not in document") + if tuple(page_id) == self.last_location: + return () + %} + PyObject *next_location(PyObject *page_id) + { + fz_document *this_doc = (fz_document *) $self; + fz_location next_loc, loc; + PyObject *val; + int pno; + fz_try(gctx) { + val = PySequence_GetItem(page_id, 0); + if (!val) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + int chapter = (int) PyLong_AsLong(val); + Py_DECREF(val); + if (PyErr_Occurred()) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + + val = PySequence_GetItem(page_id, 1); + if (!val) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + pno = (int) PyLong_AsLong(val); + Py_DECREF(val); + if (PyErr_Occurred()) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + loc = fz_make_location(chapter, pno); + next_loc = fz_next_page(gctx, this_doc, loc); + } + fz_catch(gctx) { + PyErr_Clear(); + return NULL; + } + return Py_BuildValue("ii", next_loc.chapter, next_loc.page); + } + + + FITZEXCEPTION(location_from_page_number, !result) + CLOSECHECK0(location_from_page_number, """Convert pno to (chapter, page).""") + PyObject *location_from_page_number(int pno) + { + fz_document *this_doc = (fz_document *) $self; + fz_location loc = fz_make_location(-1, -1); + int page_count = fz_count_pages(gctx, this_doc); + while (pno < 0) pno += page_count; + fz_try(gctx) { + if (pno >= page_count) { + RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError); + } + loc = fz_location_from_page_number(gctx, this_doc, pno); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("ii", loc.chapter, loc.page); + } + + FITZEXCEPTION(page_number_from_location, !result) + %pythonprepend page_number_from_location%{ + """Convert (chapter, pno) to page number.""" + if type(page_id) is int: + np = self.page_count + while page_id < 0: + page_id += np + page_id = (0, page_id) + if page_id not in self: + raise ValueError("page id not in document") + %} + PyObject *page_number_from_location(PyObject *page_id) + { + fz_document *this_doc = (fz_document *) $self; + fz_location loc; + long page_n = -1; + PyObject *val; + int pno; + fz_try(gctx) { + val = PySequence_GetItem(page_id, 0); + if (!val) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + int chapter = (int) PyLong_AsLong(val); + Py_DECREF(val); + if (PyErr_Occurred()) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + + val = PySequence_GetItem(page_id, 1); + if (!val) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + pno = (int) PyLong_AsLong(val); + Py_DECREF(val); + if (PyErr_Occurred()) { + RAISEPY(gctx, MSG_BAD_PAGEID, PyExc_ValueError); + } + + loc = fz_make_location(chapter, pno); + page_n = (long) fz_page_number_from_location(gctx, this_doc, loc); + } + fz_catch(gctx) { + PyErr_Clear(); + return NULL; + } + return PyLong_FromLong(page_n); + } + + FITZEXCEPTION(_getMetadata, !result) + CLOSECHECK0(_getMetadata, """Get metadata.""") + PyObject * + _getMetadata(const char *key) + { + PyObject *res = NULL; + fz_document *doc = (fz_document *) $self; + int vsize; + char *value; + fz_try(gctx) { + vsize = fz_lookup_metadata(gctx, doc, key, NULL, 0)+1; + if(vsize > 1) { + value = JM_Alloc(char, vsize); + fz_lookup_metadata(gctx, doc, key, value, vsize); + res = JM_UnicodeFromStr(value); + JM_Free(value); + } else { + res = EMPTY_STRING; + } + } + fz_always(gctx) { + PyErr_Clear(); + } + fz_catch(gctx) { + return EMPTY_STRING; + } + return res; + } + + CLOSECHECK0(needs_pass, """Indicate password required.""") + %pythoncode%{@property%} + PyObject *needs_pass() { + return JM_BOOL(fz_needs_password(gctx, (fz_document *) $self)); + } + + %pythoncode%{@property%} + CLOSECHECK0(language, """Document language.""") + PyObject *language() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_NONE; + fz_text_language lang = pdf_document_language(gctx, pdf); + char buf[8]; + if (lang == FZ_LANG_UNSET) Py_RETURN_NONE; + return PyUnicode_FromString(fz_string_from_text_language(buf, lang)); + } + + FITZEXCEPTION(set_language, !result) + PyObject *set_language(char *language=NULL) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_try(gctx) { + ASSERT_PDF(pdf); + fz_text_language lang; + if (!language) + lang = FZ_LANG_UNSET; + else + lang = fz_text_language_from_string(language); + pdf_set_document_language(gctx, pdf, lang); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_TRUE; + } + + + %pythonprepend resolve_link %{ + """Calculate internal link destination. + + Args: + uri: (str) some Link.uri + chapters: (bool) whether to use (chapter, page) format + Returns: + (page_id, x, y) where x, y are point coordinates on the page. + page_id is either page number (if chapters=0), or (chapter, pno). + """ + %} + PyObject *resolve_link(char *uri=NULL, int chapters=0) + { + if (!uri) { + if (chapters) return Py_BuildValue("(ii)ff", -1, -1, 0, 0); + return Py_BuildValue("iff", -1, 0, 0); + } + fz_document *this_doc = (fz_document *) $self; + float xp = 0, yp = 0; + fz_location loc = {0, 0}; + fz_try(gctx) { + loc = fz_resolve_link(gctx, (fz_document *) $self, uri, &xp, &yp); + } + fz_catch(gctx) { + if (chapters) return Py_BuildValue("(ii)ff", -1, -1, 0, 0); + return Py_BuildValue("iff", -1, 0, 0); + } + if (chapters) + return Py_BuildValue("(ii)ff", loc.chapter, loc.page, xp, yp); + int pno = fz_page_number_from_location(gctx, this_doc, loc); + return Py_BuildValue("iff", pno, xp, yp); + } + + FITZEXCEPTION(layout, !result) + CLOSECHECK(layout, """Re-layout a reflowable document.""") + %pythonappend layout %{ + self._reset_page_refs() + self.init_doc()%} + PyObject *layout(PyObject *rect = NULL, float width = 0, float height = 0, float fontsize = 11) + { + fz_document *doc = (fz_document *) $self; + if (!fz_is_document_reflowable(gctx, doc)) Py_RETURN_NONE; + fz_try(gctx) { + float w = width, h = height; + fz_rect r = JM_rect_from_py(rect); + if (!fz_is_infinite_rect(r)) { + w = r.x1 - r.x0; + h = r.y1 - r.y0; + } + if (w <= 0.0f || h <= 0.0f) { + RAISEPY(gctx, "bad page size", PyExc_ValueError); + } + fz_layout_document(gctx, doc, w, h, fontsize); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION(make_bookmark, !result) + CLOSECHECK(make_bookmark, """Make a page pointer before layouting document.""") + PyObject *make_bookmark(PyObject *loc) + { + fz_document *doc = (fz_document *) $self; + fz_location location; + fz_bookmark mark; + fz_try(gctx) { + if (JM_INT_ITEM(loc, 0, &location.chapter) == 1) { + RAISEPY(gctx, MSG_BAD_LOCATION, PyExc_ValueError); + } + if (JM_INT_ITEM(loc, 1, &location.page) == 1) { + RAISEPY(gctx, MSG_BAD_LOCATION, PyExc_ValueError); + } + mark = fz_make_bookmark(gctx, doc, location); + if (!mark) { + RAISEPY(gctx, MSG_BAD_LOCATION, PyExc_ValueError); + } + } + fz_catch(gctx) { + return NULL; + } + return PyLong_FromVoidPtr((void *) mark); + } + + + FITZEXCEPTION(find_bookmark, !result) + CLOSECHECK(find_bookmark, """Find new location after layouting a document.""") + PyObject *find_bookmark(PyObject *bm) + { + fz_document *doc = (fz_document *) $self; + fz_location location; + fz_try(gctx) { + intptr_t mark = (intptr_t) PyLong_AsVoidPtr(bm); + location = fz_lookup_bookmark(gctx, doc, mark); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("ii", location.chapter, location.page); + } + + + CLOSECHECK0(is_reflowable, """Check if document is layoutable.""") + %pythoncode%{@property%} + PyObject *is_reflowable() + { + return JM_BOOL(fz_is_document_reflowable(gctx, (fz_document *) $self)); + } + + FITZEXCEPTION(_deleteObject, !result) + CLOSECHECK0(_deleteObject, """Delete object.""") + PyObject *_deleteObject(int xref) + { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_specifics(gctx, doc); + fz_try(gctx) { + ASSERT_PDF(pdf); + if (!INRANGE(xref, 1, pdf_xref_len(gctx, pdf)-1)) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + pdf_delete_object(gctx, pdf, xref); + } + fz_catch(gctx) { + return NULL; + } + + Py_RETURN_NONE; + } + + FITZEXCEPTION(pdf_catalog, !result) + CLOSECHECK0(pdf_catalog, """Get xref of PDF catalog.""") + PyObject *pdf_catalog() + { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_specifics(gctx, doc); + int xref = 0; + if (!pdf) return Py_BuildValue("i", xref); + fz_try(gctx) { + pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root)); + xref = pdf_to_num(gctx, root); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", xref); + } + + FITZEXCEPTION(_getPDFfileid, !result) + CLOSECHECK0(_getPDFfileid, """Get PDF file id.""") + PyObject *_getPDFfileid() + { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_specifics(gctx, doc); + if (!pdf) Py_RETURN_NONE; + PyObject *idlist = PyList_New(0); + fz_buffer *buffer = NULL; + unsigned char *hex; + pdf_obj *o; + int n, i, len; + PyObject *bytes; + + fz_try(gctx) { + pdf_obj *identity = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(ID)); + if (identity) { + n = pdf_array_len(gctx, identity); + for (i = 0; i < n; i++) { + o = pdf_array_get(gctx, identity, i); + len = (int) pdf_to_str_len(gctx, o); + buffer = fz_new_buffer(gctx, 2 * len); + fz_buffer_storage(gctx, buffer, &hex); + hexlify(len, (unsigned char *) pdf_to_text_string(gctx, o), hex); + LIST_APPEND_DROP(idlist, JM_UnicodeFromStr(hex)); + Py_CLEAR(bytes); + fz_drop_buffer(gctx, buffer); + buffer = NULL; + } + } + } + fz_catch(gctx) { + fz_drop_buffer(gctx, buffer); + } + return idlist; + } + + CLOSECHECK0(version_count, """Count versions of PDF document.""") + %pythoncode%{@property%} + PyObject *version_count() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) return Py_BuildValue("i", 0); + return Py_BuildValue("i", pdf_count_versions(gctx, pdf)); + } + + + CLOSECHECK0(is_pdf, """Check for PDF.""") + %pythoncode%{@property%} + PyObject *is_pdf() + { + if (pdf_specifics(gctx, (fz_document *) $self)) Py_RETURN_TRUE; + else Py_RETURN_FALSE; + } + + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR <= 21 + /* The underlying struct members that these methods give access to, are + not in mupdf-1.22. */ + CLOSECHECK0(has_xref_streams, """Check if xref table is a stream.""") + %pythoncode%{@property%} + PyObject *has_xref_streams() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_FALSE; + if (pdf->has_xref_streams) Py_RETURN_TRUE; + Py_RETURN_FALSE; + } + + CLOSECHECK0(has_old_style_xrefs, """Check if xref table is old style.""") + %pythoncode%{@property%} + PyObject *has_old_style_xrefs() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_FALSE; + if (pdf->has_old_style_xrefs) Py_RETURN_TRUE; + Py_RETURN_FALSE; + } + #endif + + CLOSECHECK0(is_dirty, """True if PDF has unsaved changes.""") + %pythoncode%{@property%} + PyObject *is_dirty() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_FALSE; + return JM_BOOL(pdf_has_unsaved_changes(gctx, pdf)); + } + + CLOSECHECK0(can_save_incrementally, """Check whether incremental saves are possible.""") + PyObject *can_save_incrementally() + { + pdf_document *pdf = pdf_document_from_fz_document(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_FALSE; // gracefully handle non-PDF + return JM_BOOL(pdf_can_be_saved_incrementally(gctx, pdf)); + } + + CLOSECHECK0(is_fast_webaccess, """Check whether we have a linearized PDF.""") + %pythoncode%{@property%} + PyObject *is_fast_webaccess() + { + pdf_document *pdf = pdf_document_from_fz_document(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_FALSE; // gracefully handle non-PDF + return JM_BOOL(pdf_doc_was_linearized(gctx, pdf)); + } + + CLOSECHECK0(is_repaired, """Check whether PDF was repaired.""") + %pythoncode%{@property%} + PyObject *is_repaired() + { + pdf_document *pdf = pdf_document_from_fz_document(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_FALSE; // gracefully handle non-PDF + return JM_BOOL(pdf_was_repaired(gctx, pdf)); + } + + FITZEXCEPTION(save_snapshot, !result) + %pythonprepend save_snapshot %{ + """Save a file snapshot suitable for journalling.""" + if self.is_closed: + raise ValueError("doc is closed") + if type(filename) == str: + pass + elif hasattr(filename, "open"): # assume: pathlib.Path + filename = str(filename) + elif hasattr(filename, "name"): # assume: file object + filename = filename.name + else: + raise ValueError("filename must be str, Path or file object") + if filename == self.name: + raise ValueError("cannot snapshot to original") + %} + PyObject *save_snapshot(const char *filename) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_try(gctx) { + ASSERT_PDF(pdf); + pdf_save_snapshot(gctx, pdf, filename); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + CLOSECHECK0(authenticate, """Decrypt document.""") + %pythonappend authenticate %{ + if val: # the doc is decrypted successfully and we init the outline + self.is_encrypted = False + self.isEncrypted = False + self.init_doc() + self.thisown = True + %} + PyObject *authenticate(char *password) + { + return Py_BuildValue("i", fz_authenticate_password(gctx, (fz_document *) $self, (const char *) password)); + } + + //------------------------------------------------------------------ + // save a PDF + //------------------------------------------------------------------ + FITZEXCEPTION(save, !result) + %pythonprepend save %{ + """Save PDF to file, pathlib.Path or file pointer.""" + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if type(filename) == str: + pass + elif hasattr(filename, "open"): # assume: pathlib.Path + filename = str(filename) + elif hasattr(filename, "name"): # assume: file object + filename = filename.name + elif not hasattr(filename, "seek"): # assume file object + raise ValueError("filename must be str, Path or file object") + if filename == self.name and not incremental: + raise ValueError("save to original must be incremental") + if self.page_count < 1: + raise ValueError("cannot save with zero pages") + if incremental: + if self.name != filename or self.stream: + raise ValueError("incremental needs original file") + if user_pw and len(user_pw) > 40 or owner_pw and len(owner_pw) > 40: + raise ValueError("password length must not exceed 40") + %} + + PyObject * + save(PyObject *filename, int garbage=0, int clean=0, + int deflate=0, int deflate_images=0, int deflate_fonts=0, + int incremental=0, int ascii=0, int expand=0, int linear=0, + int no_new_id=0, int appearance=0, + int pretty=0, int encryption=1, int permissions=4095, + char *owner_pw=NULL, char *user_pw=NULL) + { + pdf_write_options opts = pdf_default_write_options; + opts.do_incremental = incremental; + opts.do_ascii = ascii; + opts.do_compress = deflate; + opts.do_compress_images = deflate_images; + opts.do_compress_fonts = deflate_fonts; + opts.do_decompress = expand; + opts.do_garbage = garbage; + opts.do_pretty = pretty; + opts.do_linear = linear; + opts.do_clean = clean; + opts.do_sanitize = clean; + opts.dont_regenerate_id = no_new_id; + opts.do_appearance = appearance; + opts.do_encrypt = encryption; + opts.permissions = permissions; + if (owner_pw) { + memcpy(&opts.opwd_utf8, owner_pw, strlen(owner_pw)+1); + } else if (user_pw) { + memcpy(&opts.opwd_utf8, user_pw, strlen(user_pw)+1); + } + if (user_pw) { + memcpy(&opts.upwd_utf8, user_pw, strlen(user_pw)+1); + } + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_specifics(gctx, doc); + fz_output *out = NULL; + fz_try(gctx) { + ASSERT_PDF(pdf); + pdf->resynth_required = 0; + JM_embedded_clean(gctx, pdf); + if (no_new_id == 0) { + JM_ensure_identity(gctx, pdf); + } + if (PyUnicode_Check(filename)) { + pdf_save_document(gctx, pdf, JM_StrAsChar(filename), &opts); + } else { + out = JM_new_output_fileptr(gctx, filename); + pdf_write_document(gctx, pdf, out, &opts); + } + } + fz_always(gctx) { + fz_drop_output(gctx, out); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + %pythoncode %{ + def write(self, garbage=False, clean=False, + deflate=False, deflate_images=False, deflate_fonts=False, + incremental=False, ascii=False, expand=False, linear=False, + no_new_id=False, appearance=False, pretty=False, encryption=1, permissions=4095, + owner_pw=None, user_pw=None): + from io import BytesIO + bio = BytesIO() + self.save(bio, garbage=garbage, clean=clean, + no_new_id=no_new_id, appearance=appearance, + deflate=deflate, deflate_images=deflate_images, deflate_fonts=deflate_fonts, + incremental=incremental, ascii=ascii, expand=expand, linear=linear, + pretty=pretty, encryption=encryption, permissions=permissions, + owner_pw=owner_pw, user_pw=user_pw) + return bio.getvalue() + %} + + //---------------------------------------------------------------- + // Insert pages from a source PDF into this PDF. + // For reconstructing the links (_do_links method), we must save the + // insertion point (start_at) if it was specified as -1. + //---------------------------------------------------------------- + FITZEXCEPTION(insert_pdf, !result) + %pythonprepend insert_pdf %{ + """Insert a page range from another PDF. + + Args: + docsrc: PDF to copy from. Must be different object, but may be same file. + from_page: (int) first source page to copy, 0-based, default 0. + to_page: (int) last source page to copy, 0-based, default last page. + start_at: (int) from_page will become this page number in target. + rotate: (int) rotate copied pages, default -1 is no change. + links: (int/bool) whether to also copy links. + annots: (int/bool) whether to also copy annotations. + show_progress: (int) progress message interval, 0 is no messages. + final: (bool) indicates last insertion from this source PDF. + _gmap: internal use only + + Copy sequence reversed if from_page > to_page.""" + + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if self._graft_id == docsrc._graft_id: + raise ValueError("source and target cannot be same object") + sa = start_at + if sa < 0: + sa = self.page_count + if len(docsrc) > show_progress > 0: + inname = os.path.basename(docsrc.name) + if not inname: + inname = "memory PDF" + outname = os.path.basename(self.name) + if not outname: + outname = "memory PDF" + print("Inserting '%s' at '%s'" % (inname, outname)) + + # retrieve / make a Graftmap to avoid duplicate objects + isrt = docsrc._graft_id + _gmap = self.Graftmaps.get(isrt, None) + if _gmap is None: + _gmap = Graftmap(self) + self.Graftmaps[isrt] = _gmap + %} + + %pythonappend insert_pdf %{ + self._reset_page_refs() + if links: + self._do_links(docsrc, from_page = from_page, to_page = to_page, + start_at = sa) + if final == 1: + self.Graftmaps[isrt] = None%} + + PyObject * + insert_pdf(struct Document *docsrc, + int from_page=-1, + int to_page=-1, + int start_at=-1, + int rotate=-1, + int links=1, + int annots=1, + int show_progress=0, + int final = 1, + struct Graftmap *_gmap=NULL) + { + fz_document *doc = (fz_document *) $self; + fz_document *src = (fz_document *) docsrc; + pdf_document *pdfout = pdf_specifics(gctx, doc); + pdf_document *pdfsrc = pdf_specifics(gctx, src); + int outCount = fz_count_pages(gctx, doc); + int srcCount = fz_count_pages(gctx, src); + + // local copies of page numbers + int fp = from_page, tp = to_page, sa = start_at; + + // normalize page numbers + fp = Py_MAX(fp, 0); // -1 = first page + fp = Py_MIN(fp, srcCount - 1); // but do not exceed last page + + if (tp < 0) tp = srcCount - 1; // -1 = last page + tp = Py_MIN(tp, srcCount - 1); // but do not exceed last page + + if (sa < 0) sa = outCount; // -1 = behind last page + sa = Py_MIN(sa, outCount); // but that is also the limit + + fz_try(gctx) { + if (!pdfout || !pdfsrc) { + RAISEPY(gctx, "source or target not a PDF", PyExc_TypeError); + } + ENSURE_OPERATION(gctx, pdfout); + JM_merge_range(gctx, pdfout, pdfsrc, fp, tp, sa, rotate, links, annots, show_progress, (pdf_graft_map *) _gmap); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + %pythoncode %{ + def insert_file(self, infile, from_page=-1, to_page=-1, start_at=-1, rotate=-1, links=True, annots=True,show_progress=0, final=1): + """Insert an arbitrary supported document to an existing PDF. + + The infile may be given as a filename, a Document or a Pixmap. + Other paramters - where applicable - equal those of insert_pdf(). + """ + src = None + if isinstance(infile, Pixmap): + if infile.colorspace.n > 3: + infile = Pixmap(csRGB, infile) + src = Document("png", infile.tobytes()) + elif isinstance(infile, Document): + src = infile + else: + src = Document(infile) + if not src: + raise ValueError("bad infile parameter") + if not src.is_pdf: + pdfbytes = src.convert_to_pdf() + src = Document("pdf", pdfbytes) + return self.insert_pdf(src, from_page=from_page, to_page=to_page, start_at=start_at, rotate=rotate,links=links, annots=annots, show_progress=show_progress, final=final) + %} + + //------------------------------------------------------------------ + // Create and insert a new page (PDF) + //------------------------------------------------------------------ + FITZEXCEPTION(_newPage, !result) + CLOSECHECK(_newPage, """Make a new PDF page.""") + %pythonappend _newPage %{self._reset_page_refs()%} + PyObject *_newPage(int pno=-1, float width=595, float height=842) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_rect mediabox = fz_unit_rect; + mediabox.x1 = width; + mediabox.y1 = height; + pdf_obj *resources = NULL, *page_obj = NULL; + fz_buffer *contents = NULL; + fz_var(contents); + fz_var(page_obj); + fz_var(resources); + fz_try(gctx) { + ASSERT_PDF(pdf); + if (pno < -1) { + RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError); + } + ENSURE_OPERATION(gctx, pdf); + // create /Resources and /Contents objects + resources = pdf_add_new_dict(gctx, pdf, 1); + page_obj = pdf_add_page(gctx, pdf, mediabox, 0, resources, contents); + pdf_insert_page(gctx, pdf, pno, page_obj); + } + fz_always(gctx) { + fz_drop_buffer(gctx, contents); + pdf_drop_obj(gctx, page_obj); + pdf_drop_obj(gctx, resources); + } + fz_catch(gctx) { + return NULL; + } + + Py_RETURN_NONE; + } + + //------------------------------------------------------------------ + // Create sub-document to keep only selected pages. + // Parameter is a Python sequence of the wanted page numbers. + //------------------------------------------------------------------ + FITZEXCEPTION(select, !result) + %pythonprepend select %{"""Build sub-pdf with page numbers in the list.""" +if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") +if not self.is_pdf: + raise ValueError("is no PDF") +if not hasattr(pyliste, "__getitem__"): + raise ValueError("sequence required") +if len(pyliste) == 0 or min(pyliste) not in range(len(self)) or max(pyliste) not in range(len(self)): + raise ValueError("bad page number(s)")%} + %pythonappend select %{self._reset_page_refs()%} + PyObject *select(PyObject *pyliste) + { + // preparatory stuff: + // (1) get underlying pdf document, + // (2) transform Python list into integer array + + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_try(gctx) { + // call retainpages (code copy of fz_clean_file.c) + globals glo = {0}; + glo.ctx = gctx; + glo.doc = pdf; + retainpages(gctx, &glo, pyliste); + if (pdf->rev_page_map) + { + pdf_drop_page_tree(gctx, pdf); + } + } + fz_catch(gctx) { + return NULL; + } + + Py_RETURN_NONE; + } + + //------------------------------------------------------------------ + // remove one page + //------------------------------------------------------------------ + FITZEXCEPTION(_delete_page, !result) + PyObject *_delete_page(int pno) + { + fz_try(gctx) { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_specifics(gctx, doc); + pdf_delete_page(gctx, pdf, pno); + if (pdf->rev_page_map) + { + pdf_drop_page_tree(gctx, pdf); + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //------------------------------------------------------------------ + // get document permissions + //------------------------------------------------------------------ + %pythoncode%{@property%} + %pythonprepend permissions %{ + """Document permissions.""" + + if self.is_encrypted: + return 0 + %} + PyObject *permissions() + { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_document_from_fz_document(gctx, doc); + + // for PDF return result of standard function + if (pdf) + return Py_BuildValue("i", pdf_document_permissions(gctx, pdf)); + + // otherwise simulate the PDF return value + int perm = (int) 0xFFFFFFFC; // all permissions granted + // now switch off where needed + if (!fz_has_permission(gctx, doc, FZ_PERMISSION_PRINT)) + perm = perm ^ PDF_PERM_PRINT; + if (!fz_has_permission(gctx, doc, FZ_PERMISSION_EDIT)) + perm = perm ^ PDF_PERM_MODIFY; + if (!fz_has_permission(gctx, doc, FZ_PERMISSION_COPY)) + perm = perm ^ PDF_PERM_COPY; + if (!fz_has_permission(gctx, doc, FZ_PERMISSION_ANNOTATE)) + perm = perm ^ PDF_PERM_ANNOTATE; + return Py_BuildValue("i", perm); + } + + + FITZEXCEPTION(journal_enable, !result) + CLOSECHECK(journal_enable, """Activate document journalling.""") + PyObject *journal_enable() + { + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + pdf_enable_journal(gctx, pdf); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(journal_start_op, !result) + CLOSECHECK(journal_start_op, """Begin a journalling operation.""") + PyObject *journal_start_op(const char *name=NULL) + { + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + if (!pdf->journal) { + RAISEPY(gctx, "Journalling not enabled", PyExc_RuntimeError); + } + if (name) { + pdf_begin_operation(gctx, pdf, name); + } else { + pdf_begin_implicit_operation(gctx, pdf); + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(journal_stop_op, !result) + CLOSECHECK(journal_stop_op, """End a journalling operation.""") + PyObject *journal_stop_op() + { + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + pdf_end_operation(gctx, pdf); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(journal_position, !result) + CLOSECHECK(journal_position, """Show journalling state.""") + PyObject *journal_position() + { + int rc, steps=0; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + rc = pdf_undoredo_state(gctx, pdf, &steps); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("ii", rc, steps); + } + + + FITZEXCEPTION(journal_op_name, !result) + CLOSECHECK(journal_op_name, """Show operation name for given step.""") + PyObject *journal_op_name(int step) + { + const char *name=NULL; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + name = pdf_undoredo_step(gctx, pdf, step); + } + fz_catch(gctx) { + return NULL; + } + if (name) { + return PyUnicode_FromString(name); + } else { + Py_RETURN_NONE; + } + } + + + FITZEXCEPTION(journal_can_do, !result) + CLOSECHECK(journal_can_do, """Show if undo and / or redo are possible.""") + PyObject *journal_can_do() + { + int undo=0, redo=0; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + undo = pdf_can_undo(gctx, pdf); + redo = pdf_can_redo(gctx, pdf); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("{s:N,s:N}", "undo", JM_BOOL(undo), "redo", JM_BOOL(redo)); + } + + + FITZEXCEPTION(journal_undo, !result) + CLOSECHECK(journal_undo, """Move backwards in the journal.""") + PyObject *journal_undo() + { + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + pdf_undo(gctx, pdf); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_TRUE; + } + + + FITZEXCEPTION(journal_redo, !result) + CLOSECHECK(journal_redo, """Move forward in the journal.""") + PyObject *journal_redo() + { + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + pdf_redo(gctx, pdf); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_TRUE; + } + + + FITZEXCEPTION(journal_save, !result) + CLOSECHECK(journal_save, """Save journal to a file.""") + PyObject *journal_save(PyObject *filename) + { + fz_output *out = NULL; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + if (PyUnicode_Check(filename)) { + pdf_save_journal(gctx, pdf, (const char *) PyUnicode_AsUTF8(filename)); + } else { + out = JM_new_output_fileptr(gctx, filename); + pdf_write_journal(gctx, pdf, out); + } + } + fz_always(gctx) { + fz_drop_output(gctx, out); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(journal_load, !result) + CLOSECHECK(journal_load, """Load a journal from a file.""") + PyObject *journal_load(PyObject *filename) + { + fz_buffer *res = NULL; + fz_stream *stm = NULL; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + if (PyUnicode_Check(filename)) { + pdf_load_journal(gctx, pdf, PyUnicode_AsUTF8(filename)); + } else { + res = JM_BufferFromBytes(gctx, filename); + stm = fz_open_buffer(gctx, res); + pdf_deserialise_journal(gctx, pdf, stm); + } + if (!pdf->journal) { + RAISEPY(gctx, "Journal and document do not match", JM_Exc_FileDataError); + } + } + fz_always(gctx) { + fz_drop_stream(gctx, stm); + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(journal_is_enabled, !result) + CLOSECHECK(journal_is_enabled, """Check if journalling is enabled.""") + PyObject *journal_is_enabled() + { + int enabled = 0; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + enabled = pdf && pdf->journal; + } + fz_catch(gctx) { + return NULL; + } + return JM_BOOL(enabled); + } + + + FITZEXCEPTION(_get_char_widths, !result) + CLOSECHECK(_get_char_widths, """Return list of glyphs and glyph widths of a font.""") + PyObject *_get_char_widths(int xref, char *bfname, char *ext, + int ordering, int limit, int idx = 0) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + PyObject *wlist = NULL; + int i, glyph, mylimit; + mylimit = limit; + if (mylimit < 256) mylimit = 256; + const unsigned char *data; + int size, index; + fz_font *font = NULL; + fz_buffer *buf = NULL; + + fz_try(gctx) { + ASSERT_PDF(pdf); + if (ordering >= 0) { + data = fz_lookup_cjk_font(gctx, ordering, &size, &index); + font = fz_new_font_from_memory(gctx, NULL, data, size, index, 0); + goto weiter; + } + data = fz_lookup_base14_font(gctx, bfname, &size); + if (data) { + font = fz_new_font_from_memory(gctx, bfname, data, size, 0, 0); + goto weiter; + } + buf = JM_get_fontbuffer(gctx, pdf, xref); + if (!buf) { + fz_throw(gctx, FZ_ERROR_GENERIC, "font at xref %d is not supported", xref); + } + font = fz_new_font_from_buffer(gctx, NULL, buf, idx, 0); + + weiter:; + wlist = PyList_New(0); + float adv; + for (i = 0; i < mylimit; i++) { + glyph = fz_encode_character(gctx, font, i); + adv = fz_advance_glyph(gctx, font, glyph, 0); + if (ordering >= 0) { + glyph = i; + } + if (glyph > 0) { + LIST_APPEND_DROP(wlist, Py_BuildValue("if", glyph, adv)); + } else { + LIST_APPEND_DROP(wlist, Py_BuildValue("if", glyph, 0.0)); + } + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, buf); + fz_drop_font(gctx, font); + } + fz_catch(gctx) { + return NULL; + } + return wlist; + } + + + FITZEXCEPTION(page_xref, !result) + CLOSECHECK0(page_xref, """Get xref of page number.""") + PyObject *page_xref(int pno) + { + fz_document *this_doc = (fz_document *) $self; + int page_count = fz_count_pages(gctx, this_doc); + int n = pno; + while (n < 0) n += page_count; + pdf_document *pdf = pdf_specifics(gctx, this_doc); + int xref = 0; + fz_try(gctx) { + if (n >= page_count) { + RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError); + } + ASSERT_PDF(pdf); + xref = pdf_to_num(gctx, pdf_lookup_page_obj(gctx, pdf, n)); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", xref); + } + + + FITZEXCEPTION(page_annot_xrefs, !result) + CLOSECHECK0(page_annot_xrefs, """Get list annotations of page number.""") + PyObject *page_annot_xrefs(int pno) + { + fz_document *this_doc = (fz_document *) $self; + int page_count = fz_count_pages(gctx, this_doc); + int n = pno; + while (n < 0) n += page_count; + pdf_document *pdf = pdf_specifics(gctx, this_doc); + PyObject *annots = NULL; + fz_try(gctx) { + if (n >= page_count) { + RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError); + } + ASSERT_PDF(pdf); + annots = JM_get_annot_xref_list(gctx, pdf_lookup_page_obj(gctx, pdf, n)); + } + fz_catch(gctx) { + return NULL; + } + return annots; + } + + + FITZEXCEPTION(page_cropbox, !result) + CLOSECHECK0(page_cropbox, """Get CropBox of page number (without loading page).""") + %pythonappend page_cropbox %{val = Rect(JM_TUPLE3(val))%} + PyObject *page_cropbox(int pno) + { + fz_document *this_doc = (fz_document *) $self; + int page_count = fz_count_pages(gctx, this_doc); + int n = pno; + while (n < 0) n += page_count; + pdf_obj *pageref = NULL; + fz_var(pageref); + pdf_document *pdf = pdf_specifics(gctx, this_doc); + fz_try(gctx) { + if (n >= page_count) { + RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError); + } + ASSERT_PDF(pdf); + pageref = pdf_lookup_page_obj(gctx, pdf, n); + } + fz_catch(gctx) { + return NULL; + } + return JM_py_from_rect(JM_cropbox(gctx, pageref)); + } + + + FITZEXCEPTION(_getPageInfo, !result) + CLOSECHECK(_getPageInfo, """List fonts, images, XObjects used on a page.""") + PyObject *_getPageInfo(int pno, int what) + { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_specifics(gctx, doc); + pdf_obj *pageref, *rsrc; + PyObject *liste = NULL, *tracer = NULL; + fz_var(liste); + fz_var(tracer); + fz_try(gctx) { + int page_count = fz_count_pages(gctx, doc); + int n = pno; // pno < 0 is allowed + while (n < 0) n += page_count; // make it non-negative + if (n >= page_count) { + RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError); + } + ASSERT_PDF(pdf); + pageref = pdf_lookup_page_obj(gctx, pdf, n); + rsrc = pdf_dict_get_inheritable(gctx, + pageref, PDF_NAME(Resources)); + liste = PyList_New(0); + tracer = PyList_New(0); + if (rsrc) { + JM_scan_resources(gctx, pdf, rsrc, liste, what, 0, tracer); + } + } + fz_always(gctx) { + Py_CLEAR(tracer); + } + fz_catch(gctx) { + Py_CLEAR(liste); + return NULL; + } + return liste; + } + + FITZEXCEPTION(extract_font, !result) + CLOSECHECK(extract_font, """Get a font by xref. Returns a tuple or dictionary.""") + PyObject *extract_font(int xref=0, int info_only=0, PyObject *named=NULL) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + + fz_try(gctx) { + ASSERT_PDF(pdf); + } + fz_catch(gctx) { + return NULL; + } + + fz_buffer *buffer = NULL; + pdf_obj *obj, *basefont, *bname; + PyObject *bytes = NULL; + char *ext = NULL; + PyObject *rc; + fz_try(gctx) { + obj = pdf_load_object(gctx, pdf, xref); + pdf_obj *type = pdf_dict_get(gctx, obj, PDF_NAME(Type)); + pdf_obj *subtype = pdf_dict_get(gctx, obj, PDF_NAME(Subtype)); + if(pdf_name_eq(gctx, type, PDF_NAME(Font)) && + strncmp(pdf_to_name(gctx, subtype), "CIDFontType", 11) != 0) { + basefont = pdf_dict_get(gctx, obj, PDF_NAME(BaseFont)); + if (!basefont || pdf_is_null(gctx, basefont)) { + bname = pdf_dict_get(gctx, obj, PDF_NAME(Name)); + } else { + bname = basefont; + } + ext = JM_get_fontextension(gctx, pdf, xref); + if (strcmp(ext, "n/a") != 0 && !info_only) { + buffer = JM_get_fontbuffer(gctx, pdf, xref); + bytes = JM_BinFromBuffer(gctx, buffer); + fz_drop_buffer(gctx, buffer); + } else { + bytes = Py_BuildValue("y", ""); + } + if (PyObject_Not(named)) { + rc = PyTuple_New(4); + PyTuple_SET_ITEM(rc, 0, JM_EscapeStrFromStr(pdf_to_name(gctx, bname))); + PyTuple_SET_ITEM(rc, 1, JM_UnicodeFromStr(ext)); + PyTuple_SET_ITEM(rc, 2, JM_UnicodeFromStr(pdf_to_name(gctx, subtype))); + PyTuple_SET_ITEM(rc, 3, bytes); + } else { + rc = PyDict_New(); + DICT_SETITEM_DROP(rc, dictkey_name, JM_EscapeStrFromStr(pdf_to_name(gctx, bname))); + DICT_SETITEM_DROP(rc, dictkey_ext, JM_UnicodeFromStr(ext)); + DICT_SETITEM_DROP(rc, dictkey_type, JM_UnicodeFromStr(pdf_to_name(gctx, subtype))); + DICT_SETITEM_DROP(rc, dictkey_content, bytes); + } + } else { + if (PyObject_Not(named)) { + rc = Py_BuildValue("sssy", "", "", "", ""); + } else { + rc = PyDict_New(); + DICT_SETITEM_DROP(rc, dictkey_name, Py_BuildValue("s", "")); + DICT_SETITEM_DROP(rc, dictkey_ext, Py_BuildValue("s", "")); + DICT_SETITEM_DROP(rc, dictkey_type, Py_BuildValue("s", "")); + DICT_SETITEM_DROP(rc, dictkey_content, Py_BuildValue("y", "")); + } + } + } + fz_always(gctx) { + pdf_drop_obj(gctx, obj); + JM_PyErr_Clear; + } + fz_catch(gctx) { + if (PyObject_Not(named)) { + rc = Py_BuildValue("sssy", "invalid-name", "", "", ""); + } else { + rc = PyDict_New(); + DICT_SETITEM_DROP(rc, dictkey_name, Py_BuildValue("s", "invalid-name")); + DICT_SETITEM_DROP(rc, dictkey_ext, Py_BuildValue("s", "")); + DICT_SETITEM_DROP(rc, dictkey_type, Py_BuildValue("s", "")); + DICT_SETITEM_DROP(rc, dictkey_content, Py_BuildValue("y", "")); + } + } + return rc; + } + + + FITZEXCEPTION(extract_image, !result) + CLOSECHECK(extract_image, """Get image by xref. Returns a dictionary.""") + PyObject *extract_image(int xref) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + pdf_obj *obj = NULL; + fz_buffer *res = NULL; + fz_image *img = NULL; + PyObject *rc = NULL; + const char *ext = NULL; + const char *cs_name = NULL; + int img_type = 0, xres, yres, colorspace; + int smask = 0, width, height, bpc; + fz_compressed_buffer *cbuf = NULL; + fz_var(img); + fz_var(res); + fz_var(obj); + + fz_try(gctx) { + ASSERT_PDF(pdf); + if (!INRANGE(xref, 1, pdf_xref_len(gctx, pdf)-1)) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + obj = pdf_new_indirect(gctx, pdf, xref, 0); + pdf_obj *subtype = pdf_dict_get(gctx, obj, PDF_NAME(Subtype)); + + if (!pdf_name_eq(gctx, subtype, PDF_NAME(Image))) { + RAISEPY(gctx, "not an image", PyExc_ValueError); + } + + pdf_obj *o = pdf_dict_geta(gctx, obj, PDF_NAME(SMask), PDF_NAME(Mask)); + if (o) smask = pdf_to_num(gctx, o); + + if (pdf_is_jpx_image(gctx, obj)) { + img_type = FZ_IMAGE_JPX; + res = pdf_load_stream(gctx, obj); + ext = "jpx"; + } + if (JM_is_jbig2_image(gctx, obj)) { + img_type = FZ_IMAGE_JBIG2; + res = pdf_load_stream(gctx, obj); + ext = "jb2"; + } + if (img_type == FZ_IMAGE_UNKNOWN) { + res = pdf_load_raw_stream(gctx, obj); + unsigned char *c = NULL; + fz_buffer_storage(gctx, res, &c); + img_type = fz_recognize_image_format(gctx, c); + ext = JM_image_extension(img_type); + } + if (img_type == FZ_IMAGE_UNKNOWN) { + fz_drop_buffer(gctx, res); + res = NULL; + img = pdf_load_image(gctx, pdf, obj); + cbuf = fz_compressed_image_buffer(gctx, img); + if (cbuf && + cbuf->params.type != FZ_IMAGE_RAW && + cbuf->params.type != FZ_IMAGE_FAX && + cbuf->params.type != FZ_IMAGE_FLATE && + cbuf->params.type != FZ_IMAGE_LZW && + cbuf->params.type != FZ_IMAGE_RLD) { + img_type = cbuf->params.type; + ext = JM_image_extension(img_type); + res = cbuf->buffer; + } else { + res = fz_new_buffer_from_image_as_png(gctx, img, + fz_default_color_params); + ext = "png"; + } + } else { + img = fz_new_image_from_buffer(gctx, res); + } + + fz_image_resolution(img, &xres, &yres); + width = img->w; + height = img->h; + colorspace = img->n; + bpc = img->bpc; + cs_name = fz_colorspace_name(gctx, img->colorspace); + + rc = PyDict_New(); + DICT_SETITEM_DROP(rc, dictkey_ext, + JM_UnicodeFromStr(ext)); + DICT_SETITEM_DROP(rc, dictkey_smask, + Py_BuildValue("i", smask)); + DICT_SETITEM_DROP(rc, dictkey_width, + Py_BuildValue("i", width)); + DICT_SETITEM_DROP(rc, dictkey_height, + Py_BuildValue("i", height)); + DICT_SETITEM_DROP(rc, dictkey_colorspace, + Py_BuildValue("i", colorspace)); + DICT_SETITEM_DROP(rc, dictkey_bpc, + Py_BuildValue("i", bpc)); + DICT_SETITEM_DROP(rc, dictkey_xres, + Py_BuildValue("i", xres)); + DICT_SETITEM_DROP(rc, dictkey_yres, + Py_BuildValue("i", yres)); + DICT_SETITEM_DROP(rc, dictkey_cs_name, + JM_UnicodeFromStr(cs_name)); + DICT_SETITEM_DROP(rc, dictkey_image, + JM_BinFromBuffer(gctx, res)); + } + fz_always(gctx) { + fz_drop_image(gctx, img); + if (!cbuf) fz_drop_buffer(gctx, res); + pdf_drop_obj(gctx, obj); + } + + fz_catch(gctx) { + Py_CLEAR(rc); + fz_warn(gctx, "%s", fz_caught_message(gctx)); + Py_RETURN_FALSE; + } + if (!rc) + Py_RETURN_NONE; + return rc; + } + + + //------------------------------------------------------------------ + // Delete all bookmarks (table of contents) + // returns list of deleted (now available) xref numbers + //------------------------------------------------------------------ + CLOSECHECK(_delToC, """Delete the TOC.""") + %pythonappend _delToC %{self.init_doc()%} + PyObject *_delToC() + { + PyObject *xrefs = PyList_New(0); // create Python list + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) return xrefs; // not a pdf + + pdf_obj *root, *olroot, *first; + int xref_count, olroot_xref, i, xref; + + // get the main root + root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root)); + // get the outline root + olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines)); + if (!olroot) return xrefs; // no outlines or some problem + + first = pdf_dict_get(gctx, olroot, PDF_NAME(First)); // first outline + + xrefs = JM_outline_xrefs(gctx, first, xrefs); + xref_count = (int) PyList_Size(xrefs); + + olroot_xref = pdf_to_num(gctx, olroot); // delete OL root + pdf_delete_object(gctx, pdf, olroot_xref); // delete OL root + pdf_dict_del(gctx, root, PDF_NAME(Outlines)); // delete OL root + + for (i = 0; i < xref_count; i++) + { + JM_INT_ITEM(xrefs, i, &xref); + pdf_delete_object(gctx, pdf, xref); // delete outline item + } + LIST_APPEND_DROP(xrefs, Py_BuildValue("i", olroot_xref)); + + return xrefs; + } + + + //------------------------------------------------------------------ + // Check: is xref a stream object? + //------------------------------------------------------------------ + CLOSECHECK0(xref_is_stream, """Check if xref is a stream object.""") + PyObject *xref_is_stream(int xref=0) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_FALSE; // not a PDF + return JM_BOOL(pdf_obj_num_is_stream(gctx, pdf, xref)); + } + + //------------------------------------------------------------------ + // Return or set NeedAppearances + //------------------------------------------------------------------ + %pythonprepend need_appearances +%{"""Get/set the NeedAppearances value.""" +if self.is_closed: + raise ValueError("document closed") +if not self.is_form_pdf: + return None +%} + PyObject *need_appearances(PyObject *value=NULL) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + int oldval = -1; + pdf_obj *app = NULL; + char appkey[] = "NeedAppearances"; + fz_try(gctx) { + pdf_obj *form = pdf_dict_getp(gctx, pdf_trailer(gctx, pdf), + "Root/AcroForm"); + app = pdf_dict_gets(gctx, form, appkey); + if (pdf_is_bool(gctx, app)) { + oldval = pdf_to_bool(gctx, app); + } + + if (EXISTS(value)) { + pdf_dict_puts_drop(gctx, form, appkey, PDF_TRUE); + } else if (value == Py_False) { + pdf_dict_puts_drop(gctx, form, appkey, PDF_FALSE); + } + } + fz_catch(gctx) { + Py_RETURN_NONE; + } + if (value != Py_None) { + return value; + } + if (oldval >= 0) { + return JM_BOOL(oldval); + } + Py_RETURN_NONE; + } + + //------------------------------------------------------------------ + // Return the /SigFlags value + //------------------------------------------------------------------ + CLOSECHECK0(get_sigflags, """Get the /SigFlags value.""") + int get_sigflags() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) return -1; // not a PDF + int sigflag = -1; + fz_try(gctx) { + pdf_obj *sigflags = pdf_dict_getl(gctx, + pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(AcroForm), + PDF_NAME(SigFlags), + NULL); + if (sigflags) { + sigflag = (int) pdf_to_int(gctx, sigflags); + } + } + fz_catch(gctx) { + return -1; // any problem + } + return sigflag; + } + + //------------------------------------------------------------------ + // Check: is this an AcroForm with at least one field? + //------------------------------------------------------------------ + CLOSECHECK0(is_form_pdf, """Either False or PDF field count.""") + %pythoncode%{@property%} + PyObject *is_form_pdf() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_FALSE; // not a PDF + int count = -1; // init count + fz_try(gctx) { + pdf_obj *fields = pdf_dict_getl(gctx, + pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(AcroForm), + PDF_NAME(Fields), + NULL); + if (pdf_is_array(gctx, fields)) { + count = pdf_array_len(gctx, fields); + } + } + fz_catch(gctx) { + Py_RETURN_FALSE; + } + if (count >= 0) { + return Py_BuildValue("i", count); + } else { + Py_RETURN_FALSE; + } + } + + //------------------------------------------------------------------ + // Return the list of field font resource names + //------------------------------------------------------------------ + CLOSECHECK0(FormFonts, """Get list of field font resource names.""") + %pythoncode%{@property%} + PyObject *FormFonts() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_NONE; // not a PDF + pdf_obj *fonts = NULL; + PyObject *liste = PyList_New(0); + fz_var(liste); + fz_try(gctx) { + fonts = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root), PDF_NAME(AcroForm), PDF_NAME(DR), PDF_NAME(Font), NULL); + if (fonts && pdf_is_dict(gctx, fonts)) // fonts exist + { + int i, n = pdf_dict_len(gctx, fonts); + for (i = 0; i < n; i++) + { + pdf_obj *f = pdf_dict_get_key(gctx, fonts, i); + LIST_APPEND_DROP(liste, JM_UnicodeFromStr(pdf_to_name(gctx, f))); + } + } + } + fz_catch(gctx) { + Py_DECREF(liste); + Py_RETURN_NONE; // any problem yields None + } + return liste; + } + + //------------------------------------------------------------------ + // Add a field font + //------------------------------------------------------------------ + FITZEXCEPTION(_addFormFont, !result) + CLOSECHECK(_addFormFont, """Add new form font.""") + PyObject *_addFormFont(char *name, char *font) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_NONE; // not a PDF + pdf_obj *fonts = NULL; + fz_try(gctx) { + fonts = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root), + PDF_NAME(AcroForm), PDF_NAME(DR), PDF_NAME(Font), NULL); + if (!fonts || !pdf_is_dict(gctx, fonts)) { + RAISEPY(gctx, "PDF has no form fonts yet", PyExc_RuntimeError); + } + pdf_obj *k = pdf_new_name(gctx, (const char *) name); + pdf_obj *v = JM_pdf_obj_from_str(gctx, pdf, font); + pdf_dict_put(gctx, fonts, k, v); + } + fz_catch(gctx) NULL; + Py_RETURN_NONE; + } + + //------------------------------------------------------------------ + // Get Xref Number of Outline Root, create it if missing + //------------------------------------------------------------------ + FITZEXCEPTION(_getOLRootNumber, !result) + CLOSECHECK(_getOLRootNumber, """Get xref of Outline Root, create it if missing.""") + PyObject *_getOLRootNumber() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + pdf_obj *ind_obj = NULL; + pdf_obj *olroot2 = NULL; + int ret; + fz_var(ind_obj); + fz_var(olroot2); + fz_try(gctx) { + ASSERT_PDF(pdf); + // get main root + pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root)); + // get outline root + pdf_obj *olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines)); + if (!olroot) + { + olroot2 = pdf_new_dict(gctx, pdf, 4); + pdf_dict_put(gctx, olroot2, PDF_NAME(Type), PDF_NAME(Outlines)); + ind_obj = pdf_add_object(gctx, pdf, olroot2); + pdf_dict_put(gctx, root, PDF_NAME(Outlines), ind_obj); + olroot = pdf_dict_get(gctx, root, PDF_NAME(Outlines)); + + } + ret = pdf_to_num(gctx, olroot); + } + fz_always(gctx) { + pdf_drop_obj(gctx, ind_obj); + pdf_drop_obj(gctx, olroot2); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", ret); + } + + //------------------------------------------------------------------ + // Get a new Xref number + //------------------------------------------------------------------ + FITZEXCEPTION(get_new_xref, !result) + CLOSECHECK(get_new_xref, """Make a new xref.""") + PyObject *get_new_xref() + { + int xref = 0; + fz_try(gctx) { + fz_document *doc = (fz_document *) $self; + pdf_document *pdf = pdf_specifics(gctx, doc); + ASSERT_PDF(pdf); + ENSURE_OPERATION(gctx, pdf); + xref = pdf_create_object(gctx, pdf); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", xref); + } + + //------------------------------------------------------------------ + // Get Length of XREF table + //------------------------------------------------------------------ + FITZEXCEPTION(xref_length, !result) + CLOSECHECK0(xref_length, """Get length of xref table.""") + PyObject *xref_length() + { + int xreflen = 0; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (pdf) xreflen = pdf_xref_len(gctx, pdf); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", xreflen); + } + + //------------------------------------------------------------------ + // Get XML Metadata + //------------------------------------------------------------------ + CLOSECHECK0(get_xml_metadata, """Get document XML metadata.""") + PyObject *get_xml_metadata() + { + PyObject *rc = NULL; + fz_buffer *buff = NULL; + pdf_obj *xml = NULL; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (pdf) { + xml = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root), PDF_NAME(Metadata), NULL); + } + if (xml) { + buff = pdf_load_stream(gctx, xml); + rc = JM_UnicodeFromBuffer(gctx, buff); + } else { + rc = EMPTY_STRING; + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, buff); + PyErr_Clear(); + } + fz_catch(gctx) { + return EMPTY_STRING; + } + return rc; + } + + //------------------------------------------------------------------ + // Get XML Metadata xref + //------------------------------------------------------------------ + FITZEXCEPTION(xref_xml_metadata, !result) + CLOSECHECK0(xref_xml_metadata, """Get xref of document XML metadata.""") + PyObject *xref_xml_metadata() + { + int xref = 0; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + ASSERT_PDF(pdf); + pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root)); + if (!root) { + RAISEPY(gctx, MSG_BAD_PDFROOT, JM_Exc_FileDataError); + } + pdf_obj *xml = pdf_dict_get(gctx, root, PDF_NAME(Metadata)); + if (xml) xref = pdf_to_num(gctx, xml); + } + fz_catch(gctx) {;} + return Py_BuildValue("i", xref); + } + + //------------------------------------------------------------------ + // Delete XML Metadata + //------------------------------------------------------------------ + FITZEXCEPTION(del_xml_metadata, !result) + CLOSECHECK(del_xml_metadata, """Delete XML metadata.""") + PyObject *del_xml_metadata() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_try(gctx) { + ASSERT_PDF(pdf); + pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root)); + if (root) pdf_dict_del(gctx, root, PDF_NAME(Metadata)); + } + fz_catch(gctx) { + return NULL; + } + + Py_RETURN_NONE; + } + + //------------------------------------------------------------------ + // Set XML-based Metadata + //------------------------------------------------------------------ + FITZEXCEPTION(set_xml_metadata, !result) + CLOSECHECK(set_xml_metadata, """Store XML document level metadata.""") + PyObject *set_xml_metadata(char *metadata) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_buffer *res = NULL; + fz_try(gctx) { + ASSERT_PDF(pdf); + pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root)); + if (!root) { + RAISEPY(gctx, MSG_BAD_PDFROOT, JM_Exc_FileDataError); + } + res = fz_new_buffer_from_copied_data(gctx, (const unsigned char *) metadata, strlen(metadata)); + pdf_obj *xml = pdf_dict_get(gctx, root, PDF_NAME(Metadata)); + if (xml) { + JM_update_stream(gctx, pdf, xml, res, 0); + } else { + xml = pdf_add_stream(gctx, pdf, res, NULL, 0); + pdf_dict_put(gctx, xml, PDF_NAME(Type), PDF_NAME(Metadata)); + pdf_dict_put(gctx, xml, PDF_NAME(Subtype), PDF_NAME(XML)); + pdf_dict_put_drop(gctx, root, PDF_NAME(Metadata), xml); + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + + Py_RETURN_NONE; + } + + //------------------------------------------------------------------ + // Get Object String of xref + //------------------------------------------------------------------ + FITZEXCEPTION(xref_object, !result) + CLOSECHECK0(xref_object, """Get xref object source as a string.""") + PyObject *xref_object(int xref, int compressed=0, int ascii=0) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + pdf_obj *obj = NULL; + PyObject *text = NULL; + fz_buffer *res=NULL; + fz_try(gctx) { + ASSERT_PDF(pdf); + int xreflen = pdf_xref_len(gctx, pdf); + if (!INRANGE(xref, 1, xreflen-1) && xref != -1) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + if (xref > 0) { + obj = pdf_load_object(gctx, pdf, xref); + } else { + obj = pdf_trailer(gctx, pdf); + } + res = JM_object_to_buffer(gctx, pdf_resolve_indirect(gctx, obj), compressed, ascii); + text = JM_EscapeStrFromBuffer(gctx, res); + } + fz_always(gctx) { + if (xref > 0) { + pdf_drop_obj(gctx, obj); + } + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) return EMPTY_STRING; + return text; + } + %pythoncode %{ + def pdf_trailer(self, compressed: bool=False, ascii:bool=False)->str: + """Get PDF trailer as a string.""" + return self.xref_object(-1, compressed=compressed, ascii=ascii)%} + + + //------------------------------------------------------------------ + // Get compressed stream of an object by xref + // Py_RETURN_NONE if not stream + //------------------------------------------------------------------ + FITZEXCEPTION(xref_stream_raw, !result) + CLOSECHECK(xref_stream_raw, """Get xref stream without decompression.""") + PyObject *xref_stream_raw(int xref) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + PyObject *r = NULL; + pdf_obj *obj = NULL; + fz_var(obj); + fz_buffer *res = NULL; + fz_var(res); + fz_try(gctx) { + ASSERT_PDF(pdf); + int xreflen = pdf_xref_len(gctx, pdf); + if (!INRANGE(xref, 1, xreflen-1) && xref != -1) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + if (xref >= 0) { + obj = pdf_new_indirect(gctx, pdf, xref, 0); + } else { + obj = pdf_trailer(gctx, pdf); + } + if (pdf_is_stream(gctx, obj)) + { + res = pdf_load_raw_stream_number(gctx, pdf, xref); + r = JM_BinFromBuffer(gctx, res); + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + if (xref >= 0) { + pdf_drop_obj(gctx, obj); + } + } + fz_catch(gctx) + { + Py_CLEAR(r); + return NULL; + } + if (!r) Py_RETURN_NONE; + return r; + } + + //------------------------------------------------------------------ + // Get decompressed stream of an object by xref + // Py_RETURN_NONE if not stream + //------------------------------------------------------------------ + FITZEXCEPTION(xref_stream, !result) + CLOSECHECK(xref_stream, """Get decompressed xref stream.""") + PyObject *xref_stream(int xref) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + PyObject *r = Py_None; + pdf_obj *obj = NULL; + fz_var(obj); + fz_buffer *res = NULL; + fz_var(res); + fz_try(gctx) { + ASSERT_PDF(pdf); + int xreflen = pdf_xref_len(gctx, pdf); + if (!INRANGE(xref, 1, xreflen-1) && xref != -1) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + if (xref >= 0) { + obj = pdf_new_indirect(gctx, pdf, xref, 0); + } else { + obj = pdf_trailer(gctx, pdf); + } + if (pdf_is_stream(gctx, obj)) + { + res = pdf_load_stream_number(gctx, pdf, xref); + r = JM_BinFromBuffer(gctx, res); + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + if (xref >= 0) { + pdf_drop_obj(gctx, obj); + } + } + fz_catch(gctx) + { + Py_CLEAR(r); + return NULL; + } + return r; + } + + //------------------------------------------------------------------ + // Update an Xref number with a new object given as a string + //------------------------------------------------------------------ + FITZEXCEPTION(update_object, !result) + CLOSECHECK(update_object, """Replace object definition source.""") + PyObject *update_object(int xref, char *text, struct Page *page = NULL) + { + pdf_obj *new_obj; + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_try(gctx) { + ASSERT_PDF(pdf); + int xreflen = pdf_xref_len(gctx, pdf); + if (!INRANGE(xref, 1, xreflen-1)) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + ENSURE_OPERATION(gctx, pdf); + // create new object with passed-in string + new_obj = JM_pdf_obj_from_str(gctx, pdf, text); + pdf_update_object(gctx, pdf, xref, new_obj); + pdf_drop_obj(gctx, new_obj); + if (page) { + pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page); + JM_refresh_links(gctx, pdfpage); + } + } + fz_catch(gctx) { + return NULL; + } + + Py_RETURN_NONE; + } + + //------------------------------------------------------------------ + // Update a stream identified by its xref + //------------------------------------------------------------------ + FITZEXCEPTION(update_stream, !result) + CLOSECHECK(update_stream, """Replace xref stream part.""") + PyObject *update_stream(int xref=0, PyObject *stream=NULL, int new=1, int compress=1) + { + pdf_obj *obj = NULL; + fz_var(obj); + fz_buffer *res = NULL; + fz_var(res); + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_try(gctx) { + ASSERT_PDF(pdf); + int xreflen = pdf_xref_len(gctx, pdf); + if (!INRANGE(xref, 1, xreflen-1)) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + ENSURE_OPERATION(gctx, pdf); + // get the object + obj = pdf_new_indirect(gctx, pdf, xref, 0); + if (!pdf_is_dict(gctx, obj)) { + RAISEPY(gctx, MSG_IS_NO_DICT, PyExc_ValueError); + } + res = JM_BufferFromBytes(gctx, stream); + if (!res) { + RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_TypeError); + } + JM_update_stream(gctx, pdf, obj, res, compress); + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + pdf_drop_obj(gctx, obj); + } + fz_catch(gctx) + return NULL; + + Py_RETURN_NONE; + } + + + //------------------------------------------------------------------ + // create / refresh the page map + //------------------------------------------------------------------ + FITZEXCEPTION(_make_page_map, !result) + CLOSECHECK0(_make_page_map, """Make an array page number -> page object.""") + PyObject *_make_page_map() + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + if (!pdf) Py_RETURN_NONE; + fz_try(gctx) { + pdf_drop_page_tree(gctx, pdf); + pdf_load_page_tree(gctx, pdf); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", pdf->map_page_count); + } + + + //------------------------------------------------------------------ + // full (deep) copy of one page + //------------------------------------------------------------------ + FITZEXCEPTION(fullcopy_page, !result) + CLOSECHECK0(fullcopy_page, """Make a full page duplicate.""") + %pythonappend fullcopy_page %{self._reset_page_refs()%} + PyObject *fullcopy_page(int pno, int to = -1) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + int page_count = pdf_count_pages(gctx, pdf); + fz_buffer *res = NULL, *nres=NULL; + fz_buffer *contents_buffer = NULL; + fz_var(pdf); + fz_var(res); + fz_var(nres); + fz_var(contents_buffer); + fz_try(gctx) { + ASSERT_PDF(pdf); + if (!INRANGE(pno, 0, page_count - 1) || + !INRANGE(to, -1, page_count - 1)) { + RAISEPY(gctx, MSG_BAD_PAGENO, PyExc_ValueError); + } + + pdf_obj *page1 = pdf_resolve_indirect(gctx, + pdf_lookup_page_obj(gctx, pdf, pno)); + + pdf_obj *page2 = pdf_deep_copy_obj(gctx, page1); + pdf_obj *old_annots = pdf_dict_get(gctx, page2, PDF_NAME(Annots)); + + // copy annotations, but remove Popup and IRT types + if (old_annots) { + int i, n = pdf_array_len(gctx, old_annots); + pdf_obj *new_annots = pdf_new_array(gctx, pdf, n); + for (i = 0; i < n; i++) { + pdf_obj *o = pdf_array_get(gctx, old_annots, i); + pdf_obj *subtype = pdf_dict_get(gctx, o, PDF_NAME(Subtype)); + if (pdf_name_eq(gctx, subtype, PDF_NAME(Popup))) continue; + if (pdf_dict_gets(gctx, o, "IRT")) continue; + pdf_obj *copy_o = pdf_deep_copy_obj(gctx, + pdf_resolve_indirect(gctx, o)); + int xref = pdf_create_object(gctx, pdf); + pdf_update_object(gctx, pdf, xref, copy_o); + pdf_drop_obj(gctx, copy_o); + copy_o = pdf_new_indirect(gctx, pdf, xref, 0); + pdf_dict_del(gctx, copy_o, PDF_NAME(Popup)); + pdf_dict_del(gctx, copy_o, PDF_NAME(P)); + pdf_array_push_drop(gctx, new_annots, copy_o); + } + pdf_dict_put_drop(gctx, page2, PDF_NAME(Annots), new_annots); + } + + // copy the old contents stream(s) + res = JM_read_contents(gctx, page1); + + // create new /Contents object for page2 + if (res) { + contents_buffer = fz_new_buffer_from_copied_data(gctx, " ", 1); + pdf_obj *contents = pdf_add_stream(gctx, pdf, contents_buffer, NULL, 0); + JM_update_stream(gctx, pdf, contents, res, 1); + pdf_dict_put_drop(gctx, page2, PDF_NAME(Contents), contents); + } + + // now insert target page, making sure it is an indirect object + int xref = pdf_create_object(gctx, pdf); // get new xref + pdf_update_object(gctx, pdf, xref, page2); // store new page + pdf_drop_obj(gctx, page2); // give up this object for now + + page2 = pdf_new_indirect(gctx, pdf, xref, 0); // reread object + pdf_insert_page(gctx, pdf, to, page2); // and store the page + pdf_drop_obj(gctx, page2); + } + fz_always(gctx) { + pdf_drop_page_tree(gctx, pdf); + fz_drop_buffer(gctx, res); + fz_drop_buffer(gctx, nres); + fz_drop_buffer(gctx, contents_buffer); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //------------------------------------------------------------------ + // move or copy one page + //------------------------------------------------------------------ + FITZEXCEPTION(_move_copy_page, !result) + CLOSECHECK0(_move_copy_page, """Move or copy a PDF page reference.""") + %pythonappend _move_copy_page %{self._reset_page_refs()%} + PyObject *_move_copy_page(int pno, int nb, int before, int copy) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + int i1, i2, pos, count, same = 0; + pdf_obj *parent1 = NULL, *parent2 = NULL, *parent = NULL; + pdf_obj *kids1, *kids2; + fz_try(gctx) { + ASSERT_PDF(pdf); + // get the two page objects ----------------------------------- + // locate the /Kids arrays and indices in each + pdf_obj *page1 = pdf_lookup_page_loc(gctx, pdf, pno, &parent1, &i1); + kids1 = pdf_dict_get(gctx, parent1, PDF_NAME(Kids)); + + pdf_obj *page2 = pdf_lookup_page_loc(gctx, pdf, nb, &parent2, &i2); + (void) page2; + kids2 = pdf_dict_get(gctx, parent2, PDF_NAME(Kids)); + + if (before) // calc index of source page in target /Kids + pos = i2; + else + pos = i2 + 1; + + // same /Kids array? ------------------------------------------ + same = pdf_objcmp(gctx, kids1, kids2); + + // put source page in target /Kids array ---------------------- + if (!copy && same != 0) // update parent in page object + { + pdf_dict_put(gctx, page1, PDF_NAME(Parent), parent2); + } + pdf_array_insert(gctx, kids2, page1, pos); + + if (same != 0) // different /Kids arrays ---------------------- + { + parent = parent2; + while (parent) // increase /Count objects in parents + { + count = pdf_dict_get_int(gctx, parent, PDF_NAME(Count)); + pdf_dict_put_int(gctx, parent, PDF_NAME(Count), count + 1); + parent = pdf_dict_get(gctx, parent, PDF_NAME(Parent)); + } + if (!copy) // delete original item + { + pdf_array_delete(gctx, kids1, i1); + parent = parent1; + while (parent) // decrease /Count objects in parents + { + count = pdf_dict_get_int(gctx, parent, PDF_NAME(Count)); + pdf_dict_put_int(gctx, parent, PDF_NAME(Count), count - 1); + parent = pdf_dict_get(gctx, parent, PDF_NAME(Parent)); + } + } + } + else { // same /Kids array + if (copy) { // source page is copied + parent = parent2; + while (parent) // increase /Count object in parents + { + count = pdf_dict_get_int(gctx, parent, PDF_NAME(Count)); + pdf_dict_put_int(gctx, parent, PDF_NAME(Count), count + 1); + parent = pdf_dict_get(gctx, parent, PDF_NAME(Parent)); + } + } else { + if (i1 < pos) + pdf_array_delete(gctx, kids1, i1); + else + pdf_array_delete(gctx, kids1, i1 + 1); + } + } + if (pdf->rev_page_map) { // page map no longer valid: drop it + pdf_drop_page_tree(gctx, pdf); + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION(_remove_toc_item, !result) + PyObject *_remove_toc_item(int xref) + { + // "remove" bookmark by letting it point to nowhere + pdf_obj *item = NULL, *color; + int i; + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_try(gctx) { + item = pdf_new_indirect(gctx, pdf, xref, 0); + pdf_dict_del(gctx, item, PDF_NAME(Dest)); + pdf_dict_del(gctx, item, PDF_NAME(A)); + color = pdf_new_array(gctx, pdf, 3); + for (i=0; i < 3; i++) { + pdf_array_push_real(gctx, color, 0.8); + } + pdf_dict_put_drop(gctx, item, PDF_NAME(C), color); + } + fz_always(gctx) { + pdf_drop_obj(gctx, item); + } + fz_catch(gctx){ + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION(_update_toc_item, !result) + PyObject *_update_toc_item(int xref, char *action=NULL, char *title=NULL, int flags=0, PyObject *collapse=NULL, PyObject *color=NULL) + { + // "update" bookmark by letting it point to nowhere + pdf_obj *item = NULL; + pdf_obj *obj = NULL; + Py_ssize_t i; + double f; + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + fz_try(gctx) { + item = pdf_new_indirect(gctx, pdf, xref, 0); + if (title) { + pdf_dict_put_text_string(gctx, item, PDF_NAME(Title), title); + } + if (action) { + pdf_dict_del(gctx, item, PDF_NAME(Dest)); + obj = JM_pdf_obj_from_str(gctx, pdf, action); + pdf_dict_put_drop(gctx, item, PDF_NAME(A), obj); + } + pdf_dict_put_int(gctx, item, PDF_NAME(F), flags); + if (EXISTS(color)) { + pdf_obj *c = pdf_new_array(gctx, pdf, 3); + for (i = 0; i < 3; i++) { + JM_FLOAT_ITEM(color, i, &f); + pdf_array_push_real(gctx, c, f); + } + pdf_dict_put_drop(gctx, item, PDF_NAME(C), c); + } else if (color != Py_None) { + pdf_dict_del(gctx, item, PDF_NAME(C)); + } + if (collapse != Py_None) { + if (pdf_dict_get(gctx, item, PDF_NAME(Count))) { + i = pdf_dict_get_int(gctx, item, PDF_NAME(Count)); + if ((i < 0 && collapse == Py_False) || (i > 0 && collapse == Py_True)) { + i = i * (-1); + pdf_dict_put_int(gctx, item, PDF_NAME(Count), i); + } + } + } + } + fz_always(gctx) { + pdf_drop_obj(gctx, item); + } + fz_catch(gctx){ + return NULL; + } + Py_RETURN_NONE; + } + + //------------------------------------------------------------------ + // PDF page label getting / setting + //------------------------------------------------------------------ + FITZEXCEPTION(_get_page_labels, !result) + PyObject * + _get_page_labels() + { + pdf_obj *obj, *nums, *kids; + PyObject *rc = NULL; + int i, n; + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + + pdf_obj *pagelabels = NULL; + fz_var(pagelabels); + fz_try(gctx) { + ASSERT_PDF(pdf); + rc = PyList_New(0); + pagelabels = pdf_new_name(gctx, "PageLabels"); + obj = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), pagelabels, NULL); + if (!obj) { + goto finished; + } + // simple case: direct /Nums object + nums = pdf_resolve_indirect(gctx, + pdf_dict_get(gctx, obj, PDF_NAME(Nums))); + if (nums) { + JM_get_page_labels(gctx, rc, nums); + goto finished; + } + // case: /Kids/Nums + nums = pdf_resolve_indirect(gctx, + pdf_dict_getl(gctx, obj, PDF_NAME(Kids), PDF_NAME(Nums), NULL) + ); + if (nums) { + JM_get_page_labels(gctx, rc, nums); + goto finished; + } + // case: /Kids is an array of multiple /Nums + kids = pdf_resolve_indirect(gctx, + pdf_dict_get(gctx, obj, PDF_NAME(Kids))); + if (!kids || !pdf_is_array(gctx, kids)) { + goto finished; + } + + n = pdf_array_len(gctx, kids); + for (i = 0; i < n; i++) { + nums = pdf_resolve_indirect(gctx, + pdf_dict_get(gctx, + pdf_array_get(gctx, kids, i), + PDF_NAME(Nums))); + JM_get_page_labels(gctx, rc, nums); + } + finished:; + } + fz_always(gctx) { + PyErr_Clear(); + pdf_drop_obj(gctx, pagelabels); + } + fz_catch(gctx){ + Py_CLEAR(rc); + return NULL; + } + return rc; + } + + + FITZEXCEPTION(_set_page_labels, !result) + %pythonappend _set_page_labels %{ + xref = self.pdf_catalog() + text = self.xref_object(xref, compressed=True) + text = text.replace("/Nums[]", "/Nums[%s]" % labels) + self.update_object(xref, text)%} + PyObject * + _set_page_labels(char *labels) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) $self); + pdf_obj *pagelabels = NULL; + fz_var(pagelabels); + fz_try(gctx) { + ASSERT_PDF(pdf); + pagelabels = pdf_new_name(gctx, "PageLabels"); + pdf_obj *root = pdf_dict_get(gctx, pdf_trailer(gctx, pdf), PDF_NAME(Root)); + pdf_dict_del(gctx, root, pagelabels); + pdf_dict_putl_drop(gctx, root, pdf_new_array(gctx, pdf, 0), pagelabels, PDF_NAME(Nums), NULL); + } + fz_always(gctx) { + PyErr_Clear(); + pdf_drop_obj(gctx, pagelabels); + } + fz_catch(gctx){ + return NULL; + } + Py_RETURN_NONE; + } + + + //------------------------------------------------------------------ + // PDF Optional Content functions + //------------------------------------------------------------------ + FITZEXCEPTION(get_layers, !result) + CLOSECHECK0(get_layers, """Show optional OC layers.""") + PyObject * + get_layers() + { + PyObject *rc = NULL; + pdf_layer_config info = {NULL, NULL}; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self); + ASSERT_PDF(pdf); + int i, n = pdf_count_layer_configs(gctx, pdf); + if (n == 1) { + pdf_obj *obj = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), PDF_NAME(OCProperties), PDF_NAME(Configs), NULL); + if (!pdf_is_array(gctx, obj)) n = 0; + } + rc = PyTuple_New(n); + for (i = 0; i < n; i++) { + pdf_layer_config_info(gctx, pdf, i, &info); + PyObject *item = Py_BuildValue("{s:i,s:s,s:s}", + "number", i, "name", info.name, "creator", info.creator); + PyTuple_SET_ITEM(rc, i, item); + info.name = NULL; + info.creator = NULL; + } + } + fz_catch(gctx) { + Py_CLEAR(rc); + return NULL; + } + return rc; + } + + + FITZEXCEPTION(switch_layer, !result) + CLOSECHECK0(switch_layer, """Activate an OC layer.""") + PyObject * + switch_layer(int config, int as_default=0) + { + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self); + ASSERT_PDF(pdf); + pdf_obj *cfgs = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), PDF_NAME(OCProperties), PDF_NAME(Configs), NULL); + if (!pdf_is_array(gctx, cfgs) || !pdf_array_len(gctx, cfgs)) { + if (config < 1) goto finished; + RAISEPY(gctx, MSG_BAD_OC_LAYER, PyExc_ValueError); + } + if (config < 0) goto finished; + pdf_select_layer_config(gctx, pdf, config); + if (as_default) { + pdf_set_layer_config_as_default(gctx, pdf); + pdf_read_ocg(gctx, pdf); + } + finished:; + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(get_layer, !result) + CLOSECHECK0(get_layer, """Content of ON, OFF, RBGroups of an OC layer.""") + PyObject * + get_layer(int config=-1) + { + PyObject *rc; + pdf_obj *obj = NULL; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self); + ASSERT_PDF(pdf); + pdf_obj *ocp = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), PDF_NAME(OCProperties), NULL); + if (!ocp) { + rc = Py_BuildValue("s", NULL); + goto finished; + } + if (config == -1) { + obj = pdf_dict_get(gctx, ocp, PDF_NAME(D)); + } else { + obj = pdf_array_get(gctx, pdf_dict_get(gctx, ocp, PDF_NAME(Configs)), config); + } + if (!obj) { + RAISEPY(gctx, MSG_BAD_OC_CONFIG, PyExc_ValueError); + } + rc = JM_get_ocg_arrays(gctx, obj); + finished:; + } + fz_catch(gctx) { + Py_CLEAR(rc); + PyErr_Clear(); + return NULL; + } + return rc; + } + + + FITZEXCEPTION(set_layer, !result) + %pythonprepend set_layer +%{"""Set the PDF keys /ON, /OFF, /RBGroups of an OC layer.""" +if self.is_closed: + raise ValueError("document closed") +ocgs = set(self.get_ocgs().keys()) +if ocgs == set(): + raise ValueError("document has no optional content") + +if on: + if type(on) not in (list, tuple): + raise ValueError("bad type: 'on'") + s = set(on).difference(ocgs) + if s != set(): + raise ValueError("bad OCGs in 'on': %s" % s) + +if off: + if type(off) not in (list, tuple): + raise ValueError("bad type: 'off'") + s = set(off).difference(ocgs) + if s != set(): + raise ValueError("bad OCGs in 'off': %s" % s) + +if locked: + if type(locked) not in (list, tuple): + raise ValueError("bad type: 'locked'") + s = set(locked).difference(ocgs) + if s != set(): + raise ValueError("bad OCGs in 'locked': %s" % s) + +if rbgroups: + if type(rbgroups) not in (list, tuple): + raise ValueError("bad type: 'rbgroups'") + for x in rbgroups: + if not type(x) in (list, tuple): + raise ValueError("bad RBGroup '%s'" % x) + s = set(x).difference(ocgs) + if f != set(): + raise ValueError("bad OCGs in RBGroup: %s" % s) + +if basestate: + basestate = str(basestate).upper() + if basestate == "UNCHANGED": + basestate = "Unchanged" + if basestate not in ("ON", "OFF", "Unchanged"): + raise ValueError("bad 'basestate'") +%} + PyObject * + set_layer(int config, const char *basestate=NULL, PyObject *on=NULL, + PyObject *off=NULL, PyObject *rbgroups=NULL, PyObject *locked=NULL) + { + pdf_obj *obj = NULL; + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self); + ASSERT_PDF(pdf); + pdf_obj *ocp = pdf_dict_getl(gctx, pdf_trailer(gctx, pdf), + PDF_NAME(Root), PDF_NAME(OCProperties), NULL); + if (!ocp) { + goto finished; + } + if (config == -1) { + obj = pdf_dict_get(gctx, ocp, PDF_NAME(D)); + } else { + obj = pdf_array_get(gctx, pdf_dict_get(gctx, ocp, PDF_NAME(Configs)), config); + } + if (!obj) { + RAISEPY(gctx, MSG_BAD_OC_CONFIG, PyExc_ValueError); + } + JM_set_ocg_arrays(gctx, obj, basestate, on, off, rbgroups, locked); + pdf_read_ocg(gctx, pdf); + finished:; + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(add_layer, !result) + CLOSECHECK0(add_layer, """Add a new OC layer.""") + PyObject *add_layer(char *name, char *creator=NULL, PyObject *on=NULL) + { + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self); + ASSERT_PDF(pdf); + JM_add_layer_config(gctx, pdf, name, creator, on); + pdf_read_ocg(gctx, pdf); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(layer_ui_configs, !result) + CLOSECHECK0(layer_ui_configs, """Show OC visibility status modifiable by user.""") + PyObject *layer_ui_configs() + { + typedef struct + { + const char *text; + int depth; + pdf_layer_config_ui_type type; + int selected; + int locked; + } pdf_layer_config_ui; + PyObject *rc = NULL; + + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self); + ASSERT_PDF(pdf); + pdf_layer_config_ui info; + int i, n = pdf_count_layer_config_ui(gctx, pdf); + rc = PyTuple_New(n); + char *type = NULL; + for (i = 0; i < n; i++) { + pdf_layer_config_ui_info(gctx, pdf, i, (void *) &info); + switch (info.type) + { + case (1): type = "checkbox"; break; + case (2): type = "radiobox"; break; + default: type = "label"; break; + } + PyObject *item = Py_BuildValue("{s:i,s:N,s:i,s:s,s:N,s:N}", + "number", i, + "text", JM_EscapeStrFromStr(info.text), + "depth", info.depth, + "type", type, + "on", JM_BOOL(info.selected), + "locked", JM_BOOL(info.locked)); + PyTuple_SET_ITEM(rc, i, item); + } + } + fz_catch(gctx) { + Py_CLEAR(rc); + return NULL; + } + return rc; + } + + + FITZEXCEPTION(set_layer_ui_config, !result) + CLOSECHECK0(set_layer_ui_config, ) + %pythonprepend set_layer_ui_config %{ + """Set / unset OC intent configuration.""" + # The user might have given the name instead of sequence number, + # so select by that name and continue with corresp. number + if isinstance(number, str): + select = [ui["number"] for ui in self.layer_ui_configs() if ui["text"] == number] + if select == []: + raise ValueError(f"bad OCG '{number}'.") + number = select[0] # this is the number for the name + %} + PyObject *set_layer_ui_config(int number, int action=0) + { + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self); + ASSERT_PDF(pdf); + switch (action) + { + case (1): + pdf_toggle_layer_config_ui(gctx, pdf, number); + break; + case (2): + pdf_deselect_layer_config_ui(gctx, pdf, number); + break; + default: + pdf_select_layer_config_ui(gctx, pdf, number); + break; + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(get_ocgs, !result) + CLOSECHECK0(get_ocgs, """Show existing optional content groups.""") + PyObject * + get_ocgs() + { + PyObject *rc = NULL; + pdf_obj *ci = pdf_new_name(gctx, "CreatorInfo"); + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self); + ASSERT_PDF(pdf); + pdf_obj *ocgs = pdf_dict_getl(gctx, + pdf_dict_get(gctx, + pdf_trailer(gctx, pdf), PDF_NAME(Root)), + PDF_NAME(OCProperties), PDF_NAME(OCGs), NULL); + rc = PyDict_New(); + if (!pdf_is_array(gctx, ocgs)) goto fertig; + int i, n = pdf_array_len(gctx, ocgs); + for (i = 0; i < n; i++) { + pdf_obj *ocg = pdf_array_get(gctx, ocgs, i); + int xref = pdf_to_num(gctx, ocg); + const char *name = pdf_to_text_string(gctx, pdf_dict_get(gctx, ocg, PDF_NAME(Name))); + pdf_obj *obj = pdf_dict_getl(gctx, ocg, PDF_NAME(Usage), ci, PDF_NAME(Subtype), NULL); + const char *usage = NULL; + if (obj) usage = pdf_to_name(gctx, obj); + PyObject *intents = PyList_New(0); + pdf_obj *intent = pdf_dict_get(gctx, ocg, PDF_NAME(Intent)); + if (intent) { + if (pdf_is_name(gctx, intent)) { + LIST_APPEND_DROP(intents, Py_BuildValue("s", pdf_to_name(gctx, intent))); + } else if (pdf_is_array(gctx, intent)) { + int j, m = pdf_array_len(gctx, intent); + for (j = 0; j < m; j++) { + pdf_obj *o = pdf_array_get(gctx, intent, j); + if (pdf_is_name(gctx, o)) + LIST_APPEND_DROP(intents, Py_BuildValue("s", pdf_to_name(gctx, o))); + } + } + } + int hidden = pdf_is_ocg_hidden(gctx, pdf, NULL, usage, ocg); + PyObject *item = Py_BuildValue("{s:s,s:O,s:O,s:s}", + "name", name, + "intent", intents, + "on", JM_BOOL(!hidden), + "usage", usage); + Py_DECREF(intents); + PyObject *temp = Py_BuildValue("i", xref); + DICT_SETITEM_DROP(rc, temp, item); + Py_DECREF(temp); + } + fertig:; + } + fz_always(gctx) { + pdf_drop_obj(gctx, ci); + } + fz_catch(gctx) { + Py_CLEAR(rc); + return NULL; + } + return rc; + } + + + FITZEXCEPTION(add_ocg, !result) + CLOSECHECK0(add_ocg, """Add new optional content group.""") + PyObject * + add_ocg(char *name, int config=-1, int on=1, PyObject *intent=NULL, const char *usage=NULL) + { + int xref = 0; + pdf_obj *obj = NULL, *cfg = NULL; + pdf_obj *indocg = NULL; + pdf_obj *ocg = NULL; + pdf_obj *ci_name = NULL; + fz_var(indocg); + fz_var(ocg); + fz_var(ci_name); + fz_try(gctx) { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) self); + ASSERT_PDF(pdf); + + // ------------------------------ + // make the OCG + // ------------------------------ + ocg = pdf_add_new_dict(gctx, pdf, 3); + pdf_dict_put(gctx, ocg, PDF_NAME(Type), PDF_NAME(OCG)); + pdf_dict_put_text_string(gctx, ocg, PDF_NAME(Name), name); + pdf_obj *intents = pdf_dict_put_array(gctx, ocg, PDF_NAME(Intent), 2); + if (!EXISTS(intent)) { + pdf_array_push(gctx, intents, PDF_NAME(View)); + } else if (!PyUnicode_Check(intent)) { + int i, n = PySequence_Size(intent); + for (i = 0; i < n; i++) { + PyObject *item = PySequence_ITEM(intent, i); + char *c = JM_StrAsChar(item); + if (c) { + pdf_array_push_drop(gctx, intents, pdf_new_name(gctx, c)); + } + Py_DECREF(item); + } + } else { + char *c = JM_StrAsChar(intent); + if (c) { + pdf_array_push_drop(gctx, intents, pdf_new_name(gctx, c)); + } + } + pdf_obj *use_for = pdf_dict_put_dict(gctx, ocg, PDF_NAME(Usage), 3); + ci_name = pdf_new_name(gctx, "CreatorInfo"); + pdf_obj *cre_info = pdf_dict_put_dict(gctx, use_for, ci_name, 2); + pdf_dict_put_text_string(gctx, cre_info, PDF_NAME(Creator), "PyMuPDF"); + if (usage) { + pdf_dict_put_name(gctx, cre_info, PDF_NAME(Subtype), usage); + } else { + pdf_dict_put_name(gctx, cre_info, PDF_NAME(Subtype), "Artwork"); + } + indocg = pdf_add_object(gctx, pdf, ocg); + + // ------------------------------ + // Insert OCG in the right config + // ------------------------------ + pdf_obj *ocp = JM_ensure_ocproperties(gctx, pdf); + obj = pdf_dict_get(gctx, ocp, PDF_NAME(OCGs)); + pdf_array_push(gctx, obj, indocg); + + if (config > -1) { + obj = pdf_dict_get(gctx, ocp, PDF_NAME(Configs)); + if (!pdf_is_array(gctx, obj)) { + RAISEPY(gctx, MSG_BAD_OC_CONFIG, PyExc_ValueError); + } + cfg = pdf_array_get(gctx, obj, config); + if (!cfg) { + RAISEPY(gctx, MSG_BAD_OC_CONFIG, PyExc_ValueError); + } + } else { + cfg = pdf_dict_get(gctx, ocp, PDF_NAME(D)); + } + + obj = pdf_dict_get(gctx, cfg, PDF_NAME(Order)); + if (!obj) { + obj = pdf_dict_put_array(gctx, cfg, PDF_NAME(Order), 1); + } + pdf_array_push(gctx, obj, indocg); + if (on) { + obj = pdf_dict_get(gctx, cfg, PDF_NAME(ON)); + if (!obj) { + obj = pdf_dict_put_array(gctx, cfg, PDF_NAME(ON), 1); + } + } else { + obj = pdf_dict_get(gctx, cfg, PDF_NAME(OFF)); + if (!obj) { + obj = pdf_dict_put_array(gctx, cfg, PDF_NAME(OFF), 1); + } + } + pdf_array_push(gctx, obj, indocg); + + // let MuPDF take note: re-read OCProperties + pdf_read_ocg(gctx, pdf); + + xref = pdf_to_num(gctx, indocg); + } + fz_always(gctx) { + pdf_drop_obj(gctx, indocg); + pdf_drop_obj(gctx, ocg); + pdf_drop_obj(gctx, ci_name); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", xref); + } + + void internal_keep_annot(struct Annot* annot) + { + pdf_keep_annot(gctx, (pdf_annot*) annot); + } + + //------------------------------------------------------------------ + // Initialize document: set outline and metadata properties + //------------------------------------------------------------------ + %pythoncode %{ + def init_doc(self): + if self.is_encrypted: + raise ValueError("cannot initialize - document still encrypted") + self._outline = self._loadOutline() + if self._outline: + self._outline.thisown = True + self.metadata = dict([(k,self._getMetadata(v)) for k,v in {'format':'format', 'title':'info:Title', 'author':'info:Author','subject':'info:Subject', 'keywords':'info:Keywords','creator':'info:Creator', 'producer':'info:Producer', 'creationDate':'info:CreationDate', 'modDate':'info:ModDate', 'trapped':'info:Trapped'}.items()]) + self.metadata['encryption'] = None if self._getMetadata('encryption')=='None' else self._getMetadata('encryption') + + outline = property(lambda self: self._outline) + + + def get_page_fonts(self, pno: int, full: bool =False) -> list: + """Retrieve a list of fonts used on a page. + """ + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if not self.is_pdf: + return () + if type(pno) is not int: + try: + pno = pno.number + except: + raise ValueError("need a Page or page number") + val = self._getPageInfo(pno, 1) + if full is False: + return [v[:-1] for v in val] + return val + + + def get_page_images(self, pno: int, full: bool =False) -> list: + """Retrieve a list of images used on a page. + """ + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if not self.is_pdf: + return () + if type(pno) is not int: + try: + pno = pno.number + except: + raise ValueError("need a Page or page number") + val = self._getPageInfo(pno, 2) + if full is False: + return [v[:-1] for v in val] + return val + + + def get_page_xobjects(self, pno: int) -> list: + """Retrieve a list of XObjects used on a page. + """ + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if not self.is_pdf: + return () + if type(pno) is not int: + try: + pno = pno.number + except: + raise ValueError("need a Page or page number") + val = self._getPageInfo(pno, 3) + rc = [(v[0], v[1], v[2], Rect(v[3])) for v in val] + return rc + + + def xref_is_image(self, xref): + """Check if xref is an image object.""" + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if self.xref_get_key(xref, "Subtype")[1] == "/Image": + return True + return False + + def xref_is_font(self, xref): + """Check if xref is a font object.""" + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if self.xref_get_key(xref, "Type")[1] == "/Font": + return True + return False + + def xref_is_xobject(self, xref): + """Check if xref is a form xobject.""" + if self.is_closed or self.is_encrypted: + raise ValueError("document closed or encrypted") + if self.xref_get_key(xref, "Subtype")[1] == "/Form": + return True + return False + + def copy_page(self, pno: int, to: int =-1): + """Copy a page within a PDF document. + + This will only create another reference of the same page object. + Args: + pno: source page number + to: put before this page, '-1' means after last page. + """ + if self.is_closed: + raise ValueError("document closed") + + page_count = len(self) + if ( + pno not in range(page_count) or + to not in range(-1, page_count) + ): + raise ValueError("bad page number(s)") + before = 1 + copy = 1 + if to == -1: + to = page_count - 1 + before = 0 + + return self._move_copy_page(pno, to, before, copy) + + def move_page(self, pno: int, to: int =-1): + """Move a page within a PDF document. + + Args: + pno: source page number. + to: put before this page, '-1' means after last page. + """ + if self.is_closed: + raise ValueError("document closed") + + page_count = len(self) + if ( + pno not in range(page_count) or + to not in range(-1, page_count) + ): + raise ValueError("bad page number(s)") + before = 1 + copy = 0 + if to == -1: + to = page_count - 1 + before = 0 + + return self._move_copy_page(pno, to, before, copy) + + def delete_page(self, pno: int =-1): + """ Delete one page from a PDF. + """ + if not self.is_pdf: + raise ValueError("is no PDF") + if self.is_closed: + raise ValueError("document closed") + + page_count = self.page_count + while pno < 0: + pno += page_count + + if pno >= page_count: + raise ValueError("bad page number(s)") + + # remove TOC bookmarks pointing to deleted page + toc = self.get_toc() + ol_xrefs = self.get_outline_xrefs() + for i, item in enumerate(toc): + if item[2] == pno + 1: + self._remove_toc_item(ol_xrefs[i]) + + self._remove_links_to(frozenset((pno,))) + self._delete_page(pno) + self._reset_page_refs() + + + def delete_pages(self, *args, **kw): + """Delete pages from a PDF. + + Args: + Either keywords 'from_page'/'to_page', or two integers to + specify the first/last page to delete. + Or a list/tuple/range object, which can contain arbitrary + page numbers. + """ + if not self.is_pdf: + raise ValueError("is no PDF") + if self.is_closed: + raise ValueError("document closed") + + page_count = self.page_count # page count of document + f = t = -1 + if kw: # check if keywords were used + if args: # then no positional args are allowed + raise ValueError("cannot mix keyword and positional argument") + f = kw.get("from_page", -1) # first page to delete + t = kw.get("to_page", -1) # last page to delete + while f < 0: + f += page_count + while t < 0: + t += page_count + if not f <= t < page_count: + raise ValueError("bad page number(s)") + numbers = tuple(range(f, t + 1)) + else: + if len(args) > 2 or args == []: + raise ValueError("need 1 or 2 positional arguments") + if len(args) == 2: + f, t = args + if not (type(f) is int and type(t) is int): + raise ValueError("both arguments must be int") + if f > t: + f, t = t, f + if not f <= t < page_count: + raise ValueError("bad page number(s)") + numbers = tuple(range(f, t + 1)) + else: + r = args[0] + if type(r) not in (int, range, list, tuple): + raise ValueError("need int or sequence if one argument") + numbers = tuple(r) + + numbers = list(map(int, set(numbers))) # ensure unique integers + if numbers == []: + print("nothing to delete") + return + numbers.sort() + if numbers[0] < 0 or numbers[-1] >= page_count: + raise ValueError("bad page number(s)") + frozen_numbers = frozenset(numbers) + toc = self.get_toc() + for i, xref in enumerate(self.get_outline_xrefs()): + if toc[i][2] - 1 in frozen_numbers: + self._remove_toc_item(xref) # remove target in PDF object + + self._remove_links_to(frozen_numbers) + + for i in reversed(numbers): # delete pages, last to first + self._delete_page(i) + + self._reset_page_refs() + + + def saveIncr(self): + """ Save PDF incrementally""" + return self.save(self.name, incremental=True, encryption=PDF_ENCRYPT_KEEP) + + + def ez_save(self, filename, garbage=3, clean=False, + deflate=True, deflate_images=True, deflate_fonts=True, + incremental=False, ascii=False, expand=False, linear=False, + pretty=False, encryption=1, permissions=4095, + owner_pw=None, user_pw=None, no_new_id=True): + """ Save PDF using some different defaults""" + return self.save(filename, garbage=garbage, + clean=clean, + deflate=deflate, + deflate_images=deflate_images, + deflate_fonts=deflate_fonts, + incremental=incremental, + ascii=ascii, + expand=expand, + linear=linear, + pretty=pretty, + encryption=encryption, + permissions=permissions, + owner_pw=owner_pw, + user_pw=user_pw, + no_new_id=no_new_id,) + + + def reload_page(self, page: "struct Page *") -> "struct Page *": + """Make a fresh copy of a page.""" + old_annots = {} # copy annot references to here + pno = page.number # save the page number + for k, v in page._annot_refs.items(): # save the annot dictionary + # We need to call pdf_keep_annot() here, otherwise `v`'s + # refcount can reach zero even if there is an external + # reference. + self.internal_keep_annot(v) + old_annots[k] = v + page._erase() # remove the page + page = None + page = self.load_page(pno) # reload the page + + # copy annot refs over to the new dictionary + page_proxy = weakref.proxy(page) + for k, v in old_annots.items(): + annot = old_annots[k] + annot.parent = page_proxy # refresh parent to new page + page._annot_refs[k] = annot + return page + + + @property + def pagemode(self) -> str: + """Return the PDF PageMode value. + """ + xref = self.pdf_catalog() + if xref == 0: + return None + rc = self.xref_get_key(xref, "PageMode") + if rc[0] == "null": + return "UseNone" + if rc[0] == "name": + return rc[1][1:] + return "UseNone" + + + def set_pagemode(self, pagemode: str): + """Set the PDF PageMode value.""" + valid = ("UseNone", "UseOutlines", "UseThumbs", "FullScreen", "UseOC", "UseAttachments") + xref = self.pdf_catalog() + if xref == 0: + raise ValueError("not a PDF") + if not pagemode: + raise ValueError("bad PageMode value") + if pagemode[0] == "/": + pagemode = pagemode[1:] + for v in valid: + if pagemode.lower() == v.lower(): + self.xref_set_key(xref, "PageMode", f"/{v}") + return True + raise ValueError("bad PageMode value") + + + @property + def pagelayout(self) -> str: + """Return the PDF PageLayout value. + """ + xref = self.pdf_catalog() + if xref == 0: + return None + rc = self.xref_get_key(xref, "PageLayout") + if rc[0] == "null": + return "SinglePage" + if rc[0] == "name": + return rc[1][1:] + return "SinglePage" + + + def set_pagelayout(self, pagelayout: str): + """Set the PDF PageLayout value.""" + valid = ("SinglePage", "OneColumn", "TwoColumnLeft", "TwoColumnRight", "TwoPageLeft", "TwoPageRight") + xref = self.pdf_catalog() + if xref == 0: + raise ValueError("not a PDF") + if not pagelayout: + raise ValueError("bad PageLayout value") + if pagelayout[0] == "/": + pagelayout = pagelayout[1:] + for v in valid: + if pagelayout.lower() == v.lower(): + self.xref_set_key(xref, "PageLayout", f"/{v}") + return True + raise ValueError("bad PageLayout value") + + + @property + def markinfo(self) -> dict: + """Return the PDF MarkInfo value.""" + xref = self.pdf_catalog() + if xref == 0: + return None + rc = self.xref_get_key(xref, "MarkInfo") + if rc[0] == "null": + return {} + if rc[0] == "xref": + xref = int(rc[1].split()[0]) + val = self.xref_object(xref, compressed=True) + elif rc[0] == "dict": + val = rc[1] + else: + val = None + if val == None or not (val[:2] == "<<" and val[-2:] == ">>"): + return {} + valid = {"Marked": False, "UserProperties": False, "Suspects": False} + val = val[2:-2].split("/") + for v in val[1:]: + try: + key, value = v.split() + except: + return valid + if value == "true": + valid[key] = True + return valid + + + def set_markinfo(self, markinfo: dict) -> bool: + """Set the PDF MarkInfo values.""" + xref = self.pdf_catalog() + if xref == 0: + raise ValueError("not a PDF") + if not markinfo or not isinstance(markinfo, dict): + return False + valid = {"Marked": False, "UserProperties": False, "Suspects": False} + + if not set(valid.keys()).issuperset(markinfo.keys()): + badkeys = f"bad MarkInfo key(s): {set(markinfo.keys()).difference(valid.keys())}" + raise ValueError(badkeys) + pdfdict = "<<" + valid.update(markinfo) + for key, value in valid.items(): + value=str(value).lower() + if not value in ("true", "false"): + raise ValueError(f"bad key value '{key}': '{value}'") + pdfdict += f"/{key} {value}" + pdfdict += ">>" + self.xref_set_key(xref, "MarkInfo", pdfdict) + return True + + + def __repr__(self) -> str: + m = "closed " if self.is_closed else "" + if self.stream is None: + if self.name == "": + return m + "Document()" % self._graft_id + return m + "Document('%s')" % (self.name,) + return m + "Document('%s', )" % (self.name, self._graft_id) + + + def __contains__(self, loc) -> bool: + if type(loc) is int: + if loc < self.page_count: + return True + return False + if type(loc) not in (tuple, list) or len(loc) != 2: + return False + + chapter, pno = loc + if (type(chapter) != int or + chapter < 0 or + chapter >= self.chapter_count + ): + return False + if (type(pno) != int or + pno < 0 or + pno >= self.chapter_page_count(chapter) + ): + return False + + return True + + + def __getitem__(self, i: int =0)->"Page": + if i not in self: + raise IndexError("page not in document") + return self.load_page(i) + + + def __delitem__(self, i: AnyType)->None: + if not self.is_pdf: + raise ValueError("is no PDF") + if type(i) is int: + return self.delete_page(i) + if type(i) in (list, tuple, range): + return self.delete_pages(i) + if type(i) is not slice: + raise ValueError("bad argument type") + pc = self.page_count + start = i.start if i.start else 0 + stop = i.stop if i.stop else pc + step = i.step if i.step else 1 + while start < 0: + start += pc + if start >= pc: + raise ValueError("bad page number(s)") + while stop < 0: + stop += pc + if stop > pc: + raise ValueError("bad page number(s)") + return self.delete_pages(range(start, stop, step)) + + + def pages(self, start: OptInt =None, stop: OptInt =None, step: OptInt =None): + """Return a generator iterator over a page range. + + Arguments have the same meaning as for the range() built-in. + """ + # set the start value + start = start or 0 + while start < 0: + start += self.page_count + if start not in range(self.page_count): + raise ValueError("bad start page number") + + # set the stop value + stop = stop if stop is not None and stop <= self.page_count else self.page_count + + # set the step value + if step == 0: + raise ValueError("arg 3 must not be zero") + if step is None: + if start > stop: + step = -1 + else: + step = 1 + + for pno in range(start, stop, step): + yield (self.load_page(pno)) + + + def __len__(self) -> int: + return self.page_count + + def _forget_page(self, page: "struct Page *"): + """Remove a page from document page dict.""" + pid = id(page) + if pid in self._page_refs: + self._page_refs[pid] = None + + def _reset_page_refs(self): + """Invalidate all pages in document dictionary.""" + if getattr(self, "is_closed", True): + return + for page in self._page_refs.values(): + if page: + page._erase() + page = None + self._page_refs.clear() + + + + def _cleanup(self): + self._reset_page_refs() + for k in self.Graftmaps.keys(): + self.Graftmaps[k] = None + self.Graftmaps = {} + self.ShownPages = {} + self.InsertedImages = {} + self.FontInfos = [] + self.metadata = None + self.stream = None + self.is_closed = True + + + def close(self): + """Close the document.""" + if getattr(self, "is_closed", False): + raise ValueError("document closed") + self._cleanup() + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + return + else: + raise RuntimeError("document object unavailable") + + def __del__(self): + if not type(self) is Document: + return + self._cleanup() + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + %} + } +}; + +/*****************************************************************************/ +// fz_page +/*****************************************************************************/ +%nodefaultctor; +struct Page { + %extend { + ~Page() + { + DEBUGMSG1("Page"); + fz_page *this_page = (fz_page *) $self; + fz_drop_page(gctx, this_page); + DEBUGMSG2; + } + //---------------------------------------------------------------- + // bound() + //---------------------------------------------------------------- + FITZEXCEPTION(bound, !result) + PARENTCHECK(bound, """Get page rectangle.""") + %pythonappend bound %{ + val = Rect(val) + if val.is_infinite and self.parent.is_pdf: + cb = self.cropbox + w, h = cb.width, cb.height + if self.rotation not in (0, 180): + w, h = h, w + val = Rect(0, 0, w, h) + msg = TOOLS.mupdf_warnings(reset=False).splitlines()[-1] + print(msg, file=sys.stderr) + %} + PyObject *bound() { + fz_rect rect = fz_infinite_rect; + fz_try(gctx) { + rect = fz_bound_page(gctx, (fz_page *) $self); + } + fz_catch(gctx) { + ; + } + return JM_py_from_rect(rect); + } + %pythoncode %{rect = property(bound, doc="page rectangle")%} + + //---------------------------------------------------------------- + // Page.get_image_bbox + //---------------------------------------------------------------- + %pythonprepend get_image_bbox %{ + """Get rectangle occupied by image 'name'. + + 'name' is either an item of the image list, or the referencing + name string - elem[7] of the resp. item. + Option 'transform' also returns the image transformation matrix. + """ + CheckParent(self) + doc = self.parent + if doc.is_closed or doc.is_encrypted: + raise ValueError("document closed or encrypted") + + inf_rect = Rect(1, 1, -1, -1) + null_mat = Matrix() + if transform: + rc = (inf_rect, null_mat) + else: + rc = inf_rect + + if type(name) in (list, tuple): + if not type(name[-1]) is int: + raise ValueError("need item of full page image list") + item = name + else: + imglist = [i for i in doc.get_page_images(self.number, True) if name == i[7]] + if len(imglist) == 1: + item = imglist[0] + elif imglist == []: + raise ValueError("bad image name") + else: + raise ValueError("found multiple images named '%s'." % name) + xref = item[-1] + if xref != 0 or transform == True: + try: + return self.get_image_rects(item, transform=transform)[0] + except: + return inf_rect + %} + %pythonappend get_image_bbox %{ + if not bool(val): + return rc + + for v in val: + if v[0] != item[-3]: + continue + q = Quad(v[1]) + bbox = q.rect + if transform == 0: + rc = bbox + break + + hm = Matrix(util_hor_matrix(q.ll, q.lr)) + h = abs(q.ll - q.ul) + w = abs(q.ur - q.ul) + m0 = Matrix(1 / w, 0, 0, 1 / h, 0, 0) + m = ~(hm * m0) + rc = (bbox, m) + break + val = rc%} + PyObject * + get_image_bbox(PyObject *name, int transform=0) + { + pdf_page *pdf_page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + PyObject *rc =NULL; + fz_try(gctx) { + rc = JM_image_reporter(gctx, pdf_page); + } + fz_catch(gctx) { + Py_RETURN_NONE; + } + return rc; + } + + //---------------------------------------------------------------- + // run() + //---------------------------------------------------------------- + FITZEXCEPTION(run, !result) + PARENTCHECK(run, """Run page through a device.""") + PyObject *run(struct DeviceWrapper *dw, PyObject *m) + { + fz_try(gctx) { + fz_run_page(gctx, (fz_page *) $self, dw->device, JM_matrix_from_py(m), NULL); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------------------------------------- + // Page.extend_textpage + //---------------------------------------------------------------- + FITZEXCEPTION(extend_textpage, !result) + PyObject * + extend_textpage(struct TextPage *tpage, int flags=0, PyObject *matrix=NULL) + { + fz_page *page = (fz_page *) $self; + fz_stext_page *tp = (fz_stext_page *) tpage; + fz_device *dev = NULL; + fz_stext_options options; + memset(&options, 0, sizeof options); + options.flags = flags; + fz_try(gctx) { + fz_matrix ctm = JM_matrix_from_py(matrix); + dev = fz_new_stext_device(gctx, tp, &options); + fz_run_page(gctx, page, dev, ctm, NULL); + fz_close_device(gctx, dev); + } + fz_always(gctx) { + fz_drop_device(gctx, dev); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // Page.get_textpage + //---------------------------------------------------------------- + FITZEXCEPTION(_get_textpage, !result) + %pythonappend _get_textpage %{val.thisown = True%} + struct TextPage * + _get_textpage(PyObject *clip=NULL, int flags=0, PyObject *matrix=NULL) + { + fz_stext_page *tpage=NULL; + fz_page *page = (fz_page *) $self; + fz_device *dev = NULL; + fz_stext_options options; + memset(&options, 0, sizeof options); + options.flags = flags; + fz_try(gctx) { + // Default to page's rect if `clip` not specified, for #2048. + fz_rect rect = (clip==Py_None) ? fz_bound_page(gctx, page) : JM_rect_from_py(clip); + fz_matrix ctm = JM_matrix_from_py(matrix); + tpage = fz_new_stext_page(gctx, rect); + dev = fz_new_stext_device(gctx, tpage, &options); + fz_run_page(gctx, page, dev, ctm, NULL); + fz_close_device(gctx, dev); + } + fz_always(gctx) { + fz_drop_device(gctx, dev); + } + fz_catch(gctx) { + return NULL; + } + return (struct TextPage *) tpage; + } + + + %pythoncode %{ + def get_textpage(self, clip: rect_like = None, flags: int = 0, matrix=None) -> "TextPage": + CheckParent(self) + if matrix is None: + matrix = Matrix(1, 1) + old_rotation = self.rotation + if old_rotation != 0: + self.set_rotation(0) + try: + textpage = self._get_textpage(clip, flags=flags, matrix=matrix) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + textpage.parent = weakref.proxy(self) + return textpage + %} + + /* ****************** currently inactive + //---------------------------------------------------------------- + // Page._get_textpage_ocr + //---------------------------------------------------------------- + FITZEXCEPTION(_get_textpage_ocr, !result) + %pythonappend _get_textpage_ocr %{val.thisown = True%} + struct TextPage * + _get_textpage_ocr(PyObject *clip=NULL, int flags=0, const char *language=NULL, const char *tessdata=NULL) + { + fz_stext_page *textpage=NULL; + fz_try(gctx) { + fz_rect rect = JM_rect_from_py(clip); + textpage = JM_new_stext_page_ocr_from_page(gctx, (fz_page *) $self, rect, flags, language, tessdata); + } + fz_catch(gctx) { + return NULL; + } + return (struct TextPage *) textpage; + } + ************************* */ + + //---------------------------------------------------------------- + // Page.language + //---------------------------------------------------------------- + %pythoncode%{@property%} + %pythonprepend language %{"""Page language."""%} + PyObject *language() + { + pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (!pdfpage) Py_RETURN_NONE; + pdf_obj *lang = pdf_dict_get_inheritable(gctx, pdfpage->obj, PDF_NAME(Lang)); + if (!lang) Py_RETURN_NONE; + return Py_BuildValue("s", pdf_to_str_buf(gctx, lang)); + } + + + //---------------------------------------------------------------- + // Page.set_language + //---------------------------------------------------------------- + FITZEXCEPTION(set_language, !result) + PARENTCHECK(set_language, """Set PDF page default language.""") + PyObject *set_language(char *language=NULL) + { + pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) $self); + fz_try(gctx) { + ASSERT_PDF(pdfpage); + fz_text_language lang; + char buf[8]; + if (!language) { + pdf_dict_del(gctx, pdfpage->obj, PDF_NAME(Lang)); + } else { + lang = fz_text_language_from_string(language); + pdf_dict_put_text_string(gctx, pdfpage->obj, + PDF_NAME(Lang), + fz_string_from_text_language(buf, lang)); + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_TRUE; + } + + + //---------------------------------------------------------------- + // Page.get_svg_image + //---------------------------------------------------------------- + FITZEXCEPTION(get_svg_image, !result) + PARENTCHECK(get_svg_image, """Make SVG image from page.""") + PyObject *get_svg_image(PyObject *matrix = NULL, int text_as_path=1) + { + fz_rect mediabox = fz_bound_page(gctx, (fz_page *) $self); + fz_device *dev = NULL; + fz_buffer *res = NULL; + PyObject *text = NULL; + fz_matrix ctm = JM_matrix_from_py(matrix); + fz_output *out = NULL; + fz_var(out); + fz_var(dev); + fz_var(res); + fz_rect tbounds = mediabox; + int text_option = (text_as_path == 1) ? FZ_SVG_TEXT_AS_PATH : FZ_SVG_TEXT_AS_TEXT; + tbounds = fz_transform_rect(tbounds, ctm); + + fz_try(gctx) { + res = fz_new_buffer(gctx, 1024); + out = fz_new_output_with_buffer(gctx, res); + dev = fz_new_svg_device(gctx, out, + tbounds.x1-tbounds.x0, // width + tbounds.y1-tbounds.y0, // height + text_option, 1); + fz_run_page(gctx, (fz_page *) $self, dev, ctm, NULL); + fz_close_device(gctx, dev); + text = JM_EscapeStrFromBuffer(gctx, res); + } + fz_always(gctx) { + fz_drop_device(gctx, dev); + fz_drop_output(gctx, out); + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + return text; + } + + + //---------------------------------------------------------------- + // page set opacity + //---------------------------------------------------------------- + FITZEXCEPTION(_set_opacity, !result) + %pythonprepend _set_opacity %{ + if CA >= 1 and ca >= 1 and blendmode == None: + return None + tCA = int(round(max(CA , 0) * 100)) + if tCA >= 100: + tCA = 99 + tca = int(round(max(ca, 0) * 100)) + if tca >= 100: + tca = 99 + gstate = "fitzca%02i%02i" % (tCA, tca) + %} + PyObject * + _set_opacity(char *gstate=NULL, float CA=1, float ca=1, char *blendmode=NULL) + { + if (!gstate) Py_RETURN_NONE; + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + fz_try(gctx) { + ASSERT_PDF(page); + pdf_obj *resources = pdf_dict_get(gctx, page->obj, PDF_NAME(Resources)); + if (!resources) { + resources = pdf_dict_put_dict(gctx, page->obj, PDF_NAME(Resources), 2); + } + pdf_obj *extg = pdf_dict_get(gctx, resources, PDF_NAME(ExtGState)); + if (!extg) { + extg = pdf_dict_put_dict(gctx, resources, PDF_NAME(ExtGState), 2); + } + int i, n = pdf_dict_len(gctx, extg); + for (i = 0; i < n; i++) { + pdf_obj *o1 = pdf_dict_get_key(gctx, extg, i); + char *name = (char *) pdf_to_name(gctx, o1); + if (strcmp(name, gstate) == 0) goto finished; + } + pdf_obj *opa = pdf_new_dict(gctx, page->doc, 3); + pdf_dict_put_real(gctx, opa, PDF_NAME(CA), (double) CA); + pdf_dict_put_real(gctx, opa, PDF_NAME(ca), (double) ca); + pdf_dict_puts_drop(gctx, extg, gstate, opa); + finished:; + } + fz_always(gctx) { + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("s", gstate); + } + + //---------------------------------------------------------------- + // page add_caret_annot + //---------------------------------------------------------------- + FITZEXCEPTION(_add_caret_annot, !result) + struct Annot * + _add_caret_annot(PyObject *point) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + fz_try(gctx) { + annot = pdf_create_annot(gctx, page, PDF_ANNOT_CARET); + if (point) + { + fz_point p = JM_point_from_py(point); + fz_rect r = pdf_annot_rect(gctx, annot); + r = fz_make_rect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0); + pdf_set_annot_rect(gctx, annot, r); + } + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + + //---------------------------------------------------------------- + // page addRedactAnnot + //---------------------------------------------------------------- + FITZEXCEPTION(_add_redact_annot, !result) + struct Annot * + _add_redact_annot(PyObject *quad, + PyObject *text=NULL, + PyObject *da_str=NULL, + int align=0, + PyObject *fill=NULL, + PyObject *text_color=NULL) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + float fcol[4] = { 1, 1, 1, 0}; + int nfcol = 0, i; + fz_try(gctx) { + annot = pdf_create_annot(gctx, page, PDF_ANNOT_REDACT); + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + fz_quad q = JM_quad_from_py(quad); + fz_rect r = fz_rect_from_quad(q); + + // TODO calculate de-rotated rect + pdf_set_annot_rect(gctx, annot, r); + if (EXISTS(fill)) { + JM_color_FromSequence(fill, &nfcol, fcol); + pdf_obj *arr = pdf_new_array(gctx, page->doc, nfcol); + for (i = 0; i < nfcol; i++) { + pdf_array_push_real(gctx, arr, fcol[i]); + } + pdf_dict_put_drop(gctx, annot_obj, PDF_NAME(IC), arr); + } + if (EXISTS(text)) { + const char *otext = PyUnicode_AsUTF8(text); + pdf_dict_puts_drop(gctx, annot_obj, "OverlayText", + pdf_new_text_string(gctx, otext)); + pdf_dict_put_text_string(gctx,annot_obj, PDF_NAME(DA), PyUnicode_AsUTF8(da_str)); + pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Q), (int64_t) align); + } + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + //---------------------------------------------------------------- + // page addLineAnnot + //---------------------------------------------------------------- + FITZEXCEPTION(_add_line_annot, !result) + struct Annot * + _add_line_annot(PyObject *p1, PyObject *p2) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + fz_try(gctx) { + ASSERT_PDF(page); + annot = pdf_create_annot(gctx, page, PDF_ANNOT_LINE); + fz_point a = JM_point_from_py(p1); + fz_point b = JM_point_from_py(p2); + pdf_set_annot_line(gctx, annot, a, b); + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + //---------------------------------------------------------------- + // page addTextAnnot + //---------------------------------------------------------------- + FITZEXCEPTION(_add_text_annot, !result) + struct Annot * + _add_text_annot(PyObject *point, + char *text, + char *icon=NULL) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + fz_rect r; + fz_point p = JM_point_from_py(point); + fz_var(annot); + fz_try(gctx) { + ASSERT_PDF(page); + annot = pdf_create_annot(gctx, page, PDF_ANNOT_TEXT); + r = pdf_annot_rect(gctx, annot); + r = fz_make_rect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0); + pdf_set_annot_rect(gctx, annot, r); + pdf_set_annot_contents(gctx, annot, text); + if (icon) { + pdf_set_annot_icon_name(gctx, annot, icon); + } + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + //---------------------------------------------------------------- + // page addInkAnnot + //---------------------------------------------------------------- + FITZEXCEPTION(_add_ink_annot, !result) + struct Annot * + _add_ink_annot(PyObject *list) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + PyObject *p = NULL, *sublist = NULL; + pdf_obj *inklist = NULL, *stroke = NULL; + fz_matrix ctm, inv_ctm; + fz_point point; + fz_var(annot); + fz_try(gctx) { + ASSERT_PDF(page); + if (!PySequence_Check(list)) { + RAISEPY(gctx, MSG_BAD_ARG_INK_ANNOT, PyExc_ValueError); + } + pdf_page_transform(gctx, page, NULL, &ctm); + inv_ctm = fz_invert_matrix(ctm); + annot = pdf_create_annot(gctx, page, PDF_ANNOT_INK); + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + Py_ssize_t i, j, n0 = PySequence_Size(list), n1; + inklist = pdf_new_array(gctx, page->doc, n0); + + for (j = 0; j < n0; j++) { + sublist = PySequence_ITEM(list, j); + n1 = PySequence_Size(sublist); + stroke = pdf_new_array(gctx, page->doc, 2 * n1); + + for (i = 0; i < n1; i++) { + p = PySequence_ITEM(sublist, i); + if (!PySequence_Check(p) || PySequence_Size(p) != 2) { + RAISEPY(gctx, MSG_BAD_ARG_INK_ANNOT, PyExc_ValueError); + } + point = fz_transform_point(JM_point_from_py(p), inv_ctm); + Py_CLEAR(p); + pdf_array_push_real(gctx, stroke, point.x); + pdf_array_push_real(gctx, stroke, point.y); + } + + pdf_array_push_drop(gctx, inklist, stroke); + stroke = NULL; + Py_CLEAR(sublist); + } + + pdf_dict_put_drop(gctx, annot_obj, PDF_NAME(InkList), inklist); + inklist = NULL; + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + + fz_catch(gctx) { + Py_CLEAR(p); + Py_CLEAR(sublist); + return NULL; + } + return (struct Annot *) annot; + } + + //---------------------------------------------------------------- + // page addStampAnnot + //---------------------------------------------------------------- + FITZEXCEPTION(_add_stamp_annot, !result) + struct Annot * + _add_stamp_annot(PyObject *rect, int stamp=0) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + pdf_obj *stamp_id[] = {PDF_NAME(Approved), PDF_NAME(AsIs), + PDF_NAME(Confidential), PDF_NAME(Departmental), + PDF_NAME(Experimental), PDF_NAME(Expired), + PDF_NAME(Final), PDF_NAME(ForComment), + PDF_NAME(ForPublicRelease), PDF_NAME(NotApproved), + PDF_NAME(NotForPublicRelease), PDF_NAME(Sold), + PDF_NAME(TopSecret), PDF_NAME(Draft)}; + int n = nelem(stamp_id); + pdf_obj *name = stamp_id[0]; + fz_try(gctx) { + ASSERT_PDF(page); + fz_rect r = JM_rect_from_py(rect); + if (fz_is_infinite_rect(r) || fz_is_empty_rect(r)) { + RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError); + } + if (INRANGE(stamp, 0, n-1)) { + name = stamp_id[stamp]; + } + annot = pdf_create_annot(gctx, page, PDF_ANNOT_STAMP); + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_set_annot_rect(gctx, annot, r); + pdf_dict_put(gctx, annot_obj, PDF_NAME(Name), name); + pdf_set_annot_contents(gctx, annot, + pdf_dict_get_name(gctx, annot_obj, PDF_NAME(Name))); + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + //---------------------------------------------------------------- + // page addFileAnnot + //---------------------------------------------------------------- + FITZEXCEPTION(_add_file_annot, !result) + struct Annot * + _add_file_annot(PyObject *point, + PyObject *buffer, + char *filename, + char *ufilename=NULL, + char *desc=NULL, + char *icon=NULL) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + char *uf = ufilename, *d = desc; + if (!ufilename) uf = filename; + if (!desc) d = filename; + fz_buffer *filebuf = NULL; + fz_rect r; + fz_point p = JM_point_from_py(point); + fz_var(filebuf); + fz_try(gctx) { + ASSERT_PDF(page); + filebuf = JM_BufferFromBytes(gctx, buffer); + if (!filebuf) { + RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_TypeError); + } + annot = pdf_create_annot(gctx, page, PDF_ANNOT_FILE_ATTACHMENT); + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + r = pdf_annot_rect(gctx, annot); + r = fz_make_rect(p.x, p.y, p.x + r.x1 - r.x0, p.y + r.y1 - r.y0); + pdf_set_annot_rect(gctx, annot, r); + int flags = PDF_ANNOT_IS_PRINT; + pdf_set_annot_flags(gctx, annot, flags); + + if (icon) + pdf_set_annot_icon_name(gctx, annot, icon); + + pdf_obj *val = JM_embed_file(gctx, page->doc, filebuf, + filename, uf, d, 1); + pdf_dict_put_drop(gctx, annot_obj, PDF_NAME(FS), val); + pdf_dict_put_text_string(gctx, annot_obj, PDF_NAME(Contents), filename); + pdf_update_annot(gctx, annot); + pdf_set_annot_rect(gctx, annot, r); + pdf_set_annot_flags(gctx, annot, flags); + JM_add_annot_id(gctx, annot, "A"); + } + fz_always(gctx) { + fz_drop_buffer(gctx, filebuf); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + + //---------------------------------------------------------------- + // page: add a text marker annotation + //---------------------------------------------------------------- + FITZEXCEPTION(_add_text_marker, !result) + %pythonprepend _add_text_marker %{ + CheckParent(self) + if not self.parent.is_pdf: + raise ValueError("is no PDF")%} + + %pythonappend _add_text_marker %{ + if not val: + return None + val.parent = weakref.proxy(self) + self._annot_refs[id(val)] = val%} + + struct Annot * + _add_text_marker(PyObject *quads, int annot_type) + { + pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + PyObject *item = NULL; + int rotation = JM_page_rotation(gctx, pdfpage); + fz_quad q; + fz_var(annot); + fz_var(item); + fz_try(gctx) { + if (rotation != 0) { + pdf_dict_put_int(gctx, pdfpage->obj, PDF_NAME(Rotate), 0); + } + annot = pdf_create_annot(gctx, pdfpage, annot_type); + Py_ssize_t i, len = PySequence_Size(quads); + for (i = 0; i < len; i++) { + item = PySequence_ITEM(quads, i); + q = JM_quad_from_py(item); + Py_DECREF(item); + pdf_add_annot_quad_point(gctx, annot, q); + } + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + fz_always(gctx) { + if (rotation != 0) { + pdf_dict_put_int(gctx, pdfpage->obj, PDF_NAME(Rotate), rotation); + } + } + fz_catch(gctx) { + pdf_drop_annot(gctx, annot); + return NULL; + } + return (struct Annot *) annot; + } + + + //---------------------------------------------------------------- + // page: add circle or rectangle annotation + //---------------------------------------------------------------- + FITZEXCEPTION(_add_square_or_circle, !result) + struct Annot * + _add_square_or_circle(PyObject *rect, int annot_type) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + fz_try(gctx) { + fz_rect r = JM_rect_from_py(rect); + if (fz_is_infinite_rect(r) || fz_is_empty_rect(r)) { + RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError); + } + annot = pdf_create_annot(gctx, page, annot_type); + pdf_set_annot_rect(gctx, annot, r); + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + + //---------------------------------------------------------------- + // page: add multiline annotation + //---------------------------------------------------------------- + FITZEXCEPTION(_add_multiline, !result) + struct Annot * + _add_multiline(PyObject *points, int annot_type) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *annot = NULL; + fz_try(gctx) { + Py_ssize_t i, n = PySequence_Size(points); + if (n < 2) { + RAISEPY(gctx, MSG_BAD_ARG_POINTS, PyExc_ValueError); + } + annot = pdf_create_annot(gctx, page, annot_type); + for (i = 0; i < n; i++) { + PyObject *p = PySequence_ITEM(points, i); + if (PySequence_Size(p) != 2) { + Py_DECREF(p); + RAISEPY(gctx, MSG_BAD_ARG_POINTS, PyExc_ValueError); + } + fz_point point = JM_point_from_py(p); + Py_DECREF(p); + pdf_add_annot_vertex(gctx, annot, point); + } + + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + + //---------------------------------------------------------------- + // page addFreetextAnnot + //---------------------------------------------------------------- + FITZEXCEPTION(_add_freetext_annot, !result) + %pythonappend _add_freetext_annot %{ + ap = val._getAP() + BT = ap.find(b"BT") + ET = ap.find(b"ET") + 2 + ap = ap[BT:ET] + w = rect[2]-rect[0] + h = rect[3]-rect[1] + if rotate in (90, -90, 270): + w, h = h, w + re = b"0 0 %g %g re" % (w, h) + ap = re + b"\nW\nn\n" + ap + ope = None + bwidth = b"" + fill_string = ColorCode(fill_color, "f").encode() + if fill_string: + fill_string += b"\n" + ope = b"f" + stroke_string = ColorCode(border_color, "c").encode() + if stroke_string: + stroke_string += b"\n" + bwidth = b"1 w\n" + ope = b"S" + if fill_string and stroke_string: + ope = b"B" + if ope != None: + ap = bwidth + fill_string + stroke_string + re + b"\n" + ope + b"\n" + ap + val._setAP(ap) + %} + struct Annot * + _add_freetext_annot(PyObject *rect, char *text, + float fontsize=11, + char *fontname=NULL, + PyObject *text_color=NULL, + PyObject *fill_color=NULL, + PyObject *border_color=NULL, + int align=0, + int rotate=0) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + float fcol[4] = {1, 1, 1, 1}; // fill color: white + int nfcol = 0; + JM_color_FromSequence(fill_color, &nfcol, fcol); + float tcol[4] = {0, 0, 0, 0}; // std. text color: black + int ntcol = 0; + JM_color_FromSequence(text_color, &ntcol, tcol); + fz_rect r = JM_rect_from_py(rect); + pdf_annot *annot = NULL; + fz_try(gctx) { + if (fz_is_infinite_rect(r) || fz_is_empty_rect(r)) { + RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError); + } + annot = pdf_create_annot(gctx, page, PDF_ANNOT_FREE_TEXT); + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_set_annot_contents(gctx, annot, text); + pdf_set_annot_rect(gctx, annot, r); + pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Rotate), rotate); + pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Q), align); + + if (nfcol > 0) { + pdf_set_annot_color(gctx, annot, nfcol, fcol); + } + + // insert the default appearance string + JM_make_annot_DA(gctx, annot, ntcol, tcol, fontname, fontsize); + pdf_update_annot(gctx, annot); + JM_add_annot_id(gctx, annot, "A"); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + + %pythoncode %{ + @property + def rotation_matrix(self) -> Matrix: + """Reflects page rotation.""" + return Matrix(TOOLS._rotate_matrix(self)) + + @property + def derotation_matrix(self) -> Matrix: + """Reflects page de-rotation.""" + return Matrix(TOOLS._derotate_matrix(self)) + + def add_caret_annot(self, point: point_like) -> "struct Annot *": + """Add a 'Caret' annotation.""" + old_rotation = annot_preprocess(self) + try: + annot = self._add_caret_annot(point) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_strikeout_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *": + """Add a 'StrikeOut' annotation.""" + if quads is None: + q = get_highlight_selection(self, start=start, stop=stop, clip=clip) + else: + q = CheckMarkerArg(quads) + return self._add_text_marker(q, PDF_ANNOT_STRIKE_OUT) + + + def add_underline_annot(self, quads=None, start=None, stop=None, clip=None) -> "struct Annot *": + """Add a 'Underline' annotation.""" + if quads is None: + q = get_highlight_selection(self, start=start, stop=stop, clip=clip) + else: + q = CheckMarkerArg(quads) + return self._add_text_marker(q, PDF_ANNOT_UNDERLINE) + + + def add_squiggly_annot(self, quads=None, start=None, + stop=None, clip=None) -> "struct Annot *": + """Add a 'Squiggly' annotation.""" + if quads is None: + q = get_highlight_selection(self, start=start, stop=stop, clip=clip) + else: + q = CheckMarkerArg(quads) + return self._add_text_marker(q, PDF_ANNOT_SQUIGGLY) + + + def add_highlight_annot(self, quads=None, start=None, + stop=None, clip=None) -> "struct Annot *": + """Add a 'Highlight' annotation.""" + if quads is None: + q = get_highlight_selection(self, start=start, stop=stop, clip=clip) + else: + q = CheckMarkerArg(quads) + return self._add_text_marker(q, PDF_ANNOT_HIGHLIGHT) + + + def add_rect_annot(self, rect: rect_like) -> "struct Annot *": + """Add a 'Square' (rectangle) annotation.""" + old_rotation = annot_preprocess(self) + try: + annot = self._add_square_or_circle(rect, PDF_ANNOT_SQUARE) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_circle_annot(self, rect: rect_like) -> "struct Annot *": + """Add a 'Circle' (ellipse, oval) annotation.""" + old_rotation = annot_preprocess(self) + try: + annot = self._add_square_or_circle(rect, PDF_ANNOT_CIRCLE) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_text_annot(self, point: point_like, text: str, icon: str ="Note") -> "struct Annot *": + """Add a 'Text' (sticky note) annotation.""" + old_rotation = annot_preprocess(self) + try: + annot = self._add_text_annot(point, text, icon=icon) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_line_annot(self, p1: point_like, p2: point_like) -> "struct Annot *": + """Add a 'Line' annotation.""" + old_rotation = annot_preprocess(self) + try: + annot = self._add_line_annot(p1, p2) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_polyline_annot(self, points: list) -> "struct Annot *": + """Add a 'PolyLine' annotation.""" + old_rotation = annot_preprocess(self) + try: + annot = self._add_multiline(points, PDF_ANNOT_POLY_LINE) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_polygon_annot(self, points: list) -> "struct Annot *": + """Add a 'Polygon' annotation.""" + old_rotation = annot_preprocess(self) + try: + annot = self._add_multiline(points, PDF_ANNOT_POLYGON) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_stamp_annot(self, rect: rect_like, stamp: int =0) -> "struct Annot *": + """Add a ('rubber') 'Stamp' annotation.""" + old_rotation = annot_preprocess(self) + try: + annot = self._add_stamp_annot(rect, stamp) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_ink_annot(self, handwriting: list) -> "struct Annot *": + """Add a 'Ink' ('handwriting') annotation. + + The argument must be a list of lists of point_likes. + """ + old_rotation = annot_preprocess(self) + try: + annot = self._add_ink_annot(handwriting) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_file_annot(self, point: point_like, + buffer: typing.ByteString, + filename: str, + ufilename: OptStr =None, + desc: OptStr =None, + icon: OptStr =None) -> "struct Annot *": + """Add a 'FileAttachment' annotation.""" + + old_rotation = annot_preprocess(self) + try: + annot = self._add_file_annot(point, + buffer, + filename, + ufilename=ufilename, + desc=desc, + icon=icon) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_freetext_annot(self, rect: rect_like, text: str, fontsize: float =11, + fontname: OptStr =None, border_color: OptSeq =None, + text_color: OptSeq =None, + fill_color: OptSeq =None, align: int =0, rotate: int =0) -> "struct Annot *": + """Add a 'FreeText' annotation.""" + + old_rotation = annot_preprocess(self) + try: + annot = self._add_freetext_annot(rect, text, fontsize=fontsize, + fontname=fontname, border_color=border_color,text_color=text_color, + fill_color=fill_color, align=align, rotate=rotate) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + return annot + + + def add_redact_annot(self, quad, text: OptStr =None, fontname: OptStr =None, + fontsize: float =11, align: int =0, fill: OptSeq =None, text_color: OptSeq =None, + cross_out: bool =True) -> "struct Annot *": + """Add a 'Redact' annotation.""" + da_str = None + if text: + CheckColor(fill) + CheckColor(text_color) + if not fontname: + fontname = "Helv" + if not fontsize: + fontsize = 11 + if not text_color: + text_color = (0, 0, 0) + if hasattr(text_color, "__float__"): + text_color = (text_color, text_color, text_color) + if len(text_color) > 3: + text_color = text_color[:3] + fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf" + da_str = fmt.format(*text_color, f=fontname, s=fontsize) + if fill is None: + fill = (1, 1, 1) + if fill: + if hasattr(fill, "__float__"): + fill = (fill, fill, fill) + if len(fill) > 3: + fill = fill[:3] + + old_rotation = annot_preprocess(self) + try: + annot = self._add_redact_annot(quad, text=text, da_str=da_str, + align=align, fill=fill) + finally: + if old_rotation != 0: + self.set_rotation(old_rotation) + annot_postprocess(self, annot) + #------------------------------------------------------------- + # change appearance to show a crossed-out rectangle + #------------------------------------------------------------- + if cross_out: + ap_tab = annot._getAP().splitlines()[:-1] # get the 4 commands only + _, LL, LR, UR, UL = ap_tab + ap_tab.append(LR) + ap_tab.append(LL) + ap_tab.append(UR) + ap_tab.append(LL) + ap_tab.append(UL) + ap_tab.append(b"S") + ap = b"\n".join(ap_tab) + annot._setAP(ap, 0) + return annot + %} + + + //---------------------------------------------------------------- + // page load annot by name or xref + //---------------------------------------------------------------- + FITZEXCEPTION(_load_annot, !result) + struct Annot * + _load_annot(char *name, int xref) + { + pdf_annot *annot = NULL; + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + fz_try(gctx) { + ASSERT_PDF(page); + if (xref == 0) + annot = JM_get_annot_by_name(gctx, page, name); + else + annot = JM_get_annot_by_xref(gctx, page, xref); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + + //---------------------------------------------------------------- + // page load widget by xref + //---------------------------------------------------------------- + FITZEXCEPTION(load_widget, !result) + %pythonprepend load_widget %{ + """Load a widget by its xref.""" + CheckParent(self) + %} + %pythonappend load_widget %{ + if not val: + return val + val.thisown = True + val.parent = weakref.proxy(self) + self._annot_refs[id(val)] = val + widget = Widget() + TOOLS._fill_widget(val, widget) + val = widget + %} + struct Annot * + load_widget(int xref) + { + pdf_annot *annot = NULL; + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + fz_try(gctx) { + ASSERT_PDF(page); + annot = JM_get_widget_by_xref(gctx, page, xref); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + + //---------------------------------------------------------------- + // page list Resource/Properties + //---------------------------------------------------------------- + FITZEXCEPTION(_get_resource_properties, !result) + PyObject * + _get_resource_properties() + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + PyObject *rc; + fz_try(gctx) { + ASSERT_PDF(page); + rc = JM_get_resource_properties(gctx, page->obj); + } + fz_catch(gctx) { + return NULL; + } + return rc; + } + + + //---------------------------------------------------------------- + // page list Resource/Properties + //---------------------------------------------------------------- + FITZEXCEPTION(_set_resource_property, !result) + PyObject * + _set_resource_property(char *name, int xref) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + fz_try(gctx) { + ASSERT_PDF(page); + JM_set_resource_property(gctx, page->obj, name, xref); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + %pythoncode %{ +def _get_optional_content(self, oc: OptInt) -> OptStr: + if oc == None or oc == 0: + return None + doc = self.parent + check = doc.xref_object(oc, compressed=True) + if not ("/Type/OCG" in check or "/Type/OCMD" in check): + raise ValueError("bad optional content: 'oc'") + props = {} + for p, x in self._get_resource_properties(): + props[x] = p + if oc in props.keys(): + return props[oc] + i = 0 + mc = "MC%i" % i + while mc in props.values(): + i += 1 + mc = "MC%i" % i + self._set_resource_property(mc, oc) + return mc + +def get_oc_items(self) -> list: + """Get OCGs and OCMDs used in the page's contents. + + Returns: + List of items (name, xref, type), where type is one of "ocg" / "ocmd", + and name is the property name. + """ + rc = [] + for pname, xref in self._get_resource_properties(): + text = self.parent.xref_object(xref, compressed=True) + if "/Type/OCG" in text: + octype = "ocg" + elif "/Type/OCMD" in text: + octype = "ocmd" + else: + continue + rc.append((pname, xref, octype)) + return rc +%} + + //---------------------------------------------------------------- + // page get list of annot names + //---------------------------------------------------------------- + PARENTCHECK(annot_names, """List of names of annotations, fields and links.""") + PyObject *annot_names() + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + + if (!page) { + PyObject *rc = PyList_New(0); + return rc; + } + return JM_get_annot_id_list(gctx, page); + } + + + //---------------------------------------------------------------- + // page retrieve list of annotation xrefs + //---------------------------------------------------------------- + PARENTCHECK(annot_xrefs,"""List of xref numbers of annotations, fields and links.""") + PyObject *annot_xrefs() + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (!page) { + PyObject *rc = PyList_New(0); + return rc; + } + return JM_get_annot_xref_list(gctx, page->obj); + } + + + %pythoncode %{ + def load_annot(self, ident: typing.Union[str, int]) -> "struct Annot *": + """Load an annot by name (/NM key) or xref. + + Args: + ident: identifier, either name (str) or xref (int). + """ + + CheckParent(self) + if type(ident) is str: + xref = 0 + name = ident + elif type(ident) is int: + xref = ident + name = None + else: + raise ValueError("identifier must be string or integer") + val = self._load_annot(name, xref) + if not val: + return val + val.thisown = True + val.parent = weakref.proxy(self) + self._annot_refs[id(val)] = val + return val + + + #--------------------------------------------------------------------- + # page addWidget + #--------------------------------------------------------------------- + def add_widget(self, widget: Widget) -> "struct Annot *": + """Add a 'Widget' (form field).""" + CheckParent(self) + doc = self.parent + if not doc.is_pdf: + raise ValueError("is no PDF") + widget._validate() + annot = self._addWidget(widget.field_type, widget.field_name) + if not annot: + return None + annot.thisown = True + annot.parent = weakref.proxy(self) # owning page object + self._annot_refs[id(annot)] = annot + widget.parent = annot.parent + widget._annot = annot + widget.update() + return annot + %} + + FITZEXCEPTION(_addWidget, !result) + struct Annot *_addWidget(int field_type, char *field_name) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_document *pdf = page->doc; + pdf_annot *annot = NULL; + fz_var(annot); + fz_try(gctx) { + annot = JM_create_widget(gctx, pdf, page, field_type, field_name); + if (!annot) { + RAISEPY(gctx, "cannot create widget", PyExc_RuntimeError); + } + JM_add_annot_id(gctx, annot, "W"); + } + fz_catch(gctx) { + return NULL; + } + return (struct Annot *) annot; + } + + //---------------------------------------------------------------- + // Page.get_displaylist + //---------------------------------------------------------------- + FITZEXCEPTION(get_displaylist, !result) + %pythonprepend get_displaylist %{ + """Make a DisplayList from the page for Pixmap generation. + + Include (default) or exclude annotations.""" + + CheckParent(self) + %} + %pythonappend get_displaylist %{val.thisown = True%} + struct DisplayList *get_displaylist(int annots=1) + { + fz_display_list *dl = NULL; + fz_try(gctx) { + if (annots) { + dl = fz_new_display_list_from_page(gctx, (fz_page *) $self); + } else { + dl = fz_new_display_list_from_page_contents(gctx, (fz_page *) $self); + } + } + fz_catch(gctx) { + return NULL; + } + return (struct DisplayList *) dl; + } + + + //---------------------------------------------------------------- + // Page.get_drawings + //---------------------------------------------------------------- + %pythoncode %{ + def get_drawings(self, extended: bool = False) -> list: + """Retrieve vector graphics. The extended version includes clips. + + Note: + For greater comfort, this method converts point-like, rect-like, quad-like + tuples of the C version to respective Point / Rect / Quad objects. + It also adds default items that are missing in original path types. + """ + allkeys = ( + "closePath", "fill", "color", "width", "lineCap", + "lineJoin", "dashes", "stroke_opacity", "fill_opacity", "even_odd", + ) + val = self.get_cdrawings(extended=extended) + for i in range(len(val)): + npath = val[i] + if not npath["type"].startswith("clip"): + npath["rect"] = Rect(npath["rect"]) + else: + npath["scissor"] = Rect(npath["scissor"]) + if npath["type"]!="group": + items = npath["items"] + newitems = [] + for item in items: + cmd = item[0] + rest = item[1:] + if cmd == "re": + item = ("re", Rect(rest[0]), rest[1]) + elif cmd == "qu": + item = ("qu", Quad(rest[0])) + else: + item = tuple([cmd] + [Point(i) for i in rest]) + newitems.append(item) + npath["items"] = newitems + if npath["type"] in ("f", "s"): + for k in allkeys: + npath[k] = npath.get(k) + val[i] = npath + return val + + class Drawpath(object): + """Reflects a path dictionary from get_cdrawings().""" + def __init__(self, **args): + self.__dict__.update(args) + + class Drawpathlist(object): + """List of Path objects representing get_cdrawings() output.""" + def __init__(self): + self.paths = [] + self.path_count = 0 + self.group_count = 0 + self.clip_count = 0 + self.fill_count = 0 + self.stroke_count = 0 + self.fillstroke_count = 0 + + def append(self, path): + self.paths.append(path) + self.path_count += 1 + if path.type == "clip": + self.clip_count += 1 + elif path.type == "group": + self.group_count += 1 + elif path.type == "f": + self.fill_count += 1 + elif path.type == "s": + self.stroke_count += 1 + elif path.type == "fs": + self.fillstroke_count += 1 + + def clip_parents(self, i): + """Return list of parent clip paths. + + Args: + i: (int) return parents of this path. + Returns: + List of the clip parents.""" + if i >= self.path_count: + raise IndexError("bad path index") + while i < 0: + i += self.path_count + lvl = self.paths[i].level + clips = list( # clip paths before identified one + reversed( + [ + p + for p in self.paths[:i] + if p.type == "clip" and p.level < lvl + ] + ) + ) + if clips == []: # none found: empty list + return [] + nclips = [clips[0]] # init return list + for p in clips[1:]: + if p.level >= nclips[-1].level: + continue # only accept smaller clip levels + nclips.append(p) + return nclips + + def group_parents(self, i): + """Return list of parent group paths. + + Args: + i: (int) return parents of this path. + Returns: + List of the group parents.""" + if i >= self.path_count: + raise IndexError("bad path index") + while i < 0: + i += self.path_count + lvl = self.paths[i].level + groups = list( # group paths before identified one + reversed( + [ + p + for p in self.paths[:i] + if p.type == "group" and p.level < lvl + ] + ) + ) + if groups == []: # none found: empty list + return [] + ngroups = [groups[0]] # init return list + for p in groups[1:]: + if p.level >= ngroups[-1].level: + continue # only accept smaller group levels + ngroups.append(p) + return ngroups + + def __getitem__(self, item): + return self.paths.__getitem__(item) + + def __len__(self): + return self.paths.__len__() + + + def get_lineart(self) -> object: + """Get page drawings paths. + + Note: + For greater comfort, this method converts point-like, rect-like, quad-like + tuples of the C version to respective Point / Rect / Quad objects. + Also adds default items that are missing in original path types. + In contrast to get_drawings(), this output is an object. + """ + + val = self.get_cdrawings(extended=True) + paths = self.Drawpathlist() + for path in val: + npath = self.Drawpath(**path) + if npath.type != "clip": + npath.rect = Rect(path["rect"]) + else: + npath.scissor = Rect(path["scissor"]) + if npath.type != "group": + items = path["items"] + newitems = [] + for item in items: + cmd = item[0] + rest = item[1:] + if cmd == "re": + item = ("re", Rect(rest[0]), rest[1]) + elif cmd == "qu": + item = ("qu", Quad(rest[0])) + else: + item = tuple([cmd] + [Point(i) for i in rest]) + newitems.append(item) + npath.items = newitems + + if npath.type == "f": + npath.stroke_opacity = None + npath.dashes = None + npath.lineJoin = None + npath.lineCap = None + npath.color = None + npath.width = None + + paths.append(npath) + + val = None + return paths + %} + + + FITZEXCEPTION(get_cdrawings, !result) + %pythonprepend get_cdrawings %{ + """Extract vector graphics ("line art") from the page.""" + CheckParent(self) + old_rotation = self.rotation + if old_rotation != 0: + self.set_rotation(0) + %} + %pythonappend get_cdrawings %{ + if old_rotation != 0: + self.set_rotation(old_rotation) + %} + PyObject * + get_cdrawings(PyObject *extended=NULL, PyObject *callback=NULL, PyObject *method=NULL) + { + fz_page *page = (fz_page *) $self; + fz_device *dev = NULL; + PyObject *rc = NULL; + int clips = PyObject_IsTrue(extended); + fz_var(rc); + fz_try(gctx) { + fz_rect prect = fz_bound_page(gctx, page); + trace_device_ptm = fz_make_matrix(1, 0, 0, -1, 0, prect.y1); + if (PyCallable_Check(callback) || method != Py_None) { + dev = JM_new_lineart_device(gctx, callback, clips, method); + } else { + rc = PyList_New(0); + dev = JM_new_lineart_device(gctx, rc, clips, method); + } + fz_run_page(gctx, page, dev, fz_identity, NULL); + fz_close_device(gctx, dev); + } + fz_always(gctx) { + fz_drop_device(gctx, dev); + } + fz_catch(gctx) { + Py_CLEAR(rc); + return NULL; + } + if (PyCallable_Check(callback) || method != Py_None) { + Py_RETURN_NONE; + } + return rc; + } + + + FITZEXCEPTION(get_bboxlog, !result) + %pythonprepend get_bboxlog %{ + CheckParent(self) + old_rotation = self.rotation + if old_rotation != 0: + self.set_rotation(0) + %} + %pythonappend get_bboxlog %{ + if old_rotation != 0: + self.set_rotation(old_rotation) + %} + PyObject * + get_bboxlog(PyObject *layers=NULL) + { + fz_page *page = (fz_page *) $self; + fz_device *dev = NULL; + PyObject *rc = PyList_New(0); + int inc_layers = PyObject_IsTrue(layers); + fz_try(gctx) { + dev = JM_new_bbox_device(gctx, rc, inc_layers); + fz_run_page(gctx, page, dev, fz_identity, NULL); + fz_close_device(gctx, dev); + } + fz_always(gctx) { + fz_drop_device(gctx, dev); + } + fz_catch(gctx) { + Py_CLEAR(rc); + return NULL; + } + return rc; + } + + + FITZEXCEPTION(get_texttrace, !result) + %pythonprepend get_texttrace %{ + CheckParent(self) + old_rotation = self.rotation + if old_rotation != 0: + self.set_rotation(0) + %} + %pythonappend get_texttrace %{ + if old_rotation != 0: + self.set_rotation(old_rotation) + %} + PyObject * + get_texttrace() + { + fz_page *page = (fz_page *) $self; + fz_device *dev = NULL; + PyObject *rc = PyList_New(0); + fz_try(gctx) { + dev = JM_new_texttrace_device(gctx, rc); + fz_rect prect = fz_bound_page(gctx, page); + trace_device_rot = fz_identity; + trace_device_ptm = fz_make_matrix(1, 0, 0, -1, 0, prect.y1); + fz_run_page(gctx, page, dev, fz_identity, NULL); + fz_close_device(gctx, dev); + } + fz_always(gctx) { + fz_drop_device(gctx, dev); + } + fz_catch(gctx) { + Py_CLEAR(rc); + return NULL; + } + return rc; + } + + + //---------------------------------------------------------------- + // Page apply redactions + //---------------------------------------------------------------- + FITZEXCEPTION(_apply_redactions, !result) + PyObject *_apply_redactions(int images=PDF_REDACT_IMAGE_PIXELS) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + int success = 0; + pdf_redact_options opts; + opts.black_boxes = 0; // no black boxes + opts.image_method = images; // how to treat images + fz_try(gctx) { + ASSERT_PDF(page); + success = pdf_redact_page(gctx, page->doc, page, &opts); + } + fz_catch(gctx) { + return NULL; + } + return JM_BOOL(success); + } + + + //---------------------------------------------------------------- + // Page._makePixmap + //---------------------------------------------------------------- + FITZEXCEPTION(_makePixmap, !result) + struct Pixmap * + _makePixmap(struct Document *doc, + PyObject *ctm, + struct Colorspace *cs, + int alpha=0, + int annots=1, + PyObject *clip=NULL) + { + fz_pixmap *pix = NULL; + fz_try(gctx) { + pix = JM_pixmap_from_page(gctx, (fz_document *) doc, (fz_page *) $self, ctm, (fz_colorspace *) cs, alpha, annots, clip); + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) pix; + } + + + //---------------------------------------------------------------- + // Page.set_mediabox + //---------------------------------------------------------------- + FITZEXCEPTION(set_mediabox, !result) + PARENTCHECK(set_mediabox, """Set the MediaBox.""") + PyObject *set_mediabox(PyObject *rect) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + fz_try(gctx) { + ASSERT_PDF(page); + fz_rect mediabox = JM_rect_from_py(rect); + if (fz_is_empty_rect(mediabox) || + fz_is_infinite_rect(mediabox)) { + RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError); + } + pdf_dict_put_rect(gctx, page->obj, PDF_NAME(MediaBox), mediabox); + pdf_dict_del(gctx, page->obj, PDF_NAME(CropBox)); + pdf_dict_del(gctx, page->obj, PDF_NAME(ArtBox)); + pdf_dict_del(gctx, page->obj, PDF_NAME(BleedBox)); + pdf_dict_del(gctx, page->obj, PDF_NAME(TrimBox)); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // Page.load_links() + //---------------------------------------------------------------- + PARENTCHECK(load_links, """Get first Link.""") + %pythonappend load_links %{ + if val: + val.thisown = True + val.parent = weakref.proxy(self) # owning page object + self._annot_refs[id(val)] = val + if self.parent.is_pdf: + link_id = [x for x in self.annot_xrefs() if x[1] == PDF_ANNOT_LINK][0] + val.xref = link_id[0] + val.id = link_id[2] + else: + val.xref = 0 + val.id = "" + %} + struct Link *load_links() + { + fz_link *l = NULL; + fz_try(gctx) { + l = fz_load_links(gctx, (fz_page *) $self); + } + fz_catch(gctx) { + return NULL; + } + return (struct Link *) l; + } + %pythoncode %{first_link = property(load_links, doc="First link on page")%} + + //---------------------------------------------------------------- + // Page.first_annot + //---------------------------------------------------------------- + PARENTCHECK(first_annot, """First annotation.""") + %pythonappend first_annot %{ + if val: + val.thisown = True + val.parent = weakref.proxy(self) # owning page object + self._annot_refs[id(val)] = val + %} + %pythoncode %{@property%} + struct Annot *first_annot() + { + pdf_annot *annot = NULL; + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (page) + { + annot = pdf_first_annot(gctx, page); + if (annot) pdf_keep_annot(gctx, annot); + } + return (struct Annot *) annot; + } + + //---------------------------------------------------------------- + // first_widget + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(first_widget, """First widget/field.""") + %pythonappend first_widget %{ + if val: + val.thisown = True + val.parent = weakref.proxy(self) # owning page object + self._annot_refs[id(val)] = val + widget = Widget() + TOOLS._fill_widget(val, widget) + val = widget + %} + struct Annot *first_widget() + { + pdf_annot *annot = NULL; + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (page) { + annot = pdf_first_widget(gctx, page); + if (annot) pdf_keep_annot(gctx, annot); + } + return (struct Annot *) annot; + } + + + //---------------------------------------------------------------- + // Page.delete_link() - delete link + //---------------------------------------------------------------- + PARENTCHECK(delete_link, """Delete a Link.""") + %pythonappend delete_link %{ + if linkdict["xref"] == 0: return + try: + linkid = linkdict["id"] + linkobj = self._annot_refs[linkid] + linkobj._erase() + except: + pass + %} + void delete_link(PyObject *linkdict) + { + if (!PyDict_Check(linkdict)) return; // have no dictionary + fz_try(gctx) { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (!page) goto finished; // have no PDF + int xref = (int) PyInt_AsLong(PyDict_GetItem(linkdict, dictkey_xref)); + if (xref < 1) goto finished; // invalid xref + pdf_obj *annots = pdf_dict_get(gctx, page->obj, PDF_NAME(Annots)); + if (!annots) goto finished; // have no annotations + int len = pdf_array_len(gctx, annots); + if (len == 0) goto finished; + int i, oxref = 0; + + for (i = 0; i < len; i++) { + oxref = pdf_to_num(gctx, pdf_array_get(gctx, annots, i)); + if (xref == oxref) break; // found xref in annotations + } + + if (xref != oxref) goto finished; // xref not in annotations + pdf_array_delete(gctx, annots, i); // delete entry in annotations + pdf_delete_object(gctx, page->doc, xref); // delete link obj + pdf_dict_put(gctx, page->obj, PDF_NAME(Annots), annots); + JM_refresh_links(gctx, page); + finished:; + + } + fz_catch(gctx) {;} + } + + //---------------------------------------------------------------- + // Page.delete_annot() - delete annotation and return the next one + //---------------------------------------------------------------- + %pythonprepend delete_annot %{ + """Delete annot and return next one.""" + CheckParent(self) + CheckParent(annot)%} + + %pythonappend delete_annot %{ + if val: + val.thisown = True + val.parent = weakref.proxy(self) # owning page object + val.parent._annot_refs[id(val)] = val + %} + + struct Annot *delete_annot(struct Annot *annot) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_annot *irt_annot = NULL; + while (1) { + // first loop through all /IRT annots and remove them + irt_annot = JM_find_annot_irt(gctx, (pdf_annot *) annot); + if (!irt_annot) // no more there + break; + pdf_delete_annot(gctx, page, irt_annot); + } + pdf_annot *nextannot = pdf_next_annot(gctx, (pdf_annot *) annot); // store next + pdf_delete_annot(gctx, page, (pdf_annot *) annot); + if (nextannot) { + nextannot = pdf_keep_annot(gctx, nextannot); + } + return (struct Annot *) nextannot; + } + + + //---------------------------------------------------------------- + // mediabox: get the /MediaBox (PDF only) + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(mediabox, """The MediaBox.""") + %pythonappend mediabox %{val = Rect(JM_TUPLE3(val))%} + PyObject *mediabox() + { + fz_rect rect = fz_infinite_rect; + fz_try(gctx) { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (!page) { + rect = fz_bound_page(gctx, (fz_page *) $self); + } else { + rect = JM_mediabox(gctx, page->obj); + } + } + fz_catch(gctx) {;} + return JM_py_from_rect(rect); + } + + + //---------------------------------------------------------------- + // cropbox: get the /CropBox (PDF only) + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(cropbox, """The CropBox.""") + %pythonappend cropbox %{val = Rect(JM_TUPLE3(val))%} + PyObject *cropbox() + { + fz_rect rect = fz_infinite_rect; + fz_try(gctx) { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (!page) { + rect = fz_bound_page(gctx, (fz_page *) $self); + } else { + rect = JM_cropbox(gctx, page->obj); + } + } + fz_catch(gctx) {;} + return JM_py_from_rect(rect); + } + + + PyObject *_other_box(const char *boxtype) + { + fz_rect rect = fz_infinite_rect; + fz_try(gctx) { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (page) { + pdf_obj *obj = pdf_dict_gets(gctx, page->obj, boxtype); + if (pdf_is_array(gctx, obj)) { + rect = pdf_to_rect(gctx, obj); + } + } + } + fz_catch(gctx) {;} + if (fz_is_infinite_rect(rect)) { + Py_RETURN_NONE; + } + return JM_py_from_rect(rect); + } + + + //---------------------------------------------------------------- + // CropBox position: x0, y0 of /CropBox + //---------------------------------------------------------------- + %pythoncode %{ + @property + def cropbox_position(self): + return self.cropbox.tl + + @property + def artbox(self): + """The ArtBox""" + rect = self._other_box("ArtBox") + if rect == None: + return self.cropbox + mb = self.mediabox + return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1]) + + @property + def trimbox(self): + """The TrimBox""" + rect = self._other_box("TrimBox") + if rect == None: + return self.cropbox + mb = self.mediabox + return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1]) + + @property + def bleedbox(self): + """The BleedBox""" + rect = self._other_box("BleedBox") + if rect == None: + return self.cropbox + mb = self.mediabox + return Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1]) + + def _set_pagebox(self, boxtype, rect): + doc = self.parent + if doc == None: + raise ValueError("orphaned object: parent is None") + if not doc.is_pdf: + raise ValueError("is no PDF") + valid_boxes = ("CropBox", "BleedBox", "TrimBox", "ArtBox") + if boxtype not in valid_boxes: + raise ValueError("bad boxtype") + mb = self.mediabox + rect = Rect(rect[0], mb.y1 - rect[3], rect[2], mb.y1 - rect[1]) + rect = Rect(JM_TUPLE3(rect)) + if rect.is_infinite or rect.is_empty: + raise ValueError("rect is infinite or empty") + if rect not in mb: + raise ValueError("rect not in mediabox") + doc.xref_set_key(self.xref, boxtype, "[%g %g %g %g]" % tuple(rect)) + + def set_cropbox(self, rect): + """Set the CropBox. Will also change Page.rect.""" + return self._set_pagebox("CropBox", rect) + + def set_artbox(self, rect): + """Set the ArtBox.""" + return self._set_pagebox("ArtBox", rect) + + def set_bleedbox(self, rect): + """Set the BleedBox.""" + return self._set_pagebox("BleedBox", rect) + + def set_trimbox(self, rect): + """Set the TrimBox.""" + return self._set_pagebox("TrimBox", rect) + %} + + + //---------------------------------------------------------------- + // rotation - return page rotation + //---------------------------------------------------------------- + PARENTCHECK(rotation, """Page rotation.""") + %pythoncode %{@property%} + int rotation() + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (!page) return 0; + return JM_page_rotation(gctx, page); + } + + /*********************************************************************/ + // set_rotation() - set page rotation + /*********************************************************************/ + FITZEXCEPTION(set_rotation, !result) + PARENTCHECK(set_rotation, """Set page rotation.""") + PyObject *set_rotation(int rotation) + { + fz_try(gctx) { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + ASSERT_PDF(page); + int rot = JM_norm_rotation(rotation); + pdf_dict_put_int(gctx, page->obj, PDF_NAME(Rotate), (int64_t) rot); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + /*********************************************************************/ + // Page._addAnnot_FromString + // Add new links provided as an array of string object definitions. + /*********************************************************************/ + FITZEXCEPTION(_addAnnot_FromString, !result) + PARENTCHECK(_addAnnot_FromString, """Add links from list of object sources.""") + PyObject *_addAnnot_FromString(PyObject *linklist) + { + pdf_obj *annots, *annot, *ind_obj; + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + PyObject *txtpy = NULL; + char *text = NULL; + Py_ssize_t lcount = PyTuple_Size(linklist); // link count + if (lcount < 1) Py_RETURN_NONE; + Py_ssize_t i = -1; + fz_var(text); + + // insert links from the provided sources + fz_try(gctx) { + ASSERT_PDF(page); + if (!PyTuple_Check(linklist)) { + RAISEPY(gctx, "bad 'linklist' argument", PyExc_ValueError); + } + if (!pdf_dict_get(gctx, page->obj, PDF_NAME(Annots))) { + pdf_dict_put_array(gctx, page->obj, PDF_NAME(Annots), lcount); + } + annots = pdf_dict_get(gctx, page->obj, PDF_NAME(Annots)); + for (i = 0; i < lcount; i++) { + fz_try(gctx) { + for (; i < lcount; i++) { + text = JM_StrAsChar(PyTuple_GET_ITEM(linklist, i)); + if (!text) { + PySys_WriteStderr("skipping bad link / annot item %zi.\n", i); + continue; + } + annot = pdf_add_object_drop(gctx, page->doc, + JM_pdf_obj_from_str(gctx, page->doc, text)); + ind_obj = pdf_new_indirect(gctx, page->doc, pdf_to_num(gctx, annot), 0); + pdf_array_push_drop(gctx, annots, ind_obj); + pdf_drop_obj(gctx, annot); + } + } + fz_catch(gctx) { + PySys_WriteStderr("skipping bad link / annot item %zi.\n", i); + } + } + } + fz_catch(gctx) { + PyErr_Clear(); + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------------------------------------- + // Page clean contents stream + //---------------------------------------------------------------- + FITZEXCEPTION(clean_contents, !result) + %pythonprepend clean_contents +%{"""Clean page /Contents into one object.""" +CheckParent(self) +if not sanitize and not self.is_wrapped: + self.wrap_contents()%} + PyObject *clean_contents(int sanitize=1) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (!page) { + Py_RETURN_NONE; + } + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + pdf_filter_factory list[2] = { 0 }; + pdf_sanitize_filter_options sopts = { 0 }; + pdf_filter_options filter = { + 1, // recurse: true + 1, // instance forms + 0, // do not ascii-escape binary data + 1, // no_update + NULL, // end_page_opaque + NULL, // end page + list, // filters + }; + if (sanitize) { + list[0].filter = pdf_new_sanitize_filter; + list[0].options = &sopts; + } + #else + pdf_filter_options filter = { + NULL, // opaque + NULL, // image filter + NULL, // text filter + NULL, // after text + NULL, // end page + 1, // recurse: true + 1, // instance forms + 1, // sanitize plus filtering + 0 // do not ascii-escape binary data + }; + filter.sanitize = sanitize; + #endif + fz_try(gctx) { + pdf_filter_page_contents(gctx, page->doc, page, &filter); + } + fz_catch(gctx) { + Py_RETURN_NONE; + } + Py_RETURN_NONE; + } + + //---------------------------------------------------------------- + // Show a PDF page + //---------------------------------------------------------------- + FITZEXCEPTION(_show_pdf_page, !result) + PyObject *_show_pdf_page(struct Page *fz_srcpage, int overlay=1, PyObject *matrix=NULL, int xref=0, int oc=0, PyObject *clip = NULL, struct Graftmap *graftmap = NULL, char *_imgname = NULL) + { + pdf_obj *xobj1=NULL, *xobj2=NULL, *resources; + fz_buffer *res=NULL, *nres=NULL; + fz_rect cropbox = JM_rect_from_py(clip); + fz_matrix mat = JM_matrix_from_py(matrix); + int rc_xref = xref; + fz_var(xobj1); + fz_var(xobj2); + fz_try(gctx) { + pdf_page *tpage = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_obj *tpageref = tpage->obj; + pdf_document *pdfout = tpage->doc; // target PDF + ENSURE_OPERATION(gctx, pdfout); + //------------------------------------------------------------- + // convert the source page to a Form XObject + //------------------------------------------------------------- + xobj1 = JM_xobject_from_page(gctx, pdfout, (fz_page *) fz_srcpage, + xref, (pdf_graft_map *) graftmap); + if (!rc_xref) rc_xref = pdf_to_num(gctx, xobj1); + + //------------------------------------------------------------- + // create referencing XObject (controls display on target page) + //------------------------------------------------------------- + // fill reference to xobj1 into the /Resources + //------------------------------------------------------------- + pdf_obj *subres1 = pdf_new_dict(gctx, pdfout, 5); + pdf_dict_puts(gctx, subres1, "fullpage", xobj1); + pdf_obj *subres = pdf_new_dict(gctx, pdfout, 5); + pdf_dict_put_drop(gctx, subres, PDF_NAME(XObject), subres1); + + res = fz_new_buffer(gctx, 20); + fz_append_string(gctx, res, "/fullpage Do"); + + xobj2 = pdf_new_xobject(gctx, pdfout, cropbox, mat, subres, res); + if (oc > 0) { + JM_add_oc_object(gctx, pdfout, pdf_resolve_indirect(gctx, xobj2), oc); + } + pdf_drop_obj(gctx, subres); + fz_drop_buffer(gctx, res); + + //------------------------------------------------------------- + // update target page with xobj2: + //------------------------------------------------------------- + // 1. insert Xobject in Resources + //------------------------------------------------------------- + resources = pdf_dict_get_inheritable(gctx, tpageref, PDF_NAME(Resources)); + subres = pdf_dict_get(gctx, resources, PDF_NAME(XObject)); + if (!subres) { + subres = pdf_dict_put_dict(gctx, resources, PDF_NAME(XObject), 5); + } + + pdf_dict_puts(gctx, subres, _imgname, xobj2); + + //------------------------------------------------------------- + // 2. make and insert new Contents object + //------------------------------------------------------------- + nres = fz_new_buffer(gctx, 50); // buffer for Do-command + fz_append_string(gctx, nres, " q /"); // Do-command + fz_append_string(gctx, nres, _imgname); + fz_append_string(gctx, nres, " Do Q "); + + JM_insert_contents(gctx, pdfout, tpageref, nres, overlay); + fz_drop_buffer(gctx, nres); + } + fz_always(gctx) { + pdf_drop_obj(gctx, xobj1); + pdf_drop_obj(gctx, xobj2); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", rc_xref); + } + + //---------------------------------------------------------------- + // insert an image + //---------------------------------------------------------------- + FITZEXCEPTION(_insert_image, !result) + PyObject * + _insert_image(char *filename=NULL, + struct Pixmap *pixmap=NULL, + PyObject *stream=NULL, + PyObject *imask=NULL, + PyObject *clip=NULL, + int overlay=1, + int rotate=0, + int keep_proportion=1, + int oc=0, + int width=0, + int height=0, + int xref=0, + int alpha=-1, + const char *_imgname=NULL, + PyObject *digests=NULL) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_document *pdf = page->doc; + float w = width, h = height; + fz_pixmap *pm = NULL; + fz_pixmap *pix = NULL; + fz_image *mask = NULL, *zimg = NULL, *image = NULL, *freethis = NULL; + pdf_obj *resources, *xobject, *ref; + fz_buffer *nres = NULL, *imgbuf = NULL, *maskbuf = NULL; + fz_compressed_buffer *cbuf1 = NULL; + int xres, yres, bpc, img_xref = xref, rc_digest = 0; + unsigned char digest[16]; + PyObject *md5_py = NULL, *temp; + const char *template = "\nq\n%g %g %g %g %g %g cm\n/%s Do\nQ\n"; + + fz_try(gctx) { + if (xref > 0) { + ref = pdf_new_indirect(gctx, pdf, xref, 0); + w = pdf_to_int(gctx, + pdf_dict_geta(gctx, ref, + PDF_NAME(Width), PDF_NAME(W))); + h = pdf_to_int(gctx, + pdf_dict_geta(gctx, ref, + PDF_NAME(Height), PDF_NAME(H))); + if ((w + h) == 0) { + RAISEPY(gctx, MSG_IS_NO_IMAGE, PyExc_ValueError); + } + goto have_xref; + } + if (EXISTS(stream)) { + imgbuf = JM_BufferFromBytes(gctx, stream); + goto have_stream; + } + if (filename) { + imgbuf = fz_read_file(gctx, filename); + goto have_stream; + } + // process pixmap --------------------------------- + fz_pixmap *arg_pix = (fz_pixmap *) pixmap; + w = arg_pix->w; + h = arg_pix->h; + fz_md5_pixmap(gctx, arg_pix, digest); + md5_py = PyBytes_FromStringAndSize(digest, 16); + temp = PyDict_GetItem(digests, md5_py); + if (temp) { + img_xref = (int) PyLong_AsLong(temp); + ref = pdf_new_indirect(gctx, page->doc, img_xref, 0); + goto have_xref; + } + if (arg_pix->alpha == 0) { + image = fz_new_image_from_pixmap(gctx, arg_pix, NULL); + } else { + pm = fz_convert_pixmap(gctx, arg_pix, NULL, NULL, NULL, + fz_default_color_params, 1); + pm->alpha = 0; + pm->colorspace = NULL; + mask = fz_new_image_from_pixmap(gctx, pm, NULL); + image = fz_new_image_from_pixmap(gctx, arg_pix, mask); + } + goto have_image; + + // process stream --------------------------------- + have_stream:; + fz_md5 state; + fz_md5_init(&state); + fz_md5_update(&state, imgbuf->data, imgbuf->len); + if (imask != Py_None) { + maskbuf = JM_BufferFromBytes(gctx, imask); + fz_md5_update(&state, maskbuf->data, maskbuf->len); + } + fz_md5_final(&state, digest); + md5_py = PyBytes_FromStringAndSize(digest, 16); + temp = PyDict_GetItem(digests, md5_py); + if (temp) { + img_xref = (int) PyLong_AsLong(temp); + ref = pdf_new_indirect(gctx, page->doc, img_xref, 0); + w = pdf_to_int(gctx, + pdf_dict_geta(gctx, ref, + PDF_NAME(Width), PDF_NAME(W))); + h = pdf_to_int(gctx, + pdf_dict_geta(gctx, ref, + PDF_NAME(Height), PDF_NAME(H))); + goto have_xref; + } + image = fz_new_image_from_buffer(gctx, imgbuf); + w = image->w; + h = image->h; + if (imask == Py_None) { + goto have_image; + } + + cbuf1 = fz_compressed_image_buffer(gctx, image); + if (!cbuf1) { + RAISEPY(gctx, "uncompressed image cannot have mask", PyExc_ValueError); + } + bpc = image->bpc; + fz_colorspace *colorspace = image->colorspace; + fz_image_resolution(image, &xres, &yres); + mask = fz_new_image_from_buffer(gctx, maskbuf); + zimg = fz_new_image_from_compressed_buffer(gctx, w, h, + bpc, colorspace, xres, yres, 1, 0, NULL, + NULL, cbuf1, mask); + freethis = image; + image = zimg; + zimg = NULL; + goto have_image; + + have_image:; + ref = pdf_add_image(gctx, pdf, image); + if (oc) { + JM_add_oc_object(gctx, pdf, ref, oc); + } + img_xref = pdf_to_num(gctx, ref); + DICT_SETITEM_DROP(digests, md5_py, Py_BuildValue("i", img_xref)); + rc_digest = 1; + have_xref:; + resources = pdf_dict_get_inheritable(gctx, page->obj, + PDF_NAME(Resources)); + if (!resources) { + resources = pdf_dict_put_dict(gctx, page->obj, + PDF_NAME(Resources), 2); + } + xobject = pdf_dict_get(gctx, resources, PDF_NAME(XObject)); + if (!xobject) { + xobject = pdf_dict_put_dict(gctx, resources, + PDF_NAME(XObject), 2); + } + fz_matrix mat = calc_image_matrix(w, h, clip, rotate, keep_proportion); + pdf_dict_puts_drop(gctx, xobject, _imgname, ref); + nres = fz_new_buffer(gctx, 50); + fz_append_printf(gctx, nres, template, + mat.a, mat.b, mat.c, mat.d, mat.e, mat.f, _imgname); + JM_insert_contents(gctx, pdf, page->obj, nres, overlay); + } + fz_always(gctx) { + if (freethis) { + fz_drop_image(gctx, freethis); + } else { + fz_drop_image(gctx, image); + } + fz_drop_image(gctx, mask); + fz_drop_image(gctx, zimg); + fz_drop_pixmap(gctx, pix); + fz_drop_pixmap(gctx, pm); + fz_drop_buffer(gctx, imgbuf); + fz_drop_buffer(gctx, maskbuf); + fz_drop_buffer(gctx, nres); + } + fz_catch(gctx) { + return NULL; + } + + if (rc_digest) { + return Py_BuildValue("iO", img_xref, digests); + } else { + return Py_BuildValue("iO", img_xref, Py_None); + } + } + + + //---------------------------------------------------------------- + // Page.refresh() + //---------------------------------------------------------------- + %pythoncode %{ + def refresh(self): + doc = self.parent + page = doc.reload_page(self) + self = page + %} + + + //---------------------------------------------------------------- + // insert font + //---------------------------------------------------------------- + %pythoncode +%{ +def insert_font(self, fontname="helv", fontfile=None, fontbuffer=None, + set_simple=False, wmode=0, encoding=0): + doc = self.parent + if doc is None: + raise ValueError("orphaned object: parent is None") + idx = 0 + + if fontname.startswith("/"): + fontname = fontname[1:] + + font = CheckFont(self, fontname) + if font is not None: # font already in font list of page + xref = font[0] # this is the xref + if CheckFontInfo(doc, xref): # also in our document font list? + return xref # yes: we are done + # need to build the doc FontInfo entry - done via get_char_widths + doc.get_char_widths(xref) + return xref + + #-------------------------------------------------------------------------- + # the font is not present for this page + #-------------------------------------------------------------------------- + + bfname = Base14_fontdict.get(fontname.lower(), None) # BaseFont if Base-14 font + + serif = 0 + CJK_number = -1 + CJK_list_n = ["china-t", "china-s", "japan", "korea"] + CJK_list_s = ["china-ts", "china-ss", "japan-s", "korea-s"] + + try: + CJK_number = CJK_list_n.index(fontname) + serif = 0 + except: + pass + + if CJK_number < 0: + try: + CJK_number = CJK_list_s.index(fontname) + serif = 1 + except: + pass + + if fontname.lower() in fitz_fontdescriptors.keys(): + import pymupdf_fonts + fontbuffer = pymupdf_fonts.myfont(fontname) # make a copy + del pymupdf_fonts + + # install the font for the page + if fontfile != None: + if type(fontfile) is str: + fontfile_str = fontfile + elif hasattr(fontfile, "absolute"): + fontfile_str = str(fontfile) + elif hasattr(fontfile, "name"): + fontfile_str = fontfile.name + else: + raise ValueError("bad fontfile") + else: + fontfile_str = None + val = self._insertFont(fontname, bfname, fontfile_str, fontbuffer, set_simple, idx, + wmode, serif, encoding, CJK_number) + + if not val: # did not work, error return + return val + + xref = val[0] # xref of installed font + fontdict = val[1] + + if CheckFontInfo(doc, xref): # check again: document already has this font + return xref # we are done + + # need to create document font info + doc.get_char_widths(xref, fontdict=fontdict) + return xref + +%} + + FITZEXCEPTION(_insertFont, !result) + PyObject *_insertFont(char *fontname, char *bfname, + char *fontfile, + PyObject *fontbuffer, + int set_simple, int idx, + int wmode, int serif, + int encoding, int ordering) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + pdf_document *pdf; + pdf_obj *resources, *fonts, *font_obj; + PyObject *value; + fz_try(gctx) { + ASSERT_PDF(page); + pdf = page->doc; + + value = JM_insert_font(gctx, pdf, bfname, fontfile,fontbuffer, + set_simple, idx, wmode, serif, encoding, ordering); + + // get the objects /Resources, /Resources/Font + resources = pdf_dict_get_inheritable(gctx, page->obj, PDF_NAME(Resources)); + fonts = pdf_dict_get(gctx, resources, PDF_NAME(Font)); + if (!fonts) { // page has no fonts yet + fonts = pdf_new_dict(gctx, pdf, 5); + pdf_dict_putl_drop(gctx, page->obj, fonts, PDF_NAME(Resources), PDF_NAME(Font), NULL); + } + // store font in resources and fonts objects will contain named reference to font + int xref = 0; + JM_INT_ITEM(value, 0, &xref); + if (!xref) { + RAISEPY(gctx, "cannot insert font", PyExc_RuntimeError); + } + font_obj = pdf_new_indirect(gctx, pdf, xref, 0); + pdf_dict_puts_drop(gctx, fonts, fontname, font_obj); + } + fz_always(gctx) { + ; + } + fz_catch(gctx) { + return NULL; + } + + return value; + } + + //---------------------------------------------------------------- + // Get page transformation matrix + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(transformation_matrix, """Page transformation matrix.""") + %pythonappend transformation_matrix %{ + if self.rotation % 360 == 0: + val = Matrix(val) + else: + val = Matrix(1, 0, 0, -1, 0, self.cropbox.height) + %} + PyObject *transformation_matrix() + { + fz_matrix ctm = fz_identity; + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + if (!page) return JM_py_from_matrix(ctm); + fz_try(gctx) { + pdf_page_transform(gctx, page, NULL, &ctm); + } + fz_catch(gctx) {;} + return JM_py_from_matrix(ctm); + } + + //---------------------------------------------------------------- + // Page Get list of contents objects + //---------------------------------------------------------------- + FITZEXCEPTION(get_contents, !result) + PARENTCHECK(get_contents, """Get xrefs of /Contents objects.""") + PyObject *get_contents() + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) $self); + PyObject *list = NULL; + pdf_obj *contents = NULL, *icont = NULL; + int i, xref; + size_t n = 0; + fz_try(gctx) { + ASSERT_PDF(page); + contents = pdf_dict_get(gctx, page->obj, PDF_NAME(Contents)); + if (pdf_is_array(gctx, contents)) { + n = pdf_array_len(gctx, contents); + list = PyList_New(n); + for (i = 0; i < n; i++) { + icont = pdf_array_get(gctx, contents, i); + xref = pdf_to_num(gctx, icont); + PyList_SET_ITEM(list, i, Py_BuildValue("i", xref)); + } + } + else if (contents) { + list = PyList_New(1); + xref = pdf_to_num(gctx, contents); + PyList_SET_ITEM(list, 0, Py_BuildValue("i", xref)); + } + } + fz_catch(gctx) { + return NULL; + } + if (list) { + return list; + } + return PyList_New(0); + } + + //---------------------------------------------------------------- + // + //---------------------------------------------------------------- + %pythoncode %{ + def set_contents(self, xref: int)->None: + """Set object at 'xref' as the page's /Contents.""" + CheckParent(self) + doc = self.parent + if doc.is_closed: + raise ValueError("document closed") + if not doc.is_pdf: + raise ValueError("is no PDF") + if not xref in range(1, doc.xref_length()): + raise ValueError("bad xref") + if not doc.xref_is_stream(xref): + raise ValueError("xref is no stream") + doc.xref_set_key(self.xref, "Contents", "%i 0 R" % xref) + + + @property + def is_wrapped(self): + """Check if /Contents is wrapped with string pair "q" / "Q".""" + if getattr(self, "was_wrapped", False): # costly checks only once + return True + cont = self.read_contents().split() + if cont == []: # no contents treated as okay + self.was_wrapped = True + return True + if cont[0] != b"q" or cont[-1] != b"Q": + return False # potential "geometry" issue + self.was_wrapped = True # cheap check next time + return True + + + def wrap_contents(self): + if self.is_wrapped: # avoid unnecessary wrapping + return + TOOLS._insert_contents(self, b"q\n", False) + TOOLS._insert_contents(self, b"\nQ", True) + self.was_wrapped = True # indicate not needed again + + + def links(self, kinds=None): + """ Generator over the links of a page. + + Args: + kinds: (list) link kinds to subselect from. If none, + all links are returned. E.g. kinds=[LINK_URI] + will only yield URI links. + """ + all_links = self.get_links() + for link in all_links: + if kinds is None or link["kind"] in kinds: + yield (link) + + + def annots(self, types=None): + """ Generator over the annotations of a page. + + Args: + types: (list) annotation types to subselect from. If none, + all annotations are returned. E.g. types=[PDF_ANNOT_LINE] + will only yield line annotations. + """ + skip_types = (PDF_ANNOT_LINK, PDF_ANNOT_POPUP, PDF_ANNOT_WIDGET) + if not hasattr(types, "__getitem__"): + annot_xrefs = [a[0] for a in self.annot_xrefs() if a[1] not in skip_types] + else: + annot_xrefs = [a[0] for a in self.annot_xrefs() if a[1] in types and a[1] not in skip_types] + for xref in annot_xrefs: + annot = self.load_annot(xref) + annot._yielded=True + yield annot + + + def widgets(self, types=None): + """ Generator over the widgets of a page. + + Args: + types: (list) field types to subselect from. If none, + all fields are returned. E.g. types=[PDF_WIDGET_TYPE_TEXT] + will only yield text fields. + """ + widget_xrefs = [a[0] for a in self.annot_xrefs() if a[1] == PDF_ANNOT_WIDGET] + for xref in widget_xrefs: + widget = self.load_widget(xref) + if types == None or widget.field_type in types: + yield (widget) + + + def __str__(self): + CheckParent(self) + x = self.parent.name + if self.parent.stream is not None: + x = "" % (self.parent._graft_id,) + if x == "": + x = "" % self.parent._graft_id + return "page %s of %s" % (self.number, x) + + def __repr__(self): + CheckParent(self) + x = self.parent.name + if self.parent.stream is not None: + x = "" % (self.parent._graft_id,) + if x == "": + x = "" % self.parent._graft_id + return "page %s of %s" % (self.number, x) + + def _reset_annot_refs(self): + """Invalidate / delete all annots of this page.""" + for annot in self._annot_refs.values(): + if annot: + annot._erase() + self._annot_refs.clear() + + @property + def xref(self): + """PDF xref number of page.""" + CheckParent(self) + return self.parent.page_xref(self.number) + + def _erase(self): + self._reset_annot_refs() + self._image_infos = None + try: + self.parent._forget_page(self) + except: + pass + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + self.parent = None + self.number = None + + + def __del__(self): + self._erase() + + + def get_fonts(self, full=False): + """List of fonts defined in the page object.""" + CheckParent(self) + return self.parent.get_page_fonts(self.number, full=full) + + + def get_images(self, full=False): + """List of images defined in the page object.""" + CheckParent(self) + ret = self.parent.get_page_images(self.number, full=full) + return ret + + + def get_xobjects(self): + """List of xobjects defined in the page object.""" + CheckParent(self) + return self.parent.get_page_xobjects(self.number) + + + def read_contents(self): + """All /Contents streams concatenated to one bytes object.""" + return TOOLS._get_all_contents(self) + + + @property + def mediabox_size(self): + return Point(self.mediabox.x1, self.mediabox.y1) + %} + } +}; +%clearnodefaultctor; + +//------------------------------------------------------------------------ +// Pixmap +//------------------------------------------------------------------------ +struct Pixmap +{ + %extend { + ~Pixmap() { + DEBUGMSG1("Pixmap"); + fz_pixmap *this_pix = (fz_pixmap *) $self; + fz_drop_pixmap(gctx, this_pix); + DEBUGMSG2; + } + FITZEXCEPTION(Pixmap, !result) + %pythonprepend Pixmap +%{"""Pixmap(colorspace, irect, alpha) - empty pixmap. +Pixmap(colorspace, src) - copy changing colorspace. +Pixmap(src, width, height,[clip]) - scaled copy, float dimensions. +Pixmap(src, alpha=True) - copy adding / dropping alpha. +Pixmap(source, mask) - from a non-alpha and a mask pixmap. +Pixmap(file) - from an image file. +Pixmap(memory) - from an image in memory (bytes). +Pixmap(colorspace, width, height, samples, alpha) - from samples data. +Pixmap(PDFdoc, xref) - from an image xref in a PDF document. +"""%} + //---------------------------------------------------------------- + // create empty pixmap with colorspace and IRect + //---------------------------------------------------------------- + Pixmap(struct Colorspace *cs, PyObject *bbox, int alpha = 0) + { + fz_pixmap *pm = NULL; + fz_try(gctx) { + pm = fz_new_pixmap_with_bbox(gctx, (fz_colorspace *) cs, JM_irect_from_py(bbox), NULL, alpha); + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) pm; + } + + //---------------------------------------------------------------- + // copy pixmap, converting colorspace + //---------------------------------------------------------------- + Pixmap(struct Colorspace *cs, struct Pixmap *spix) + { + fz_pixmap *pm = NULL; + fz_try(gctx) { + if (!fz_pixmap_colorspace(gctx, (fz_pixmap *) spix)) { + RAISEPY(gctx, "source colorspace must not be None", PyExc_ValueError); + } + fz_colorspace *cspace = NULL; + if (cs) { + cspace = (fz_colorspace *) cs; + } + if (cspace) { + pm = fz_convert_pixmap(gctx, (fz_pixmap *) spix, cspace, NULL, NULL, fz_default_color_params, 1); + } else { + pm = fz_new_pixmap_from_alpha_channel(gctx, (fz_pixmap *) spix); + if (!pm) { + RAISEPY(gctx, MSG_PIX_NOALPHA, PyExc_RuntimeError); + } + } + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) pm; + } + + + //---------------------------------------------------------------- + // add mask to a pixmap w/o alpha channel + //---------------------------------------------------------------- + Pixmap(struct Pixmap *spix, struct Pixmap *mpix) + { + fz_pixmap *dst = NULL; + fz_pixmap *spm = (fz_pixmap *) spix; + fz_pixmap *mpm = (fz_pixmap *) mpix; + fz_try(gctx) { + if (!spix) { // intercept NULL for spix: make alpha only pix + dst = fz_new_pixmap_from_alpha_channel(gctx, mpm); + if (!dst) { + RAISEPY(gctx, MSG_PIX_NOALPHA, PyExc_RuntimeError); + } + } else { + dst = fz_new_pixmap_from_color_and_mask(gctx, spm, mpm); + } + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) dst; + } + + + //---------------------------------------------------------------- + // create pixmap as scaled copy of another one + //---------------------------------------------------------------- + Pixmap(struct Pixmap *spix, float w, float h, PyObject *clip=NULL) + { + fz_pixmap *pm = NULL; + fz_pixmap *src_pix = (fz_pixmap *) spix; + fz_try(gctx) { + fz_irect bbox = JM_irect_from_py(clip); + if (clip != Py_None && (fz_is_infinite_irect(bbox) || fz_is_empty_irect(bbox))) { + RAISEPY(gctx, "bad clip parameter", PyExc_ValueError); + } + if (!fz_is_infinite_irect(bbox)) { + pm = fz_scale_pixmap(gctx, src_pix, src_pix->x, src_pix->y, w, h, &bbox); + } else { + pm = fz_scale_pixmap(gctx, src_pix, src_pix->x, src_pix->y, w, h, NULL); + } + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) pm; + } + + + //---------------------------------------------------------------- + // copy pixmap & add / drop the alpha channel + //---------------------------------------------------------------- + Pixmap(struct Pixmap *spix, int alpha=1) + { + fz_pixmap *pm = NULL, *src_pix = (fz_pixmap *) spix; + int n, w, h, i; + fz_separations *seps = NULL; + fz_try(gctx) { + if (!INRANGE(alpha, 0, 1)) { + RAISEPY(gctx, "bad alpha value", PyExc_ValueError); + } + fz_colorspace *cs = fz_pixmap_colorspace(gctx, src_pix); + if (!cs && !alpha) { + RAISEPY(gctx, "cannot drop alpha for 'NULL' colorspace", PyExc_ValueError); + } + n = fz_pixmap_colorants(gctx, src_pix); + w = fz_pixmap_width(gctx, src_pix); + h = fz_pixmap_height(gctx, src_pix); + pm = fz_new_pixmap(gctx, cs, w, h, seps, alpha); + pm->x = src_pix->x; + pm->y = src_pix->y; + pm->xres = src_pix->xres; + pm->yres = src_pix->yres; + + // copy samples data ------------------------------------------ + unsigned char *sptr = src_pix->samples; + unsigned char *tptr = pm->samples; + if (src_pix->alpha == pm->alpha) { // identical samples + memcpy(tptr, sptr, w * h * (n + alpha)); + } else { + for (i = 0; i < w * h; i++) { + memcpy(tptr, sptr, n); + tptr += n; + if (pm->alpha) { + tptr[0] = 255; + tptr++; + } + sptr += n + src_pix->alpha; + } + } + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) pm; + } + + //---------------------------------------------------------------- + // create pixmap from samples data + //---------------------------------------------------------------- + Pixmap(struct Colorspace *cs, int w, int h, PyObject *samples, int alpha=0) + { + int n = fz_colorspace_n(gctx, (fz_colorspace *) cs); + int stride = (n + alpha) * w; + fz_separations *seps = NULL; + fz_buffer *res = NULL; + fz_pixmap *pm = NULL; + fz_try(gctx) { + size_t size = 0; + unsigned char *c = NULL; + res = JM_BufferFromBytes(gctx, samples); + if (!res) { + RAISEPY(gctx, "bad samples data", PyExc_ValueError); + } + size = fz_buffer_storage(gctx, res, &c); + if (stride * h != size) { + RAISEPY(gctx, "bad samples length", PyExc_ValueError); + } + pm = fz_new_pixmap(gctx, (fz_colorspace *) cs, w, h, seps, alpha); + memcpy(pm->samples, c, size); + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) pm; + } + + + //---------------------------------------------------------------- + // create pixmap from filename, file object, pathlib.Path or memory + //---------------------------------------------------------------- + Pixmap(PyObject *imagedata) + { + fz_buffer *res = NULL; + fz_image *img = NULL; + fz_pixmap *pm = NULL; + PyObject *fname = NULL; + PyObject *name = PyUnicode_FromString("name"); + fz_try(gctx) { + if (PyObject_HasAttrString(imagedata, "resolve")) { + fname = PyObject_CallMethod(imagedata, "__str__", NULL); + if (fname) { + img = fz_new_image_from_file(gctx, JM_StrAsChar(fname)); + } + } else if (PyObject_HasAttr(imagedata, name)) { + fname = PyObject_GetAttr(imagedata, name); + if (fname) { + img = fz_new_image_from_file(gctx, JM_StrAsChar(fname)); + } + } else if (PyUnicode_Check(imagedata)) { + img = fz_new_image_from_file(gctx, JM_StrAsChar(imagedata)); + } else { + res = JM_BufferFromBytes(gctx, imagedata); + if (!res || !fz_buffer_storage(gctx, res, NULL)) { + RAISEPY(gctx, "bad image data", PyExc_ValueError); + } + img = fz_new_image_from_buffer(gctx, res); + } + pm = fz_get_pixmap_from_image(gctx, img, NULL, NULL, NULL, NULL); + int xres, yres; + fz_image_resolution(img, &xres, &yres); + pm->xres = xres; + pm->yres = yres; + } + fz_always(gctx) { + Py_CLEAR(fname); + Py_CLEAR(name); + fz_drop_image(gctx, img); + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) pm; + } + + + //---------------------------------------------------------------- + // Create pixmap from PDF image identified by XREF number + //---------------------------------------------------------------- + Pixmap(struct Document *doc, int xref) + { + fz_image *img = NULL; + fz_pixmap *pix = NULL; + pdf_obj *ref = NULL; + pdf_obj *type; + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) doc); + fz_try(gctx) { + ASSERT_PDF(pdf); + int xreflen = pdf_xref_len(gctx, pdf); + if (!INRANGE(xref, 1, xreflen-1)) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + ref = pdf_new_indirect(gctx, pdf, xref, 0); + type = pdf_dict_get(gctx, ref, PDF_NAME(Subtype)); + if (!pdf_name_eq(gctx, type, PDF_NAME(Image)) && + !pdf_name_eq(gctx, type, PDF_NAME(Alpha)) && + !pdf_name_eq(gctx, type, PDF_NAME(Luminosity))) { + RAISEPY(gctx, MSG_IS_NO_IMAGE, PyExc_ValueError); + } + img = pdf_load_image(gctx, pdf, ref); + pix = fz_get_pixmap_from_image(gctx, img, NULL, NULL, NULL, NULL); + } + fz_always(gctx) { + fz_drop_image(gctx, img); + pdf_drop_obj(gctx, ref); + } + fz_catch(gctx) { + fz_drop_pixmap(gctx, pix); + return NULL; + } + return (struct Pixmap *) pix; + } + + + //---------------------------------------------------------------- + // warp + //---------------------------------------------------------------- + FITZEXCEPTION(warp, !result) + %pythonprepend warp %{ + """Return pixmap from a warped quad.""" + EnsureOwnership(self) + if not quad.is_convex: raise ValueError("quad must be convex")%} + struct Pixmap *warp(PyObject *quad, int width, int height) + { + fz_point points[4]; + fz_quad q = JM_quad_from_py(quad); + fz_pixmap *dst = NULL; + points[0] = q.ul; + points[1] = q.ur; + points[2] = q.lr; + points[3] = q.ll; + + fz_try(gctx) { + dst = fz_warp_pixmap(gctx, (fz_pixmap *) $self, points, width, height); + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) dst; + } + + + //---------------------------------------------------------------- + // shrink + //---------------------------------------------------------------- + ENSURE_OWNERSHIP(shrink, """Divide width and height by 2**factor. + E.g. factor=1 shrinks to 25% of original size (in place).""") + void shrink(int factor) + { + if (factor < 1) + { + JM_Warning("ignoring shrink factor < 1"); + return; + } + fz_subsample_pixmap(gctx, (fz_pixmap *) $self, factor); + } + + //---------------------------------------------------------------- + // apply gamma correction + //---------------------------------------------------------------- + ENSURE_OWNERSHIP(gamma_with, """Apply correction with some float. +gamma=1 is a no-op.""") + void gamma_with(float gamma) + { + if (!fz_pixmap_colorspace(gctx, (fz_pixmap *) $self)) + { + JM_Warning("colorspace invalid for function"); + return; + } + fz_gamma_pixmap(gctx, (fz_pixmap *) $self, gamma); + } + + //---------------------------------------------------------------- + // tint pixmap with color + //---------------------------------------------------------------- + %pythonprepend tint_with +%{"""Tint colors with modifiers for black and white.""" +EnsureOwnership(self) +if not self.colorspace or self.colorspace.n > 3: + print("warning: colorspace invalid for function") + return%} + void tint_with(int black, int white) + { + fz_tint_pixmap(gctx, (fz_pixmap *) $self, black, white); + } + + //----------------------------------------------------------------- + // clear all of pixmap samples to 0x00 */ + //----------------------------------------------------------------- + ENSURE_OWNERSHIP(clear_with, """Fill all color components with same value.""") + void clear_with() + { + fz_clear_pixmap(gctx, (fz_pixmap *) $self); + } + + //----------------------------------------------------------------- + // clear total pixmap with value */ + //----------------------------------------------------------------- + void clear_with(int value) + { + fz_clear_pixmap_with_value(gctx, (fz_pixmap *) $self, value); + } + + //----------------------------------------------------------------- + // clear pixmap rectangle with value + //----------------------------------------------------------------- + void clear_with(int value, PyObject *bbox) + { + JM_clear_pixmap_rect_with_value(gctx, (fz_pixmap *) $self, value, JM_irect_from_py(bbox)); + } + + //----------------------------------------------------------------- + // copy pixmaps + //----------------------------------------------------------------- + FITZEXCEPTION(copy, !result) + ENSURE_OWNERSHIP(copy, """Copy bbox from another Pixmap.""") + PyObject *copy(struct Pixmap *src, PyObject *bbox) + { + fz_try(gctx) { + fz_pixmap *pm = (fz_pixmap *) $self, *src_pix = (fz_pixmap *) src; + if (!fz_pixmap_colorspace(gctx, src_pix)) { + RAISEPY(gctx, "cannot copy pixmap with NULL colorspace", PyExc_ValueError); + } + if (pm->alpha != src_pix->alpha) { + RAISEPY(gctx, "source and target alpha must be equal", PyExc_ValueError); + } + fz_copy_pixmap_rect(gctx, pm, src_pix, JM_irect_from_py(bbox), NULL); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //----------------------------------------------------------------- + // set alpha values + //----------------------------------------------------------------- + FITZEXCEPTION(set_alpha, !result) + ENSURE_OWNERSHIP(set_alpha, """Set alpha channel to values contained in a byte array. +If None, all alphas are 255. + +Args: + alphavalues: (bytes) with length (width * height) or 'None'. + premultiply: (bool, True) premultiply colors with alpha values. + opaque: (tuple, length colorspace.n) this color receives opacity 0. + matte: (tuple, length colorspace.n)) preblending background color. +""") + PyObject *set_alpha(PyObject *alphavalues=NULL, int premultiply=1, PyObject *opaque=NULL, PyObject *matte=NULL) + { + fz_buffer *res = NULL; + fz_pixmap *pix = (fz_pixmap *) $self; + unsigned char alpha = 0, m = 0; + fz_try(gctx) { + if (pix->alpha == 0) { + RAISEPY(gctx, MSG_PIX_NOALPHA, PyExc_ValueError); + } + size_t i, k, j; + size_t n = fz_pixmap_colorants(gctx, pix); + size_t w = (size_t) fz_pixmap_width(gctx, pix); + size_t h = (size_t) fz_pixmap_height(gctx, pix); + size_t balen = w * h * (n+1); + int colors[4]; // make this color opaque + int bgcolor[4]; // preblending background color + int zero_out = 0, bground = 0; + if (opaque && PySequence_Check(opaque) && PySequence_Size(opaque) == n) { + for (i = 0; i < n; i++) { + if (JM_INT_ITEM(opaque, i, &colors[i]) == 1) { + RAISEPY(gctx, "bad opaque components", PyExc_ValueError); + } + } + zero_out = 1; + } + if (matte && PySequence_Check(matte) && PySequence_Size(matte) == n) { + for (i = 0; i < n; i++) { + if (JM_INT_ITEM(matte, i, &bgcolor[i]) == 1) { + RAISEPY(gctx, "bad matte components", PyExc_ValueError); + } + } + bground = 1; + } + unsigned char *data = NULL; + size_t data_len = 0; + if (alphavalues && PyObject_IsTrue(alphavalues)) { + res = JM_BufferFromBytes(gctx, alphavalues); + data_len = fz_buffer_storage(gctx, res, &data); + if (data_len < w * h) { + RAISEPY(gctx, "bad alpha values", PyExc_ValueError); + } + } + i = k = j = 0; + int data_fix = 255; + while (i < balen) { + alpha = data[k]; + if (zero_out) { + for (j = i; j < i+n; j++) { + if (pix->samples[j] != (unsigned char) colors[j - i]) { + data_fix = 255; + break; + } else { + data_fix = 0; + } + } + } + if (data_len) { + if (data_fix == 0) { + pix->samples[i+n] = 0; + } else { + pix->samples[i+n] = alpha; + } + if (premultiply && !bground) { + for (j = i; j < i+n; j++) { + pix->samples[j] = fz_mul255(pix->samples[j], alpha); + } + } else if (bground) { + for (j = i; j < i+n; j++) { + m = (unsigned char) bgcolor[j - i]; + pix->samples[j] = m + fz_mul255((pix->samples[j] - m), alpha); + } + } + } else { + pix->samples[i+n] = data_fix; + } + i += n+1; + k += 1; + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //----------------------------------------------------------------- + // Pixmap._tobytes + //----------------------------------------------------------------- + FITZEXCEPTION(_tobytes, !result) + PyObject *_tobytes(int format, int jpg_quality) + { + fz_output *out = NULL; + fz_buffer *res = NULL; + PyObject *barray = NULL; + fz_pixmap *pm = (fz_pixmap *) $self; + fz_try(gctx) { + size_t size = fz_pixmap_stride(gctx, pm) * pm->h; + res = fz_new_buffer(gctx, size); + out = fz_new_output_with_buffer(gctx, res); + + switch(format) { + case(1): + fz_write_pixmap_as_png(gctx, out, pm); + break; + case(2): + fz_write_pixmap_as_pnm(gctx, out, pm); + break; + case(3): + fz_write_pixmap_as_pam(gctx, out, pm); + break; + case(5): // Adobe Photoshop Document + fz_write_pixmap_as_psd(gctx, out, pm); + break; + case(6): // Postscript format + fz_write_pixmap_as_ps(gctx, out, pm); + break; + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + case(7): // JPEG format + fz_write_pixmap_as_jpeg(gctx, out, pm, jpg_quality); + break; + #endif + default: + fz_write_pixmap_as_png(gctx, out, pm); + break; + } + barray = JM_BinFromBuffer(gctx, res); + } + fz_always(gctx) { + fz_drop_output(gctx, out); + fz_drop_buffer(gctx, res); + } + + fz_catch(gctx) { + return NULL; + } + return barray; + } + + %pythoncode %{ +def tobytes(self, output="png", jpg_quality=95): + """Convert to binary image stream of desired type. + + Can be used as input to GUI packages like tkinter. + + Args: + output: (str) image type, default is PNG. Others are JPG, JPEG, PNM, PGM, PPM, + PBM, PAM, PSD, PS. + Returns: + Bytes object. + """ + EnsureOwnership(self) + valid_formats = {"png": 1, "pnm": 2, "pgm": 2, "ppm": 2, "pbm": 2, + "pam": 3, "psd": 5, "ps": 6, "jpg": 7, "jpeg": 7} + + idx = valid_formats.get(output.lower(), None) + if idx==None: + raise ValueError(f"Image format {output} not in {tuple(valid_formats.keys())}") + if self.alpha and idx in (2, 6, 7): + raise ValueError("'%s' cannot have alpha" % output) + if self.colorspace and self.colorspace.n > 3 and idx in (1, 2, 4): + raise ValueError("unsupported colorspace for '%s'" % output) + if idx == 7: + self.set_dpi(self.xres, self.yres) + barray = self._tobytes(idx, jpg_quality) + return barray + %} + + + //----------------------------------------------------------------- + // output as PDF-OCR + //----------------------------------------------------------------- + FITZEXCEPTION(pdfocr_save, !result) + %pythonprepend pdfocr_save %{ + """Save pixmap as an OCR-ed PDF page.""" + EnsureOwnership(self) + if not os.getenv("TESSDATA_PREFIX") and not tessdata: + raise RuntimeError("No OCR support: TESSDATA_PREFIX not set") + %} + ENSURE_OWNERSHIP(pdfocr_save, ) + PyObject *pdfocr_save(PyObject *filename, int compress=1, char *language=NULL, char *tessdata=NULL) + { + fz_pdfocr_options opts; + memset(&opts, 0, sizeof opts); + opts.compress = compress; + if (language) { + fz_strlcpy(opts.language, language, sizeof(opts.language)); + } + if (tessdata) { + fz_strlcpy(opts.datadir, tessdata, sizeof(opts.language)); + } + fz_output *out = NULL; + fz_pixmap *pix = (fz_pixmap *) $self; + fz_try(gctx) { + if (PyUnicode_Check(filename)) { + fz_save_pixmap_as_pdfocr(gctx, pix, (char *) PyUnicode_AsUTF8(filename), 0, &opts); + } else { + out = JM_new_output_fileptr(gctx, filename); + fz_write_pixmap_as_pdfocr(gctx, out, pix, &opts); + } + } + fz_always(gctx) { + fz_drop_output(gctx, out); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + %pythoncode %{ + def pdfocr_tobytes(self, compress=True, language="eng", tessdata=None): + """Save pixmap as an OCR-ed PDF page. + + Args: + compress: (bool) compress, default 1 (True). + language: (str) language(s) occurring on page, default "eng" (English), + multiples like "eng+ger" for English and German. + tessdata: (str) folder name of Tesseract's language support. Must be + given if environment variable TESSDATA_PREFIX is not set. + Notes: + On failure, make sure Tesseract is installed and you have set the + environment variable "TESSDATA_PREFIX" to the folder containing your + Tesseract's language support data. + """ + if not os.getenv("TESSDATA_PREFIX") and not tessdata: + raise RuntimeError("No OCR support: TESSDATA_PREFIX not set") + EnsureOwnership(self) + from io import BytesIO + bio = BytesIO() + self.pdfocr_save(bio, compress=compress, language=language, tessdata=tessdata) + return bio.getvalue() + %} + + + //----------------------------------------------------------------- + // _writeIMG + //----------------------------------------------------------------- + FITZEXCEPTION(_writeIMG, !result) + PyObject *_writeIMG(char *filename, int format, int jpg_quality) + { + fz_try(gctx) { + fz_pixmap *pm = (fz_pixmap *) $self; + switch(format) { + case(1): + fz_save_pixmap_as_png(gctx, pm, filename); + break; + case(2): + fz_save_pixmap_as_pnm(gctx, pm, filename); + break; + case(3): + fz_save_pixmap_as_pam(gctx, pm, filename); + break; + case(5): // Adobe Photoshop Document + fz_save_pixmap_as_psd(gctx, pm, filename); + break; + case(6): // Postscript + fz_save_pixmap_as_ps(gctx, pm, filename, 0); + break; + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + case(7): // JPEG + fz_save_pixmap_as_jpeg(gctx, pm, filename, jpg_quality); + break; + #endif + default: + fz_save_pixmap_as_png(gctx, pm, filename); + break; + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + %pythoncode %{ +def save(self, filename, output=None, jpg_quality=95): + """Output as image in format determined by filename extension. + + Args: + output: (str) only use to overrule filename extension. Default is PNG. + Others are JPEG, JPG, PNM, PGM, PPM, PBM, PAM, PSD, PS. + """ + EnsureOwnership(self) + valid_formats = {"png": 1, "pnm": 2, "pgm": 2, "ppm": 2, "pbm": 2, + "pam": 3, "psd": 5, "ps": 6, "jpg": 7, "jpeg": 7} + + if type(filename) is str: + pass + elif hasattr(filename, "absolute"): + filename = str(filename) + elif hasattr(filename, "name"): + filename = filename.name + if output is None: + _, ext = os.path.splitext(filename) + output = ext[1:] + + idx = valid_formats.get(output.lower(), None) + if idx == None: + raise ValueError(f"Image format {output} not in {tuple(valid_formats.keys())}") + if self.alpha and idx in (2, 6, 7): + raise ValueError("'%s' cannot have alpha" % output) + if self.colorspace and self.colorspace.n > 3 and idx in (1, 2, 4): + raise ValueError("unsupported colorspace for '%s'" % output) + if idx == 7: + self.set_dpi(self.xres, self.yres) + return self._writeIMG(filename, idx, jpg_quality) + +def pil_save(self, *args, **kwargs): + """Write to image file using Pillow. + + Args are passed to Pillow's Image.save method, see their documentation. + Use instead of save when other output formats are desired. + """ + EnsureOwnership(self) + try: + from PIL import Image + except ImportError: + print("PIL/Pillow not installed") + raise + + cspace = self.colorspace + if cspace is None: + mode = "L" + elif cspace.n == 1: + mode = "L" if self.alpha == 0 else "LA" + elif cspace.n == 3: + mode = "RGB" if self.alpha == 0 else "RGBA" + else: + mode = "CMYK" + + img = Image.frombytes(mode, (self.width, self.height), self.samples) + + if "dpi" not in kwargs.keys(): + kwargs["dpi"] = (self.xres, self.yres) + + img.save(*args, **kwargs) + +def pil_tobytes(self, *args, **kwargs): + """Convert to binary image stream using pillow. + + Args are passed to Pillow's Image.save method, see their documentation. + Use instead of 'tobytes' when other output formats are needed. + """ + EnsureOwnership(self) + from io import BytesIO + bytes_out = BytesIO() + self.pil_save(bytes_out, *args, **kwargs) + return bytes_out.getvalue() + + %} + //----------------------------------------------------------------- + // invert_irect + //----------------------------------------------------------------- + %pythonprepend invert_irect + %{"""Invert the colors inside a bbox."""%} + PyObject *invert_irect(PyObject *bbox = NULL) + { + fz_pixmap *pm = (fz_pixmap *) $self; + if (!fz_pixmap_colorspace(gctx, pm)) + { + JM_Warning("ignored for stencil pixmap"); + return JM_BOOL(0); + } + + fz_irect r = JM_irect_from_py(bbox); + if (fz_is_infinite_irect(r)) + r = fz_pixmap_bbox(gctx, pm); + + return JM_BOOL(JM_invert_pixmap_rect(gctx, pm, r)); + } + + //----------------------------------------------------------------- + // get one pixel as a list + //----------------------------------------------------------------- + FITZEXCEPTION(pixel, !result) + ENSURE_OWNERSHIP(pixel, """Get color tuple of pixel (x, y). +Includes alpha byte if applicable.""") + PyObject *pixel(int x, int y) + { + PyObject *p = NULL; + fz_try(gctx) { + fz_pixmap *pm = (fz_pixmap *) $self; + if (!INRANGE(x, 0, pm->w - 1) || !INRANGE(y, 0, pm->h - 1)) { + RAISEPY(gctx, MSG_PIXEL_OUTSIDE, PyExc_ValueError); + } + int n = pm->n; + int stride = fz_pixmap_stride(gctx, pm); + int j, i = stride * y + n * x; + p = PyTuple_New(n); + for (j = 0; j < n; j++) { + PyTuple_SET_ITEM(p, j, Py_BuildValue("i", pm->samples[i + j])); + } + } + fz_catch(gctx) { + return NULL; + } + return p; + } + + //----------------------------------------------------------------- + // Set one pixel to a given color tuple + //----------------------------------------------------------------- + FITZEXCEPTION(set_pixel, !result) + ENSURE_OWNERSHIP(set_pixel, """Set color of pixel (x, y).""") + PyObject *set_pixel(int x, int y, PyObject *color) + { + fz_try(gctx) { + fz_pixmap *pm = (fz_pixmap *) $self; + if (!INRANGE(x, 0, pm->w - 1) || !INRANGE(y, 0, pm->h - 1)) { + RAISEPY(gctx, MSG_PIXEL_OUTSIDE, PyExc_ValueError); + } + int n = pm->n; + if (!PySequence_Check(color) || PySequence_Size(color) != n) { + RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError); + } + int i, j; + unsigned char c[5]; + for (j = 0; j < n; j++) { + if (JM_INT_ITEM(color, j, &i) == 1) { + RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError); + } + if (!INRANGE(i, 0, 255)) { + RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError); + } + c[j] = (unsigned char) i; + } + int stride = fz_pixmap_stride(gctx, pm); + i = stride * y + n * x; + for (j = 0; j < n; j++) { + pm->samples[i + j] = c[j]; + } + } + fz_catch(gctx) { + PyErr_Clear(); + return NULL; + } + Py_RETURN_NONE; + } + + + //----------------------------------------------------------------- + // Set Pixmap origin + //----------------------------------------------------------------- + ENSURE_OWNERSHIP(set_origin, """Set top-left coordinates.""") + PyObject *set_origin(int x, int y) + { + fz_pixmap *pm = (fz_pixmap *) $self; + pm->x = x; + pm->y = y; + Py_RETURN_NONE; + } + + ENSURE_OWNERSHIP(set_dpi, """Set resolution in both dimensions.""") + PyObject *set_dpi(int xres, int yres) + { + fz_pixmap *pm = (fz_pixmap *) $self; + pm->xres = xres; + pm->yres = yres; + Py_RETURN_NONE; + } + + //----------------------------------------------------------------- + // Set a rect to a given color tuple + //----------------------------------------------------------------- + FITZEXCEPTION(set_rect, !result) + ENSURE_OWNERSHIP(set_rect, """Set color of all pixels in bbox.""") + PyObject *set_rect(PyObject *bbox, PyObject *color) + { + PyObject *rc = NULL; + fz_try(gctx) { + fz_pixmap *pm = (fz_pixmap *) $self; + Py_ssize_t j, n = (Py_ssize_t) pm->n; + if (!PySequence_Check(color) || PySequence_Size(color) != n) { + RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError); + } + unsigned char c[5]; + int i; + for (j = 0; j < n; j++) { + if (JM_INT_ITEM(color, j, &i) == 1) { + RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError); + } + if (!INRANGE(i, 0, 255)) { + RAISEPY(gctx, MSG_BAD_COLOR_SEQ, PyExc_ValueError); + } + c[j] = (unsigned char) i; + } + i = JM_fill_pixmap_rect_with_color(gctx, pm, c, JM_irect_from_py(bbox)); + rc = JM_BOOL(i); + } + fz_catch(gctx) { + PyErr_Clear(); + return NULL; + } + return rc; + } + + //----------------------------------------------------------------- + // check if monochrome + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(is_monochrome, """Check if pixmap is monochrome.""") + PyObject *is_monochrome() + { + return JM_BOOL(fz_is_pixmap_monochrome(gctx, (fz_pixmap *) $self)); + } + + //----------------------------------------------------------------- + // check if unicolor (only one color there) + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(is_unicolor, """Check if pixmap has only one color.""") + PyObject *is_unicolor() + { + fz_pixmap *pm = (fz_pixmap *) $self; + size_t i, n = pm->n, count = pm->w * pm->h * n; + unsigned char *s = pm->samples; + for (i = n; i < count; i += n) { + if (memcmp(s, s + i, n) != 0) { + Py_RETURN_FALSE; + } + } + Py_RETURN_TRUE; + } + + + //----------------------------------------------------------------- + // count each pixmap color + //----------------------------------------------------------------- + FITZEXCEPTION(color_count, !result) + ENSURE_OWNERSHIP(color_count, """Return count of each color.""") + PyObject *color_count(int colors=0, PyObject *clip=NULL) + { + fz_pixmap *pm = (fz_pixmap *) $self; + PyObject *rc = NULL; + fz_try(gctx) { + rc = JM_color_count(gctx, pm, clip); + if (!rc) { + RAISEPY(gctx, MSG_COLOR_COUNT_FAILED, PyExc_RuntimeError); + } + } + fz_catch(gctx) { + return NULL; + } + if (!colors) { + Py_ssize_t len = PyDict_Size(rc); + Py_DECREF(rc); + return PyLong_FromSsize_t(len); + } + return rc; + } + + %pythoncode %{ + def color_topusage(self, clip=None): + """Return most frequent color and its usage ratio.""" + EnsureOwnership(self) + allpixels = 0 + cnt = 0 + if clip != None and self.irect in Rect(clip): + clip = self.irect + for pixel, count in self.color_count(colors=True,clip=clip).items(): + allpixels += count + if count > cnt: + cnt = count + maxpixel = pixel + if not allpixels: + return (1, bytes([255] * self.n)) + return (cnt / allpixels, maxpixel) + + %} + + //----------------------------------------------------------------- + // MD5 digest of pixmap + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(digest, """MD5 digest of pixmap (bytes).""") + PyObject *digest() + { + unsigned char digest[16]; + fz_md5_pixmap(gctx, (fz_pixmap *) $self, digest); + return PyBytes_FromStringAndSize(digest, 16); + } + + //----------------------------------------------------------------- + // get length of one image row + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(stride, """Length of one image line (width * n).""") + PyObject *stride() + { + return PyLong_FromSize_t((size_t) fz_pixmap_stride(gctx, (fz_pixmap *) $self)); + } + + //----------------------------------------------------------------- + // x, y, width, height, xres, yres, n + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(xres, """Resolution in x direction.""") + int xres() + { + fz_pixmap *this_pix = (fz_pixmap *) $self; + return this_pix->xres; + } + + %pythoncode %{@property%} + ENSURE_OWNERSHIP(yres, """Resolution in y direction.""") + int yres() + { + fz_pixmap *this_pix = (fz_pixmap *) $self; + return this_pix->yres; + } + + %pythoncode %{@property%} + ENSURE_OWNERSHIP(w, """The width.""") + PyObject *w() + { + return PyLong_FromSize_t((size_t) fz_pixmap_width(gctx, (fz_pixmap *) $self)); + } + + %pythoncode %{@property%} + ENSURE_OWNERSHIP(h, """The height.""") + PyObject *h() + { + return PyLong_FromSize_t((size_t) fz_pixmap_height(gctx, (fz_pixmap *) $self)); + } + + %pythoncode %{@property%} + ENSURE_OWNERSHIP(x, """x component of Pixmap origin.""") + int x() + { + return fz_pixmap_x(gctx, (fz_pixmap *) $self); + } + + %pythoncode %{@property%} + ENSURE_OWNERSHIP(y, """y component of Pixmap origin.""") + int y() + { + return fz_pixmap_y(gctx, (fz_pixmap *) $self); + } + + %pythoncode %{@property%} + ENSURE_OWNERSHIP(n, """The size of one pixel.""") + int n() + { + return fz_pixmap_components(gctx, (fz_pixmap *) $self); + } + + //----------------------------------------------------------------- + // check alpha channel + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(alpha, """Indicates presence of alpha channel.""") + int alpha() + { + return fz_pixmap_alpha(gctx, (fz_pixmap *) $self); + } + + //----------------------------------------------------------------- + // get colorspace of pixmap + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(colorspace, """Pixmap Colorspace.""") + struct Colorspace *colorspace() + { + return (struct Colorspace *) fz_pixmap_colorspace(gctx, (fz_pixmap *) $self); + } + + //----------------------------------------------------------------- + // return irect of pixmap + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(irect, """Pixmap bbox - an IRect object.""") + %pythonappend irect %{val = IRect(val)%} + PyObject *irect() + { + return JM_py_from_irect(fz_pixmap_bbox(gctx, (fz_pixmap *) $self)); + } + + //----------------------------------------------------------------- + // return size of pixmap + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(size, """Pixmap size.""") + PyObject *size() + { + return PyLong_FromSize_t(fz_pixmap_size(gctx, (fz_pixmap *) $self)); + } + + //----------------------------------------------------------------- + // samples + //----------------------------------------------------------------- + %pythoncode %{@property%} + ENSURE_OWNERSHIP(samples_mv, """Pixmap samples memoryview.""") + PyObject *samples_mv() + { + fz_pixmap *pm = (fz_pixmap *) $self; + Py_ssize_t s = (Py_ssize_t) pm->w; + s *= pm->h; + s *= pm->n; + return PyMemoryView_FromMemory((char *) pm->samples, s, PyBUF_READ); + } + + + %pythoncode %{@property%} + ENSURE_OWNERSHIP(samples_ptr, """Pixmap samples pointer.""") + PyObject *samples_ptr() + { + fz_pixmap *pm = (fz_pixmap *) $self; + return PyLong_FromVoidPtr((void *) pm->samples); + } + + %pythoncode %{ + @property + def samples(self)->bytes: + return bytes(self.samples_mv) + + width = w + height = h + + def __len__(self): + return self.size + + def __repr__(self): + EnsureOwnership(self) + if not type(self) is Pixmap: return + if self.colorspace: + return "Pixmap(%s, %s, %s)" % (self.colorspace.name, self.irect, self.alpha) + else: + return "Pixmap(%s, %s, %s)" % ('None', self.irect, self.alpha) + + def __enter__(self): + return self + + def __exit__(self, *args): + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + + def __del__(self): + if not type(self) is Pixmap: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + + %} + } +}; + +/* fz_colorspace */ +struct Colorspace +{ + %extend { + ~Colorspace() + { + DEBUGMSG1("Colorspace"); + fz_colorspace *this_cs = (fz_colorspace *) $self; + fz_drop_colorspace(gctx, this_cs); + DEBUGMSG2; + } + + %pythonprepend Colorspace + %{"""Supported are GRAY, RGB and CMYK."""%} + Colorspace(int type) + { + fz_colorspace *cs = NULL; + switch(type) { + case CS_GRAY: + cs = fz_device_gray(gctx); + break; + case CS_CMYK: + cs = fz_device_cmyk(gctx); + break; + case CS_RGB: + default: + cs = fz_device_rgb(gctx); + break; + } + fz_keep_colorspace(gctx, cs); + return (struct Colorspace *) cs; + } + //----------------------------------------------------------------- + // number of bytes to define color of one pixel + //----------------------------------------------------------------- + %pythoncode %{@property%} + %pythonprepend n %{"""Size of one pixel."""%} + PyObject *n() + { + return Py_BuildValue("i", fz_colorspace_n(gctx, (fz_colorspace *) $self)); + } + + //----------------------------------------------------------------- + // name of colorspace + //----------------------------------------------------------------- + PyObject *_name() + { + return JM_UnicodeFromStr(fz_colorspace_name(gctx, (fz_colorspace *) $self)); + } + + %pythoncode %{ + @property + def name(self): + """Name of the Colorspace.""" + + if self.n == 1: + return csGRAY._name() + elif self.n == 3: + return csRGB._name() + elif self.n == 4: + return csCMYK._name() + return self._name() + + def __repr__(self): + x = ("", "GRAY", "", "RGB", "CMYK")[self.n] + return "Colorspace(CS_%s) - %s" % (x, self.name) + %} + } +}; + + +/* fz_device wrapper */ +%rename(Device) DeviceWrapper; +struct DeviceWrapper +{ + %extend { + FITZEXCEPTION(DeviceWrapper, !result) + DeviceWrapper(struct Pixmap *pm, PyObject *clip) { + struct DeviceWrapper *dw = NULL; + fz_try(gctx) { + dw = (struct DeviceWrapper *)calloc(1, sizeof(struct DeviceWrapper)); + fz_irect bbox = JM_irect_from_py(clip); + if (fz_is_infinite_irect(bbox)) + dw->device = fz_new_draw_device(gctx, fz_identity, (fz_pixmap *) pm); + else + dw->device = fz_new_draw_device_with_bbox(gctx, fz_identity, (fz_pixmap *) pm, &bbox); + } + fz_catch(gctx) { + return NULL; + } + return dw; + } + DeviceWrapper(struct DisplayList *dl) { + struct DeviceWrapper *dw = NULL; + fz_try(gctx) { + dw = (struct DeviceWrapper *)calloc(1, sizeof(struct DeviceWrapper)); + dw->device = fz_new_list_device(gctx, (fz_display_list *) dl); + dw->list = (fz_display_list *) dl; + fz_keep_display_list(gctx, (fz_display_list *) dl); + } + fz_catch(gctx) { + return NULL; + } + return dw; + } + DeviceWrapper(struct TextPage *tp, int flags = 0) { + struct DeviceWrapper *dw = NULL; + fz_try(gctx) { + dw = (struct DeviceWrapper *)calloc(1, sizeof(struct DeviceWrapper)); + fz_stext_options opts = { 0 }; + opts.flags = flags; + dw->device = fz_new_stext_device(gctx, (fz_stext_page *) tp, &opts); + } + fz_catch(gctx) { + return NULL; + } + return dw; + } + ~DeviceWrapper() { + fz_display_list *list = $self->list; + DEBUGMSG1("Device"); + fz_close_device(gctx, $self->device); + fz_drop_device(gctx, $self->device); + DEBUGMSG2; + if(list) + { + DEBUGMSG1("DisplayList after Device"); + fz_drop_display_list(gctx, list); + DEBUGMSG2; + } + } + } +}; + +//------------------------------------------------------------------------ +// fz_outline +//------------------------------------------------------------------------ +%nodefaultctor; +struct Outline { + %immutable; + %extend { + ~Outline() + { + DEBUGMSG1("Outline"); + fz_outline *this_ol = (fz_outline *) $self; + fz_drop_outline(gctx, this_ol); + DEBUGMSG2; + } + + %pythoncode %{@property%} + PyObject *uri() + { + fz_outline *ol = (fz_outline *) $self; + return JM_UnicodeFromStr(ol->uri); + } + + /* `%newobject foo;` is equivalent to wrapping C fn in python like: + ret = _foo() + ret.thisown=true + return ret. + */ + %newobject next; + %pythoncode %{@property%} + struct Outline *next() + { + fz_outline *ol = (fz_outline *) $self; + fz_outline *next_ol = ol->next; + if (!next_ol) return NULL; + next_ol = fz_keep_outline(gctx, next_ol); + return (struct Outline *) next_ol; + } + + %newobject down; + %pythoncode %{@property%} + struct Outline *down() + { + fz_outline *ol = (fz_outline *) $self; + fz_outline *down_ol = ol->down; + if (!down_ol) return NULL; + down_ol = fz_keep_outline(gctx, down_ol); + return (struct Outline *) down_ol; + } + + %pythoncode %{@property%} + PyObject *is_external() + { + fz_outline *ol = (fz_outline *) $self; + if (!ol->uri) Py_RETURN_FALSE; + return JM_BOOL(fz_is_external_link(gctx, ol->uri)); + } + + %pythoncode %{@property%} + int page() + { + fz_outline *ol = (fz_outline *) $self; + return ol->page.page; + } + + %pythoncode %{@property%} + float x() + { + fz_outline *ol = (fz_outline *) $self; + return ol->x; + } + + %pythoncode %{@property%} + float y() + { + fz_outline *ol = (fz_outline *) $self; + return ol->y; + } + + %pythoncode %{@property%} + PyObject *title() + { + fz_outline *ol = (fz_outline *) $self; + return JM_UnicodeFromStr(ol->title); + } + + %pythoncode %{@property%} + PyObject *is_open() + { + fz_outline *ol = (fz_outline *) $self; + return JM_BOOL(ol->is_open); + } + + %pythoncode %{ + @property + def dest(self): + '''outline destination details''' + return linkDest(self, None) + + def __del__(self): + if not isinstance(self, Outline): + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; +%clearnodefaultctor; + + +//------------------------------------------------------------------------ +// Annotation +//------------------------------------------------------------------------ +%nodefaultctor; +struct Annot +{ + %extend + { + ~Annot() + { + DEBUGMSG1("Annot"); + pdf_annot *this_annot = (pdf_annot *) $self; + pdf_drop_annot(gctx, this_annot); + DEBUGMSG2; + } + //---------------------------------------------------------------- + // annotation rectangle + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(rect, """annotation rectangle""") + %pythonappend rect %{ + val = Rect(val) + val *= self.parent.derotation_matrix + %} + PyObject * + rect() + { + fz_rect r = pdf_bound_annot(gctx, (pdf_annot *) $self); + return JM_py_from_rect(r); + } + + %pythoncode %{@property%} + PARENTCHECK(rect_delta, """annotation delta values to rectangle""") + PyObject * + rect_delta() + { + PyObject *rc=NULL; + float d; + fz_try(gctx) { + pdf_obj *annot_obj = pdf_annot_obj(gctx, (pdf_annot *) $self); + pdf_obj *arr = pdf_dict_get(gctx, annot_obj, PDF_NAME(RD)); + int i, n = pdf_array_len(gctx, arr); + if (n != 4) { + rc = Py_BuildValue("s", NULL); + } else { + rc = PyTuple_New(4); + for (i = 0; i < n; i++) { + d = pdf_to_real(gctx, pdf_array_get(gctx, arr, i)); + if (i == 2 || i == 3) d *= -1; + PyTuple_SET_ITEM(rc, i, Py_BuildValue("f", d)); + } + } + } + fz_catch(gctx) { + Py_RETURN_NONE; + } + return rc; + } + + //---------------------------------------------------------------- + // annotation xref number + //---------------------------------------------------------------- + PARENTCHECK(xref, """annotation xref""") + %pythoncode %{@property%} + PyObject *xref() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + return Py_BuildValue("i", pdf_to_num(gctx, annot_obj)); + } + + //---------------------------------------------------------------- + // annotation get IRT xref number + //---------------------------------------------------------------- + PARENTCHECK(irt_xref, """annotation IRT xref""") + %pythoncode %{@property%} + PyObject *irt_xref() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *irt = pdf_dict_get(gctx, annot_obj, PDF_NAME(IRT)); + if (!irt) return PyLong_FromLong(0); + return PyLong_FromLong((long) pdf_to_num(gctx, irt)); + } + + //---------------------------------------------------------------- + // annotation set IRT xref number + //---------------------------------------------------------------- + FITZEXCEPTION(set_irt_xref, !result) + PARENTCHECK(set_irt_xref, """Set annotation IRT xref""") + PyObject *set_irt_xref(int xref) + { + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_page *page = pdf_annot_page(gctx, annot); + if (!INRANGE(xref, 1, pdf_xref_len(gctx, page->doc) - 1)) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + pdf_obj *irt = pdf_new_indirect(gctx, page->doc, xref, 0); + pdf_obj *subt = pdf_dict_get(gctx, irt, PDF_NAME(Subtype)); + int irt_subt = pdf_annot_type_from_string(gctx, pdf_to_name(gctx, subt)); + if (irt_subt < 0) { + pdf_drop_obj(gctx, irt); + RAISEPY(gctx, MSG_IS_NO_ANNOT, PyExc_ValueError); + } + pdf_dict_put_drop(gctx, annot_obj, PDF_NAME(IRT), irt); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------------------------------------- + // annotation get AP/N Matrix + //---------------------------------------------------------------- + PARENTCHECK(apn_matrix, """annotation appearance matrix""") + %pythonappend apn_matrix %{val = Matrix(val)%} + %pythoncode %{@property%} + PyObject * + apn_matrix() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP), + PDF_NAME(N), NULL); + if (!ap) + return JM_py_from_matrix(fz_identity); + fz_matrix mat = pdf_dict_get_matrix(gctx, ap, PDF_NAME(Matrix)); + return JM_py_from_matrix(mat); + } + + + //---------------------------------------------------------------- + // annotation get AP/N BBox + //---------------------------------------------------------------- + PARENTCHECK(apn_bbox, """annotation appearance bbox""") + %pythonappend apn_bbox %{ + val = Rect(val) * self.parent.transformation_matrix + val *= self.parent.derotation_matrix%} + %pythoncode %{@property%} + PyObject * + apn_bbox() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP), + PDF_NAME(N), NULL); + if (!ap) + return JM_py_from_rect(fz_infinite_rect); + fz_rect rect = pdf_dict_get_rect(gctx, ap, PDF_NAME(BBox)); + return JM_py_from_rect(rect); + } + + + //---------------------------------------------------------------- + // annotation set AP/N Matrix + //---------------------------------------------------------------- + FITZEXCEPTION(set_apn_matrix, !result) + PARENTCHECK(set_apn_matrix, """Set annotation appearance matrix.""") + PyObject * + set_apn_matrix(PyObject *matrix) + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + fz_try(gctx) { + pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP), + PDF_NAME(N), NULL); + if (!ap) { + RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError); + } + fz_matrix mat = JM_matrix_from_py(matrix); + pdf_dict_put_matrix(gctx, ap, PDF_NAME(Matrix), mat); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation set AP/N BBox + //---------------------------------------------------------------- + FITZEXCEPTION(set_apn_bbox, !result) + %pythonprepend set_apn_bbox %{ + """Set annotation appearance bbox.""" + + CheckParent(self) + page = self.parent + rot = page.rotation_matrix + mat = page.transformation_matrix + bbox *= rot * ~mat + %} + PyObject * + set_apn_bbox(PyObject *bbox) + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + fz_try(gctx) { + pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP), + PDF_NAME(N), NULL); + if (!ap) { + RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError); + } + fz_rect rect = JM_rect_from_py(bbox); + pdf_dict_put_rect(gctx, ap, PDF_NAME(BBox), rect); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation show blend mode (/BM) + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(blendmode, """annotation BlendMode""") + PyObject *blendmode() + { + PyObject *blend_mode = NULL; + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *obj, *obj1, *obj2; + obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(BM)); + if (obj) { + blend_mode = JM_UnicodeFromStr(pdf_to_name(gctx, obj)); + goto finished; + } + // loop through the /AP/N/Resources/ExtGState objects + obj = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP), + PDF_NAME(N), + PDF_NAME(Resources), + PDF_NAME(ExtGState), + NULL); + + if (pdf_is_dict(gctx, obj)) { + int i, j, m, n = pdf_dict_len(gctx, obj); + for (i = 0; i < n; i++) { + obj1 = pdf_dict_get_val(gctx, obj, i); + if (pdf_is_dict(gctx, obj1)) { + m = pdf_dict_len(gctx, obj1); + for (j = 0; j < m; j++) { + obj2 = pdf_dict_get_key(gctx, obj1, j); + if (pdf_objcmp(gctx, obj2, PDF_NAME(BM)) == 0) { + blend_mode = JM_UnicodeFromStr(pdf_to_name(gctx, pdf_dict_get_val(gctx, obj1, j))); + goto finished; + } + } + } + } + } + finished:; + } + fz_catch(gctx) { + Py_RETURN_NONE; + } + if (blend_mode) return blend_mode; + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation set blend mode (/BM) + //---------------------------------------------------------------- + FITZEXCEPTION(set_blendmode, !result) + PARENTCHECK(set_blendmode, """Set annotation BlendMode.""") + PyObject * + set_blendmode(char *blend_mode) + { + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(BM), blend_mode); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation get optional content + //---------------------------------------------------------------- + FITZEXCEPTION(get_oc, !result) + PARENTCHECK(get_oc, """Get annotation optional content reference.""") + PyObject *get_oc() + { + int oc = 0; + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(OC)); + if (obj) { + oc = pdf_to_num(gctx, obj); + } + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", oc); + } + + + //---------------------------------------------------------------- + // annotation set open + //---------------------------------------------------------------- + FITZEXCEPTION(set_open, !result) + PARENTCHECK(set_open, """Set 'open' status of annotation or its Popup.""") + PyObject *set_open(int is_open) + { + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_set_annot_is_open(gctx, annot, is_open); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation inquiry: is open + //---------------------------------------------------------------- + FITZEXCEPTION(is_open, !result) + PARENTCHECK(is_open, """Get 'open' status of annotation or its Popup.""") + %pythoncode %{@property%} + PyObject * + is_open() + { + int is_open; + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + is_open = pdf_annot_is_open(gctx, annot); + } + fz_catch(gctx) { + return NULL; + } + return JM_BOOL(is_open); + } + + + //---------------------------------------------------------------- + // annotation inquiry: has Popup + //---------------------------------------------------------------- + FITZEXCEPTION(has_popup, !result) + PARENTCHECK(has_popup, """Check if annotation has a Popup.""") + %pythoncode %{@property%} + PyObject * + has_popup() + { + int has_popup = 0; + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(Popup)); + if (obj) has_popup = 1; + } + fz_catch(gctx) { + return NULL; + } + return JM_BOOL(has_popup); + } + + + //---------------------------------------------------------------- + // annotation set Popup + //---------------------------------------------------------------- + FITZEXCEPTION(set_popup, !result) + PARENTCHECK(set_popup, """Create annotation 'Popup' or update rectangle.""") + PyObject * + set_popup(PyObject *rect) + { + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_page *pdfpage = pdf_annot_page(gctx, annot); + fz_matrix rot = JM_rotate_page_matrix(gctx, pdfpage); + fz_rect r = fz_transform_rect(JM_rect_from_py(rect), rot); + pdf_set_annot_popup(gctx, annot, r); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------------------------------------- + // annotation Popup rectangle + //---------------------------------------------------------------- + FITZEXCEPTION(popup_rect, !result) + PARENTCHECK(popup_rect, """annotation 'Popup' rectangle""") + %pythoncode %{@property%} + %pythonappend popup_rect %{ + val = Rect(val) * self.parent.transformation_matrix + val *= self.parent.derotation_matrix%} + PyObject * + popup_rect() + { + fz_rect rect = fz_infinite_rect; + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(Popup)); + if (obj) { + rect = pdf_dict_get_rect(gctx, obj, PDF_NAME(Rect)); + } + } + fz_catch(gctx) { + return NULL; + } + return JM_py_from_rect(rect); + } + + + //---------------------------------------------------------------- + // annotation Popup xref + //---------------------------------------------------------------- + FITZEXCEPTION(popup_xref, !result) + PARENTCHECK(popup_xref, """annotation 'Popup' xref""") + %pythoncode %{@property%} + PyObject * + popup_xref() + { + int xref = 0; + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(Popup)); + if (obj) { + xref = pdf_to_num(gctx, obj); + } + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", xref); + } + + + //---------------------------------------------------------------- + // annotation set optional content + //---------------------------------------------------------------- + FITZEXCEPTION(set_oc, !result) + PARENTCHECK(set_oc, """Set / remove annotation OC xref.""") + PyObject * + set_oc(int oc=0) + { + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + if (!oc) { + pdf_dict_del(gctx, annot_obj, PDF_NAME(OC)); + } else { + JM_add_oc_object(gctx, pdf_get_bound_document(gctx, annot_obj), annot_obj, oc); + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + %pythoncode%{@property%} + %pythonprepend language %{"""annotation language"""%} + PyObject *language() + { + pdf_annot *this_annot = (pdf_annot *) $self; + fz_text_language lang = pdf_annot_language(gctx, this_annot); + char buf[8]; + if (lang == FZ_LANG_UNSET) Py_RETURN_NONE; + return Py_BuildValue("s", fz_string_from_text_language(buf, lang)); + } + + //---------------------------------------------------------------- + // annotation set language (/Lang) + //---------------------------------------------------------------- + FITZEXCEPTION(set_language, !result) + PARENTCHECK(set_language, """Set annotation language.""") + PyObject *set_language(char *language=NULL) + { + pdf_annot *this_annot = (pdf_annot *) $self; + fz_try(gctx) { + fz_text_language lang; + if (!language) + lang = FZ_LANG_UNSET; + else + lang = fz_text_language_from_string(language); + pdf_set_annot_language(gctx, this_annot, lang); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation get decompressed appearance stream source + //---------------------------------------------------------------- + FITZEXCEPTION(_getAP, !result) + PyObject * + _getAP() + { + PyObject *r = NULL; + fz_buffer *res = NULL; + fz_var(res); + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP), + PDF_NAME(N), NULL); + + if (pdf_is_stream(gctx, ap)) res = pdf_load_stream(gctx, ap); + if (res) { + r = JM_BinFromBuffer(gctx, res); + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + Py_RETURN_NONE; + } + if (!r) Py_RETURN_NONE; + return r; + } + + //---------------------------------------------------------------- + // annotation update /AP stream + //---------------------------------------------------------------- + FITZEXCEPTION(_setAP, !result) + PyObject * + _setAP(PyObject *buffer, int rect=0) + { + fz_buffer *res = NULL; + fz_var(res); + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_page *page = pdf_annot_page(gctx, annot); + pdf_obj *apobj = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP), + PDF_NAME(N), NULL); + if (!apobj) { + RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError); + } + if (!pdf_is_stream(gctx, apobj)) { + RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError); + } + res = JM_BufferFromBytes(gctx, buffer); + if (!res) { + RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_ValueError); + } + JM_update_stream(gctx, page->doc, apobj, res, 1); + if (rect) { + fz_rect bbox = pdf_dict_get_rect(gctx, annot_obj, PDF_NAME(Rect)); + pdf_dict_put_rect(gctx, apobj, PDF_NAME(BBox), bbox); + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // redaction annotation get values + //---------------------------------------------------------------- + FITZEXCEPTION(_get_redact_values, !result) + %pythonappend _get_redact_values %{ + if not val: + return val + val["rect"] = self.rect + text_color, fontname, fontsize = TOOLS._parse_da(self) + val["text_color"] = text_color + val["fontname"] = fontname + val["fontsize"] = fontsize + fill = self.colors["fill"] + val["fill"] = fill + + %} + PyObject * + _get_redact_values() + { + pdf_annot *annot = (pdf_annot *) $self; + if (pdf_annot_type(gctx, annot) != PDF_ANNOT_REDACT) + Py_RETURN_NONE; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + PyObject *values = PyDict_New(); + pdf_obj *obj = NULL; + const char *text = NULL; + fz_try(gctx) { + obj = pdf_dict_gets(gctx, annot_obj, "RO"); + if (obj) { + JM_Warning("Ignoring redaction key '/RO'."); + int xref = pdf_to_num(gctx, obj); + DICT_SETITEM_DROP(values, dictkey_xref, Py_BuildValue("i", xref)); + } + obj = pdf_dict_gets(gctx, annot_obj, "OverlayText"); + if (obj) { + text = pdf_to_text_string(gctx, obj); + DICT_SETITEM_DROP(values, dictkey_text, JM_UnicodeFromStr(text)); + } else { + DICT_SETITEM_DROP(values, dictkey_text, Py_BuildValue("s", "")); + } + obj = pdf_dict_get(gctx, annot_obj, PDF_NAME(Q)); + int align = 0; + if (obj) { + align = pdf_to_int(gctx, obj); + } + DICT_SETITEM_DROP(values, dictkey_align, Py_BuildValue("i", align)); + } + fz_catch(gctx) { + Py_DECREF(values); + return NULL; + } + return values; + } + + //---------------------------------------------------------------- + // annotation get TextPage + //---------------------------------------------------------------- + %pythonappend get_textpage %{ + if val: + val.thisown = True + %} + FITZEXCEPTION(get_textpage, !result) + PARENTCHECK(get_textpage, """Make annotation TextPage.""") + struct TextPage * + get_textpage(PyObject *clip=NULL, int flags = 0) + { + fz_stext_page *textpage=NULL; + fz_stext_options options = { 0 }; + options.flags = flags; + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + textpage = pdf_new_stext_page_from_annot(gctx, annot, &options); + } + fz_catch(gctx) { + return NULL; + } + return (struct TextPage *) textpage; + } + + + //---------------------------------------------------------------- + // annotation set name + //---------------------------------------------------------------- + FITZEXCEPTION(set_name, !result) + PARENTCHECK(set_name, """Set /Name (icon) of annotation.""") + PyObject * + set_name(char *name) + { + fz_try(gctx) { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(Name), name); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation set rectangle + //---------------------------------------------------------------- + PARENTCHECK(set_rect, """Set annotation rectangle.""") + FITZEXCEPTION(set_rect, !result) + PyObject * + set_rect(PyObject *rect) + { + pdf_annot *annot = (pdf_annot *) $self; + int type = pdf_annot_type(gctx, annot); + if (type == PDF_ANNOT_LINE || type == PDF_ANNOT_POLY_LINE || + type == PDF_ANNOT_POLYGON) { + fz_warn(gctx, "setting rectangle ignored for annot type %s", pdf_string_from_annot_type(gctx, type)); + Py_RETURN_NONE; + } + fz_try(gctx) { + pdf_page *pdfpage = pdf_annot_page(gctx, annot); + fz_matrix rot = JM_rotate_page_matrix(gctx, pdfpage); + fz_rect r = fz_transform_rect(JM_rect_from_py(rect), rot); + if (fz_is_empty_rect(r) || fz_is_infinite_rect(r)) { + RAISEPY(gctx, MSG_BAD_RECT, PyExc_ValueError); + } + pdf_set_annot_rect(gctx, annot, r); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation set rotation + //---------------------------------------------------------------- + PARENTCHECK(set_rotation, """Set annotation rotation.""") + PyObject * + set_rotation(int rotate=0) + { + pdf_annot *annot = (pdf_annot *) $self; + int type = pdf_annot_type(gctx, annot); + switch (type) + { + case PDF_ANNOT_CARET: break; + case PDF_ANNOT_CIRCLE: break; + case PDF_ANNOT_FREE_TEXT: break; + case PDF_ANNOT_FILE_ATTACHMENT: break; + case PDF_ANNOT_INK: break; + case PDF_ANNOT_LINE: break; + case PDF_ANNOT_POLY_LINE: break; + case PDF_ANNOT_POLYGON: break; + case PDF_ANNOT_SQUARE: break; + case PDF_ANNOT_STAMP: break; + case PDF_ANNOT_TEXT: break; + default: Py_RETURN_NONE; + } + int rot = rotate; + while (rot < 0) rot += 360; + while (rot >= 360) rot -= 360; + if (type == PDF_ANNOT_FREE_TEXT && rot % 90 != 0) + rot = 0; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Rotate), rot); + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation get rotation + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(rotation, """annotation rotation""") + int rotation() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_obj *rotation = pdf_dict_get(gctx, annot_obj, PDF_NAME(Rotate)); + if (!rotation) return -1; + return pdf_to_int(gctx, rotation); + } + + + //---------------------------------------------------------------- + // annotation vertices (for "Line", "Polgon", "Ink", etc. + //---------------------------------------------------------------- + PARENTCHECK(vertices, """annotation vertex points""") + %pythoncode %{@property%} + PyObject *vertices() + { + PyObject *res = NULL, *res1 = NULL; + pdf_obj *o, *o1; + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_page *page = pdf_annot_page(gctx, annot); + int i, j; + fz_point point; // point object to work with + fz_matrix page_ctm; // page transformation matrix + pdf_page_transform(gctx, page, NULL, &page_ctm); + fz_matrix derot = JM_derotate_page_matrix(gctx, page); + page_ctm = fz_concat(page_ctm, derot); + + //---------------------------------------------------------------- + // The following objects occur in different annotation types. + // So we are sure that (!o) occurs at most once. + // Every pair of floats is one point, that needs to be separately + // transformed with the page transformation matrix. + //---------------------------------------------------------------- + o = pdf_dict_get(gctx, annot_obj, PDF_NAME(Vertices)); + if (o) goto weiter; + o = pdf_dict_get(gctx, annot_obj, PDF_NAME(L)); + if (o) goto weiter; + o = pdf_dict_get(gctx, annot_obj, PDF_NAME(QuadPoints)); + if (o) goto weiter; + o = pdf_dict_gets(gctx, annot_obj, "CL"); + if (o) goto weiter; + o = pdf_dict_get(gctx, annot_obj, PDF_NAME(InkList)); + if (o) goto inklist; + Py_RETURN_NONE; + + // handle lists with 1-level depth -------------------------------- + weiter:; + res = PyList_New(0); // create Python list + for (i = 0; i < pdf_array_len(gctx, o); i += 2) + { + point.x = pdf_to_real(gctx, pdf_array_get(gctx, o, i)); + point.y = pdf_to_real(gctx, pdf_array_get(gctx, o, i+1)); + point = fz_transform_point(point, page_ctm); + LIST_APPEND_DROP(res, Py_BuildValue("ff", point.x, point.y)); + } + return res; + + // InkList has 2-level lists -------------------------------------- + inklist:; + res = PyList_New(0); + for (i = 0; i < pdf_array_len(gctx, o); i++) + { + res1 = PyList_New(0); + o1 = pdf_array_get(gctx, o, i); + for (j = 0; j < pdf_array_len(gctx, o1); j += 2) + { + point.x = pdf_to_real(gctx, pdf_array_get(gctx, o1, j)); + point.y = pdf_to_real(gctx, pdf_array_get(gctx, o1, j+1)); + point = fz_transform_point(point, page_ctm); + LIST_APPEND_DROP(res1, Py_BuildValue("ff", point.x, point.y)); + } + LIST_APPEND_DROP(res, res1); + } + return res; + } + + //---------------------------------------------------------------- + // annotation colors + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(colors, """Color definitions.""") + PyObject *colors() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + return JM_annot_colors(gctx, annot_obj); + } + + //---------------------------------------------------------------- + // annotation update appearance + //---------------------------------------------------------------- + PyObject *_update_appearance(float opacity=-1, + char *blend_mode=NULL, + PyObject *fill_color=NULL, + int rotate = -1) + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_page *page = pdf_annot_page(gctx, annot); + pdf_document *pdf = page->doc; + int type = pdf_annot_type(gctx, annot); + float fcol[4] = {1,1,1,1}; // std fill color: white + int i, nfcol = 0; // number of color components + JM_color_FromSequence(fill_color, &nfcol, fcol); + fz_try(gctx) { + // remove fill color from unsupported annots + // or if so requested + if ((type != PDF_ANNOT_SQUARE + && type != PDF_ANNOT_CIRCLE + && type != PDF_ANNOT_LINE + && type != PDF_ANNOT_POLY_LINE + && type != PDF_ANNOT_POLYGON + ) + || nfcol == 0 + ) { + pdf_dict_del(gctx, annot_obj, PDF_NAME(IC)); + } else if (nfcol > 0) { + pdf_set_annot_interior_color(gctx, annot, nfcol, fcol); + } + + int insert_rot = (rotate >= 0) ? 1 : 0; + switch (type) { + case PDF_ANNOT_CARET: + case PDF_ANNOT_CIRCLE: + case PDF_ANNOT_FREE_TEXT: + case PDF_ANNOT_FILE_ATTACHMENT: + case PDF_ANNOT_INK: + case PDF_ANNOT_LINE: + case PDF_ANNOT_POLY_LINE: + case PDF_ANNOT_POLYGON: + case PDF_ANNOT_SQUARE: + case PDF_ANNOT_STAMP: + case PDF_ANNOT_TEXT: break; + default: insert_rot = 0; + } + + if (insert_rot) { + pdf_dict_put_int(gctx, annot_obj, PDF_NAME(Rotate), rotate); + } + + pdf_dirty_annot(gctx, annot); + pdf_update_annot(gctx, annot); // let MuPDF update + pdf->resynth_required = 0; + // insert fill color + if (type == PDF_ANNOT_FREE_TEXT) { + if (nfcol > 0) { + pdf_set_annot_color(gctx, annot, nfcol, fcol); + } + } else if (nfcol > 0) { + pdf_obj *col = pdf_new_array(gctx, page->doc, nfcol); + for (i = 0; i < nfcol; i++) { + pdf_array_push_real(gctx, col, fcol[i]); + } + pdf_dict_put_drop(gctx,annot_obj, PDF_NAME(IC), col); + } + } + fz_catch(gctx) { + PySys_WriteStderr("cannot update annot: '%s'\n", fz_caught_message(gctx)); + Py_RETURN_FALSE; + } + + if ((opacity < 0 || opacity >= 1) && !blend_mode) // no opacity, no blend_mode + goto normal_exit; + + fz_try(gctx) { // create or update /ExtGState + pdf_obj *ap = pdf_dict_getl(gctx, annot_obj, PDF_NAME(AP), + PDF_NAME(N), NULL); + if (!ap) { // should never happen + RAISEPY(gctx, MSG_BAD_APN, PyExc_RuntimeError); + } + + pdf_obj *resources = pdf_dict_get(gctx, ap, PDF_NAME(Resources)); + if (!resources) { // no Resources yet: make one + resources = pdf_dict_put_dict(gctx, ap, PDF_NAME(Resources), 2); + } + pdf_obj *alp0 = pdf_new_dict(gctx, page->doc, 3); + if (opacity >= 0 && opacity < 1) { + pdf_dict_put_real(gctx, alp0, PDF_NAME(CA), (double) opacity); + pdf_dict_put_real(gctx, alp0, PDF_NAME(ca), (double) opacity); + pdf_dict_put_real(gctx, annot_obj, PDF_NAME(CA), (double) opacity); + } + if (blend_mode) { + pdf_dict_put_name(gctx, alp0, PDF_NAME(BM), blend_mode); + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(BM), blend_mode); + } + pdf_obj *extg = pdf_dict_get(gctx, resources, PDF_NAME(ExtGState)); + if (!extg) { // no ExtGState yet: make one + extg = pdf_dict_put_dict(gctx, resources, PDF_NAME(ExtGState), 2); + } + pdf_dict_put_drop(gctx, extg, PDF_NAME(H), alp0); + } + + fz_catch(gctx) { + PySys_WriteStderr("cannot set opacity or blend mode\n"); + Py_RETURN_FALSE; + } + normal_exit:; + Py_RETURN_TRUE; + } + + + %pythoncode %{ + def update(self, + blend_mode: OptStr =None, + opacity: OptFloat =None, + fontsize: float =0, + fontname: OptStr =None, + text_color: OptSeq =None, + border_color: OptSeq =None, + fill_color: OptSeq =None, + cross_out: bool =True, + rotate: int =-1, + ): + + """Update annot appearance. + + Notes: + Depending on the annot type, some parameters make no sense, + while others are only available in this method to achieve the + desired result. This is especially true for 'FreeText' annots. + Args: + blend_mode: set the blend mode, all annotations. + opacity: set the opacity, all annotations. + fontsize: set fontsize, 'FreeText' only. + fontname: set the font, 'FreeText' only. + border_color: set border color, 'FreeText' only. + text_color: set text color, 'FreeText' only. + fill_color: set fill color, all annotations. + cross_out: draw diagonal lines, 'Redact' only. + rotate: set rotation, 'FreeText' and some others. + """ + CheckParent(self) + def color_string(cs, code): + """Return valid PDF color operator for a given color sequence. + """ + cc = ColorCode(cs, code) + if not cc: + return b"" + return (cc + "\n").encode() + + annot_type = self.type[0] # get the annot type + dt = self.border["dashes"] # get the dashes spec + bwidth = self.border["width"] # get border line width + stroke = self.colors["stroke"] # get the stroke color + if fill_color != None: # change of fill color requested + fill = fill_color + else: # put in current annot value + fill = self.colors["fill"] + + rect = None # self.rect # prevent MuPDF fiddling with it + apnmat = self.apn_matrix # prevent MuPDF fiddling with it + if rotate != -1: # sanitize rotation value + while rotate < 0: + rotate += 360 + while rotate >= 360: + rotate -= 360 + if annot_type == PDF_ANNOT_FREE_TEXT and rotate % 90 != 0: + rotate = 0 + + #------------------------------------------------------------------ + # handle opacity and blend mode + #------------------------------------------------------------------ + if blend_mode is None: + blend_mode = self.blendmode + if not hasattr(opacity, "__float__"): + opacity = self.opacity + + if 0 <= opacity < 1 or blend_mode is not None: + opa_code = "/H gs\n" # then we must reference this 'gs' + else: + opa_code = "" + + if annot_type == PDF_ANNOT_FREE_TEXT: + CheckColor(border_color) + CheckColor(text_color) + CheckColor(fill_color) + tcol, fname, fsize = TOOLS._parse_da(self) + + # read and update default appearance as necessary + update_default_appearance = False + if fsize <= 0: + fsize = 12 + update_default_appearance = True + if text_color is not None: + tcol = text_color + update_default_appearance = True + if fontname is not None: + fname = fontname + update_default_appearance = True + if fontsize > 0: + fsize = fontsize + update_default_appearance = True + + if update_default_appearance: + da_str = "" + if len(tcol) == 3: + fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf" + elif len(tcol) == 1: + fmt = "{:g} g /{f:s} {s:g} Tf" + elif len(tcol) == 4: + fmt = "{:g} {:g} {:g} {:g} k /{f:s} {s:g} Tf" + da_str = fmt.format(*tcol, f=fname, s=fsize) + TOOLS._update_da(self, da_str) + + #------------------------------------------------------------------ + # now invoke MuPDF to update the annot appearance + #------------------------------------------------------------------ + val = self._update_appearance( + opacity=opacity, + blend_mode=blend_mode, + fill_color=fill, + rotate=rotate, + ) + if val == False: + raise RuntimeError("Error updating annotation.") + + bfill = color_string(fill, "f") + bstroke = color_string(stroke, "c") + + p_ctm = self.parent.transformation_matrix + imat = ~p_ctm # inverse page transf. matrix + + if dt: + dashes = "[" + " ".join(map(str, dt)) + "] 0 d\n" + dashes = dashes.encode("utf-8") + else: + dashes = None + + if self.line_ends: + line_end_le, line_end_ri = self.line_ends + else: + line_end_le, line_end_ri = 0, 0 # init line end codes + + # read contents as created by MuPDF + ap = self._getAP() + ap_tab = ap.splitlines() # split in single lines + ap_updated = False # assume we did nothing + + if annot_type == PDF_ANNOT_REDACT: + if cross_out: # create crossed-out rect + ap_updated = True + ap_tab = ap_tab[:-1] + _, LL, LR, UR, UL = ap_tab + ap_tab.append(LR) + ap_tab.append(LL) + ap_tab.append(UR) + ap_tab.append(LL) + ap_tab.append(UL) + ap_tab.append(b"S") + + if bwidth > 0 or bstroke != b"": + ap_updated = True + ntab = [b"%g w" % bwidth] if bwidth > 0 else [] + for line in ap_tab: + if line.endswith(b"w"): + continue + if line.endswith(b"RG") and bstroke != b"": + line = bstroke[:-1] + ntab.append(line) + ap_tab = ntab + + ap = b"\n".join(ap_tab) + + if annot_type == PDF_ANNOT_FREE_TEXT: + BT = ap.find(b"BT") + ET = ap.find(b"ET") + 2 + ap = ap[BT:ET] + w, h = self.rect.width, self.rect.height + if rotate in (90, 270) or not (apnmat.b == apnmat.c == 0): + w, h = h, w + re = b"0 0 %g %g re" % (w, h) + ap = re + b"\nW\nn\n" + ap + ope = None + fill_string = color_string(fill, "f") + if fill_string: + ope = b"f" + stroke_string = color_string(border_color, "c") + if stroke_string and bwidth > 0: + ope = b"S" + bwidth = b"%g w\n" % bwidth + else: + bwidth = stroke_string = b"" + if fill_string and stroke_string: + ope = b"B" + if ope != None: + ap = bwidth + fill_string + stroke_string + re + b"\n" + ope + b"\n" + ap + + if dashes != None: # handle dashes + ap = dashes + b"\n" + ap + dashes = None + + ap_updated = True + + if annot_type in (PDF_ANNOT_POLYGON, PDF_ANNOT_POLY_LINE): + ap = b"\n".join(ap_tab[:-1]) + b"\n" + ap_updated = True + if bfill != b"": + if annot_type == PDF_ANNOT_POLYGON: + ap = ap + bfill + b"b" # close, fill, and stroke + elif annot_type == PDF_ANNOT_POLY_LINE: + ap = ap + b"S" # stroke + else: + if annot_type == PDF_ANNOT_POLYGON: + ap = ap + b"s" # close and stroke + elif annot_type == PDF_ANNOT_POLY_LINE: + ap = ap + b"S" # stroke + + if dashes is not None: # handle dashes + ap = dashes + ap + # reset dashing - only applies for LINE annots with line ends given + ap = ap.replace(b"\nS\n", b"\nS\n[] 0 d\n", 1) + ap_updated = True + + if opa_code: + ap = opa_code.encode("utf-8") + ap + ap_updated = True + + ap = b"q\n" + ap + b"\nQ\n" + #---------------------------------------------------------------------- + # the following handles line end symbols for 'Polygon' and 'Polyline' + #---------------------------------------------------------------------- + if line_end_le + line_end_ri > 0 and annot_type in (PDF_ANNOT_POLYGON, PDF_ANNOT_POLY_LINE): + + le_funcs = (None, TOOLS._le_square, TOOLS._le_circle, + TOOLS._le_diamond, TOOLS._le_openarrow, + TOOLS._le_closedarrow, TOOLS._le_butt, + TOOLS._le_ropenarrow, TOOLS._le_rclosedarrow, + TOOLS._le_slash) + le_funcs_range = range(1, len(le_funcs)) + d = 2 * max(1, self.border["width"]) + rect = self.rect + (-d, -d, d, d) + ap_updated = True + points = self.vertices + if line_end_le in le_funcs_range: + p1 = Point(points[0]) * imat + p2 = Point(points[1]) * imat + left = le_funcs[line_end_le](self, p1, p2, False, fill_color) + ap += left.encode() + if line_end_ri in le_funcs_range: + p1 = Point(points[-2]) * imat + p2 = Point(points[-1]) * imat + left = le_funcs[line_end_ri](self, p1, p2, True, fill_color) + ap += left.encode() + + if ap_updated: + if rect: # rect modified here? + self.set_rect(rect) + self._setAP(ap, rect=1) + else: + self._setAP(ap, rect=0) + + #------------------------------- + # handle annotation rotations + #------------------------------- + if annot_type not in ( # only these types are supported + PDF_ANNOT_CARET, + PDF_ANNOT_CIRCLE, + PDF_ANNOT_FILE_ATTACHMENT, + PDF_ANNOT_INK, + PDF_ANNOT_LINE, + PDF_ANNOT_POLY_LINE, + PDF_ANNOT_POLYGON, + PDF_ANNOT_SQUARE, + PDF_ANNOT_STAMP, + PDF_ANNOT_TEXT, + ): + return + + rot = self.rotation # get value from annot object + if rot == -1: # nothing to change + return + + M = (self.rect.tl + self.rect.br) / 2 # center of annot rect + + if rot == 0: # undo rotations + if abs(apnmat - Matrix(1, 1)) < 1e-5: + return # matrix already is a no-op + quad = self.rect.morph(M, ~apnmat) # derotate rect + self.set_rect(quad.rect) + self.set_apn_matrix(Matrix(1, 1)) # appearance matrix = no-op + return + + mat = Matrix(rot) + quad = self.rect.morph(M, mat) + self.set_rect(quad.rect) + self.set_apn_matrix(apnmat * mat) + %} + + //---------------------------------------------------------------- + // annotation set colors + //---------------------------------------------------------------- + %pythoncode %{ + def set_colors(self, colors=None, stroke=None, fill=None): + """Set 'stroke' and 'fill' colors. + + Use either a dict or the direct arguments. + """ + CheckParent(self) + doc = self.parent.parent + if type(colors) is not dict: + colors = {"fill": fill, "stroke": stroke} + fill = colors.get("fill") + stroke = colors.get("stroke") + fill_annots = (PDF_ANNOT_CIRCLE, PDF_ANNOT_SQUARE, PDF_ANNOT_LINE, PDF_ANNOT_POLY_LINE, PDF_ANNOT_POLYGON, + PDF_ANNOT_REDACT,) + if stroke in ([], ()): + doc.xref_set_key(self.xref, "C", "[]") + elif stroke is not None: + if hasattr(stroke, "__float__"): + stroke = [float(stroke)] + CheckColor(stroke) + if len(stroke) == 1: + s = "[%g]" % stroke[0] + elif len(stroke) == 3: + s = "[%g %g %g]" % tuple(stroke) + else: + s = "[%g %g %g %g]" % tuple(stroke) + doc.xref_set_key(self.xref, "C", s) + + if fill and self.type[0] not in fill_annots: + print("Warning: fill color ignored for annot type '%s'." % self.type[1]) + return + if fill in ([], ()): + doc.xref_set_key(self.xref, "IC", "[]") + elif fill is not None: + if hasattr(fill, "__float__"): + fill = [float(fill)] + CheckColor(fill) + if len(fill) == 1: + s = "[%g]" % fill[0] + elif len(fill) == 3: + s = "[%g %g %g]" % tuple(fill) + else: + s = "[%g %g %g %g]" % tuple(fill) + doc.xref_set_key(self.xref, "IC", s) + %} + + + //---------------------------------------------------------------- + // annotation line_ends + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(line_ends, """Line end codes.""") + PyObject * + line_ends() + { + pdf_annot *annot = (pdf_annot *) $self; + + // return nothing for invalid annot types + if (!pdf_annot_has_line_ending_styles(gctx, annot)) + Py_RETURN_NONE; + + int lstart = (int) pdf_annot_line_start_style(gctx, annot); + int lend = (int) pdf_annot_line_end_style(gctx, annot); + return Py_BuildValue("ii", lstart, lend); + } + + + //---------------------------------------------------------------- + // annotation set line ends + //---------------------------------------------------------------- + PARENTCHECK(set_line_ends, """Set line end codes.""") + void set_line_ends(int start, int end) + { + pdf_annot *annot = (pdf_annot *) $self; + if (pdf_annot_has_line_ending_styles(gctx, annot)) + pdf_set_annot_line_ending_styles(gctx, annot, start, end); + else + JM_Warning("bad annot type for line ends"); + } + + + //---------------------------------------------------------------- + // annotation type + //---------------------------------------------------------------- + PARENTCHECK(type, """annotation type""") + %pythoncode %{@property%} + PyObject *type() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + int type = pdf_annot_type(gctx, annot); + const char *c = pdf_string_from_annot_type(gctx, type); + pdf_obj *o = pdf_dict_gets(gctx, annot_obj, "IT"); + if (!o || !pdf_is_name(gctx, o)) + return Py_BuildValue("is", type, c); // no IT entry + const char *it = pdf_to_name(gctx, o); + return Py_BuildValue("iss", type, c, it); + } + + //---------------------------------------------------------------- + // annotation opacity + //---------------------------------------------------------------- + PARENTCHECK(opacity, """Opacity.""") + %pythoncode %{@property%} + PyObject *opacity() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + double opy = -1; + pdf_obj *ca = pdf_dict_get(gctx, annot_obj, PDF_NAME(CA)); + if (pdf_is_number(gctx, ca)) + opy = pdf_to_real(gctx, ca); + return Py_BuildValue("f", opy); + } + + //---------------------------------------------------------------- + // annotation set opacity + //---------------------------------------------------------------- + PARENTCHECK(set_opacity, """Set opacity.""") + void set_opacity(float opacity) + { + pdf_annot *annot = (pdf_annot *) $self; + if (!INRANGE(opacity, 0.0f, 1.0f)) + { + pdf_set_annot_opacity(gctx, annot, 1); + return; + } + pdf_set_annot_opacity(gctx, annot, opacity); + if (opacity < 1.0f) + { + pdf_page *page = pdf_annot_page(gctx, annot); + page->transparency = 1; + } + } + + + //---------------------------------------------------------------- + // annotation get attached file info + //---------------------------------------------------------------- + %pythoncode %{@property%} + FITZEXCEPTION(file_info, !result) + PARENTCHECK(file_info, """Attached file information.""") + PyObject *file_info() + { + PyObject *res = PyDict_New(); // create Python dict + char *filename = NULL; + char *desc = NULL; + int length = -1, size = -1; + pdf_obj *stream = NULL, *o = NULL, *fs = NULL; + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + fz_try(gctx) { + int type = (int) pdf_annot_type(gctx, annot); + if (type != PDF_ANNOT_FILE_ATTACHMENT) { + RAISEPY(gctx, MSG_BAD_ANNOT_TYPE, PyExc_TypeError); + } + stream = pdf_dict_getl(gctx, annot_obj, PDF_NAME(FS), + PDF_NAME(EF), PDF_NAME(F), NULL); + if (!stream) { + RAISEPY(gctx, "bad PDF: file entry not found", JM_Exc_FileDataError); + } + } + fz_catch(gctx) { + return NULL; + } + + fs = pdf_dict_get(gctx, annot_obj, PDF_NAME(FS)); + + o = pdf_dict_get(gctx, fs, PDF_NAME(UF)); + if (o) { + filename = (char *) pdf_to_text_string(gctx, o); + } else { + o = pdf_dict_get(gctx, fs, PDF_NAME(F)); + if (o) filename = (char *) pdf_to_text_string(gctx, o); + } + + o = pdf_dict_get(gctx, fs, PDF_NAME(Desc)); + if (o) desc = (char *) pdf_to_text_string(gctx, o); + + o = pdf_dict_get(gctx, stream, PDF_NAME(Length)); + if (o) length = pdf_to_int(gctx, o); + + o = pdf_dict_getl(gctx, stream, PDF_NAME(Params), + PDF_NAME(Size), NULL); + if (o) size = pdf_to_int(gctx, o); + + DICT_SETITEM_DROP(res, dictkey_filename, JM_EscapeStrFromStr(filename)); + DICT_SETITEM_DROP(res, dictkey_desc, JM_UnicodeFromStr(desc)); + DICT_SETITEM_DROP(res, dictkey_length, Py_BuildValue("i", length)); + DICT_SETITEM_DROP(res, dictkey_size, Py_BuildValue("i", size)); + return res; + } + + + //---------------------------------------------------------------- + // annotation get attached file content + //---------------------------------------------------------------- + FITZEXCEPTION(get_file, !result) + PARENTCHECK(get_file, """Retrieve attached file content.""") + PyObject * + get_file() + { + PyObject *res = NULL; + pdf_obj *stream = NULL; + fz_buffer *buf = NULL; + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + fz_var(buf); + fz_try(gctx) { + int type = (int) pdf_annot_type(gctx, annot); + if (type != PDF_ANNOT_FILE_ATTACHMENT) { + RAISEPY(gctx, MSG_BAD_ANNOT_TYPE, PyExc_TypeError); + } + stream = pdf_dict_getl(gctx, annot_obj, PDF_NAME(FS), + PDF_NAME(EF), PDF_NAME(F), NULL); + if (!stream) { + RAISEPY(gctx, "bad PDF: file entry not found", JM_Exc_FileDataError); + } + buf = pdf_load_stream(gctx, stream); + res = JM_BinFromBuffer(gctx, buf); + } + fz_always(gctx) { + fz_drop_buffer(gctx, buf); + } + fz_catch(gctx) { + return NULL; + } + return res; + } + + + //---------------------------------------------------------------- + // annotation get attached sound stream + //---------------------------------------------------------------- + FITZEXCEPTION(get_sound, !result) + PARENTCHECK(get_sound, """Retrieve sound stream.""") + PyObject * + get_sound() + { + PyObject *res = NULL; + PyObject *stream = NULL; + fz_buffer *buf = NULL; + pdf_obj *obj = NULL; + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + fz_var(buf); + fz_try(gctx) { + int type = (int) pdf_annot_type(gctx, annot); + pdf_obj *sound = pdf_dict_get(gctx, annot_obj, PDF_NAME(Sound)); + if (type != PDF_ANNOT_SOUND || !sound) { + RAISEPY(gctx, MSG_BAD_ANNOT_TYPE, PyExc_TypeError); + } + if (pdf_dict_get(gctx, sound, PDF_NAME(F))) { + RAISEPY(gctx, "unsupported sound stream", JM_Exc_FileDataError); + } + res = PyDict_New(); + obj = pdf_dict_get(gctx, sound, PDF_NAME(R)); + if (obj) { + DICT_SETITEMSTR_DROP(res, "rate", + Py_BuildValue("f", pdf_to_real(gctx, obj))); + } + obj = pdf_dict_get(gctx, sound, PDF_NAME(C)); + if (obj) { + DICT_SETITEMSTR_DROP(res, "channels", + Py_BuildValue("i", pdf_to_int(gctx, obj))); + } + obj = pdf_dict_get(gctx, sound, PDF_NAME(B)); + if (obj) { + DICT_SETITEMSTR_DROP(res, "bps", + Py_BuildValue("i", pdf_to_int(gctx, obj))); + } + obj = pdf_dict_get(gctx, sound, PDF_NAME(E)); + if (obj) { + DICT_SETITEMSTR_DROP(res, "encoding", + Py_BuildValue("s", pdf_to_name(gctx, obj))); + } + obj = pdf_dict_gets(gctx, sound, "CO"); + if (obj) { + DICT_SETITEMSTR_DROP(res, "compression", + Py_BuildValue("s", pdf_to_name(gctx, obj))); + } + buf = pdf_load_stream(gctx, sound); + stream = JM_BinFromBuffer(gctx, buf); + DICT_SETITEMSTR_DROP(res, "stream", stream); + } + fz_always(gctx) { + fz_drop_buffer(gctx, buf); + } + fz_catch(gctx) { + Py_CLEAR(res); + return NULL; + } + return res; + } + + + //---------------------------------------------------------------- + // annotation update attached file + //---------------------------------------------------------------- + FITZEXCEPTION(update_file, !result) + %pythonprepend update_file +%{"""Update attached file.""" +CheckParent(self)%} + + PyObject * + update_file(PyObject *buffer=NULL, char *filename=NULL, char *ufilename=NULL, char *desc=NULL) + { + pdf_document *pdf = NULL; // to be filled in + fz_buffer *res = NULL; // for compressed content + pdf_obj *stream = NULL, *fs = NULL; + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + fz_try(gctx) { + pdf = pdf_get_bound_document(gctx, annot_obj); // the owning PDF + int type = (int) pdf_annot_type(gctx, annot); + if (type != PDF_ANNOT_FILE_ATTACHMENT) { + RAISEPY(gctx, MSG_BAD_ANNOT_TYPE, PyExc_TypeError); + } + stream = pdf_dict_getl(gctx, annot_obj, PDF_NAME(FS), + PDF_NAME(EF), PDF_NAME(F), NULL); + // the object for file content + if (!stream) { + RAISEPY(gctx, "bad PDF: no /EF object", JM_Exc_FileDataError); + } + + fs = pdf_dict_get(gctx, annot_obj, PDF_NAME(FS)); + + // file content given + res = JM_BufferFromBytes(gctx, buffer); + if (buffer && !res) { + RAISEPY(gctx, MSG_BAD_BUFFER, PyExc_ValueError); + } + if (res) { + JM_update_stream(gctx, pdf, stream, res, 1); + // adjust /DL and /Size parameters + int64_t len = (int64_t) fz_buffer_storage(gctx, res, NULL); + pdf_obj *l = pdf_new_int(gctx, len); + pdf_dict_put(gctx, stream, PDF_NAME(DL), l); + pdf_dict_putl(gctx, stream, l, PDF_NAME(Params), PDF_NAME(Size), NULL); + } + + if (filename) { + pdf_dict_put_text_string(gctx, stream, PDF_NAME(F), filename); + pdf_dict_put_text_string(gctx, fs, PDF_NAME(F), filename); + pdf_dict_put_text_string(gctx, stream, PDF_NAME(UF), filename); + pdf_dict_put_text_string(gctx, fs, PDF_NAME(UF), filename); + pdf_dict_put_text_string(gctx, annot_obj, PDF_NAME(Contents), filename); + } + + if (ufilename) { + pdf_dict_put_text_string(gctx, stream, PDF_NAME(UF), ufilename); + pdf_dict_put_text_string(gctx, fs, PDF_NAME(UF), ufilename); + } + + if (desc) { + pdf_dict_put_text_string(gctx, stream, PDF_NAME(Desc), desc); + pdf_dict_put_text_string(gctx, fs, PDF_NAME(Desc), desc); + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation info + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(info, """Various information details.""") + PyObject *info() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + PyObject *res = PyDict_New(); + pdf_obj *o; + + DICT_SETITEM_DROP(res, dictkey_content, + JM_UnicodeFromStr(pdf_annot_contents(gctx, annot))); + + o = pdf_dict_get(gctx, annot_obj, PDF_NAME(Name)); + DICT_SETITEM_DROP(res, dictkey_name, JM_UnicodeFromStr(pdf_to_name(gctx, o))); + + // Title (= author) + o = pdf_dict_get(gctx, annot_obj, PDF_NAME(T)); + DICT_SETITEM_DROP(res, dictkey_title, JM_UnicodeFromStr(pdf_to_text_string(gctx, o))); + + // CreationDate + o = pdf_dict_gets(gctx, annot_obj, "CreationDate"); + DICT_SETITEM_DROP(res, dictkey_creationDate, + JM_UnicodeFromStr(pdf_to_text_string(gctx, o))); + + // ModDate + o = pdf_dict_get(gctx, annot_obj, PDF_NAME(M)); + DICT_SETITEM_DROP(res, dictkey_modDate, JM_UnicodeFromStr(pdf_to_text_string(gctx, o))); + + // Subj + o = pdf_dict_gets(gctx, annot_obj, "Subj"); + DICT_SETITEM_DROP(res, dictkey_subject, + Py_BuildValue("s",pdf_to_text_string(gctx, o))); + + // Identification (PDF key /NM) + o = pdf_dict_gets(gctx, annot_obj, "NM"); + DICT_SETITEM_DROP(res, dictkey_id, + JM_UnicodeFromStr(pdf_to_text_string(gctx, o))); + + return res; + } + + //---------------------------------------------------------------- + // annotation set information + //---------------------------------------------------------------- + FITZEXCEPTION(set_info, !result) + %pythonprepend set_info %{ + """Set various properties.""" + CheckParent(self) + if type(info) is dict: # build the args from the dictionary + content = info.get("content", None) + title = info.get("title", None) + creationDate = info.get("creationDate", None) + modDate = info.get("modDate", None) + subject = info.get("subject", None) + info = None + %} + PyObject * + set_info(PyObject *info=NULL, char *content=NULL, char *title=NULL, + char *creationDate=NULL, char *modDate=NULL, char *subject=NULL) + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + // use this to indicate a 'markup' annot type + int is_markup = pdf_annot_has_author(gctx, annot); + fz_try(gctx) { + // contents + if (content) + pdf_set_annot_contents(gctx, annot, content); + + if (is_markup) { + // title (= author) + if (title) + pdf_set_annot_author(gctx, annot, title); + + // creation date + if (creationDate) + pdf_dict_put_text_string(gctx, annot_obj, + PDF_NAME(CreationDate), creationDate); + + // mod date + if (modDate) + pdf_dict_put_text_string(gctx, annot_obj, + PDF_NAME(M), modDate); + + // subject + if (subject) + pdf_dict_puts_drop(gctx, annot_obj, "Subj", + pdf_new_text_string(gctx, subject)); + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // annotation border + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(border, """Border information.""") + PyObject *border() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + return JM_annot_border(gctx, annot_obj); + } + + //---------------------------------------------------------------- + // set annotation border + //---------------------------------------------------------------- + %pythonprepend set_border %{ + """Set border properties. + + Either a dict, or direct arguments width, style, dashes or clouds.""" + + CheckParent(self) + if type(border) is not dict: + border = {"width": width, "style": style, "dashes": dashes, "clouds": clouds} + border.setdefault("width", -1) + border.setdefault("style", None) + border.setdefault("dashes", None) + border.setdefault("clouds", -1) + if border["width"] == None: + border["width"] = -1 + if border["clouds"] == None: + border["clouds"] = -1 + if hasattr(border["dashes"], "__getitem__"): # ensure sequence items are integers + border["dashes"] = tuple(border["dashes"]) + for item in border["dashes"]: + if not isinstance(item, int): + border["dashes"] = None + break + %} + PyObject * + set_border(PyObject *border=NULL, float width=-1, char *style=NULL, PyObject *dashes=NULL, int clouds=-1) + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_document *pdf = pdf_get_bound_document(gctx, annot_obj); + return JM_annot_set_border(gctx, border, pdf, annot_obj); + } + + + //---------------------------------------------------------------- + // annotation flags + //---------------------------------------------------------------- + %pythoncode %{@property%} + PARENTCHECK(flags, """Flags field.""") + int flags() + { + pdf_annot *annot = (pdf_annot *) $self; + return pdf_annot_flags(gctx, annot); + } + + //---------------------------------------------------------------- + // annotation clean contents + //---------------------------------------------------------------- + FITZEXCEPTION(clean_contents, !result) + PARENTCHECK(clean_contents, """Clean appearance contents stream.""") + PyObject *clean_contents(int sanitize=1) + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_document *pdf = pdf_get_bound_document(gctx, pdf_annot_obj(gctx, annot)); + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + pdf_filter_factory list[2] = { 0 }; + pdf_sanitize_filter_options sopts = { 0 }; + pdf_filter_options filter = { + 1, // recurse: true + 1, // instance forms + 0, // do not ascii-escape binary data + 1, // no_update + NULL, // end_page_opaque + NULL, // end page + list, // filters + }; + if (sanitize) { + list[0].filter = pdf_new_sanitize_filter; + list[0].options = &sopts; + } + #else + pdf_filter_options filter = { + NULL, // opaque + NULL, // image filter + NULL, // text filter + NULL, // after text + NULL, // end page + 1, // recurse: true + 1, // instance forms + 1, // sanitize, + 0 // do not ascii-escape binary data + }; + filter.sanitize = sanitize; + #endif + fz_try(gctx) { + pdf_filter_annot_contents(gctx, pdf, annot, &filter); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + //---------------------------------------------------------------- + // set annotation flags + //---------------------------------------------------------------- + PARENTCHECK(set_flags, """Set annotation flags.""") + void + set_flags(int flags) + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_set_annot_flags(gctx, annot, flags); + } + + + //---------------------------------------------------------------- + // annotation delete responses + //---------------------------------------------------------------- + FITZEXCEPTION(delete_responses, !result) + PARENTCHECK(delete_responses, """Delete 'Popup' and responding annotations.""") + PyObject * + delete_responses() + { + pdf_annot *annot = (pdf_annot *) $self; + pdf_obj *annot_obj = pdf_annot_obj(gctx, annot); + pdf_page *page = pdf_annot_page(gctx, annot); + pdf_annot *irt_annot = NULL; + fz_try(gctx) { + while (1) { + irt_annot = JM_find_annot_irt(gctx, annot); + if (!irt_annot) + break; + pdf_delete_annot(gctx, page, irt_annot); + } + pdf_dict_del(gctx, annot_obj, PDF_NAME(Popup)); + + pdf_obj *annots = pdf_dict_get(gctx, page->obj, PDF_NAME(Annots)); + int i, n = pdf_array_len(gctx, annots), found = 0; + for (i = n - 1; i >= 0; i--) { + pdf_obj *o = pdf_array_get(gctx, annots, i); + pdf_obj *p = pdf_dict_get(gctx, o, PDF_NAME(Parent)); + if (!p) + continue; + if (!pdf_objcmp(gctx, p, annot_obj)) { + pdf_array_delete(gctx, annots, i); + found = 1; + } + } + if (found > 0) { + pdf_dict_put(gctx, page->obj, PDF_NAME(Annots), annots); + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------------------------------------- + // next annotation + //---------------------------------------------------------------- + PARENTCHECK(next, """Next annotation.""") + %pythonappend next %{ + if not val: + return None + val.thisown = True + val.parent = self.parent # copy owning page object from previous annot + val.parent._annot_refs[id(val)] = val + + if val.type[0] == PDF_ANNOT_WIDGET: + widget = Widget() + TOOLS._fill_widget(val, widget) + val = widget + %} + %pythoncode %{@property%} + struct Annot *next() + { + pdf_annot *this_annot = (pdf_annot *) $self; + int type = pdf_annot_type(gctx, this_annot); + pdf_annot *annot; + + if (type != PDF_ANNOT_WIDGET) { + annot = pdf_next_annot(gctx, this_annot); + } else { + annot = pdf_next_widget(gctx, this_annot); + } + + if (annot) + pdf_keep_annot(gctx, annot); + return (struct Annot *) annot; + } + + + //---------------------------------------------------------------- + // annotation pixmap + //---------------------------------------------------------------- + FITZEXCEPTION(get_pixmap, !result) + %pythonprepend get_pixmap +%{"""annotation Pixmap""" + +CheckParent(self) +cspaces = {"gray": csGRAY, "rgb": csRGB, "cmyk": csCMYK} +if type(colorspace) is str: + colorspace = cspaces.get(colorspace.lower(), None) +if dpi: + matrix = Matrix(dpi / 72, dpi / 72) +%} + %pythonappend get_pixmap +%{ + val.thisown = True + if dpi: + val.set_dpi(dpi, dpi) +%} + struct Pixmap * + get_pixmap(PyObject *matrix = NULL, PyObject *dpi=NULL, struct Colorspace *colorspace = NULL, int alpha = 0) + { + fz_matrix ctm = JM_matrix_from_py(matrix); + fz_colorspace *cs = (fz_colorspace *) colorspace; + fz_pixmap *pix = NULL; + if (!cs) { + cs = fz_device_rgb(gctx); + } + + fz_try(gctx) { + pix = pdf_new_pixmap_from_annot(gctx, (pdf_annot *) $self, ctm, cs, NULL, alpha); + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) pix; + } + %pythoncode %{ + def _erase(self): + self.__swig_destroy__(self) + self.parent = None + + def __str__(self): + CheckParent(self) + return "'%s' annotation on %s" % (self.type[1], str(self.parent)) + + def __repr__(self): + CheckParent(self) + return "'%s' annotation on %s" % (self.type[1], str(self.parent)) + + def __del__(self): + if self.parent is None: + return + self._erase()%} + } +}; +%clearnodefaultctor; + +//------------------------------------------------------------------------ +// fz_link +//------------------------------------------------------------------------ +%nodefaultctor; +struct Link +{ + %immutable; + %extend { + ~Link() { + DEBUGMSG1("Link"); + fz_link *this_link = (fz_link *) $self; + fz_drop_link(gctx, this_link); + DEBUGMSG2; + } + + PyObject *_border(struct Document *doc, int xref) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) doc); + if (!pdf) Py_RETURN_NONE; + pdf_obj *link_obj = pdf_new_indirect(gctx, pdf, xref, 0); + if (!link_obj) Py_RETURN_NONE; + PyObject *b = JM_annot_border(gctx, link_obj); + pdf_drop_obj(gctx, link_obj); + return b; + } + + PyObject *_setBorder(PyObject *border, struct Document *doc, int xref) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) doc); + if (!pdf) Py_RETURN_NONE; + pdf_obj *link_obj = pdf_new_indirect(gctx, pdf, xref, 0); + if (!link_obj) Py_RETURN_NONE; + PyObject *b = JM_annot_set_border(gctx, border, pdf, link_obj); + pdf_drop_obj(gctx, link_obj); + return b; + } + + FITZEXCEPTION(_colors, !result) + PyObject *_colors(struct Document *doc, int xref) + { + pdf_document *pdf = pdf_specifics(gctx, (fz_document *) doc); + if (!pdf) Py_RETURN_NONE; + PyObject *b = NULL; + pdf_obj *link_obj; + fz_try(gctx) { + link_obj = pdf_new_indirect(gctx, pdf, xref, 0); + if (!link_obj) { + RAISEPY(gctx, MSG_BAD_XREF, PyExc_ValueError); + } + b = JM_annot_colors(gctx, link_obj); + } + fz_always(gctx) { + pdf_drop_obj(gctx, link_obj); + } + fz_catch(gctx) { + return NULL; + } + return b; + } + + + %pythoncode %{ + @property + def border(self): + return self._border(self.parent.parent.this, self.xref) + + @property + def flags(self)->int: + CheckParent(self) + doc = self.parent.parent + if not doc.is_pdf: + return 0 + f = doc.xref_get_key(self.xref, "F") + if f[1] != "null": + return int(f[1]) + return 0 + + def set_flags(self, flags): + CheckParent(self) + doc = self.parent.parent + if not doc.is_pdf: + raise ValueError("is no PDF") + if not type(flags) is int: + raise ValueError("bad 'flags' value") + doc.xref_set_key(self.xref, "F", str(flags)) + return None + + def set_border(self, border=None, width=0, dashes=None, style=None): + if type(border) is not dict: + border = {"width": width, "style": style, "dashes": dashes} + return self._setBorder(border, self.parent.parent.this, self.xref) + + @property + def colors(self): + return self._colors(self.parent.parent.this, self.xref) + + def set_colors(self, colors=None, stroke=None, fill=None): + """Set border colors.""" + CheckParent(self) + doc = self.parent.parent + if type(colors) is not dict: + colors = {"fill": fill, "stroke": stroke} + fill = colors.get("fill") + stroke = colors.get("stroke") + if fill is not None: + print("warning: links have no fill color") + if stroke in ([], ()): + doc.xref_set_key(self.xref, "C", "[]") + return + if hasattr(stroke, "__float__"): + stroke = [float(stroke)] + CheckColor(stroke) + if len(stroke) == 1: + s = "[%g]" % stroke[0] + elif len(stroke) == 3: + s = "[%g %g %g]" % tuple(stroke) + else: + s = "[%g %g %g %g]" % tuple(stroke) + doc.xref_set_key(self.xref, "C", s) + %} + %pythoncode %{@property%} + PARENTCHECK(uri, """Uri string.""") + PyObject *uri() + { + fz_link *this_link = (fz_link *) $self; + return JM_UnicodeFromStr(this_link->uri); + } + + %pythoncode %{@property%} + PARENTCHECK(is_external, """Flag the link as external.""") + PyObject *is_external() + { + fz_link *this_link = (fz_link *) $self; + if (!this_link->uri) Py_RETURN_FALSE; + return JM_BOOL(fz_is_external_link(gctx, this_link->uri)); + } + + %pythoncode + %{ + page = -1 + @property + def dest(self): + """Create link destination details.""" + if hasattr(self, "parent") and self.parent is None: + raise ValueError("orphaned object: parent is None") + if self.parent.parent.is_closed or self.parent.parent.is_encrypted: + raise ValueError("document closed or encrypted") + doc = self.parent.parent + + if self.is_external or self.uri.startswith("#"): + uri = None + else: + uri = doc.resolve_link(self.uri) + + return linkDest(self, uri) + %} + + PARENTCHECK(rect, """Rectangle ('hot area').""") + %pythoncode %{@property%} + %pythonappend rect %{val = Rect(val)%} + PyObject *rect() + { + fz_link *this_link = (fz_link *) $self; + return JM_py_from_rect(this_link->rect); + } + + //---------------------------------------------------------------- + // next link + //---------------------------------------------------------------- + // we need to increase the link refs number + // so that it will not be freed when the head is dropped + PARENTCHECK(next, """Next link.""") + %pythonappend next %{ + if val: + val.thisown = True + val.parent = self.parent # copy owning page from prev link + val.parent._annot_refs[id(val)] = val + if self.xref > 0: # prev link has an xref + link_xrefs = [x[0] for x in self.parent.annot_xrefs() if x[1] == PDF_ANNOT_LINK] + link_ids = [x[2] for x in self.parent.annot_xrefs() if x[1] == PDF_ANNOT_LINK] + idx = link_xrefs.index(self.xref) + val.xref = link_xrefs[idx + 1] + val.id = link_ids[idx + 1] + else: + val.xref = 0 + val.id = "" + %} + %pythoncode %{@property%} + struct Link *next() + { + fz_link *this_link = (fz_link *) $self; + fz_link *next_link = this_link->next; + if (!next_link) return NULL; + next_link = fz_keep_link(gctx, next_link); + return (struct Link *) next_link; + } + + %pythoncode %{ + def _erase(self): + self.__swig_destroy__(self) + self.parent = None + + def __str__(self): + CheckParent(self) + return "link on " + str(self.parent) + + def __repr__(self): + CheckParent(self) + return "link on " + str(self.parent) + + def __del__(self): + self._erase()%} + } +}; +%clearnodefaultctor; + +//------------------------------------------------------------------------ +// fz_display_list +//------------------------------------------------------------------------ +struct DisplayList { + %extend + { + ~DisplayList() { + DEBUGMSG1("DisplayList"); + fz_display_list *this_dl = (fz_display_list *) $self; + fz_drop_display_list(gctx, this_dl); + DEBUGMSG2; + } + FITZEXCEPTION(DisplayList, !result) + DisplayList(PyObject *mediabox) + { + fz_display_list *dl = NULL; + fz_try(gctx) { + dl = fz_new_display_list(gctx, JM_rect_from_py(mediabox)); + } + fz_catch(gctx) { + return NULL; + } + return (struct DisplayList *) dl; + } + + FITZEXCEPTION(run, !result) + PyObject *run(struct DeviceWrapper *dw, PyObject *m, PyObject *area) { + fz_try(gctx) { + fz_run_display_list(gctx, (fz_display_list *) $self, dw->device, + JM_matrix_from_py(m), JM_rect_from_py(area), NULL); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------------------------------------- + // DisplayList.rect + //---------------------------------------------------------------- + %pythoncode%{@property%} + %pythonappend rect %{val = Rect(val)%} + PyObject *rect() + { + return JM_py_from_rect(fz_bound_display_list(gctx, (fz_display_list *) $self)); + } + + //---------------------------------------------------------------- + // DisplayList.get_pixmap + //---------------------------------------------------------------- + FITZEXCEPTION(get_pixmap, !result) + %pythonappend get_pixmap %{val.thisown = True%} + struct Pixmap *get_pixmap(PyObject *matrix=NULL, + struct Colorspace *colorspace=NULL, + int alpha=0, + PyObject *clip=NULL) + { + fz_colorspace *cs = NULL; + fz_pixmap *pix = NULL; + + if (colorspace) cs = (fz_colorspace *) colorspace; + else cs = fz_device_rgb(gctx); + + fz_try(gctx) { + pix = JM_pixmap_from_display_list(gctx, + (fz_display_list *) $self, matrix, cs, + alpha, clip, NULL); + } + fz_catch(gctx) { + return NULL; + } + return (struct Pixmap *) pix; + } + + //---------------------------------------------------------------- + // DisplayList.get_textpage + //---------------------------------------------------------------- + FITZEXCEPTION(get_textpage, !result) + %pythonappend get_textpage %{val.thisown = True%} + struct TextPage *get_textpage(int flags = 3) + { + fz_display_list *this_dl = (fz_display_list *) $self; + fz_stext_page *tp = NULL; + fz_try(gctx) { + fz_stext_options stext_options = { 0 }; + stext_options.flags = flags; + tp = fz_new_stext_page_from_display_list(gctx, this_dl, &stext_options); + } + fz_catch(gctx) { + return NULL; + } + return (struct TextPage *) tp; + } + %pythoncode %{ + def __del__(self): + if not type(self) is DisplayList: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; + +//------------------------------------------------------------------------ +// fz_stext_page +//------------------------------------------------------------------------ +struct TextPage { + %extend { + ~TextPage() + { + DEBUGMSG1("TextPage"); + fz_stext_page *this_tp = (fz_stext_page *) $self; + fz_drop_stext_page(gctx, this_tp); + DEBUGMSG2; + } + + FITZEXCEPTION(TextPage, !result) + %pythonappend TextPage %{self.thisown=True%} + TextPage(PyObject *mediabox) + { + fz_stext_page *tp = NULL; + fz_try(gctx) { + tp = fz_new_stext_page(gctx, JM_rect_from_py(mediabox)); + } + fz_catch(gctx) { + return NULL; + } + return (struct TextPage *) tp; + } + + //---------------------------------------------------------------- + // method search() + //---------------------------------------------------------------- + FITZEXCEPTION(search, !result) + %pythonprepend search + %{"""Locate 'needle' returning rects or quads."""%} + %pythonappend search %{ + if not val: + return val + items = len(val) + for i in range(items): # change entries to quads or rects + q = Quad(val[i]) + if quads: + val[i] = q + else: + val[i] = q.rect + if quads: + return val + i = 0 # join overlapping rects on the same line + while i < items - 1: + v1 = val[i] + v2 = val[i + 1] + if v1.y1 != v2.y1 or (v1 & v2).is_empty: + i += 1 + continue # no overlap on same line + val[i] = v1 | v2 # join rectangles + del val[i + 1] # remove v2 + items -= 1 # reduce item count + %} + PyObject *search(const char *needle, int hit_max=0, int quads=1) + { + PyObject *liste = NULL; + fz_try(gctx) { + liste = JM_search_stext_page(gctx, (fz_stext_page *) $self, needle); + } + fz_catch(gctx) { + return NULL; + } + return liste; + } + + + //---------------------------------------------------------------- + // Get list of all blocks with block type and bbox as a Python list + //---------------------------------------------------------------- + FITZEXCEPTION(_getNewBlockList, !result) + PyObject * + _getNewBlockList(PyObject *page_dict, int raw) + { + fz_try(gctx) { + JM_make_textpage_dict(gctx, (fz_stext_page *) $self, page_dict, raw); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + %pythoncode %{ + def _textpage_dict(self, raw=False): + page_dict = {"width": self.rect.width, "height": self.rect.height} + self._getNewBlockList(page_dict, raw) + return page_dict + %} + + + //---------------------------------------------------------------- + // Get image meta information as a Python dictionary + //---------------------------------------------------------------- + FITZEXCEPTION(extractIMGINFO, !result) + %pythonprepend extractIMGINFO + %{"""Return a list with image meta information."""%} + PyObject * + extractIMGINFO(int hashes=0) + { + fz_stext_block *block; + int block_n = -1; + fz_stext_page *this_tpage = (fz_stext_page *) $self; + PyObject *rc = NULL, *block_dict = NULL; + fz_pixmap *pix = NULL; + fz_try(gctx) { + rc = PyList_New(0); + for (block = this_tpage->first_block; block; block = block->next) { + block_n++; + if (block->type == FZ_STEXT_BLOCK_TEXT) { + continue; + } + unsigned char digest[16]; + fz_image *img = block->u.i.image; + if (hashes) { + pix = fz_get_pixmap_from_image(gctx, img, NULL, NULL, NULL, NULL); + fz_md5_pixmap(gctx, pix, digest); + fz_drop_pixmap(gctx, pix); + pix = NULL; + } + fz_colorspace *cs = img->colorspace; + block_dict = PyDict_New(); + DICT_SETITEM_DROP(block_dict, dictkey_number, Py_BuildValue("i", block_n)); + DICT_SETITEM_DROP(block_dict, dictkey_bbox, + JM_py_from_rect(block->bbox)); + DICT_SETITEM_DROP(block_dict, dictkey_matrix, + JM_py_from_matrix(block->u.i.transform)); + DICT_SETITEM_DROP(block_dict, dictkey_width, + Py_BuildValue("i", img->w)); + DICT_SETITEM_DROP(block_dict, dictkey_height, + Py_BuildValue("i", img->h)); + DICT_SETITEM_DROP(block_dict, dictkey_colorspace, + Py_BuildValue("i", + fz_colorspace_n(gctx, cs))); + DICT_SETITEM_DROP(block_dict, dictkey_cs_name, + Py_BuildValue("s", + fz_colorspace_name(gctx, cs))); + DICT_SETITEM_DROP(block_dict, dictkey_xres, + Py_BuildValue("i", img->xres)); + DICT_SETITEM_DROP(block_dict, dictkey_yres, + Py_BuildValue("i", img->xres)); + DICT_SETITEM_DROP(block_dict, dictkey_bpc, + Py_BuildValue("i", (int) img->bpc)); + DICT_SETITEM_DROP(block_dict, dictkey_size, + Py_BuildValue("n", (Py_ssize_t) fz_image_size(gctx, img))); + if (hashes) { + DICT_SETITEMSTR_DROP(block_dict, "digest", + PyBytes_FromStringAndSize(digest, 16)); + } + LIST_APPEND_DROP(rc, block_dict); + } + } + fz_always(gctx) { + } + fz_catch(gctx) { + Py_CLEAR(rc); + Py_CLEAR(block_dict); + fz_drop_pixmap(gctx, pix); + return NULL; + } + return rc; + } + + + //---------------------------------------------------------------- + // Get text blocks with their bbox and concatenated lines + // as a Python list + //---------------------------------------------------------------- + FITZEXCEPTION(extractBLOCKS, !result) + %pythonprepend extractBLOCKS + %{"""Return a list with text block information."""%} + PyObject * + extractBLOCKS() + { + fz_stext_block *block; + fz_stext_line *line; + fz_stext_char *ch; + int block_n = -1; + PyObject *text = NULL, *litem; + fz_buffer *res = NULL; + fz_var(res); + fz_stext_page *this_tpage = (fz_stext_page *) $self; + fz_rect tp_rect = this_tpage->mediabox; + PyObject *lines = NULL; + fz_try(gctx) { + res = fz_new_buffer(gctx, 1024); + lines = PyList_New(0); + for (block = this_tpage->first_block; block; block = block->next) { + block_n++; + fz_rect blockrect = fz_empty_rect; + if (block->type == FZ_STEXT_BLOCK_TEXT) { + fz_clear_buffer(gctx, res); // set text buffer to empty + int line_n = -1; + int last_char = 0; + for (line = block->u.t.first_line; line; line = line->next) { + line_n++; + fz_rect linerect = fz_empty_rect; + for (ch = line->first_char; ch; ch = ch->next) { + fz_rect cbbox = JM_char_bbox(gctx, line, ch); + if (!JM_rects_overlap(tp_rect, cbbox) && + !fz_is_infinite_rect(tp_rect)) { + continue; + } + JM_append_rune(gctx, res, ch->c); + last_char = ch->c; + linerect = fz_union_rect(linerect, cbbox); + } + if (last_char != 10 && !fz_is_empty_rect(linerect)) { + fz_append_byte(gctx, res, 10); + } + blockrect = fz_union_rect(blockrect, linerect); + } + text = JM_EscapeStrFromBuffer(gctx, res); + } else if (JM_rects_overlap(tp_rect, block->bbox) || fz_is_infinite_rect(tp_rect)) { + fz_image *img = block->u.i.image; + fz_colorspace *cs = img->colorspace; + text = PyUnicode_FromFormat("", fz_colorspace_name(gctx, cs), img->w, img->h, img->bpc); + blockrect = fz_union_rect(blockrect, block->bbox); + } + if (!fz_is_empty_rect(blockrect)) { + litem = PyTuple_New(7); + PyTuple_SET_ITEM(litem, 0, Py_BuildValue("f", blockrect.x0)); + PyTuple_SET_ITEM(litem, 1, Py_BuildValue("f", blockrect.y0)); + PyTuple_SET_ITEM(litem, 2, Py_BuildValue("f", blockrect.x1)); + PyTuple_SET_ITEM(litem, 3, Py_BuildValue("f", blockrect.y1)); + PyTuple_SET_ITEM(litem, 4, Py_BuildValue("O", text)); + PyTuple_SET_ITEM(litem, 5, Py_BuildValue("i", block_n)); + PyTuple_SET_ITEM(litem, 6, Py_BuildValue("i", block->type)); + LIST_APPEND_DROP(lines, litem); + } + Py_CLEAR(text); + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + PyErr_Clear(); + } + fz_catch(gctx) { + Py_CLEAR(lines); + return NULL; + } + return lines; + } + + //---------------------------------------------------------------- + // Get text words with their bbox + //---------------------------------------------------------------- + FITZEXCEPTION(extractWORDS, !result) + %pythonprepend extractWORDS + %{"""Return a list with text word information."""%} + PyObject * + extractWORDS() + { + fz_stext_block *block; + fz_stext_line *line; + fz_stext_char *ch; + fz_buffer *buff = NULL; + fz_var(buff); + size_t buflen = 0; + int block_n = -1, line_n, word_n; + fz_rect wbbox = fz_empty_rect; // word bbox + fz_stext_page *this_tpage = (fz_stext_page *) $self; + fz_rect tp_rect = this_tpage->mediabox; + + PyObject *lines = NULL; + fz_try(gctx) { + buff = fz_new_buffer(gctx, 64); + lines = PyList_New(0); + for (block = this_tpage->first_block; block; block = block->next) { + block_n++; + if (block->type != FZ_STEXT_BLOCK_TEXT) { + continue; + } + line_n = -1; + for (line = block->u.t.first_line; line; line = line->next) { + line_n++; + word_n = 0; // word counter per line + fz_clear_buffer(gctx, buff); // reset word buffer + buflen = 0; // reset char counter + for (ch = line->first_char; ch; ch = ch->next) { + fz_rect cbbox = JM_char_bbox(gctx, line, ch); + if (!JM_rects_overlap(tp_rect, cbbox) && + !fz_is_infinite_rect(tp_rect)) { + continue; + } + if (ch->c == 32 && buflen == 0) + continue; // skip spaces at line start + if (ch->c == 32) { + if (!fz_is_empty_rect(wbbox)) { + word_n = JM_append_word(gctx, lines, buff, &wbbox, + block_n, line_n, word_n); + } + fz_clear_buffer(gctx, buff); + buflen = 0; // reset char counter + continue; + } + // append one unicode character to the word + JM_append_rune(gctx, buff, ch->c); + buflen++; + // enlarge word bbox + wbbox = fz_union_rect(wbbox, JM_char_bbox(gctx, line, ch)); + } + if (buflen && !fz_is_empty_rect(wbbox)) { + word_n = JM_append_word(gctx, lines, buff, &wbbox, + block_n, line_n, word_n); + } + fz_clear_buffer(gctx, buff); + buflen = 0; + } + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, buff); + PyErr_Clear(); + } + fz_catch(gctx) { + return NULL; + } + return lines; + } + + //---------------------------------------------------------------- + // TextPage poolsize + //---------------------------------------------------------------- + %pythonprepend poolsize + %{"""TextPage current poolsize."""%} + PyObject *poolsize() + { + fz_stext_page *tpage = (fz_stext_page *) $self; + size_t size = fz_pool_size(gctx, tpage->pool); + return PyLong_FromSize_t(size); + } + + //---------------------------------------------------------------- + // TextPage rectangle + //---------------------------------------------------------------- + %pythoncode %{@property%} + %pythonprepend rect + %{"""TextPage rectangle."""%} + %pythonappend rect %{val = Rect(val)%} + PyObject *rect() + { + fz_stext_page *this_tpage = (fz_stext_page *) $self; + fz_rect mediabox = this_tpage->mediabox; + return JM_py_from_rect(mediabox); + } + + //---------------------------------------------------------------- + // method _extractText() + //---------------------------------------------------------------- + FITZEXCEPTION(_extractText, !result) + %newobject _extractText; + PyObject *_extractText(int format) + { + fz_buffer *res = NULL; + fz_output *out = NULL; + PyObject *text = NULL; + fz_var(res); + fz_var(out); + fz_stext_page *this_tpage = (fz_stext_page *) $self; + fz_try(gctx) { + res = fz_new_buffer(gctx, 1024); + out = fz_new_output_with_buffer(gctx, res); + switch(format) { + case(1): + fz_print_stext_page_as_html(gctx, out, this_tpage, 0); + break; + case(3): + fz_print_stext_page_as_xml(gctx, out, this_tpage, 0); + break; + case(4): + fz_print_stext_page_as_xhtml(gctx, out, this_tpage, 0); + break; + default: + JM_print_stext_page_as_text(gctx, out, this_tpage); + break; + } + text = JM_UnicodeFromBuffer(gctx, res); + + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + fz_drop_output(gctx, out); + } + fz_catch(gctx) { + return NULL; + } + return text; + } + + + //---------------------------------------------------------------- + // method extractRect() + //---------------------------------------------------------------- + PyObject *extractTextbox(PyObject *rect) + { + fz_stext_page *this_tpage = (fz_stext_page *) $self; + fz_rect area = JM_rect_from_py(rect); + PyObject *rc = NULL; + char *found = NULL; + fz_try(gctx) { + char *found = JM_copy_rectangle(gctx, this_tpage, area); + if (found) { + rc = JM_UnicodeFromStr(found); + JM_Free(found); + } else { + rc = EMPTY_STRING; + } + } + fz_catch(gctx) { + if (found) JM_Free(found); + return EMPTY_STRING; + } + + return rc; + } + + //---------------------------------------------------------------- + // method extractSelection() + //---------------------------------------------------------------- + PyObject *extractSelection(PyObject *pointa, PyObject *pointb) + { + fz_stext_page *this_tpage = (fz_stext_page *) $self; + fz_point a = JM_point_from_py(pointa); + fz_point b = JM_point_from_py(pointb); + char *found = fz_copy_selection(gctx, this_tpage, a, b, 0); + PyObject *rc = NULL; + if (found) { + rc = PyUnicode_FromString(found); + JM_Free(found); + } else { + rc = EMPTY_STRING; + } + return rc; + } + + %pythoncode %{ + def extractText(self, sort=False) -> str: + """Return simple, bare text on the page.""" + if sort is False: + return self._extractText(0) + blocks = self.extractBLOCKS()[:] + blocks.sort(key=lambda b: (b[3], b[0])) + return "".join([b[4] for b in blocks]) + + def extractHTML(self) -> str: + """Return page content as a HTML string.""" + return self._extractText(1) + + def extractJSON(self, cb=None, sort=False) -> str: + """Return 'extractDICT' converted to JSON format.""" + import base64, json + val = self._textpage_dict(raw=False) + + class b64encode(json.JSONEncoder): + def default(self, s): + if type(s) in (bytes, bytearray): + return base64.b64encode(s).decode() + + if cb is not None: + val["width"] = cb.width + val["height"] = cb.height + if sort is True: + blocks = val["blocks"] + blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0])) + val["blocks"] = blocks + val = json.dumps(val, separators=(",", ":"), cls=b64encode, indent=1) + return val + + def extractRAWJSON(self, cb=None, sort=False) -> str: + """Return 'extractRAWDICT' converted to JSON format.""" + import base64, json + val = self._textpage_dict(raw=True) + + class b64encode(json.JSONEncoder): + def default(self,s): + if type(s) in (bytes, bytearray): + return base64.b64encode(s).decode() + + if cb is not None: + val["width"] = cb.width + val["height"] = cb.height + if sort is True: + blocks = val["blocks"] + blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0])) + val["blocks"] = blocks + val = json.dumps(val, separators=(",", ":"), cls=b64encode, indent=1) + return val + + def extractXML(self) -> str: + """Return page content as a XML string.""" + return self._extractText(3) + + def extractXHTML(self) -> str: + """Return page content as a XHTML string.""" + return self._extractText(4) + + def extractDICT(self, cb=None, sort=False) -> dict: + """Return page content as a Python dict of images and text spans.""" + val = self._textpage_dict(raw=False) + if cb is not None: + val["width"] = cb.width + val["height"] = cb.height + if sort is True: + blocks = val["blocks"] + blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0])) + val["blocks"] = blocks + return val + + def extractRAWDICT(self, cb=None, sort=False) -> dict: + """Return page content as a Python dict of images and text characters.""" + val = self._textpage_dict(raw=True) + if cb is not None: + val["width"] = cb.width + val["height"] = cb.height + if sort is True: + blocks = val["blocks"] + blocks.sort(key=lambda b: (b["bbox"][3], b["bbox"][0])) + val["blocks"] = blocks + return val + + def __del__(self): + if not type(self) is TextPage: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; + +//------------------------------------------------------------------------ +// Graftmap - only used internally for inter-PDF object copy operations +//------------------------------------------------------------------------ +struct Graftmap +{ + %extend + { + ~Graftmap() + { + DEBUGMSG1("Graftmap"); + pdf_graft_map *this_gm = (pdf_graft_map *) $self; + pdf_drop_graft_map(gctx, this_gm); + DEBUGMSG2; + } + + FITZEXCEPTION(Graftmap, !result) + Graftmap(struct Document *doc) + { + pdf_graft_map *map = NULL; + fz_try(gctx) { + pdf_document *dst = pdf_specifics(gctx, (fz_document *) doc); + ASSERT_PDF(dst); + map = pdf_new_graft_map(gctx, dst); + } + fz_catch(gctx) { + return NULL; + } + return (struct Graftmap *) map; + } + + %pythoncode %{ + def __del__(self): + if not type(self) is Graftmap: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; + + +//------------------------------------------------------------------------ +// TextWriter +//------------------------------------------------------------------------ +struct TextWriter +{ + %extend { + ~TextWriter() + { + DEBUGMSG1("TextWriter"); + fz_text *this_tw = (fz_text *) $self; + fz_drop_text(gctx, this_tw); + DEBUGMSG2; + } + + FITZEXCEPTION(TextWriter, !result) + %pythonprepend TextWriter + %{"""Stores text spans for later output on compatible PDF pages."""%} + %pythonappend TextWriter %{ + self.opacity = opacity + self.color = color + self.rect = Rect(page_rect) + self.ctm = Matrix(1, 0, 0, -1, 0, self.rect.height) + self.ictm = ~self.ctm + self.last_point = Point() + self.last_point.__doc__ = "Position following last text insertion." + self.text_rect = Rect() + + self.text_rect.__doc__ = "Accumulated area of text spans." + self.used_fonts = set() + self.thisown = True + %} + TextWriter(PyObject *page_rect, float opacity=1, PyObject *color=NULL ) + { + fz_text *text = NULL; + fz_try(gctx) { + text = fz_new_text(gctx); + } + fz_catch(gctx) { + return NULL; + } + return (struct TextWriter *) text; + } + + FITZEXCEPTION(append, !result) + %pythonprepend append %{ + """Store 'text' at point 'pos' using 'font' and 'fontsize'.""" + + pos = Point(pos) * self.ictm + if font is None: + font = Font("helv") + if not font.is_writable: + raise ValueError("Unsupported font '%s'." % font.name) + if right_to_left: + text = self.clean_rtl(text) + text = "".join(reversed(text)) + right_to_left = 0 + %} + %pythonappend append %{ + self.last_point = Point(val[-2:]) * self.ctm + self.text_rect = self._bbox * self.ctm + val = self.text_rect, self.last_point + if font.flags["mono"] == 1: + self.used_fonts.add(font) + %} + PyObject * + append(PyObject *pos, char *text, struct Font *font=NULL, float fontsize=11, char *language=NULL, int right_to_left=0, int small_caps=0) + { + fz_text_language lang = fz_text_language_from_string(language); + fz_point p = JM_point_from_py(pos); + fz_matrix trm = fz_make_matrix(fontsize, 0, 0, fontsize, p.x, p.y); + int markup_dir = 0, wmode = 0; + fz_try(gctx) { + if (small_caps == 0) { + trm = fz_show_string(gctx, (fz_text *) $self, (fz_font *) font, + trm, text, wmode, right_to_left, markup_dir, lang); + } else { + trm = JM_show_string_cs(gctx, (fz_text *) $self, (fz_font *) font, + trm, text, wmode, right_to_left, markup_dir, lang); + } + } + fz_catch(gctx) { + return NULL; + } + return JM_py_from_matrix(trm); + } + + %pythoncode %{ + def appendv(self, pos, text, font=None, fontsize=11, + language=None, small_caps=False): + """Append text in vertical write mode.""" + lheight = fontsize * 1.2 + for c in text: + self.append(pos, c, font=font, fontsize=fontsize, + language=language, small_caps=small_caps) + pos.y += lheight + return self.text_rect, self.last_point + + + def clean_rtl(self, text): + """Revert the sequence of Latin text parts. + + Text with right-to-left writing direction (Arabic, Hebrew) often + contains Latin parts, which are written in left-to-right: numbers, names, + etc. For output as PDF text we need *everything* in right-to-left. + E.g. an input like " ABCDE FG HIJ KL " will be + converted to " JIH GF EDCBA LK ". The Arabic + parts remain untouched. + + Args: + text: str + Returns: + Massaged string. + """ + if not text: + return text + # split into words at space boundaries + words = text.split(" ") + idx = [] + for i in range(len(words)): + w = words[i] + # revert character sequence for Latin only words + if not (len(w) < 2 or max([ord(c) for c in w]) > 255): + words[i] = "".join(reversed(w)) + idx.append(i) # stored index of Latin word + + # adjacent Latin words must revert their sequence, too + idx2 = [] # store indices of adjacent Latin words + for i in range(len(idx)): + if idx2 == []: # empty yet? + idx2.append(idx[i]) # store Latin word number + + elif idx[i] > idx2[-1] + 1: # large gap to last? + if len(idx2) > 1: # at least two consecutives? + words[idx2[0] : idx2[-1] + 1] = reversed( + words[idx2[0] : idx2[-1] + 1] + ) # revert their sequence + idx2 = [idx[i]] # re-initialize + + elif idx[i] == idx2[-1] + 1: # new adjacent Latin word + idx2.append(idx[i]) + + text = " ".join(words) + return text + %} + + + %pythoncode %{@property%} + %pythonappend _bbox%{val = Rect(val)%} + PyObject *_bbox() + { + return JM_py_from_rect(fz_bound_text(gctx, (fz_text *) $self, NULL, fz_identity)); + } + + FITZEXCEPTION(write_text, !result) + %pythonprepend write_text%{ + """Write the text to a PDF page having the TextWriter's page size. + + Args: + page: a PDF page having same size. + color: override text color. + opacity: override transparency. + overlay: put in foreground or background. + morph: tuple(Point, Matrix), apply a matrix with a fixpoint. + matrix: Matrix to be used instead of 'morph' argument. + render_mode: (int) PDF render mode operator 'Tr'. + """ + + CheckParent(page) + if abs(self.rect - page.rect) > 1e-3: + raise ValueError("incompatible page rect") + if morph != None: + if (type(morph) not in (tuple, list) + or type(morph[0]) is not Point + or type(morph[1]) is not Matrix + ): + raise ValueError("morph must be (Point, Matrix) or None") + if matrix != None and morph != None: + raise ValueError("only one of matrix, morph is allowed") + if getattr(opacity, "__float__", None) is None or opacity == -1: + opacity = self.opacity + if color is None: + color = self.color + %} + + %pythonappend write_text%{ + max_nums = val[0] + content = val[1] + max_alp, max_font = max_nums + old_cont_lines = content.splitlines() + + optcont = page._get_optional_content(oc) + if optcont != None: + bdc = "/OC /%s BDC" % optcont + emc = "EMC" + else: + bdc = emc = "" + + new_cont_lines = ["q"] + if bdc: + new_cont_lines.append(bdc) + + cb = page.cropbox_position + if page.rotation in (90, 270): + delta = page.rect.height - page.rect.width + else: + delta = 0 + mb = page.mediabox + if bool(cb) or mb.y0 != 0 or delta != 0: + new_cont_lines.append("1 0 0 1 %g %g cm" % (cb.x, cb.y + mb.y0 - delta)) + + if morph: + p = morph[0] * self.ictm + delta = Matrix(1, 1).pretranslate(p.x, p.y) + matrix = ~delta * morph[1] * delta + if morph or matrix: + new_cont_lines.append("%g %g %g %g %g %g cm" % JM_TUPLE(matrix)) + + for line in old_cont_lines: + if line.endswith(" cm"): + continue + if line == "BT": + new_cont_lines.append(line) + new_cont_lines.append("%i Tr" % render_mode) + continue + if line.endswith(" gs"): + alp = int(line.split()[0][4:]) + max_alp + line = "/Alp%i gs" % alp + elif line.endswith(" Tf"): + temp = line.split() + fsize = float(temp[1]) + if render_mode != 0: + w = fsize * 0.05 + else: + w = 1 + new_cont_lines.append("%g w" % w) + font = int(temp[0][2:]) + max_font + line = " ".join(["/F%i" % font] + temp[1:]) + elif line.endswith(" rg"): + new_cont_lines.append(line.replace("rg", "RG")) + elif line.endswith(" g"): + new_cont_lines.append(line.replace(" g", " G")) + elif line.endswith(" k"): + new_cont_lines.append(line.replace(" k", " K")) + new_cont_lines.append(line) + if emc: + new_cont_lines.append(emc) + new_cont_lines.append("Q\n") + content = "\n".join(new_cont_lines).encode("utf-8") + TOOLS._insert_contents(page, content, overlay=overlay) + val = None + for font in self.used_fonts: + repair_mono_font(page, font) + %} + PyObject *write_text(struct Page *page, PyObject *color=NULL, float opacity=-1, int overlay=1, + PyObject *morph=NULL, PyObject *matrix=NULL, int render_mode=0, int oc=0) + { + pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page); + pdf_obj *resources = NULL; + fz_buffer *contents = NULL; + fz_device *dev = NULL; + PyObject *result = NULL, *max_nums, *cont_string; + float alpha = 1; + if (opacity >= 0 && opacity < 1) + alpha = opacity; + fz_colorspace *colorspace; + int ncol = 1; + float dev_color[4] = {0, 0, 0, 0}; + if (EXISTS(color)) { + JM_color_FromSequence(color, &ncol, dev_color); + } + switch(ncol) { + case 3: colorspace = fz_device_rgb(gctx); break; + case 4: colorspace = fz_device_cmyk(gctx); break; + default: colorspace = fz_device_gray(gctx); break; + } + + fz_var(contents); + fz_var(resources); + fz_var(dev); + fz_try(gctx) { + ASSERT_PDF(pdfpage); + resources = pdf_new_dict(gctx, pdfpage->doc, 5); + contents = fz_new_buffer(gctx, 1024); + dev = pdf_new_pdf_device(gctx, pdfpage->doc, fz_identity, + resources, contents); + fz_fill_text(gctx, dev, (fz_text *) $self, fz_identity, + colorspace, dev_color, alpha, fz_default_color_params); + fz_close_device(gctx, dev); + + // copy generated resources into the one of the page + max_nums = JM_merge_resources(gctx, pdfpage, resources); + cont_string = JM_EscapeStrFromBuffer(gctx, contents); + result = Py_BuildValue("OO", max_nums, cont_string); + Py_DECREF(cont_string); + Py_DECREF(max_nums); + } + fz_always(gctx) { + fz_drop_buffer(gctx, contents); + pdf_drop_obj(gctx, resources); + fz_drop_device(gctx, dev); + } + fz_catch(gctx) { + return NULL; + } + return result; + } + %pythoncode %{ + def __del__(self): + if not type(self) is TextWriter: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; + + +//------------------------------------------------------------------------ +// Font +//------------------------------------------------------------------------ +struct Font +{ + %extend + { + ~Font() + { + DEBUGMSG1("Font"); + fz_font *this_font = (fz_font *) $self; + fz_drop_font(gctx, this_font); + DEBUGMSG2; + } + + FITZEXCEPTION(Font, !result) + %pythonprepend Font %{ + if fontbuffer: + if hasattr(fontbuffer, "getvalue"): + fontbuffer = fontbuffer.getvalue() + elif isinstance(fontbuffer, bytearray): + fontbuffer = bytes(fontbuffer) + if not isinstance(fontbuffer, bytes): + raise ValueError("bad type: 'fontbuffer'") + + if isinstance(fontname, str): + fname_lower = fontname.lower() + if "/" in fname_lower or "\\" in fname_lower or "." in fname_lower: + print("Warning: did you mean a fontfile?") + + if fname_lower in ("cjk", "china-t", "china-ts"): + ordering = 0 + elif fname_lower.startswith("china-s"): + ordering = 1 + elif fname_lower.startswith("korea"): + ordering = 3 + elif fname_lower.startswith("japan"): + ordering = 2 + elif fname_lower in fitz_fontdescriptors.keys(): + import pymupdf_fonts # optional fonts + fontbuffer = pymupdf_fonts.myfont(fname_lower) # make a copy + fontname = None # ensure using fontbuffer only + del pymupdf_fonts # remove package again + + elif ordering < 0: + fontname = Base14_fontdict.get(fontname, fontname) + %} + %pythonappend Font %{self.thisown = True%} + Font(char *fontname=NULL, char *fontfile=NULL, + PyObject *fontbuffer=NULL, int script=0, + char *language=NULL, int ordering=-1, int is_bold=0, + int is_italic=0, int is_serif=0, int embed=1) + { + fz_font *font = NULL; + fz_try(gctx) { + fz_text_language lang = fz_text_language_from_string(language); + font = JM_get_font(gctx, fontname, fontfile, + fontbuffer, script, lang, ordering, + is_bold, is_italic, is_serif, embed); + } + fz_catch(gctx) { + return NULL; + } + return (struct Font *) font; + } + + + %pythonprepend glyph_advance + %{"""Return the glyph width of a unicode (font size 1)."""%} + PyObject *glyph_advance(int chr, char *language=NULL, int script=0, int wmode=0, int small_caps=0) + { + fz_font *font, *thisfont = (fz_font *) $self; + int gid; + fz_text_language lang = fz_text_language_from_string(language); + if (small_caps) { + gid = fz_encode_character_sc(gctx, thisfont, chr); + if (gid >= 0) font = thisfont; + } else { + gid = fz_encode_character_with_fallback(gctx, thisfont, chr, script, lang, &font); + } + return PyFloat_FromDouble((double) fz_advance_glyph(gctx, font, gid, wmode)); + } + + + FITZEXCEPTION(text_length, !result) + %pythonprepend text_length + %{"""Return length of unicode 'text' under a fontsize."""%} + PyObject *text_length(PyObject *text, double fontsize=11, char *language=NULL, int script=0, int wmode=0, int small_caps=0) + { + fz_font *font=NULL, *thisfont = (fz_font *) $self; + fz_text_language lang = fz_text_language_from_string(language); + double rc = 0; + int gid; + fz_try(gctx) { + if (!PyUnicode_Check(text) || PyUnicode_READY(text) != 0) { + RAISEPY(gctx, MSG_BAD_TEXT, PyExc_TypeError); + } + Py_ssize_t i, len = PyUnicode_GET_LENGTH(text); + int kind = PyUnicode_KIND(text); + void *data = PyUnicode_DATA(text); + for (i = 0; i < len; i++) { + int c = PyUnicode_READ(kind, data, i); + if (small_caps) { + gid = fz_encode_character_sc(gctx, thisfont, c); + if (gid >= 0) font = thisfont; + } else { + gid = fz_encode_character_with_fallback(gctx,thisfont, c, script, lang, &font); + } + rc += (double) fz_advance_glyph(gctx, font, gid, wmode); + } + } + fz_catch(gctx) { + PyErr_Clear(); + return NULL; + } + rc *= fontsize; + return PyFloat_FromDouble(rc); + } + + + FITZEXCEPTION(char_lengths, !result) + %pythonprepend char_lengths + %{"""Return tuple of char lengths of unicode 'text' under a fontsize."""%} + PyObject *char_lengths(PyObject *text, double fontsize=11, char *language=NULL, int script=0, int wmode=0, int small_caps=0) + { + fz_font *font, *thisfont = (fz_font *) $self; + fz_text_language lang = fz_text_language_from_string(language); + PyObject *rc = NULL; + int gid; + fz_try(gctx) { + if (!PyUnicode_Check(text) || PyUnicode_READY(text) != 0) { + RAISEPY(gctx, MSG_BAD_TEXT, PyExc_TypeError); + } + Py_ssize_t i, len = PyUnicode_GET_LENGTH(text); + int kind = PyUnicode_KIND(text); + void *data = PyUnicode_DATA(text); + rc = PyTuple_New(len); + for (i = 0; i < len; i++) { + int c = PyUnicode_READ(kind, data, i); + if (small_caps) { + gid = fz_encode_character_sc(gctx, thisfont, c); + if (gid >= 0) font = thisfont; + } else { + gid = fz_encode_character_with_fallback(gctx,thisfont, c, script, lang, &font); + } + PyTuple_SET_ITEM(rc, i, + PyFloat_FromDouble(fontsize * (double) fz_advance_glyph(gctx, font, gid, wmode))); + } + } + fz_catch(gctx) { + PyErr_Clear(); + Py_CLEAR(rc); + return NULL; + } + return rc; + } + + + %pythonprepend glyph_bbox + %{"""Return the glyph bbox of a unicode (font size 1)."""%} + %pythonappend glyph_bbox %{val = Rect(val)%} + PyObject *glyph_bbox(int chr, char *language=NULL, int script=0, int small_caps=0) + { + fz_font *font, *thisfont = (fz_font *) $self; + int gid; + fz_text_language lang = fz_text_language_from_string(language); + if (small_caps) { + gid = fz_encode_character_sc(gctx, thisfont, chr); + if (gid >= 0) font = thisfont; + } else { + gid = fz_encode_character_with_fallback(gctx, thisfont, chr, script, lang, &font); + } + return JM_py_from_rect(fz_bound_glyph(gctx, font, gid, fz_identity)); + } + + %pythonprepend has_glyph + %{"""Check whether font has a glyph for this unicode."""%} + PyObject *has_glyph(int chr, char *language=NULL, int script=0, int fallback=0, int small_caps=0) + { + fz_font *font, *thisfont = (fz_font *) $self; + fz_text_language lang; + int gid = 0; + if (fallback) { + lang = fz_text_language_from_string(language); + gid = fz_encode_character_with_fallback(gctx, (fz_font *) $self, chr, script, lang, &font); + } else { + if (!small_caps) { + gid = fz_encode_character(gctx, thisfont, chr); + } else { + gid = fz_encode_character_sc(gctx, thisfont, chr); + } + } + return Py_BuildValue("i", gid); + } + + + %pythoncode %{ + def valid_codepoints(self): + from array import array + gc = self.glyph_count + cp = array("l", (0,) * gc) + arr = cp.buffer_info() + self._valid_unicodes(arr) + return array("l", sorted(set(cp))[1:]) + %} + void _valid_unicodes(PyObject *arr) + { + fz_font *font = (fz_font *) $self; + PyObject *temp = PySequence_ITEM(arr, 0); + void *ptr = PyLong_AsVoidPtr(temp); + JM_valid_chars(gctx, font, ptr); + Py_DECREF(temp); + } + + + %pythoncode %{@property%} + PyObject *flags() + { + fz_font_flags_t *f = fz_font_flags((fz_font *) $self); + if (!f) Py_RETURN_NONE; + return Py_BuildValue( + "{s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N,s:N" + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + ",s:N,s:N" + #endif + "}", + "mono", JM_BOOL(f->is_mono), + "serif", JM_BOOL(f->is_serif), + "bold", JM_BOOL(f->is_bold), + "italic", JM_BOOL(f->is_italic), + "substitute", JM_BOOL(f->ft_substitute), + "stretch", JM_BOOL(f->ft_stretch), + "fake-bold", JM_BOOL(f->fake_bold), + "fake-italic", JM_BOOL(f->fake_italic), + "opentype", JM_BOOL(f->has_opentype), + "invalid-bbox", JM_BOOL(f->invalid_bbox), + "cjk", JM_BOOL(f->cjk), + "cjk-lang", (f->cjk ? PyLong_FromUnsignedLong((unsigned long) f->cjk_lang) : Py_BuildValue("s",NULL)) + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + , + "embed", JM_BOOL(f->embed), + "never-embed", JM_BOOL(f->never_embed) + #endif + ); + + } + + + %pythoncode %{@property%} + PyObject *is_bold() + { + fz_font *font = (fz_font *) $self; + if (fz_font_is_bold(gctx,font)) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + } + + + %pythoncode %{@property%} + PyObject *is_serif() + { + fz_font *font = (fz_font *) $self; + if (fz_font_is_serif(gctx,font)) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + } + + + %pythoncode %{@property%} + PyObject *is_italic() + { + fz_font *font = (fz_font *) $self; + if (fz_font_is_italic(gctx,font)) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + } + + + %pythoncode %{@property%} + PyObject *is_monospaced() + { + fz_font *font = (fz_font *) $self; + if (fz_font_is_monospaced(gctx,font)) { + Py_RETURN_TRUE; + } + Py_RETURN_FALSE; + } + + + /* temporarily disabled + * PyObject *is_writable() + * { + * fz_font *font = (fz_font *) $self; + * if (fz_font_t3_procs(gctx, font) || + * fz_font_flags(font)->ft_substitute || + * !pdf_font_writing_supported(font)) { + * Py_RETURN_FALSE; + * } + * Py_RETURN_TRUE; + * } + */ + + %pythoncode %{@property%} + PyObject *name() + { + return JM_UnicodeFromStr(fz_font_name(gctx, (fz_font *) $self)); + } + + %pythoncode %{@property%} + int glyph_count() + { + fz_font *this_font = (fz_font *) $self; + return this_font->glyph_count; + } + + %pythoncode %{@property%} + PyObject *buffer() + { + fz_font *this_font = (fz_font *) $self; + unsigned char *data = NULL; + size_t len = fz_buffer_storage(gctx, this_font->buffer, &data); + return JM_BinFromCharSize(data, len); + } + + %pythoncode %{@property%} + %pythonappend bbox%{val = Rect(val)%} + PyObject *bbox() + { + fz_font *this_font = (fz_font *) $self; + return JM_py_from_rect(fz_font_bbox(gctx, this_font)); + } + + %pythoncode %{@property%} + %pythonprepend ascender + %{"""Return the glyph ascender value."""%} + float ascender() + { + return fz_font_ascender(gctx, (fz_font *) $self); + } + + + %pythoncode %{@property%} + %pythonprepend descender + %{"""Return the glyph descender value."""%} + float descender() + { + return fz_font_descender(gctx, (fz_font *) $self); + } + + + %pythoncode %{ + + @property + def is_writable(self): + return True + + def glyph_name_to_unicode(self, name): + """Return the unicode for a glyph name.""" + return glyph_name_to_unicode(name) + + def unicode_to_glyph_name(self, ch): + """Return the glyph name for a unicode.""" + return unicode_to_glyph_name(ch) + + def __repr__(self): + return "Font('%s')" % self.name + + def __del__(self): + if not type(self) is Font: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; + + +//------------------------------------------------------------------------ +// DocumentWriter +//------------------------------------------------------------------------ + +struct DocumentWriter +{ + %extend + { + ~DocumentWriter() + { + // need this structure to free any fz_output the writer may have + typedef struct { // copied from pdf_write.c + fz_document_writer super; + pdf_document *pdf; + pdf_write_options opts; + fz_output *out; + fz_rect mediabox; + pdf_obj *resources; + fz_buffer *contents; + } pdf_writer; + + fz_document_writer *writer_fz = (fz_document_writer *) $self; + fz_output *out = NULL; + pdf_writer *writer_pdf = (pdf_writer *) writer_fz; + if (writer_pdf) { + out = writer_pdf->out; + if (out) { + DEBUGMSG1("Output of DocumentWriter"); + fz_drop_output(gctx, out); + writer_pdf->out = NULL; + DEBUGMSG2; + } + } + DEBUGMSG1("DocumentWriter"); + fz_drop_document_writer( gctx, writer_fz); + DEBUGMSG2; + } + + FITZEXCEPTION(DocumentWriter, !result) + %pythonprepend DocumentWriter + %{ + if type(path) is str: + pass + elif hasattr(path, "absolute"): + path = str(path) + elif hasattr(path, "name"): + path = path.name + if options==None: + options="" + %} + %pythonappend DocumentWriter + %{ + %} + DocumentWriter( PyObject* path, const char* options=NULL) + { + fz_output *out = NULL; + fz_document_writer* ret=NULL; + fz_try(gctx) { + if (PyUnicode_Check(path)) { + ret = fz_new_pdf_writer( gctx, PyUnicode_AsUTF8(path), options); + } else { + out = JM_new_output_fileptr(gctx, path); + ret = fz_new_pdf_writer_with_output(gctx, out, options); + } + } + + fz_catch(gctx) { + return NULL; + } + return (struct DocumentWriter*) ret; + } + + struct DeviceWrapper* begin_page( PyObject* mediabox) + { + fz_rect mediabox2 = JM_rect_from_py(mediabox); + fz_device* device = fz_begin_page( gctx, (fz_document_writer*) $self, mediabox2); + struct DeviceWrapper* device_wrapper + = (struct DeviceWrapper*) calloc(1, sizeof(struct DeviceWrapper)) + ; + device_wrapper->device = device; + device_wrapper->list = NULL; + return device_wrapper; + } + + void end_page() + { + fz_end_page( gctx, (fz_document_writer*) $self); + } + + void close() + { + fz_document_writer *writer = (fz_document_writer*) $self; + fz_close_document_writer( gctx, writer); + } + %pythoncode + %{ + def __del__(self): + if not type(self) is DocumentWriter: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + %} + } +}; + +//------------------------------------------------------------------------ +// Archive +//------------------------------------------------------------------------ +struct Archive +{ + %extend + { + ~Archive() + { + DEBUGMSG1("Archive"); + fz_drop_archive( gctx, (fz_archive *) $self); + DEBUGMSG2; + } + FITZEXCEPTION(Archive, !result) + %pythonprepend Archive %{ + self._subarchives = [] + %} + %pythonappend Archive %{ + self.thisown = True + if args != (): + self.add(*args) + %} + + //--------------------------------------- + // new empty archive + //--------------------------------------- + Archive(struct Archive *a0=NULL, const char *path=NULL) + { + fz_archive *arch=NULL; + fz_try(gctx) { + arch = fz_new_multi_archive(gctx); + } + fz_catch(gctx) { + return NULL; + } + return (struct Archive *) arch; + } + + Archive(PyObject *a0=NULL, const char *path=NULL) + { + fz_archive *arch=NULL; + fz_try(gctx) { + arch = fz_new_multi_archive(gctx); + } + fz_catch(gctx) { + return NULL; + } + return (struct Archive *) arch; + } + + FITZEXCEPTION(has_entry, !result) + PyObject *has_entry(const char *name) + { + fz_archive *arch = (fz_archive *) $self; + int ret = 0; + fz_try(gctx) { + ret = fz_has_archive_entry(gctx, arch, name); + } + fz_catch(gctx) { + return NULL; + } + return JM_BOOL(ret); + } + + FITZEXCEPTION(read_entry, !result) + PyObject *read_entry(const char *name) + { + fz_archive *arch = (fz_archive *) $self; + PyObject *ret = NULL; + fz_buffer *buff = NULL; + fz_try(gctx) { + buff = fz_read_archive_entry(gctx, arch, name); + ret = JM_BinFromBuffer(gctx, buff); + } + fz_always(gctx) { + fz_drop_buffer(gctx, buff); + } + fz_catch(gctx) { + return NULL; + } + return ret; + } + + //-------------------------------------- + // add dir + //-------------------------------------- + FITZEXCEPTION(_add_dir, !result) + PyObject *_add_dir(const char *folder, const char *path=NULL) + { + fz_archive *arch = (fz_archive *) $self; + fz_archive *sub = NULL; + fz_try(gctx) { + sub = fz_open_directory(gctx, folder); + fz_mount_multi_archive(gctx, arch, sub, path); + } + fz_always(gctx) { + fz_drop_archive(gctx, sub); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------- + // add archive + //---------------------------------- + FITZEXCEPTION(_add_arch, !result) + PyObject *_add_arch(struct Archive *subarch, const char *path=NULL) + { + fz_archive *arch = (fz_archive *) $self; + fz_archive *sub = (fz_archive *) subarch; + fz_try(gctx) { + fz_mount_multi_archive(gctx, arch, sub, path); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------- + // add ZIP/TAR from file + //---------------------------------- + FITZEXCEPTION(_add_ziptarfile, !result) + PyObject *_add_ziptarfile(const char *filepath, int type, const char *path=NULL) + { + fz_archive *arch = (fz_archive *) $self; + fz_archive *sub = NULL; + fz_try(gctx) { + if (type==1) { + sub = fz_open_zip_archive(gctx, filepath); + } else { + sub = fz_open_tar_archive(gctx, filepath); + } + fz_mount_multi_archive(gctx, arch, sub, path); + } + fz_always(gctx) { + fz_drop_archive(gctx, sub); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------- + // add ZIP/TAR from memory + //---------------------------------- + FITZEXCEPTION(_add_ziptarmemory, !result) + PyObject *_add_ziptarmemory(PyObject *memory, int type, const char *path=NULL) + { + fz_archive *arch = (fz_archive *) $self; + fz_archive *sub = NULL; + fz_stream *stream = NULL; + fz_buffer *buff = NULL; + fz_try(gctx) { + buff = JM_BufferFromBytes(gctx, memory); + stream = fz_open_buffer(gctx, buff); + if (type==1) { + sub = fz_open_zip_archive_with_stream(gctx, stream); + } else { + sub = fz_open_tar_archive_with_stream(gctx, stream); + } + fz_mount_multi_archive(gctx, arch, sub, path); + } + fz_always(gctx) { + fz_drop_stream(gctx, stream); + fz_drop_buffer(gctx, buff); + fz_drop_archive(gctx, sub); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + //---------------------------------- + // add "tree" item + //---------------------------------- + FITZEXCEPTION(_add_treeitem, !result) + PyObject *_add_treeitem(PyObject *memory, const char *name, const char *path=NULL) + { + fz_archive *arch = (fz_archive *) $self; + fz_archive *sub = NULL; + fz_buffer *buff = NULL; + int drop_sub = 0; + fz_try(gctx) { + buff = JM_BufferFromBytes(gctx, memory); + sub = JM_last_tree(gctx, arch, path); + if (!sub) { + sub = fz_new_tree_archive(gctx, NULL); + drop_sub = 1; + } + fz_tree_archive_add_buffer(gctx, sub, name, buff); + if (drop_sub) { + fz_mount_multi_archive(gctx, arch, sub, path); + } + } + fz_always(gctx) { + fz_drop_buffer(gctx, buff); + if (drop_sub) { + fz_drop_archive(gctx, sub); + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + %pythoncode %{ + def add(self, content, path=None): + """Add a sub-archive. + + Args: + content: content to be added. May be one of Archive, folder + name, file name, raw bytes (bytes, bytearray), zipfile, + tarfile, or a sequence of any of these types. + path: (str) a "virtual" path name, under which the elements + of content can be retrieved. Use it to e.g. cope with + duplicate element names. + """ + bin_ok = lambda x: isinstance(x, (bytes, bytearray, io.BytesIO)) + + entries = [] + mount = None + fmt = None + + def make_subarch(): + subarch = {"fmt": fmt, "entries": entries, "path": mount} + if fmt != "tree" or self._subarchives == []: + self._subarchives.append(subarch) + else: + ltree = self._subarchives[-1] + if ltree["fmt"] != "tree" or ltree["path"] != subarch["path"]: + self._subarchives.append(subarch) + else: + ltree["entries"].extend(subarch["entries"]) + self._subarchives[-1] = ltree + return + + if isinstance(content, zipfile.ZipFile): + fmt = "zip" + entries = content.namelist() + mount = path + filename = getattr(content, "filename", None) + fp = getattr(content, "fp", None) + if filename: + self._add_ziptarfile(filename, 1, path) + else: + self._add_ziptarmemory(fp.getvalue(), 1, path) + return make_subarch() + + if isinstance(content, tarfile.TarFile): + fmt = "tar" + entries = content.getnames() + mount = path + filename = getattr(content.fileobj, "name", None) + fp = content.fileobj + if not isinstance(fp, io.BytesIO) and not filename: + fp = fp.fileobj + if filename: + self._add_ziptarfile(filename, 0, path) + else: + self._add_ziptarmemory(fp.getvalue(), 0, path) + return make_subarch() + + if isinstance(content, Archive): + fmt = "multi" + mount = path + self._add_arch(content, path) + return make_subarch() + + if bin_ok(content): + if not (path and type(path) is str): + raise ValueError("need name for binary content") + fmt = "tree" + mount = None + entries = [path] + self._add_treeitem(content, path) + return make_subarch() + + if hasattr(content, "name"): + content = content.name + elif isinstance(content, pathlib.Path): + content = str(content) + + if os.path.isdir(str(content)): + a0 = str(content) + fmt = "dir" + mount = path + entries = os.listdir(a0) + self._add_dir(a0, path) + return make_subarch() + + if os.path.isfile(str(content)): + if not (path and type(path) is str): + raise ValueError("need name for binary content") + a0 = str(content) + _ = open(a0, "rb") + ff = _.read() + _.close() + fmt = "tree" + mount = None + entries = [path] + self._add_treeitem(ff, path) + return make_subarch() + + if type(content) is str or not getattr(content, "__getitem__", None): + raise ValueError("bad archive content") + + #---------------------------------------- + # handling sequence types here + #---------------------------------------- + + if len(content) == 2: # covers the tree item plus path + data, name = content + if bin_ok(data) or os.path.isfile(str(data)): + if not type(name) is str: + raise ValueError(f"bad item name {name}") + mount = path + fmt = "tree" + if bin_ok(data): + self._add_treeitem(data, name, path=mount) + else: + _ = open(str(data), "rb") + ff = _.read() + _.close() + seld._add_treeitem(ff, name, path=mount) + entries = [name] + return make_subarch() + + # deal with sequence of disparate items + for item in content: + self.add(item, path) + + __doc__ = """Archive(dirname [, path]) - from folder + Archive(file [, path]) - from file name or object + Archive(data, name) - from memory item + Archive() - empty archive + Archive(archive [, path]) - from archive + """ + + @property + def entry_list(self): + """List of sub archives.""" + return self._subarchives + + def __repr__(self): + return f"Archive, sub-archives: {len(self._subarchives)}" + + def __del__(self): + if not type(self) is Archive: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; +//------------------------------------------------------------------------ +// Xml +//------------------------------------------------------------------------ +struct Xml +{ + %extend + { + ~Xml() + { + DEBUGMSG1("Xml"); + fz_drop_xml( gctx, (fz_xml*) $self); + DEBUGMSG2; + } + + FITZEXCEPTION(Xml, !result) + Xml(fz_xml* xml) + { + fz_keep_xml( gctx, xml); + return (struct Xml*) xml; + } + + Xml(const char *html) + { + fz_buffer *buff = NULL; + fz_xml *ret = NULL; + fz_try(gctx) { + buff = fz_new_buffer_from_copied_data(gctx, html, strlen(html)+1); + ret = fz_parse_xml_from_html5(gctx, buff); + } + fz_always(gctx) { + fz_drop_buffer(gctx, buff); + } + fz_catch(gctx) { + return NULL; + } + fz_keep_xml(gctx, ret); + return (struct Xml*) ret; + } + + %pythoncode %{@property%} + FITZEXCEPTION (root, !result) + struct Xml* root() + { + fz_xml* ret = NULL; + fz_try(gctx) { + ret = fz_xml_root((fz_xml_doc *) $self); + } + fz_catch(gctx) { + return NULL; + } + return (struct Xml*) ret; + } + + FITZEXCEPTION (bodytag, !result) + struct Xml* bodytag() + { + fz_xml* ret = NULL; + fz_try(gctx) { + ret = fz_keep_xml( gctx, fz_dom_body( gctx, (fz_xml *) $self)); + } + fz_catch(gctx) { + return NULL; + } + return (struct Xml*) ret; + } + + FITZEXCEPTION (append_child, !result) + PyObject *append_child( struct Xml* child) + { + fz_try(gctx) { + fz_dom_append_child( gctx, (fz_xml *) $self, (fz_xml *) child); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION (create_text_node, !result) + struct Xml* create_text_node( const char *text) + { + fz_xml* ret = NULL; + fz_try(gctx) { + ret = fz_dom_create_text_node( gctx,(fz_xml *) $self, text); + } + fz_catch(gctx) { + return NULL; + } + fz_keep_xml( gctx, ret); + return (struct Xml*) ret; + } + + FITZEXCEPTION (create_element, !result) + struct Xml* create_element( const char *tag) + { + fz_xml* ret = NULL; + fz_try(gctx) { + ret = fz_dom_create_element( gctx, (fz_xml *)$self, tag); + } + fz_catch(gctx) { + return NULL; + } + fz_keep_xml( gctx, ret); + return (struct Xml*) ret; + } + + struct Xml *find(const char *tag, const char *att, const char *match) + { + fz_xml* ret=NULL; + ret = fz_dom_find( gctx, (fz_xml *)$self, tag, att, match); + if (!ret) { + return NULL; + } + fz_keep_xml( gctx, ret); + return (struct Xml*) ret; + } + + struct Xml *find_next( const char *tag, const char *att, const char *match) + { + fz_xml* ret=NULL; + ret = fz_dom_find_next( gctx, (fz_xml *)$self, tag, att, match); + if (!ret) { + return NULL; + } + fz_keep_xml( gctx, ret); + return (struct Xml*) ret; + } + + %pythoncode %{@property%} + struct Xml *next() + { + fz_xml* ret=NULL; + ret = fz_dom_next( gctx, (fz_xml *)$self); + if (!ret) { + return NULL; + } + fz_keep_xml( gctx, ret); + return (struct Xml*) ret; + } + + %pythoncode %{@property%} + struct Xml *previous() + { + fz_xml* ret=NULL; + ret = fz_dom_previous( gctx, (fz_xml *)$self); + if (!ret) { + return NULL; + } + fz_keep_xml( gctx, ret); + return (struct Xml*) ret; + } + + FITZEXCEPTION (set_attribute, !result) + PyObject *set_attribute(const char *key, const char *value) + { + fz_try(gctx) { + if (strlen(key)==0) { + RAISEPY(gctx, "key must not be empty", PyExc_ValueError); + } + fz_dom_add_attribute(gctx, (fz_xml *)$self, key, value); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION (remove_attribute, !result) + PyObject *remove_attribute(const char *key) + { + fz_try(gctx) { + if (strlen(key)==0) { + RAISEPY(gctx, "key must not be empty", PyExc_ValueError); + } + fz_xml *elt = (fz_xml *)$self; + fz_dom_remove_attribute(gctx, elt, key); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION (get_attribute_value, !result) + PyObject *get_attribute_value(const char *key) + { + const char *ret=NULL; + fz_try(gctx) { + if (strlen(key)==0) { + RAISEPY(gctx, "key must not be empty", PyExc_ValueError); + } + fz_xml *elt = (fz_xml *)$self; + ret=fz_dom_attribute(gctx, elt, key); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("s", ret); + } + + + FITZEXCEPTION (get_attributes, !result) + PyObject *get_attributes() + { + fz_xml *this = (fz_xml *) $self; + if (fz_xml_text(this)) { // text node has none + Py_RETURN_NONE; + } + PyObject *result=PyDict_New(); + fz_try(gctx) { + int i=0; + const char *key=NULL; + const char *val=NULL; + while (1) { + val = fz_dom_get_attribute(gctx, this, i, &key); + if (!val || !key) { + break; + } + PyObject *temp = Py_BuildValue("s",val); + PyDict_SetItemString(result, key, temp); + Py_DECREF(temp); + i += 1; + } + } + fz_catch(gctx) { + Py_DECREF(result); + return NULL; + } + return result; + } + + + FITZEXCEPTION (insert_before, !result) + PyObject *insert_before(struct Xml *node) + { + fz_xml *existing = (fz_xml *) $self; + fz_xml *what = (fz_xml *) node; + fz_try(gctx) + { + fz_dom_insert_before(gctx, existing, what); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION (insert_after, !result) + PyObject *insert_after(struct Xml *node) + { + fz_xml *existing = (fz_xml *) $self; + fz_xml *what = (fz_xml *) node; + fz_try(gctx) + { + fz_dom_insert_after(gctx, existing, what); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION (clone, !result) + struct Xml* clone() + { + fz_xml* ret = NULL; + fz_try(gctx) { + ret = fz_dom_clone( gctx, (fz_xml *)$self); + } + fz_catch(gctx) { + return NULL; + } + fz_keep_xml( gctx, ret); + return (struct Xml*) ret; + } + + %pythoncode %{@property%} + struct Xml *parent() + { + fz_xml* ret = NULL; + ret = fz_dom_parent( gctx, (fz_xml *)$self); + if (!ret) { + return NULL; + } + fz_keep_xml( gctx, ret); + return (struct Xml*) ret; + } + + %pythoncode %{@property%} + struct Xml *first_child() + { + fz_xml* ret = NULL; + fz_xml *this = (fz_xml *)$self; + if (fz_xml_text(this)) { // a text node has no child + return NULL; + } + ret = fz_dom_first_child( gctx, (fz_xml *)$self); + if (!ret) { + return NULL; + } + fz_keep_xml( gctx, ret); + return (struct Xml*) ret; + } + + + FITZEXCEPTION (remove, !result) + PyObject *remove() + { + fz_try(gctx) { + fz_dom_remove( gctx, (fz_xml *)$self); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + %pythoncode %{@property%} + PyObject *text() + { + return Py_BuildValue("s", fz_xml_text((fz_xml *)$self)); + } + + %pythoncode %{@property%} + PyObject *tagname() + { + return Py_BuildValue("s", fz_xml_tag((fz_xml *)$self)); + } + + + %pythoncode %{ + def _get_node_tree(self): + def show_node(node, items, shift): + while node != None: + if node.is_text: + items.append((shift, f'"{node.text}"')) + node = node.next + continue + items.append((shift, f"({node.tagname}")) + for k, v in node.get_attributes().items(): + items.append((shift, f"={k} '{v}'")) + child = node.first_child + if child: + items = show_node(child, items, shift + 1) + items.append((shift, f"){node.tagname}")) + node = node.next + return items + + shift = 0 + items = [] + items = show_node(self, items, shift) + return items + + def debug(self): + """Print a list of the node tree below self.""" + items = self._get_node_tree() + for item in items: + print(" " * item[0] + item[1].replace("\n", "\\n")) + + @property + def is_text(self): + """Check if this is a text node.""" + return self.text != None + + @property + def last_child(self): + """Return last child node.""" + child = self.first_child + if child==None: + return None + while True: + if child.next == None: + return child + child = child.next + + @staticmethod + def color_text(color): + if type(color) is str: + return color + if type(color) is int: + return f"rgb({sRGB_to_rgb(color)})" + if type(color) in (tuple, list): + return f"rgb{tuple(color)}" + return color + + def add_number_list(self, start=1, numtype=None): + """Add numbered list ("ol" tag)""" + child = self.create_element("ol") + if start > 1: + child.set_attribute("start", str(start)) + if numtype != None: + child.set_attribute("type", numtype) + self.append_child(child) + return child + + def add_description_list(self): + """Add description list ("dl" tag)""" + child = self.create_element("dl") + self.append_child(child) + return child + + def add_image(self, name, width=None, height=None, imgfloat=None, align=None): + """Add image node (tag "img").""" + child = self.create_element("img") + if width != None: + child.set_attribute("width", f"{width}") + if height != None: + child.set_attribute("height", f"{height}") + if imgfloat != None: + child.set_attribute("style", f"float: {imgfloat}") + if align != None: + child.set_attribute("align", f"{align}") + child.set_attribute("src", f"{name}") + self.append_child(child) + return child + + def add_bullet_list(self): + """Add bulleted list ("ul" tag)""" + child = self.create_element("ul") + self.append_child(child) + return child + + def add_list_item(self): + """Add item ("li" tag) under a (numbered or bulleted) list.""" + if self.tagname not in ("ol", "ul"): + raise ValueError("cannot add list item to", self.tagname) + child = self.create_element("li") + self.append_child(child) + return child + + def add_span(self): + child = self.create_element("span") + self.append_child(child) + return child + + def add_paragraph(self): + """Add "p" tag""" + child = self.create_element("p") + if self.tagname != "p": + self.append_child(child) + else: + self.parent.append_child(child) + return child + + def add_header(self, level=1): + """Add header tag""" + if level not in range(1, 7): + raise ValueError("Header level must be in [1, 6]") + this_tag = self.tagname + new_tag = f"h{level}" + child = self.create_element(new_tag) + prev = self + if this_tag not in ("h1", "h2", "h3", "h4", "h5", "h6", "p"): + self.append_child(child) + return child + self.parent.append_child(child) + return child + + def add_division(self): + """Add "div" tag""" + child = self.create_element("div") + self.append_child(child) + return child + + def add_horizontal_line(self): + """Add horizontal line ("hr" tag)""" + child = self.create_element("hr") + self.append_child(child) + return child + + def add_link(self, href, text=None): + """Add a hyperlink ("a" tag)""" + child = self.create_element("a") + if not isinstance(text, str): + text = href + child.set_attribute("href", href) + child.append_child(self.create_text_node(text)) + prev = self.span_bottom() + if prev == None: + prev = self + prev.append_child(child) + return self + + def add_code(self, text=None): + """Add a "code" tag""" + child = self.create_element("code") + if type(text) is str: + child.append_child(self.create_text_node(text)) + prev = self.span_bottom() + if prev == None: + prev = self + prev.append_child(child) + return self + + add_var = add_code + add_samp = add_code + add_kbd = add_code + + def add_superscript(self, text=None): + """Add a superscript ("sup" tag)""" + child = self.create_element("sup") + if type(text) is str: + child.append_child(self.create_text_node(text)) + prev = self.span_bottom() + if prev == None: + prev = self + prev.append_child(child) + return self + + def add_subscript(self, text=None): + """Add a subscript ("sub" tag)""" + child = self.create_element("sub") + if type(text) is str: + child.append_child(self.create_text_node(text)) + prev = self.span_bottom() + if prev == None: + prev = self + prev.append_child(child) + return self + + def add_codeblock(self): + """Add monospaced lines ("pre" node)""" + child = self.create_element("pre") + self.append_child(child) + return child + + def span_bottom(self): + """Find deepest level in stacked spans.""" + parent = self + child = self.last_child + if child == None: + return None + while child.is_text: + child = child.previous + if child == None: + break + if child == None or child.tagname != "span": + return None + + while True: + if child == None: + return parent + if child.tagname in ("a", "sub","sup","body") or child.is_text: + child = child.next + continue + if child.tagname == "span": + parent = child + child = child.first_child + else: + return parent + + def append_styled_span(self, style): + span = self.create_element("span") + span.add_style(style) + prev = self.span_bottom() + if prev == None: + prev = self + prev.append_child(span) + return prev + + def set_margins(self, val): + """Set margin values via CSS style""" + text = "margins: %s" % val + self.append_styled_span(text) + return self + + def set_font(self, font): + """Set font-family name via CSS style""" + text = "font-family: %s" % font + self.append_styled_span(text) + return self + + def set_color(self, color): + """Set text color via CSS style""" + text = f"color: %s" % self.color_text(color) + self.append_styled_span(text) + return self + + def set_columns(self, cols): + """Set number of text columns via CSS style""" + text = f"columns: {cols}" + self.append_styled_span(text) + return self + + def set_bgcolor(self, color): + """Set background color via CSS style""" + text = f"background-color: %s" % self.color_text(color) + self.add_style(text) # does not work on span level + return self + + def set_opacity(self, opacity): + """Set opacity via CSS style""" + text = f"opacity: {opacity}" + self.append_styled_span(text) + return self + + def set_align(self, align): + """Set text alignment via CSS style""" + text = "text-align: %s" + if isinstance( align, str): + t = align + elif align == TEXT_ALIGN_LEFT: + t = "left" + elif align == TEXT_ALIGN_CENTER: + t = "center" + elif align == TEXT_ALIGN_RIGHT: + t = "right" + elif align == TEXT_ALIGN_JUSTIFY: + t = "justify" + else: + raise ValueError(f"Unrecognised align={align}") + text = text % t + self.add_style(text) + return self + + def set_underline(self, val="underline"): + text = "text-decoration: %s" % val + self.append_styled_span(text) + return self + + def set_pagebreak_before(self): + """Insert a page break before this node.""" + text = "page-break-before: always" + self.add_style(text) + return self + + def set_pagebreak_after(self): + """Insert a page break after this node.""" + text = "page-break-after: always" + self.add_style(text) + return self + + def set_fontsize(self, fontsize): + """Set font size name via CSS style""" + if type(fontsize) is str: + px="" + else: + px="px" + text = f"font-size: {fontsize}{px}" + self.append_styled_span(text) + return self + + def set_lineheight(self, lineheight): + """Set line height name via CSS style - block-level only.""" + text = f"line-height: {lineheight}" + self.add_style(text) + return self + + def set_leading(self, leading): + """Set inter-line spacing value via CSS style - block-level only.""" + text = f"-mupdf-leading: {leading}" + self.add_style(text) + return self + + def set_word_spacing(self, spacing): + """Set inter-word spacing value via CSS style""" + text = f"word-spacing: {spacing}" + self.append_styled_span(text) + return self + + def set_letter_spacing(self, spacing): + """Set inter-letter spacing value via CSS style""" + text = f"letter-spacing: {spacing}" + self.append_styled_span(text) + return self + + def set_text_indent(self, indent): + """Set text indentation name via CSS style - block-level only.""" + text = f"text-indent: {indent}" + self.add_style(text) + return self + + def set_bold(self, val=True): + """Set bold on / off via CSS style""" + if val: + val="bold" + else: + val="normal" + text = "font-weight: %s" % val + self.append_styled_span(text) + return self + + def set_italic(self, val=True): + """Set italic on / off via CSS style""" + if val: + val="italic" + else: + val="normal" + text = "font-style: %s" % val + self.append_styled_span(text) + return self + + def set_properties( + self, + align=None, + bgcolor=None, + bold=None, + color=None, + columns=None, + font=None, + fontsize=None, + indent=None, + italic=None, + leading=None, + letter_spacing=None, + lineheight=None, + margins=None, + pagebreak_after=None, + pagebreak_before=None, + word_spacing=None, + unqid=None, + cls=None, + ): + """Set any or all properties of a node. + + To be used for existing nodes preferrably. + """ + root = self.root + temp = root.add_division() + if align is not None: + temp.set_align(align) + if bgcolor is not None: + temp.set_bgcolor(bgcolor) + if bold is not None: + temp.set_bold(bold) + if color is not None: + temp.set_color(color) + if columns is not None: + temp.set_columns(columns) + if font is not None: + temp.set_font(font) + if fontsize is not None: + temp.set_fontsize(fontsize) + if indent is not None: + temp.set_text_indent(indent) + if italic is not None: + temp.set_italic(italic) + if leading is not None: + temp.set_leading(leading) + if letter_spacing is not None: + temp.set_letter_spacing(letter_spacing) + if lineheight is not None: + temp.set_lineheight(lineheight) + if margins is not None: + temp.set_margins(margins) + if pagebreak_after is not None: + temp.set_pagebreak_after() + if pagebreak_before is not None: + temp.set_pagebreak_before() + if word_spacing is not None: + temp.set_word_spacing(word_spacing) + if unqid is not None: + self.set_id(unqid) + if cls is not None: + self.add_class(cls) + + styles = [] + top_style = temp.get_attribute_value("style") + if top_style is not None: + styles.append(top_style) + child = temp.first_child + while child: + styles.append(child.get_attribute_value("style")) + child = child.first_child + self.set_attribute("style", ";".join(styles)) + temp.remove() + return self + + def set_id(self, unique): + """Set a unique id.""" + # check uniqueness + tagname = self.tagname + root = self.root + if root.find(None, "id", unique): + raise ValueError(f"id '{unique}' already exists") + self.set_attribute("id", unique) + return self + + def add_text(self, text): + """Add text. Line breaks are honored.""" + lines = text.splitlines() + line_count = len(lines) + prev = self.span_bottom() + if prev == None: + prev = self + + for i, line in enumerate(lines): + prev.append_child(self.create_text_node(line)) + if i < line_count - 1: + prev.append_child(self.create_element("br")) + return self + + def add_style(self, text): + """Set some style via CSS style. Replaces complete style spec.""" + style = self.get_attribute_value("style") + if style != None and text in style: + return self + self.remove_attribute("style") + if style == None: + style = text + else: + style += ";" + text + self.set_attribute("style", style) + return self + + def add_class(self, text): + """Set some class via CSS. Replaces complete class spec.""" + cls = self.get_attribute_value("class") + if cls != None and text in cls: + return self + self.remove_attribute("class") + if cls == None: + cls = text + else: + cls += " " + text + self.set_attribute("class", cls) + return self + + def insert_text(self, text): + lines = text.splitlines() + line_count = len(lines) + for i, line in enumerate(lines): + self.append_child(self.create_text_node(line)) + if i < line_count - 1: + self.append_child(self.create_element("br")) + return self + + def __enter__(self): + return self + + def __exit__(self, *args): + pass + + def __del__(self): + if not type(self) is Xml: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; + +//------------------------------------------------------------------------ +// Story +//------------------------------------------------------------------------ +struct Story +{ + %extend + { + ~Story() + { + DEBUGMSG1("Story"); + fz_story *this_story = (fz_story *) $self; + fz_drop_story(gctx, this_story); + DEBUGMSG2; + } + + FITZEXCEPTION(Story, !result) + %pythonprepend Story %{ + if archive != None and isinstance(archive, Archive) == False: + archive = Archive(archive) + %} + Story(const char* html=NULL, const char *user_css=NULL, double em=12, struct Archive *archive=NULL) + { + fz_story* story = NULL; + fz_buffer *buffer = NULL; + fz_archive* arch = NULL; + fz_var(story); + fz_var(buffer); + const char *html2=""; + if (html) { + html2=html; + } + + fz_try(gctx) + { + buffer = fz_new_buffer_from_copied_data(gctx, html2, strlen(html2)+1); + if (archive) { + arch = (fz_archive *) archive; + } + story = fz_new_story(gctx, buffer, user_css, em, arch); + } + fz_always(gctx) + { + fz_drop_buffer(gctx, buffer); + } + fz_catch(gctx) + { + return NULL; + } + struct Story* ret = (struct Story *) story; + return ret; + } + + FITZEXCEPTION(reset, !result) + PyObject* reset() + { + fz_try(gctx) + { + fz_reset_story(gctx, (fz_story *)$self); + } + fz_catch(gctx) + { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION(place, !result) + PyObject* place( PyObject* where) + { + PyObject* ret = NULL; + fz_try(gctx) + { + fz_rect where2 = JM_rect_from_py(where); + fz_rect filled; + int more = fz_place_story( gctx, (fz_story*) $self, where2, &filled); + ret = PyTuple_New(2); + PyTuple_SET_ITEM( ret, 0, Py_BuildValue( "i", more)); + PyTuple_SET_ITEM( ret, 1, JM_py_from_rect( filled)); + } + fz_catch(gctx) + { + return NULL; + } + return ret; + } + + FITZEXCEPTION(draw, !result) + PyObject* draw( struct DeviceWrapper* device, PyObject* matrix=NULL) + { + fz_try(gctx) + { + fz_matrix ctm2 = JM_matrix_from_py( matrix); + fz_device *dev = (device) ? device->device : NULL; + fz_draw_story( gctx, (fz_story*) $self, dev, ctm2); + } + fz_catch(gctx) + { + return NULL; + } + Py_RETURN_NONE; + } + + FITZEXCEPTION(document, !result) + struct Xml* document() + { + fz_xml* dom=NULL; + fz_try(gctx) { + dom = fz_story_document( gctx, (fz_story*) $self); + } + fz_catch(gctx) { + return NULL; + } + fz_keep_xml( gctx, dom); + return (struct Xml*) dom; + } + + FITZEXCEPTION(element_positions, !result) + %pythonprepend element_positions %{ + """Trigger a callback function to record where items have been placed. + + Args: + function: a function accepting exactly one argument. + args: an optional dictionary for passing additional data. + """ + if type(args) is dict: + for k in args.keys(): + if not (type(k) is str and k.isidentifier()): + raise ValueError(f"invalid key '{k}'") + else: + args = {} + if not callable(function) or function.__code__.co_argcount != 1: + raise ValueError("callback 'function' must be a callable with exactly one argument") + %} + PyObject* element_positions(PyObject *function, PyObject *args) + { + PyObject *callarg=NULL; + fz_try(gctx) { + callarg = Py_BuildValue("OO", function, args); + fz_story_positions(gctx, (fz_story *) $self, Story_Callback, callarg); + } + fz_always(gctx) { + Py_CLEAR(callarg); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + %pythoncode + %{ + def write(self, writer, rectfn, positionfn=None, pagefn=None): + dev = None + page_num = 0 + rect_num = 0 + filled = Rect(0, 0, 0, 0) + while 1: + mediabox, rect, ctm = rectfn(rect_num, filled) + rect_num += 1 + if mediabox: + # new page. + page_num += 1 + more, filled = self.place( rect) + #print(f"write(): positionfn={positionfn}") + if positionfn: + def positionfn2(position): + # We add a `.page_num` member to the + # `ElementPosition` instance. + position.page_num = page_num + #print(f"write(): position={position}") + positionfn(position) + self.element_positions(positionfn2, {}) + if writer: + if mediabox: + # new page. + if dev: + if pagefn: + pagefn(page_num, medibox, dev, 1) + writer.end_page() + dev = writer.begin_page( mediabox) + if pagefn: + pagefn(page_num, mediabox, dev, 0) + self.draw( dev, ctm) + if not more: + if pagefn: + pagefn( page_num, mediabox, dev, 1) + writer.end_page() + else: + self.draw(None, ctm) + if not more: + break + + @staticmethod + def write_stabilized(writer, contentfn, rectfn, user_css=None, em=12, positionfn=None, pagefn=None, archive=None, add_header_ids=True): + positions = list() + content = None + # Iterate until stable. + while 1: + content_prev = content + content = contentfn( positions) + stable = False + if content == content_prev: + stable = True + content2 = content + story = Story(content2, user_css, em, archive) + + if add_header_ids: + story.add_header_ids() + + positions = list() + def positionfn2(position): + #print(f"write_stabilized(): stable={stable} positionfn={positionfn} position={position}") + positions.append(position) + if stable and positionfn: + positionfn(position) + story.write( + writer if stable else None, + rectfn, + positionfn2, + pagefn, + ) + if stable: + break + + def add_header_ids(self): + ''' + Look for `` items in `self` and adds unique `id` + attributes if not already present. + ''' + dom = self.body + i = 0 + x = dom.find(None, None, None) + while x: + name = x.tagname + if len(name) == 2 and name[0]=="h" and name[1] in "123456": + attr = x.get_attribute_value("id") + if not attr: + id_ = f"h_id_{i}" + #print(f"name={name}: setting id={id_}") + x.set_attribute("id", id_) + i += 1 + x = x.find_next(None, None, None) + + def write_with_links(self, rectfn, positionfn=None, pagefn=None): + #print("write_with_links()") + stream = io.BytesIO() + writer = DocumentWriter(stream) + positions = [] + def positionfn2(position): + #print(f"write_with_links(): position={position}") + positions.append(position) + if positionfn: + positionfn(position) + self.write(writer, rectfn, positionfn=positionfn2, pagefn=pagefn) + writer.close() + stream.seek(0) + return Story.add_pdf_links(stream, positions) + + @staticmethod + def write_stabilized_with_links(contentfn, rectfn, user_css=None, em=12, positionfn=None, pagefn=None, archive=None, add_header_ids=True): + #print("write_stabilized_with_links()") + stream = io.BytesIO() + writer = DocumentWriter(stream) + positions = [] + def positionfn2(position): + #print(f"write_stabilized_with_links(): position={position}") + positions.append(position) + if positionfn: + positionfn(position) + Story.write_stabilized(writer, contentfn, rectfn, user_css, em, positionfn2, pagefn, archive, add_header_ids) + writer.close() + stream.seek(0) + return Story.add_pdf_links(stream, positions) + + @staticmethod + def add_pdf_links(document_or_stream, positions): + """ + Adds links to PDF document. + Args: + document_or_stream: + A PDF `Document` or raw PDF content, for example an + `io.BytesIO` instance. + positions: + List of `ElementPosition`'s for `document_or_stream`, + typically from Story.element_positions(). We raise an + exception if two or more positions have same id. + Returns: + `document_or_stream` if a `Document` instance, otherwise a + new `Document` instance. + We raise an exception if an `href` in `positions` refers to an + internal position `#` but no item in `postions` has `id = + name`. + """ + if isinstance(document_or_stream, Document): + document = document_or_stream + else: + document = Document("pdf", document_or_stream) + + # Create dict from id to position, which we will use to find + # link destinations. + # + id_to_position = dict() + #print(f"positions: {positions}") + for position in positions: + #print(f"add_pdf_links(): position: {position}") + if (position.open_close & 1) and position.id: + #print(f"add_pdf_links(): position with id: {position}") + if position.id in id_to_position: + #print(f"Ignoring duplicate positions with id={position.id!r}") + pass + else: + id_to_position[ position.id] = position + + # Insert links for all positions that have an `href` starting + # with '#'. + # + for position_from in positions: + if ((position_from.open_close & 1) + and position_from.href + and position_from.href.startswith("#") + ): + # This is a `...` internal link. + #print(f"add_pdf_links(): position with href: {position}") + target_id = position_from.href[1:] + try: + position_to = id_to_position[ target_id] + except Exception as e: + raise RuntimeError(f"No destination with id={target_id}, required by position_from: {position_from}") + # Make link from `position_from`'s rect to top-left of + # `position_to`'s rect. + if 0: + print(f"add_pdf_links(): making link from:") + print(f"add_pdf_links(): {position_from}") + print(f"add_pdf_links(): to:") + print(f"add_pdf_links(): {position_to}") + link = dict() + link["kind"] = LINK_GOTO + link["from"] = Rect(position_from.rect) + x0, y0, x1, y1 = position_to.rect + # This appears to work well with viewers which scroll + # to make destination point top-left of window. + link["to"] = Point(x0, y0) + link["page"] = position_to.page_num - 1 + document[position_from.page_num - 1].insert_link(link) + return document + + @property + def body(self): + dom = self.document() + return dom.bodytag() + + def __del__(self): + if not type(self) is Story: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; + + +//------------------------------------------------------------------------ +// Tools - a collection of tools and utilities +//------------------------------------------------------------------------ +struct Tools +{ + %extend + { + Tools() + { + /* It looks like global objects are never destructed when running + with SWIG, so we use Memento_startLeaking()/Memento_stopLeaking(). + */ + Memento_startLeaking(); + void* p = malloc( sizeof(struct Tools)); + Memento_stopLeaking(); + //fprintf(stderr, "Tools constructor p=%p\n", p); + return (struct Tools*) p; + } + + ~Tools() + { + /* This is not called. */ + struct Tools* p = (struct Tools*) $self; + //fprintf(stderr, "~Tools() p=%p\n", p); + free(p); + } + + %pythonprepend gen_id + %{"""Return a unique positive integer."""%} + PyObject *gen_id() + { + JM_UNIQUE_ID += 1; + if (JM_UNIQUE_ID < 0) JM_UNIQUE_ID = 1; + return Py_BuildValue("i", JM_UNIQUE_ID); + } + + + FITZEXCEPTION(set_icc, !result) + %pythonprepend set_icc + %{"""Set ICC color handling on or off."""%} + PyObject *set_icc(int on=0) + { + fz_try(gctx) { + if (on) { + if (FZ_ENABLE_ICC) + fz_enable_icc(gctx); + else { + RAISEPY(gctx, "MuPDF built w/o ICC support",PyExc_ValueError); + } + } else if (FZ_ENABLE_ICC) { + fz_disable_icc(gctx); + } + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + %pythonprepend set_annot_stem + %{"""Get / set id prefix for annotations."""%} + char *set_annot_stem(char *stem=NULL) + { + if (!stem) { + return JM_annot_id_stem; + } + size_t len = strlen(stem) + 1; + if (len > 50) len = 50; + memcpy(&JM_annot_id_stem, stem, len); + return JM_annot_id_stem; + } + + + %pythonprepend set_small_glyph_heights + %{"""Set / unset small glyph heights."""%} + PyObject *set_small_glyph_heights(PyObject *on=NULL) + { + if (!on || on == Py_None) { + return JM_BOOL(small_glyph_heights); + } + if (PyObject_IsTrue(on)) { + small_glyph_heights = 1; + } else { + small_glyph_heights = 0; + } + return JM_BOOL(small_glyph_heights); + } + + + %pythonprepend set_subset_fontnames + %{"""Set / unset returning fontnames with their subset prefix."""%} + PyObject *set_subset_fontnames(PyObject *on=NULL) + { + if (!on || on == Py_None) { + return JM_BOOL(subset_fontnames); + } + if (PyObject_IsTrue(on)) { + subset_fontnames = 1; + } else { + subset_fontnames = 0; + } + return JM_BOOL(subset_fontnames); + } + + + %pythonprepend set_low_memory + %{"""Set / unset MuPDF device caching."""%} + PyObject *set_low_memory(PyObject *on=NULL) + { + if (!on || on == Py_None) { + return JM_BOOL(no_device_caching); + } + if (PyObject_IsTrue(on)) { + no_device_caching = 1; + } else { + no_device_caching = 0; + } + return JM_BOOL(no_device_caching); + } + + + %pythonprepend unset_quad_corrections + %{"""Set ascender / descender corrections on or off."""%} + PyObject *unset_quad_corrections(PyObject *on=NULL) + { + if (!on || on == Py_None) { + return JM_BOOL(skip_quad_corrections); + } + if (PyObject_IsTrue(on)) { + skip_quad_corrections = 1; + } else { + skip_quad_corrections = 0; + } + return JM_BOOL(skip_quad_corrections); + } + + + %pythonprepend store_shrink + %{"""Free 'percent' of current store size."""%} + PyObject *store_shrink(int percent) + { + if (percent >= 100) { + fz_empty_store(gctx); + return Py_BuildValue("i", 0); + } + if (percent > 0) fz_shrink_store(gctx, 100 - percent); + return Py_BuildValue("i", (int) gctx->store->size); + } + + + %pythoncode%{@property%} + %pythonprepend store_size + %{"""MuPDF current store size."""%} + PyObject *store_size() + { + return Py_BuildValue("i", (int) gctx->store->size); + } + + + %pythoncode%{@property%} + %pythonprepend store_maxsize + %{"""MuPDF store size limit."""%} + PyObject *store_maxsize() + { + return Py_BuildValue("i", (int) gctx->store->max); + } + + + %pythonprepend show_aa_level + %{"""Show anti-aliasing values."""%} + %pythonappend show_aa_level %{ + temp = {"graphics": val[0], "text": val[1], "graphics_min_line_width": val[2]} + val = temp%} + PyObject *show_aa_level() + { + return Py_BuildValue("iif", + fz_graphics_aa_level(gctx), + fz_text_aa_level(gctx), + fz_graphics_min_line_width(gctx)); + } + + + %pythonprepend set_aa_level + %{"""Set anti-aliasing level."""%} + void set_aa_level(int level) + { + fz_set_aa_level(gctx, level); + } + + + %pythonprepend set_graphics_min_line_width + %{"""Set the graphics minimum line width."""%} + void set_graphics_min_line_width(float min_line_width) + { + fz_set_graphics_min_line_width(gctx, min_line_width); + } + + + FITZEXCEPTION(image_profile, !result) + %pythonprepend image_profile + %{"""Metadata of an image binary stream."""%} + PyObject *image_profile(PyObject *stream, int keep_image=0) + { + PyObject *rc = NULL; + fz_try(gctx) { + rc = JM_image_profile(gctx, stream, keep_image); + } + fz_catch(gctx) { + return NULL; + } + return rc; + } + + + PyObject *_rotate_matrix(struct Page *page) + { + pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page); + if (!pdfpage) return JM_py_from_matrix(fz_identity); + return JM_py_from_matrix(JM_rotate_page_matrix(gctx, pdfpage)); + } + + + PyObject *_derotate_matrix(struct Page *page) + { + pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page); + if (!pdfpage) return JM_py_from_matrix(fz_identity); + return JM_py_from_matrix(JM_derotate_page_matrix(gctx, pdfpage)); + } + + + %pythoncode%{@property%} + %pythonprepend fitz_config + %{"""PyMuPDF configuration parameters."""%} + PyObject *fitz_config() + { + return JM_fitz_config(); + } + + + %pythonprepend glyph_cache_empty + %{"""Empty the glyph cache."""%} + void glyph_cache_empty() + { + fz_purge_glyph_cache(gctx); + } + + + FITZEXCEPTION(_fill_widget, !result) + %pythonappend _fill_widget %{ + widget.rect = Rect(annot.rect) + widget.xref = annot.xref + widget.parent = annot.parent + widget._annot = annot # backpointer to annot object + if not widget.script: + widget.script = None + if not widget.script_stroke: + widget.script_stroke = None + if not widget.script_format: + widget.script_format = None + if not widget.script_change: + widget.script_change = None + if not widget.script_calc: + widget.script_calc = None + %} + PyObject *_fill_widget(struct Annot *annot, PyObject *widget) + { + fz_try(gctx) { + JM_get_widget_properties(gctx, (pdf_annot *) annot, widget); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(_save_widget, !result) + PyObject *_save_widget(struct Annot *annot, PyObject *widget) + { + fz_try(gctx) { + JM_set_widget_properties(gctx, (pdf_annot *) annot, widget); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(_reset_widget, !result) + PyObject *_reset_widget(struct Annot *annot) + { + fz_try(gctx) { + pdf_annot *this_annot = (pdf_annot *) annot; + pdf_obj *this_annot_obj = pdf_annot_obj(gctx, this_annot); + pdf_document *pdf = pdf_get_bound_document(gctx, this_annot_obj); + pdf_field_reset(gctx, pdf, this_annot_obj); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + // Ensure that widgets with a /AA/C JavaScript are in AcroForm/CO + FITZEXCEPTION(_ensure_widget_calc, !result) + PyObject *_ensure_widget_calc(struct Annot *annot) + { + pdf_obj *PDFNAME_CO=NULL; + fz_try(gctx) { + pdf_obj *annot_obj = pdf_annot_obj(gctx, (pdf_annot *) annot); + pdf_document *pdf = pdf_get_bound_document(gctx, annot_obj); + PDFNAME_CO = pdf_new_name(gctx, "CO"); // = PDF_NAME(CO) + pdf_obj *acro = pdf_dict_getl(gctx, // get AcroForm dict + pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(AcroForm), + NULL); + + pdf_obj *CO = pdf_dict_get(gctx, acro, PDFNAME_CO); // = AcroForm/CO + if (!CO) { + CO = pdf_dict_put_array(gctx, acro, PDFNAME_CO, 2); + } + int i, n = pdf_array_len(gctx, CO); + int xref, nxref, found = 0; + xref = pdf_to_num(gctx, annot_obj); + for (i = 0; i < n; i++) { + nxref = pdf_to_num(gctx, pdf_array_get(gctx, CO, i)); + if (xref == nxref) { + found = 1; + break; + } + } + if (!found) { + pdf_array_push_drop(gctx, CO, pdf_new_indirect(gctx, pdf, xref, 0)); + } + } + fz_always(gctx) { + pdf_drop_obj(gctx, PDFNAME_CO); + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(_parse_da, !result) + %pythonappend _parse_da %{ + if not val: + return ((0,), "", 0) + font = "Helv" + fsize = 12 + col = (0, 0, 0) + dat = val.split() # split on any whitespace + for i, item in enumerate(dat): + if item == "Tf": + font = dat[i - 2][1:] + fsize = float(dat[i - 1]) + dat[i] = dat[i-1] = dat[i-2] = "" + continue + if item == "g": # unicolor text + col = [(float(dat[i - 1]))] + dat[i] = dat[i-1] = "" + continue + if item == "rg": # RGB colored text + col = [float(f) for f in dat[i - 3:i]] + dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = "" + continue + if item == "k": # CMYK colored text + col = [float(f) for f in dat[i - 4:i]] + dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = dat[i-4] = "" + continue + + val = (col, font, fsize) + %} + PyObject *_parse_da(struct Annot *annot) + { + char *da_str = NULL; + pdf_annot *this_annot = (pdf_annot *) annot; + pdf_obj *this_annot_obj = pdf_annot_obj(gctx, this_annot); + pdf_document *pdf = pdf_get_bound_document(gctx, this_annot_obj); + fz_try(gctx) { + pdf_obj *da = pdf_dict_get_inheritable(gctx, this_annot_obj, + PDF_NAME(DA)); + if (!da) { + pdf_obj *trailer = pdf_trailer(gctx, pdf); + da = pdf_dict_getl(gctx, trailer, PDF_NAME(Root), + PDF_NAME(AcroForm), + PDF_NAME(DA), + NULL); + } + da_str = (char *) pdf_to_text_string(gctx, da); + } + fz_catch(gctx) { + return NULL; + } + return JM_UnicodeFromStr(da_str); + } + + + FITZEXCEPTION(_update_da, !result) + PyObject *_update_da(struct Annot *annot, char *da_str) + { + fz_try(gctx) { + pdf_annot *this_annot = (pdf_annot *) annot; + pdf_obj *this_annot_obj = pdf_annot_obj(gctx, this_annot); + pdf_dict_put_text_string(gctx, this_annot_obj, PDF_NAME(DA), da_str); + pdf_dict_del(gctx, this_annot_obj, PDF_NAME(DS)); /* not supported */ + pdf_dict_del(gctx, this_annot_obj, PDF_NAME(RC)); /* not supported */ + } + fz_catch(gctx) { + return NULL; + } + Py_RETURN_NONE; + } + + + FITZEXCEPTION(_get_all_contents, !result) + %pythonprepend _get_all_contents + %{"""Concatenate all /Contents objects of a page into a bytes object."""%} + PyObject *_get_all_contents(struct Page *fzpage) + { + pdf_page *page = pdf_page_from_fz_page(gctx, (fz_page *) fzpage); + fz_buffer *res = NULL; + PyObject *result = NULL; + fz_try(gctx) { + ASSERT_PDF(page); + res = JM_read_contents(gctx, page->obj); + result = JM_BinFromBuffer(gctx, res); + } + fz_always(gctx) { + fz_drop_buffer(gctx, res); + } + fz_catch(gctx) { + return NULL; + } + return result; + } + + + FITZEXCEPTION(_insert_contents, !result) + %pythonprepend _insert_contents + %{"""Add bytes as a new /Contents object for a page, and return its xref."""%} + PyObject *_insert_contents(struct Page *page, PyObject *newcont, int overlay=1) + { + fz_buffer *contbuf = NULL; + int xref = 0; + pdf_page *pdfpage = pdf_page_from_fz_page(gctx, (fz_page *) page); + fz_try(gctx) { + ASSERT_PDF(pdfpage); + ENSURE_OPERATION(gctx, pdfpage->doc); + contbuf = JM_BufferFromBytes(gctx, newcont); + xref = JM_insert_contents(gctx, pdfpage->doc, pdfpage->obj, contbuf, overlay); + } + fz_always(gctx) { + fz_drop_buffer(gctx, contbuf); + } + fz_catch(gctx) { + return NULL; + } + return Py_BuildValue("i", xref); + } + + %pythonprepend mupdf_version + %{"""Get version of MuPDF binary build."""%} + PyObject *mupdf_version() + { + return Py_BuildValue("s", FZ_VERSION); + } + + %pythonprepend mupdf_warnings + %{"""Get the MuPDF warnings/errors with optional reset (default)."""%} + %pythonappend mupdf_warnings %{ + val = "\n".join(val) + if reset: + self.reset_mupdf_warnings()%} + PyObject *mupdf_warnings(int reset=1) + { + Py_INCREF(JM_mupdf_warnings_store); + return JM_mupdf_warnings_store; + } + + int _int_from_language(char *language) + { + return fz_text_language_from_string(language); + } + + %pythonprepend reset_mupdf_warnings + %{"""Empty the MuPDF warnings/errors store."""%} + void reset_mupdf_warnings() + { + Py_CLEAR(JM_mupdf_warnings_store); + JM_mupdf_warnings_store = PyList_New(0); + } + + %pythonprepend mupdf_display_errors + %{"""Set MuPDF error display to True or False."""%} + PyObject *mupdf_display_errors(PyObject *on=NULL) + { + if (!on || on == Py_None) { + return JM_BOOL(JM_mupdf_show_errors); + } + if (PyObject_IsTrue(on)) { + JM_mupdf_show_errors = 1; + } else { + JM_mupdf_show_errors = 0; + } + return JM_BOOL(JM_mupdf_show_errors); + } + + %pythonprepend mupdf_display_warnings + %{"""Set MuPDF warnings display to True or False."""%} + PyObject *mupdf_display_warnings(PyObject *on=NULL) + { + if (!on || on == Py_None) { + return JM_BOOL(JM_mupdf_show_warnings); + } + if (PyObject_IsTrue(on)) { + JM_mupdf_show_warnings = 1; + } else { + JM_mupdf_show_warnings = 0; + } + return JM_BOOL(JM_mupdf_show_warnings); + } + + %pythoncode %{ +def _le_annot_parms(self, annot, p1, p2, fill_color): + """Get common parameters for making annot line end symbols. + + Returns: + m: matrix that maps p1, p2 to points L, P on the x-axis + im: its inverse + L, P: transformed p1, p2 + w: line width + scol: stroke color string + fcol: fill color store_shrink + opacity: opacity string (gs command) + """ + w = annot.border["width"] # line width + sc = annot.colors["stroke"] # stroke color + if not sc: # black if missing + sc = (0,0,0) + scol = " ".join(map(str, sc)) + " RG\n" + if fill_color: + fc = fill_color + else: + fc = annot.colors["fill"] # fill color + if not fc: + fc = (1,1,1) # white if missing + fcol = " ".join(map(str, fc)) + " rg\n" + # nr = annot.rect + np1 = p1 # point coord relative to annot rect + np2 = p2 # point coord relative to annot rect + m = Matrix(util_hor_matrix(np1, np2)) # matrix makes the line horizontal + im = ~m # inverted matrix + L = np1 * m # converted start (left) point + R = np2 * m # converted end (right) point + if 0 <= annot.opacity < 1: + opacity = "/H gs\n" + else: + opacity = "" + return m, im, L, R, w, scol, fcol, opacity + +def _oval_string(self, p1, p2, p3, p4): + """Return /AP string defining an oval within a 4-polygon provided as points + """ + def bezier(p, q, r): + f = "%f %f %f %f %f %f c\n" + return f % (p.x, p.y, q.x, q.y, r.x, r.y) + + kappa = 0.55228474983 # magic number + ml = p1 + (p4 - p1) * 0.5 # middle points ... + mo = p1 + (p2 - p1) * 0.5 # for each ... + mr = p2 + (p3 - p2) * 0.5 # polygon ... + mu = p4 + (p3 - p4) * 0.5 # side + ol1 = ml + (p1 - ml) * kappa # the 8 bezier + ol2 = mo + (p1 - mo) * kappa # helper points + or1 = mo + (p2 - mo) * kappa + or2 = mr + (p2 - mr) * kappa + ur1 = mr + (p3 - mr) * kappa + ur2 = mu + (p3 - mu) * kappa + ul1 = mu + (p4 - mu) * kappa + ul2 = ml + (p4 - ml) * kappa + # now draw, starting from middle point of left side + ap = "%f %f m\n" % (ml.x, ml.y) + ap += bezier(ol1, ol2, mo) + ap += bezier(or1, or2, mr) + ap += bezier(ur1, ur2, mu) + ap += bezier(ul1, ul2, ml) + return ap + +def _le_diamond(self, annot, p1, p2, lr, fill_color): + """Make stream commands for diamond line end symbol. "lr" denotes left (False) or right point. + """ + m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color) + shift = 2.5 # 2*shift*width = length of square edge + d = shift * max(1, w) + M = R - (d/2., 0) if lr else L + (d/2., 0) + r = Rect(M, M) + (-d, -d, d, d) # the square + # the square makes line longer by (2*shift - 1)*width + p = (r.tl + (r.bl - r.tl) * 0.5) * im + ap = "q\n%s%f %f m\n" % (opacity, p.x, p.y) + p = (r.tl + (r.tr - r.tl) * 0.5) * im + ap += "%f %f l\n" % (p.x, p.y) + p = (r.tr + (r.br - r.tr) * 0.5) * im + ap += "%f %f l\n" % (p.x, p.y) + p = (r.br + (r.bl - r.br) * 0.5) * im + ap += "%f %f l\n" % (p.x, p.y) + ap += "%g w\n" % w + ap += scol + fcol + "b\nQ\n" + return ap + +def _le_square(self, annot, p1, p2, lr, fill_color): + """Make stream commands for square line end symbol. "lr" denotes left (False) or right point. + """ + m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color) + shift = 2.5 # 2*shift*width = length of square edge + d = shift * max(1, w) + M = R - (d/2., 0) if lr else L + (d/2., 0) + r = Rect(M, M) + (-d, -d, d, d) # the square + # the square makes line longer by (2*shift - 1)*width + p = r.tl * im + ap = "q\n%s%f %f m\n" % (opacity, p.x, p.y) + p = r.tr * im + ap += "%f %f l\n" % (p.x, p.y) + p = r.br * im + ap += "%f %f l\n" % (p.x, p.y) + p = r.bl * im + ap += "%f %f l\n" % (p.x, p.y) + ap += "%g w\n" % w + ap += scol + fcol + "b\nQ\n" + return ap + +def _le_circle(self, annot, p1, p2, lr, fill_color): + """Make stream commands for circle line end symbol. "lr" denotes left (False) or right point. + """ + m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color) + shift = 2.5 # 2*shift*width = length of square edge + d = shift * max(1, w) + M = R - (d/2., 0) if lr else L + (d/2., 0) + r = Rect(M, M) + (-d, -d, d, d) # the square + ap = "q\n" + opacity + self._oval_string(r.tl * im, r.tr * im, r.br * im, r.bl * im) + ap += "%g w\n" % w + ap += scol + fcol + "b\nQ\n" + return ap + +def _le_butt(self, annot, p1, p2, lr, fill_color): + """Make stream commands for butt line end symbol. "lr" denotes left (False) or right point. + """ + m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color) + shift = 3 + d = shift * max(1, w) + M = R if lr else L + top = (M + (0, -d/2.)) * im + bot = (M + (0, d/2.)) * im + ap = "\nq\n%s%f %f m\n" % (opacity, top.x, top.y) + ap += "%f %f l\n" % (bot.x, bot.y) + ap += "%g w\n" % w + ap += scol + "s\nQ\n" + return ap + +def _le_slash(self, annot, p1, p2, lr, fill_color): + """Make stream commands for slash line end symbol. "lr" denotes left (False) or right point. + """ + m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color) + rw = 1.1547 * max(1, w) * 1.0 # makes rect diagonal a 30 deg inclination + M = R if lr else L + r = Rect(M.x - rw, M.y - 2 * w, M.x + rw, M.y + 2 * w) + top = r.tl * im + bot = r.br * im + ap = "\nq\n%s%f %f m\n" % (opacity, top.x, top.y) + ap += "%f %f l\n" % (bot.x, bot.y) + ap += "%g w\n" % w + ap += scol + "s\nQ\n" + return ap + +def _le_openarrow(self, annot, p1, p2, lr, fill_color): + """Make stream commands for open arrow line end symbol. "lr" denotes left (False) or right point. + """ + m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color) + shift = 2.5 + d = shift * max(1, w) + p2 = R + (d/2., 0) if lr else L - (d/2., 0) + p1 = p2 + (-2*d, -d) if lr else p2 + (2*d, -d) + p3 = p2 + (-2*d, d) if lr else p2 + (2*d, d) + p1 *= im + p2 *= im + p3 *= im + ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y) + ap += "%f %f l\n" % (p2.x, p2.y) + ap += "%f %f l\n" % (p3.x, p3.y) + ap += "%g w\n" % w + ap += scol + "S\nQ\n" + return ap + +def _le_closedarrow(self, annot, p1, p2, lr, fill_color): + """Make stream commands for closed arrow line end symbol. "lr" denotes left (False) or right point. + """ + m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color) + shift = 2.5 + d = shift * max(1, w) + p2 = R + (d/2., 0) if lr else L - (d/2., 0) + p1 = p2 + (-2*d, -d) if lr else p2 + (2*d, -d) + p3 = p2 + (-2*d, d) if lr else p2 + (2*d, d) + p1 *= im + p2 *= im + p3 *= im + ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y) + ap += "%f %f l\n" % (p2.x, p2.y) + ap += "%f %f l\n" % (p3.x, p3.y) + ap += "%g w\n" % w + ap += scol + fcol + "b\nQ\n" + return ap + +def _le_ropenarrow(self, annot, p1, p2, lr, fill_color): + """Make stream commands for right open arrow line end symbol. "lr" denotes left (False) or right point. + """ + m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color) + shift = 2.5 + d = shift * max(1, w) + p2 = R - (d/3., 0) if lr else L + (d/3., 0) + p1 = p2 + (2*d, -d) if lr else p2 + (-2*d, -d) + p3 = p2 + (2*d, d) if lr else p2 + (-2*d, d) + p1 *= im + p2 *= im + p3 *= im + ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y) + ap += "%f %f l\n" % (p2.x, p2.y) + ap += "%f %f l\n" % (p3.x, p3.y) + ap += "%g w\n" % w + ap += scol + fcol + "S\nQ\n" + return ap + +def _le_rclosedarrow(self, annot, p1, p2, lr, fill_color): + """Make stream commands for right closed arrow line end symbol. "lr" denotes left (False) or right point. + """ + m, im, L, R, w, scol, fcol, opacity = self._le_annot_parms(annot, p1, p2, fill_color) + shift = 2.5 + d = shift * max(1, w) + p2 = R - (2*d, 0) if lr else L + (2*d, 0) + p1 = p2 + (2*d, -d) if lr else p2 + (-2*d, -d) + p3 = p2 + (2*d, d) if lr else p2 + (-2*d, d) + p1 *= im + p2 *= im + p3 *= im + ap = "\nq\n%s%f %f m\n" % (opacity, p1.x, p1.y) + ap += "%f %f l\n" % (p2.x, p2.y) + ap += "%f %f l\n" % (p3.x, p3.y) + ap += "%g w\n" % w + ap += scol + fcol + "b\nQ\n" + return ap + +def __del__(self): + if not type(self) is Tools: + return + if getattr(self, "thisown", False): + self.__swig_destroy__(self) + %} + } +}; diff --git a/fitz/helper-annot.i b/fitz/helper-annot.i new file mode 100644 index 0000000..d23392e --- /dev/null +++ b/fitz/helper-annot.i @@ -0,0 +1,455 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +//------------------------------------------------------------------------ +// return pdf_obj "border style" from Python str +//------------------------------------------------------------------------ +pdf_obj *JM_get_border_style(fz_context *ctx, PyObject *style) +{ + pdf_obj *val = PDF_NAME(S); + if (!style) return val; + char *s = JM_StrAsChar(style); + JM_PyErr_Clear; + if (!s) return val; + if (!strncmp(s, "b", 1) || !strncmp(s, "B", 1)) val = PDF_NAME(B); + else if (!strncmp(s, "d", 1) || !strncmp(s, "D", 1)) val = PDF_NAME(D); + else if (!strncmp(s, "i", 1) || !strncmp(s, "I", 1)) val = PDF_NAME(I); + else if (!strncmp(s, "u", 1) || !strncmp(s, "U", 1)) val = PDF_NAME(U); + else if (!strncmp(s, "s", 1) || !strncmp(s, "S", 1)) val = PDF_NAME(S); + return val; +} + +//------------------------------------------------------------------------ +// Make /DA string of annotation +//------------------------------------------------------------------------ +const char *JM_expand_fname(const char **name) +{ + if (!*name) return "Helv"; + if (!strncmp(*name, "Co", 2)) return "Cour"; + if (!strncmp(*name, "co", 2)) return "Cour"; + if (!strncmp(*name, "Ti", 2)) return "TiRo"; + if (!strncmp(*name, "ti", 2)) return "TiRo"; + if (!strncmp(*name, "Sy", 2)) return "Symb"; + if (!strncmp(*name, "sy", 2)) return "Symb"; + if (!strncmp(*name, "Za", 2)) return "ZaDb"; + if (!strncmp(*name, "za", 2)) return "ZaDb"; + return "Helv"; +} + +void JM_make_annot_DA(fz_context *ctx, pdf_annot *annot, int ncol, float col[4], const char *fontname, float fontsize) +{ + fz_buffer *buf = NULL; + fz_try(ctx) + { + buf = fz_new_buffer(ctx, 50); + if (ncol <= 1) + fz_append_printf(ctx, buf, "%g g ", col[0]); + else if (ncol < 4) + fz_append_printf(ctx, buf, "%g %g %g rg ", col[0], col[1], col[2]); + else + fz_append_printf(ctx, buf, "%g %g %g %g k ", col[0], col[1], col[2], col[3]); + fz_append_printf(ctx, buf, "/%s %g Tf", JM_expand_fname(&fontname), fontsize); + unsigned char *da = NULL; + size_t len = fz_buffer_storage(ctx, buf, &da); + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + pdf_dict_put_string(ctx, annot_obj, PDF_NAME(DA), (const char *) da, len); + } + fz_always(ctx) fz_drop_buffer(ctx, buf); + fz_catch(ctx) fz_rethrow(ctx); + return; +} + +//------------------------------------------------------------------------ +// refreshes the link and annotation tables of a page +//------------------------------------------------------------------------ +void JM_refresh_links(fz_context *ctx, pdf_page *page) +{ + if (!page) return; + fz_try(ctx) { + pdf_obj *obj = pdf_dict_get(ctx, page->obj, PDF_NAME(Annots)); + if (obj) + { + pdf_document *pdf = page->doc; + int number = pdf_lookup_page_number(ctx, pdf, page->obj); + fz_rect page_mediabox; + fz_matrix page_ctm; + pdf_page_transform(ctx, page, &page_mediabox, &page_ctm); + page->links = pdf_load_link_annots(ctx, pdf, page, obj, number, page_ctm); + } + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return; +} + + +PyObject *JM_annot_border(fz_context *ctx, pdf_obj *annot_obj) +{ + PyObject *res = PyDict_New(); + PyObject *dash_py = PyList_New(0); + PyObject *val; + int i; + const char *style = NULL; + float width = -1.0f; + int clouds = -1; + pdf_obj *obj = NULL; + + obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(Border)); + if (pdf_is_array(ctx, obj)) { + width = pdf_to_real(ctx, pdf_array_get(ctx, obj, 2)); + if (pdf_array_len(ctx, obj) == 4) { + pdf_obj *dash = pdf_array_get(ctx, obj, 3); + for (i = 0; i < pdf_array_len(ctx, dash); i++) { + val = Py_BuildValue("i", pdf_to_int(ctx, pdf_array_get(ctx, dash, i))); + LIST_APPEND_DROP(dash_py, val); + } + } + } + + pdf_obj *bs_o = pdf_dict_get(ctx, annot_obj, PDF_NAME(BS)); + if (bs_o) { + width = pdf_to_real(ctx, pdf_dict_get(ctx, bs_o, PDF_NAME(W))); + style = pdf_to_name(ctx, pdf_dict_get(ctx, bs_o, PDF_NAME(S))); + if (style && strcmp(style, "") == 0) { + style = NULL; + } + obj = pdf_dict_get(ctx, bs_o, PDF_NAME(D)); + if (obj) { + for (i = 0; i < pdf_array_len(ctx, obj); i++) { + val = Py_BuildValue("i", pdf_to_int(ctx, pdf_array_get(ctx, obj, i))); + LIST_APPEND_DROP(dash_py, val); + } + } + } + + obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(BE)); + if (obj) { + clouds = pdf_to_int(ctx, pdf_dict_get(ctx, obj, PDF_NAME(I))); + } + val = PySequence_Tuple(dash_py); + Py_CLEAR(dash_py); + DICT_SETITEM_DROP(res, dictkey_width, Py_BuildValue("f", width)); + DICT_SETITEM_DROP(res, dictkey_dashes, val); + DICT_SETITEM_DROP(res, dictkey_style, Py_BuildValue("s", style)); + DICT_SETITEMSTR_DROP(res, "clouds", Py_BuildValue("i", clouds)); + return res; +} + +PyObject *JM_annot_set_border(fz_context *ctx, PyObject *border, pdf_document *doc, pdf_obj *annot_obj) +{ + if (!PyDict_Check(border)) { + JM_Warning("arg must be a dict"); + Py_RETURN_NONE; // not a dict + } + pdf_obj *obj = NULL; + Py_ssize_t i = 0, dashlen = 0; + int d; + double nwidth = PyFloat_AsDouble(PyDict_GetItem(border, dictkey_width)); // new width + PyObject *ndashes = PyDict_GetItem(border, dictkey_dashes); // new dashes + PyObject *nstyle = PyDict_GetItem(border, dictkey_style); // new style + int nclouds = (int) PyLong_AsLong(PyDict_GetItemString(border, "clouds")); // new clouds value + + // get old border properties + PyObject *oborder = JM_annot_border(ctx, annot_obj); + + // delete border-related entries + pdf_dict_del(ctx, annot_obj, PDF_NAME(BS)); + pdf_dict_del(ctx, annot_obj, PDF_NAME(BE)); + pdf_dict_del(ctx, annot_obj, PDF_NAME(Border)); + + // populate border items: keep old values for any omitted new ones + if (nwidth < 0) nwidth = PyFloat_AsDouble(PyDict_GetItem(oborder, dictkey_width)); // no new width: keep current + if (ndashes == Py_None) ndashes = PyDict_GetItem(oborder, dictkey_dashes); // no new dashes: keep old + if (nstyle == Py_None) nstyle = PyDict_GetItem(oborder, dictkey_style); // no new style: keep old + if (nclouds < 0) nclouds = (int) PyLong_AsLong(PyDict_GetItemString(oborder, "clouds")); // no new clouds: keep old + + if (ndashes && PyTuple_Check(ndashes) && PyTuple_Size(ndashes) > 0) { + dashlen = PyTuple_Size(ndashes); + pdf_obj *darr = pdf_new_array(ctx, doc, dashlen); + for (i = 0; i < dashlen; i++) { + d = (int) PyLong_AsLong(PyTuple_GetItem(ndashes, i)); + pdf_array_push_int(ctx, darr, (int64_t) d); + } + pdf_dict_putl_drop(ctx, annot_obj, darr, PDF_NAME(BS), PDF_NAME(D), NULL); + } + + pdf_dict_putl_drop(ctx, annot_obj, pdf_new_real(ctx, (float) nwidth), + PDF_NAME(BS), PDF_NAME(W), NULL); + + if (dashlen == 0) { + obj = JM_get_border_style(ctx, nstyle); + } else { + obj = PDF_NAME(D); + } + pdf_dict_putl_drop(ctx, annot_obj, obj, PDF_NAME(BS), PDF_NAME(S), NULL); + + if (nclouds > 0) { + pdf_dict_put_dict(ctx, annot_obj, PDF_NAME(BE), 2); + pdf_obj *obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(BE)); + pdf_dict_put(ctx, obj, PDF_NAME(S), PDF_NAME(C)); + pdf_dict_put_int(ctx, obj, PDF_NAME(I), (int64_t) nclouds); + } + + PyErr_Clear(); + Py_RETURN_NONE; +} + +PyObject *JM_annot_colors(fz_context *ctx, pdf_obj *annot_obj) +{ + PyObject *res = PyDict_New(); + PyObject *color = NULL; + int i, n; + float col; + pdf_obj *o = NULL; + + o = pdf_dict_get(ctx, annot_obj, PDF_NAME(C)); + if (pdf_is_array(ctx, o)) { + n = pdf_array_len(ctx, o); + color = PyTuple_New((Py_ssize_t) n); + for (i = 0; i < n; i++) { + col = pdf_to_real(ctx, pdf_array_get(ctx, o, i)); + PyTuple_SET_ITEM(color, i, Py_BuildValue("f", col)); + } + DICT_SETITEM_DROP(res, dictkey_stroke, color); + } else { + DICT_SETITEM_DROP(res, dictkey_stroke, Py_BuildValue("s", NULL)); + } + + o = pdf_dict_get(ctx, annot_obj, PDF_NAME(IC)); + if (pdf_is_array(ctx, o)) { + n = pdf_array_len(ctx, o); + color = PyTuple_New((Py_ssize_t) n); + for (i = 0; i < n; i++) { + col = pdf_to_real(ctx, pdf_array_get(ctx, o, i)); + PyTuple_SET_ITEM(color, i, Py_BuildValue("f", col)); + } + DICT_SETITEM_DROP(res, dictkey_fill, color); + } else { + DICT_SETITEM_DROP(res, dictkey_fill, Py_BuildValue("s", NULL)); + } + + return res; +} + + +//------------------------------------------------------------------------ +// Return the first annotation whose /IRT key ("In Response To") points to +// annot. Used to remove the response chain of a given annotation. +//------------------------------------------------------------------------ +pdf_annot *JM_find_annot_irt(fz_context *ctx, pdf_annot *annot) +{ + pdf_annot *irt_annot = NULL; // returning this + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + pdf_obj *o = NULL; + int found = 0; + fz_try(ctx) { // loop thru MuPDF's internal annots array + pdf_page *page = pdf_annot_page(ctx, annot); + irt_annot = pdf_first_annot(ctx, page); + while (irt_annot) { + pdf_obj *irt_annot_obj = pdf_annot_obj(ctx, irt_annot); + o = pdf_dict_gets(ctx, irt_annot_obj, "IRT"); + if (o) { + if (!pdf_objcmp(ctx, o, annot_obj)) { + found = 1; + break; + } + } + irt_annot = pdf_next_annot(ctx, irt_annot); + } + } + fz_catch(ctx) {;} + if (found) return pdf_keep_annot(ctx, irt_annot); + return NULL; +} + +//------------------------------------------------------------------------ +// return the annotation names (list of /NM entries) +//------------------------------------------------------------------------ +PyObject *JM_get_annot_id_list(fz_context *ctx, pdf_page *page) +{ + PyObject *names = PyList_New(0); + pdf_obj *annot_obj = NULL; + pdf_obj *annots = pdf_dict_get(ctx, page->obj, PDF_NAME(Annots)); + pdf_obj *name = NULL; + if (!annots) return names; + fz_try(ctx) { + int i, n = pdf_array_len(ctx, annots); + for (i = 0; i < n; i++) { + annot_obj = pdf_array_get(ctx, annots, i); + name = pdf_dict_gets(ctx, annot_obj, "NM"); + if (name) { + LIST_APPEND_DROP(names, Py_BuildValue("s", pdf_to_text_string(ctx, name))); + } + } + } + fz_catch(ctx) { + return names; + } + return names; +} + + +//------------------------------------------------------------------------ +// return the xrefs and /NM ids of a page's annots, links and fields +//------------------------------------------------------------------------ +PyObject *JM_get_annot_xref_list(fz_context *ctx, pdf_obj *page_obj) +{ + PyObject *names = PyList_New(0); + pdf_obj *id, *subtype, *annots, *annot_obj; + int xref, type, i, n; + fz_try(ctx) { + annots = pdf_dict_get(ctx, page_obj, PDF_NAME(Annots)); + n = pdf_array_len(ctx, annots); + for (i = 0; i < n; i++) { + annot_obj = pdf_array_get(ctx, annots, i); + xref = pdf_to_num(ctx, annot_obj); + subtype = pdf_dict_get(ctx, annot_obj, PDF_NAME(Subtype)); + if (!subtype) { + continue; // subtype is required + } + type = pdf_annot_type_from_string(ctx, pdf_to_name(ctx, subtype)); + if (type == PDF_ANNOT_UNKNOWN) { + continue; // only accept valid annot types + } + id = pdf_dict_gets(ctx, annot_obj, "NM"); + LIST_APPEND_DROP(names, Py_BuildValue("iis", xref, type, pdf_to_text_string(ctx, id))); + } + } + fz_catch(ctx) { + return names; + } + return names; +} + + +//------------------------------------------------------------------------ +// Add a unique /NM key to an annotation or widget. +// Append a number to 'stem' such that the result is a unique name. +//------------------------------------------------------------------------ +static char JM_annot_id_stem[50] = "fitz"; +void JM_add_annot_id(fz_context *ctx, pdf_annot *annot, char *stem) +{ + fz_try(ctx) { + PyObject *names = NULL; + pdf_page *page = pdf_annot_page(ctx, annot); + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + names = JM_get_annot_id_list(ctx, page); + int i = 0; + PyObject *stem_id = NULL; + while (1) { + stem_id = PyUnicode_FromFormat("%s-%s%d", JM_annot_id_stem, stem, i); + if (!PySequence_Contains(names, stem_id)) break; + i += 1; + Py_DECREF(stem_id); + } + char *response = JM_StrAsChar(stem_id); + pdf_obj *name = pdf_new_string(ctx, (const char *) response, strlen(response)); + pdf_dict_puts_drop(ctx, annot_obj, "NM", name); + Py_CLEAR(stem_id); + Py_CLEAR(names); + page->doc->resynth_required = 0; + } + fz_catch(ctx) { + fz_rethrow(ctx); + } +} + +//------------------------------------------------------------------------ +// retrieve annot by name (/NM key) +//------------------------------------------------------------------------ +pdf_annot *JM_get_annot_by_name(fz_context *ctx, pdf_page *page, char *name) +{ + if (!name || strlen(name) == 0) { + return NULL; + } + pdf_annot *annot = NULL; + int found = 0; + size_t len = 0; + + fz_try(ctx) { // loop thru MuPDF's internal annots and widget arrays + annot = pdf_first_annot(ctx, page); + while (annot) { + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + const char *response = pdf_to_string(ctx, pdf_dict_gets(ctx, annot_obj, "NM"), &len); + if (strcmp(name, response) == 0) { + found = 1; + break; + } + annot = pdf_next_annot(ctx, annot); + } + if (!found) { + fz_throw(ctx, FZ_ERROR_GENERIC, "'%s' is not an annot of this page", name); + } + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return pdf_keep_annot(ctx, annot); +} + +//------------------------------------------------------------------------ +// retrieve annot by its xref +//------------------------------------------------------------------------ +pdf_annot *JM_get_annot_by_xref(fz_context *ctx, pdf_page *page, int xref) +{ + pdf_annot *annot = NULL; + int found = 0; + + fz_try(ctx) { // loop thru MuPDF's internal annots array + annot = pdf_first_annot(ctx, page); + while (annot) { + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + if (xref == pdf_to_num(ctx, annot_obj)) { + found = 1; + break; + } + annot = pdf_next_annot(ctx, annot); + } + if (!found) { + fz_throw(ctx, FZ_ERROR_GENERIC, "xref %d is not an annot of this page", xref); + } + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return pdf_keep_annot(ctx, annot); +} + +//------------------------------------------------------------------------ +// retrieve widget by its xref +//------------------------------------------------------------------------ +pdf_annot *JM_get_widget_by_xref(fz_context *ctx, pdf_page *page, int xref) +{ + pdf_annot *annot = NULL; + int found = 0; + + fz_try(ctx) { // loop thru MuPDF's internal annots array + annot = pdf_first_widget(ctx, page); + while (annot) { + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + if (xref == pdf_to_num(ctx, annot_obj)) { + found = 1; + break; + } + annot = pdf_next_widget(ctx, annot); + } + if (!found) { + fz_throw(ctx, FZ_ERROR_GENERIC, "xref %d is not a widget of this page", xref); + } + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return pdf_keep_annot(ctx, annot); +} + +%} diff --git a/fitz/helper-convert.i b/fitz/helper-convert.i new file mode 100644 index 0000000..9310e42 --- /dev/null +++ b/fitz/helper-convert.i @@ -0,0 +1,98 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +//----------------------------------------------------------------------------- +// Convert any MuPDF document to a PDF +// Returns bytes object containing the PDF, created via 'write' function. +//----------------------------------------------------------------------------- +PyObject *JM_convert_to_pdf(fz_context *ctx, fz_document *doc, int fp, int tp, int rotate) +{ + pdf_document *pdfout = pdf_create_document(ctx); // new PDF document + int i, incr = 1, s = fp, e = tp; + if (fp > tp) { + incr = -1; // count backwards + s = tp; // adjust ... + e = fp; // ... range + } + fz_rect mediabox; + int rot = JM_norm_rotation(rotate); + fz_device *dev = NULL; + fz_buffer *contents = NULL; + pdf_obj *resources = NULL; + fz_page *page=NULL; + fz_var(dev); + fz_var(contents); + fz_var(resources); + fz_var(page); + for (i = fp; INRANGE(i, s, e); i += incr) { // interpret & write document pages as PDF pages + fz_try(ctx) { + page = fz_load_page(ctx, doc, i); + mediabox = fz_bound_page(ctx, page); + dev = pdf_page_write(ctx, pdfout, mediabox, &resources, &contents); + fz_run_page(ctx, page, dev, fz_identity, NULL); + fz_close_device(ctx, dev); + fz_drop_device(ctx, dev); + dev = NULL; + pdf_obj *page_obj = pdf_add_page(ctx, pdfout, mediabox, rot, resources, contents); + pdf_insert_page(ctx, pdfout, -1, page_obj); + pdf_drop_obj(ctx, page_obj); + } + fz_always(ctx) { + pdf_drop_obj(ctx, resources); + fz_drop_buffer(ctx, contents); + fz_drop_device(ctx, dev); + fz_drop_page(ctx, page); + page = NULL; + dev = NULL; + contents = NULL; + resources = NULL; + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + } + // PDF created - now write it to Python bytearray + PyObject *r = NULL; + fz_output *out = NULL; + fz_buffer *res = NULL; + // prepare write options structure + pdf_write_options opts = { 0 }; + opts.do_garbage = 4; + opts.do_compress = 1; + opts.do_compress_images = 1; + opts.do_compress_fonts = 1; + opts.do_sanitize = 1; + opts.do_incremental = 0; + opts.do_ascii = 0; + opts.do_decompress = 0; + opts.do_linear = 0; + opts.do_clean = 1; + opts.do_pretty = 0; + + fz_try(ctx) { + res = fz_new_buffer(ctx, 8192); + out = fz_new_output_with_buffer(ctx, res); + pdf_write_document(ctx, pdfout, out, &opts); + unsigned char *c = NULL; + size_t len = fz_buffer_storage(ctx, res, &c); + r = PyBytes_FromStringAndSize((const char *) c, (Py_ssize_t) len); + } + fz_always(ctx) { + pdf_drop_document(ctx, pdfout); + fz_drop_output(ctx, out); + fz_drop_buffer(ctx, res); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return r; +} +%} diff --git a/fitz/helper-defines.i b/fitz/helper-defines.i new file mode 100644 index 0000000..6bf1405 --- /dev/null +++ b/fitz/helper-defines.i @@ -0,0 +1,816 @@ +%inline %{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +//---------------------------------------------------------------------------- +// general +//---------------------------------------------------------------------------- +#define EPSILON 1e-5 + +//---------------------------------------------------------------------------- +// annotation types +//---------------------------------------------------------------------------- +#define PDF_ANNOT_TEXT 0 +#define PDF_ANNOT_LINK 1 +#define PDF_ANNOT_FREE_TEXT 2 +#define PDF_ANNOT_LINE 3 +#define PDF_ANNOT_SQUARE 4 +#define PDF_ANNOT_CIRCLE 5 +#define PDF_ANNOT_POLYGON 6 +#define PDF_ANNOT_POLY_LINE 7 +#define PDF_ANNOT_HIGHLIGHT 8 +#define PDF_ANNOT_UNDERLINE 9 +#define PDF_ANNOT_SQUIGGLY 10 +#define PDF_ANNOT_STRIKE_OUT 11 +#define PDF_ANNOT_REDACT 12 +#define PDF_ANNOT_STAMP 13 +#define PDF_ANNOT_CARET 14 +#define PDF_ANNOT_INK 15 +#define PDF_ANNOT_POPUP 16 +#define PDF_ANNOT_FILE_ATTACHMENT 17 +#define PDF_ANNOT_SOUND 18 +#define PDF_ANNOT_MOVIE 19 +#define PDF_ANNOT_RICH_MEDIA 20 +#define PDF_ANNOT_WIDGET 21 +#define PDF_ANNOT_SCREEN 22 +#define PDF_ANNOT_PRINTER_MARK 23 +#define PDF_ANNOT_TRAP_NET 24 +#define PDF_ANNOT_WATERMARK 25 +#define PDF_ANNOT_3D 26 +#define PDF_ANNOT_PROJECTION 27 +#define PDF_ANNOT_UNKNOWN -1 + +//------------------------ +// redaction annot options +//------------------------ +#define PDF_REDACT_IMAGE_NONE 0 +#define PDF_REDACT_IMAGE_REMOVE 1 +#define PDF_REDACT_IMAGE_PIXELS 2 + +//---------------------------------------------------------------------------- +// annotation flag bits +//---------------------------------------------------------------------------- +#define PDF_ANNOT_IS_INVISIBLE 1 << (1-1) +#define PDF_ANNOT_IS_HIDDEN 1 << (2-1) +#define PDF_ANNOT_IS_PRINT 1 << (3-1) +#define PDF_ANNOT_IS_NO_ZOOM 1 << (4-1) +#define PDF_ANNOT_IS_NO_ROTATE 1 << (5-1) +#define PDF_ANNOT_IS_NO_VIEW 1 << (6-1) +#define PDF_ANNOT_IS_READ_ONLY 1 << (7-1) +#define PDF_ANNOT_IS_LOCKED 1 << (8-1) +#define PDF_ANNOT_IS_TOGGLE_NO_VIEW 1 << (9-1) +#define PDF_ANNOT_IS_LOCKED_CONTENTS 1 << (10-1) + + +//---------------------------------------------------------------------------- +// annotation line ending styles +//---------------------------------------------------------------------------- +#define PDF_ANNOT_LE_NONE 0 +#define PDF_ANNOT_LE_SQUARE 1 +#define PDF_ANNOT_LE_CIRCLE 2 +#define PDF_ANNOT_LE_DIAMOND 3 +#define PDF_ANNOT_LE_OPEN_ARROW 4 +#define PDF_ANNOT_LE_CLOSED_ARROW 5 +#define PDF_ANNOT_LE_BUTT 6 +#define PDF_ANNOT_LE_R_OPEN_ARROW 7 +#define PDF_ANNOT_LE_R_CLOSED_ARROW 8 +#define PDF_ANNOT_LE_SLASH 9 + + +//---------------------------------------------------------------------------- +// annotation field (widget) types +//---------------------------------------------------------------------------- +#define PDF_WIDGET_TYPE_UNKNOWN 0 +#define PDF_WIDGET_TYPE_BUTTON 1 +#define PDF_WIDGET_TYPE_CHECKBOX 2 +#define PDF_WIDGET_TYPE_COMBOBOX 3 +#define PDF_WIDGET_TYPE_LISTBOX 4 +#define PDF_WIDGET_TYPE_RADIOBUTTON 5 +#define PDF_WIDGET_TYPE_SIGNATURE 6 +#define PDF_WIDGET_TYPE_TEXT 7 + + +//---------------------------------------------------------------------------- +// annotation text widget subtypes +//---------------------------------------------------------------------------- +#define PDF_WIDGET_TX_FORMAT_NONE 0 +#define PDF_WIDGET_TX_FORMAT_NUMBER 1 +#define PDF_WIDGET_TX_FORMAT_SPECIAL 2 +#define PDF_WIDGET_TX_FORMAT_DATE 3 +#define PDF_WIDGET_TX_FORMAT_TIME 4 + + +//---------------------------------------------------------------------------- +// annotation widget flags +//---------------------------------------------------------------------------- +// Common to all field types +#define PDF_FIELD_IS_READ_ONLY 1 +#define PDF_FIELD_IS_REQUIRED 1 << 1 +#define PDF_FIELD_IS_NO_EXPORT 1 << 2 + + +// Text fields +#define PDF_TX_FIELD_IS_MULTILINE 1 << 12 +#define PDF_TX_FIELD_IS_PASSWORD 1 << 13 +#define PDF_TX_FIELD_IS_FILE_SELECT 1 << 20 +#define PDF_TX_FIELD_IS_DO_NOT_SPELL_CHECK 1 << 22 +#define PDF_TX_FIELD_IS_DO_NOT_SCROLL 1 << 23 +#define PDF_TX_FIELD_IS_COMB 1 << 24 +#define PDF_TX_FIELD_IS_RICH_TEXT 1 << 25 + + +// Button fields +#define PDF_BTN_FIELD_IS_NO_TOGGLE_TO_OFF 1 << 14 +#define PDF_BTN_FIELD_IS_RADIO 1 << 15 +#define PDF_BTN_FIELD_IS_PUSHBUTTON 1 << 16 +#define PDF_BTN_FIELD_IS_RADIOS_IN_UNISON 1 << 25 + + +// Choice fields +#define PDF_CH_FIELD_IS_COMBO 1 << 17 +#define PDF_CH_FIELD_IS_EDIT 1 << 18 +#define PDF_CH_FIELD_IS_SORT 1 << 19 +#define PDF_CH_FIELD_IS_MULTI_SELECT 1 << 21 +#define PDF_CH_FIELD_IS_DO_NOT_SPELL_CHECK 1 << 22 +#define PDF_CH_FIELD_IS_COMMIT_ON_SEL_CHANGE 1 << 25 + + +// Signature fields errors +#define PDF_SIGNATURE_ERROR_OKAY 0 +#define PDF_SIGNATURE_ERROR_NO_SIGNATURES 1 +#define PDF_SIGNATURE_ERROR_NO_CERTIFICATE 2 +#define PDF_SIGNATURE_ERROR_DIGEST_FAILURE 3 +#define PDF_SIGNATURE_ERROR_SELF_SIGNED 4 +#define PDF_SIGNATURE_ERROR_SELF_SIGNED_IN_CHAIN 5 +#define PDF_SIGNATURE_ERROR_NOT_TRUSTED 6 +#define PDF_SIGNATURE_ERROR_UNKNOWN 7 + +// Signature appearances + +#define PDF_SIGNATURE_SHOW_LABELS 1 +#define PDF_SIGNATURE_SHOW_DN 2 +#define PDF_SIGNATURE_SHOW_DATE 4 +#define PDF_SIGNATURE_SHOW_TEXT_NAME 8 +#define PDF_SIGNATURE_SHOW_GRAPHIC_NAME 16 +#define PDF_SIGNATURE_SHOW_LOGO 32 +#define PDF_SIGNATURE_DEFAULT_APPEARANCE ( \ + PDF_SIGNATURE_SHOW_LABELS | \ + PDF_SIGNATURE_SHOW_DN | \ + PDF_SIGNATURE_SHOW_DATE | \ + PDF_SIGNATURE_SHOW_TEXT_NAME | \ + PDF_SIGNATURE_SHOW_GRAPHIC_NAME | \ + PDF_SIGNATURE_SHOW_LOGO ) + +//---------------------------------------------------------------------------- +// colorspace identifiers +//---------------------------------------------------------------------------- +#define CS_RGB 1 +#define CS_GRAY 2 +#define CS_CMYK 3 + +//---------------------------------------------------------------------------- +// PDF encryption algorithms +//---------------------------------------------------------------------------- +#define PDF_ENCRYPT_KEEP 0 +#define PDF_ENCRYPT_NONE 1 +#define PDF_ENCRYPT_RC4_40 2 +#define PDF_ENCRYPT_RC4_128 3 +#define PDF_ENCRYPT_AES_128 4 +#define PDF_ENCRYPT_AES_256 5 +#define PDF_ENCRYPT_UNKNOWN 6 + +//---------------------------------------------------------------------------- +// PDF permission codes +//---------------------------------------------------------------------------- +#define PDF_PERM_PRINT 1 << 2 +#define PDF_PERM_MODIFY 1 << 3 +#define PDF_PERM_COPY 1 << 4 +#define PDF_PERM_ANNOTATE 1 << 5 +#define PDF_PERM_FORM 1 << 8 +#define PDF_PERM_ACCESSIBILITY 1 << 9 +#define PDF_PERM_ASSEMBLE 1 << 10 +#define PDF_PERM_PRINT_HQ 1 << 11 + +//---------------------------------------------------------------------------- +// PDF Blend Modes +//---------------------------------------------------------------------------- +#define PDF_BM_Color "Color" +#define PDF_BM_ColorBurn "ColorBurn" +#define PDF_BM_ColorDodge "ColorDodge" +#define PDF_BM_Darken "Darken" +#define PDF_BM_Difference "Difference" +#define PDF_BM_Exclusion "Exclusion" +#define PDF_BM_HardLight "HardLight" +#define PDF_BM_Hue "Hue" +#define PDF_BM_Lighten "Lighten" +#define PDF_BM_Luminosity "Luminosity" +#define PDF_BM_Multiply "Multiply" +#define PDF_BM_Normal "Normal" +#define PDF_BM_Overlay "Overlay" +#define PDF_BM_Saturation "Saturation" +#define PDF_BM_Screen "Screen" +#define PDF_BM_SoftLight "Softlight" + + +// General text flags +#define TEXT_FONT_SUPERSCRIPT 1 +#define TEXT_FONT_ITALIC 2 +#define TEXT_FONT_SERIFED 4 +#define TEXT_FONT_MONOSPACED 8 +#define TEXT_FONT_BOLD 16 + +// UCDN Script codes +#define UCDN_SCRIPT_COMMON 0 +#define UCDN_SCRIPT_LATIN 1 +#define UCDN_SCRIPT_GREEK 2 +#define UCDN_SCRIPT_CYRILLIC 3 +#define UCDN_SCRIPT_ARMENIAN 4 +#define UCDN_SCRIPT_HEBREW 5 +#define UCDN_SCRIPT_ARABIC 6 +#define UCDN_SCRIPT_SYRIAC 7 +#define UCDN_SCRIPT_THAANA 8 +#define UCDN_SCRIPT_DEVANAGARI 9 +#define UCDN_SCRIPT_BENGALI 10 +#define UCDN_SCRIPT_GURMUKHI 11 +#define UCDN_SCRIPT_GUJARATI 12 +#define UCDN_SCRIPT_ORIYA 13 +#define UCDN_SCRIPT_TAMIL 14 +#define UCDN_SCRIPT_TELUGU 15 +#define UCDN_SCRIPT_KANNADA 16 +#define UCDN_SCRIPT_MALAYALAM 17 +#define UCDN_SCRIPT_SINHALA 18 +#define UCDN_SCRIPT_THAI 19 +#define UCDN_SCRIPT_LAO 20 +#define UCDN_SCRIPT_TIBETAN 21 +#define UCDN_SCRIPT_MYANMAR 22 +#define UCDN_SCRIPT_GEORGIAN 23 +#define UCDN_SCRIPT_HANGUL 24 +#define UCDN_SCRIPT_ETHIOPIC 25 +#define UCDN_SCRIPT_CHEROKEE 26 +#define UCDN_SCRIPT_CANADIAN_ABORIGINAL 27 +#define UCDN_SCRIPT_OGHAM 28 +#define UCDN_SCRIPT_RUNIC 29 +#define UCDN_SCRIPT_KHMER 30 +#define UCDN_SCRIPT_MONGOLIAN 31 +#define UCDN_SCRIPT_HIRAGANA 32 +#define UCDN_SCRIPT_KATAKANA 33 +#define UCDN_SCRIPT_BOPOMOFO 34 +#define UCDN_SCRIPT_HAN 35 +#define UCDN_SCRIPT_YI 36 +#define UCDN_SCRIPT_OLD_ITALIC 37 +#define UCDN_SCRIPT_GOTHIC 38 +#define UCDN_SCRIPT_DESERET 39 +#define UCDN_SCRIPT_INHERITED 40 +#define UCDN_SCRIPT_TAGALOG 41 +#define UCDN_SCRIPT_HANUNOO 42 +#define UCDN_SCRIPT_BUHID 43 +#define UCDN_SCRIPT_TAGBANWA 44 +#define UCDN_SCRIPT_LIMBU 45 +#define UCDN_SCRIPT_TAI_LE 46 +#define UCDN_SCRIPT_LINEAR_B 47 +#define UCDN_SCRIPT_UGARITIC 48 +#define UCDN_SCRIPT_SHAVIAN 49 +#define UCDN_SCRIPT_OSMANYA 50 +#define UCDN_SCRIPT_CYPRIOT 51 +#define UCDN_SCRIPT_BRAILLE 52 +#define UCDN_SCRIPT_BUGINESE 53 +#define UCDN_SCRIPT_COPTIC 54 +#define UCDN_SCRIPT_NEW_TAI_LUE 55 +#define UCDN_SCRIPT_GLAGOLITIC 56 +#define UCDN_SCRIPT_TIFINAGH 57 +#define UCDN_SCRIPT_SYLOTI_NAGRI 58 +#define UCDN_SCRIPT_OLD_PERSIAN 59 +#define UCDN_SCRIPT_KHAROSHTHI 60 +#define UCDN_SCRIPT_BALINESE 61 +#define UCDN_SCRIPT_CUNEIFORM 62 +#define UCDN_SCRIPT_PHOENICIAN 63 +#define UCDN_SCRIPT_PHAGS_PA 64 +#define UCDN_SCRIPT_NKO 65 +#define UCDN_SCRIPT_SUNDANESE 66 +#define UCDN_SCRIPT_LEPCHA 67 +#define UCDN_SCRIPT_OL_CHIKI 68 +#define UCDN_SCRIPT_VAI 69 +#define UCDN_SCRIPT_SAURASHTRA 70 +#define UCDN_SCRIPT_KAYAH_LI 71 +#define UCDN_SCRIPT_REJANG 72 +#define UCDN_SCRIPT_LYCIAN 73 +#define UCDN_SCRIPT_CARIAN 74 +#define UCDN_SCRIPT_LYDIAN 75 +#define UCDN_SCRIPT_CHAM 76 +#define UCDN_SCRIPT_TAI_THAM 77 +#define UCDN_SCRIPT_TAI_VIET 78 +#define UCDN_SCRIPT_AVESTAN 79 +#define UCDN_SCRIPT_EGYPTIAN_HIEROGLYPHS 80 +#define UCDN_SCRIPT_SAMARITAN 81 +#define UCDN_SCRIPT_LISU 82 +#define UCDN_SCRIPT_BAMUM 83 +#define UCDN_SCRIPT_JAVANESE 84 +#define UCDN_SCRIPT_MEETEI_MAYEK 85 +#define UCDN_SCRIPT_IMPERIAL_ARAMAIC 86 +#define UCDN_SCRIPT_OLD_SOUTH_ARABIAN 87 +#define UCDN_SCRIPT_INSCRIPTIONAL_PARTHIAN 88 +#define UCDN_SCRIPT_INSCRIPTIONAL_PAHLAVI 89 +#define UCDN_SCRIPT_OLD_TURKIC 90 +#define UCDN_SCRIPT_KAITHI 91 +#define UCDN_SCRIPT_BATAK 92 +#define UCDN_SCRIPT_BRAHMI 93 +#define UCDN_SCRIPT_MANDAIC 94 +#define UCDN_SCRIPT_CHAKMA 95 +#define UCDN_SCRIPT_MEROITIC_CURSIVE 96 +#define UCDN_SCRIPT_MEROITIC_HIEROGLYPHS 97 +#define UCDN_SCRIPT_MIAO 98 +#define UCDN_SCRIPT_SHARADA 99 +#define UCDN_SCRIPT_SORA_SOMPENG 100 +#define UCDN_SCRIPT_TAKRI 101 +#define UCDN_SCRIPT_UNKNOWN 102 +#define UCDN_SCRIPT_BASSA_VAH 103 +#define UCDN_SCRIPT_CAUCASIAN_ALBANIAN 104 +#define UCDN_SCRIPT_DUPLOYAN 105 +#define UCDN_SCRIPT_ELBASAN 106 +#define UCDN_SCRIPT_GRANTHA 107 +#define UCDN_SCRIPT_KHOJKI 108 +#define UCDN_SCRIPT_KHUDAWADI 109 +#define UCDN_SCRIPT_LINEAR_A 110 +#define UCDN_SCRIPT_MAHAJANI 111 +#define UCDN_SCRIPT_MANICHAEAN 112 +#define UCDN_SCRIPT_MENDE_KIKAKUI 113 +#define UCDN_SCRIPT_MODI 114 +#define UCDN_SCRIPT_MRO 115 +#define UCDN_SCRIPT_NABATAEAN 116 +#define UCDN_SCRIPT_OLD_NORTH_ARABIAN 117 +#define UCDN_SCRIPT_OLD_PERMIC 118 +#define UCDN_SCRIPT_PAHAWH_HMONG 119 +#define UCDN_SCRIPT_PALMYRENE 120 +#define UCDN_SCRIPT_PAU_CIN_HAU 121 +#define UCDN_SCRIPT_PSALTER_PAHLAVI 122 +#define UCDN_SCRIPT_SIDDHAM 123 +#define UCDN_SCRIPT_TIRHUTA 124 +#define UCDN_SCRIPT_WARANG_CITI 125 +#define UCDN_SCRIPT_AHOM 126 +#define UCDN_SCRIPT_ANATOLIAN_HIEROGLYPHS 127 +#define UCDN_SCRIPT_HATRAN 128 +#define UCDN_SCRIPT_MULTANI 129 +#define UCDN_SCRIPT_OLD_HUNGARIAN 130 +#define UCDN_SCRIPT_SIGNWRITING 131 +#define UCDN_SCRIPT_ADLAM 132 +#define UCDN_SCRIPT_BHAIKSUKI 133 +#define UCDN_SCRIPT_MARCHEN 134 +#define UCDN_SCRIPT_NEWA 135 +#define UCDN_SCRIPT_OSAGE 136 +#define UCDN_SCRIPT_TANGUT 137 +#define UCDN_SCRIPT_MASARAM_GONDI 138 +#define UCDN_SCRIPT_NUSHU 139 +#define UCDN_SCRIPT_SOYOMBO 140 +#define UCDN_SCRIPT_ZANABAZAR_SQUARE 141 +#define UCDN_SCRIPT_DOGRA 142 +#define UCDN_SCRIPT_GUNJALA_GONDI 143 +#define UCDN_SCRIPT_HANIFI_ROHINGYA 144 +#define UCDN_SCRIPT_MAKASAR 145 +#define UCDN_SCRIPT_MEDEFAIDRIN 146 +#define UCDN_SCRIPT_OLD_SOGDIAN 147 +#define UCDN_SCRIPT_SOGDIAN 148 +#define UCDN_SCRIPT_ELYMAIC 149 +#define UCDN_SCRIPT_NANDINAGARI 150 +#define UCDN_SCRIPT_NYIAKENG_PUACHUE_HMONG 151 +#define UCDN_SCRIPT_WANCHO 152 + + +// exceptions +PyObject *_set_FileDataError(PyObject *value) +{ + if (!value) { + Py_RETURN_FALSE; + } + JM_Exc_FileDataError = value; + Py_RETURN_TRUE; +} + +//------------------------------------------------------------------- +// minor tools +//------------------------------------------------------------------- +PyObject *util_sine_between(PyObject *C, PyObject *P, PyObject *Q) +{ + // for points C, P, Q compute the sine between lines CP and QP + fz_point c = JM_point_from_py(C); + fz_point p = JM_point_from_py(P); + fz_point q = JM_point_from_py(Q); + fz_point s = JM_normalize_vector(q.x - p.x, q.y - p.y); + fz_matrix m1 = fz_make_matrix(1, 0, 0, 1, -p.x, -p.y); + fz_matrix m2 = fz_make_matrix(s.x, -s.y, s.y, s.x, 0, 0); + m1 = fz_concat(m1, m2); + c = fz_transform_point(c, m1); + c = JM_normalize_vector(c.x, c.y); + return Py_BuildValue("f", c.y); +} + + +// Return the matrix that maps two points C, P to the x-axis such that +// C -> (0,0) and the image of P have the same distance. +PyObject *util_hor_matrix(PyObject *C, PyObject *P) +{ + fz_point c = JM_point_from_py(C); + fz_point p = JM_point_from_py(P); + + // compute (cosine, sine) of vector P-C with double precision: + fz_point s = JM_normalize_vector(p.x - c.x, p.y - c.y); + + fz_matrix m1 = fz_make_matrix(1, 0, 0, 1, -c.x, -c.y); + fz_matrix m2 = fz_make_matrix(s.x, -s.y, s.y, s.x, 0, 0); + return JM_py_from_matrix(fz_concat(m1, m2)); +} + +struct Annot; + +// Ensure that widgets with /AA/C JavaScript are in array AcroForm/CO +PyObject *util_ensure_widget_calc(struct Annot *annot) +{ + pdf_obj *PDFNAME_CO=NULL; + fz_try(gctx) { + pdf_obj *annot_obj = pdf_annot_obj(gctx, (pdf_annot *) annot); + pdf_document *pdf = pdf_get_bound_document(gctx, annot_obj); + PDFNAME_CO = pdf_new_name(gctx, "CO"); // = PDF_NAME(CO) + pdf_obj *acro = pdf_dict_getl(gctx, // get AcroForm dict + pdf_trailer(gctx, pdf), + PDF_NAME(Root), + PDF_NAME(AcroForm), + NULL); + + pdf_obj *CO = pdf_dict_get(gctx, acro, PDFNAME_CO); // = AcroForm/CO + if (!CO) { + CO = pdf_dict_put_array(gctx, acro, PDFNAME_CO, 2); + } + int i, n = pdf_array_len(gctx, CO); + int xref, nxref, found = 0; + xref = pdf_to_num(gctx, annot_obj); + for (i = 0; i < n; i++) { + nxref = pdf_to_num(gctx, pdf_array_get(gctx, CO, i)); + if (xref == nxref) { + found = 1; + break; + } + } + if (!found) { + pdf_array_push_drop(gctx, CO, pdf_new_indirect(gctx, pdf, xref, 0)); + } + } + fz_always(gctx) { + pdf_drop_obj(gctx, PDFNAME_CO); + } + fz_catch(gctx) { + PyErr_SetString(PyExc_RuntimeError, fz_caught_message(gctx)); + return NULL; + } + Py_RETURN_NONE; +} + + +//----------------------------------------------------------- +// Compute Rect coordinates using different alternatives +//----------------------------------------------------------- +PyObject *util_make_rect(PyObject *a) +{ + Py_ssize_t i, n = PyTuple_GET_SIZE(a); + PyObject *p1, *p2, *l = a; + char *msg = "Rect: bad args"; + double c[4] = { 0, 0, 0, 0 }; + switch (n) { + case 0: goto exit_normal; + case 1: goto size1; + case 2: goto size2; + case 3: goto size31; + case 4: goto size4; + default: + msg = "Rect: bad seq len"; + goto exit_error; + } + + size4:; + for (i = 0; i < 4; i++) { + if (JM_FLOAT_ITEM(l, i, &c[i]) == 1) { + goto exit_error; + } + } + goto exit_normal; + + size1:; + l = PyTuple_GET_ITEM(a, 0); + if (!PySequence_Check(l) || PySequence_Size(l) != 4) { + msg = "Rect: bad seq len"; + goto exit_error; + } + goto size4; + + size2:; + msg = "Rect: bad args"; + p1 = PyTuple_GET_ITEM(a, 0); + p2 = PyTuple_GET_ITEM(a, 1); + if (!PySequence_Check(p1) || PySequence_Size(p1) != 2) { + goto exit_error; + } + if (!PySequence_Check(p2) || PySequence_Size(p2) != 2) { + goto exit_error; + } + if (JM_FLOAT_ITEM(p1, 0, &c[0]) == 1) goto exit_error; + if (JM_FLOAT_ITEM(p1, 1, &c[1]) == 1) goto exit_error; + if (JM_FLOAT_ITEM(p2, 0, &c[2]) == 1) goto exit_error; + if (JM_FLOAT_ITEM(p2, 1, &c[3]) == 1) goto exit_error; + goto exit_normal; + + size31:; + p1 = PyTuple_GET_ITEM(a, 0); + if (PySequence_Check(p1)) goto size32; + if (JM_FLOAT_ITEM(a, 0, &c[0]) == 1) goto exit_error; + if (JM_FLOAT_ITEM(a, 1, &c[1]) == 1) goto exit_error; + p2 = PyTuple_GET_ITEM(a, 2); + if (!PySequence_Check(p2) || PySequence_Size(p2) != 2) { + goto exit_error; + } + if (JM_FLOAT_ITEM(p2, 0, &c[2]) == 1) goto exit_error; + if (JM_FLOAT_ITEM(p2, 1, &c[3]) == 1) goto exit_error; + goto exit_normal; + + size32:; + if (PySequence_Size(p1) != 2) goto exit_error; + if (JM_FLOAT_ITEM(p1, 0, &c[0]) == 1) goto exit_error; + if (JM_FLOAT_ITEM(p1, 1, &c[1]) == 1) goto exit_error; + if (JM_FLOAT_ITEM(a, 1, &c[2]) == 1) goto exit_error; + if (JM_FLOAT_ITEM(a, 2, &c[3]) == 1) goto exit_error; + goto exit_normal; + + exit_normal:; + for (i = 0; i < 4; i++) { + if (c[i] < FZ_MIN_INF_RECT) c[i] = FZ_MIN_INF_RECT; + if (c[i] > FZ_MAX_INF_RECT) c[i] = FZ_MAX_INF_RECT; + } + return Py_BuildValue("dddd", c[0], c[1], c[2], c[3]); + + exit_error:; + PyErr_SetString(PyExc_ValueError, msg); + return NULL; +} + + +//----------------------------------------------------------- +// Compute IRect coordinates using different alternatives +//----------------------------------------------------------- +PyObject *util_make_irect(PyObject *a) +{ + Py_ssize_t i, n = PyTuple_GET_SIZE(a); + PyObject *p1, *p2, *l = a; + char *msg = "IRect: bad args"; + int c[4] = { 0, 0, 0, 0 }; + switch (n) { + case 0: goto exit_normal; + case 1: goto size1; + case 2: goto size2; + case 3: goto size31; + case 4: goto size4; + default: + msg = "IRect: bad seq len"; + goto exit_error; + } + + size4:; + for (i = 0; i < 4; i++) { + if (JM_INT_ITEM(l, i, &c[i]) == 1) { + goto exit_error; + } + } + goto exit_normal; + + size1:; + l = PyTuple_GET_ITEM(a, 0); + if (!PySequence_Check(l) || PySequence_Size(l) != 4) { + msg = "IRect: bad seq len"; + goto exit_error; + } + goto size4; + + size2:; + p1 = PyTuple_GET_ITEM(a, 0); + p2 = PyTuple_GET_ITEM(a, 1); + if (!PySequence_Check(p1) || PySequence_Size(p1) != 2) { + goto exit_error; + } + if (!PySequence_Check(p2) || PySequence_Size(p2) != 2) { + goto exit_error; + } + msg = "IRect: bad int values"; + if (JM_INT_ITEM(p1, 0, &c[0]) == 1) goto exit_error; + if (JM_INT_ITEM(p1, 1, &c[1]) == 1) goto exit_error; + if (JM_INT_ITEM(p2, 0, &c[2]) == 1) goto exit_error; + if (JM_INT_ITEM(p2, 1, &c[3]) == 1) goto exit_error; + goto exit_normal; + + size31:; + p1 = PyTuple_GET_ITEM(a, 0); + if (PySequence_Check(p1)) goto size32; + if (JM_INT_ITEM(a, 0, &c[0]) == 1) goto exit_error; + if (JM_INT_ITEM(a, 1, &c[1]) == 1) goto exit_error; + p2 = PyTuple_GET_ITEM(a, 2); + if (!PySequence_Check(p2) || PySequence_Size(p2) != 2) { + goto exit_error; + } + if (JM_INT_ITEM(p2, 0, &c[2]) == 1) goto exit_error; + if (JM_INT_ITEM(p2, 1, &c[3]) == 1) goto exit_error; + goto exit_normal; + + size32:; + if (PySequence_Size(p1) != 2) goto exit_error; + if (JM_INT_ITEM(p1, 0, &c[0]) == 1) goto exit_error; + if (JM_INT_ITEM(p1, 1, &c[1]) == 1) goto exit_error; + if (JM_INT_ITEM(a, 1, &c[2]) == 1) goto exit_error; + if (JM_INT_ITEM(a, 2, &c[3]) == 1) goto exit_error; + goto exit_normal; + + exit_normal:; + for (i = 0; i < 4; i++) { + if (c[i] < FZ_MIN_INF_RECT) c[i] = FZ_MIN_INF_RECT; + if (c[i] > FZ_MAX_INF_RECT) c[i] = FZ_MAX_INF_RECT; + } + return Py_BuildValue("iiii", c[0], c[1], c[2], c[3]); + + exit_error:; + PyErr_SetString(PyExc_ValueError, msg); + return NULL; +} + + +PyObject *util_round_rect(PyObject *rect) +{ + return JM_py_from_irect(fz_round_rect(JM_rect_from_py(rect))); +} + + +PyObject *util_transform_rect(PyObject *rect, PyObject *matrix) +{ + return JM_py_from_rect(fz_transform_rect(JM_rect_from_py(rect), JM_matrix_from_py(matrix))); +} + + +PyObject *util_intersect_rect(PyObject *r1, PyObject *r2) +{ + return JM_py_from_rect(fz_intersect_rect(JM_rect_from_py(r1), + JM_rect_from_py(r2))); +} + + +PyObject *util_is_point_in_rect(PyObject *p, PyObject *r) +{ + return JM_BOOL(fz_is_point_inside_rect(JM_point_from_py(p), JM_rect_from_py(r))); +} + + +PyObject *util_include_point_in_rect(PyObject *r, PyObject *p) +{ + return JM_py_from_rect(fz_include_point_in_rect(JM_rect_from_py(r), + JM_point_from_py(p))); +} + + +PyObject *util_point_in_quad(PyObject *P, PyObject *Q) +{ + fz_point p = JM_point_from_py(P); + fz_quad q = JM_quad_from_py(Q); + return JM_BOOL(fz_is_point_inside_quad(p, q)); +} + + +PyObject *util_transform_point(PyObject *point, PyObject *matrix) +{ + return JM_py_from_point(fz_transform_point(JM_point_from_py(point), JM_matrix_from_py(matrix))); +} + + +PyObject *util_union_rect(PyObject *r1, PyObject *r2) +{ + return JM_py_from_rect(fz_union_rect(JM_rect_from_py(r1), + JM_rect_from_py(r2))); +} + + +PyObject *util_concat_matrix(PyObject *m1, PyObject *m2) +{ + return JM_py_from_matrix(fz_concat(JM_matrix_from_py(m1), + JM_matrix_from_py(m2))); +} + + +PyObject *util_invert_matrix(PyObject *matrix) +{ + fz_matrix src = JM_matrix_from_py(matrix); + float a = src.a; + float det = a * src.d - src.b * src.c; + if (det < -FLT_EPSILON || det > FLT_EPSILON) + { + fz_matrix dst; + float rdet = 1 / det; + dst.a = src.d * rdet; + dst.b = -src.b * rdet; + dst.c = -src.c * rdet; + dst.d = a * rdet; + a = -src.e * dst.a - src.f * dst.c; + dst.f = -src.e * dst.b - src.f * dst.d; + dst.e = a; + return Py_BuildValue("iN", 0, JM_py_from_matrix(dst)); + } + return Py_BuildValue("(i, ())", 1); +} + + +PyObject *util_measure_string(const char *text, const char *fontname, double fontsize, int encoding) +{ + double w = 0; + fz_font *font = NULL; + fz_try(gctx) { + font = fz_new_base14_font(gctx, fontname); + while (*text) + { + int c, g; + text += fz_chartorune(&c, text); + switch (encoding) + { + case PDF_SIMPLE_ENCODING_GREEK: + c = fz_iso8859_7_from_unicode(c); break; + case PDF_SIMPLE_ENCODING_CYRILLIC: + c = fz_windows_1251_from_unicode(c); break; + default: + c = fz_windows_1252_from_unicode(c); break; + } + if (c < 0) c = 0xB7; + g = fz_encode_character(gctx, font, c); + w += (double) fz_advance_glyph(gctx, font, g, 0); + } + } + fz_always(gctx) { + fz_drop_font(gctx, font); + } + fz_catch(gctx) { + return PyFloat_FromDouble(0); + } + return PyFloat_FromDouble(w * fontsize); +} + +%} + +%{ +// Global Constants - Python dictionary keys +PyObject *dictkey_align; +PyObject *dictkey_ascender; +PyObject *dictkey_bbox; +PyObject *dictkey_blocks; +PyObject *dictkey_bpc; +PyObject *dictkey_c; +PyObject *dictkey_chars; +PyObject *dictkey_color; +PyObject *dictkey_colorspace; +PyObject *dictkey_content; +PyObject *dictkey_creationDate; +PyObject *dictkey_cs_name; +PyObject *dictkey_da; +PyObject *dictkey_dashes; +PyObject *dictkey_desc; +PyObject *dictkey_descender; +PyObject *dictkey_dir; +PyObject *dictkey_effect; +PyObject *dictkey_ext; +PyObject *dictkey_filename; +PyObject *dictkey_fill; +PyObject *dictkey_flags; +PyObject *dictkey_font; +PyObject *dictkey_glyph; +PyObject *dictkey_height; +PyObject *dictkey_id; +PyObject *dictkey_image; +PyObject *dictkey_items; +PyObject *dictkey_length; +PyObject *dictkey_lines; +PyObject *dictkey_matrix; +PyObject *dictkey_modDate; +PyObject *dictkey_name; +PyObject *dictkey_number; +PyObject *dictkey_origin; +PyObject *dictkey_rect; +PyObject *dictkey_size; +PyObject *dictkey_smask; +PyObject *dictkey_spans; +PyObject *dictkey_stroke; +PyObject *dictkey_style; +PyObject *dictkey_subject; +PyObject *dictkey_text; +PyObject *dictkey_title; +PyObject *dictkey_type; +PyObject *dictkey_ufilename; +PyObject *dictkey_width; +PyObject *dictkey_wmode; +PyObject *dictkey_xref; +PyObject *dictkey_xres; +PyObject *dictkey_yres; +%} diff --git a/fitz/helper-devices.i b/fitz/helper-devices.i new file mode 100644 index 0000000..40c5def --- /dev/null +++ b/fitz/helper-devices.i @@ -0,0 +1,1019 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +typedef struct +{ + fz_device super; + PyObject *out; + size_t seqno; + long depth; + int clips; + PyObject *method; +} jm_lineart_device; + +static PyObject *dev_pathdict = NULL; +static PyObject *scissors = NULL; +static float dev_linewidth = 0; // border width if present +static fz_matrix trace_device_ptm; // page transformation matrix +static fz_matrix trace_device_ctm; // trace device matrix +static fz_matrix trace_device_rot; +static fz_point dev_lastpoint = {0, 0}; +static fz_rect dev_pathrect; +static float dev_pathfactor = 0; +static int dev_linecount = 0; +static const char *layer_name=NULL; // optional content name +static int path_type = 0; // one of the following values: +#define FILL_PATH 1 +#define STROKE_PATH 2 +#define CLIP_PATH 3 +#define CLIP_STROKE_PATH 4 + +static void trace_device_reset() +{ + Py_CLEAR(dev_pathdict); + Py_CLEAR(scissors); + layer_name = NULL; + dev_linewidth = 0; + trace_device_ptm = fz_identity; + trace_device_ctm = fz_identity; + trace_device_rot = fz_identity; + dev_lastpoint.x = 0; + dev_lastpoint.y = 0; + dev_pathrect.x0 = 0; + dev_pathrect.y0 = 0; + dev_pathrect.x1 = 0; + dev_pathrect.y1 = 0; + dev_pathfactor = 0; + dev_linecount = 0; + path_type = 0; +} + +// Every scissor of a clip is a sub rectangle of the preceeding clip +// scissor if the clip level is larger. +static fz_rect compute_scissor() +{ + PyObject *last_scissor = NULL; + fz_rect scissor; + if (!scissors) { + scissors = PyList_New(0); + } + Py_ssize_t num_scissors = PyList_Size(scissors); + if (num_scissors > 0) { + last_scissor = PyList_GET_ITEM(scissors, num_scissors-1); + scissor = JM_rect_from_py(last_scissor); + scissor = fz_intersect_rect(scissor, dev_pathrect); + } else { + scissor = dev_pathrect; + } + LIST_APPEND_DROP(scissors, JM_py_from_rect(scissor)); + return scissor; +} + + +static void +jm_increase_seqno(fz_context *ctx, fz_device *dev_, ...) +{ + jm_lineart_device *dev = (jm_lineart_device *) dev_; + dev->seqno += 1; +} + +/* +-------------------------------------------------------------------------- +Check whether the last 4 lines represent a quad. +Because of how we count, the lines are a polyline already, i.e. last point +of a line equals 1st point of next line. +So we check for a polygon (last line's end point equals start point). +If not true we return 0. +-------------------------------------------------------------------------- +*/ +static int +jm_checkquad() +{ + PyObject *items = PyDict_GetItem(dev_pathdict, dictkey_items); + Py_ssize_t i, len = PyList_Size(items); + float f[8]; // coordinates of the 4 corners + fz_point temp, lp; // line = (temp, lp) + PyObject *rect; + PyObject *line; + // fill the 8 floats in f, start from items[-4:] + for (i = 0; i < 4; i++) { // store line start points + line = PyList_GET_ITEM(items, len - 4 + i); + temp = JM_point_from_py(PyTuple_GET_ITEM(line, 1)); + f[i * 2] = temp.x; + f[i * 2 + 1] = temp.y; + lp = JM_point_from_py(PyTuple_GET_ITEM(line, 2)); + } + if (lp.x != f[0] || lp.y != f[1]) { + // not a polygon! + //dev_linecount -= 1; + return 0; + } + + // we have detected a quad + dev_linecount = 0; // reset this + // a quad item is ("qu", (ul, ur, ll, lr)), where the tuple items + // are pairs of floats representing a quad corner each. + rect = PyTuple_New(2); + PyTuple_SET_ITEM(rect, 0, PyUnicode_FromString("qu")); + /* ---------------------------------------------------- + * relationship of float array to quad points: + * (0, 1) = ul, (2, 3) = ll, (6, 7) = ur, (4, 5) = lr + ---------------------------------------------------- */ + fz_quad q = fz_make_quad(f[0], f[1], f[6], f[7], f[2], f[3], f[4], f[5]); + PyTuple_SET_ITEM(rect, 1, JM_py_from_quad(q)); + PyList_SetItem(items, len - 4, rect); // replace item -4 by rect + PyList_SetSlice(items, len - 3, len, NULL); // delete remaining 3 items + return 1; +} + + +/* +-------------------------------------------------------------------------- +Check whether the last 3 path items represent a rectangle. +Line 1 and 3 must be horizontal, line 2 must be vertical. +Returns 1 if we have modified the path, otherwise 0. +-------------------------------------------------------------------------- +*/ +static int +jm_checkrect() +{ + dev_linecount = 0; // reset line count + long orientation = 0; // area orientation of rectangle + fz_point ll, lr, ur, ul; + fz_rect r; + PyObject *rect; + PyObject *line0, *line2; + PyObject *items = PyDict_GetItem(dev_pathdict, dictkey_items); + Py_ssize_t len = PyList_Size(items); + + line0 = PyList_GET_ITEM(items, len - 3); + ll = JM_point_from_py(PyTuple_GET_ITEM(line0, 1)); + lr = JM_point_from_py(PyTuple_GET_ITEM(line0, 2)); + // no need to extract "line1"! + line2 = PyList_GET_ITEM(items, len - 1); + ur = JM_point_from_py(PyTuple_GET_ITEM(line2, 1)); + ul = JM_point_from_py(PyTuple_GET_ITEM(line2, 2)); + + /* + --------------------------------------------------------------------- + Assumption: + When decomposing rects, MuPDF always starts with a horizontal line, + followed by a vertical line, followed by a horizontal line. + First line: (ll, lr), third line: (ul, ur). + If 1st line is below 3rd line, we record anti-clockwise (+1), else + clockwise (-1) orientation. + --------------------------------------------------------------------- + */ + if (ll.y != lr.y || + ll.x != ul.x || + ur.y != ul.y || + ur.x != lr.x) { + goto drop_out; // not a rectangle + } + + // we have a rect, replace last 3 "l" items by one "re" item. + if (ul.y < lr.y) { + r = fz_make_rect(ul.x, ul.y, lr.x, lr.y); + orientation = 1; + } else { + r = fz_make_rect(ll.x, ll.y, ur.x, ur.y); + orientation = -1; + } + rect = PyTuple_New(3); + PyTuple_SET_ITEM(rect, 0, PyUnicode_FromString("re")); + PyTuple_SET_ITEM(rect, 1, JM_py_from_rect(r)); + PyTuple_SET_ITEM(rect, 2, PyLong_FromLong(orientation)); + PyList_SetItem(items, len - 3, rect); // replace item -3 by rect + PyList_SetSlice(items, len - 2, len, NULL); // delete remaining 2 items + return 1; + drop_out:; + return 0; +} + +static PyObject * +jm_lineart_color(fz_context *ctx, fz_colorspace *colorspace, const float *color) +{ + float rgb[3]; + if (colorspace) { + fz_convert_color(ctx, colorspace, color, fz_device_rgb(ctx), + rgb, NULL, fz_default_color_params); + return Py_BuildValue("fff", rgb[0], rgb[1], rgb[2]); + } + return PyTuple_New(0); +} + +static void +trace_moveto(fz_context *ctx, void *dev_, float x, float y) +{ + dev_lastpoint = fz_transform_point(fz_make_point(x, y), trace_device_ctm); + if (fz_is_infinite_rect(dev_pathrect)) { + dev_pathrect = fz_make_rect(dev_lastpoint.x, dev_lastpoint.y, + dev_lastpoint.x, dev_lastpoint.y); + } + dev_linecount = 0; // reset # of consec. lines +} + +static void +trace_lineto(fz_context *ctx, void *dev_, float x, float y) +{ + fz_point p1 = fz_transform_point(fz_make_point(x, y), trace_device_ctm); + dev_pathrect = fz_include_point_in_rect(dev_pathrect, p1); + PyObject *list = PyTuple_New(3); + PyTuple_SET_ITEM(list, 0, PyUnicode_FromString("l")); + PyTuple_SET_ITEM(list, 1, JM_py_from_point(dev_lastpoint)); + PyTuple_SET_ITEM(list, 2, JM_py_from_point(p1)); + dev_lastpoint = p1; + PyObject *items = PyDict_GetItem(dev_pathdict, dictkey_items); + LIST_APPEND_DROP(items, list); + dev_linecount += 1; // counts consecutive lines + if (dev_linecount == 4 && path_type != FILL_PATH) { // shrink to "re" or "qu" item + jm_checkquad(); + } +} + +static void +trace_curveto(fz_context *ctx, void *dev_, float x1, float y1, float x2, float y2, float x3, float y3) +{ + dev_linecount = 0; // reset # of consec. lines + fz_point p1 = fz_make_point(x1, y1); + fz_point p2 = fz_make_point(x2, y2); + fz_point p3 = fz_make_point(x3, y3); + p1 = fz_transform_point(p1, trace_device_ctm); + p2 = fz_transform_point(p2, trace_device_ctm); + p3 = fz_transform_point(p3, trace_device_ctm); + dev_pathrect = fz_include_point_in_rect(dev_pathrect, p1); + dev_pathrect = fz_include_point_in_rect(dev_pathrect, p2); + dev_pathrect = fz_include_point_in_rect(dev_pathrect, p3); + + PyObject *list = PyTuple_New(5); + PyTuple_SET_ITEM(list, 0, PyUnicode_FromString("c")); + PyTuple_SET_ITEM(list, 1, JM_py_from_point(dev_lastpoint)); + PyTuple_SET_ITEM(list, 2, JM_py_from_point(p1)); + PyTuple_SET_ITEM(list, 3, JM_py_from_point(p2)); + PyTuple_SET_ITEM(list, 4, JM_py_from_point(p3)); + dev_lastpoint = p3; + PyObject *items = PyDict_GetItem(dev_pathdict, dictkey_items); + LIST_APPEND_DROP(items, list); +} + +static void +trace_close(fz_context *ctx, void *dev_) +{ + if (dev_linecount == 3) { + if (jm_checkrect()) { + return; + } + } + DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(1)); + dev_linecount = 0; // reset # of consec. lines +} + +static const fz_path_walker trace_path_walker = + { + trace_moveto, + trace_lineto, + trace_curveto, + trace_close + }; + +/* +--------------------------------------------------------------------- +Create the "items" list of the path dictionary +* either create or empty the path dictionary +* reset the end point of the path +* reset count of consecutive lines +* invoke fz_walk_path(), which create the single items +* if no items detected, empty path dict again +--------------------------------------------------------------------- +*/ +static void +jm_lineart_path(fz_context *ctx, jm_lineart_device *dev, const fz_path *path) +{ + dev_pathrect = fz_infinite_rect; + dev_linecount = 0; + dev_lastpoint = fz_make_point(0, 0); + if (dev_pathdict) { + Py_CLEAR(dev_pathdict); + } + dev_pathdict = PyDict_New(); + DICT_SETITEM_DROP(dev_pathdict, dictkey_items, PyList_New(0)); + fz_walk_path(ctx, path, &trace_path_walker, dev); + // Check if any items were added ... + if (!PyList_Size(PyDict_GetItem(dev_pathdict, dictkey_items))) { + Py_CLEAR(dev_pathdict); + } +} + +//--------------------------------------------------------------------------- +// Append current path to list or merge into last path of the list. +// (1) Append if first path, different item lists or not a 'stroke' version +// of previous path +// (2) If new path has the same items, merge its content into previous path +// and change path["type"] to "fs". +// (3) If "out" is callable, skip the previous and pass dictionary to it. +//--------------------------------------------------------------------------- +static void +jm_append_merge(PyObject *out, PyObject *method) +{ + if (PyCallable_Check(out) || method != Py_None) { // function or method + goto callback; + } + Py_ssize_t len = PyList_Size(out); // len of output list so far + if (len == 0) { // always append first path + goto append; + } + const char *thistype = PyUnicode_AsUTF8(PyDict_GetItem(dev_pathdict, dictkey_type)); + if (strcmp(thistype, "s") != 0) { // if not stroke, then append + goto append; + } + PyObject *prev = PyList_GET_ITEM(out, len - 1); // get prev path + const char *prevtype = PyUnicode_AsUTF8(PyDict_GetItem(prev, dictkey_type)); + if (strcmp(prevtype, "f") != 0) { // if previous not fill, append + goto append; + } + // last check: there must be the same list of items for "f" and "s". + PyObject *previtems = PyDict_GetItem(prev, dictkey_items); + PyObject *thisitems = PyDict_GetItem(dev_pathdict, dictkey_items); + if (PyObject_RichCompareBool(previtems, thisitems, Py_NE)) { + goto append; + } + int rc = PyDict_Merge(prev, dev_pathdict, 0); // merge with no override + if (rc == 0) { + DICT_SETITEM_DROP(prev, dictkey_type, PyUnicode_FromString("fs")); + goto postappend; + } else { + PySys_WriteStderr("could not merge stroke and fill path"); + goto append; + } + append:; + PyList_Append(out, dev_pathdict); + postappend:; + Py_CLEAR(dev_pathdict); + return; + + callback:; // callback function or method + PyObject *resp = NULL; + if (method == Py_None) { + resp = PyObject_CallFunctionObjArgs(out, dev_pathdict, NULL); + } else { + resp = PyObject_CallMethodObjArgs(out, method, dev_pathdict, NULL); + } + if (resp) { + Py_DECREF(resp); + } else { + PySys_WriteStderr("calling cdrawings callback function/method failed!"); + PyErr_Clear(); + } + Py_CLEAR(dev_pathdict); + return; +} + + +static void +jm_lineart_fill_path(fz_context *ctx, fz_device *dev_, const fz_path *path, + int even_odd, fz_matrix ctm, fz_colorspace *colorspace, + const float *color, float alpha, fz_color_params color_params) +{ + jm_lineart_device *dev = (jm_lineart_device *) dev_; + PyObject *out = dev->out; + trace_device_ctm = ctm; //fz_concat(ctm, trace_device_ptm); + path_type = FILL_PATH; + jm_lineart_path(ctx, dev, path); + if (!dev_pathdict) { + return; + } + DICT_SETITEM_DROP(dev_pathdict, dictkey_type, PyUnicode_FromString("f")); + DICT_SETITEMSTR_DROP(dev_pathdict, "even_odd", JM_BOOL(even_odd)); + DICT_SETITEMSTR_DROP(dev_pathdict, "fill_opacity", Py_BuildValue("f", alpha)); + DICT_SETITEMSTR_DROP(dev_pathdict, "fill", jm_lineart_color(ctx, colorspace, color)); + DICT_SETITEM_DROP(dev_pathdict, dictkey_rect, JM_py_from_rect(dev_pathrect)); + DICT_SETITEMSTR_DROP(dev_pathdict, "seqno", PyLong_FromSize_t(dev->seqno)); + DICT_SETITEMSTR_DROP(dev_pathdict, "layer", JM_EscapeStrFromStr(layer_name)); + if (dev->clips) { + DICT_SETITEMSTR_DROP(dev_pathdict, "level", PyLong_FromLong(dev->depth)); + } + jm_append_merge(out, dev->method); + dev->seqno += 1; +} + +static void +jm_lineart_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, + const fz_stroke_state *stroke, fz_matrix ctm, + fz_colorspace *colorspace, const float *color, float alpha, + fz_color_params color_params) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + PyObject *out = dev->out; + int i; + dev_pathfactor = 1; + if (fz_abs(ctm.a) == fz_abs(ctm.d)) { + dev_pathfactor = fz_abs(ctm.a); + } + trace_device_ctm = ctm; // fz_concat(ctm, trace_device_ptm); + path_type = STROKE_PATH; + + jm_lineart_path(ctx, dev, path); + if (!dev_pathdict) { + return; + } + DICT_SETITEM_DROP(dev_pathdict, dictkey_type, PyUnicode_FromString("s")); + DICT_SETITEMSTR_DROP(dev_pathdict, "stroke_opacity", Py_BuildValue("f", alpha)); + DICT_SETITEMSTR_DROP(dev_pathdict, "color", jm_lineart_color(ctx, colorspace, color)); + DICT_SETITEM_DROP(dev_pathdict, dictkey_width, Py_BuildValue("f", dev_pathfactor * stroke->linewidth)); + DICT_SETITEMSTR_DROP(dev_pathdict, "lineCap", Py_BuildValue("iii", stroke->start_cap, stroke->dash_cap, stroke->end_cap)); + DICT_SETITEMSTR_DROP(dev_pathdict, "lineJoin", Py_BuildValue("f", dev_pathfactor * stroke->linejoin)); + if (!PyDict_GetItemString(dev_pathdict, "closePath")) { + DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(0)); + } + + // output the "dashes" string + if (stroke->dash_len) { + fz_buffer *buff = fz_new_buffer(ctx, 256); + fz_append_string(ctx, buff, "[ "); // left bracket + for (i = 0; i < stroke->dash_len; i++) { + fz_append_printf(ctx, buff, "%g ", dev_pathfactor * stroke->dash_list[i]); + } + fz_append_printf(ctx, buff, "] %g", dev_pathfactor * stroke->dash_phase); + DICT_SETITEMSTR_DROP(dev_pathdict, "dashes", JM_EscapeStrFromBuffer(ctx, buff)); + fz_drop_buffer(ctx, buff); + } else { + DICT_SETITEMSTR_DROP(dev_pathdict, "dashes", PyUnicode_FromString("[] 0")); + } + + DICT_SETITEM_DROP(dev_pathdict, dictkey_rect, JM_py_from_rect(dev_pathrect)); + DICT_SETITEMSTR_DROP(dev_pathdict, "layer", JM_EscapeStrFromStr(layer_name)); + DICT_SETITEMSTR_DROP(dev_pathdict, "seqno", PyLong_FromSize_t(dev->seqno)); + if (dev->clips) { + DICT_SETITEMSTR_DROP(dev_pathdict, "level", PyLong_FromLong(dev->depth)); + } + // output the dict - potentially merging it with a previous fill_path twin + jm_append_merge(out, dev->method); + dev->seqno += 1; +} + +static void +jm_lineart_clip_path(fz_context *ctx, fz_device *dev_, const fz_path *path, int even_odd, fz_matrix ctm, fz_rect scissor) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + if (!dev->clips) return; + PyObject *out = dev->out; + trace_device_ctm = ctm; //fz_concat(ctm, trace_device_ptm); + path_type = CLIP_PATH; + jm_lineart_path(ctx, dev, path); + DICT_SETITEM_DROP(dev_pathdict, dictkey_type, PyUnicode_FromString("clip")); + DICT_SETITEMSTR_DROP(dev_pathdict, "even_odd", JM_BOOL(even_odd)); + if (!PyDict_GetItemString(dev_pathdict, "closePath")) { + DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(0)); + } + DICT_SETITEMSTR_DROP(dev_pathdict, "scissor", JM_py_from_rect(compute_scissor())); + DICT_SETITEMSTR_DROP(dev_pathdict, "level", PyLong_FromLong(dev->depth)); + DICT_SETITEMSTR_DROP(dev_pathdict, "layer", JM_EscapeStrFromStr(layer_name)); + jm_append_merge(out, dev->method); + dev->depth++; +} + +static void +jm_lineart_clip_stroke_path(fz_context *ctx, fz_device *dev_, const fz_path *path, const fz_stroke_state *stroke, fz_matrix ctm, fz_rect scissor) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + if (!dev->clips) return; + PyObject *out = dev->out; + trace_device_ctm = ctm; //fz_concat(ctm, trace_device_ptm); + path_type = CLIP_STROKE_PATH; + jm_lineart_path(ctx, dev, path); + DICT_SETITEM_DROP(dev_pathdict, dictkey_type, PyUnicode_FromString("clip")); + DICT_SETITEMSTR_DROP(dev_pathdict, "even_odd", Py_BuildValue("s", NULL)); + if (!PyDict_GetItemString(dev_pathdict, "closePath")) { + DICT_SETITEMSTR_DROP(dev_pathdict, "closePath", JM_BOOL(0)); + } + DICT_SETITEMSTR_DROP(dev_pathdict, "scissor", JM_py_from_rect(compute_scissor())); + DICT_SETITEMSTR_DROP(dev_pathdict, "level", PyLong_FromLong(dev->depth)); + DICT_SETITEMSTR_DROP(dev_pathdict, "layer", JM_EscapeStrFromStr(layer_name)); + jm_append_merge(out, dev->method); + dev->depth++; +} + +static void +jm_lineart_clip_stroke_text(fz_context *ctx, fz_device *dev_, const fz_text *text, const fz_stroke_state *stroke, fz_matrix ctm, fz_rect scissor) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + if (!dev->clips) return; + PyObject *out = dev->out; + compute_scissor(); + dev->depth++; +} + +static void +jm_lineart_clip_text(fz_context *ctx, fz_device *dev_, const fz_text *text, fz_matrix ctm, fz_rect scissor) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + if (!dev->clips) return; + PyObject *out = dev->out; + compute_scissor(); + dev->depth++; +} + +static void +jm_lineart_clip_image_mask(fz_context *ctx, fz_device *dev_, fz_image *image, fz_matrix ctm, fz_rect scissor) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + if (!dev->clips) return; + PyObject *out = dev->out; + compute_scissor(); + dev->depth++; +} + +static void +jm_lineart_pop_clip(fz_context *ctx, fz_device *dev_) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + if (!dev->clips) return; + if (!scissors) return; + Py_ssize_t len = PyList_Size(scissors); + if (len < 1) return; + PyList_SetSlice(scissors, len - 1, len, NULL); + dev->depth--; +} + + +static void +jm_lineart_begin_layer(fz_context *ctx, fz_device *dev_, const char *name) +{ + layer_name = name; +} + +static void +jm_lineart_end_layer(fz_context *ctx, fz_device *dev_) +{ + layer_name = NULL; +} + +static void +jm_lineart_begin_group(fz_context *ctx, fz_device *dev_, fz_rect bbox, fz_colorspace *cs, int isolated, int knockout, int blendmode, float alpha) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + if (!dev->clips) return; + PyObject *out = dev->out; + dev_pathdict = Py_BuildValue("{s:s,s:N,s:N,s:N,s:s,s:f,s:i,s:N}", + "type", "group", + "rect", JM_py_from_rect(bbox), + "isolated", JM_BOOL(isolated), + "knockout", JM_BOOL(knockout), + "blendmode", fz_blendmode_name(blendmode), + "opacity", alpha, + "level", dev->depth, + "layer", JM_EscapeStrFromStr(layer_name) + ); + jm_append_merge(out, dev->method); + dev->depth++; +} + +static void +jm_lineart_end_group(fz_context *ctx, fz_device *dev_) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + if (!dev->clips) return; + dev->depth--; +} + + +static void +jm_dev_linewidth(fz_context *ctx, fz_device *dev_, const fz_path *path, const fz_stroke_state *stroke, fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params) +{ + dev_linewidth = stroke->linewidth; + jm_increase_seqno(ctx, dev_); +} + + +static void +jm_trace_text_span(fz_context *ctx, PyObject *out, fz_text_span *span, int type, fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, size_t seqno) +{ + fz_font *out_font = NULL; + int i; + const char *fontname = JM_font_name(ctx, span->font); + float rgb[3]; + PyObject *chars = PyTuple_New(span->len); + fz_matrix join = fz_concat(span->trm, ctm); + fz_point dir = fz_transform_vector(fz_make_point(1, 0), join); + double fsize = sqrt(fabs((double) join.a * (double) join.d)); + double linewidth, adv, asc, dsc; + double space_adv = 0; + float x0, y0, x1, y1; + asc = (double) JM_font_ascender(ctx, span->font); + dsc = (double) JM_font_descender(ctx, span->font); + if (asc < 1e-3) { // probably Tesseract font + dsc = -0.1; + asc = 0.9; + } + + double ascsize = asc * fsize / (asc - dsc); + double dscsize = dsc * fsize / (asc - dsc); + int fflags = 0; + int mono = fz_font_is_monospaced(ctx, span->font); + fflags += mono * TEXT_FONT_MONOSPACED; + fflags += fz_font_is_italic(ctx, span->font) * TEXT_FONT_ITALIC; + fflags += fz_font_is_serif(ctx, span->font) * TEXT_FONT_SERIFED; + fflags += fz_font_is_bold(ctx, span->font) * TEXT_FONT_BOLD; + fz_matrix mat = trace_device_ptm; + fz_matrix ctm_rot = fz_concat(ctm, trace_device_rot); + mat = fz_concat(mat, ctm_rot); + + if (dev_linewidth > 0) { + linewidth = (double) dev_linewidth; + } else { + linewidth = fsize * 0.05; + } + fz_point char_orig; + double last_adv = 0; + + // walk through characters of span + fz_rect span_bbox; + dir = fz_normalize_vector(dir); + fz_matrix rot = fz_make_matrix(dir.x, dir.y, -dir.y, dir.x, 0, 0); + if (dir.x == -1) { // left-right flip + rot.d = 1; + } + + for (i = 0; i < span->len; i++) { + adv = 0; + if (span->items[i].gid >= 0) { + adv = (double) fz_advance_glyph(ctx, span->font, span->items[i].gid, span->wmode); + } + adv *= fsize; + last_adv = adv; + if (span->items[i].ucs == 32) { + space_adv = adv; + } + char_orig = fz_make_point(span->items[i].x, span->items[i].y); + char_orig.y = trace_device_ptm.f - char_orig.y; + char_orig = fz_transform_point(char_orig, mat); + fz_matrix m1 = fz_make_matrix(1, 0, 0, 1, -char_orig.x, -char_orig.y); + m1 = fz_concat(m1, rot); + m1 = fz_concat(m1, fz_make_matrix(1, 0, 0, 1, char_orig.x, char_orig.y)); + x0 = char_orig.x; + x1 = x0 + adv; + if (dir.x == 1 && span->trm.d < 0) { // up-down flip + y0 = char_orig.y + dscsize; + y1 = char_orig.y + ascsize; + } else { + y0 = char_orig.y - ascsize; + y1 = char_orig.y - dscsize; + } + fz_rect char_bbox = fz_make_rect(x0, y0, x1, y1); + char_bbox = fz_transform_rect(char_bbox, m1); + PyTuple_SET_ITEM(chars, (Py_ssize_t) i, Py_BuildValue("ii(ff)(ffff)", + span->items[i].ucs, span->items[i].gid, + char_orig.x, char_orig.y, char_bbox.x0, char_bbox.y0, char_bbox.x1, char_bbox.y1)); + if (i > 0) { + span_bbox = fz_union_rect(span_bbox, char_bbox); + } else { + span_bbox = char_bbox; + } + } + if (!space_adv) { + if (!mono) { + space_adv = fz_advance_glyph(ctx, span->font, + fz_encode_character_with_fallback(ctx, span->font, 32, 0, 0, &out_font), + span->wmode); + space_adv *= fsize; + if (!space_adv) { + space_adv = last_adv; + } + } else { + space_adv = last_adv; // for mono fonts this suffices + } + } + // make the span dictionary + PyObject *span_dict = PyDict_New(); + DICT_SETITEMSTR_DROP(span_dict, "dir", JM_py_from_point(dir)); + DICT_SETITEM_DROP(span_dict, dictkey_font, JM_EscapeStrFromStr(fontname)); + DICT_SETITEM_DROP(span_dict, dictkey_wmode, PyLong_FromLong((long) span->wmode)); + DICT_SETITEM_DROP(span_dict, dictkey_flags, PyLong_FromLong((long) fflags)); + DICT_SETITEMSTR_DROP(span_dict, "bidi_lvl", PyLong_FromLong((long) span->bidi_level)); + DICT_SETITEMSTR_DROP(span_dict, "bidi_dir", PyLong_FromLong((long) span->markup_dir)); + DICT_SETITEM_DROP(span_dict, dictkey_ascender, PyFloat_FromDouble(asc)); + DICT_SETITEM_DROP(span_dict, dictkey_descender, PyFloat_FromDouble(dsc)); + if (colorspace) { + fz_convert_color(ctx, colorspace, color, fz_device_rgb(ctx), + rgb, NULL, fz_default_color_params); + DICT_SETITEM_DROP(span_dict, dictkey_colorspace, PyLong_FromLong(3)); + DICT_SETITEM_DROP(span_dict, dictkey_color, Py_BuildValue("fff", rgb[0], rgb[1], rgb[2])); + } else { + DICT_SETITEM_DROP(span_dict, dictkey_colorspace, PyLong_FromLong(1)); + DICT_SETITEM_DROP(span_dict, dictkey_color, PyFloat_FromDouble(1)); + } + DICT_SETITEM_DROP(span_dict, dictkey_size, PyFloat_FromDouble(fsize)); + DICT_SETITEMSTR_DROP(span_dict, "opacity", PyFloat_FromDouble((double) alpha)); + DICT_SETITEMSTR_DROP(span_dict, "linewidth", PyFloat_FromDouble((double) linewidth)); + DICT_SETITEMSTR_DROP(span_dict, "spacewidth", PyFloat_FromDouble(space_adv)); + DICT_SETITEM_DROP(span_dict, dictkey_type, PyLong_FromLong((long) type)); + DICT_SETITEM_DROP(span_dict, dictkey_chars, chars); + DICT_SETITEM_DROP(span_dict, dictkey_bbox, JM_py_from_rect(span_bbox)); + DICT_SETITEMSTR_DROP(span_dict, "layer", JM_EscapeStrFromStr(layer_name)); + DICT_SETITEMSTR_DROP(span_dict, "seqno", PyLong_FromSize_t(seqno)); + LIST_APPEND_DROP(out, span_dict); +} + +static void +jm_trace_text(fz_context *ctx, PyObject *out, const fz_text *text, int type, fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, size_t seqno) +{ + fz_text_span *span; + for (span = text->head; span; span = span->next) + jm_trace_text_span(ctx, out, span, type, ctm, colorspace, color, alpha, seqno); +} + +/*--------------------------------------------------------- +There are 3 text trace types: +0 - fill text (PDF Tr 0) +1 - stroke text (PDF Tr 1) +3 - ignore text (PDF Tr 3) +---------------------------------------------------------*/ +static void +jm_lineart_fill_text(fz_context *ctx, fz_device *dev_, const fz_text *text, fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + PyObject *out = dev->out; + jm_trace_text(ctx, out, text, 0, ctm, colorspace, color, alpha, dev->seqno); + dev->seqno += 1; +} + +static void +jm_lineart_stroke_text(fz_context *ctx, fz_device *dev_, const fz_text *text, const fz_stroke_state *stroke, fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + PyObject *out = dev->out; + jm_trace_text(ctx, out, text, 1, ctm, colorspace, color, alpha, dev->seqno); + dev->seqno += 1; +} + + +static void +jm_lineart_ignore_text(fz_context *ctx, fz_device *dev_, const fz_text *text, fz_matrix ctm) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + PyObject *out = dev->out; + jm_trace_text(ctx, out, text, 3, ctm, NULL, NULL, 1, dev->seqno); + dev->seqno += 1; +} + +static void jm_lineart_drop_device(fz_context *ctx, fz_device *dev_) +{ + jm_lineart_device *dev = (jm_lineart_device *)dev_; + if (PyList_Check(dev->out)) { + Py_CLEAR(dev->out); + } + Py_CLEAR(dev->method); + Py_CLEAR(scissors); +} + +//------------------------------------------------------------------- +// LINEART device for Python method Page.get_cdrawings() +//------------------------------------------------------------------- +fz_device *JM_new_lineart_device(fz_context *ctx, PyObject *out, int clips, PyObject *method) +{ + jm_lineart_device *dev = fz_new_derived_device(ctx, jm_lineart_device); + + dev->super.close_device = NULL; + dev->super.drop_device = jm_lineart_drop_device; + dev->super.fill_path = jm_lineart_fill_path; + dev->super.stroke_path = jm_lineart_stroke_path; + dev->super.clip_path = jm_lineart_clip_path; + dev->super.clip_stroke_path = jm_lineart_clip_stroke_path; + + dev->super.fill_text = jm_increase_seqno; + dev->super.stroke_text = jm_increase_seqno; + dev->super.clip_text = jm_lineart_clip_text; + dev->super.clip_stroke_text = jm_lineart_clip_stroke_text; + dev->super.ignore_text = jm_increase_seqno; + + dev->super.fill_shade = jm_increase_seqno; + dev->super.fill_image = jm_increase_seqno; + dev->super.fill_image_mask = jm_increase_seqno; + dev->super.clip_image_mask = jm_lineart_clip_image_mask; + + dev->super.pop_clip = jm_lineart_pop_clip; + + dev->super.begin_mask = NULL; + dev->super.end_mask = NULL; + dev->super.begin_group = jm_lineart_begin_group; + dev->super.end_group = jm_lineart_end_group; + + dev->super.begin_tile = NULL; + dev->super.end_tile = NULL; + + dev->super.begin_layer = jm_lineart_begin_layer; + dev->super.end_layer = jm_lineart_end_layer; + + dev->super.begin_structure = NULL; + dev->super.end_structure = NULL; + + dev->super.begin_metatext = NULL; + dev->super.end_metatext = NULL; + + dev->super.render_flags = NULL; + dev->super.set_default_colorspaces = NULL; + + if (PyList_Check(out)) { + Py_INCREF(out); + } + Py_INCREF(method); + dev->out = out; + dev->seqno = 0; + dev->depth = 0; + dev->clips = clips; + dev->method = method; + trace_device_reset(); + return (fz_device *)dev; +} + +//------------------------------------------------------------------- +// Trace TEXT device for Python method Page.get_texttrace() +//------------------------------------------------------------------- +fz_device *JM_new_texttrace_device(fz_context *ctx, PyObject *out) +{ + jm_lineart_device *dev = fz_new_derived_device(ctx, jm_lineart_device); + + dev->super.close_device = NULL; + dev->super.drop_device = jm_lineart_drop_device; + dev->super.fill_path = jm_increase_seqno; + dev->super.stroke_path = jm_dev_linewidth; + dev->super.clip_path = NULL; + dev->super.clip_stroke_path = NULL; + + dev->super.fill_text = jm_lineart_fill_text; + dev->super.stroke_text = jm_lineart_stroke_text; + dev->super.clip_text = NULL; + dev->super.clip_stroke_text = NULL; + dev->super.ignore_text = jm_lineart_ignore_text; + + dev->super.fill_shade = jm_increase_seqno; + dev->super.fill_image = jm_increase_seqno; + dev->super.fill_image_mask = jm_increase_seqno; + dev->super.clip_image_mask = NULL; + + dev->super.pop_clip = NULL; + + dev->super.begin_mask = NULL; + dev->super.end_mask = NULL; + dev->super.begin_group = NULL; + dev->super.end_group = NULL; + + dev->super.begin_tile = NULL; + dev->super.end_tile = NULL; + + dev->super.begin_layer = jm_lineart_begin_layer; + dev->super.end_layer = jm_lineart_end_layer; + + dev->super.begin_structure = NULL; + dev->super.end_structure = NULL; + + dev->super.begin_metatext = NULL; + dev->super.end_metatext = NULL; + + dev->super.render_flags = NULL; + dev->super.set_default_colorspaces = NULL; + + if (PyList_Check(out)) { + Py_XINCREF(out); + } + dev->out = out; + dev->seqno = 0; + dev->depth = 0; + dev->clips = 0; + dev->method = NULL; + trace_device_reset(); + + return (fz_device *)dev; +} + +//------------------------------------------------------------------- +// BBOX device +//------------------------------------------------------------------- +typedef struct jm_bbox_device_s +{ + fz_device super; + PyObject *result; + int layers; +} jm_bbox_device; + +static void +jm_bbox_add_rect(fz_context *ctx, fz_device *dev, fz_rect rect, char *code) +{ + jm_bbox_device *bdev = (jm_bbox_device *)dev; + if (!bdev->layers) { + LIST_APPEND_DROP(bdev->result, Py_BuildValue("sN", code, JM_py_from_rect(rect))); + } else { + LIST_APPEND_DROP(bdev->result, Py_BuildValue("sNN", code, JM_py_from_rect(rect), JM_EscapeStrFromStr(layer_name))); + } +} + +static void +jm_bbox_fill_path(fz_context *ctx, fz_device *dev, const fz_path *path, int even_odd, fz_matrix ctm, + fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params) +{ + jm_bbox_add_rect(ctx, dev, fz_bound_path(ctx, path, NULL, ctm), "fill-path"); +} + +static void +jm_bbox_stroke_path(fz_context *ctx, fz_device *dev, const fz_path *path, const fz_stroke_state *stroke, + fz_matrix ctm, fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params) +{ + jm_bbox_add_rect(ctx, dev, fz_bound_path(ctx, path, stroke, ctm), "stroke-path"); +} + +static void +jm_bbox_fill_text(fz_context *ctx, fz_device *dev, const fz_text *text, fz_matrix ctm, ...) +{ + jm_bbox_add_rect(ctx, dev, fz_bound_text(ctx, text, NULL, ctm), "fill-text"); +} + +static void +jm_bbox_ignore_text(fz_context *ctx, fz_device *dev, const fz_text *text, fz_matrix ctm) +{ + jm_bbox_add_rect(ctx, dev, fz_bound_text(ctx, text, NULL, ctm), "ignore-text"); +} + +static void +jm_bbox_stroke_text(fz_context *ctx, fz_device *dev, const fz_text *text, const fz_stroke_state *stroke, fz_matrix ctm, ...) +{ + jm_bbox_add_rect(ctx, dev, fz_bound_text(ctx, text, stroke, ctm), "stroke-text"); +} + +static void +jm_bbox_fill_shade(fz_context *ctx, fz_device *dev, fz_shade *shade, fz_matrix ctm, float alpha, fz_color_params color_params) +{ + jm_bbox_add_rect(ctx, dev, fz_bound_shade(ctx, shade, ctm), "fill-shade"); +} + +static void +jm_bbox_fill_image(fz_context *ctx, fz_device *dev, fz_image *image, fz_matrix ctm, float alpha, fz_color_params color_params) +{ + jm_bbox_add_rect(ctx, dev, fz_transform_rect(fz_unit_rect, ctm), "fill-image"); +} + +static void +jm_bbox_fill_image_mask(fz_context *ctx, fz_device *dev, fz_image *image, fz_matrix ctm, + fz_colorspace *colorspace, const float *color, float alpha, fz_color_params color_params) +{ + jm_bbox_add_rect(ctx, dev, fz_transform_rect(fz_unit_rect, ctm), "fill-imgmask"); +} + +fz_device * +JM_new_bbox_device(fz_context *ctx, PyObject *result, int layers) +{ + jm_bbox_device *dev = fz_new_derived_device(ctx, jm_bbox_device); + + dev->super.fill_path = jm_bbox_fill_path; + dev->super.stroke_path = jm_bbox_stroke_path; + dev->super.clip_path = NULL; + dev->super.clip_stroke_path = NULL; + + dev->super.fill_text = jm_bbox_fill_text; + dev->super.stroke_text = jm_bbox_stroke_text; + dev->super.clip_text = NULL; + dev->super.clip_stroke_text = NULL; + dev->super.ignore_text = jm_bbox_ignore_text; + + dev->super.fill_shade = jm_bbox_fill_shade; + dev->super.fill_image = jm_bbox_fill_image; + dev->super.fill_image_mask = jm_bbox_fill_image_mask; + dev->super.clip_image_mask = NULL; + + dev->super.pop_clip = NULL; + + dev->super.begin_mask = NULL; + dev->super.end_mask = NULL; + dev->super.begin_group = NULL; + dev->super.end_group = NULL; + + dev->super.begin_tile = NULL; + dev->super.end_tile = NULL; + + dev->super.begin_layer = jm_lineart_begin_layer; + dev->super.end_layer = jm_lineart_end_layer; + + dev->super.begin_structure = NULL; + dev->super.end_structure = NULL; + + dev->super.begin_metatext = NULL; + dev->super.end_metatext = NULL; + + dev->super.render_flags = NULL; + dev->super.set_default_colorspaces = NULL; + + dev->result = result; + dev->layers = layers; + trace_device_reset(); + + return (fz_device *)dev; +} + +%} diff --git a/fitz/helper-fields.i b/fitz/helper-fields.i new file mode 100644 index 0000000..b4e63df --- /dev/null +++ b/fitz/helper-fields.i @@ -0,0 +1,1121 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +#define SETATTR(a, v) PyObject_SetAttrString(Widget, a, v) +#define GETATTR(a) PyObject_GetAttrString(Widget, a) +#define CALLATTR(m, p) PyObject_CallMethod(Widget, m, p) + +static void +SETATTR_DROP(PyObject *mod, const char *attr, PyObject *value) +{ + if (!value) + PyObject_DelAttrString(mod, attr); + else + { + PyObject_SetAttrString(mod, attr, value); + Py_DECREF(value); + } +} + +//----------------------------------------------------------------------------- +// Functions dealing with PDF form fields (widgets) +//----------------------------------------------------------------------------- +enum +{ + SigFlag_SignaturesExist = 1, + SigFlag_AppendOnly = 2 +}; + + +// make new PDF action object from JavaScript source +// Parameters are a PDF document and a Python string. +// Returns a PDF action object. +//----------------------------------------------------------------------------- +pdf_obj * +JM_new_javascript(fz_context *ctx, pdf_document *pdf, PyObject *value) +{ + fz_buffer *res = NULL; + if (!PyObject_IsTrue(value)) // no argument given + return NULL; + + char *data = JM_StrAsChar(value); + if (!data) // not convertible to char* + return NULL; + + res = fz_new_buffer_from_copied_data(ctx, data, strlen(data)); + pdf_obj *source = pdf_add_stream(ctx, pdf, res, NULL, 0); + pdf_obj *newaction = pdf_add_new_dict(ctx, pdf, 4); + pdf_dict_put(ctx, newaction, PDF_NAME(S), pdf_new_name(ctx, "JavaScript")); + pdf_dict_put(ctx, newaction, PDF_NAME(JS), source); + fz_drop_buffer(ctx, res); + return pdf_keep_obj(ctx, newaction); +} + + +// JavaScript extractor +// Returns either the script source or None. Parameter is a PDF action +// dictionary, which must have keys /S and /JS. The value of /S must be +// '/JavaScript'. The value of /JS is returned. +//----------------------------------------------------------------------------- +PyObject * +JM_get_script(fz_context *ctx, pdf_obj *key) +{ + pdf_obj *js = NULL; + fz_buffer *res = NULL; + PyObject *script = NULL; + if (!key) Py_RETURN_NONE; + + if (!strcmp(pdf_to_name(ctx, + pdf_dict_get(ctx, key, PDF_NAME(S))), "JavaScript")) { + js = pdf_dict_get(ctx, key, PDF_NAME(JS)); + } + if (!js) Py_RETURN_NONE; + + if (pdf_is_string(ctx, js)) { + script = JM_UnicodeFromStr(pdf_to_text_string(ctx, js)); + } else if (pdf_is_stream(ctx, js)) { + res = pdf_load_stream(ctx, js); + script = JM_EscapeStrFromBuffer(ctx, res); + fz_drop_buffer(ctx, res); + } else { + Py_RETURN_NONE; + } + if (PyObject_IsTrue(script)) { // do not return an empty script + return script; + } + Py_CLEAR(script); + Py_RETURN_NONE; +} + + +// Create a JavaScript PDF action. +// Usable for all object types which support PDF actions, even if the +// argument name suggests annotations. Up to 2 key values can be specified, so +// JavaScript actions can be stored for '/A' and '/AA/?' keys. +//----------------------------------------------------------------------------- +void JM_put_script(fz_context *ctx, pdf_obj *annot_obj, pdf_obj *key1, pdf_obj *key2, PyObject *value) +{ + PyObject *script = NULL; + pdf_obj *key1_obj = pdf_dict_get(ctx, annot_obj, key1); + pdf_document *pdf = pdf_get_bound_document(ctx, annot_obj); // owning PDF + + // if no new script given, just delete corresponding key + if (!value || !PyObject_IsTrue(value)) { + if (!key2) { + pdf_dict_del(ctx, annot_obj, key1); + } else if (key1_obj) { + pdf_dict_del(ctx, key1_obj, key2); + } + return; + } + + // read any existing script as a PyUnicode string + if (!key2 || !key1_obj) { + script = JM_get_script(ctx, key1_obj); + } else { + script = JM_get_script(ctx, pdf_dict_get(ctx, key1_obj, key2)); + } + + // replace old script, if different from new one + if (!PyObject_RichCompareBool(value, script, Py_EQ)) { + pdf_obj *newaction = JM_new_javascript(ctx, pdf, value); + if (!key2) { + pdf_dict_put_drop(ctx, annot_obj, key1, newaction); + } else { + pdf_dict_putl_drop(ctx, annot_obj, newaction, key1, key2, NULL); + } + } + Py_XDECREF(script); + return; +} + +/* +// Execute a JavaScript action for annot or field. +//----------------------------------------------------------------------------- +PyObject * +JM_exec_script(fz_context *ctx, pdf_obj *annot_obj, pdf_obj *key1, pdf_obj *key2) +{ + PyObject *script = NULL; + char *code = NULL; + fz_try(ctx) { + pdf_document *pdf = pdf_get_bound_document(ctx, annot_obj); + char buf[100]; + if (!key2) { + script = JM_get_script(ctx, key1_obj); + } else { + script = JM_get_script(ctx, pdf_dict_get(ctx, key1_obj, key2)); + } + code = JM_StrAsChar(script); + fz_snprintf(buf, sizeof buf, "%d/A", pdf_to_num(ctx, annot_obj)); + pdf_js_execute(pdf->js, buf, code); + } + fz_always(ctx) { + Py_XDECREF(string); + } + fz_catch(ctx) { + Py_RETURN_FALSE; + } + Py_RETURN_TRUE; +} +*/ + +// String from widget type +//----------------------------------------------------------------------------- +char *JM_field_type_text(int wtype) +{ + switch(wtype) { + case(PDF_WIDGET_TYPE_BUTTON): + return "Button"; + case(PDF_WIDGET_TYPE_CHECKBOX): + return "CheckBox"; + case(PDF_WIDGET_TYPE_RADIOBUTTON): + return "RadioButton"; + case(PDF_WIDGET_TYPE_TEXT): + return "Text"; + case(PDF_WIDGET_TYPE_LISTBOX): + return "ListBox"; + case(PDF_WIDGET_TYPE_COMBOBOX): + return "ComboBox"; + case(PDF_WIDGET_TYPE_SIGNATURE): + return "Signature"; + default: + return "unknown"; + } +} + +// Set the field type +//----------------------------------------------------------------------------- +void JM_set_field_type(fz_context *ctx, pdf_document *doc, pdf_obj *obj, int type) +{ + int setbits = 0; + int clearbits = 0; + pdf_obj *typename = NULL; + + switch(type) { + case PDF_WIDGET_TYPE_BUTTON: + typename = PDF_NAME(Btn); + setbits = PDF_BTN_FIELD_IS_PUSHBUTTON; + break; + case PDF_WIDGET_TYPE_RADIOBUTTON: + typename = PDF_NAME(Btn); + clearbits = PDF_BTN_FIELD_IS_PUSHBUTTON; + setbits = PDF_BTN_FIELD_IS_RADIO; + break; + case PDF_WIDGET_TYPE_CHECKBOX: + typename = PDF_NAME(Btn); + clearbits = (PDF_BTN_FIELD_IS_PUSHBUTTON|PDF_BTN_FIELD_IS_RADIO); + break; + case PDF_WIDGET_TYPE_TEXT: + typename = PDF_NAME(Tx); + break; + case PDF_WIDGET_TYPE_LISTBOX: + typename = PDF_NAME(Ch); + clearbits = PDF_CH_FIELD_IS_COMBO; + break; + case PDF_WIDGET_TYPE_COMBOBOX: + typename = PDF_NAME(Ch); + setbits = PDF_CH_FIELD_IS_COMBO; + break; + case PDF_WIDGET_TYPE_SIGNATURE: + typename = PDF_NAME(Sig); + break; + } + + if (typename) + pdf_dict_put_drop(ctx, obj, PDF_NAME(FT), typename); + + if (setbits != 0 || clearbits != 0) { + int bits = pdf_dict_get_int(ctx, obj, PDF_NAME(Ff)); + bits &= ~clearbits; + bits |= setbits; + pdf_dict_put_int(ctx, obj, PDF_NAME(Ff), bits); + } +} + +// Copied from MuPDF v1.14 +// Create widget. +// Returns a kept reference to a pdf_annot - caller must drop it. +//----------------------------------------------------------------------------- +pdf_annot *JM_create_widget(fz_context *ctx, pdf_document *doc, pdf_page *page, int type, char *fieldname) +{ + pdf_obj *form = NULL; + int old_sigflags = pdf_to_int(ctx, pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/SigFlags")); + pdf_annot *annot = pdf_create_annot_raw(ctx, page, PDF_ANNOT_WIDGET); // returns a kept reference. + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + fz_try(ctx) { + JM_set_field_type(ctx, doc, annot_obj, type); + pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(T), fieldname); + + if (type == PDF_WIDGET_TYPE_SIGNATURE) { + int sigflags = (old_sigflags | (SigFlag_SignaturesExist|SigFlag_AppendOnly)); + pdf_dict_putl_drop(ctx, pdf_trailer(ctx, doc), pdf_new_int(ctx, sigflags), PDF_NAME(Root), PDF_NAME(AcroForm), PDF_NAME(SigFlags), NULL); + } + + /* + pdf_create_annot will have linked the new widget into the page's + annot array. We also need it linked into the document's form + */ + form = pdf_dict_getp(ctx, pdf_trailer(ctx, doc), "Root/AcroForm/Fields"); + if (!form) { + form = pdf_new_array(ctx, doc, 1); + pdf_dict_putl_drop(ctx, pdf_trailer(ctx, doc), + form, + PDF_NAME(Root), + PDF_NAME(AcroForm), + PDF_NAME(Fields), + NULL); + } + + pdf_array_push(ctx, form, annot_obj); // Cleanup relies on this statement being last + } + fz_catch(ctx) { + pdf_delete_annot(ctx, page, annot); + + if (type == PDF_WIDGET_TYPE_SIGNATURE) { + pdf_dict_putl_drop(ctx, pdf_trailer(ctx, doc), pdf_new_int(ctx, old_sigflags), PDF_NAME(Root), PDF_NAME(AcroForm), PDF_NAME(SigFlags), NULL); + } + + fz_rethrow(ctx); + } + + return annot; +} + + + +// PushButton get state +//----------------------------------------------------------------------------- +PyObject *JM_pushbtn_state(fz_context *ctx, pdf_annot *annot) +{ // pushed buttons do not reflect status changes in the PDF + // always reflect them as untouched + Py_RETURN_FALSE; +} + + +// Text field retrieve value +//----------------------------------------------------------------------------- +PyObject *JM_text_value(fz_context *ctx, pdf_annot *annot) +{ + const char *text = NULL; + fz_var(text); + fz_try(ctx) { + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + text = pdf_field_value(ctx, annot_obj); + } + fz_catch(ctx) Py_RETURN_NONE; + return JM_UnicodeFromStr(text); +} + +// ListBox retrieve value +//----------------------------------------------------------------------------- +PyObject *JM_listbox_value(fz_context *ctx, pdf_annot *annot) +{ + int i = 0, n = 0; + // may be single value or array + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + pdf_obj *optarr = pdf_dict_get(ctx, annot_obj, PDF_NAME(V)); + if (pdf_is_string(ctx, optarr)) // a single string + return PyString_FromString(pdf_to_text_string(ctx, optarr)); + + // value is an array (may have len 0) + n = pdf_array_len(ctx, optarr); + PyObject *liste = PyList_New(0); + + // extract a list of strings + // each entry may again be an array: take second entry then + for (i = 0; i < n; i++) { + pdf_obj *elem = pdf_array_get(ctx, optarr, i); + if (pdf_is_array(ctx, elem)) + elem = pdf_array_get(ctx, elem, 1); + LIST_APPEND_DROP(liste, JM_UnicodeFromStr(pdf_to_text_string(ctx, elem))); + } + return liste; +} + +// ComboBox retrieve value +//----------------------------------------------------------------------------- +PyObject *JM_combobox_value(fz_context *ctx, pdf_annot *annot) +{ // combobox treated like listbox + return JM_listbox_value(ctx, annot); +} + +// Signature field retrieve value +PyObject *JM_signature_value(fz_context *ctx, pdf_annot *annot) +{ // signatures are currently not supported + Py_RETURN_NONE; +} + +// retrieve ListBox / ComboBox choice values +//----------------------------------------------------------------------------- +PyObject *JM_choice_options(fz_context *ctx, pdf_annot *annot) +{ // return list of choices for list or combo boxes + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + PyObject *val; + int n = pdf_choice_widget_options(ctx, annot, 0, NULL); + if (n == 0) Py_RETURN_NONE; // wrong widget type + + pdf_obj *optarr = pdf_dict_get(ctx, annot_obj, PDF_NAME(Opt)); + int i, m; + PyObject *liste = PyList_New(0); + + for (i = 0; i < n; i++) { + m = pdf_array_len(ctx, pdf_array_get(ctx, optarr, i)); + if (m == 2) { + val = Py_BuildValue("ss", + pdf_to_text_string(ctx, pdf_array_get(ctx, pdf_array_get(ctx, optarr, i), 0)), + pdf_to_text_string(ctx, pdf_array_get(ctx, pdf_array_get(ctx, optarr, i), 1))); + LIST_APPEND_DROP(liste, val); + } else { + val = JM_UnicodeFromStr(pdf_to_text_string(ctx, pdf_array_get(ctx, optarr, i))); + LIST_APPEND_DROP(liste, val); + } + } + return liste; +} + + +// set ListBox / ComboBox values +//----------------------------------------------------------------------------- +void JM_set_choice_options(fz_context *ctx, pdf_annot *annot, PyObject *liste) +{ + if (!liste) return; + if (!PySequence_Check(liste)) return; + Py_ssize_t i, n = PySequence_Size(liste); + if (n < 1) return; + PyObject *tuple = PySequence_Tuple(liste); + PyObject *val = NULL, *val1 = NULL, *val2 = NULL; + pdf_obj *optarrsub = NULL, *optarr = NULL, *annot_obj = NULL; + pdf_document *pdf = NULL; + const char *opt = NULL, *opt1 = NULL, *opt2 = NULL; + fz_try(ctx) { + annot_obj = pdf_annot_obj(ctx, annot); + pdf = pdf_get_bound_document(ctx, annot_obj); + optarr = pdf_new_array(ctx, pdf, (int) n); + for (i = 0; i < n; i++) { + val = PyTuple_GET_ITEM(tuple, i); + opt = PyUnicode_AsUTF8(val); + if (opt) { + pdf_array_push_text_string(ctx, optarr, opt); + } else { + if (!PySequence_Check(val) || PySequence_Size(val) != 2) { + RAISEPY(ctx, "bad choice field list", PyExc_ValueError); + } + val1 = PySequence_GetItem(val, 0); + opt1 = PyUnicode_AsUTF8(val1); + if (!opt1) { + RAISEPY(ctx, "bad choice field list", PyExc_ValueError); + } + val2 = PySequence_GetItem(val, 1); + opt2 = PyUnicode_AsUTF8(val2); + if (!opt2) { + RAISEPY(ctx, "bad choice field list", PyExc_ValueError); + }; + Py_CLEAR(val1); + Py_CLEAR(val2); + optarrsub = pdf_array_push_array(ctx, optarr, 2); + pdf_array_push_text_string(ctx, optarrsub, opt1); + pdf_array_push_text_string(ctx, optarrsub, opt2); + } + } + pdf_dict_put_drop(ctx, annot_obj, PDF_NAME(Opt), optarr); + } + fz_always(ctx) { + Py_CLEAR(tuple); + Py_CLEAR(val1); + Py_CLEAR(val2); + PyErr_Clear(); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return; +} + + +//----------------------------------------------------------------------------- +// Populate a Python Widget object with the values from a PDF form field. +// Called by "Page.firstWidget" and "Widget.next". +//----------------------------------------------------------------------------- +void JM_get_widget_properties(fz_context *ctx, pdf_annot *annot, PyObject *Widget) +{ + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + pdf_page *page = pdf_annot_page(ctx, annot); + pdf_document *pdf = page->doc; + pdf_annot *tw = annot; + pdf_obj *obj = NULL; + Py_ssize_t i = 0, n = 0; + fz_try(ctx) { + int field_type = pdf_widget_type(ctx, tw); + SETATTR_DROP(Widget, "field_type", Py_BuildValue("i", field_type)); + if (field_type == PDF_WIDGET_TYPE_SIGNATURE) { + if (pdf_signature_is_signed(ctx, pdf, annot_obj)) { + SETATTR("is_signed", Py_True); + } else { + SETATTR("is_signed", Py_False); + } + } else { + SETATTR("is_signed", Py_None); + } + SETATTR_DROP(Widget, "border_style", + JM_UnicodeFromStr(pdf_field_border_style(ctx, annot_obj))); + SETATTR_DROP(Widget, "field_type_string", + JM_UnicodeFromStr(JM_field_type_text(field_type))); + + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR <= 22 + char *field_name = pdf_field_name(ctx, annot_obj); + #else + char *field_name = pdf_load_field_name(ctx, annot_obj); + #endif + SETATTR_DROP(Widget, "field_name", JM_UnicodeFromStr(field_name)); + JM_Free(field_name); + + const char *label = NULL; + obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(TU)); + if (obj) label = pdf_to_text_string(ctx, obj); + SETATTR_DROP(Widget, "field_label", JM_UnicodeFromStr(label)); + + const char *fvalue = NULL; + if (field_type == PDF_WIDGET_TYPE_RADIOBUTTON) { + obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(Parent)); // owning RB group + if (obj) { + SETATTR_DROP(Widget, "rb_parent", Py_BuildValue("i", pdf_to_num(ctx, obj))); + } + obj = pdf_dict_get(ctx, annot_obj, PDF_NAME(AS)); + if (obj) { + fvalue = pdf_to_name(ctx, obj); + } + } + if (!fvalue) { + fvalue = pdf_field_value(ctx, annot_obj); + } + SETATTR_DROP(Widget, "field_value", JM_UnicodeFromStr(fvalue)); + + SETATTR_DROP(Widget, "field_display", + Py_BuildValue("i", pdf_field_display(ctx, annot_obj))); + + float border_width = pdf_to_real(ctx, pdf_dict_getl(ctx, annot_obj, + PDF_NAME(BS), PDF_NAME(W), NULL)); + if (border_width == 0) border_width = 1; + SETATTR_DROP(Widget, "border_width", + Py_BuildValue("f", border_width)); + + obj = pdf_dict_getl(ctx, annot_obj, + PDF_NAME(BS), PDF_NAME(D), NULL); + if (pdf_is_array(ctx, obj)) { + n = (Py_ssize_t) pdf_array_len(ctx, obj); + PyObject *d = PyList_New(n); + for (i = 0; i < n; i++) { + PyList_SET_ITEM(d, i, Py_BuildValue("i", pdf_to_int(ctx, + pdf_array_get(ctx, obj, (int) i)))); + } + SETATTR_DROP(Widget, "border_dashes", d); + } + + SETATTR_DROP(Widget, "text_maxlen", + Py_BuildValue("i", pdf_text_widget_max_len(ctx, tw))); + + SETATTR_DROP(Widget, "text_format", + Py_BuildValue("i", pdf_text_widget_format(ctx, tw))); + + obj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(MK), PDF_NAME(BG), NULL); + if (pdf_is_array(ctx, obj)) { + n = (Py_ssize_t) pdf_array_len(ctx, obj); + PyObject *col = PyList_New(n); + for (i = 0; i < n; i++) { + PyList_SET_ITEM(col, i, Py_BuildValue("f", + pdf_to_real(ctx, pdf_array_get(ctx, obj, (int) i)))); + } + SETATTR_DROP(Widget, "fill_color", col); + } + + obj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(MK), PDF_NAME(BC), NULL); + if (pdf_is_array(ctx, obj)) { + n = (Py_ssize_t) pdf_array_len(ctx, obj); + PyObject *col = PyList_New(n); + for (i = 0; i < n; i++) { + PyList_SET_ITEM(col, i, Py_BuildValue("f", + pdf_to_real(ctx, pdf_array_get(ctx, obj, (int) i)))); + } + SETATTR_DROP(Widget, "border_color", col); + } + + SETATTR_DROP(Widget, "choice_values", JM_choice_options(ctx, annot)); + + const char *da = pdf_to_text_string(ctx, pdf_dict_get_inheritable(ctx, + annot_obj, PDF_NAME(DA))); + SETATTR_DROP(Widget, "_text_da", JM_UnicodeFromStr(da)); + + obj = pdf_dict_getl(ctx, annot_obj, PDF_NAME(MK), PDF_NAME(CA), NULL); + if (obj) { + SETATTR_DROP(Widget, "button_caption", + JM_UnicodeFromStr((char *)pdf_to_text_string(ctx, obj))); + } + + SETATTR_DROP(Widget, "field_flags", + Py_BuildValue("i", pdf_field_flags(ctx, annot_obj))); + + // call Py method to reconstruct text color, font name, size + PyObject *call = CALLATTR("_parse_da", NULL); + Py_XDECREF(call); + + // extract JavaScript action texts + SETATTR_DROP(Widget, "script", + JM_get_script(ctx, pdf_dict_get(ctx, annot_obj, PDF_NAME(A)))); + + SETATTR_DROP(Widget, "script_stroke", + JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(K), NULL))); + + SETATTR_DROP(Widget, "script_format", + JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(F), NULL))); + + SETATTR_DROP(Widget, "script_change", + JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(V), NULL))); + + SETATTR_DROP(Widget, "script_calc", + JM_get_script(ctx, pdf_dict_getl(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(C), NULL))); + } + fz_always(ctx) PyErr_Clear(); + fz_catch(ctx) fz_rethrow(ctx); + return; +} + + +//----------------------------------------------------------------------------- +// Update the PDF form field with the properties from a Python Widget object. +// Called by "Page.addWidget" and "Annot.updateWidget". +//----------------------------------------------------------------------------- +void JM_set_widget_properties(fz_context *ctx, pdf_annot *annot, PyObject *Widget) +{ + pdf_page *page = pdf_annot_page(ctx, annot); + pdf_obj *annot_obj = pdf_annot_obj(ctx, annot); + pdf_document *pdf = page->doc; + fz_rect rect; + pdf_obj *fill_col = NULL, *border_col = NULL; + pdf_obj *dashes = NULL; + Py_ssize_t i, n = 0; + int d; + PyObject *value = GETATTR("field_type"); + int field_type = (int) PyInt_AsLong(value); + Py_DECREF(value); + + // rectangle -------------------------------------------------------------- + value = GETATTR("rect"); + rect = JM_rect_from_py(value); + Py_XDECREF(value); + fz_matrix rot_mat = JM_rotate_page_matrix(ctx, page); + rect = fz_transform_rect(rect, rot_mat); + pdf_set_annot_rect(ctx, annot, rect); + + // fill color ------------------------------------------------------------- + value = GETATTR("fill_color"); + if (value && PySequence_Check(value)) { + n = PySequence_Size(value); + fill_col = pdf_new_array(ctx, pdf, n); + double col = 0; + for (i = 0; i < n; i++) { + JM_FLOAT_ITEM(value, i, &col); + pdf_array_push_real(ctx, fill_col, col); + } + pdf_field_set_fill_color(ctx, annot_obj, fill_col); + pdf_drop_obj(ctx, fill_col); + } + Py_XDECREF(value); + + // dashes ----------------------------------------------------------------- + value = GETATTR("border_dashes"); + if (value && PySequence_Check(value)) { + n = PySequence_Size(value); + dashes = pdf_new_array(ctx, pdf, n); + for (i = 0; i < n; i++) { + pdf_array_push_int(ctx, dashes, + (int64_t) PyInt_AsLong(PySequence_ITEM(value, i))); + } + pdf_dict_putl_drop(ctx, annot_obj, dashes, + PDF_NAME(BS), + PDF_NAME(D), + NULL); + } + Py_XDECREF(value); + + // border color ----------------------------------------------------------- + value = GETATTR("border_color"); + if (value && PySequence_Check(value)) { + n = PySequence_Size(value); + border_col = pdf_new_array(ctx, pdf, n); + double col = 0; + for (i = 0; i < n; i++) { + JM_FLOAT_ITEM(value, i, &col); + pdf_array_push_real(ctx, border_col, col); + } + pdf_dict_putl_drop(ctx, annot_obj, border_col, + PDF_NAME(MK), + PDF_NAME(BC), + NULL); + } + Py_XDECREF(value); + + // entry ignored - may be used later + /* + int text_format = (int) PyInt_AsLong(GETATTR("text_format")); + */ + + // field label ----------------------------------------------------------- + value = GETATTR("field_label"); + if (value != Py_None) { + char *label = JM_StrAsChar(value); + pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(TU), label); + } + Py_XDECREF(value); + + // field name ------------------------------------------------------------- + value = GETATTR("field_name"); + if (value != Py_None) { + char *name = JM_StrAsChar(value); + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR <= 22 + char *old_name = pdf_field_name(ctx, annot_obj); + #else + char *old_name = pdf_load_field_name(ctx, annot_obj); + #endif + if (strcmp(name, old_name) != 0) { + pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(T), name); + } + JM_Free(old_name); + } + Py_XDECREF(value); + + // max text len ----------------------------------------------------------- + if (field_type == PDF_WIDGET_TYPE_TEXT) + { + value = GETATTR("text_maxlen"); + int text_maxlen = (int) PyInt_AsLong(value); + if (text_maxlen) { + pdf_dict_put_int(ctx, annot_obj, PDF_NAME(MaxLen), text_maxlen); + } + Py_XDECREF(value); + } + value = GETATTR("field_display"); + d = (int) PyInt_AsLong(value); + Py_XDECREF(value); + pdf_field_set_display(ctx, annot_obj, d); + + // choice values ---------------------------------------------------------- + if (field_type == PDF_WIDGET_TYPE_LISTBOX || + field_type == PDF_WIDGET_TYPE_COMBOBOX) { + value = GETATTR("choice_values"); + JM_set_choice_options(ctx, annot, value); + Py_XDECREF(value); + } + + // border style ----------------------------------------------------------- + value = GETATTR("border_style"); + pdf_obj *val = JM_get_border_style(ctx, value); + Py_XDECREF(value); + pdf_dict_putl_drop(ctx, annot_obj, val, + PDF_NAME(BS), + PDF_NAME(S), + NULL); + + // border width ----------------------------------------------------------- + value = GETATTR("border_width"); + float border_width = (float) PyFloat_AsDouble(value); + Py_XDECREF(value); + pdf_dict_putl_drop(ctx, annot_obj, pdf_new_real(ctx, border_width), + PDF_NAME(BS), + PDF_NAME(W), + NULL); + + // /DA string ------------------------------------------------------------- + value = GETATTR("_text_da"); + char *da = JM_StrAsChar(value); + Py_XDECREF(value); + pdf_dict_put_text_string(ctx, annot_obj, PDF_NAME(DA), da); + pdf_dict_del(ctx, annot_obj, PDF_NAME(DS)); /* not supported by MuPDF */ + pdf_dict_del(ctx, annot_obj, PDF_NAME(RC)); /* not supported by MuPDF */ + + // field flags ------------------------------------------------------------ + value = GETATTR("field_flags"); + int field_flags = (int) PyInt_AsLong(value); + Py_XDECREF(value); + if (!PyErr_Occurred()) { + if (field_type == PDF_WIDGET_TYPE_COMBOBOX) { + field_flags |= PDF_CH_FIELD_IS_COMBO; + } else if (field_type == PDF_WIDGET_TYPE_RADIOBUTTON) { + field_flags |= PDF_BTN_FIELD_IS_RADIO; + } else if (field_type == PDF_WIDGET_TYPE_BUTTON) { + field_flags |= PDF_BTN_FIELD_IS_PUSHBUTTON; + } + pdf_dict_put_int(ctx, annot_obj, PDF_NAME(Ff), field_flags); + } + + // button caption --------------------------------------------------------- + value = GETATTR("button_caption"); + char *ca = JM_StrAsChar(value); + if (ca) { + pdf_field_set_button_caption(ctx, annot_obj, ca); + } + Py_XDECREF(value); + + // script (/A) ------------------------------------------------------- + value = GETATTR("script"); + JM_put_script(ctx, annot_obj, PDF_NAME(A), NULL, value); + Py_CLEAR(value); + + // script (/AA/K) ------------------------------------------------------- + value = GETATTR("script_stroke"); + JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(K), value); + Py_CLEAR(value); + + // script (/AA/F) ------------------------------------------------------- + value = GETATTR("script_format"); + JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(F), value); + Py_CLEAR(value); + + // script (/AA/V) ------------------------------------------------------- + value = GETATTR("script_change"); + JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(V), value); + Py_CLEAR(value); + + // script (/AA/C) ------------------------------------------------------- + value = GETATTR("script_calc"); + JM_put_script(ctx, annot_obj, PDF_NAME(AA), PDF_NAME(C), value); + Py_CLEAR(value); + + // field value ------------------------------------------------------------ + value = GETATTR("field_value"); // field value + char *text = JM_StrAsChar(value); // convert to text (may fail!) + + switch(field_type) + { + case PDF_WIDGET_TYPE_RADIOBUTTON: + if (PyObject_RichCompareBool(value, Py_False, Py_EQ)) { + pdf_set_field_value(ctx, pdf, annot_obj, "Off", 1); + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), "Off"); + } else { + pdf_obj *onstate = pdf_button_field_on_state(ctx, annot_obj); + if (onstate) { + const char *on = pdf_to_name(ctx, onstate); + pdf_set_field_value(ctx, pdf, annot_obj, on, 1); + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), on); + } else if (text) { + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), text); + } + } + break; + + case PDF_WIDGET_TYPE_CHECKBOX: // will always be "Yes" or "Off" + if (PyObject_RichCompareBool(value, Py_True, Py_EQ) || text && strcmp(text, "Yes")==0) { + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), "Yes"); + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(V), "Yes"); + } else { + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(AS), "Off"); + pdf_dict_put_name(gctx, annot_obj, PDF_NAME(V), "Off"); + } + break; + + default: + if (text) { + pdf_set_field_value(ctx, pdf, annot_obj, (const char *)text, 1); + if (field_type == PDF_WIDGET_TYPE_COMBOBOX || field_type == PDF_WIDGET_TYPE_LISTBOX) { + pdf_dict_del(ctx, annot_obj, PDF_NAME(I)); + } + } + } + Py_CLEAR(value); + PyErr_Clear(); + pdf_dirty_annot(ctx, annot); + pdf_set_annot_hot(ctx, annot, 1); + pdf_set_annot_active(ctx, annot, 1); + pdf_update_annot(ctx, annot); +} +#undef SETATTR +#undef GETATTR +#undef CALLATTR +%} + +%pythoncode %{ +#------------------------------------------------------------------------------ +# Class describing a PDF form field ("widget") +#------------------------------------------------------------------------------ +class Widget(object): + def __init__(self): + self.thisown = True + self.border_color = None + self.border_style = "S" + self.border_width = 0 + self.border_dashes = None + self.choice_values = None # choice fields only + self.rb_parent = None # radio buttons only: xref of owning parent + + self.field_name = None # field name + self.field_label = None # field label + self.field_value = None + self.field_flags = 0 + self.field_display = 0 + self.field_type = 0 # valid range 1 through 7 + self.field_type_string = None # field type as string + + self.fill_color = None + self.button_caption = None # button caption + self.is_signed = None # True / False if signature + self.text_color = (0, 0, 0) + self.text_font = "Helv" + self.text_fontsize = 0 + self.text_maxlen = 0 # text fields only + self.text_format = 0 # text fields only + self._text_da = "" # /DA = default apparance + + self.script = None # JavaScript (/A) + self.script_stroke = None # JavaScript (/AA/K) + self.script_format = None # JavaScript (/AA/F) + self.script_change = None # JavaScript (/AA/V) + self.script_calc = None # JavaScript (/AA/C) + + self.rect = None # annot value + self.xref = 0 # annot value + + + def _validate(self): + """Validate the class entries. + """ + if (self.rect.is_infinite + or self.rect.is_empty + ): + raise ValueError("bad rect") + + if not self.field_name: + raise ValueError("field name missing") + + if self.field_label == "Unnamed": + self.field_label = None + CheckColor(self.border_color) + CheckColor(self.fill_color) + if not self.text_color: + self.text_color = (0, 0, 0) + CheckColor(self.text_color) + + if not self.border_width: + self.border_width = 0 + + if not self.text_fontsize: + self.text_fontsize = 0 + + self.border_style = self.border_style.upper()[0:1] + + # standardize content of JavaScript entries + btn_type = self.field_type in ( + PDF_WIDGET_TYPE_BUTTON, + PDF_WIDGET_TYPE_CHECKBOX, + PDF_WIDGET_TYPE_RADIOBUTTON + ) + if not self.script: + self.script = None + elif type(self.script) is not str: + raise ValueError("script content must be a string") + + # buttons cannot have the following script actions + if btn_type or not self.script_calc: + self.script_calc = None + elif type(self.script_calc) is not str: + raise ValueError("script_calc content must be a string") + + if btn_type or not self.script_change: + self.script_change = None + elif type(self.script_change) is not str: + raise ValueError("script_change content must be a string") + + if btn_type or not self.script_format: + self.script_format = None + elif type(self.script_format) is not str: + raise ValueError("script_format content must be a string") + + if btn_type or not self.script_stroke: + self.script_stroke = None + elif type(self.script_stroke) is not str: + raise ValueError("script_stroke content must be a string") + + self._checker() # any field_type specific checks + + + def _adjust_font(self): + """Ensure text_font is correctly spelled if empty or from our list. + + Otherwise assume the font is in an existing field. + """ + if not self.text_font: + self.text_font = "Helv" + return + doc = self.parent.parent + for f in doc.FormFonts + ["Cour", "TiRo", "Helv", "ZaDb"]: + if self.text_font.lower() == f.lower(): + self.text_font = f + return + self.text_font = "Helv" + return + + + def _parse_da(self): + """Extract font name, size and color from default appearance string (/DA object). + + Equivalent to 'pdf_parse_default_appearance' function in MuPDF's 'pdf-annot.c'. + """ + if not self._text_da: + return + font = "Helv" + fsize = 0 + col = (0, 0, 0) + dat = self._text_da.split() # split on any whitespace + for i, item in enumerate(dat): + if item == "Tf": + font = dat[i - 2][1:] + fsize = float(dat[i - 1]) + dat[i] = dat[i-1] = dat[i-2] = "" + continue + if item == "g": # unicolor text + col = [(float(dat[i - 1]))] + dat[i] = dat[i-1] = "" + continue + if item == "rg": # RGB colored text + col = [float(f) for f in dat[i - 3:i]] + dat[i] = dat[i-1] = dat[i-2] = dat[i-3] = "" + continue + self.text_font = font + self.text_fontsize = fsize + self.text_color = col + self._text_da = "" + return + + + def _checker(self): + """Any widget type checks. + """ + if self.field_type not in range(1, 8): + raise ValueError("bad field type") + + + # if setting a radio button to ON, first set Off all buttons + # in the group - this is not done by MuPDF: + if self.field_type == PDF_WIDGET_TYPE_RADIOBUTTON and self.field_value not in (False, "Off") and hasattr(self, "parent"): + # so we are about setting this button to ON/True + # check other buttons in same group and set them to 'Off' + doc = self.parent.parent + kids_type, kids_value = doc.xref_get_key(self.xref, "Parent/Kids") + if kids_type == "array": + doc.xref_set_key(self.xref, "Parent/V", "(Off)") # set off old value + xrefs = tuple(map(int, kids_value[1:-1].replace("0 R","").split())) + for xref in xrefs: + if xref != self.xref: + doc.xref_set_key(xref, "AS", "/Off") + # the calling method will now set the intended button to on and + # will find everything prepared for correct functioning. + + + def update(self): + """Reflect Python object in the PDF. + """ + doc = self.parent.parent + self._validate() + + self._adjust_font() # ensure valid text_font name + + # now create the /DA string + self._text_da = "" + if len(self.text_color) == 3: + fmt = "{:g} {:g} {:g} rg /{f:s} {s:g} Tf" + self._text_da + elif len(self.text_color) == 1: + fmt = "{:g} g /{f:s} {s:g} Tf" + self._text_da + elif len(self.text_color) == 4: + fmt = "{:g} {:g} {:g} {:g} k /{f:s} {s:g} Tf" + self._text_da + self._text_da = fmt.format(*self.text_color, f=self.text_font, + s=self.text_fontsize) + + # if widget has a '/AA/C' script, make sure it is in the '/CO' + # array of the '/AcroForm' dictionary. + if self.script_calc: # there is a "calculation" script: + # make sure we are in the /CO array + util_ensure_widget_calc(self._annot) + + # finally update the widget + TOOLS._save_widget(self._annot, self) + self._text_da = "" + + + def button_states(self): + """Return the on/off state names for button widgets. + + A button may have 'normal' or 'pressed down' appearances. While the 'Off' + state is usually called like this, the 'On' state is often given a name + relating to the functional context. + """ + if self.field_type not in (2, 5): + return None # no button type + if hasattr(self, "parent"): # field already exists on page + doc = self.parent.parent + else: + return None + xref = self.xref + states = {"normal": None, "down": None} + APN = doc.xref_get_key(xref, "AP/N") + if APN[0] == "dict": + nstates = [] + APN = APN[1][2:-2] + apnt = APN.split("/")[1:] + for x in apnt: + nstates.append(x.split()[0]) + states["normal"] = nstates + APD = doc.xref_get_key(xref, "AP/D") + if APD[0] == "dict": + dstates = [] + APD = APD[1][2:-2] + apdt = APD.split("/")[1:] + for x in apdt: + dstates.append(x.split()[0]) + states["down"] = dstates + return states + + def on_state(self): + """Return the "On" value for button widgets. + + This is useful for radio buttons mainly. Checkboxes will always return + "Yes". Radio buttons will return the string that is unequal to "Off" + as returned by method button_states(). + If the radio button is new / being created, it does not yet have an + "On" value. In this case, a warning is shown and True is returned. + """ + if self.field_type not in (2, 5): + return None # no checkbox or radio button + if self.field_type == 2: + return "Yes" + bstate = self.button_states() + if bstate==None: + bstate = {} + for k in bstate.keys(): + for v in bstate[k]: + if v != "Off": + return v + print("warning: radio button has no 'On' value.") + return True + + def reset(self): + """Reset the field value to its default. + """ + TOOLS._reset_widget(self._annot) + + def __repr__(self): + return "'%s' widget on %s" % (self.field_type_string, str(self.parent)) + + def __del__(self): + if hasattr(self, "_annot"): + del self._annot + + @property + def next(self): + return self._annot.next +%} diff --git a/fitz/helper-fileobj.i b/fitz/helper-fileobj.i new file mode 100644 index 0000000..9d22609 --- /dev/null +++ b/fitz/helper-fileobj.i @@ -0,0 +1,113 @@ +%{ +//------------------------------------- +// fz_output for Python file objects +//------------------------------------- +static void +JM_bytesio_write(fz_context *ctx, void *opaque, const void *data, size_t len) +{ // bio.write(bytes object) + PyObject *bio = opaque, *b, *name, *rc; + fz_try(ctx){ + b = PyBytes_FromStringAndSize((const char *) data, (Py_ssize_t) len); + name = PyUnicode_FromString("write"); + PyObject_CallMethodObjArgs(bio, name, b, NULL); + rc = PyErr_Occurred(); + if (rc) { + RAISEPY(ctx, "could not write to Py file obj", rc); + } + } + fz_always(ctx) { + Py_XDECREF(b); + Py_XDECREF(name); + Py_XDECREF(rc); + PyErr_Clear(); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } +} + +static void +JM_bytesio_truncate(fz_context *ctx, void *opaque) +{ // bio.truncate(bio.tell()) !!! + PyObject *bio = opaque, *trunc = NULL, *tell = NULL, *rctell= NULL, *rc = NULL; + fz_try(ctx) { + trunc = PyUnicode_FromString("truncate"); + tell = PyUnicode_FromString("tell"); + rctell = PyObject_CallMethodObjArgs(bio, tell, NULL); + PyObject_CallMethodObjArgs(bio, trunc, rctell, NULL); + rc = PyErr_Occurred(); + if (rc) { + RAISEPY(ctx, "could not truncate Py file obj", rc); + } + } + fz_always(ctx) { + Py_XDECREF(tell); + Py_XDECREF(trunc); + Py_XDECREF(rc); + Py_XDECREF(rctell); + PyErr_Clear(); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } +} + +static int64_t +JM_bytesio_tell(fz_context *ctx, void *opaque) +{ // returns bio.tell() -> int + PyObject *bio = opaque, *rc = NULL, *name = NULL; + int64_t pos = 0; + fz_try(ctx) { + name = PyUnicode_FromString("tell"); + rc = PyObject_CallMethodObjArgs(bio, name, NULL); + if (!rc) { + RAISEPY(ctx, "could not tell Py file obj", PyErr_Occurred()); + } + pos = (int64_t) PyLong_AsUnsignedLongLong(rc); + } + fz_always(ctx) { + Py_XDECREF(name); + Py_XDECREF(rc); + PyErr_Clear(); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return pos; +} + + +static void +JM_bytesio_seek(fz_context *ctx, void *opaque, int64_t off, int whence) +{ // bio.seek(off, whence=0) + PyObject *bio = opaque, *rc = NULL, *name = NULL, *pos = NULL; + fz_try(ctx) { + name = PyUnicode_FromString("seek"); + pos = PyLong_FromUnsignedLongLong((unsigned long long) off); + PyObject_CallMethodObjArgs(bio, name, pos, whence, NULL); + rc = PyErr_Occurred(); + if (rc) { + RAISEPY(ctx, "could not seek Py file obj", rc); + } + } + fz_always(ctx) { + Py_XDECREF(rc); + Py_XDECREF(name); + Py_XDECREF(pos); + PyErr_Clear(); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } +} + +fz_output * +JM_new_output_fileptr(fz_context *ctx, PyObject *bio) +{ + fz_output *out = fz_new_output(ctx, 0, bio, JM_bytesio_write, NULL, NULL); + out->seek = JM_bytesio_seek; + out->tell = JM_bytesio_tell; + out->truncate = JM_bytesio_truncate; + return out; +} +%} diff --git a/fitz/helper-geo-c.i b/fitz/helper-geo-c.i new file mode 100644 index 0000000..948d6c0 --- /dev/null +++ b/fitz/helper-geo-c.i @@ -0,0 +1,243 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +//----------------------------------------------------------------------------- +// Functions converting betwenn PySequences and fitz geometry objects +//----------------------------------------------------------------------------- +static int +JM_INT_ITEM(PyObject *obj, Py_ssize_t idx, int *result) +{ + PyObject *temp = PySequence_ITEM(obj, idx); + if (!temp) return 1; + if (PyLong_Check(temp)) { + *result = (int) PyLong_AsLong(temp); + Py_DECREF(temp); + } else if (PyFloat_Check(temp)) { + *result = (int) PyFloat_AsDouble(temp); + Py_DECREF(temp); + } else { + Py_DECREF(temp); + return 1; + } + if (PyErr_Occurred()) { + PyErr_Clear(); + return 1; + } + return 0; +} + +static int +JM_FLOAT_ITEM(PyObject *obj, Py_ssize_t idx, double *result) +{ + PyObject *temp = PySequence_ITEM(obj, idx); + if (!temp) return 1; + *result = PyFloat_AsDouble(temp); + Py_DECREF(temp); + if (PyErr_Occurred()) { + PyErr_Clear(); + return 1; + } + return 0; +} + + +static fz_point +JM_normalize_vector(float x, float y) +{ + double px = x, py = y, len = (double) (x * x + y * y); + + if (len != 0) { + len = sqrt(len); + px /= len; + py /= len; + } + return fz_make_point((float) px, (float) py); +} + + +//----------------------------------------------------------------------------- +// PySequence to fz_rect. Default: infinite rect +//----------------------------------------------------------------------------- +static fz_rect +JM_rect_from_py(PyObject *r) +{ + if (!r || !PySequence_Check(r) || PySequence_Size(r) != 4) + return fz_infinite_rect; + Py_ssize_t i; + double f[4]; + + for (i = 0; i < 4; i++) { + if (JM_FLOAT_ITEM(r, i, &f[i]) == 1) return fz_infinite_rect; + if (f[i] < FZ_MIN_INF_RECT) f[i] = FZ_MIN_INF_RECT; + if (f[i] > FZ_MAX_INF_RECT) f[i] = FZ_MAX_INF_RECT; + } + + return fz_make_rect((float) f[0], (float) f[1], (float) f[2], (float) f[3]); +} + +//----------------------------------------------------------------------------- +// PySequence from fz_rect +//----------------------------------------------------------------------------- +static PyObject * +JM_py_from_rect(fz_rect r) +{ + return Py_BuildValue("ffff", r.x0, r.y0, r.x1, r.y1); +} + +//----------------------------------------------------------------------------- +// PySequence to fz_irect. Default: infinite irect +//----------------------------------------------------------------------------- +static fz_irect +JM_irect_from_py(PyObject *r) +{ + if (!r || !PySequence_Check(r) || PySequence_Size(r) != 4) + return fz_infinite_irect; + int x[4]; + Py_ssize_t i; + + for (i = 0; i < 4; i++) { + if (JM_INT_ITEM(r, i, &x[i]) == 1) return fz_infinite_irect; + if (x[i] < FZ_MIN_INF_RECT) x[i] = FZ_MIN_INF_RECT; + if (x[i] > FZ_MAX_INF_RECT) x[i] = FZ_MAX_INF_RECT; + } + + return fz_make_irect(x[0], x[1], x[2], x[3]); +} + +//----------------------------------------------------------------------------- +// PySequence from fz_irect +//----------------------------------------------------------------------------- +static PyObject * +JM_py_from_irect(fz_irect r) +{ + return Py_BuildValue("iiii", r.x0, r.y0, r.x1, r.y1); +} + + +//----------------------------------------------------------------------------- +// PySequence to fz_point. Default: (FZ_MIN_INF_RECT, FZ_MIN_INF_RECT) +//----------------------------------------------------------------------------- +static fz_point +JM_point_from_py(PyObject *p) +{ + fz_point p0 = fz_make_point(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT); + double x, y; + + if (!p || !PySequence_Check(p) || PySequence_Size(p) != 2) + return p0; + + if (JM_FLOAT_ITEM(p, 0, &x) == 1) return p0; + if (JM_FLOAT_ITEM(p, 1, &y) == 1) return p0; + if (x < FZ_MIN_INF_RECT) x = FZ_MIN_INF_RECT; + if (y < FZ_MIN_INF_RECT) y = FZ_MIN_INF_RECT; + if (x > FZ_MAX_INF_RECT) x = FZ_MAX_INF_RECT; + if (y > FZ_MAX_INF_RECT) y = FZ_MAX_INF_RECT; + + return fz_make_point((float) x, (float) y); +} + +//----------------------------------------------------------------------------- +// PySequence from fz_point +//----------------------------------------------------------------------------- +static PyObject * +JM_py_from_point(fz_point p) +{ + return Py_BuildValue("ff", p.x, p.y); +} + + +//----------------------------------------------------------------------------- +// PySequence to fz_matrix. Default: fz_identity +//----------------------------------------------------------------------------- +static fz_matrix +JM_matrix_from_py(PyObject *m) +{ + Py_ssize_t i; + double a[6]; + + if (!m || !PySequence_Check(m) || PySequence_Size(m) != 6) + return fz_identity; + + for (i = 0; i < 6; i++) + if (JM_FLOAT_ITEM(m, i, &a[i]) == 1) return fz_identity; + + return fz_make_matrix((float) a[0], (float) a[1], (float) a[2], (float) a[3], (float) a[4], (float) a[5]); +} + +//----------------------------------------------------------------------------- +// PySequence from fz_matrix +//----------------------------------------------------------------------------- +static PyObject * +JM_py_from_matrix(fz_matrix m) +{ + return Py_BuildValue("ffffff", m.a, m.b, m.c, m.d, m.e, m.f); +} + +//----------------------------------------------------------------------------- +// fz_quad from PySequence. Four floats are treated as rect. +// Else must be four pairs of floats. +//----------------------------------------------------------------------------- +static fz_quad +JM_quad_from_py(PyObject *r) +{ + fz_quad q = fz_make_quad(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, + FZ_MAX_INF_RECT, FZ_MIN_INF_RECT, + FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, + FZ_MAX_INF_RECT, FZ_MAX_INF_RECT); + fz_point p[4]; + double test, x, y; + Py_ssize_t i; + PyObject *obj = NULL; + + if (!r || !PySequence_Check(r) || PySequence_Size(r) != 4) + return q; + + if (JM_FLOAT_ITEM(r, 0, &test) == 0) + return fz_quad_from_rect(JM_rect_from_py(r)); + + for (i = 0; i < 4; i++) { + obj = PySequence_ITEM(r, i); // next point item + if (!obj || !PySequence_Check(obj) || PySequence_Size(obj) != 2) + goto exit_result; // invalid: cancel the rest + + if (JM_FLOAT_ITEM(obj, 0, &x) == 1) goto exit_result; + if (JM_FLOAT_ITEM(obj, 1, &y) == 1) goto exit_result; + if (x < FZ_MIN_INF_RECT) x = FZ_MIN_INF_RECT; + if (y < FZ_MIN_INF_RECT) y = FZ_MIN_INF_RECT; + if (x > FZ_MAX_INF_RECT) x = FZ_MAX_INF_RECT; + if (y > FZ_MAX_INF_RECT) y = FZ_MAX_INF_RECT; + p[i] = fz_make_point((float) x, (float) y); + + Py_CLEAR(obj); + } + q.ul = p[0]; + q.ur = p[1]; + q.ll = p[2]; + q.lr = p[3]; + return q; + + exit_result:; + Py_CLEAR(obj); + return q; +} + +//----------------------------------------------------------------------------- +// PySequence from fz_quad. +//----------------------------------------------------------------------------- +static PyObject * +JM_py_from_quad(fz_quad q) +{ + return Py_BuildValue("((f,f),(f,f),(f,f),(f,f))", + q.ul.x, q.ul.y, q.ur.x, q.ur.y, + q.ll.x, q.ll.y, q.lr.x, q.lr.y); +} + +%} diff --git a/fitz/helper-geo-py.i b/fitz/helper-geo-py.i new file mode 100644 index 0000000..85d7978 --- /dev/null +++ b/fitz/helper-geo-py.i @@ -0,0 +1,1155 @@ +%pythoncode %{ + +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ + +# largest 32bit integers surviving C float conversion roundtrips +# used by MuPDF to define infinite rectangles +FZ_MIN_INF_RECT = -0x80000000 +FZ_MAX_INF_RECT = 0x7fffff80 + + +class Matrix(object): + """Matrix() - all zeros + Matrix(a, b, c, d, e, f) + Matrix(zoom-x, zoom-y) - zoom + Matrix(shear-x, shear-y, 1) - shear + Matrix(degree) - rotate + Matrix(Matrix) - new copy + Matrix(sequence) - from 'sequence'""" + def __init__(self, *args): + if not args: + self.a = self.b = self.c = self.d = self.e = self.f = 0.0 + return None + if len(args) > 6: + raise ValueError("Matrix: bad seq len") + if len(args) == 6: # 6 numbers + self.a, self.b, self.c, self.d, self.e, self.f = map(float, args) + return None + if len(args) == 1: # either an angle or a sequ + if hasattr(args[0], "__float__"): + theta = math.radians(args[0]) + c = round(math.cos(theta), 12) + s = round(math.sin(theta), 12) + self.a = self.d = c + self.b = s + self.c = -s + self.e = self.f = 0.0 + return None + else: + self.a, self.b, self.c, self.d, self.e, self.f = map(float, args[0]) + return None + if len(args) == 2 or len(args) == 3 and args[2] == 0: + self.a, self.b, self.c, self.d, self.e, self.f = float(args[0]), \ + 0.0, 0.0, float(args[1]), 0.0, 0.0 + return None + if len(args) == 3 and args[2] == 1: + self.a, self.b, self.c, self.d, self.e, self.f = 1.0, \ + float(args[1]), float(args[0]), 1.0, 0.0, 0.0 + return None + raise ValueError("Matrix: bad args") + + def invert(self, src=None): + """Calculate the inverted matrix. Return 0 if successful and replace + current one. Else return 1 and do nothing. + """ + if src is None: + dst = util_invert_matrix(self) + else: + dst = util_invert_matrix(src) + if dst[0] == 1: + return 1 + self.a, self.b, self.c, self.d, self.e, self.f = dst[1] + return 0 + + def pretranslate(self, tx, ty): + """Calculate pre translation and replace current matrix.""" + tx = float(tx) + ty = float(ty) + self.e += tx * self.a + ty * self.c + self.f += tx * self.b + ty * self.d + return self + + def prescale(self, sx, sy): + """Calculate pre scaling and replace current matrix.""" + sx = float(sx) + sy = float(sy) + self.a *= sx + self.b *= sx + self.c *= sy + self.d *= sy + return self + + def preshear(self, h, v): + """Calculate pre shearing and replace current matrix.""" + h = float(h) + v = float(v) + a, b = self.a, self.b + self.a += v * self.c + self.b += v * self.d + self.c += h * a + self.d += h * b + return self + + def prerotate(self, theta): + """Calculate pre rotation and replace current matrix.""" + theta = float(theta) + while theta < 0: theta += 360 + while theta >= 360: theta -= 360 + if abs(0 - theta) < EPSILON: + pass + + elif abs(90.0 - theta) < EPSILON: + a = self.a + b = self.b + self.a = self.c + self.b = self.d + self.c = -a + self.d = -b + + elif abs(180.0 - theta) < EPSILON: + self.a = -self.a + self.b = -self.b + self.c = -self.c + self.d = -self.d + + elif abs(270.0 - theta) < EPSILON: + a = self.a + b = self.b + self.a = -self.c + self.b = -self.d + self.c = a + self.d = b + + else: + rad = math.radians(theta) + s = math.sin(rad) + c = math.cos(rad) + a = self.a + b = self.b + self.a = c * a + s * self.c + self.b = c * b + s * self.d + self.c =-s * a + c * self.c + self.d =-s * b + c * self.d + + return self + + def concat(self, one, two): + """Multiply two matrices and replace current one.""" + if not len(one) == len(two) == 6: + raise ValueError("Matrix: bad seq len") + self.a, self.b, self.c, self.d, self.e, self.f = util_concat_matrix(one, two) + return self + + def __getitem__(self, i): + return (self.a, self.b, self.c, self.d, self.e, self.f)[i] + + def __setitem__(self, i, v): + v = float(v) + if i == 0: self.a = v + elif i == 1: self.b = v + elif i == 2: self.c = v + elif i == 3: self.d = v + elif i == 4: self.e = v + elif i == 5: self.f = v + else: + raise IndexError("index out of range") + return + + def __len__(self): + return 6 + + def __repr__(self): + return "Matrix" + str(tuple(self)) + + def __invert__(self): + """Calculate inverted matrix.""" + m1 = Matrix() + m1.invert(self) + return m1 + __inv__ = __invert__ + + def __mul__(self, m): + if hasattr(m, "__float__"): + return Matrix(self.a * m, self.b * m, self.c * m, + self.d * m, self.e * m, self.f * m) + m1 = Matrix(1,1) + return m1.concat(self, m) + + def __truediv__(self, m): + if hasattr(m, "__float__"): + return Matrix(self.a * 1./m, self.b * 1./m, self.c * 1./m, + self.d * 1./m, self.e * 1./m, self.f * 1./m) + m1 = util_invert_matrix(m)[1] + if not m1: + raise ZeroDivisionError("matrix not invertible") + m2 = Matrix(1,1) + return m2.concat(self, m1) + __div__ = __truediv__ + + def __add__(self, m): + if hasattr(m, "__float__"): + return Matrix(self.a + m, self.b + m, self.c + m, + self.d + m, self.e + m, self.f + m) + if len(m) != 6: + raise ValueError("Matrix: bad seq len") + return Matrix(self.a + m[0], self.b + m[1], self.c + m[2], + self.d + m[3], self.e + m[4], self.f + m[5]) + + def __sub__(self, m): + if hasattr(m, "__float__"): + return Matrix(self.a - m, self.b - m, self.c - m, + self.d - m, self.e - m, self.f - m) + if len(m) != 6: + raise ValueError("Matrix: bad seq len") + return Matrix(self.a - m[0], self.b - m[1], self.c - m[2], + self.d - m[3], self.e - m[4], self.f - m[5]) + + def __pos__(self): + return Matrix(self) + + def __neg__(self): + return Matrix(-self.a, -self.b, -self.c, -self.d, -self.e, -self.f) + + def __bool__(self): + return not (max(self) == min(self) == 0) + + def __nonzero__(self): + return not (max(self) == min(self) == 0) + + def __eq__(self, mat): + if not hasattr(mat, "__len__"): + return False + return len(mat) == 6 and bool(self - mat) is False + + def __abs__(self): + return math.sqrt(sum([c*c for c in self])) + + norm = __abs__ + + @property + def is_rectilinear(self): + """True if rectangles are mapped to rectangles.""" + return (abs(self.b) < EPSILON and abs(self.c) < EPSILON) or \ + (abs(self.a) < EPSILON and abs(self.d) < EPSILON); + + +class IdentityMatrix(Matrix): + """Identity matrix [1, 0, 0, 1, 0, 0]""" + def __init__(self): + Matrix.__init__(self, 1.0, 1.0) + def __setattr__(self, name, value): + if name in "ad": + self.__dict__[name] = 1.0 + elif name in "bcef": + self.__dict__[name] = 0.0 + else: + self.__dict__[name] = value + + def checkargs(*args): + raise NotImplementedError("Identity is readonly") + + prerotate = checkargs + preshear = checkargs + prescale = checkargs + pretranslate = checkargs + concat = checkargs + invert = checkargs + + def __repr__(self): + return "IdentityMatrix(1.0, 0.0, 0.0, 1.0, 0.0, 0.0)" + + def __hash__(self): + return hash((1,0,0,1,0,0)) + + +Identity = IdentityMatrix() + +class Point(object): + """Point() - all zeros\nPoint(x, y)\nPoint(Point) - new copy\nPoint(sequence) - from 'sequence'""" + def __init__(self, *args): + if not args: + self.x = 0.0 + self.y = 0.0 + return None + + if len(args) > 2: + raise ValueError("Point: bad seq len") + if len(args) == 2: + self.x = float(args[0]) + self.y = float(args[1]) + return None + if len(args) == 1: + l = args[0] + if hasattr(l, "__getitem__") is False: + raise ValueError("Point: bad args") + if len(l) != 2: + raise ValueError("Point: bad seq len") + self.x = float(l[0]) + self.y = float(l[1]) + return None + raise ValueError("Point: bad args") + + def transform(self, m): + """Replace point by its transformation with matrix-like m.""" + if len(m) != 6: + raise ValueError("Matrix: bad seq len") + self.x, self.y = util_transform_point(self, m) + return self + + @property + def unit(self): + """Unit vector of the point.""" + s = self.x * self.x + self.y * self.y + if s < EPSILON: + return Point(0,0) + s = math.sqrt(s) + return Point(self.x / s, self.y / s) + + @property + def abs_unit(self): + """Unit vector with positive coordinates.""" + s = self.x * self.x + self.y * self.y + if s < EPSILON: + return Point(0,0) + s = math.sqrt(s) + return Point(abs(self.x) / s, abs(self.y) / s) + + def distance_to(self, *args): + """Return distance to rectangle or another point.""" + if not len(args) > 0: + raise ValueError("at least one parameter must be given") + + x = args[0] + if len(x) == 2: + x = Point(x) + elif len(x) == 4: + x = Rect(x) + else: + raise ValueError("arg1 must be point-like or rect-like") + + if len(args) > 1: + unit = args[1] + else: + unit = "px" + u = {"px": (1.,1.), "in": (1.,72.), "cm": (2.54, 72.), + "mm": (25.4, 72.)} + f = u[unit][0] / u[unit][1] + + if type(x) is Point: + return abs(self - x) * f + + # from here on, x is a rectangle + # as a safeguard, make a finite copy of it + r = Rect(x.top_left, x.top_left) + r = r | x.bottom_right + if self in r: + return 0.0 + if self.x > r.x1: + if self.y >= r.y1: + return self.distance_to(r.bottom_right, unit) + elif self.y <= r.y0: + return self.distance_to(r.top_right, unit) + else: + return (self.x - r.x1) * f + elif r.x0 <= self.x <= r.x1: + if self.y >= r.y1: + return (self.y - r.y1) * f + else: + return (r.y0 - self.y) * f + else: + if self.y >= r.y1: + return self.distance_to(r.bottom_left, unit) + elif self.y <= r.y0: + return self.distance_to(r.top_left, unit) + else: + return (r.x0 - self.x) * f + + def __getitem__(self, i): + return (self.x, self.y)[i] + + def __len__(self): + return 2 + + def __setitem__(self, i, v): + v = float(v) + if i == 0: self.x = v + elif i == 1: self.y = v + else: + raise IndexError("index out of range") + return None + + def __repr__(self): + return "Point" + str(tuple(self)) + + def __pos__(self): + return Point(self) + + def __neg__(self): + return Point(-self.x, -self.y) + + def __bool__(self): + return not (max(self) == min(self) == 0) + + def __nonzero__(self): + return not (max(self) == min(self) == 0) + + def __eq__(self, p): + if not hasattr(p, "__len__"): + return False + return len(p) == 2 and bool(self - p) is False + + def __abs__(self): + return math.sqrt(self.x * self.x + self.y * self.y) + + norm = __abs__ + + def __add__(self, p): + if hasattr(p, "__float__"): + return Point(self.x + p, self.y + p) + if len(p) != 2: + raise ValueError("Point: bad seq len") + return Point(self.x + p[0], self.y + p[1]) + + def __sub__(self, p): + if hasattr(p, "__float__"): + return Point(self.x - p, self.y - p) + if len(p) != 2: + raise ValueError("Point: bad seq len") + return Point(self.x - p[0], self.y - p[1]) + + def __mul__(self, m): + if hasattr(m, "__float__"): + return Point(self.x * m, self.y * m) + p = Point(self) + return p.transform(m) + + def __truediv__(self, m): + if hasattr(m, "__float__"): + return Point(self.x * 1./m, self.y * 1./m) + m1 = util_invert_matrix(m)[1] + if not m1: + raise ZeroDivisionError("matrix not invertible") + p = Point(self) + return p.transform(m1) + + __div__ = __truediv__ + + def __hash__(self): + return hash(tuple(self)) + +class Rect(object): + """Rect() - all zeros + Rect(x0, y0, x1, y1) - 4 coordinates + Rect(top-left, x1, y1) - point and 2 coordinates + Rect(x0, y0, bottom-right) - 2 coordinates and point + Rect(top-left, bottom-right) - 2 points + Rect(sequ) - new from sequence or rect-like + """ + def __init__(self, *args): + self.x0, self.y0, self.x1, self.y1 = util_make_rect(args) + return None + + def normalize(self): + """Replace rectangle with its valid version.""" + if self.x1 < self.x0: + self.x0, self.x1 = self.x1, self.x0 + if self.y1 < self.y0: + self.y0, self.y1 = self.y1, self.y0 + return self + + @property + def is_empty(self): + """True if rectangle area is empty.""" + return self.x0 >= self.x1 or self.y0 >= self.y1 + + @property + def is_valid(self): + """True if rectangle is valid.""" + return self.x0 <= self.x1 and self.y0 <= self.y1 + + @property + def is_infinite(self): + """True if this is the infinite rectangle.""" + return self.x0 == self.y0 == FZ_MIN_INF_RECT and self.x1 == self.y1 == FZ_MAX_INF_RECT + + @property + def top_left(self): + """Top-left corner.""" + return Point(self.x0, self.y0) + + @property + def top_right(self): + """Top-right corner.""" + return Point(self.x1, self.y0) + + @property + def bottom_left(self): + """Bottom-left corner.""" + return Point(self.x0, self.y1) + + @property + def bottom_right(self): + """Bottom-right corner.""" + return Point(self.x1, self.y1) + + tl = top_left + tr = top_right + bl = bottom_left + br = bottom_right + + @property + def quad(self): + """Return Quad version of rectangle.""" + return Quad(self.tl, self.tr, self.bl, self.br) + + def torect(self, r): + """Return matrix that converts to target rect.""" + + r = Rect(r) + if self.is_infinite or self.is_empty or r.is_infinite or r.is_empty: + raise ValueError("rectangles must be finite and not empty") + return ( + Matrix(1, 0, 0, 1, -self.x0, -self.y0) + * Matrix(r.width / self.width, r.height / self.height) + * Matrix(1, 0, 0, 1, r.x0, r.y0) + ) + + def morph(self, p, m): + """Morph with matrix-like m and point-like p. + + Returns a new quad.""" + if self.is_infinite: + return INFINITE_QUAD() + return self.quad.morph(p, m) + + def round(self): + """Return the IRect.""" + return IRect(util_round_rect(self)) + + irect = property(round) + + width = property(lambda self: self.x1 - self.x0 if self.x1 > self.x0 else 0) + height = property(lambda self: self.y1 - self.y0 if self.y1 > self.y0 else 0) + + def include_point(self, p): + """Extend to include point-like p.""" + if len(p) != 2: + raise ValueError("Point: bad seq len") + self.x0, self.y0, self.x1, self.y1 = util_include_point_in_rect(self, p) + return self + + def include_rect(self, r): + """Extend to include rect-like r.""" + if len(r) != 4: + raise ValueError("Rect: bad seq len") + r = Rect(r) + if r.is_infinite or self.is_infinite: + self.x0, self.y0, self.x1, self.y1 = FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT + elif r.is_empty: + return self + elif self.is_empty: + self.x0, self.y0, self.x1, self.y1 = r.x0, r.y0, r.x1, r.y1 + else: + self.x0, self.y0, self.x1, self.y1 = util_union_rect(self, r) + return self + + def intersect(self, r): + """Restrict to common rect with rect-like r.""" + if not len(r) == 4: + raise ValueError("Rect: bad seq len") + r = Rect(r) + if r.is_infinite: + return self + elif self.is_infinite: + self.x0, self.y0, self.x1, self.y1 = r.x0, r.y0, r.x1, r.y1 + elif r.is_empty: + self.x0, self.y0, self.x1, self.y1 = r.x0, r.y0, r.x1, r.y1 + elif self.is_empty: + return self + else: + self.x0, self.y0, self.x1, self.y1 = util_intersect_rect(self, r) + return self + + def contains(self, x): + """Check if containing point-like or rect-like x.""" + return self.__contains__(x) + + def transform(self, m): + """Replace with the transformation by matrix-like m.""" + if not len(m) == 6: + raise ValueError("Matrix: bad seq len") + self.x0, self.y0, self.x1, self.y1 = util_transform_rect(self, m) + return self + + def __getitem__(self, i): + return (self.x0, self.y0, self.x1, self.y1)[i] + + def __len__(self): + return 4 + + def __setitem__(self, i, v): + v = float(v) + if i == 0: self.x0 = v + elif i == 1: self.y0 = v + elif i == 2: self.x1 = v + elif i == 3: self.y1 = v + else: + raise IndexError("index out of range") + return None + + def __repr__(self): + return "Rect" + str(tuple(self)) + + def __pos__(self): + return Rect(self) + + def __neg__(self): + return Rect(-self.x0, -self.y0, -self.x1, -self.y1) + + def __bool__(self): + return not self.x0 == self.y0 == self.x1 == self.y1 == 0 + + def __nonzero__(self): + return not self.x0 == self.y0 == self.x1 == self.y1 == 0 + + def __eq__(self, r): + if not hasattr(r, "__len__"): + return False + return len(r) == 4 and self.x0 == r[0] and self.y0 == r[1] and self.x1 == r[2] and self.y1 == r[3] + + def __abs__(self): + if self.is_infinite or not self.is_valid: + return 0.0 + return self.width * self.height + + def norm(self): + return math.sqrt(sum([c*c for c in self])) + + def __add__(self, p): + if hasattr(p, "__float__"): + return Rect(self.x0 + p, self.y0 + p, self.x1 + p, self.y1 + p) + if len(p) != 4: + raise ValueError("Rect: bad seq len") + return Rect(self.x0 + p[0], self.y0 + p[1], self.x1 + p[2], self.y1 + p[3]) + + + def __sub__(self, p): + if hasattr(p, "__float__"): + return Rect(self.x0 - p, self.y0 - p, self.x1 - p, self.y1 - p) + if len(p) != 4: + raise ValueError("Rect: bad seq len") + return Rect(self.x0 - p[0], self.y0 - p[1], self.x1 - p[2], self.y1 - p[3]) + + + def __mul__(self, m): + if hasattr(m, "__float__"): + return Rect(self.x0 * m, self.y0 * m, self.x1 * m, self.y1 * m) + r = Rect(self) + r = r.transform(m) + return r + + def __truediv__(self, m): + if hasattr(m, "__float__"): + return Rect(self.x0 * 1./m, self.y0 * 1./m, self.x1 * 1./m, self.y1 * 1./m) + im = util_invert_matrix(m)[1] + if not im: + raise ZeroDivisionError("Matrix not invertible") + r = Rect(self) + r = r.transform(im) + return r + + __div__ = __truediv__ + + def __contains__(self, x): + if hasattr(x, "__float__"): + return x in tuple(self) + l = len(x) + if l == 2: + return util_is_point_in_rect(x, self) + if l == 4: + r = INFINITE_RECT() + try: + r = Rect(x) + except: + r = Quad(x).rect + return (self.x0 <= r.x0 <= r.x1 <= self.x1 and + self.y0 <= r.y0 <= r.y1 <= self.y1) + return False + + + def __or__(self, x): + if not hasattr(x, "__len__"): + raise ValueError("bad type op 2") + + r = Rect(self) + if len(x) == 2: + return r.include_point(x) + if len(x) == 4: + return r.include_rect(x) + raise ValueError("bad type op 2") + + def __and__(self, x): + if not hasattr(x, "__len__") or len(x) != 4: + raise ValueError("bad type op 2") + r = Rect(self) + return r.intersect(x) + + def intersects(self, x): + """Check if intersection with rectangle x is not empty.""" + r1 = Rect(x) + if self.is_empty or self.is_infinite or r1.is_empty or r1.is_infinite: + return False + r = Rect(self) + if r.intersect(r1).is_empty: + return False + return True + + def __hash__(self): + return hash(tuple(self)) + +class IRect(object): + """IRect() - all zeros + IRect(x0, y0, x1, y1) - 4 coordinates + IRect(top-left, x1, y1) - point and 2 coordinates + IRect(x0, y0, bottom-right) - 2 coordinates and point + IRect(top-left, bottom-right) - 2 points + IRect(sequ) - new from sequence or rect-like + """ + def __init__(self, *args): + self.x0, self.y0, self.x1, self.y1 = util_make_irect(args) + return None + + def normalize(self): + """Replace rectangle with its valid version.""" + if self.x1 < self.x0: + self.x0, self.x1 = self.x1, self.x0 + if self.y1 < self.y0: + self.y0, self.y1 = self.y1, self.y0 + return self + + @property + def is_empty(self): + """True if rectangle area is empty.""" + return self.x0 >= self.x1 or self.y0 >= self.y1 + + @property + def is_valid(self): + """True if rectangle is valid.""" + return self.x0 <= self.x1 and self.y0 <= self.y1 + + @property + def is_infinite(self): + """True if rectangle is infinite.""" + return self.x0 == self.y0 == FZ_MIN_INF_RECT and self.x1 == self.y1 == FZ_MAX_INF_RECT + + @property + def top_left(self): + """Top-left corner.""" + return Point(self.x0, self.y0) + + @property + def top_right(self): + """Top-right corner.""" + return Point(self.x1, self.y0) + + @property + def bottom_left(self): + """Bottom-left corner.""" + return Point(self.x0, self.y1) + + @property + def bottom_right(self): + """Bottom-right corner.""" + return Point(self.x1, self.y1) + + tl = top_left + tr = top_right + bl = bottom_left + br = bottom_right + + @property + def quad(self): + """Return Quad version of rectangle.""" + return Quad(self.tl, self.tr, self.bl, self.br) + + + def torect(self, r): + """Return matrix that converts to target rect.""" + + r = Rect(r) + if self.is_infinite or self.is_empty or r.is_infinite or r.is_empty: + raise ValueError("rectangles must be finite and not empty") + return ( + Matrix(1, 0, 0, 1, -self.x0, -self.y0) + * Matrix(r.width / self.width, r.height / self.height) + * Matrix(1, 0, 0, 1, r.x0, r.y0) + ) + + def morph(self, p, m): + """Morph with matrix-like m and point-like p. + + Returns a new quad.""" + if self.is_infinite: + return INFINITE_QUAD() + return self.quad.morph(p, m) + + @property + def rect(self): + return Rect(self) + + width = property(lambda self: self.x1 - self.x0 if self.x1 > self.x0 else 0) + height = property(lambda self: self.y1 - self.y0 if self.y1 > self.y0 else 0) + + def include_point(self, p): + """Extend rectangle to include point p.""" + rect = self.rect.include_point(p) + return rect.irect + + def include_rect(self, r): + """Extend rectangle to include rectangle r.""" + rect = self.rect.include_rect(r) + return rect.irect + + def intersect(self, r): + """Restrict rectangle to intersection with rectangle r.""" + rect = self.rect.intersect(r) + return rect.irect + + def __getitem__(self, i): + return (self.x0, self.y0, self.x1, self.y1)[i] + + def __len__(self): + return 4 + + def __setitem__(self, i, v): + v = int(v) + if i == 0: self.x0 = v + elif i == 1: self.y0 = v + elif i == 2: self.x1 = v + elif i == 3: self.y1 = v + else: + raise IndexError("index out of range") + return None + + def __repr__(self): + return "IRect" + str(tuple(self)) + + def __pos__(self): + return IRect(self) + + def __neg__(self): + return IRect(-self.x0, -self.y0, -self.x1, -self.y1) + + def __bool__(self): + return not self.x0 == self.y0 == self.x1 == self.y1 == 0 + + def __nonzero__(self): + return not self.x0 == self.y0 == self.x1 == self.y1 == 0 + + def __eq__(self, r): + if not hasattr(r, "__len__"): + return False + return len(r) == 4 and self.x0 == r[0] and self.y0 == r[1] and self.x1 == r[2] and self.y1 == r[3] + + def __abs__(self): + if self.is_infinite or not self.is_valid: + return 0 + return self.width * self.height + + def norm(self): + return math.sqrt(sum([c*c for c in self])) + + def __add__(self, p): + return Rect.__add__(self, p).round() + + def __sub__(self, p): + return Rect.__sub__(self, p).round() + + def transform(self, m): + return Rect.transform(self, m).round() + + def __mul__(self, m): + return Rect.__mul__(self, m).round() + + def __truediv__(self, m): + return Rect.__truediv__(self, m).round() + + __div__ = __truediv__ + + + def __contains__(self, x): + return Rect.__contains__(self, x) + + + def __or__(self, x): + return Rect.__or__(self, x).round() + + def __and__(self, x): + return Rect.__and__(self, x).round() + + def intersects(self, x): + return Rect.intersects(self, x) + + def __hash__(self): + return hash(tuple(self)) + + +class Quad(object): + """Quad() - all zero points\nQuad(ul, ur, ll, lr)\nQuad(quad) - new copy\nQuad(sequence) - from 'sequence'""" + def __init__(self, *args): + if not args: + self.ul = self.ur = self.ll = self.lr = Point() + return None + + if len(args) > 4: + raise ValueError("Quad: bad seq len") + if len(args) == 4: + self.ul, self.ur, self.ll, self.lr = map(Point, args) + return None + if len(args) == 1: + l = args[0] + if hasattr(l, "__getitem__") is False: + raise ValueError("Quad: bad args") + if len(l) != 4: + raise ValueError("Quad: bad seq len") + self.ul, self.ur, self.ll, self.lr = map(Point, l) + return None + raise ValueError("Quad: bad args") + + @property + def is_rectangular(self)->bool: + """Check if quad is rectangular. + + Notes: + Some rotation matrix can thus transform it into a rectangle. + This is equivalent to three corners enclose 90 degrees. + Returns: + True or False. + """ + + sine = util_sine_between(self.ul, self.ur, self.lr) + if abs(sine - 1) > EPSILON: # the sine of the angle + return False + + sine = util_sine_between(self.ur, self.lr, self.ll) + if abs(sine - 1) > EPSILON: + return False + + sine = util_sine_between(self.lr, self.ll, self.ul) + if abs(sine - 1) > EPSILON: + return False + + return True + + + @property + def is_convex(self)->bool: + """Check if quad is convex and not degenerate. + + Notes: + Check that for the two diagonals, the other two corners are not + on the same side of the diagonal. + Returns: + True or False. + """ + m = planish_line(self.ul, self.lr) # puts this diagonal on x-axis + p1 = self.ll * m # transform the + p2 = self.ur * m # other two points + if p1.y * p2.y > 0: + return False + m = planish_line(self.ll, self.ur) # puts other diagonal on x-axis + p1 = self.lr * m # tranform the + p2 = self.ul * m # remaining points + if p1.y * p2.y > 0: + return False + return True + + + width = property(lambda self: max(abs(self.ul - self.ur), abs(self.ll - self.lr))) + height = property(lambda self: max(abs(self.ul - self.ll), abs(self.ur - self.lr))) + + @property + def is_empty(self): + """Check whether all quad corners are on the same line. + + This is the case if width or height is zero. + """ + return self.width < EPSILON or self.height < EPSILON + + @property + def is_infinite(self): + """Check whether this is the infinite quad.""" + return self.rect.is_infinite + + @property + def rect(self): + r = Rect() + r.x0 = min(self.ul.x, self.ur.x, self.lr.x, self.ll.x) + r.y0 = min(self.ul.y, self.ur.y, self.lr.y, self.ll.y) + r.x1 = max(self.ul.x, self.ur.x, self.lr.x, self.ll.x) + r.y1 = max(self.ul.y, self.ur.y, self.lr.y, self.ll.y) + return r + + + def __contains__(self, x): + try: + l = x.__len__() + except: + return False + if l == 2: + return util_point_in_quad(x, self) + if l != 4: + return False + if CheckRect(x): + if Rect(x).is_empty: + return True + return util_point_in_quad(x[:2], self) and util_point_in_quad(x[2:], self) + if CheckQuad(x): + for i in range(4): + if not util_point_in_quad(x[i], self): + return False + return True + return False + + + def __getitem__(self, i): + return (self.ul, self.ur, self.ll, self.lr)[i] + + def __len__(self): + return 4 + + def __setitem__(self, i, v): + if i == 0: self.ul = Point(v) + elif i == 1: self.ur = Point(v) + elif i == 2: self.ll = Point(v) + elif i == 3: self.lr = Point(v) + else: + raise IndexError("index out of range") + return None + + def __repr__(self): + return "Quad" + str(tuple(self)) + + def __pos__(self): + return Quad(self) + + def __neg__(self): + return Quad(-self.ul, -self.ur, -self.ll, -self.lr) + + def __bool__(self): + return not self.is_empty + + def __nonzero__(self): + return not self.is_empty + + def __eq__(self, quad): + if not hasattr(quad, "__len__"): + return False + return len(quad) == 4 and ( + self.ul == quad[0] and + self.ur == quad[1] and + self.ll == quad[2] and + self.lr == quad[3] + ) + + def __abs__(self): + if self.is_empty: + return 0.0 + return abs(self.ul - self.ur) * abs(self.ul - self.ll) + + + def morph(self, p, m): + """Morph the quad with matrix-like 'm' and point-like 'p'. + + Return a new quad.""" + if self.is_infinite: + return INFINITE_QUAD() + delta = Matrix(1, 1).pretranslate(p.x, p.y) + q = self * ~delta * m * delta + return q + + + def transform(self, m): + """Replace quad by its transformation with matrix m.""" + if hasattr(m, "__float__"): + pass + elif len(m) != 6: + raise ValueError("Matrix: bad seq len") + self.ul *= m + self.ur *= m + self.ll *= m + self.lr *= m + return self + + def __mul__(self, m): + q = Quad(self) + q = q.transform(m) + return q + + def __add__(self, q): + if hasattr(q, "__float__"): + return Quad(self.ul + q, self.ur + q, self.ll + q, self.lr + q) + if len(p) != 4: + raise ValueError("Quad: bad seq len") + return Quad(self.ul + q[0], self.ur + q[1], self.ll + q[2], self.lr + q[3]) + + + def __sub__(self, q): + if hasattr(q, "__float__"): + return Quad(self.ul - q, self.ur - q, self.ll - q, self.lr - q) + if len(p) != 4: + raise ValueError("Quad: bad seq len") + return Quad(self.ul - q[0], self.ur - q[1], self.ll - q[2], self.lr - q[3]) + + + def __truediv__(self, m): + if hasattr(m, "__float__"): + im = 1. / m + else: + im = util_invert_matrix(m)[1] + if not im: + raise ZeroDivisionError("Matrix not invertible") + q = Quad(self) + q = q.transform(im) + return q + + __div__ = __truediv__ + + + def __hash__(self): + return hash(tuple(self)) + + +# some special geometry objects +def EMPTY_RECT(): + return Rect(FZ_MAX_INF_RECT, FZ_MAX_INF_RECT, FZ_MIN_INF_RECT, FZ_MIN_INF_RECT) + + +def INFINITE_RECT(): + return Rect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT) + + +def EMPTY_IRECT(): + return IRect(FZ_MAX_INF_RECT, FZ_MAX_INF_RECT, FZ_MIN_INF_RECT, FZ_MIN_INF_RECT) + + +def INFINITE_IRECT(): + return IRect(FZ_MIN_INF_RECT, FZ_MIN_INF_RECT, FZ_MAX_INF_RECT, FZ_MAX_INF_RECT) + + +def INFINITE_QUAD(): + return INFINITE_RECT().quad + + +def EMPTY_QUAD(): + return EMPTY_RECT().quad + + +%} diff --git a/fitz/helper-globals.i b/fitz/helper-globals.i new file mode 100644 index 0000000..a2c3cb6 --- /dev/null +++ b/fitz/helper-globals.i @@ -0,0 +1,53 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +// Global switches +// Switch for device hints = no cache +static int no_device_caching = 0; + +// Switch for computing glyph of fontsize height +static int small_glyph_heights = 0; + +// Switch for returning fontnames including subset prefix +static int subset_fontnames = 0; + +// Unset ascender / descender corrections +static int skip_quad_corrections = 0; + +// constants: error messages +static const char MSG_BAD_ANNOT_TYPE[] = "bad annot type"; +static const char MSG_BAD_APN[] = "bad or missing annot AP/N"; +static const char MSG_BAD_ARG_INK_ANNOT[] = "arg must be seq of seq of float pairs"; +static const char MSG_BAD_ARG_POINTS[] = "bad seq of points"; +static const char MSG_BAD_BUFFER[] = "bad type: 'buffer'"; +static const char MSG_BAD_COLOR_SEQ[] = "bad color sequence"; +static const char MSG_BAD_DOCUMENT[] = "cannot open broken document"; +static const char MSG_BAD_FILETYPE[] = "bad filetype"; +static const char MSG_BAD_LOCATION[] = "bad location"; +static const char MSG_BAD_OC_CONFIG[] = "bad config number"; +static const char MSG_BAD_OC_LAYER[] = "bad layer number"; +static const char MSG_BAD_OC_REF[] = "bad 'oc' reference"; +static const char MSG_BAD_PAGEID[] = "bad page id"; +static const char MSG_BAD_PAGENO[] = "bad page number(s)"; +static const char MSG_BAD_PDFROOT[] = "PDF has no root"; +static const char MSG_BAD_RECT[] = "rect is infinite or empty"; +static const char MSG_BAD_TEXT[] = "bad type: 'text'"; +static const char MSG_BAD_XREF[] = "bad xref"; +static const char MSG_COLOR_COUNT_FAILED[] = "color count failed"; +static const char MSG_FILE_OR_BUFFER[] = "need font file or buffer"; +static const char MSG_FONT_FAILED[] = "cannot create font"; +static const char MSG_IS_NO_ANNOT[] = "is no annotation"; +static const char MSG_IS_NO_IMAGE[] = "is no image"; +static const char MSG_IS_NO_PDF[] = "is no PDF"; +static const char MSG_IS_NO_DICT[] = "object is no PDF dict"; +static const char MSG_PIX_NOALPHA[] = "source pixmap has no alpha"; +static const char MSG_PIXEL_OUTSIDE[] = "pixel(s) outside image"; +%} diff --git a/fitz/helper-other.i b/fitz/helper-other.i new file mode 100644 index 0000000..860f21d --- /dev/null +++ b/fitz/helper-other.i @@ -0,0 +1,1346 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +fz_buffer *JM_object_to_buffer(fz_context *ctx, pdf_obj *val, int a, int b); +PyObject *JM_EscapeStrFromBuffer(fz_context *ctx, fz_buffer *buff); +pdf_obj *JM_pdf_obj_from_str(fz_context *ctx, pdf_document *doc, char *src); + +// exception handling +void *JM_ReturnException(fz_context *ctx) +{ + PyErr_SetString(JM_Exc_CurrentException, fz_caught_message(ctx)); + JM_Exc_CurrentException = PyExc_RuntimeError; + return NULL; +} + + +static int LIST_APPEND_DROP(PyObject *list, PyObject *item) +{ + if (!list || !PyList_Check(list) || !item) return -2; + int rc = PyList_Append(list, item); + Py_DECREF(item); + return rc; +} + +static int DICT_SETITEM_DROP(PyObject *dict, PyObject *key, PyObject *value) +{ + if (!dict || !PyDict_Check(dict) || !key || !value) return -2; + int rc = PyDict_SetItem(dict, key, value); + Py_DECREF(value); + return rc; +} + +static int DICT_SETITEMSTR_DROP(PyObject *dict, const char *key, PyObject *value) +{ + if (!dict || !PyDict_Check(dict) || !key || !value) return -2; + int rc = PyDict_SetItemString(dict, key, value); + Py_DECREF(value); + return rc; +} + + +//-------------------------------------- +// Ensure valid journalling state +//-------------------------------------- +int JM_have_operation(fz_context *ctx, pdf_document *pdf) +{ + if (pdf->journal && !pdf_undoredo_step(ctx, pdf, 0)) { + return 0; + } + return 1; +} + +//---------------------------------- +// Set a PDF dict key to some value +//---------------------------------- +static pdf_obj +*JM_set_object_value(fz_context *ctx, pdf_obj *obj, const char *key, char *value) +{ + fz_buffer *res = NULL; + pdf_obj *new_obj = NULL, *testkey = NULL; + PyObject *skey = PyUnicode_FromString(key); // Python version of dict key + PyObject *slash = PyUnicode_FromString("/"); // PDF path separator + PyObject *list = NULL, *newval=NULL, *newstr=NULL, *nullval=NULL; + const char eyecatcher[] = "fitz: replace me!"; + pdf_document *pdf = NULL; + fz_try(ctx) + { + pdf = pdf_get_bound_document(ctx, obj); + // split PDF key at path seps and take last key part + list = PyUnicode_Split(skey, slash, -1); + Py_ssize_t len = PySequence_Size(list); + Py_ssize_t i = len - 1; + Py_DECREF(skey); + skey = PySequence_GetItem(list, i); + + PySequence_DelItem(list, i); // del the last sub-key + len = PySequence_Size(list); // remaining length + testkey = pdf_dict_getp(ctx, obj, key); // check if key already exists + if (!testkey) { + /*----------------------------------------------------------------- + No, it will be created here. But we cannot allow this happening if + indirect objects are referenced. So we check all higher level + sub-paths for indirect references. + -----------------------------------------------------------------*/ + while (len > 0) { + PyObject *t = PyUnicode_Join(slash, list); // next high level + if (pdf_is_indirect(ctx, pdf_dict_getp(ctx, obj, JM_StrAsChar(t)))) { + Py_DECREF(t); + fz_throw(ctx, FZ_ERROR_GENERIC, "path to '%s' has indirects", JM_StrAsChar(skey)); + } + PySequence_DelItem(list, len - 1); // del last sub-key + len = PySequence_Size(list); // remaining length + Py_DECREF(t); + } + } + // Insert our eyecatcher. Will create all sub-paths in the chain, or + // respectively remove old value of key-path. + pdf_dict_putp_drop(ctx, obj, key, pdf_new_text_string(ctx, eyecatcher)); + testkey = pdf_dict_getp(ctx, obj, key); + if (!pdf_is_string(ctx, testkey)) { + fz_throw(ctx, FZ_ERROR_GENERIC, "cannot insert value for '%s'", key); + } + const char *temp = pdf_to_text_string(ctx, testkey); + if (strcmp(temp, eyecatcher) != 0) { + fz_throw(ctx, FZ_ERROR_GENERIC, "cannot insert value for '%s'", key); + } + // read the result as a string + res = JM_object_to_buffer(ctx, obj, 1, 0); + PyObject *objstr = JM_EscapeStrFromBuffer(ctx, res); + + // replace 'eyecatcher' by desired 'value' + nullval = PyUnicode_FromFormat("/%s(%s)", JM_StrAsChar(skey), eyecatcher); + newval = PyUnicode_FromFormat("/%s %s", JM_StrAsChar(skey), value); + newstr = PyUnicode_Replace(objstr, nullval, newval, 1); + + // make PDF object from resulting string + new_obj = JM_pdf_obj_from_str(ctx, pdf, JM_StrAsChar(newstr)); + } + fz_always(ctx) { + fz_drop_buffer(ctx, res); + Py_CLEAR(skey); + Py_CLEAR(slash); + Py_CLEAR(list); + Py_CLEAR(newval); + Py_CLEAR(newstr); + Py_CLEAR(nullval); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return new_obj; +} + + +static void +JM_get_page_labels(fz_context *ctx, PyObject *liste, pdf_obj *nums) +{ + int pno, i, n = pdf_array_len(ctx, nums); + char *c = NULL; + pdf_obj *val; + fz_buffer *res = NULL; + for (i = 0; i < n; i += 2) { + pdf_obj *key = pdf_resolve_indirect(ctx, pdf_array_get(ctx, nums, i)); + pno = pdf_to_int(ctx, key); + val = pdf_resolve_indirect(ctx, pdf_array_get(ctx, nums, i + 1)); + res = JM_object_to_buffer(ctx, val, 1, 0); + fz_buffer_storage(ctx, res, &c); + LIST_APPEND_DROP(liste, Py_BuildValue("is", pno, c)); + fz_drop_buffer(ctx, res); + } +} + + +PyObject *JM_EscapeStrFromBuffer(fz_context *ctx, fz_buffer *buff) +{ + if (!buff) return EMPTY_STRING; + unsigned char *s = NULL; + size_t len = fz_buffer_storage(ctx, buff, &s); + PyObject *val = PyUnicode_DecodeRawUnicodeEscape((const char *) s, (Py_ssize_t) len, "replace"); + if (!val) { + val = EMPTY_STRING; + PyErr_Clear(); + } + return val; +} + +PyObject *JM_UnicodeFromBuffer(fz_context *ctx, fz_buffer *buff) +{ + unsigned char *s = NULL; + Py_ssize_t len = (Py_ssize_t) fz_buffer_storage(ctx, buff, &s); + PyObject *val = PyUnicode_DecodeUTF8((const char *) s, len, "replace"); + if (!val) { + val = EMPTY_STRING; + PyErr_Clear(); + } + return val; +} + +PyObject *JM_UnicodeFromStr(const char *c) +{ + if (!c) return EMPTY_STRING; + PyObject *val = Py_BuildValue("s", c); + if (!val) { + val = EMPTY_STRING; + PyErr_Clear(); + } + return val; +} + +PyObject *JM_EscapeStrFromStr(const char *c) +{ + if (!c) return EMPTY_STRING; + PyObject *val = PyUnicode_DecodeRawUnicodeEscape(c, (Py_ssize_t) strlen(c), "replace"); + if (!val) { + val = EMPTY_STRING; + PyErr_Clear(); + } + return val; +} + + +// list of valid unicodes of a fz_font +void JM_valid_chars(fz_context *ctx, fz_font *font, void *arr) +{ + FT_Face face = font->ft_face; + FT_ULong ucs; + FT_UInt gid; + long *table = (long *)arr; + fz_lock(ctx, FZ_LOCK_FREETYPE); + ucs = FT_Get_First_Char(face, &gid); + while (gid > 0) + { + if (gid < (FT_ULong)face->num_glyphs && face->num_glyphs > 0) + table[gid] = (long)ucs; + ucs = FT_Get_Next_Char(face, ucs, &gid); + } + fz_unlock(ctx, FZ_LOCK_FREETYPE); + return; +} + + +// redirect MuPDF warnings +void JM_mupdf_warning(void *user, const char *message) +{ + LIST_APPEND_DROP(JM_mupdf_warnings_store, JM_EscapeStrFromStr(message)); + if (JM_mupdf_show_warnings) { + PySys_WriteStderr("mupdf: %s\n", message); + } +} + +// redirect MuPDF errors +void JM_mupdf_error(void *user, const char *message) +{ + LIST_APPEND_DROP(JM_mupdf_warnings_store, JM_EscapeStrFromStr(message)); + if (JM_mupdf_show_errors) { + PySys_WriteStderr("mupdf: %s\n", message); + } +} + +// a simple tracer +void JM_TRACE(const char *id) +{ + PySys_WriteStdout("%s\n", id); +} + + +// put a warning on Python-stdout +void JM_Warning(const char *id) +{ + PySys_WriteStdout("warning: %s\n", id); +} + +#if JM_MEMORY == 1 +//----------------------------------------------------------------------------- +// The following 3 functions replace MuPDF standard memory allocation. +// This will ensure, that MuPDF memory handling becomes part of Python's +// memory management. +//----------------------------------------------------------------------------- +static void *JM_Py_Malloc(void *opaque, size_t size) +{ + void *mem = PyMem_Malloc((Py_ssize_t) size); + if (mem) return mem; + fz_throw(gctx, FZ_ERROR_MEMORY, "malloc of %zu bytes failed", size); +} + +static void *JM_Py_Realloc(void *opaque, void *old, size_t size) +{ + void *mem = PyMem_Realloc(old, (Py_ssize_t) size); + if (mem) return mem; + fz_throw(gctx, FZ_ERROR_MEMORY, "realloc of %zu bytes failed", size); +} + +static void JM_PY_Free(void *opaque, void *ptr) +{ + PyMem_Free(ptr); +} + +const fz_alloc_context JM_Alloc_Context = +{ + NULL, + JM_Py_Malloc, + JM_Py_Realloc, + JM_PY_Free +}; +#endif + +PyObject *JM_fitz_config() +{ +#if defined(TOFU) +#define have_TOFU JM_BOOL(0) +#else +#define have_TOFU JM_BOOL(1) +#endif +#if defined(TOFU_CJK) +#define have_TOFU_CJK JM_BOOL(0) +#else +#define have_TOFU_CJK JM_BOOL(1) +#endif +#if defined(TOFU_CJK_EXT) +#define have_TOFU_CJK_EXT JM_BOOL(0) +#else +#define have_TOFU_CJK_EXT JM_BOOL(1) +#endif +#if defined(TOFU_CJK_LANG) +#define have_TOFU_CJK_LANG JM_BOOL(0) +#else +#define have_TOFU_CJK_LANG JM_BOOL(1) +#endif +#if defined(TOFU_EMOJI) +#define have_TOFU_EMOJI JM_BOOL(0) +#else +#define have_TOFU_EMOJI JM_BOOL(1) +#endif +#if defined(TOFU_HISTORIC) +#define have_TOFU_HISTORIC JM_BOOL(0) +#else +#define have_TOFU_HISTORIC JM_BOOL(1) +#endif +#if defined(TOFU_SYMBOL) +#define have_TOFU_SYMBOL JM_BOOL(0) +#else +#define have_TOFU_SYMBOL JM_BOOL(1) +#endif +#if defined(TOFU_SIL) +#define have_TOFU_SIL JM_BOOL(0) +#else +#define have_TOFU_SIL JM_BOOL(1) +#endif +#if defined(TOFU_BASE14) +#define have_TOFU_BASE14 JM_BOOL(0) +#else +#define have_TOFU_BASE14 JM_BOOL(1) +#endif + PyObject *dict = PyDict_New(); + DICT_SETITEMSTR_DROP(dict, "plotter-g", JM_BOOL(FZ_PLOTTERS_G)); + DICT_SETITEMSTR_DROP(dict, "plotter-rgb", JM_BOOL(FZ_PLOTTERS_RGB)); + DICT_SETITEMSTR_DROP(dict, "plotter-cmyk", JM_BOOL(FZ_PLOTTERS_CMYK)); + DICT_SETITEMSTR_DROP(dict, "plotter-n", JM_BOOL(FZ_PLOTTERS_N)); + DICT_SETITEMSTR_DROP(dict, "pdf", JM_BOOL(FZ_ENABLE_PDF)); + DICT_SETITEMSTR_DROP(dict, "xps", JM_BOOL(FZ_ENABLE_XPS)); + DICT_SETITEMSTR_DROP(dict, "svg", JM_BOOL(FZ_ENABLE_SVG)); + DICT_SETITEMSTR_DROP(dict, "cbz", JM_BOOL(FZ_ENABLE_CBZ)); + DICT_SETITEMSTR_DROP(dict, "img", JM_BOOL(FZ_ENABLE_IMG)); + DICT_SETITEMSTR_DROP(dict, "html", JM_BOOL(FZ_ENABLE_HTML)); + DICT_SETITEMSTR_DROP(dict, "epub", JM_BOOL(FZ_ENABLE_EPUB)); + DICT_SETITEMSTR_DROP(dict, "jpx", JM_BOOL(FZ_ENABLE_JPX)); + DICT_SETITEMSTR_DROP(dict, "js", JM_BOOL(FZ_ENABLE_JS)); + DICT_SETITEMSTR_DROP(dict, "tofu", have_TOFU); + DICT_SETITEMSTR_DROP(dict, "tofu-cjk", have_TOFU_CJK); + DICT_SETITEMSTR_DROP(dict, "tofu-cjk-ext", have_TOFU_CJK_EXT); + DICT_SETITEMSTR_DROP(dict, "tofu-cjk-lang", have_TOFU_CJK_LANG); + DICT_SETITEMSTR_DROP(dict, "tofu-emoji", have_TOFU_EMOJI); + DICT_SETITEMSTR_DROP(dict, "tofu-historic", have_TOFU_HISTORIC); + DICT_SETITEMSTR_DROP(dict, "tofu-symbol", have_TOFU_SYMBOL); + DICT_SETITEMSTR_DROP(dict, "tofu-sil", have_TOFU_SIL); + DICT_SETITEMSTR_DROP(dict, "icc", JM_BOOL(FZ_ENABLE_ICC)); + DICT_SETITEMSTR_DROP(dict, "base14", have_TOFU_BASE14); + DICT_SETITEMSTR_DROP(dict, "py-memory", JM_BOOL(JM_MEMORY)); + return dict; +} + +//---------------------------------------------------------------------------- +// Update a color float array with values from a Python sequence. +// Any error condition is treated as a no-op. +//---------------------------------------------------------------------------- +void JM_color_FromSequence(PyObject *color, int *n, float col[4]) +{ + if (!color || color == Py_None) { + *n = -1; + return; + } + if (PyFloat_Check(color)) { // maybe just a single float + *n = 1; + float c = (float) PyFloat_AsDouble(color); + if (!INRANGE(c, 0, 1)) { + c = 1; + } + col[0] = c; + return; + } + + if (!PySequence_Check(color)) { + *n = -1; + return; + } + int len = (int) PySequence_Size(color), rc; + if (len == 0) { + *n = 0; + return; + } + if (!INRANGE(len, 1, 4) || len == 2) { + *n = -1; + return; + } + + double mcol[4] = {0,0,0,0}; // local color storage + Py_ssize_t i; + for (i = 0; i < len; i++) { + rc = JM_FLOAT_ITEM(color, i, &mcol[i]); + if (!INRANGE(mcol[i], 0, 1) || rc == 1) mcol[i] = 1; + } + + *n = len; + for (i = 0; i < len; i++) + col[i] = (float) mcol[i]; + return; +} + +// return extension for fitz image type +const char *JM_image_extension(int type) +{ + switch (type) { + case(FZ_IMAGE_FAX): return "fax"; + case(FZ_IMAGE_RAW): return "raw"; + case(FZ_IMAGE_FLATE): return "flate"; + case(FZ_IMAGE_LZW): return "lzw"; + case(FZ_IMAGE_RLD): return "rld"; + case(FZ_IMAGE_BMP): return "bmp"; + case(FZ_IMAGE_GIF): return "gif"; + case(FZ_IMAGE_JBIG2): return "jb2"; + case(FZ_IMAGE_JPEG): return "jpeg"; + case(FZ_IMAGE_JPX): return "jpx"; + case(FZ_IMAGE_JXR): return "jxr"; + case(FZ_IMAGE_PNG): return "png"; + case(FZ_IMAGE_PNM): return "pnm"; + case(FZ_IMAGE_TIFF): return "tiff"; + // case(FZ_IMAGE_PSD): return "psd"; + case(FZ_IMAGE_UNKNOWN): return "n/a"; + default: return "n/a"; + } +} + +//---------------------------------------------------------------------------- +// Turn fz_buffer into a Python bytes object +//---------------------------------------------------------------------------- +PyObject *JM_BinFromBuffer(fz_context *ctx, fz_buffer *buffer) +{ + if (!buffer) { + return PyBytes_FromString(""); + } + unsigned char *c = NULL; + size_t len = fz_buffer_storage(ctx, buffer, &c); + return PyBytes_FromStringAndSize((const char *) c, (Py_ssize_t) len); +} + +//---------------------------------------------------------------------------- +// Turn fz_buffer into a Python bytearray object +//---------------------------------------------------------------------------- +PyObject *JM_BArrayFromBuffer(fz_context *ctx, fz_buffer *buffer) +{ + if (!buffer) { + return PyByteArray_FromStringAndSize("", 0); + } + unsigned char *c = NULL; + size_t len = fz_buffer_storage(ctx, buffer, &c); + return PyByteArray_FromStringAndSize((const char *) c, (Py_ssize_t) len); +} + + +//---------------------------------------------------------------------------- +// compress char* into a new buffer +//---------------------------------------------------------------------------- +fz_buffer *JM_compress_buffer(fz_context *ctx, fz_buffer *inbuffer) +{ + fz_buffer *buf = NULL; + fz_try(ctx) { + size_t compressed_length = 0; + unsigned char *data = fz_new_deflated_data_from_buffer(ctx, + &compressed_length, inbuffer, FZ_DEFLATE_BEST); + if (data == NULL || compressed_length == 0) + return NULL; + buf = fz_new_buffer_from_data(ctx, data, compressed_length); + fz_resize_buffer(ctx, buf, compressed_length); + } + fz_catch(ctx) { + fz_drop_buffer(ctx, buf); + fz_rethrow(ctx); + } + return buf; +} + +//---------------------------------------------------------------------------- +// update a stream object +// compress stream when beneficial +//---------------------------------------------------------------------------- +void JM_update_stream(fz_context *ctx, pdf_document *doc, pdf_obj *obj, fz_buffer *buffer, int compress) +{ + + fz_buffer *nres = NULL; + size_t len = fz_buffer_storage(ctx, buffer, NULL); + size_t nlen = len; + + if (compress == 1 && len > 30) { // ignore small stuff + nres = JM_compress_buffer(ctx, buffer); + nlen = fz_buffer_storage(ctx, nres, NULL); + } + + if (nlen < len && nres && compress==1) { // was it worth the effort? + pdf_dict_put(ctx, obj, PDF_NAME(Filter), PDF_NAME(FlateDecode)); + pdf_update_stream(ctx, doc, obj, nres, 1); + } else { + pdf_update_stream(ctx, doc, obj, buffer, 0); + } + fz_drop_buffer(ctx, nres); +} + +//----------------------------------------------------------------------------- +// return hex characters for n characters in input 'in' +//----------------------------------------------------------------------------- +void hexlify(int n, unsigned char *in, unsigned char *out) +{ + const unsigned char hdigit[17] = "0123456789abcedf"; + int i, i1, i2; + for (i = 0; i < n; i++) { + i1 = in[i]>>4; + i2 = in[i] - i1*16; + out[2*i] = hdigit[i1]; + out[2*i + 1] = hdigit[i2]; + } + out[2*n] = 0; +} + +//---------------------------------------------------------------------------- +// Make fz_buffer from a PyBytes, PyByteArray, or io.BytesIO object +//---------------------------------------------------------------------------- +fz_buffer *JM_BufferFromBytes(fz_context *ctx, PyObject *stream) +{ + char *c = NULL; + PyObject *mybytes = NULL; + size_t len = 0; + fz_buffer *res = NULL; + fz_var(res); + fz_try(ctx) { + if (PyBytes_Check(stream)) { + c = PyBytes_AS_STRING(stream); + len = (size_t) PyBytes_GET_SIZE(stream); + } else if (PyByteArray_Check(stream)) { + c = PyByteArray_AS_STRING(stream); + len = (size_t) PyByteArray_GET_SIZE(stream); + } else if (PyObject_HasAttrString(stream, "getvalue")) { + // we assume here that this delivers what we expect + mybytes = PyObject_CallMethod(stream, "getvalue", NULL); + c = PyBytes_AS_STRING(mybytes); + len = (size_t) PyBytes_GET_SIZE(mybytes); + } + // if none of the above, c is NULL and we return an empty buffer + if (c) { + res = fz_new_buffer_from_copied_data(ctx, (const unsigned char *) c, len); + } else { + res = fz_new_buffer(ctx, 1); + fz_append_byte(ctx, res, 10); + } + fz_terminate_buffer(ctx, res); + } + fz_always(ctx) { + Py_CLEAR(mybytes); + PyErr_Clear(); + } + fz_catch(ctx) { + fz_drop_buffer(ctx, res); + fz_rethrow(ctx); + } + return res; +} + + +//---------------------------------------------------------------------------- +// Deep-copies a specified source page to the target location. +// Modified copy of function of pdfmerge.c: we also copy annotations, but +// we skip **link** annotations. In addition we rotate output. +//---------------------------------------------------------------------------- +static void +page_merge(fz_context *ctx, pdf_document *doc_des, pdf_document *doc_src, int page_from, int page_to, int rotate, int links, int copy_annots, pdf_graft_map *graft_map) +{ + pdf_obj *page_ref = NULL; + pdf_obj *page_dict = NULL; + pdf_obj *obj = NULL, *ref = NULL; + + // list of object types (per page) we want to copy + static pdf_obj * const known_page_objs[] = { + PDF_NAME(Contents), + PDF_NAME(Resources), + PDF_NAME(MediaBox), + PDF_NAME(CropBox), + PDF_NAME(BleedBox), + PDF_NAME(TrimBox), + PDF_NAME(ArtBox), + PDF_NAME(Rotate), + PDF_NAME(UserUnit) + }; + + int i, n; + + fz_var(ref); + fz_var(page_dict); + + fz_try(ctx) { + page_ref = pdf_lookup_page_obj(ctx, doc_src, page_from); + + // make new page dict in dest doc + page_dict = pdf_new_dict(ctx, doc_des, 4); + pdf_dict_put(ctx, page_dict, PDF_NAME(Type), PDF_NAME(Page)); + + for (i = 0; i < (int) nelem(known_page_objs); i++) { + obj = pdf_dict_get_inheritable(ctx, page_ref, known_page_objs[i]); + if (obj != NULL) { + pdf_dict_put_drop(ctx, page_dict, known_page_objs[i], pdf_graft_mapped_object(ctx, graft_map, obj)); + } + } + + // Copy the annotations, but skip types Link, Popup, IRT. + // Remove dict keys P (parent) and Popup from copied annot. + if (copy_annots) { + pdf_obj *old_annots = pdf_dict_get(ctx, page_ref, PDF_NAME(Annots)); + if (old_annots) { + n = pdf_array_len(ctx, old_annots); + pdf_obj *new_annots = pdf_dict_put_array(ctx, page_dict, PDF_NAME(Annots), n); + for (i = 0; i < n; i++) { + pdf_obj *o = pdf_array_get(ctx, old_annots, i); + if (pdf_dict_get(ctx, o, PDF_NAME(IRT))) continue; + pdf_obj *subtype = pdf_dict_get(ctx, o, PDF_NAME(Subtype)); + if (pdf_name_eq(ctx, subtype, PDF_NAME(Link))) continue; + if (pdf_name_eq(ctx, subtype, PDF_NAME(Popup))) continue; + if (pdf_name_eq(ctx, subtype, PDF_NAME(Widget))) { + fz_warn(ctx, "skipping widget annotation"); + continue; + } + pdf_dict_del(ctx, o, PDF_NAME(Popup)); + pdf_dict_del(ctx, o, PDF_NAME(P)); + pdf_obj *copy_o = pdf_graft_mapped_object(ctx, graft_map, o); + pdf_obj *annot = pdf_new_indirect(ctx, doc_des, + pdf_to_num(ctx, copy_o), 0); + pdf_array_push_drop(ctx, new_annots, annot); + pdf_drop_obj(ctx, copy_o); + } + } + } + // rotate the page + if (rotate != -1) { + pdf_dict_put_int(ctx, page_dict, PDF_NAME(Rotate), (int64_t) rotate); + } + // Now add the page dictionary to dest PDF + ref = pdf_add_object(ctx, doc_des, page_dict); + + // Insert new page at specified location + pdf_insert_page(ctx, doc_des, page_to, ref); + + } + fz_always(ctx) { + pdf_drop_obj(ctx, page_dict); + pdf_drop_obj(ctx, ref); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } +} + +//----------------------------------------------------------------------------- +// Copy a range of pages (spage, epage) from a source PDF to a specified +// location (apage) of the target PDF. +// If spage > epage, the sequence of source pages is reversed. +//----------------------------------------------------------------------------- +void JM_merge_range(fz_context *ctx, pdf_document *doc_des, pdf_document *doc_src, int spage, int epage, int apage, int rotate, int links, int annots, int show_progress, pdf_graft_map *graft_map) +{ + int page, afterpage; + afterpage = apage; + int counter = 0; // copied pages counter + int total = fz_absi(epage - spage) + 1; // total pages to copy + + fz_try(ctx) { + if (spage < epage) { + for (page = spage; page <= epage; page++, afterpage++) { + page_merge(ctx, doc_des, doc_src, page, afterpage, rotate, links, annots, graft_map); + counter++; + if (show_progress > 0 && counter % show_progress == 0) { + PySys_WriteStdout("Inserted %i of %i pages.\n", counter, total); + } + } + } else { + for (page = spage; page >= epage; page--, afterpage++) { + page_merge(ctx, doc_des, doc_src, page, afterpage, rotate, links, annots, graft_map); + counter++; + if (show_progress > 0 && counter % show_progress == 0) { + PySys_WriteStdout("Inserted %i of %i pages.\n", counter, total); + } + } + } + } + + fz_catch(ctx) { + fz_rethrow(ctx); + } +} + +//---------------------------------------------------------------------------- +// Return list of outline xref numbers. Recursive function. Arguments: +// 'obj' first OL item +// 'xrefs' empty Python list +//---------------------------------------------------------------------------- +PyObject *JM_outline_xrefs(fz_context *ctx, pdf_obj *obj, PyObject *xrefs) +{ + pdf_obj *first, *parent, *thisobj; + if (!obj) return xrefs; + PyObject *newxref = NULL; + thisobj = obj; + while (thisobj) { + newxref = PyLong_FromLong((long) pdf_to_num(ctx, thisobj)); + if (PySequence_Contains(xrefs, newxref) || + pdf_dict_get(ctx, thisobj, PDF_NAME(Type))) { + // circular ref or top of chain: terminate + Py_DECREF(newxref); + break; + } + LIST_APPEND_DROP(xrefs, newxref); + first = pdf_dict_get(ctx, thisobj, PDF_NAME(First)); // try go down + if (pdf_is_dict(ctx, first)) xrefs = JM_outline_xrefs(ctx, first, xrefs); + thisobj = pdf_dict_get(ctx, thisobj, PDF_NAME(Next)); // try go next + parent = pdf_dict_get(ctx, thisobj, PDF_NAME(Parent)); // get parent + if (!pdf_is_dict(ctx, thisobj)) { + thisobj = parent; + } + } + return xrefs; +} + + +//------------------------------------------------------------------- +// Return the contents of a font file, identified by xref +//------------------------------------------------------------------- +fz_buffer *JM_get_fontbuffer(fz_context *ctx, pdf_document *doc, int xref) +{ + if (xref < 1) return NULL; + pdf_obj *o, *obj = NULL, *desft, *stream = NULL; + o = pdf_load_object(ctx, doc, xref); + desft = pdf_dict_get(ctx, o, PDF_NAME(DescendantFonts)); + if (desft) { + obj = pdf_resolve_indirect(ctx, pdf_array_get(ctx, desft, 0)); + obj = pdf_dict_get(ctx, obj, PDF_NAME(FontDescriptor)); + } else { + obj = pdf_dict_get(ctx, o, PDF_NAME(FontDescriptor)); + } + + if (!obj) { + pdf_drop_obj(ctx, o); + PySys_WriteStdout("invalid font - FontDescriptor missing"); + return NULL; + } + pdf_drop_obj(ctx, o); + o = obj; + + obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile)); + if (obj) stream = obj; // ext = "pfa" + + obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile2)); + if (obj) stream = obj; // ext = "ttf" + + obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile3)); + if (obj) { + stream = obj; + + obj = pdf_dict_get(ctx, obj, PDF_NAME(Subtype)); + if (obj && !pdf_is_name(ctx, obj)) { + PySys_WriteStdout("invalid font descriptor subtype"); + return NULL; + } + + if (pdf_name_eq(ctx, obj, PDF_NAME(Type1C))) + ; /*Prev code did: ext = "cff", but this has no effect. */ + else if (pdf_name_eq(ctx, obj, PDF_NAME(CIDFontType0C))) + ; /*Prev code did: ext = "cid", but this has no effect. */ + else if (pdf_name_eq(ctx, obj, PDF_NAME(OpenType))) + ; /*Prev code did: ext = "otf", but this has no effect. */ + else + PySys_WriteStdout("warning: unhandled font type '%s'", pdf_to_name(ctx, obj)); + } + + if (!stream) { + PySys_WriteStdout("warning: unhandled font type"); + return NULL; + } + + return pdf_load_stream(ctx, stream); +} + +//----------------------------------------------------------------------------- +// Return the file extension of a font file, identified by xref +//----------------------------------------------------------------------------- +char *JM_get_fontextension(fz_context *ctx, pdf_document *doc, int xref) +{ + if (xref < 1) return "n/a"; + pdf_obj *o, *obj = NULL, *desft; + o = pdf_load_object(ctx, doc, xref); + desft = pdf_dict_get(ctx, o, PDF_NAME(DescendantFonts)); + if (desft) { + obj = pdf_resolve_indirect(ctx, pdf_array_get(ctx, desft, 0)); + obj = pdf_dict_get(ctx, obj, PDF_NAME(FontDescriptor)); + } else { + obj = pdf_dict_get(ctx, o, PDF_NAME(FontDescriptor)); + } + + pdf_drop_obj(ctx, o); + if (!obj) return "n/a"; // this is a base-14 font + + o = obj; // we have the FontDescriptor + + obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile)); + if (obj) return "pfa"; + + obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile2)); + if (obj) return "ttf"; + + obj = pdf_dict_get(ctx, o, PDF_NAME(FontFile3)); + if (obj) { + obj = pdf_dict_get(ctx, obj, PDF_NAME(Subtype)); + if (obj && !pdf_is_name(ctx, obj)) { + PySys_WriteStdout("invalid font descriptor subtype"); + return "n/a"; + } + if (pdf_name_eq(ctx, obj, PDF_NAME(Type1C))) + return "cff"; + else if (pdf_name_eq(ctx, obj, PDF_NAME(CIDFontType0C))) + return "cid"; + else if (pdf_name_eq(ctx, obj, PDF_NAME(OpenType))) + return "otf"; + else + PySys_WriteStdout("unhandled font type '%s'", pdf_to_name(ctx, obj)); + } + + return "n/a"; +} + + +//----------------------------------------------------------------------------- +// create PDF object from given string (new in v1.14.0: MuPDF dropped it) +//----------------------------------------------------------------------------- +pdf_obj *JM_pdf_obj_from_str(fz_context *ctx, pdf_document *doc, char *src) +{ + pdf_obj *result = NULL; + pdf_lexbuf lexbuf; + fz_stream *stream = fz_open_memory(ctx, (unsigned char *)src, strlen(src)); + + pdf_lexbuf_init(ctx, &lexbuf, PDF_LEXBUF_SMALL); + + fz_try(ctx) { + result = pdf_parse_stm_obj(ctx, doc, stream, &lexbuf); + } + + fz_always(ctx) { + pdf_lexbuf_fin(ctx, &lexbuf); + fz_drop_stream(ctx, stream); + } + + fz_catch(ctx) { + fz_rethrow(ctx); + } + + return result; + +} + +//---------------------------------------------------------------------------- +// return normalized /Rotate value:one of 0, 90, 180, 270 +//---------------------------------------------------------------------------- +int JM_norm_rotation(int rotate) +{ + while (rotate < 0) rotate += 360; + while (rotate >= 360) rotate -= 360; + if (rotate % 90 != 0) return 0; + return rotate; +} + + +//---------------------------------------------------------------------------- +// return a PDF page's /Rotate value: one of (0, 90, 180, 270) +//---------------------------------------------------------------------------- +int JM_page_rotation(fz_context *ctx, pdf_page *page) +{ + int rotate = 0; + fz_try(ctx) + { + rotate = pdf_to_int(ctx, + pdf_dict_get_inheritable(ctx, page->obj, PDF_NAME(Rotate))); + rotate = JM_norm_rotation(rotate); + } + fz_catch(ctx) return 0; + return rotate; +} + + +//---------------------------------------------------------------------------- +// return a PDF page's MediaBox +//---------------------------------------------------------------------------- +fz_rect JM_mediabox(fz_context *ctx, pdf_obj *page_obj) +{ + fz_rect mediabox, page_mediabox; + + mediabox = pdf_to_rect(ctx, pdf_dict_get_inheritable(ctx, page_obj, + PDF_NAME(MediaBox))); + if (fz_is_empty_rect(mediabox) || fz_is_infinite_rect(mediabox)) + { + mediabox.x0 = 0; + mediabox.y0 = 0; + mediabox.x1 = 612; + mediabox.y1 = 792; + } + + page_mediabox.x0 = fz_min(mediabox.x0, mediabox.x1); + page_mediabox.y0 = fz_min(mediabox.y0, mediabox.y1); + page_mediabox.x1 = fz_max(mediabox.x0, mediabox.x1); + page_mediabox.y1 = fz_max(mediabox.y0, mediabox.y1); + + if (page_mediabox.x1 - page_mediabox.x0 < 1 || + page_mediabox.y1 - page_mediabox.y0 < 1) + page_mediabox = fz_unit_rect; + + return page_mediabox; +} + + +//---------------------------------------------------------------------------- +// return a PDF page's CropBox +//---------------------------------------------------------------------------- +fz_rect JM_cropbox(fz_context *ctx, pdf_obj *page_obj) +{ + fz_rect mediabox = JM_mediabox(ctx, page_obj); + fz_rect cropbox = pdf_to_rect(ctx, + pdf_dict_get_inheritable(ctx, page_obj, PDF_NAME(CropBox))); + if (fz_is_infinite_rect(cropbox) || fz_is_empty_rect(cropbox)) + cropbox = mediabox; + float y0 = mediabox.y1 - cropbox.y1; + float y1 = mediabox.y1 - cropbox.y0; + cropbox.y0 = y0; + cropbox.y1 = y1; + return cropbox; +} + + +//---------------------------------------------------------------------------- +// calculate width and height of the UNROTATED page +//---------------------------------------------------------------------------- +fz_point JM_cropbox_size(fz_context *ctx, pdf_obj *page_obj) +{ + fz_point size; + fz_try(ctx) + { + fz_rect rect = JM_cropbox(ctx, page_obj); + float w = (rect.x0 < rect.x1 ? rect.x1 - rect.x0 : rect.x0 - rect.x1); + float h = (rect.y0 < rect.y1 ? rect.y1 - rect.y0 : rect.y0 - rect.y1); + size = fz_make_point(w, h); + } + fz_catch(ctx) fz_rethrow(ctx); + return size; +} + + +//---------------------------------------------------------------------------- +// calculate page rotation matrices +//---------------------------------------------------------------------------- +fz_matrix JM_rotate_page_matrix(fz_context *ctx, pdf_page *page) +{ + if (!page) return fz_identity; // no valid pdf page given + int rotation = JM_page_rotation(ctx, page); + if (rotation == 0) return fz_identity; // no rotation + fz_matrix m; + fz_point cb_size = JM_cropbox_size(ctx, page->obj); + float w = cb_size.x; + float h = cb_size.y; + if (rotation == 90) + m = fz_make_matrix(0, 1, -1, 0, h, 0); + else if (rotation == 180) + m = fz_make_matrix(-1, 0, 0, -1, w, h); + else + m = fz_make_matrix(0, -1, 1, 0, 0, w); + return m; +} + + +fz_matrix JM_derotate_page_matrix(fz_context *ctx, pdf_page *page) +{ // just the inverse of rotation + return fz_invert_matrix(JM_rotate_page_matrix(ctx, page)); +} + + +//----------------------------------------------------------------------------- +// Insert a font in a PDF +//----------------------------------------------------------------------------- +PyObject * +JM_insert_font(fz_context *ctx, pdf_document *pdf, char *bfname, char *fontfile, + PyObject *fontbuffer, int set_simple, int idx, int wmode, int serif, + int encoding, int ordering) +{ + pdf_obj *font_obj = NULL; + fz_font *font = NULL; + fz_buffer *res = NULL; + const unsigned char *data = NULL; + int size, ixref = 0, index = 0, simple = 0; + PyObject *value=NULL, *name=NULL, *subt=NULL, *exto = NULL; + + fz_var(exto); + fz_var(name); + fz_var(subt); + fz_var(res); + fz_var(font); + fz_var(font_obj); + fz_try(ctx) { + ENSURE_OPERATION(ctx, pdf); + //------------------------------------------------------------- + // check for CJK font + //------------------------------------------------------------- + if (ordering > -1) { + data = fz_lookup_cjk_font(ctx, ordering, &size, &index); + } + if (data) { + font = fz_new_font_from_memory(ctx, NULL, data, size, index, 0); + font_obj = pdf_add_cjk_font(ctx, pdf, font, ordering, wmode, serif); + exto = JM_UnicodeFromStr("n/a"); + simple = 0; + goto weiter; + } + + //------------------------------------------------------------- + // check for PDF Base-14 font + //------------------------------------------------------------- + if (bfname) { + data = fz_lookup_base14_font(ctx, bfname, &size); + } + if (data) { + font = fz_new_font_from_memory(ctx, bfname, data, size, 0, 0); + font_obj = pdf_add_simple_font(ctx, pdf, font, encoding); + exto = JM_UnicodeFromStr("n/a"); + simple = 1; + goto weiter; + } + + if (fontfile) { + font = fz_new_font_from_file(ctx, NULL, fontfile, idx, 0); + } else { + res = JM_BufferFromBytes(ctx, fontbuffer); + if (!res) { + RAISEPY(ctx, MSG_FILE_OR_BUFFER, PyExc_ValueError); + } + font = fz_new_font_from_buffer(ctx, NULL, res, idx, 0); + } + + if (!set_simple) { + font_obj = pdf_add_cid_font(ctx, pdf, font); + simple = 0; + } else { + font_obj = pdf_add_simple_font(ctx, pdf, font, encoding); + simple = 2; + } + + weiter: ; + ixref = pdf_to_num(ctx, font_obj); + name = JM_EscapeStrFromStr(pdf_to_name(ctx, + pdf_dict_get(ctx, font_obj, PDF_NAME(BaseFont)))); + + subt = JM_UnicodeFromStr(pdf_to_name(ctx, + pdf_dict_get(ctx, font_obj, PDF_NAME(Subtype)))); + + if (!exto) + exto = JM_UnicodeFromStr(JM_get_fontextension(ctx, pdf, ixref)); + + float asc = fz_font_ascender(ctx, font); + float dsc = fz_font_descender(ctx, font); + value = Py_BuildValue("[i,{s:O,s:O,s:O,s:O,s:i,s:f,s:f}]", + ixref, + "name", name, // base font name + "type", subt, // subtype + "ext", exto, // file extension + "simple", JM_BOOL(simple), // simple font? + "ordering", ordering, // CJK font? + "ascender", asc, + "descender", dsc + ); + } + fz_always(ctx) { + Py_CLEAR(exto); + Py_CLEAR(name); + Py_CLEAR(subt); + fz_drop_buffer(ctx, res); + fz_drop_font(ctx, font); + pdf_drop_obj(ctx, font_obj); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return value; +} + + +//----------------------------------------------------------------------------- +// compute image insertion matrix +//----------------------------------------------------------------------------- +fz_matrix +calc_image_matrix(int width, int height, PyObject *tr, int rotate, int keep) +{ + float large, small, fw, fh, trw, trh, f, w, h; + fz_rect trect = JM_rect_from_py(tr); + fz_matrix rot = fz_rotate((float) rotate); + trw = trect.x1 - trect.x0; + trh = trect.y1 - trect.y0; + w = trw; + h = trh; + if (keep) { + large = (float) Py_MAX(width, height); + fw = (float) width / large; + fh = (float) height / large; + } else { + fw = fh = 1; + } + small = Py_MIN(fw, fh); + if (rotate != 0 && rotate != 180) { + f = fw; + fw = fh; + fh = f; + } + if (fw < 1) { + if ((trw / fw) > (trh / fh)) { + w = trh * small; + h = trh; + } else { + w = trw; + h = trw / small; + } + } else if (fw != fh) { + if ((trw / fw) > (trh / fh)) { + w = trh / small; + h = trh; + } else { + w = trw; + h = trw * small; + } + } else { + w = trw; + h = trh; + } + fz_point tmp = fz_make_point((trect.x0 + trect.x1) / 2, + (trect.y0 + trect.y1) / 2); + fz_matrix mat = fz_make_matrix(1, 0, 0, 1, -0.5, -0.5); + mat = fz_concat(mat, rot); + mat = fz_concat(mat, fz_scale(w, h)); + mat = fz_concat(mat, fz_translate(tmp.x, tmp.y)); + return mat; +} + +// -------------------------------------------------------- +// Callback function for the Story class +// -------------------------------------------------------- +static PyObject *make_story_elpos = NULL; // Py function returning object +void Story_Callback(fz_context *ctx, void *opaque, fz_story_element_position *pos) +{ +#define SETATTR(a, v) PyObject_SetAttrString(arg, a, v);Py_DECREF(v) + // ------------------------------------------------------------------------ + // 'opaque' is a tuple (userfunc, userdict), where 'userfunc' is a function + // in the user's script and 'userdict' is a dictionary containing any + // additional parameters of the user + // userfunc will be called with the joined info of userdict and pos. + // ------------------------------------------------------------------------ + PyObject *callarg = (PyObject *) opaque; + PyObject *userfunc = PyTuple_GET_ITEM(callarg, 0); + PyObject *userdict = PyTuple_GET_ITEM(callarg, 1); + + PyObject *this_module = PyImport_AddModule("fitz"); // get our module + if (!make_story_elpos) { // locate ElementPosition maker once + make_story_elpos = Py_BuildValue("s", "make_story_elpos"); + } + // get access to ElementPosition() object + PyObject *arg = PyObject_CallMethodObjArgs(this_module, make_story_elpos, NULL); + Py_INCREF(arg); + SETATTR("depth", Py_BuildValue("i", pos->depth)); + SETATTR("heading", Py_BuildValue("i", pos->heading)); + SETATTR("id", Py_BuildValue("s", pos->id)); + SETATTR("rect", JM_py_from_rect(pos->rect)); + SETATTR("text", Py_BuildValue("s", pos->text)); + SETATTR("open_close", Py_BuildValue("i", pos->open_close)); + SETATTR("rect_num", Py_BuildValue("i", pos->rectangle_num)); + SETATTR("href", Py_BuildValue("s", pos->href)); + + // iterate over userdict items and set their attributes + PyObject *pkey = NULL; + PyObject *pval = NULL; + Py_ssize_t ppos = 0; + while (PyDict_Next(userdict, &ppos, &pkey, &pval)) { + PyObject_SetAttr(arg, pkey, pval); + } + PyObject_CallFunctionObjArgs(userfunc, arg, NULL); +#undef SETATTR +} + +// ----------------------------------------------------------- +// Return last archive if it is a tree and mount points match +// ----------------------------------------------------------- +fz_archive *JM_last_tree(fz_context *ctx, fz_archive *arch, const char *mount) +{ + typedef struct + { + fz_archive *arch; + char *dir; + } multi_archive_entry; + + typedef struct + { + fz_archive super; + int len; + int max; + multi_archive_entry *sub; + } fz_multi_archive; + + if (!arch) { + return NULL; + } + + fz_multi_archive *multi = (fz_multi_archive *) arch; + if (multi->len == 0) { // archive is empty + return NULL; + } + int i = multi->len - 1; // read last sub archive + multi_archive_entry *e = &multi->sub[i]; + fz_archive *arch_ = e->arch; + const char *mount_ = e->dir; + const char *fmt = fz_archive_format(ctx, arch_); + if (strcmp(fmt, "tree") != 0) { // not a tree archive + return NULL; + } + if ((mount_ && mount && strcmp(mount, mount_) == 0) || (!mount && !mount_)) { // last sub archive is eligible! + return arch_; + } + return NULL; +} + +fz_archive *JM_archive_from_py(fz_context *ctx, fz_archive *arch, PyObject *path, const char *mount, int *drop_sub) +{ + fz_stream *stream = NULL; + fz_buffer *buff = NULL; + *drop_sub = 1; + fz_archive *sub = NULL; + const char *my_mount = mount; + fz_try(ctx) { + // tree archive: tuple of memory items + // check if we can add to last sub-archive + sub = JM_last_tree(ctx, arch, my_mount); + if (!sub) { + sub = fz_new_tree_archive(ctx, NULL); + } else { + *drop_sub = 0; // never drop last sub-archive + } + + // a single tree item + if (PyBytes_Check(path) || PyByteArray_Check(path) || PyObject_HasAttrString(path, "getvalue")) { + buff = JM_BufferFromBytes(ctx, path); + fz_tree_archive_add_buffer(ctx, sub, mount, buff); + goto finished; + } + + // a tuple of tree items + Py_ssize_t i, n = PyTuple_Size(path); + for (i = 0; i < n; i++) { + PyObject *item = PyTuple_GET_ITEM(path, i); + PyObject *i0 = PySequence_GetItem(item, 0); // data + PyObject *i1 = PySequence_GetItem(item, 1); // name + buff = JM_BufferFromBytes(ctx, i0); + fz_tree_archive_add_buffer(ctx, sub, PyUnicode_AsUTF8(i1), buff); + fz_drop_buffer(ctx, buff); + Py_DECREF(i0); + Py_DECREF(i1); + } + buff = NULL; + goto finished; + + finished:; + } + + fz_always(ctx) { + fz_drop_buffer(ctx, buff); + fz_drop_stream(ctx, stream); + } + + fz_catch(ctx) { + fz_rethrow(ctx); + } + + return sub; +} + + +int JM_rects_overlap(const fz_rect a, const fz_rect b) +{ + if (0 + || a.x0 >= b.x1 + || a.y0 >= b.y1 + || a.x1 <= b.x0 + || a.y1 <= b.y0 + ) + return 0; + return 1; +} + +//----------------------------------------------------------------------------- +// dummy structure for various tools and utilities +//----------------------------------------------------------------------------- +struct Tools {int index;}; + +typedef struct fz_item fz_item; + +struct fz_item +{ + void *key; + fz_storable *val; + size_t size; + fz_item *next; + fz_item *prev; + fz_store *store; + const fz_store_type *type; +}; + +struct fz_store +{ + int refs; + + /* Every item in the store is kept in a doubly linked list, ordered + * by usage (so LRU entries are at the end). */ + fz_item *head; + fz_item *tail; + + /* We have a hash table that allows to quickly find a subset of the + * entries (those whose keys are indirect objects). */ + fz_hash_table *hash; + + /* We keep track of the size of the store, and keep it below max. */ + size_t max; + size_t size; + + int defer_reap_count; + int needs_reaping; +}; + +%} diff --git a/fitz/helper-pdfinfo.i b/fitz/helper-pdfinfo.i new file mode 100644 index 0000000..c332bbf --- /dev/null +++ b/fitz/helper-pdfinfo.i @@ -0,0 +1,611 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +//------------------------------------------------------------------------ +// Store ID in PDF trailer +//------------------------------------------------------------------------ +void JM_ensure_identity(fz_context *ctx, pdf_document *pdf) +{ + unsigned char rnd[16]; + pdf_obj *id; + id = pdf_dict_get(ctx, pdf_trailer(ctx, pdf), PDF_NAME(ID)); + if (!id) { + fz_memrnd(ctx, rnd, nelem(rnd)); + id = pdf_dict_put_array(ctx, pdf_trailer(ctx, pdf), PDF_NAME(ID), 2); + pdf_array_push_drop(ctx, id, pdf_new_string(ctx, (char *) rnd + 0, nelem(rnd))); + pdf_array_push_drop(ctx, id, pdf_new_string(ctx, (char *) rnd + 0, nelem(rnd))); + } +} + + +//------------------------------------------------------------------------ +// Ensure OCProperties, return /OCProperties key +//------------------------------------------------------------------------ +pdf_obj * +JM_ensure_ocproperties(fz_context *ctx, pdf_document *pdf) +{ + pdf_obj *D, *ocp; + fz_try(ctx) { + ocp = pdf_dict_get(ctx, pdf_dict_get(ctx, pdf_trailer(ctx, pdf), PDF_NAME(Root)), PDF_NAME(OCProperties)); + if (ocp) goto finished; + pdf_obj *root = pdf_dict_get(ctx, pdf_trailer(ctx, pdf), PDF_NAME(Root)); + ocp = pdf_dict_put_dict(ctx, root, PDF_NAME(OCProperties), 2); + pdf_dict_put_array(ctx, ocp, PDF_NAME(OCGs), 0); + D = pdf_dict_put_dict(ctx, ocp, PDF_NAME(D), 5); + pdf_dict_put_array(ctx, D, PDF_NAME(ON), 0); + pdf_dict_put_array(ctx, D, PDF_NAME(OFF), 0); + pdf_dict_put_array(ctx, D, PDF_NAME(Order), 0); + pdf_dict_put_array(ctx, D, PDF_NAME(RBGroups), 0); + finished:; + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return ocp; +} + + +//------------------------------------------------------------------------ +// Add OC configuration to the PDF catalog +//------------------------------------------------------------------------ +void +JM_add_layer_config(fz_context *ctx, pdf_document *pdf, char *name, char *creator, PyObject *ON) +{ + pdf_obj *D, *ocp, *configs; + fz_try(ctx) { + ocp = JM_ensure_ocproperties(ctx, pdf); + configs = pdf_dict_get(ctx, ocp, PDF_NAME(Configs)); + if (!pdf_is_array(ctx, configs)) { + configs = pdf_dict_put_array(ctx,ocp, PDF_NAME(Configs), 1); + } + D = pdf_new_dict(ctx, pdf, 5); + pdf_dict_put_text_string(ctx, D, PDF_NAME(Name), name); + if (creator) { + pdf_dict_put_text_string(ctx, D, PDF_NAME(Creator), creator); + } + pdf_dict_put(ctx, D, PDF_NAME(BaseState), PDF_NAME(OFF)); + pdf_obj *onarray = pdf_dict_put_array(ctx, D, PDF_NAME(ON), 5); + if (!EXISTS(ON) || !PySequence_Check(ON) || !PySequence_Size(ON)) { + ; + } else { + pdf_obj *ocgs = pdf_dict_get(ctx, ocp, PDF_NAME(OCGs)); + int i, n = PySequence_Size(ON); + for (i = 0; i < n; i++) { + int xref = 0; + if (JM_INT_ITEM(ON, (Py_ssize_t) i, &xref) == 1) continue; + pdf_obj *ind = pdf_new_indirect(ctx, pdf, xref, 0); + if (pdf_array_contains(ctx, ocgs, ind)) { + pdf_array_push_drop(ctx, onarray, ind); + } else { + pdf_drop_obj(ctx, ind); + } + } + } + pdf_array_push_drop(ctx, configs, D); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } +} + + +//------------------------------------------------------------------------ +// Get OCG arrays from OC configuration +// Returns dict +// {"basestate":name, "on":list, "off":list, "rbg":list, "locked":list} +//------------------------------------------------------------------------ +static PyObject * +JM_get_ocg_arrays_imp(fz_context *ctx, pdf_obj *arr) +{ + int i, n; + PyObject *list = PyList_New(0), *item = NULL; + pdf_obj *obj = NULL; + if (pdf_is_array(ctx, arr)) { + n = pdf_array_len(ctx, arr); + for (i = 0; i < n; i++) { + obj = pdf_array_get(ctx, arr, i); + item = Py_BuildValue("i", pdf_to_num(ctx, obj)); + if (!PySequence_Contains(list, item)) { + LIST_APPEND_DROP(list, item); + } else { + Py_DECREF(item); + } + } + } + return list; +} + +PyObject * +JM_get_ocg_arrays(fz_context *ctx, pdf_obj *conf) +{ + PyObject *rc = PyDict_New(), *list = NULL, *list1 = NULL; + int i, n; + pdf_obj *arr = NULL, *obj = NULL; + fz_try(ctx) { + arr = pdf_dict_get(ctx, conf, PDF_NAME(ON)); + list = JM_get_ocg_arrays_imp(ctx, arr); + if (PySequence_Size(list)) { + PyDict_SetItemString(rc, "on", list); + } + Py_DECREF(list); + arr = pdf_dict_get(ctx, conf, PDF_NAME(OFF)); + list = JM_get_ocg_arrays_imp(ctx, arr); + if (PySequence_Size(list)) { + PyDict_SetItemString(rc, "off", list); + } + Py_DECREF(list); + arr = pdf_dict_get(ctx, conf, PDF_NAME(Locked)); + list = JM_get_ocg_arrays_imp(ctx, arr); + if (PySequence_Size(list)) { + PyDict_SetItemString(rc, "locked", list); + } + Py_DECREF(list); + list = PyList_New(0); + arr = pdf_dict_get(ctx, conf, PDF_NAME(RBGroups)); + if (pdf_is_array(ctx, arr)) { + n = pdf_array_len(ctx, arr); + for (i = 0; i < n; i++) { + obj = pdf_array_get(ctx, arr, i); + list1 = JM_get_ocg_arrays_imp(ctx, obj); + LIST_APPEND_DROP(list, list1); + } + } + if (PySequence_Size(list)) { + PyDict_SetItemString(rc, "rbgroups", list); + } + Py_DECREF(list); + obj = pdf_dict_get(ctx, conf, PDF_NAME(BaseState)); + + if (obj) { + PyObject *state = NULL; + state = Py_BuildValue("s", pdf_to_name(ctx, obj)); + PyDict_SetItemString(rc, "basestate", state); + Py_DECREF(state); + } + } + fz_always(ctx) { + } + fz_catch(ctx) { + Py_CLEAR(rc); + PyErr_Clear(); + fz_rethrow(ctx); + } + return rc; +} + + +//------------------------------------------------------------------------ +// Set OCG arrays from dict of Python lists +// Works with dict like {"basestate":name, "on":list, "off":list, "rbg":list} +//------------------------------------------------------------------------ +static void +JM_set_ocg_arrays_imp(fz_context *ctx, pdf_obj *arr, PyObject *list) +{ + int i, n = PySequence_Size(list); + pdf_obj *obj = NULL; + pdf_document *pdf = pdf_get_bound_document(ctx, arr); + for (i = 0; i < n; i++) { + int xref = 0; + if (JM_INT_ITEM(list, i, &xref) == 1) continue; + obj = pdf_new_indirect(ctx, pdf, xref, 0); + pdf_array_push_drop(ctx, arr, obj); + } + return; +} + +static void +JM_set_ocg_arrays(fz_context *ctx, pdf_obj *conf, const char *basestate, + PyObject *on, PyObject *off, PyObject *rbgroups, PyObject *locked) +{ + int i, n; + pdf_obj *arr = NULL, *obj = NULL; + fz_try(ctx) { + if (basestate) { + pdf_dict_put_name(ctx, conf, PDF_NAME(BaseState), basestate); + } + + if (on != Py_None) { + pdf_dict_del(ctx, conf, PDF_NAME(ON)); + if (PySequence_Size(on)) { + arr = pdf_dict_put_array(ctx, conf, PDF_NAME(ON), 1); + JM_set_ocg_arrays_imp(ctx, arr, on); + } + } + + if (off != Py_None) { + pdf_dict_del(ctx, conf, PDF_NAME(OFF)); + if (PySequence_Size(off)) { + arr = pdf_dict_put_array(ctx, conf, PDF_NAME(OFF), 1); + JM_set_ocg_arrays_imp(ctx, arr, off); + } + } + + if (locked != Py_None) { + pdf_dict_del(ctx, conf, PDF_NAME(Locked)); + if (PySequence_Size(locked)) { + arr = pdf_dict_put_array(ctx, conf, PDF_NAME(Locked), 1); + JM_set_ocg_arrays_imp(ctx, arr, locked); + } + } + + if (rbgroups != Py_None) { + pdf_dict_del(ctx, conf, PDF_NAME(RBGroups)); + if (PySequence_Size(rbgroups)) { + arr = pdf_dict_put_array(ctx, conf, PDF_NAME(RBGroups), 1); + n = PySequence_Size(rbgroups); + for (i = 0; i < n; i++) { + PyObject *item0 = PySequence_ITEM(rbgroups, i); + obj = pdf_array_push_array(ctx, arr, 1); + JM_set_ocg_arrays_imp(ctx, obj, item0); + Py_DECREF(item0); + } + } + } + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return; +} + + +//------------------------------------------------------------------------ +// Return the items of Resources/Properties (used for Marked Content) +// Argument may be e.g. a page object or a Form XObject +//------------------------------------------------------------------------ +PyObject * +JM_get_resource_properties(fz_context *ctx, pdf_obj *ref) +{ + PyObject *rc = NULL; + fz_try(ctx) { + pdf_obj *properties = pdf_dict_getl(ctx, ref, + PDF_NAME(Resources), + PDF_NAME(Properties), NULL); + if (!properties) { + rc = PyTuple_New(0); + } else { + int i, n = pdf_dict_len(ctx, properties); + if (n < 1) { + rc = PyTuple_New(0); + goto finished; + } + rc = PyTuple_New(n); + for (i = 0; i < n; i++) { + pdf_obj *key = pdf_dict_get_key(ctx, properties, i); + pdf_obj *val = pdf_dict_get_val(ctx, properties, i); + const char *c = pdf_to_name(ctx, key); + int xref = pdf_to_num(ctx, val); + PyTuple_SET_ITEM(rc, i, Py_BuildValue("si", c, xref)); + } + } + finished:; + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return rc; +} + + +//------------------------------------------------------------------------ +// Insert an item into Resources/Properties (used for Marked Content) +// Arguments: +// (1) e.g. page object, Form XObject +// (2) marked content name +// (3) xref of the referenced object (insert as indirect reference) +//------------------------------------------------------------------------ +void +JM_set_resource_property(fz_context *ctx, pdf_obj *ref, const char *name, int xref) +{ + pdf_obj *ind = NULL; + pdf_obj *properties = NULL; + pdf_document *pdf = pdf_get_bound_document(ctx, ref); + pdf_obj *name2 = NULL; + fz_var(ind); + fz_var(name2); + fz_try(ctx) { + ind = pdf_new_indirect(ctx, pdf, xref, 0); + if (!ind) { + RAISEPY(ctx, MSG_BAD_XREF, PyExc_ValueError); + } + pdf_obj *resources = pdf_dict_get(ctx, ref, PDF_NAME(Resources)); + if (!resources) { + resources = pdf_dict_put_dict(ctx, ref, PDF_NAME(Resources), 1); + } + properties = pdf_dict_get(ctx, resources, PDF_NAME(Properties)); + if (!properties) { + properties = pdf_dict_put_dict(ctx, resources, PDF_NAME(Properties), 1); + } + name2 = pdf_new_name(ctx, name); + pdf_dict_put(ctx, properties, name2, ind); + } + fz_always(ctx) { + pdf_drop_obj(ctx, ind); + pdf_drop_obj(ctx, name2); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return; +} + + +//------------------------------------------------------------------------ +// Add OC object reference to a dictionary +//------------------------------------------------------------------------ +void +JM_add_oc_object(fz_context *ctx, pdf_document *pdf, pdf_obj *ref, int xref) +{ + pdf_obj *indobj = NULL; + fz_try(ctx) { + indobj = pdf_new_indirect(ctx, pdf, xref, 0); + if (!pdf_is_dict(ctx, indobj)) { + RAISEPY(ctx, MSG_BAD_OC_REF, PyExc_ValueError); + } + pdf_obj *type = pdf_dict_get(ctx, indobj, PDF_NAME(Type)); + if (pdf_objcmp(ctx, type, PDF_NAME(OCG)) == 0 || + pdf_objcmp(ctx, type, PDF_NAME(OCMD)) == 0) { + pdf_dict_put(ctx, ref, PDF_NAME(OC), indobj); + } else { + RAISEPY(ctx, MSG_BAD_OC_REF, PyExc_ValueError); + } + } + fz_always(ctx) { + pdf_drop_obj(ctx, indobj); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } +} + + +//------------------------------------------------------------------------- +// Store info of a font in Python list +//------------------------------------------------------------------------- +int JM_gather_fonts(fz_context *ctx, pdf_document *pdf, pdf_obj *dict, + PyObject *fontlist, int stream_xref) +{ + int i, n, rc = 1; + n = pdf_dict_len(ctx, dict); + for (i = 0; i < n; i++) { + pdf_obj *fontdict = NULL; + pdf_obj *subtype = NULL; + pdf_obj *basefont = NULL; + pdf_obj *name = NULL; + pdf_obj *refname = NULL; + pdf_obj *encoding = NULL; + + refname = pdf_dict_get_key(ctx, dict, i); + fontdict = pdf_dict_get_val(ctx, dict, i); + if (!pdf_is_dict(ctx, fontdict)) { + fz_warn(ctx, "'%s' is no font dict (%d 0 R)", + pdf_to_name(ctx, refname), pdf_to_num(ctx, fontdict)); + continue; + } + + subtype = pdf_dict_get(ctx, fontdict, PDF_NAME(Subtype)); + basefont = pdf_dict_get(ctx, fontdict, PDF_NAME(BaseFont)); + if (!basefont || pdf_is_null(ctx, basefont)) { + name = pdf_dict_get(ctx, fontdict, PDF_NAME(Name)); + } else { + name = basefont; + } + encoding = pdf_dict_get(ctx, fontdict, PDF_NAME(Encoding)); + if (pdf_is_dict(ctx, encoding)) { + encoding = pdf_dict_get(ctx, encoding, PDF_NAME(BaseEncoding)); + } + int xref = pdf_to_num(ctx, fontdict); + char *ext = "n/a"; + if (xref) { + ext = JM_get_fontextension(ctx, pdf, xref); + } + PyObject *entry = PyTuple_New(7); + PyTuple_SET_ITEM(entry, 0, Py_BuildValue("i", xref)); + PyTuple_SET_ITEM(entry, 1, Py_BuildValue("s", ext)); + PyTuple_SET_ITEM(entry, 2, Py_BuildValue("s", pdf_to_name(ctx, subtype))); + PyTuple_SET_ITEM(entry, 3, JM_EscapeStrFromStr(pdf_to_name(ctx, name))); + PyTuple_SET_ITEM(entry, 4, Py_BuildValue("s", pdf_to_name(ctx, refname))); + PyTuple_SET_ITEM(entry, 5, Py_BuildValue("s", pdf_to_name(ctx, encoding))); + PyTuple_SET_ITEM(entry, 6, Py_BuildValue("i", stream_xref)); + LIST_APPEND_DROP(fontlist, entry); + } + return rc; +} + +//------------------------------------------------------------------------- +// Store info of an image in Python list +//------------------------------------------------------------------------- +int JM_gather_images(fz_context *ctx, pdf_document *doc, pdf_obj *dict, + PyObject *imagelist, int stream_xref) +{ + int i, n, rc = 1; + n = pdf_dict_len(ctx, dict); + for (i = 0; i < n; i++) { + pdf_obj *imagedict, *smask; + pdf_obj *refname = NULL; + pdf_obj *type; + pdf_obj *width; + pdf_obj *height; + pdf_obj *bpc = NULL; + pdf_obj *filter = NULL; + pdf_obj *cs = NULL; + pdf_obj *altcs; + + refname = pdf_dict_get_key(ctx, dict, i); + imagedict = pdf_dict_get_val(ctx, dict, i); + if (!pdf_is_dict(ctx, imagedict)) { + fz_warn(ctx, "'%s' is no image dict (%d 0 R)", + pdf_to_name(ctx, refname), pdf_to_num(ctx, imagedict)); + continue; + } + + type = pdf_dict_get(ctx, imagedict, PDF_NAME(Subtype)); + if (!pdf_name_eq(ctx, type, PDF_NAME(Image))) + continue; + + int xref = pdf_to_num(ctx, imagedict); + int gen = 0; + smask = pdf_dict_geta(ctx, imagedict, PDF_NAME(SMask), PDF_NAME(Mask)); + if (smask) + gen = pdf_to_num(ctx, smask); + + filter = pdf_dict_geta(ctx, imagedict, PDF_NAME(Filter), PDF_NAME(F)); + if (pdf_is_array(ctx, filter)) { + filter = pdf_array_get(ctx, filter, 0); + } + + altcs = NULL; + cs = pdf_dict_geta(ctx, imagedict, PDF_NAME(ColorSpace), PDF_NAME(CS)); + if (pdf_is_array(ctx, cs)) { + pdf_obj *cses = cs; + cs = pdf_array_get(ctx, cses, 0); + if (pdf_name_eq(ctx, cs, PDF_NAME(DeviceN)) || + pdf_name_eq(ctx, cs, PDF_NAME(Separation))) { + altcs = pdf_array_get(ctx, cses, 2); + if (pdf_is_array(ctx, altcs)) { + altcs = pdf_array_get(ctx, altcs, 0); + } + } + } + + width = pdf_dict_geta(ctx, imagedict, PDF_NAME(Width), PDF_NAME(W)); + height = pdf_dict_geta(ctx, imagedict, PDF_NAME(Height), PDF_NAME(H)); + bpc = pdf_dict_geta(ctx, imagedict, PDF_NAME(BitsPerComponent), PDF_NAME(BPC)); + + PyObject *entry = PyTuple_New(10); + PyTuple_SET_ITEM(entry, 0, Py_BuildValue("i", xref)); + PyTuple_SET_ITEM(entry, 1, Py_BuildValue("i", gen)); + PyTuple_SET_ITEM(entry, 2, Py_BuildValue("i", pdf_to_int(ctx, width))); + PyTuple_SET_ITEM(entry, 3, Py_BuildValue("i", pdf_to_int(ctx, height))); + PyTuple_SET_ITEM(entry, 4, Py_BuildValue("i", pdf_to_int(ctx, bpc))); + PyTuple_SET_ITEM(entry, 5, JM_EscapeStrFromStr(pdf_to_name(ctx, cs))); + PyTuple_SET_ITEM(entry, 6, JM_EscapeStrFromStr(pdf_to_name(ctx, altcs))); + PyTuple_SET_ITEM(entry, 7, JM_EscapeStrFromStr(pdf_to_name(ctx, refname))); + PyTuple_SET_ITEM(entry, 8, JM_EscapeStrFromStr(pdf_to_name(ctx, filter))); + PyTuple_SET_ITEM(entry, 9, Py_BuildValue("i", stream_xref)); + LIST_APPEND_DROP(imagelist, entry); + } + return rc; +} + +//------------------------------------------------------------------------- +// Store info of a /Form xobject in Python list +//------------------------------------------------------------------------- +int JM_gather_forms(fz_context *ctx, pdf_document *doc, pdf_obj *dict, + PyObject *imagelist, int stream_xref) +{ + int i, rc = 1, n = pdf_dict_len(ctx, dict); + fz_rect bbox; + fz_matrix mat; + pdf_obj *o = NULL, *m = NULL; + for (i = 0; i < n; i++) { + pdf_obj *imagedict; + pdf_obj *refname = NULL; + pdf_obj *type; + + refname = pdf_dict_get_key(ctx, dict, i); + imagedict = pdf_dict_get_val(ctx, dict, i); + if (!pdf_is_dict(ctx, imagedict)) { + fz_warn(ctx, "'%s' is no form dict (%d 0 R)", + pdf_to_name(ctx, refname), pdf_to_num(ctx, imagedict)); + continue; + } + + type = pdf_dict_get(ctx, imagedict, PDF_NAME(Subtype)); + if (!pdf_name_eq(ctx, type, PDF_NAME(Form))) + continue; + + o = pdf_dict_get(ctx, imagedict, PDF_NAME(BBox)); + m = pdf_dict_get(ctx, imagedict, PDF_NAME(Matrix)); + if (m) { + mat = pdf_to_matrix(ctx, m); + } else { + mat = fz_identity; + } + if (o) { + bbox = fz_transform_rect(pdf_to_rect(ctx, o), mat); + } else { + bbox = fz_infinite_rect; + } + int xref = pdf_to_num(ctx, imagedict); + + PyObject *entry = PyTuple_New(4); + PyTuple_SET_ITEM(entry, 0, Py_BuildValue("i", xref)); + PyTuple_SET_ITEM(entry, 1, Py_BuildValue("s", pdf_to_name(ctx, refname))); + PyTuple_SET_ITEM(entry, 2, Py_BuildValue("i", stream_xref)); + PyTuple_SET_ITEM(entry, 3, JM_py_from_rect(bbox)); + LIST_APPEND_DROP(imagelist, entry); + } + return rc; +} + +//------------------------------------------------------------------------- +// Step through /Resources, looking up image, xobject or font information +//------------------------------------------------------------------------- +void JM_scan_resources(fz_context *ctx, pdf_document *pdf, pdf_obj *rsrc, + PyObject *liste, int what, int stream_xref, + PyObject *tracer) +{ + pdf_obj *font, *xobj, *subrsrc; + int i, n, sxref; + if (pdf_mark_obj(ctx, rsrc)) { + fz_warn(ctx, "Circular dependencies! Consider page cleaning."); + return; // Circular dependencies! + } + + fz_try(ctx) { + + xobj = pdf_dict_get(ctx, rsrc, PDF_NAME(XObject)); + + if (what == 1) { // lookup fonts + font = pdf_dict_get(ctx, rsrc, PDF_NAME(Font)); + JM_gather_fonts(ctx, pdf, font, liste, stream_xref); + } else if (what == 2) { // look up images + JM_gather_images(ctx, pdf, xobj, liste, stream_xref); + } else if (what == 3) { // look up form xobjects + JM_gather_forms(ctx, pdf, xobj, liste, stream_xref); + } else { // should never happen + goto finished; + } + + // check if we need to recurse into Form XObjects + n = pdf_dict_len(ctx, xobj); + for (i = 0; i < n; i++) { + pdf_obj *obj = pdf_dict_get_val(ctx, xobj, i); + if (pdf_is_stream(ctx, obj)) { + sxref = pdf_to_num(ctx, obj); + } else { + sxref = 0; + } + subrsrc = pdf_dict_get(ctx, obj, PDF_NAME(Resources)); + if (subrsrc) { + PyObject *sxref_t = Py_BuildValue("i", sxref); + if (PySequence_Contains(tracer, sxref_t) == 0) { + LIST_APPEND_DROP(tracer, sxref_t); + JM_scan_resources(ctx, pdf, subrsrc, liste, what, sxref, tracer); + } else { + Py_DECREF(sxref_t); + PyErr_Clear(); + fz_warn(ctx, "Circular dependencies! Consider page cleaning."); + goto finished; + } + } + } + finished:; + } + fz_always(ctx) { + pdf_unmark_obj(ctx, rsrc); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } +} +%} diff --git a/fitz/helper-pixmap.i b/fitz/helper-pixmap.i new file mode 100644 index 0000000..ff45591 --- /dev/null +++ b/fitz/helper-pixmap.i @@ -0,0 +1,431 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +//----------------------------------------------------------------------------- +// pixmap helper functions +//----------------------------------------------------------------------------- + +//----------------------------------------------------------------------------- +// Clear a pixmap rectangle - my version also supports non-alpha pixmaps +//----------------------------------------------------------------------------- +int +JM_clear_pixmap_rect_with_value(fz_context *ctx, fz_pixmap *dest, int value, fz_irect b) +{ + unsigned char *destp; + int x, y, w, k, destspan; + + b = fz_intersect_irect(b, fz_pixmap_bbox(ctx, dest)); + w = b.x1 - b.x0; + y = b.y1 - b.y0; + if (w <= 0 || y <= 0) + return 0; + + destspan = dest->stride; + destp = dest->samples + (unsigned int)(destspan * (b.y0 - dest->y) + dest->n * (b.x0 - dest->x)); + + /* CMYK needs special handling (and potentially any other subtractive colorspaces) */ + if (fz_colorspace_n(ctx, dest->colorspace) == 4) { + value = 255 - value; + do { + unsigned char *s = destp; + for (x = 0; x < w; x++) { + *s++ = 0; + *s++ = 0; + *s++ = 0; + *s++ = value; + if (dest->alpha) *s++ = 255; + } + destp += destspan; + } while (--y); + return 1; + } + + do { + unsigned char *s = destp; + for (x = 0; x < w; x++) { + for (k = 0; k < dest->n - 1; k++) + *s++ = value; + if (dest->alpha) *s++ = 255; + else *s++ = value; + } + destp += destspan; + } while (--y); + return 1; +} + +//----------------------------------------------------------------------------- +// fill a rect with a color tuple +//----------------------------------------------------------------------------- +int +JM_fill_pixmap_rect_with_color(fz_context *ctx, fz_pixmap *dest, unsigned char col[5], fz_irect b) +{ + unsigned char *destp; + int x, y, w, i, destspan; + + b = fz_intersect_irect(b, fz_pixmap_bbox(ctx, dest)); + w = b.x1 - b.x0; + y = b.y1 - b.y0; + if (w <= 0 || y <= 0) + return 0; + + destspan = dest->stride; + destp = dest->samples + (unsigned int)(destspan * (b.y0 - dest->y) + dest->n * (b.x0 - dest->x)); + + do { + unsigned char *s = destp; + for (x = 0; x < w; x++) { + for (i = 0; i < dest->n; i++) + *s++ = col[i]; + } + destp += destspan; + } while (--y); + return 1; +} + +//----------------------------------------------------------------------------- +// invert a rectangle - also supports non-alpha pixmaps +//----------------------------------------------------------------------------- +int +JM_invert_pixmap_rect(fz_context *ctx, fz_pixmap *dest, fz_irect b) +{ + unsigned char *destp; + int x, y, w, i, destspan; + + b = fz_intersect_irect(b, fz_pixmap_bbox(ctx, dest)); + w = b.x1 - b.x0; + y = b.y1 - b.y0; + if (w <= 0 || y <= 0) + return 0; + + destspan = dest->stride; + destp = dest->samples + (unsigned int)(destspan * (b.y0 - dest->y) + dest->n * (b.x0 - dest->x)); + int n0 = dest->n - dest->alpha; + do { + unsigned char *s = destp; + for (x = 0; x < w; x++) { + for (i = 0; i < n0; i++) { + *s = 255 - *s; + s++; + } + if (dest->alpha) s++; + } + destp += destspan; + } while (--y); + return 1; +} + +int +JM_is_jbig2_image(fz_context *ctx, pdf_obj *dict) +{ + // fixme: should we remove this function? + return 0; + /* + pdf_obj *filter; + int i, n; + + filter = pdf_dict_get(ctx, dict, PDF_NAME(Filter)); + if (pdf_name_eq(ctx, filter, PDF_NAME(JBIG2Decode))) + return 1; + n = pdf_array_len(ctx, filter); + for (i = 0; i < n; i++) + if (pdf_name_eq(ctx, pdf_array_get(ctx, filter, i), PDF_NAME(JBIG2Decode))) + return 1; + return 0; + */ +} + +//----------------------------------------------------------------------------- +// Return basic properties of an image provided as bytes or bytearray +// The function creates an fz_image and optionally returns it. +//----------------------------------------------------------------------------- +PyObject *JM_image_profile(fz_context *ctx, PyObject *imagedata, int keep_image) +{ + if (!EXISTS(imagedata)) { + Py_RETURN_NONE; // nothing given + } + fz_image *image = NULL; + fz_buffer *res = NULL; + PyObject *result = NULL; + unsigned char *c = NULL; + Py_ssize_t len = 0; + if (PyBytes_Check(imagedata)) { + c = PyBytes_AS_STRING(imagedata); + len = PyBytes_GET_SIZE(imagedata); + } else if (PyByteArray_Check(imagedata)) { + c = PyByteArray_AS_STRING(imagedata); + len = PyByteArray_GET_SIZE(imagedata); + } else { + PySys_WriteStderr("bad image data\n"); + Py_RETURN_NONE; + } + + if (len < 8) { + PySys_WriteStderr("bad image data\n"); + Py_RETURN_NONE; + } + int type = fz_recognize_image_format(ctx, c); + if (type == FZ_IMAGE_UNKNOWN) { + Py_RETURN_NONE; + } + + fz_try(ctx) { + if (keep_image) { + res = fz_new_buffer_from_copied_data(ctx, c, (size_t) len); + } else { + res = fz_new_buffer_from_shared_data(ctx, c, (size_t) len); + } + image = fz_new_image_from_buffer(ctx, res); + int xres, yres, orientation; + fz_matrix ctm = fz_image_orientation_matrix(ctx, image); + fz_image_resolution(image, &xres, &yres); + orientation = (int) fz_image_orientation(ctx, image); + const char *cs_name = fz_colorspace_name(ctx, image->colorspace); + result = PyDict_New(); + DICT_SETITEM_DROP(result, dictkey_width, + Py_BuildValue("i", image->w)); + DICT_SETITEM_DROP(result, dictkey_height, + Py_BuildValue("i", image->h)); + DICT_SETITEMSTR_DROP(result, "orientation", + Py_BuildValue("i", orientation)); + DICT_SETITEM_DROP(result, dictkey_matrix, + JM_py_from_matrix(ctm)); + DICT_SETITEM_DROP(result, dictkey_xres, + Py_BuildValue("i", xres)); + DICT_SETITEM_DROP(result, dictkey_yres, + Py_BuildValue("i", yres)); + DICT_SETITEM_DROP(result, dictkey_colorspace, + Py_BuildValue("i", image->n)); + DICT_SETITEM_DROP(result, dictkey_bpc, + Py_BuildValue("i", image->bpc)); + DICT_SETITEM_DROP(result, dictkey_ext, + Py_BuildValue("s", JM_image_extension(type))); + DICT_SETITEM_DROP(result, dictkey_cs_name, + Py_BuildValue("s", cs_name)); + + if (keep_image) { + DICT_SETITEM_DROP(result, dictkey_image, + PyLong_FromVoidPtr((void *) fz_keep_image(ctx, image))); + } + } + fz_always(ctx) { + if (!keep_image) { + fz_drop_image(ctx, image); + } else { + fz_drop_buffer(ctx, res); // drop the buffer copy + } + } + fz_catch(ctx) { + Py_CLEAR(result); + fz_rethrow(ctx); + } + PyErr_Clear(); + return result; +} + +//---------------------------------------------------------------------------- +// Version of fz_new_pixmap_from_display_list (util.c) to also support +// rendering of only the 'clip' part of the displaylist rectangle +//---------------------------------------------------------------------------- +fz_pixmap * +JM_pixmap_from_display_list(fz_context *ctx, + fz_display_list *list, + PyObject *ctm, + fz_colorspace *cs, + int alpha, + PyObject *clip, + fz_separations *seps + ) +{ + fz_rect rect = fz_bound_display_list(ctx, list); + fz_matrix matrix = JM_matrix_from_py(ctm); + fz_pixmap *pix = NULL; + fz_var(pix); + fz_device *dev = NULL; + fz_var(dev); + fz_rect rclip = JM_rect_from_py(clip); + rect = fz_intersect_rect(rect, rclip); // no-op if clip is not given + + rect = fz_transform_rect(rect, matrix); + fz_irect irect = fz_round_rect(rect); + + pix = fz_new_pixmap_with_bbox(ctx, cs, irect, seps, alpha); + if (alpha) + fz_clear_pixmap(ctx, pix); + else + fz_clear_pixmap_with_value(ctx, pix, 0xFF); + + fz_try(ctx) { + if (!fz_is_infinite_rect(rclip)) { + dev = fz_new_draw_device_with_bbox(ctx, matrix, pix, &irect); + fz_run_display_list(ctx, list, dev, fz_identity, rclip, NULL); + } else { + dev = fz_new_draw_device(ctx, matrix, pix); + fz_run_display_list(ctx, list, dev, fz_identity, fz_infinite_rect, NULL); + } + + fz_close_device(ctx, dev); + } + fz_always(ctx) { + fz_drop_device(ctx, dev); + } + fz_catch(ctx) { + fz_drop_pixmap(ctx, pix); + fz_rethrow(ctx); + } + return pix; +} + +//---------------------------------------------------------------------------- +// Pixmap creation directly using a short-lived displaylist, so we can support +// separations. +//---------------------------------------------------------------------------- +fz_pixmap * +JM_pixmap_from_page(fz_context *ctx, + fz_document *doc, + fz_page *page, + PyObject *ctm, + fz_colorspace *cs, + int alpha, + int annots, + PyObject *clip + ) +{ + enum { SPOTS_NONE, SPOTS_OVERPRINT_SIM, SPOTS_FULL }; + int spots; + if (FZ_ENABLE_SPOT_RENDERING) + spots = SPOTS_OVERPRINT_SIM; + else + spots = SPOTS_NONE; + + fz_separations *seps = NULL; + fz_pixmap *pix = NULL; + fz_colorspace *oi = NULL; + fz_var(oi); + fz_colorspace *colorspace = cs; + fz_rect rect; + fz_irect bbox; + fz_device *dev = NULL; + fz_var(dev); + fz_matrix matrix = JM_matrix_from_py(ctm); + rect = fz_bound_page(ctx, page); + fz_rect rclip = JM_rect_from_py(clip); + rect = fz_intersect_rect(rect, rclip); // no-op if clip is not given + rect = fz_transform_rect(rect, matrix); + bbox = fz_round_rect(rect); + + fz_try(ctx) { + // Pixmap of the document's /OutputIntents ("output intents") + oi = fz_document_output_intent(ctx, doc); + // if present and compatible, use it instead of the parameter + if (oi) { + if (fz_colorspace_n(ctx, oi) == fz_colorspace_n(ctx, cs)) { + colorspace = fz_keep_colorspace(ctx, oi); + } + } + + // check if spots rendering is available and if so use separations + if (spots != SPOTS_NONE) { + seps = fz_page_separations(ctx, page); + if (seps) { + int i, n = fz_count_separations(ctx, seps); + if (spots == SPOTS_FULL) + for (i = 0; i < n; i++) + fz_set_separation_behavior(ctx, seps, i, FZ_SEPARATION_SPOT); + else + for (i = 0; i < n; i++) + fz_set_separation_behavior(ctx, seps, i, FZ_SEPARATION_COMPOSITE); + } else if (fz_page_uses_overprint(ctx, page)) { + /* This page uses overprint, so we need an empty + * sep object to force the overprint simulation on. */ + seps = fz_new_separations(ctx, 0); + } else if (oi && fz_colorspace_n(ctx, oi) != fz_colorspace_n(ctx, colorspace)) { + /* We have an output intent, and it's incompatible + * with the colorspace our device needs. Force the + * overprint simulation on, because this ensures that + * we 'simulate' the output intent too. */ + seps = fz_new_separations(ctx, 0); + } + } + + pix = fz_new_pixmap_with_bbox(ctx, colorspace, bbox, seps, alpha); + + if (alpha) { + fz_clear_pixmap(ctx, pix); + } else { + fz_clear_pixmap_with_value(ctx, pix, 0xFF); + } + + dev = fz_new_draw_device(ctx, matrix, pix); + if (annots) { + fz_run_page(ctx, page, dev, fz_identity, NULL); + } else { + fz_run_page_contents(ctx, page, dev, fz_identity, NULL); + } + fz_close_device(ctx, dev); + } + fz_always(ctx) { + fz_drop_device(ctx, dev); + fz_drop_separations(ctx, seps); + fz_drop_colorspace(ctx, oi); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return pix; +} + +PyObject *JM_color_count(fz_context *ctx, fz_pixmap *pm, PyObject *clip) +{ + PyObject *rc = PyDict_New(), *pixel=NULL, *c=NULL; + long cnt=0; + fz_irect irect = fz_pixmap_bbox(ctx, pm); + irect = fz_intersect_irect(irect, fz_round_rect(JM_rect_from_py(clip))); + size_t stride = pm->stride; + size_t width = irect.x1 - irect.x0, height = irect.y1 - irect.y0; + size_t i, j, n = (size_t) pm->n, substride = width * n; + unsigned char *s = pm->samples + stride * (irect.y0 - pm->y) + (irect.x0 - pm->x) * n; + unsigned char oldpix[10], newpix[10]; + memcpy(oldpix, s, n); + cnt = 0; + fz_try(ctx) { + if (fz_is_empty_irect(irect)) goto finished; + for (i = 0; i < height; i++) { + for (j = 0; j < substride; j += n) { + memcpy(newpix, s + j, n); + if (memcmp(oldpix, newpix,n) != 0) { + pixel = PyBytes_FromStringAndSize(oldpix, n); + c = PyDict_GetItem(rc, pixel); + if (c) cnt += PyLong_AsLong(c); + DICT_SETITEM_DROP(rc, pixel, PyLong_FromLong(cnt)); + Py_DECREF(pixel); + cnt = 1; + memcpy(oldpix, newpix, n); + } else { + cnt += 1; + } + } + s += stride; + } + pixel = PyBytes_FromStringAndSize(oldpix, n); + c = PyDict_GetItem(rc, pixel); + if (c) cnt += PyLong_AsLong(c); + DICT_SETITEM_DROP(rc, pixel, PyLong_FromLong(cnt)); + Py_DECREF(pixel); + finished:; + } + fz_catch(ctx) { + Py_CLEAR(rc); + fz_rethrow(ctx); + } + PyErr_Clear(); + return rc; +} +%} diff --git a/fitz/helper-portfolio.i b/fitz/helper-portfolio.i new file mode 100644 index 0000000..c4d58ed --- /dev/null +++ b/fitz/helper-portfolio.i @@ -0,0 +1,79 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +//----------------------------------------------------------------------------- +// perform some cleaning if we have /EmbeddedFiles: +// (1) remove any /Limits if /Names exists +// (2) remove any empty /Collection +// (3) set /PageMode/UseAttachments +//----------------------------------------------------------------------------- +void JM_embedded_clean(fz_context *ctx, pdf_document *pdf) +{ + pdf_obj *root = pdf_dict_get(ctx, pdf_trailer(ctx, pdf), PDF_NAME(Root)); + + // remove any empty /Collection entry + pdf_obj *coll = pdf_dict_get(ctx, root, PDF_NAME(Collection)); + if (coll && pdf_dict_len(ctx, coll) == 0) + pdf_dict_del(ctx, root, PDF_NAME(Collection)); + + pdf_obj *efiles = pdf_dict_getl(ctx, root, + PDF_NAME(Names), + PDF_NAME(EmbeddedFiles), + PDF_NAME(Names), + NULL); + if (efiles) { + pdf_dict_put_name(ctx, root, PDF_NAME(PageMode), "UseAttachments"); + } + return; +} + +//----------------------------------------------------------------------------- +// embed a new file in a PDF (not only /EmbeddedFiles entries) +//----------------------------------------------------------------------------- +pdf_obj *JM_embed_file(fz_context *ctx, + pdf_document *pdf, + fz_buffer *buf, + char *filename, + char *ufilename, + char *desc, + int compress) +{ + size_t len = 0; + pdf_obj *ef, *f, *params, *val = NULL; + fz_buffer *buff2 = NULL; + fz_var(buff2); + fz_try(ctx) { + val = pdf_new_dict(ctx, pdf, 6); + pdf_dict_put_dict(ctx, val, PDF_NAME(CI), 4); + ef = pdf_dict_put_dict(ctx, val, PDF_NAME(EF), 4); + pdf_dict_put_text_string(ctx, val, PDF_NAME(F), filename); + pdf_dict_put_text_string(ctx, val, PDF_NAME(UF), ufilename); + pdf_dict_put_text_string(ctx, val, PDF_NAME(Desc), desc); + pdf_dict_put(ctx, val, PDF_NAME(Type), PDF_NAME(Filespec)); + buff2 = fz_new_buffer_from_copied_data(ctx, " ", 1); + f = pdf_add_stream(ctx, pdf, buff2, NULL, 0); + pdf_dict_put_drop(ctx, ef, PDF_NAME(F), f); + JM_update_stream(ctx, pdf, f, buf, compress); + len = fz_buffer_storage(ctx, buf, NULL); + pdf_dict_put_int(ctx, f, PDF_NAME(DL), len); + pdf_dict_put_int(ctx, f, PDF_NAME(Length), len); + params = pdf_dict_put_dict(ctx, f, PDF_NAME(Params), 4); + pdf_dict_put_int(ctx, params, PDF_NAME(Size), len); + } + fz_always(ctx) { + fz_drop_buffer(ctx, buff2); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return val; +} +%} diff --git a/fitz/helper-python.i b/fitz/helper-python.i new file mode 100644 index 0000000..679e71b --- /dev/null +++ b/fitz/helper-python.i @@ -0,0 +1,2065 @@ +%pythoncode %{ +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ + +# ------------------------------------------------------------------------------ +# Various PDF Optional Content Flags +# ------------------------------------------------------------------------------ +PDF_OC_ON = 0 +PDF_OC_TOGGLE = 1 +PDF_OC_OFF = 2 + +# ------------------------------------------------------------------------------ +# link kinds and link flags +# ------------------------------------------------------------------------------ +LINK_NONE = 0 +LINK_GOTO = 1 +LINK_URI = 2 +LINK_LAUNCH = 3 +LINK_NAMED = 4 +LINK_GOTOR = 5 +LINK_FLAG_L_VALID = 1 +LINK_FLAG_T_VALID = 2 +LINK_FLAG_R_VALID = 4 +LINK_FLAG_B_VALID = 8 +LINK_FLAG_FIT_H = 16 +LINK_FLAG_FIT_V = 32 +LINK_FLAG_R_IS_ZOOM = 64 + +# ------------------------------------------------------------------------------ +# Text handling flags +# ------------------------------------------------------------------------------ +TEXT_ALIGN_LEFT = 0 +TEXT_ALIGN_CENTER = 1 +TEXT_ALIGN_RIGHT = 2 +TEXT_ALIGN_JUSTIFY = 3 + +TEXT_OUTPUT_TEXT = 0 +TEXT_OUTPUT_HTML = 1 +TEXT_OUTPUT_JSON = 2 +TEXT_OUTPUT_XML = 3 +TEXT_OUTPUT_XHTML = 4 + +TEXT_PRESERVE_LIGATURES = 1 +TEXT_PRESERVE_WHITESPACE = 2 +TEXT_PRESERVE_IMAGES = 4 +TEXT_INHIBIT_SPACES = 8 +TEXT_DEHYPHENATE = 16 +TEXT_PRESERVE_SPANS = 32 +TEXT_MEDIABOX_CLIP = 64 + +TEXTFLAGS_WORDS = ( + TEXT_PRESERVE_LIGATURES + | TEXT_PRESERVE_WHITESPACE + | TEXT_MEDIABOX_CLIP +) +TEXTFLAGS_BLOCKS = ( + TEXT_PRESERVE_LIGATURES + | TEXT_PRESERVE_WHITESPACE + | TEXT_MEDIABOX_CLIP +) +TEXTFLAGS_DICT = ( + TEXT_PRESERVE_LIGATURES + | TEXT_PRESERVE_WHITESPACE + | TEXT_MEDIABOX_CLIP + | TEXT_PRESERVE_IMAGES +) +TEXTFLAGS_RAWDICT = TEXTFLAGS_DICT +TEXTFLAGS_SEARCH = ( + TEXT_PRESERVE_LIGATURES + | TEXT_PRESERVE_WHITESPACE + | TEXT_MEDIABOX_CLIP + | TEXT_DEHYPHENATE +) +TEXTFLAGS_HTML = ( + TEXT_PRESERVE_LIGATURES + | TEXT_PRESERVE_WHITESPACE + | TEXT_MEDIABOX_CLIP + | TEXT_PRESERVE_IMAGES +) +TEXTFLAGS_XHTML = ( + TEXT_PRESERVE_LIGATURES + | TEXT_PRESERVE_WHITESPACE + | TEXT_MEDIABOX_CLIP + | TEXT_PRESERVE_IMAGES +) +TEXTFLAGS_XML = ( + TEXT_PRESERVE_LIGATURES + | TEXT_PRESERVE_WHITESPACE + | TEXT_MEDIABOX_CLIP +) +TEXTFLAGS_TEXT = ( + TEXT_PRESERVE_LIGATURES + | TEXT_PRESERVE_WHITESPACE + | TEXT_MEDIABOX_CLIP +) + +# ------------------------------------------------------------------------------ +# Simple text encoding options +# ------------------------------------------------------------------------------ +TEXT_ENCODING_LATIN = 0 +TEXT_ENCODING_GREEK = 1 +TEXT_ENCODING_CYRILLIC = 2 +# ------------------------------------------------------------------------------ +# Stamp annotation icon numbers +# ------------------------------------------------------------------------------ +STAMP_Approved = 0 +STAMP_AsIs = 1 +STAMP_Confidential = 2 +STAMP_Departmental = 3 +STAMP_Experimental = 4 +STAMP_Expired = 5 +STAMP_Final = 6 +STAMP_ForComment = 7 +STAMP_ForPublicRelease = 8 +STAMP_NotApproved = 9 +STAMP_NotForPublicRelease = 10 +STAMP_Sold = 11 +STAMP_TopSecret = 12 +STAMP_Draft = 13 + +# ------------------------------------------------------------------------------ +# Base 14 font names and dictionary +# ------------------------------------------------------------------------------ +Base14_fontnames = ( + "Courier", + "Courier-Oblique", + "Courier-Bold", + "Courier-BoldOblique", + "Helvetica", + "Helvetica-Oblique", + "Helvetica-Bold", + "Helvetica-BoldOblique", + "Times-Roman", + "Times-Italic", + "Times-Bold", + "Times-BoldItalic", + "Symbol", + "ZapfDingbats", +) + +Base14_fontdict = {} +for f in Base14_fontnames: + Base14_fontdict[f.lower()] = f + del f +Base14_fontdict["helv"] = "Helvetica" +Base14_fontdict["heit"] = "Helvetica-Oblique" +Base14_fontdict["hebo"] = "Helvetica-Bold" +Base14_fontdict["hebi"] = "Helvetica-BoldOblique" +Base14_fontdict["cour"] = "Courier" +Base14_fontdict["coit"] = "Courier-Oblique" +Base14_fontdict["cobo"] = "Courier-Bold" +Base14_fontdict["cobi"] = "Courier-BoldOblique" +Base14_fontdict["tiro"] = "Times-Roman" +Base14_fontdict["tibo"] = "Times-Bold" +Base14_fontdict["tiit"] = "Times-Italic" +Base14_fontdict["tibi"] = "Times-BoldItalic" +Base14_fontdict["symb"] = "Symbol" +Base14_fontdict["zadb"] = "ZapfDingbats" + +annot_skel = { + "goto1": "<>/Rect[%s]/BS<>/Subtype/Link>>", + "goto2": "<>/Rect[%s]/BS<>/Subtype/Link>>", + "gotor1": "<>>>/Rect[%s]/BS<>/Subtype/Link>>", + "gotor2": "<>/Rect[%s]/BS<>/Subtype/Link>>", + "launch": "<>>>/Rect[%s]/BS<>/Subtype/Link>>", + "uri": "<>/Rect[%s]/BS<>/Subtype/Link>>", + "named": "<>/Rect[%s]/BS<>/Subtype/Link>>", +} + +class FileDataError(RuntimeError): + """Raised for documents with file structure issues.""" + pass + +class FileNotFoundError(RuntimeError): + """Raised if file does not exist.""" + pass + +class EmptyFileError(FileDataError): + """Raised when creating documents from zero-length data.""" + pass + +# propagate exception class to C-level code +_set_FileDataError(FileDataError) + +def css_for_pymupdf_font( + fontcode: str, *, CSS: OptStr = None, archive: AnyType = None, name: OptStr = None +) -> str: + """Create @font-face items for the given fontcode of pymupdf-fonts. + + Adds @font-face support for fonts contained in package pymupdf-fonts. + + Creates a CSS font-family for all fonts starting with string 'fontcode'. + + Note: + The font naming convention in package pymupdf-fonts is "fontcode", + where the suffix "sf" is either empty or one of "it", "bo" or "bi". + These suffixes thus represent the regular, italic, bold or bold-italic + variants of a font. For example, font code "notos" refers to fonts + "notos" - "Noto Sans Regular" + "notosit" - "Noto Sans Italic" + "notosbo" - "Noto Sans Bold" + "notosbi" - "Noto Sans Bold Italic" + + This function creates four CSS @font-face definitions and collectively + assigns the font-family name "notos" to them (or the "name" value). + + All fitting font buffers of the pymupdf-fonts package are placed / added + to the archive provided as parameter. + To use the font in fitz.Story, execute 'set_font(fontcode)'. The correct + font weight (bold) or style (italic) will automatically be selected. + Expects and returns the CSS source, with the new CSS definitions appended. + + Args: + fontcode: (str) font code for naming the font variants to include. + E.g. "fig" adds notos, notosi, notosb, notosbi fonts. + A maximum of 4 font variants is accepted. + CSS: (str) CSS string to add @font-face definitions to. + archive: (Archive, mandatory) where to place the font buffers. + name: (str) use this as family-name instead of 'fontcode'. + Returns: + Modified CSS, with appended @font-face statements for each font variant + of fontcode. + Fontbuffers associated with "fontcode" will be added to 'archive'. + """ + # @font-face template string + CSSFONT = "\n@font-face {font-family: %s; src: url(%s);%s%s}\n" + + if not type(archive) is Archive: + raise ValueError("'archive' must be an Archive") + if CSS == None: + CSS = "" + + # select font codes starting with the pass-in string + font_keys = [k for k in fitz_fontdescriptors.keys() if k.startswith(fontcode)] + if font_keys == []: + raise ValueError(f"No font code '{fontcode}' found in pymupdf-fonts.") + if len(font_keys) > 4: + raise ValueError("fontcode too short") + if name == None: # use this name for font-family + name = fontcode + + for fkey in font_keys: + font = fitz_fontdescriptors[fkey] + bold = font["bold"] # determine font property + italic = font["italic"] # determine font property + fbuff = font["loader"]() # load the fontbuffer + archive.add(fbuff, fkey) # update the archive + bold_text = "font-weight: bold;" if bold else "" + italic_text = "font-style: italic;" if italic else "" + CSS += CSSFONT % (name, fkey, bold_text, italic_text) + return CSS + + +def get_text_length(text: str, fontname: str ="helv", fontsize: float =11, encoding: int =0) -> float: + """Calculate length of a string for a built-in font. + + Args: + fontname: name of the font. + fontsize: font size points. + encoding: encoding to use, 0=Latin (default), 1=Greek, 2=Cyrillic. + Returns: + (float) length of text. + """ + fontname = fontname.lower() + basename = Base14_fontdict.get(fontname, None) + + glyphs = None + if basename == "Symbol": + glyphs = symbol_glyphs + if basename == "ZapfDingbats": + glyphs = zapf_glyphs + if glyphs is not None: + w = sum([glyphs[ord(c)][1] if ord(c) < 256 else glyphs[183][1] for c in text]) + return w * fontsize + + if fontname in Base14_fontdict.keys(): + return util_measure_string( + text, Base14_fontdict[fontname], fontsize, encoding + ) + + if fontname in ( + "china-t", + "china-s", + "china-ts", + "china-ss", + "japan", + "japan-s", + "korea", + "korea-s", + ): + return len(text) * fontsize + + raise ValueError("Font '%s' is unsupported" % fontname) + + +# ------------------------------------------------------------------------------ +# Glyph list for the built-in font 'ZapfDingbats' +# ------------------------------------------------------------------------------ +zapf_glyphs = ( + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (32, 0.278), + (33, 0.974), + (34, 0.961), + (35, 0.974), + (36, 0.98), + (37, 0.719), + (38, 0.789), + (39, 0.79), + (40, 0.791), + (41, 0.69), + (42, 0.96), + (43, 0.939), + (44, 0.549), + (45, 0.855), + (46, 0.911), + (47, 0.933), + (48, 0.911), + (49, 0.945), + (50, 0.974), + (51, 0.755), + (52, 0.846), + (53, 0.762), + (54, 0.761), + (55, 0.571), + (56, 0.677), + (57, 0.763), + (58, 0.76), + (59, 0.759), + (60, 0.754), + (61, 0.494), + (62, 0.552), + (63, 0.537), + (64, 0.577), + (65, 0.692), + (66, 0.786), + (67, 0.788), + (68, 0.788), + (69, 0.79), + (70, 0.793), + (71, 0.794), + (72, 0.816), + (73, 0.823), + (74, 0.789), + (75, 0.841), + (76, 0.823), + (77, 0.833), + (78, 0.816), + (79, 0.831), + (80, 0.923), + (81, 0.744), + (82, 0.723), + (83, 0.749), + (84, 0.79), + (85, 0.792), + (86, 0.695), + (87, 0.776), + (88, 0.768), + (89, 0.792), + (90, 0.759), + (91, 0.707), + (92, 0.708), + (93, 0.682), + (94, 0.701), + (95, 0.826), + (96, 0.815), + (97, 0.789), + (98, 0.789), + (99, 0.707), + (100, 0.687), + (101, 0.696), + (102, 0.689), + (103, 0.786), + (104, 0.787), + (105, 0.713), + (106, 0.791), + (107, 0.785), + (108, 0.791), + (109, 0.873), + (110, 0.761), + (111, 0.762), + (112, 0.762), + (113, 0.759), + (114, 0.759), + (115, 0.892), + (116, 0.892), + (117, 0.788), + (118, 0.784), + (119, 0.438), + (120, 0.138), + (121, 0.277), + (122, 0.415), + (123, 0.392), + (124, 0.392), + (125, 0.668), + (126, 0.668), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (183, 0.788), + (161, 0.732), + (162, 0.544), + (163, 0.544), + (164, 0.91), + (165, 0.667), + (166, 0.76), + (167, 0.76), + (168, 0.776), + (169, 0.595), + (170, 0.694), + (171, 0.626), + (172, 0.788), + (173, 0.788), + (174, 0.788), + (175, 0.788), + (176, 0.788), + (177, 0.788), + (178, 0.788), + (179, 0.788), + (180, 0.788), + (181, 0.788), + (182, 0.788), + (183, 0.788), + (184, 0.788), + (185, 0.788), + (186, 0.788), + (187, 0.788), + (188, 0.788), + (189, 0.788), + (190, 0.788), + (191, 0.788), + (192, 0.788), + (193, 0.788), + (194, 0.788), + (195, 0.788), + (196, 0.788), + (197, 0.788), + (198, 0.788), + (199, 0.788), + (200, 0.788), + (201, 0.788), + (202, 0.788), + (203, 0.788), + (204, 0.788), + (205, 0.788), + (206, 0.788), + (207, 0.788), + (208, 0.788), + (209, 0.788), + (210, 0.788), + (211, 0.788), + (212, 0.894), + (213, 0.838), + (214, 1.016), + (215, 0.458), + (216, 0.748), + (217, 0.924), + (218, 0.748), + (219, 0.918), + (220, 0.927), + (221, 0.928), + (222, 0.928), + (223, 0.834), + (224, 0.873), + (225, 0.828), + (226, 0.924), + (227, 0.924), + (228, 0.917), + (229, 0.93), + (230, 0.931), + (231, 0.463), + (232, 0.883), + (233, 0.836), + (234, 0.836), + (235, 0.867), + (236, 0.867), + (237, 0.696), + (238, 0.696), + (239, 0.874), + (183, 0.788), + (241, 0.874), + (242, 0.76), + (243, 0.946), + (244, 0.771), + (245, 0.865), + (246, 0.771), + (247, 0.888), + (248, 0.967), + (249, 0.888), + (250, 0.831), + (251, 0.873), + (252, 0.927), + (253, 0.97), + (183, 0.788), + (183, 0.788), +) + +# ------------------------------------------------------------------------------ +# Glyph list for the built-in font 'Symbol' +# ------------------------------------------------------------------------------ +symbol_glyphs = ( + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (32, 0.25), + (33, 0.333), + (34, 0.713), + (35, 0.5), + (36, 0.549), + (37, 0.833), + (38, 0.778), + (39, 0.439), + (40, 0.333), + (41, 0.333), + (42, 0.5), + (43, 0.549), + (44, 0.25), + (45, 0.549), + (46, 0.25), + (47, 0.278), + (48, 0.5), + (49, 0.5), + (50, 0.5), + (51, 0.5), + (52, 0.5), + (53, 0.5), + (54, 0.5), + (55, 0.5), + (56, 0.5), + (57, 0.5), + (58, 0.278), + (59, 0.278), + (60, 0.549), + (61, 0.549), + (62, 0.549), + (63, 0.444), + (64, 0.549), + (65, 0.722), + (66, 0.667), + (67, 0.722), + (68, 0.612), + (69, 0.611), + (70, 0.763), + (71, 0.603), + (72, 0.722), + (73, 0.333), + (74, 0.631), + (75, 0.722), + (76, 0.686), + (77, 0.889), + (78, 0.722), + (79, 0.722), + (80, 0.768), + (81, 0.741), + (82, 0.556), + (83, 0.592), + (84, 0.611), + (85, 0.69), + (86, 0.439), + (87, 0.768), + (88, 0.645), + (89, 0.795), + (90, 0.611), + (91, 0.333), + (92, 0.863), + (93, 0.333), + (94, 0.658), + (95, 0.5), + (96, 0.5), + (97, 0.631), + (98, 0.549), + (99, 0.549), + (100, 0.494), + (101, 0.439), + (102, 0.521), + (103, 0.411), + (104, 0.603), + (105, 0.329), + (106, 0.603), + (107, 0.549), + (108, 0.549), + (109, 0.576), + (110, 0.521), + (111, 0.549), + (112, 0.549), + (113, 0.521), + (114, 0.549), + (115, 0.603), + (116, 0.439), + (117, 0.576), + (118, 0.713), + (119, 0.686), + (120, 0.493), + (121, 0.686), + (122, 0.494), + (123, 0.48), + (124, 0.2), + (125, 0.48), + (126, 0.549), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (183, 0.46), + (160, 0.25), + (161, 0.62), + (162, 0.247), + (163, 0.549), + (164, 0.167), + (165, 0.713), + (166, 0.5), + (167, 0.753), + (168, 0.753), + (169, 0.753), + (170, 0.753), + (171, 1.042), + (172, 0.713), + (173, 0.603), + (174, 0.987), + (175, 0.603), + (176, 0.4), + (177, 0.549), + (178, 0.411), + (179, 0.549), + (180, 0.549), + (181, 0.576), + (182, 0.494), + (183, 0.46), + (184, 0.549), + (185, 0.549), + (186, 0.549), + (187, 0.549), + (188, 1), + (189, 0.603), + (190, 1), + (191, 0.658), + (192, 0.823), + (193, 0.686), + (194, 0.795), + (195, 0.987), + (196, 0.768), + (197, 0.768), + (198, 0.823), + (199, 0.768), + (200, 0.768), + (201, 0.713), + (202, 0.713), + (203, 0.713), + (204, 0.713), + (205, 0.713), + (206, 0.713), + (207, 0.713), + (208, 0.768), + (209, 0.713), + (210, 0.79), + (211, 0.79), + (212, 0.89), + (213, 0.823), + (214, 0.549), + (215, 0.549), + (216, 0.713), + (217, 0.603), + (218, 0.603), + (219, 1.042), + (220, 0.987), + (221, 0.603), + (222, 0.987), + (223, 0.603), + (224, 0.494), + (225, 0.329), + (226, 0.79), + (227, 0.79), + (228, 0.786), + (229, 0.713), + (230, 0.384), + (231, 0.384), + (232, 0.384), + (233, 0.384), + (234, 0.384), + (235, 0.384), + (236, 0.494), + (237, 0.494), + (238, 0.494), + (239, 0.494), + (183, 0.46), + (241, 0.329), + (242, 0.274), + (243, 0.686), + (244, 0.686), + (245, 0.686), + (246, 0.384), + (247, 0.549), + (248, 0.384), + (249, 0.384), + (250, 0.384), + (251, 0.384), + (252, 0.494), + (253, 0.494), + (254, 0.494), + (183, 0.46), +) + + +class linkDest(object): + """link or outline destination details""" + + def __init__(self, obj, rlink): + isExt = obj.is_external + isInt = not isExt + self.dest = "" + self.fileSpec = "" + self.flags = 0 + self.isMap = False + self.isUri = False + self.kind = LINK_NONE + self.lt = Point(0, 0) + self.named = "" + self.newWindow = "" + self.page = obj.page + self.rb = Point(0, 0) + self.uri = obj.uri + if rlink and not self.uri.startswith("#"): + self.uri = "#page=%i&zoom=0,%g,%g" % (rlink[0] + 1, rlink[1], rlink[2]) + if obj.is_external: + self.page = -1 + self.kind = LINK_URI + if not self.uri: + self.page = -1 + self.kind = LINK_NONE + if isInt and self.uri: + self.uri = self.uri.replace("&zoom=nan", "&zoom=0") + if self.uri.startswith("#"): + self.named = "" + self.kind = LINK_GOTO + m = re.match('^#page=([0-9]+)&zoom=([0-9.]+),([0-9.]+),([0-9.]+)$', self.uri) + if m: + self.page = int(m.group(1)) - 1 + self.lt = Point(float((m.group(3))), float(m.group(4))) + self.flags = self.flags | LINK_FLAG_L_VALID | LINK_FLAG_T_VALID + else: + m = re.match('^#page=([0-9]+)$', self.uri) + if m: + self.page = int(m.group(1)) - 1 + else: + self.kind = LINK_NAMED + self.named = self.uri[1:] + else: + self.kind = LINK_NAMED + self.named = self.uri + if obj.is_external: + if self.uri.startswith(("http://", "https://", "mailto:", "ftp://")): + self.isUri = True + self.kind = LINK_URI + elif self.uri.startswith("file://"): + self.fileSpec = self.uri[7:] + self.isUri = False + self.uri = "" + self.kind = LINK_LAUNCH + ftab = self.fileSpec.split("#") + if len(ftab) == 2: + if ftab[1].startswith("page="): + self.kind = LINK_GOTOR + self.fileSpec = ftab[0] + self.page = int(ftab[1][5:]) - 1 + else: + self.isUri = True + self.kind = LINK_LAUNCH + + +# ------------------------------------------------------------------------------- +# "Now" timestamp in PDF Format +# ------------------------------------------------------------------------------- +def get_pdf_now() -> str: + import time + + tz = "%s'%s'" % ( + str(abs(time.altzone // 3600)).rjust(2, "0"), + str((abs(time.altzone // 60) % 60)).rjust(2, "0"), + ) + tstamp = time.strftime("D:%Y%m%d%H%M%S", time.localtime()) + if time.altzone > 0: + tstamp += "-" + tz + elif time.altzone < 0: + tstamp += "+" + tz + else: + pass + return tstamp + + +def get_pdf_str(s: str) -> str: + """ Return a PDF string depending on its coding. + + Notes: + Returns a string bracketed with either "()" or "<>" for hex values. + If only ascii then "(original)" is returned, else if only 8 bit chars + then "(original)" with interspersed octal strings \nnn is returned, + else a string "" is returned, where [hexstring] is the + UTF-16BE encoding of the original. + """ + if not bool(s): + return "()" + + def make_utf16be(s): + r = bytearray([254, 255]) + bytearray(s, "UTF-16BE") + return "<" + r.hex() + ">" # brackets indicate hex + + # The following either returns the original string with mixed-in + # octal numbers \nnn for chars outside the ASCII range, or returns + # the UTF-16BE BOM version of the string. + r = "" + for c in s: + oc = ord(c) + if oc > 255: # shortcut if beyond 8-bit code range + return make_utf16be(s) + + if oc > 31 and oc < 127: # in ASCII range + if c in ("(", ")", "\\"): # these need to be escaped + r += "\\" + r += c + continue + + if oc > 127: # beyond ASCII + r += "\\%03o" % oc + continue + + # now the white spaces + if oc == 8: # backspace + r += "\\b" + elif oc == 9: # tab + r += "\\t" + elif oc == 10: # line feed + r += "\\n" + elif oc == 12: # form feed + r += "\\f" + elif oc == 13: # carriage return + r += "\\r" + else: + r += "\\267" # unsupported: replace by 0xB7 + + return "(" + r + ")" + + +def getTJstr(text: str, glyphs: typing.Union[list, tuple, None], simple: bool, ordering: int) -> str: + """ Return a PDF string enclosed in [] brackets, suitable for the PDF TJ + operator. + + Notes: + The input string is converted to either 2 or 4 hex digits per character. + Args: + simple: no glyphs: 2-chars, use char codes as the glyph + glyphs: 2-chars, use glyphs instead of char codes (Symbol, + ZapfDingbats) + not simple: ordering < 0: 4-chars, use glyphs not char codes + ordering >=0: a CJK font! 4 chars, use char codes as glyphs + """ + if text.startswith("[<") and text.endswith(">]"): # already done + return text + + if not bool(text): + return "[<>]" + + if simple: # each char or its glyph is coded as a 2-byte hex + if glyphs is None: # not Symbol, not ZapfDingbats: use char code + otxt = "".join(["%02x" % ord(c) if ord(c) < 256 else "b7" for c in text]) + else: # Symbol or ZapfDingbats: use glyphs + otxt = "".join( + ["%02x" % glyphs[ord(c)][0] if ord(c) < 256 else "b7" for c in text] + ) + return "[<" + otxt + ">]" + + # non-simple fonts: each char or its glyph is coded as 4-byte hex + if ordering < 0: # not a CJK font: use the glyphs + otxt = "".join(["%04x" % glyphs[ord(c)][0] for c in text]) + else: # CJK: use the char codes + otxt = "".join(["%04x" % ord(c) for c in text]) + + return "[<" + otxt + ">]" + + +def paper_sizes(): + """Known paper formats @ 72 dpi as a dictionary. Key is the format string + like "a4" for ISO-A4. Value is the tuple (width, height). + + Information taken from the following web sites: + www.din-formate.de + www.din-formate.info/amerikanische-formate.html + www.directtools.de/wissen/normen/iso.htm + """ + return { + "a0": (2384, 3370), + "a1": (1684, 2384), + "a10": (74, 105), + "a2": (1191, 1684), + "a3": (842, 1191), + "a4": (595, 842), + "a5": (420, 595), + "a6": (298, 420), + "a7": (210, 298), + "a8": (147, 210), + "a9": (105, 147), + "b0": (2835, 4008), + "b1": (2004, 2835), + "b10": (88, 125), + "b2": (1417, 2004), + "b3": (1001, 1417), + "b4": (709, 1001), + "b5": (499, 709), + "b6": (354, 499), + "b7": (249, 354), + "b8": (176, 249), + "b9": (125, 176), + "c0": (2599, 3677), + "c1": (1837, 2599), + "c10": (79, 113), + "c2": (1298, 1837), + "c3": (918, 1298), + "c4": (649, 918), + "c5": (459, 649), + "c6": (323, 459), + "c7": (230, 323), + "c8": (162, 230), + "c9": (113, 162), + "card-4x6": (288, 432), + "card-5x7": (360, 504), + "commercial": (297, 684), + "executive": (522, 756), + "invoice": (396, 612), + "ledger": (792, 1224), + "legal": (612, 1008), + "legal-13": (612, 936), + "letter": (612, 792), + "monarch": (279, 540), + "tabloid-extra": (864, 1296), + } + + +def paper_size(s: str) -> tuple: + """Return a tuple (width, height) for a given paper format string. + + Notes: + 'A4-L' will return (842, 595), the values for A4 landscape. + Suffix '-P' and no suffix return the portrait tuple. + """ + size = s.lower() + f = "p" + if size.endswith("-l"): + f = "l" + size = size[:-2] + if size.endswith("-p"): + size = size[:-2] + rc = paper_sizes().get(size, (-1, -1)) + if f == "p": + return rc + return (rc[1], rc[0]) + + +def paper_rect(s: str) -> Rect: + """Return a Rect for the paper size indicated in string 's'. Must conform to the argument of method 'PaperSize', which will be invoked. + """ + width, height = paper_size(s) + return Rect(0.0, 0.0, width, height) + + +def CheckParent(o: typing.Any): + if getattr(o, "parent", None) == None: + raise ValueError("orphaned object: parent is None") + + +def EnsureOwnership(o: typing.Any): + if not getattr(o, "thisown", False): + raise RuntimeError("object destroyed") + + +def CheckColor(c: OptSeq): + if c: + if ( + type(c) not in (list, tuple) + or len(c) not in (1, 3, 4) + or min(c) < 0 + or max(c) > 1 + ): + raise ValueError("need 1, 3 or 4 color components in range 0 to 1") + + +def ColorCode(c: typing.Union[list, tuple, float, None], f: str) -> str: + if not c: + return "" + if hasattr(c, "__float__"): + c = (c,) + CheckColor(c) + if len(c) == 1: + s = "%g " % c[0] + return s + "G " if f == "c" else s + "g " + + if len(c) == 3: + s = "%g %g %g " % tuple(c) + return s + "RG " if f == "c" else s + "rg " + + s = "%g %g %g %g " % tuple(c) + return s + "K " if f == "c" else s + "k " + + +def JM_TUPLE(o: typing.Sequence) -> tuple: + return tuple(map(lambda x: round(x, 5) if abs(x) >= 1e-4 else 0, o)) + + +def JM_TUPLE3(o: typing.Sequence) -> tuple: + return tuple(map(lambda x: round(x, 3) if abs(x) >= 1e-3 else 0, o)) + + +def CheckRect(r: typing.Any) -> bool: + """Check whether an object is non-degenerate rect-like. + + It must be a sequence of 4 numbers. + """ + try: + r = Rect(r) + except: + return False + return not (r.is_empty or r.is_infinite) + + +def CheckQuad(q: typing.Any) -> bool: + """Check whether an object is convex, not empty quad-like. + + It must be a sequence of 4 number pairs. + """ + try: + q0 = Quad(q) + except: + return False + return q0.is_convex + + +def CheckMarkerArg(quads: typing.Any) -> tuple: + if CheckRect(quads): + r = Rect(quads) + return (r.quad,) + if CheckQuad(quads): + return (quads,) + for q in quads: + if not (CheckRect(q) or CheckQuad(q)): + raise ValueError("bad quads entry") + return quads + + +def CheckMorph(o: typing.Any) -> bool: + if not bool(o): + return False + if not (type(o) in (list, tuple) and len(o) == 2): + raise ValueError("morph must be a sequence of length 2") + if not (len(o[0]) == 2 and len(o[1]) == 6): + raise ValueError("invalid morph parm 0") + if not o[1][4] == o[1][5] == 0: + raise ValueError("invalid morph parm 1") + return True + + +def CheckFont(page: "struct Page *", fontname: str) -> tuple: + """Return an entry in the page's font list if reference name matches. + """ + for f in page.get_fonts(): + if f[4] == fontname: + return f + + +def CheckFontInfo(doc: "struct Document *", xref: int) -> list: + """Return a font info if present in the document. + """ + for f in doc.FontInfos: + if xref == f[0]: + return f + + +def UpdateFontInfo(doc: "struct Document *", info: typing.Sequence): + xref = info[0] + found = False + for i, fi in enumerate(doc.FontInfos): + if fi[0] == xref: + found = True + break + if found: + doc.FontInfos[i] = info + else: + doc.FontInfos.append(info) + + +def DUMMY(*args, **kw): + return + + +def planish_line(p1: point_like, p2: point_like) -> Matrix: + """Compute matrix which maps line from p1 to p2 to the x-axis, such that it + maintains its length and p1 * matrix = Point(0, 0). + + Args: + p1, p2: point_like + Returns: + Matrix which maps p1 to Point(0, 0) and p2 to a point on the x axis at + the same distance to Point(0,0). Will always combine a rotation and a + transformation. + """ + p1 = Point(p1) + p2 = Point(p2) + return Matrix(util_hor_matrix(p1, p2)) + + +def image_profile(img: typing.ByteString) -> dict: + """ Return basic properties of an image. + + Args: + img: bytes, bytearray, io.BytesIO object or an opened image file. + Returns: + A dictionary with keys width, height, colorspace.n, bpc, type, ext and size, + where 'type' is the MuPDF image type (0 to 14) and 'ext' the suitable + file extension. + """ + if type(img) is io.BytesIO: + stream = img.getvalue() + elif hasattr(img, "read"): + stream = img.read() + elif type(img) in (bytes, bytearray): + stream = img + else: + raise ValueError("bad argument 'img'") + + return TOOLS.image_profile(stream) + + +def ConversionHeader(i: str, filename: OptStr ="unknown"): + t = i.lower() + html = """ + + + + +\n""" + + xml = ( + """ +\n""" + % filename + ) + + xhtml = """ + + + + + +\n""" + + text = "" + json = '{"document": "%s", "pages": [\n' % filename + if t == "html": + r = html + elif t == "json": + r = json + elif t == "xml": + r = xml + elif t == "xhtml": + r = xhtml + else: + r = text + + return r + + +def ConversionTrailer(i: str): + t = i.lower() + text = "" + json = "]\n}" + html = "\n\n" + xml = "\n" + xhtml = html + if t == "html": + r = html + elif t == "json": + r = json + elif t == "xml": + r = xml + elif t == "xhtml": + r = xhtml + else: + r = text + + return r + +class ElementPosition(object): + """Convert a dictionary with element position information to an object.""" + def __init__(self): + pass + def __str__(self): + ret = "" + for n, v in self.__dict__.items(): + ret += f" {n}={v!r}" + return ret + +def make_story_elpos(): + return ElementPosition() + + +def get_highlight_selection(page, start: point_like =None, stop: point_like =None, clip: rect_like =None) -> list: + """Return rectangles of text lines between two points. + + Notes: + The default of 'start' is top-left of 'clip'. The default of 'stop' + is bottom-reight of 'clip'. + + Args: + start: start point_like + stop: end point_like, must be 'below' start + clip: consider this rect_like only, default is page rectangle + Returns: + List of line bbox intersections with the area established by the + parameters. + """ + # validate and normalize arguments + if clip is None: + clip = page.rect + clip = Rect(clip) + if start is None: + start = clip.tl + if stop is None: + stop = clip.br + clip.y0 = start.y + clip.y1 = stop.y + if clip.is_empty or clip.is_infinite: + return [] + + # extract text of page, clip only, no images, expand ligatures + blocks = page.get_text( + "dict", flags=0, clip=clip, + )["blocks"] + + lines = [] # will return this list of rectangles + for b in blocks: + bbox = Rect(b["bbox"]) + if bbox.is_infinite or bbox.is_empty: + continue + for line in b["lines"]: + bbox = Rect(line["bbox"]) + if bbox.is_infinite or bbox.is_empty: + continue + lines.append(bbox) + + if lines == []: # did not select anything + return lines + + lines.sort(key=lambda bbox: bbox.y1) # sort by vertical positions + + # cut off prefix from first line if start point is close to its top + bboxf = lines.pop(0) + if bboxf.y0 - start.y <= 0.1 * bboxf.height: # close enough? + r = Rect(start.x, bboxf.y0, bboxf.br) # intersection rectangle + if not (r.is_empty or r.is_infinite): + lines.insert(0, r) # insert again if not empty + else: + lines.insert(0, bboxf) # insert again + + if lines == []: # the list might have been emptied + return lines + + # cut off suffix from last line if stop point is close to its bottom + bboxl = lines.pop() + if stop.y - bboxl.y1 <= 0.1 * bboxl.height: # close enough? + r = Rect(bboxl.tl, stop.x, bboxl.y1) # intersection rectangle + if not (r.is_empty or r.is_infinite): + lines.append(r) # append if not empty + else: + lines.append(bboxl) # append again + + return lines + + +def annot_preprocess(page: "Page") -> int: + """Prepare for annotation insertion on the page. + + Returns: + Old page rotation value. Temporarily sets rotation to 0 when required. + """ + CheckParent(page) + if not page.parent.is_pdf: + raise ValueError("is no PDF") + old_rotation = page.rotation + if old_rotation != 0: + page.set_rotation(0) + return old_rotation + + +def annot_postprocess(page: "Page", annot: "Annot") -> None: + """Clean up after annotation inertion. + + Set ownership flag and store annotation in page annotation dictionary. + """ + annot.parent = weakref.proxy(page) + page._annot_refs[id(annot)] = annot + annot.thisown = True + + +def sRGB_to_rgb(srgb: int) -> tuple: + """Convert sRGB color code to an RGB color triple. + + There is **no error checking** for performance reasons! + + Args: + srgb: (int) RRGGBB (red, green, blue), each color in range(255). + Returns: + Tuple (red, green, blue) each item in intervall 0 <= item <= 255. + """ + r = srgb >> 16 + g = (srgb - (r << 16)) >> 8 + b = srgb - (r << 16) - (g << 8) + return (r, g, b) + + +def sRGB_to_pdf(srgb: int) -> tuple: + """Convert sRGB color code to a PDF color triple. + + There is **no error checking** for performance reasons! + + Args: + srgb: (int) RRGGBB (red, green, blue), each color in range(255). + Returns: + Tuple (red, green, blue) each item in intervall 0 <= item <= 1. + """ + t = sRGB_to_rgb(srgb) + return t[0] / 255.0, t[1] / 255.0, t[2] / 255.0 + + +def make_table(rect: rect_like =(0, 0, 1, 1), cols: int =1, rows: int =1) -> list: + """Return a list of (rows x cols) equal sized rectangles. + + Notes: + A utility to fill a given area with table cells of equal size. + Args: + rect: rect_like to use as the table area + rows: number of rows + cols: number of columns + Returns: + A list with items, where each item is a list of + PyMuPDF Rect objects of equal sizes. + """ + rect = Rect(rect) # ensure this is a Rect + if rect.is_empty or rect.is_infinite: + raise ValueError("rect must be finite and not empty") + tl = rect.tl + + height = rect.height / rows # height of one table cell + width = rect.width / cols # width of one table cell + delta_h = (width, 0, width, 0) # diff to next right rect + delta_v = (0, height, 0, height) # diff to next lower rect + + r = Rect(tl, tl.x + width, tl.y + height) # first rectangle + + # make the first row + row = [r] + for i in range(1, cols): + r += delta_h # build next rect to the right + row.append(r) + + # make result, starts with first row + rects = [row] + for i in range(1, rows): + row = rects[i - 1] # take previously appended row + nrow = [] # the new row to append + for r in row: # for each previous cell add its downward copy + nrow.append(r + delta_v) + rects.append(nrow) # append new row to result + + return rects + + +def repair_mono_font(page: "Page", font: "Font") -> None: + """Repair character spacing for mono fonts. + + Notes: + Some mono-spaced fonts are displayed with a too large character + width, e.g. "a b c" instead of "abc". This utility adds an entry + "/DW w" to the descendent font of font. The int w is + taken to be the first width > 0 of the font's unicodes. + This should enforce viewers to use 'w' as the character width. + + Args: + page: fitz.Page object. + font: fitz.Font object. + """ + def set_font_width(doc, xref, width): + df = doc.xref_get_key(xref, "DescendantFonts") + if df[0] != "array": + return False + df_xref = int(df[1][1:-1].replace("0 R","")) + W = doc.xref_get_key(df_xref, "W") + if W[1] != "null": + doc.xref_set_key(df_xref, "W", "null") + doc.xref_set_key(df_xref, "DW", str(width)) + return True + + if not font.flags["mono"]: # font not flagged as monospaced + return None + doc = page.parent # the document + fontlist = page.get_fonts() # list of fonts on page + xrefs = [ # list of objects referring to font + f[0] + for f in fontlist + if (f[3] == font.name and f[4].startswith("F") and f[5].startswith("Identity")) + ] + if xrefs == []: # our font does not occur + return + xrefs = set(xrefs) # drop any double counts + maxadv = max([font.glyph_advance(cp) for cp in font.valid_codepoints()[:3]]) + width = int(round((maxadv * 1000))) + for xref in xrefs: + if not set_font_width(doc, xref, width): + print("Cannot set width for '%s' in xref %i" % (font.name, xref)) + + +# Adobe Glyph List functions +import base64, gzip + +_adobe_glyphs = {} +_adobe_unicodes = {} +def unicode_to_glyph_name(ch: int) -> str: + if _adobe_glyphs == {}: + for line in _get_glyph_text(): + if line.startswith("#"): + continue + name, unc = line.split(";") + uncl = unc.split() + for unc in uncl: + c = int(unc[:4], base=16) + _adobe_glyphs[c] = name + return _adobe_glyphs.get(ch, ".notdef") + + +def glyph_name_to_unicode(name: str) -> int: + if _adobe_unicodes == {}: + for line in _get_glyph_text(): + if line.startswith("#"): + continue + gname, unc = line.split(";") + c = int(unc[:4], base=16) + _adobe_unicodes[gname] = c + return _adobe_unicodes.get(name, 65533) + +def adobe_glyph_names() -> tuple: + if _adobe_unicodes == {}: + for line in _get_glyph_text(): + if line.startswith("#"): + continue + gname, unc = line.split(";") + c = int("0x" + unc[:4], base=16) + _adobe_unicodes[gname] = c + return tuple(_adobe_unicodes.keys()) + +def adobe_glyph_unicodes() -> tuple: + if _adobe_unicodes == {}: + for line in _get_glyph_text(): + if line.startswith("#"): + continue + gname, unc = line.split(";") + c = int("0x" + unc[:4], base=16) + _adobe_unicodes[gname] = c + return tuple(_adobe_unicodes.values()) + +def _get_glyph_text() -> bytes: + return gzip.decompress(base64.b64decode( + b'H4sIABmRaF8C/7W9SZfjRpI1useviPP15utzqroJgBjYWhEkKGWVlKnOoapVO0YQEYSCJE' + b'IcMhT569+9Ppibg8xevHdeSpmEXfPBfDZ3N3f/t7u//r//k/zb3WJ4eTv2T9vzXTaZZH/N' + b'Junsbr4Z7ru7/7s9n1/+6z//8/X19T/WRP7jYdj/57//R/Jv8Pax2/Sn87G/v5z74XC3Pm' + b'zuLqfurj/cnYbL8aEzyH1/WB/f7h6H4/70l7vX/ry9G47wzK/hcr7bD5v+sX9YM4i/3K2P' + b'3d1Ld9z353O3uXs5Dl/7DT7O2/UZ/3Tw9zjsdsNrf3i6exgOm57eTsbbvjv/1w2xTnfDo5' + b'fnYdjA3eV0vjt25zXkRJB36/vhKwN+kEw4DOf+ofsLuP3pboewGISO7bAxPkUU+EaUD7t1' + b'v++O/3FTCESmcsILgQRuLhDs/w857lz6NsPDZd8dzmtfSP85HO8GcI53+/W5O/br3QkeJa' + b'9NERmPKgE2Ue+73vgj97Ded5TH1pPDEFCT4/35RFFtAMORMezXb3dwiioCsYe77rABjjCO' + b'jHs/nLs7mx3wuYFYX+HsEQyTfHg/DY/nVxa0rzmnl+6BVQfeegTyemSlOdjqczqJ0J9/ev' + b'fp7tOH1ed/zj+2d/j+9eOHf7xbtsu75jcw27vFh19/+/jux58+3/304edl+/HT3fz9kq3i' + b'w/vPH981Xz5/APR/5p/g9/+Qhb+/3bX/8+vH9tOnuw8f79798uvP7xAcwv84f//5XfvpL/' + b'D97v3i5y/Ld+9//Msdgrh7/+Hz3c/vfnn3GQ4/f/iLifja492HFbz+0n5c/ARg3rz7+d3n' + b'30ycq3ef3zO+FSKc3/06//j53eLLz/OPd79++fjrh0/tHRIHr8t3nxY/z9/90i7/AxIg1r' + b'v2H+37z3effpr//PPN1CIF47Q2LUSdNz+3NjakdvnuY7v4/BcEGb4WyEPI+DMT++nXdvEO' + b'n8iWFomaf/ztL8wZhPqp/e8vcAbm3XL+y/xHpPH/xlnDejXKHJTQ4svH9hdK/mF19+lL8+' + b'nzu89fPrd3P374sDSZ/qn9+I93i/bTD/D+8wcWxOruy6f2L4jl89xEjkCQaZ9+4Hfz5dM7' + b'k33v3n9uP3788uvndx/e/zu8/vThn8ggSDqH56XJ6Q/vTZKRVx8+/sZgmRemIP5y98+fWu' + b'Ao8vc+z+bMjE/Iu8Vn7RBxIis/q7TevW9//Pndj+37RWuz/AND+ue7T+2/o+zefaKTdzbq' + b'f84R7xeTdJYYJLOf7z4xq11N/osp2bt3q7v58h/vKLxzjtrw6Z2rOSbzFj+5rEd7+P84UL' + b'xH8/6vO/lj2/6Pu7eX7d3P6C3Y2tb3u+7ua3dkA/yvu+w/JqyV6GeUt0/dy7nb36MjySZ/' + b'MUMO3Hz5+LNycsdx54SB5wmN/XJvRh0z/vz1/PaCf4Zhd/rP9dPur/j7eDDtfIV+dX3+r7' + b'vz63B36vb9w7AbDn/ddLseown7kr7bbU4YIhD6/03//e7JiM0O669/vbyg1/hPdKLd8WGN' + b'PmnXoSs52h5200OGk/WW/fvdl0NvhpHTw3q3Pt59Xe8uCOARA8ydCcX433Z/rjfonfbrnf' + b'hP5j9MJtM0mbf4XZT4XT9czt0Pk3S1ALFfPxyHA6g2A3WCz90Pq6qFO+dsskjdtzAB3B+7' + b'rwwDeWi/reu0nbcOeMBostv1Dz9MpsuJwzbD+b5DcuGuKR32dFx/pcfGO9oOw7MZlAj64M' + b'/9bmOAaTJ/WFuJF0t898eHXfdDNmV4JC77x133J8XONCDiTTWq5JkvNMMLNY9C1ZLNa82R' + b'rIki9ULP50AZ/6pczOyn92DSE3IqRSZs7nc2+gmqKMi+O3an/sQkTQOpszcLsBTnsg2gSE' + b'f/KskTQ4YaANrFPFn4b/ELIEo/Iu2jQkbg/QEtEJXe1Y6MtWP3sl3/MMlnqf08D4cBaclr' + b'5KzEzHTuyXhZPyCXVhkcD0/DoXsmEwEfoWVQqsJ+Sg2eW9qniOGQFqHh3n+XCNMWCMLJ3b' + b'c4BPB2vz5CYenXkKjI06Rhu8mSJlSxKmmQX+uHB6g1jC0ztEQ+TRqdISmC6A46TLiH/sfM' + b'wBczE0mo4WrXHzoJpUyaKCvglLnpJC1XiEWSBN55eIHcDChLFpQ4TxZrHWkL2mUXwl6Yto' + b'N6OLefEmyRLHy7mizwDT1yt1szryqhfCOa1AJJBtKVZFRtCd8WU3pATvFrbr5cHlo6Dome' + b'tzoF0xmAbn3/vF2fgKgcbhbkKCCrCKBYETp0uZt+2siJ5pSGc92+kOVgbLVIOREE/rw+jc' + b'JfNGSxGWBysYMmOzxrCU3qelSBOUV1VQCf456kXEGaqB4gykGJUKTJQupBnixZ9NNk+S+2' + b'ihS/0kkCjOoD6ccjhCO3niVLKfYW367Y0xY90TIU6MwSVkRfVdMM6HFYsxzpPGobc0NLrV' + b'4ky6htQIoOA9rLmWTeIupuh6aRZaij5vPp2LH15zO49PmEMH1niBrcCCWd60KgH00/Bmgp' + b'kM8t9NzL/mm930scS/j7XYuHlr2MGiXkiwoDQvnESoFVyfKEarx1uSGFA7ehkULobywiRP' + b'BNiqgAcbOCo9MFRwtGp1GVn6wSDuzTImllwJ65b2mcAPyAjZxvfcTpHN+2xC0bZboApKt6' + b'joBDPZhbIgyyEeD7B7Sx9kZ1qTWqKgeUkvZ66MUI1N4eejGytzeG3kgUP/QumFyVWyD1+E' + b'pSja9NICVYYqbrSkvzJV2Xo0WhQfIedV+EsGU0rd23hAogyuUKtNZ7kBjOxTEPBT9LS/Cv' + b'BlfE32OqDgVzo+JFfWt3uqkhATv4OEhYCFtGXrRhR/jCY7Is4kuCVWavQ0QdiVoDqoiute' + b'kS9K0eFjpDy3E8nc75EdVjKGbtgVmg+1KkWtQAVp/hpaPQM1SNl1O/YwryWeEJUS3gUkeb' + b'wTnzDLP+DdtgG0jtClLrXh86SHu6mQoIb1r5HM1KWjmksEN7xQ9VsjVpEQ1ezvA7gUqMD+' + b'97RcpruAv3Le0G8V2Oww/ZBDpq+40xQxPBh2/G6D1BqRSiKq7YJ5TJKjTdJlnpDjptk1U0' + b'phVwrbvkabJy/S5Ut1UPnyELqgwIovM1Cm6jCoGgMDERdp6sJJ/K5EeKViU/Nqc/Lutj90' + b'OeYwD8UVS6Kb7RNzMrc/sZhqsZmYenfh3EnCc/StfWJj9KniAe0WFSKFE/hpxYWEK0k5TA' + b'wIh806Z72+hRd37UjZ50NJBBxu16o3UD+N1iHrjZ7LpRfab42+5KJ5gZH5eX8+WomxFq+Y' + b'++BBALJnWqVgGIRywArlFjJgefUXkgf/142NpPKQ84le/KfdtYs1kD2gjLDJ0mP7Hg6uSn' + b'tEb8P2TFYmW+p/xGo+B3kfK7SX7CQF4ZPE1++lUKGh3sT+tbAx3G5J/WN5WyDIzj5tQ/ae' + b'cZYrMDKqraT6b8fWshK2gxGcINBb+0hBQ8uuifpPuHY4SlmwhqwU+qg6frKFcRttbIphPQ' + b'R9WCwJesxfcF85bjZb9bX84siFWEiBYBh98kv1AF3jHTZ8k7PUvMVsm7v0F+TCjefdF4m7' + b'wTJWDpvmXIAeBbSrZI3on2gcBCFrWWCAN8BEhYRFXlK5N3elStQapRdRVIP8hQ0huaNirZ' + b'u6sBmN5NW8wn5kvaoqNFjZgn77qrpQeIFrXXInn3eFw/o62hZ8IU7Z2M0Qv3LREDiNQOJK' + b'vXQZEej8mQoT9th+NZO0TxyYCL+ukInW4UZFS14AO1SrX3Jnk36ByH4DIyMjMHO/jMzJfq' + b'MEsDhNLI0VCJyIAEUiopfEt7xzj2zk2XU9T0d9GQxPrzbdufT9GgMPWgrwuaWSZ/Y02eJ3' + b'+L5nZp8rdQ+VaWkPaJucrfok6uTv42mog1yd+ijEP4kpx58ndG2SR/V0NNkfz976E/WiZ/' + b'X99DZ3/uoxF+AtjV1Nx8q8JEqDd7qhkZYwUmB/byYoqG7OuuvwX63cnibJH8XQa0Gt8yoO' + b'UlKJ9v0JT/Ho9fZKuWgX7i7/FYPwUQLU2skr9vdTKh0/19q9UBhOgHI0gSjz0QU8+WUGx/' + b'jwoFJTAgF5SXemIhmYEhH066cZUEfEE2yc8syEXyM3s9aIU//4yuEtXlZ6815DN87+83Jq' + b'fh3OdavsR3yDVyJNdSS8STlByRjPISnlz/szJfgWNp8VoGUoZiqH8/969RViOG35kMcOJs' + b'RBqibJwnP0fZCI9+gol2Y79l3IBnya9F8gvza5n8oip+mfxihVqVUD7tt0yJVwRchW+TX0' + b'ImZckvekjEGPeLSjJ0nV+iejSdJr9EMkMGEQvfVHGMioqq/cuFhbVI3lPWNnlvynaevPdl' + b'Os2T974coS++D+WIye77IGJuibgc0dG8j8uRnqKkTA0tHsrkPSv4rnuk69kyeY+yEBW2Tt' + b'6bQmvwGxUa4tGFBv3ofZQBSNjwqnMI8UiOgOmXJJep+5Y5AQCTQ8vkA3NolXzARD8tMvxK' + b'qc+TD37AX+buWwIAACXpGM1y0I048Nbwi+C8ioAS+eBzH7J9YK7Bw8aPCTPIE8pgaglRG5' + b'YR4KsW6t2HmysAy1oz/LxzmWlUD8Vx8JLgCPXzKWgAH3T/jXRhfPKVrJgYUlSXBcigutDv' + b'rXxSsEROTCkjCMiMz1JUDQCnajBhkaqxAhD1zwXoPeodVNIPkQ7Skj6yUDBImU/J3LmllR' + b'BtZiHJ0IWlo6x0IfrsahmsVlVtHvWMEcFdKTzwLroNeugP8WICa2u8mMDA9t3T2iWOn7rb' + b'd1w/LmCKbejjcDnoalzNLX7uzzutF1ULh3v1BrV031vx8pkQwqZz3VrhQjV6CCNKFtuGJc' + b'J+CXy7FQn0rh9c3zxhZTbfMqVtHSDFTRe+D0CUduDXzrX6WJH2vUThvn0GM8sNoOYxU+9B' + b'4iuSX+EZWf+rFMw0+TU0X/B111iUya+R0rwCHaldcwA3p7hzeLXr2/ywCsMccRkI8fevR1' + b'3P8+RXnf9Qtn49Gac1P3QmkOOSg+//ZnLS5L9DEsrkv6OQwBT3afKR7rPkY6R7LkD7bmCa' + b'fPS9XVHjW8Ya5MXHEEsFIhpVyFb9RzoBqXOyNrRvkMU8kKIiFJAj1s4QiJqjgL0dmCdIRt' + b'jbKlcLknFrTJFEPRoVbfIxyhXwJVf8tw8E/ut0hJ0uLx2tXMBryuQTczFPPq24YzeZYHqP' + b'/hJU5qh0Sir31ITU1FM1qcJRufFXOiozVOV5JpTa+zO8mXdJnoncxM4YUpElI+VdlimozL' + b'ssycu8SxQaKC81OltQXuqS6cu81IUJxUtdVKS81MWSlJe6oJyZl7poQOXisiUlLlekxOWc' + b'lJe6YPqmIvWMlJe6pNRTL3XJtE+91IWhvNQlZZl6qUtKPfWylCyHqZelNPF5WUrmxFRkYe' + b'yFl6Wgv0JykPlZSA4yzwrJQaa9EFmQPmll/ls3EYqw3r/0vsvHAPTJN8XSf0ceSgdKS0BB' + b'qAaLzH7YvvITvb/51OsBtYVubaNDutDSa0vIXJTlGzX9jDU6kmtiaN/2WOU8GTmDt7gzhf' + b'jR+jzSF2+AVgT05AxBbB9iCIUVzdcQ+zZy0SB5236vlk6Rov7JrLTOUYD9nyIAqkHUa4A7' + b'PJ7Ha3DwLn0JXJwZlszn5slndhbT5POaSiyGgM92wQ6p+yzFCzQUHDLsc8j/mSVirR49/+' + b'e4/6WnKHfnhpZCWCSfow1iOL+5+Tunw1AEiL07n6KNW8i6dbv3NT7d0LbgJ/WxCRQp8ymD' + b'Lmlkh4SJqNWgXJIfzwyh4n/WvTemB5+jcoAIesERk97PUEgee6OwNwtDnXrW1npqiPPrQC' + b'Gr5POxg47h1WhiCDtKH5Sxz6d4Z7EB4gsY4b12O7XkD+brIFSafGFxF8kXmY7M3bfkBwA/' + b'uUCxfJHJRY5vKfa5JcJEotGA1INSoxID3aoUIWCl6aPufNEj9RSk0vQXgfQ+llXAJOYsYJ' + b'KCmcKU2cAkwC7WlMm5NtUpAihpoTxKk4e0MnuYuW9xC0Cr9JiefPGThJX99Gofpn9fRpME' + b'iqknCVB0v4wnCegqvkSThBZ0PElg9mpIZwTy7EpTgYxab6wgmGQIGvGX6zXS1oNK1a3oUj' + b'cRZKWo7Cwr2SacF55I2T8Jy+QM03p6298PO+nAcnEgi6lN6jG9ntqMwRuBTb2bwIuEkPkI' + b'0mhNnVI0/i/jheQJMd8ikR7MG9bcJdb9WBvga+MTlJGfv2MY+hLNJCoPSFWfJv9goy6Tf4' + b'T22ST/UHUHU5N/RBOFDHS02gEHrsdpwIuKCuFG2yd18g9JHHi+rmFK90+KUSX/9KLWWfLP' + b'INLCEjJSQ+5/qipSk1QjBKZq/1RJqOvkn77q15Pkn5GIiFNEqpL/oRh18j8h6mXyPzqmBU' + b'gd0zz5n2ikz+Ges5tZm/xPFA8ClXjq5DfGM0t+k6506b6lwRPQpY6x5bcgVWuJkCFl8luo' + b'sSljuOpuVsC06K2hpY+YJr9hHqA714bI5Va3h+B9hqLl/+aLP7efvktZQSi9wzEtQOu6Xo' + b'GOhkfonL9FuYYsklzDt68wFOByuu+fdAbNHXbLYGJB3q4/n3e6LkNREfiWrzr5F8tpnvwr' + b'Mq8qQfsRZ5aIGVa1dN8y/K8ASJE5whVZ2s4myb/sonPVmC9ReBztS2aWJf+KWmAF+ub2RE' + b'3GDa23BW7VGoi+7XRa5gTGO2qLlKiO0vi7Gafl3Ih0kfxLazqzafKvqGgRsxQtv/2uVFMk' + b'tEmEvrFe33cYbXZoTzM06bVvLC1Zm+4rnM0mxJ8uv6+P6zPczWtLH/eXZ65RzA1/v0Z3qc' + b'C8BXi8yML5JAf9dYD2QwU4RNq0Gncx5hGooqbre2Zlb87D7NfHZ121VxFXBYhhVScUyb8f' + b'Xob98Dj8kNN+ay2G2Ln7FkvnlQN0vqcO03ZLlcPEENs7igySfPBipgJRZAsZiZO6vJxYQl' + b'Q4TEXWNwyxC41qq+SlZoghdqXRyBB5pjlict0kvkZAczefJoKH/T2qelpZyFKT1FFDRLoS' + b'KJx3LtkMXCRBYzUABm0XwJQ+Qi7nyAG9pgzuZrN+VnWsIuTqKPJB6aFQ9G7OTfMAB70Rgu' + b'iMSw0ZlidBmxaBWh4WF5G73fNw7FDvcq7srrvgAZE89v2EO/g/QOzCkvVsmtL4aGrIdII+' + b'yFqqe7K2xs6enFlFwJHZxFrJeDK11p+ezOyevCdzu7ftyantXjxZ2A7Ok6XdhPdkZbfaPV' + b'nbzVpPzqwpnCPzibVj82RqzdY8mdmNAk/mdg3Uk1NrU+bJwhqLebK000xPVnYm4snaWgZ6' + b'cma3Wh05ndiJmCdTa9LsycxO/T2Z22m/J6fWLsaThR2kPVnaGbsnK2vw5snaGo94cmZtTB' + b'xZTKwxkidTayDrycxaH3kyt1aWnpxao1VPFtZaxJOlHeg9Wdk9fk/WdlPUkzO73ebIcmKn' + b'qJ5M7Ua0JzOrLnsyp8WNSFVOSYpUZeEarSMpVS4FWlKqXNJbUqpc0ltSqlxCrihVLiFXlK' + b'qQoCpKlUvyK+ZVLsmvmFe5JL8yUknyKyOVJL8yUknyKyOVJL8yUkn51kYqyY2aUuVSvjWl' + b'mkrya0o1FZlrSjWV5NeUairJrynVVJJfU6qpJL+mVFNJb02pppLeGaWaSnpnlGoq6Z0ZqS' + b'S9MyOVpHdmpJL0zoxUkt6ZkUrSOzNSSXpnlGomCZxRqsInEADJXEhTglMhKVVRCEmpilJI' + b'SlVUQlKqohaSUhUzISlVMReSUhWNkEYqn8A0NVL5FKWmdU9WQpZ2DuDJyppoerK2xjmORM' + b'ai8ovMJmMLCcpkbCnJNxlbBZIRVT75NbpNBFUJaUL26a2NVEub3gy5nE1cg8y5MDxx4mO4' + b'JWHLrqhyVs6ynAsJ4UvXrkGyVpTlRMicZCrklGQmZEEyF7IkORWyIlkIyYjKUsgZycqRU9' + b'aKsqyFNELOhKQYbnAhyZDdeEGSQWVeyCmLsswyIRlUlgvJBGZTIRlyVgjJBGalkExgJkKm' + b'TGAmQnKYLjMRksN0mc2FNFKJzJmRaiGkkWoppJGqFdJIJQnkMF3mEyEpVS7p5TBd5pJeDt' + b'NlLunlMF3mkl4O02Uu6eUwXeaSXg7TZS7p5TBd5pJeDtNlLunNjVSSXo6t5VSE5NhaTkVI' + b'jq3lVITk2FpORUiOreVUhGTrK6ciJOt5ORUh2dzKqUjFwbScilSFEUOkKowYUgqFEUNKoT' + b'BiSCkURgwphcKIIaXAwbQsJIEcTMtCEsjBtCwkgZURw+dkwZ6qnE+FZFBVKySDqkshGdSs' + b'FpIJnHsxClOfq5mQTFEtjk19nqVCMkXNXEgGtfRCFqYElz6fUQ+ohXrHJUuhaLyQJRNYLH' + b'yRoZ2DXE6EpONlKmRJMhOyIhn8MqjlVMgZSRGDWVcsSyFTkpWQGclayJzkTEgjlSShMlI1' + b'QhqpFkIaqZZCGqkkvZWRymd7ySG+aCW97EWLVtLLIb5oJb0c4otW0sshvmglvRzii1bSyy' + b'G+aCW9HOKLVtLL/rloJb0c4otW0jszUkl60T+vmiyQBUmf/Ap97KqZBpJc6UUrdm7FaiIk' + b'xVilQlKMlU9ghQ5q1Ug3UnGYKJqpkExvE7imIpVCMqJGxOAwUTS1kIyoqYRkehsvVc1hom' + b'gyIVkKTSokS6HJhaRUi+CYUi2CYyPGTEgjhq8bdW7i9XWjnpqIVkIyooWXasZONXN+yzRD' + b'B5WlTicHiSLLUjdBK9McXVCWujlXmRY04p9kCyGnJJdCFiRbR7LRYSh3jvO0NCOsczydcS' + b'qUUWa/kcHqqldniiRanAG57Y/rp/Vh/UPOk7jraNoPifuwMsL5Sa+XRiBU76bYnKrGR5UR' + b'dK9iNp5V1MbDeF2IXTpvUlnfMwwz0PSHRyA7h61ogQ4M/517jTZE990mAhcER7ZUTNKNlS' + b'aqVP14pWkagSoxdP28PuOvybd5Fsjtevf42m/O2x9WKy5ByDoAR5Fd9+i6THxJMqldgN6s' + b'n7rT1iwGvrJpWVdx6uvWgNv1/tvalFIIJB9xRh6ngW0WM4LHYsQZeawt24olwu/WyGyR1a' + b'VtzzWYkVjZiDMK3bOfT5fjWnxxLA9w7GU10bxxRVjlmjuqECubCS8oqpDPmc3SP7hIeQqo' + b'SdHLFg2Vfdxu1/1xWe9+yDJqDu64PXsdfdx+DlY4bg+mXm6lHrR/6Y6n9WHzAxdWAqmdTR' + b'TuV2eN22BPjyw7qFbIHD48aWBK4Hm7PjxvL+ftGhWWRlHAuHaYcVWFn/fH9cNzdza2uJgt' + b'1FeoN5lHxnEiq7jmCiN6ml3DytfUxWSiyPLMuba+QRuZuOxsrDDRgg/DGY575m2NNnG4bN' + b'bns1/Eo2J1uJy+sjTDYm0A/VpfQHS/BzRcdoACfVmj2ML684TIsTv8kPFAwPploFgv0Uo9' + b's1Bwu0rJ/v7lBbm6qlcrfh6H9cO2OyGXqSSS/lPqTa2B4Yi+74nFwWQZnJ1ht3sT9xDyuO' + b'7UQiLbPpEAoJ8/PiAnuRJocpWdj9nbTNvZnJi50YF6RnSjQ2NpOXmNqnk8Dq/3w5n1fTa1' + b'5GZ92m6GV9oeUI/xkC1NXmQhkCtRXm8i2OWFgAt5c79zgS+ngriwl7kgLujlRBAf8jITyA' + b'S89AHbMGZ5IF0gs1mAfChUqD32uu2RGRDRuUNZb4i79ecioAzQoVlATZgOzgN8eXGYS+cW' + b'Jf2t+xM1hPocES/fJJBIlUq2Q9x+TMYrWARHB3r0qeH6gsclNQ6TFGeKjgJdKQYE//r2Q1' + b'bNWgUyKierT4zBJSqXmWfeCmSrxFQQqREuH02hzVJPbEyhFYG8PzHIeS0ISuJ+PQJ9zpUa' + b'GB5dHVhIcJL4yiMis0OMTmAKBWGdHvrebm5wr7HVQLRf5jjeTLjStHZogzj2LzRg4+zQEv' + b'5Yhmnx9gio0rxSh2mtYoxp1YLLJife8HZ65mgyF2q9456JjKRUDT3nBoY+B60yS0No0WAU' + b'gnVjUcuFIAuh0zYKo5ivrkq2pdPb/uU8mCFAdWZoIWcesEAV9/nHPuUcGYaTKfGgjwo5Bs' + b'5F6aFTkmrAI9vroeRptdPSQe0kvUNQ5y33B0OgnF5ervRRdPCXW9pihHttMQK1tgjGV2rk' + b'Wz9Icdk4ugqH2frWH9wM8o0KD4sxqCMTg4oWBlf33KPFjxoNoYDcYyT2RvKFIqOaTNxJkv' + b'FbyTq3tOSA4auKWk1In51aAb3gXivCS3KPbBz0doxaBRBVZhiD78N2ZprcRxeb5IaW8Qlu' + b'O+pyp/7PcwcnWyoKGGXLEoF2D+sLO4ospzO9RYhQaRriNdGaZKxLohMGNtYhZ8ajSvOM9E' + b'iXRM9qwG4/8r6YrYRzGnYY1DfCmhgZDsMQT2oWaJH3nc5HxqjtMljQ3dmur9xbU4LGQOuR' + b'FRQTdLYzCc4h0kCGiYUBg0JvSGjZobahJt9vdb1akvY1xhC6yjgg1BkC9nh7gZLsdVaS1g' + b'klvUMurHcPKDVzIh551B82eq4Ine6+V+YCTMEONdtXIJ6SNwBKCHVuQ6R0CAaHl6E/nKHv' + b'QEF1SjBn+YbNEcSzzW93pOfpNVd5xqzfscF5uKAYY106/d/4WqtuvuPO69dp+r850CH55P' + b'CWO8aipEU/G3jGo2ZmlnnsHs4em7vAjNvrzGnmN9g6a13Om57cFZm5u8Ch/Q7uH9kpZKXP' + b'geDMZd3pjG4kK9nySZrb98bpmireVbqCRyehEUeLOR270EyTLYdn9E0Zs09fU1SBHlBTsw' + b'JT4/toigdfwz1XNXrXP6ZI9aCrP7J20NUftMw70Gr+CLM8RIuy7oyWgnmrIey5yUnVBPL+' + b'TH4egH2/IZIpRPfCyqsfajV2fqHnNAC6klUWtrUTYiwVbeVoFeIE0Y4iSTRDRFko0MqiES' + b'1MnehGh8Gu0YAVZ6Ihq++tNBQNipF/E3fbJlGDRCTLCLGxNBFmC2weYVE8cRA2keju3frU' + b'sk7CVRvW8iVrLeQMaUpLycKWcriKWc4OJ43RzXCBwm55JXn95imKbu6wGzHk5GECcbCj/B' + b'yyiNlYjdzWuiCchiu5UEEvuh3A40W3A9KY/p251Jm5bxM/R3au9VtoQPCYtx+pss4Mdure' + b'TJfcJg/Uh/LkQVsKloDVOIY58YPc01fh2yuNxLXSaOmgNJLehWPeNcjDhoP3YaP00jrVuM' + b'v9icb8GkXkUC9TkPFysv0Lj0M+IMbh0a4lO0uwbFHZT11mCwu5KmIo9GZP3bGjEg3/Dfzr' + b'pVskQe6kW+JbriLEFOlhfBXhDJDoapklwr2D5F6OO472iMRdQdiYr3AFIenQucGdRNjUnn' + b'BpgQDGE5dV+dU/cXGHeZBb+vDoK9lyZRDdvtqJgYbd5nR+49JM5YLRdRNuotM/0PAetMIz' + b'a0j72mEIXT0cEOoHAZ27U9C3b1NckvPwzLkHJtxpbsjAn1YE/vfLFVeRE82xnm+YCxdkaC' + b'vpykR8+3LFBVnfv1yRWUUDa1bDbd9deEbKVA6/LpVVgWMGN2Gkwhj5KGeeEZbL5x6Kw2B1' + b'2w4ImlM4M8hO5h7xQG2BPjhxnobOA0yku/EQrhnPVSpKh4/S4OBxClwoQX4HjKR36GUUKM' + b'QRXbZx3/vL7ty/7N7Q2c0qh6FxgZo56mV34VrjrPD0AL1pZ+pWjs7dobxTnWMalw+MysMe' + b'daKYsnQo3DTRTTxblMnofJBrqkuFu74HjW3XUXkzDZk6/Xr3tcM8iOPAIrPQhnfW7whMLM' + b'Bp0tEiqUXkMBUx1Nbd5Z4TPvt1uvRnJ6yG3DIPbUoe9g/omUOXM0eTjHQ1+HJr6soRpNHH' + b'JdgdD+ZoywQjn/nc88TX+vjGbfJUIAk2dc64AqCciH5TWNqqmlTome12xXCZjnkOp1Dmsj' + b'buEdqTedxIceNLriBTkA4vEn2Ib1UuvEM/H574wNQS99JCqodtUwtFy0LOp78NT4szjVlu' + b'ndyFK9ngkqS75MxCds1HhxgxXHgNsRd0XZxDUJrD0/HCdJp1c75NMFyOnLA8Hc36E1Qo82' + b'DBAILG5o6YL3h5ETQqRzct78ChZuBoHsZmk7XkYs5rVNJA88Q7R09LLhcp2WmgM9JZoHPS' + b'eaCnpKdCm9irldA/89JRKhCWbnnhDNQeT77nAf1JIfQHngadSHDtJ15VzKHJ0Z952XJaBZ' + b'pnbUJmrHidoSlaSzLtqZA/GlLS+pOJS2T52fide/L9nPmaimgfjWcpg0+8b20i6fzEq1cm' + b'gWvTIdn2ycop2frpi0mHRPbpN1MqUohfTGQS+j9MaMwF9/QGFYtZIE/rw4m6voZQKR+pXR' + b'BDrRtN700ejeBoaTa75utdsTRmy2ba8gYehZvfcKADNvG+DEd7vsF3aqZCBdWL5Q9Pz08B' + b'QtbJJBTFcLx863p7FyZChALQnalWcGkGnqHpvXELM6ONvqGMOk4F/HJEIA9vzGDUwrejuV' + b'Ob+ZiSWrEvX9H0CMS9ZxmHj45VJNwaLafJJlLiSavFqBLkJtgIGNItTZnveImvaYmNl/ig' + b'RAEd2wtMErdyZsxAomUzjzxxDWSSTdy32bmZZClJtSJWGjosiJFW05+S3tX0x0S8CyuVFG' + b'5nl/ty+xlW9CIgrOk5eItA7f628XxnLGVGnLDyd8U/dU88Nek46Zgz8un5AXVAf+z/EFdT' + b'BY4C8CxoB3sBZwocuXesOH2VAkfuHctu7Qtaa3Tkw/Mu9xflo9HoyIfjxTlXKnDk3rO2ps' + b'o6cKLAkXvHYqfUCVgocOTesOImMJ8D00P/dGUBbQbisfP6MNpCmi4CJ8IOvApuZprn8SnI' + b'Pa8sYPrFCMRM4+XQcZdFjvKYQX5aQ+r7nb8/lfWIy2/XRgrzWwy9KrQcO5DetbnJ0X5b4+' + b'LIecP10or1rvZv0XN5RG1Sc1vb54tJ05NPUymUU5RXBLSOsiCAGLnayKNBlaLd8ovJGLMx' + b'GzATzsux33ujBJNJPmFcf8k4OiqMnpWGNWHC1c4MWtl9GBzQImShAFGpy+vR/MOqQG6J0W' + b'3kRP3l9XAedeOG9h23IXQP6oDQhRog9JGYtW3GFb2pIfpmIxP3Ajm6ifYxskSxM0vpWD0S' + b'oiWid6YaQ8tiMOqbfQrm1L2szdJU2GVtrni06zFjmmOqvSrUpo6bOFwQQZPvtn1oOktDh9' + b'EDFUPfQoJS0XtHC7LROYjZTeNosbspCdg9pKn9lCsDa8Z1GPbIVsiLn8sJXcHhsrfrbiEr' + b'V8j/jvdkZxjr40yuEpXHhtBZ7ICQwwTcZhE+MR6/nblD5E/rFyPMnQacJrLXwxMFjogmgS' + b'i6cOZvXifx1RNoklUS3TzhWvpUUNc8gk9pzAGK5NSFxNh1qZA+nwc3OYfaven5JhtEW1Xu' + b'm3P5zDL4wpLdxs0y6NGb6D7EAmE9n7ZmUayYwUO0P4HqEJYqobFtwj30aEPRHBhJPchmBg' + b'guomzWfokE3cKAmuW3MsjXCURb01sZC9I7M82fMA/Nt55I5g6LZpLeoVquE89iCuBD1tNF' + b'Ojo8UUdF9R7U3iBrd1h4zJazQLryrBLfgl2J5wEYFKISt2IkGGxOvDgtzVNP/c4rUluh7G' + b'KZq80mQ8/OwGJRkOCavCzzoHMyK/Fvw8YqNMYSO8ZEvzOc1wMS8qyP2LaCurUCRCOqPLzo' + b'HEMSzuveLNMii8LSPOTQS/MctvTSPCU3r2kgT75ZzYCNnpQcTS5J2CXgOZ3ffmcjJUdXYz' + b'qNVj+LVcIGARE6OWo+w/eReciTJJ1abIdbveS6SDq5ox7+7fq6X29fekCvtQt4ZchRXHG0' + b'NYfhuhbV4Hv0uAeD1UutTM3D9i2+Z6GuAMrgObVEOM0914C8+LHSqIyxM43q2zErzZAXP1' + b'KNRtde5pojb3tQelVCEFUfuwbX5zGk02eskTPuSY8q6aInPSwtR+Mhf6f3+hFOd2WHAz/6' + b'3Q/0XJ1YuNf4VsUK/1H2w2u0No/y0YZX8B2dwYfckY07gnOrBnltP8MI74BQKdvWIlK0jD' + b'0AbkeLSw52jSGrZql14HKxdAF0mEj7MKpUMN+2MdoIxAa+YXufWUzlhRdH5aSPYIs+4yoh' + b'XFT/th0uyJfMQzS1sdY3HFMbi2KwGpD/L9verRzkWeZSKl1+NqldGNECqcNUh+/z1Seucp' + b'FIyuqVAE59Wjkv/m6sykUu/V02qZwTbwBNcnwWgL5u3DqCzNVmeHUgI+N+1MHn4YBc1JcO' + b'GNCf/AehX4nJkbBdt7frlFArOvNkTKgrc4dIRrQekDLOHCIJp59d/8JGl9Go3FMyscky1o' + b'KgA+SekLdoKo/IWzTIAP0WTY6+db8xygiXK+23njmhgkZ6Bf2/cAA4je/gaMg5v506kwVw' + b'F1myQzY9YmA21x18vLn71vFmxG5dNEfH5g2chh86CkY5ehSH0PhOeRTOwSbHPGHZhRdy0M' + b'qGUMKIyN5OmzFp/HzYDSe7WDa3QHgzBoN+DInboo0ZXiFGBvjKMJ/g21+0hVl+F99qhUmC' + b'NbZEP+U+o2bnMNGpSkerBrMg1H/FvP3AdGclivWo8w5+dC5PIZFOXB1I7Qox671IjuK3n/' + b'xBBnLpLatzfjh9oi5JDEffQUIrtfTVoG0cegF2w/DCq9nmBKkbnpWk7D2vDHArh+mWP8ai' + b'1VgGfTZG+xseX6BcSttCZtoZVsUPNRzVpKXU4Ms8VbRCXsqtL0v3LUM8cuaM2M/rxwH9jE' + b'wMOXYoPFpvCbwb0LVLP/9bIu6LVG/WAHkVqbtlB1sp2BeExrTeBPzPB7PSxwVT+637hoXD' + b'7JpqLiTNuyfcSgu03KnvwWhS4UE5P0MAUzXaDpgeEbMvO3dlf6reeFoZyla8mXGjH3yaEb' + b'AqdNrMk0dqqmXyKKsNLb7VUGBoBHDYdj1XhyYz0OetWoVrLRCtwjksWmtrkke9PlMnj0F1' + b'LJLH6MWpVfKobF7R2B4jbQjN6XFsBLvMiI1XyJc50dEKOTTVR730gNgxdlASHvt+fMRMZc' + b'Lfnh8I4HHHD3gyAITpHyPVBtqIg0SzyQSRQQ8y0xq080MBnex2GMeHP63JoCVpw2jNF036' + b'nteP9iCwp8Ia+hgLy+iBE5ZVAxYWkud2sThmKC8xWxZ753ZFN8JHvhx33+3tyWRPBWcOO1' + b'wO9nSyp4ILh7109giyI4LxuIP4ikxvzyEHOrgiejydzRVMqB7diToTpvmPPeS2Vlck4kfL' + b'GLRRy/PCfAUd09JKV24MEOrCVNE3NOW6NXyvKFvfVkeF7pMWSwNo7bdxSFB+LRLrvoXDgu' + b'prkVs6rhVRq7jWbTTUWkgruBYRta62pKi3C0977da6Fx3PxqqHauvAq7agTDtDu+DBMvMm' + b'Eb4jlQxtKBwhxFThcXgUexl2GsOjX/eBqvAIXXAv7CnZR3alvM474XPYLN+p+Qr5aGlVvn' + b'MDhPLNFX2rfJeG78vX+tbF6ZFQnBaJi3PqsFCcFrlVnFYiXZzWbVScFrq1BFoZji5o61YK' + b'2joIBd142he0dS8FbeXRBW0dxH3mUjDpNNMASa9ZWMzVERfQdtSaIZEomAjkuH7g3jFP9k' + b'xJHR449ucJTxFiKvukTeRI+gOFBb69tRzxcLZ5viIZL9NjaH3iod5owGlmU6LxgNPMGLI2' + b'vasMHSzvSGs1bgFaq3Ck7UuHTW4/dwjJKRCYMDlQ3cHfTgDF7x82iZ5DTJYg/VITkifqA2' + b'RRzyEi5DBMl5YIzyEijNFziHDvnkNMzVfggI72CuBSL2EUGWiV5ob0sOcOV3QIq2A4x45v' + b'ZjDkoAAuHC7IKnfI/vLHRu3CzpbEUVl5kpCXpq5II8A33nkeB9oGVggXRQzt162BY0r3FB' + b'ld1qT1M49VZhBXsQxb1wUHhMpgAH1/wNwCoxsEWote3SGwsvhY50F9+N5bkwVZ10+KMWE3' + b'3ppE/m/D5tTcUFphJGInfiXjVE8UIkC9uQAt8UlvLsxJa12a1brfdzt7A4v5DNpPBATVx8' + b'FBiwAQbzsg0N1wxvRBXq6QK0NbzzqdOfHK2JgDoF6/gDKnGO6s7ERjaqLG/L1mOE/pLZ5u' + b'x5EIXtRsnl7DKso5Uh3e+ITbaBRFC9d7IOhVn/QeSANautOM38G0EI3syOsl7eJPlfjlSx' + b'Y1P/WyfpnojWLnwN+c6UhfjXJLhpszWwtEcjs/6jZNIh2NLjmUt57wXQWUIo0MR25vAF82' + b'Ho+GSPE/HGUJgcms8sBwIVSVQF9VfILKAgUkkEO0mIc+hUdSwdEbFgWScuEEYD/4syDzJk' + b'De5qux2Kk/PLlz5pN8FiC3OUo7zye9/dEw9ON6HzaY2Mu8hf3xWcL5O6b129uPrs7IiA0q' + b'UHV1v9fQyU177jwJJ0bpSN91a+lwoy5pddhxSXJkBpIRG/d689ygYf9nRXrUB86nAPuz2m' + b'WbJ9vIgmmlaL1MUtPhDrqkXs2ncLymRKRNLRBbqWTpnTFLCSw9K7bcheXGE2vLahXr2mNj' + b'udFFKKlgz+vTcRQeqlnEvQ7Spep0eb6MWAVznja9ZqJ65MoKM/Tqyd0pM+v4MgzmEoP79f' + b'HenJtvFh62p448vqBIoSbSs7L+ajJFm5udIiTLr5DHMRJs3zR6cJcd3OJRGLTi20zUie6K' + b'I3NqU9sFSO+voKy+gvLpFRQiiOCx0BHzSuqIG4vtWN7eq0kVbS7MipBsOkbyyRgJYWt0LL' + b'DmXcmrmbG44LhHnKtEb4NN0K7iN53RItSbzuhOgvZaWSK86VwkW/2mM/jRm865oSVkuO7s' + b'bW+8UOXMfaTCfkZ2/AoTGw6I3wXNZSpUUFuIbW90sHoVrCIpeo3xYbtG7W3VzCvNOb8O0v' + b'9h7rkdL5tZ7Dv3LTXzIuaOj4I3cyOG741HgtSaJxE2Bg2H6Iwr11OPApgplvhHNwI5OhRc' + b'6DUqBqpP4tWKjjryJRmXc3Rve14CPIjWyvw7XtQwwVHJ2rGSpSxFQXpPpf3Ur6Ch+Prucn' + b'2uqHH46PCMg8cncpYWDidyWguMTuTQmc5V9EvRCXVNRxnCaK2hK/Q+85lOFZGlmtgoIrRO' + b'B4zbuoOvmrnD4xYOMLrmH/kZ6X4oUH2mpcKgAR32xS0MsNlHJ5RJ6+RrOko+ctPZ7VIX4W' + b'c6U0RWKiLPFBFEd8A4+Q6+Sr7D4+QTPAzP24s3VMoomNvQ9zrzzEAPmnjhQgAUsG+xnWdq' + b'mHL4SLMysoJd/ZS0fop+ZuhvA482ObPLgpA7lclqOpxPL7x5ydxdwYIxN1fw0NRW5g3oPH' + b'VbQHHJPSjsIqNjtKT7Xl1klcN3dLC2UHRUfOgMoseFsuUyQlxmQeivXE9EOG8vW+508mpC' + b'+62tuzw/2ojxDkWpzz2gdspKh/EdrYzHXXrq07OkFxOgJb+VlrRK1KWEdZVoe42MpFucga' + b'C9vB+FcMOAVid9bHDTJvpdlKJMem3lAmH86qExRnIB5Vm9CpzH/tgFRpOoBUea3GJW0PmF' + b'x3yluWQLZx5xkCsqUIwpmsnNY5oSlhFqjorlPC8zRs2sZ7WC6hlxuO1/vuzMoRERo4rdHL' + b'm3EuTINdfkiCypRikzzxmjwp9CypcR/8+Hbse5ogQ9i/iP3GHFbNL7xqxVczHgHh54c4j4' + b'Lm/yJfIR+yhiZVFxbddfg8BZxIH+HbIhysieBxj9syMsgKiwduiOjkHO+oon8cUsFFmILy' + b'oU9kvCiRLGYf+B9uHCnsXsc8gSdJaaNYQqkEU18bDehyyJ0u0WnHOaSWiYx+9CgqNoMPI+' + b'SI2Z5jHrBVolaoRENovZJ24hBFHicJXpFVId5eSpe+A5JhFoFjN3jyJPlIzT8NB35zeJLx' + b'LW9nN8kjNGu6jSRfXgdB4enoWVxqzLJkQUVcjTJbTMOC72o191+1po9itXVKRAY9YwbIQT' + b'Nbpv3XFgolRtM1Um9G0q01ljAkNVGVaYkNuqxiAtAVeJMbKGoJSwFDUwjKzWFIQSKovDVS' + b'C9bVOmMG2KyjJRlpLI7KsnmKCiRvfZshw7jo9jpdTjI6XUwWOltLJwUEodMFJKgYp9I7JC' + b'2zeSpcwlQeqVYeR0ZNSJeq4HS7QJPdCxt5Hs5LeOyNIhJtJXhpkowSuzOmRnP35Wj+345r' + b'27E417E5II1DYkYPxOC2y0Q73+PU1uqujQ5ftgzAI/5ua5bIkc3V3ewgEL0GIgx6Hg+l3E' + b'PDH3dQ7Hm3d1FoY9euIKVS/Sw5EBB/RB3vwPXfbB7IHxfH+KJnXQL7WVkEIdDQrU/cBDBD' + b'zFkQbsHNP2CppCaC7Jw8EkAIo+ome0e35ZRhHPfbgVlUF89Rez8BYWkGLAvqTrr7zPqQu3' + b'OfX6ofgCIonhHJviYE2iZuZLve+4mEeIt45i9wDYbNhR+7X+xHYKAYrSjApw1JWVJX9l4p' + b'U7TNecMRaZeCHBp9N2rfd8IalsJRi+0mTRNXklQEU7U7A+UkDYvRPJjI8svtgjRzccwsFF' + b'q8CoL7eeS1slV20p15heQAb+bdufT5H5RuFBOaymmFXyO1XzefJ7dHdKClrt4i1A+i07fu' + b'sdO0uHDTvQ2tZ6kvzu9fUVv0Vfn1lCFqDQGf+OJno6df5MA3L5d3cMQ8qnWCXxBlYNutuH' + b'tdmFoUdXArYGvLoTcGXg8bo4pFQLTTNGsB2dSWuS36NdziVpn0GG0DnkgJBFBOKrWxAgWk' + b'3Oo/6/Rz0MCkYaBDJIzyKzhNeEolfByLA+bZ/7yPIyJRwkLEC6ATQnS3fjc9A3nyFsDMOm' + b'igE82mcXnpUtABpgZIbVJDcssAw4MlBjpMogyzi5slcz6HjvdkEwvttwCUjneGHokOGkda' + b'/BcMfmwVNguhdpFB0NQCUYLy+m15vbz/i+RlRzoG/dcDnsoQfsZbSqUmG8cNXqJaxj1dPA' + b'Iif4qYVxOq2hU8TcGbjH4dirDp55cdr2mzUm/EMop4mGUcF69kz2CunYzag3XTHvwjVZlF' + b'PvoxST5GrrxBTH9Q76KmGwLAYMtztjjnR8jnKWYX33kiI0o2e92N0mz9EFXjPSzmqD32K1' + b'gYnvc+h2UGSxkQbZSnGEGvIcm1dOCai9SZRiZJqh6Sg5kCK+8BM5cGWQvEJ1Ys057NaHDR' + b'OaQoF7jnqXkrQeKQoCvmEarq78Dgi13wBqH7E19Ggj0Tq62kmsDDzuIimhthmlq2AFMTOU' + b'toIggor7fL38WwtnpGsLY6xtzz0j6NuNh0YaN50Oz1u5uhHTWQMMcqtUYYHL2p8pmeQWeQ' + b'2epkT2Fzl1wtjsNVMzpgv647O+uYoZqcw8UDsiZR61OFJzNR3VHuRpfxzGG9WFQfddd9YH' + b'JFnEgAMNmXt0Gs/j/C5bzxhllcfH7icOl8zm6GGQUQDe4akfTsExcjMertF565VtDPrP6m' + b'QrCn18xxNSFg2IyP3rO55QrpENR05aPa8A4ZBkKdHUkKEF54qOygAVaECXE/IV2TSgw1cp' + b'qhkYk3s685KA48Y9U466vSJnOPhDxxwqZSwv+R0SgIhOehLHruIc5CflF4yhzDzrBeMpmH' + b'p5eK7pKDXI3a8SZgPqNVBtwmMm5SLZaSuGDKSzB4SWsBPDBeJa77R0mCeRfjat4m09eJPT' + b'IuHhgKvnT1YLj3/vnZNVfe1ivPfWrqrI0Y1XT1bzaxfXwcy8o2tW41nfe/kEffmVi+tgbD' + b'7IYDkleb8x+kTjvsUwZmYQljsfuDKfQdeKgKBtOTjoVh7wV7Is7L0rAZQbchzrztyMM+ar' + b'AG+6GvPJGil9LbHrYWaxMEVzpf6tiN7Q3BcLE/jzrZBMhhlptuOsX65YL8f6fjuxYHdDsG' + b'Vde+ZVRAvPuTW1WK7uEPL0zkwnnLtb46tyx5iOT2I7X7RIvd3mnyF3UFuN1RRi1UoQSK/0' + b'5MhcpfSQI0pPY4n4lHG+BBqrQvBk7VWhCu60vaqjxWsVSLGsy1Eo3aO9clpf9jY38PiYO5' + b'JL67EJDwXxS8zGpoEcjt6gLcuWc4NHNmrW59hALXNo8AuV3UDaOs1CsovFWM3xIYyQvDTR' + b'XaCAGKK9QzpAtqH3tS877+Ij4CwermWxfsbjHgC+Xo+RaBe60ZyE7kcJ6NER5aacI7rd1w' + b'FKb/+gTPLTgHo7ewXdWFFo8xts7xU8axbr1jEyzC+jU4dTJDGMrEukZ3jYcqvJ7dSCPTxR' + b'gbcXimWVpw+DMeNbKFpsNDPeqetwc/VYhuox7MJlnxk6zYF7rJMUw6q/QMfsRZmrdVbttE' + b'3ie3UyT/OIEeKAE5Tc8A35YM65oD7JaAwh3QML6RT+/NXlPFm706tBiOMsl3Qgl/1TTBlq' + b'01XJsPLEBTMJyK1yyZLvFgtYf4ZMzxMeuENF3Os7WtrEL3hSB7Df+p7n1GFuF3jqyGBlun' + b'RIdPVuTtAtHDBUfwkMY9N3wFg6XAFDmkq9Ots4nwoW3yNlcLUFTr/cskOn8UrjPNN/MKdX' + b'Nab2Me8oB8LBnGqm1zsaDYZb550Xpq/vnuNYUHQe1eHXjYV9yLUlx2HWc+LQfrh+oPGpwv' + b'1rGyyV/rzuMQnRTmcB9rFVBsJQG4u6CnAka+tw733m6Ctpl4aBrirO6CzAUR6nDvfhzh19' + b'lbMTMt7W+0HyqwSiDRlaRUeGDEyTPYFIKQ6nN22jwXz4Q60dNQzmePKu0fO7WU+oYAwvrB' + b'SgyPUYivDC3VhLlFEYN1ENRtMRVD9tFjdNDe07bKj4e70aCZ13f7UaiXZ+Q6FoW+t3rJ1M' + b'HXqtgSzTwBo/SsKqOZojovfb63WMmt77b7HlGLJSr220qaJ1CbF22NOM9LEPOqkig0ZqwK' + b'AektSjZsU0cikoFFjhkOfuEWNLwMsIj3sRz4tRhOSs0iokRs/MkQQz0qlrgaKdgsLwzajV' + b'oI5wKe9q+SJz+GjxwsHjyfQ0iRcEWXsIvKCK62lzNfF4NMV23uMlQOgrBo0CwPRxHxnAkd' + b'YtT9NRuTLmg7mB2iQCn9pcynF9A6FxhgHcTUWVpdwV1hg8SdLoE17xfezvI0tDdh0AA40u' + b'iqP8rnuS2S6zQi0QIL5xi0QskX6Can61QDBDevUCQZ2RVgsEKAi9IsAmenNFgMPFEORZQp' + b'5hL7oPQ6FGE4SrIkRJjfYp2of5DiwMMiEEqIR7rYEgIcF0DMSFtRM19ZL6D9XRIRWXh23Q' + b'g6HLEXDHNkpk/+UxuEZnd/Fr2I0hAg+ZqtccapSKXnNoNR3lF7LkosqPArob0CcT1peLOs' + b'FK6Q7KQp1FSyBu0ARPToE09sRzDZiLBkqTUGCP6BXttd18IM1A3Pt78RgzUOU180utkKBw' + b'L2qJBFnydd89hfzFFHevnCM1rzEfwSv/y4SqGdrrQWttNUlM2cwBooNfbZlO8e1VLTrRqp' + b'alg6pFWp/2mCeH6ByHpqNhtgBDnr9krDMAodDTRN/kMmlA2lYGBXOSHPzEE2PNIUw8MciH' + b'c63LpSXiiSc0skM88aSnaFgtDC0ekDPRbYkINroeUdNRCiFa9wr1/w+rTtuH0A+q0kOU6A' + b'TsjLRfWjeEXlp3QFhaJ4Aey+toLEK9TZwn5hYae4SJo8VhPJus4ITGIlcLtSuHj8YAB8fv' + b'EuSFR+MwUgvHJtN5adEATC0wHoXK2uORBC7Q2GllwXP/3F3OAWZUutyQ29EFipqOyo0ezX' + b'qJ1p+Z/Q71GiUKntO/Cc998SucGbe0ml2tDBCOXNeKvnWJV2b4fgJmfeuj6x4JR9ctEh9d' + b'nzksHF23yK2j61YifXTduo3WPCykD6hbRA6oLywpZ8YnnvYH1K17OaBuY9UH1K2D+L6yTD' + b'A5oF4GSCKbW8ztlCAgsxoCkeLVEDjTW2B5IKPBA6ULXcDMPqgXcCkMvadeIWGPFY3+4KsR' + b'BfFEnW1O2nerhtD9qgNCx0oguEdU0WWZiCq6LFPTUWWmxwOGr/UzzcRVD8prWP0NDTlJ34' + b'+wlIdB7aiWydUDg21rwaftBUKK02au0NEZ/ZVh3TqGUt2ZsyRkX/MMfGsZdpkF1tUMpDG8' + b'8XSmduiNwIrAugqsNbzrRxahmGDU57MA6/5ApWbCRJzVlWwzRfPVJY/4dUAWw1mpSCtFHw' + b'ZZL8TkIcL90VcTWL8xj/nZAJknZ69itZ7QQZkoeX3wbtcZU7DSAEdeO2kujK2Ni9Pl3t6p' + b'Vk8tidERKiSB1AJs1NYF8+5VT6kQpOiXkFEpOfCrGzvS619vXYF1ofKHTI2uD0WeRteHaj' + b'qq6RUZZ72DtLCIX8J0pF7zFChsHxHa37PHejKHE3JFR4cRNEMeIlkl9mIPax3lFFrMMRVq' + b'3k0UVmFZAxf8kG/mDh5otPiQee1UkcHsxIDhch2QSh1EqEr5Q2t403pGS9rrGYbQeoYDgp' + b'7RJgN1x1Uy+BMU6DSHsOucLZPhfn082jlT4Qlt7jjz4C3j2QbMIByC1iZcZLrjF1NIEF3D' + b'mqYe0PILeGUFOrviaFNQw3WHOzJ8ix7ZWkIOd6ymGvALlMtUo0qBXM40w9+JuMw1qk1s0R' + b'cN1/emYr6iTSFzCMXr4p3KXqSGlAMmKBGfR4hHGTWvykDqMkDo2oAZ/k2w8Kyun5wn3vqS' + b'B/ftt5uc18ng7YtXyDxdHggjMmlB8vQOMgKNDIxXpI8shXlqPyWHG0srQdvcQpKrS0tH+e' + b'lC9DnZMtjoqJLJPl7EjFF4uLI+hne9wz1Pbm/XI1khp5CdegkQgos9MNTGIb4wk7kcX5hJ' + b'efbeomWCb8zsaNY6s58pH+Yt7bfet08tZOxb5SrIqrLocUAfoq0vG4ufoebqmlUtHe7MYq' + b'FaDHtVnkvK09vEcJbpCHG+AKKVIriwSnKaRO+IG1KpyBXpoCFPAnnrbqc52V4/Nl5RKzpo' + b'bOgbzIMqU2L2Ni9e5tWQfOx5YzbvW1+Q1Ap1ZYGgTxsgVqdTC+14UR+GqSFWrQ33lmZtUq' + b'IVa+My0qsNcutGKJMKrW8bl6JuG3a4Dqp2pFe2jWN36pEym1SL7m3kCjadk2ZGwKvPqSX6' + b'Iy+jZA0Vw2v215aQOt0uCakhg+6vTPvpz91tCsFFQ0BRAhWrcGiWNO2iAXmeoVEdN49GXz' + b'OViI6Pm/369HDZWaQhct5SIKPgpKhv+n7PNHP01WgAj/5h81XtvuUCKoYyNveeOUz3BmMs' + b'WsRFgq0xRRRsWFBboQj0mQboQ4PoQ4X79r0E+w0DqIPybFyRWTdKzT3mwXXPVqh4t3KexE' + b'9+TAoBwn7lLGD3u9f11zeCCwE90hjk9DAcO7v3N9w6lNEo2Oe/xvQ43CQvfLZskrys1/uX' + b'oDzWBuFZrmATlcGxnmPNQfpetcC3nz4Rf+rMzZ9ZigGBlLnyAoP7SzQPMy7VNIy0XsxOQf' + b'dva0wH/CZUxuD0+jaduLPAxkh/9DTNlOzhYRvZQS+YuNFCPMNFxOxOWNHLRKvtTN2xO7gL' + b'ajD+Chkf3V/mbWCZ94XRWAWwbxgvAqD7KeUuUnxVXKL3zhSmFHwVhH0BuQmAvnjZpcbfrZ' + b'PNFD1Oz0rx7IPJtULsWZVKITpJrcKjNOkIJVFzDapU6VDse8ulQnS6DM6Z5qZ/NPO/DMCp' + b'Cyf2Tbmfolt1KUpYkCfl7l+p7GeaamKjiGytiLBF6YDxqXgHX52Kd3h8Kp7gN+UKutmLXp' + b'9FQoPCjBLSC6rQhuzNoaj50Qk4uAuXcUynQoVJDrHuW9ilyVF/rN3b2GUORjAzZhHFhxzm' + b'ib6wlOGOzlUYKceLE01RGzS0fxPO6FJB1v7ozgs6unnB25yRxMcHKOnRPVDMVm2JoHXMPR' + b'TVV3EoRkTGHRUBBNO6b612zxxmhwKqhtxZtFg0aqUO1KfxvcNIBh+LtJfMA2rPqDbYCTUF' + b'kphZrzNINY4x8G/6B75NisYxN4milcDJ2O9gYAJw4r3XGe/OflFL50ht9EZQQ9r39obQnb' + b'oDQq9OwLw5XPLD6NNF4s5FXO2zzoUz2mkVxnjte5GMz1hg9HbQaEXbOPUn0qqa1OEsdhe5' + b'iSI+4mEktTbgc/P5El4qxlzdABeZnKeMYDiteX++N8eASvpiUs9fyHSV4tzho/Q6OF7/r0' + b'qPxnlQWHhkwV1lSbyFPHXAKFucbzMgjkKYKpaEosDRPkDlgjoz+8+hRDAvsvjIOROpGzxD' + b'1m2b9KhAmAOvR93YEAj3odEUG/OljQ9XBgnb2IWh7c73hCc6DGk3tUtHqFZnA5Rmn1lSjU' + b'6oMtoD5o8vymYONSy6ngX1cuAhzcNTD83sT6pI/rIkSqp5HLSFt4h5ZuQTZhszLy/CYXQ6' + b'N0m/iAFfisTpJ6ehvAf60R6OZ+WVuQPch5VLphyasbnkz8wfUgqiHrKbWSpY/vFS6ZfjsL' + b'k8mOXaFYnfeXz1q7lFxTC5+N9t/G7BgtBLtzOWgjQkNeQxLJdmgoQF0txgmIPYY7F5pWg7' + b'aUE2nEyLrPmhpwQpgV3/nWcOUT/U6ipyJrrNBfFEd7eAVmuEqMhqjXCe/EGtO03+kKM0Nb' + b'/3ygCGgDp9l5EcGVmXxK4MjSui46N0DM1f1ea/00lErSPqQVNZFVEzTeW5pjidClRQaTwy' + b'1os8/gfPlX0H/l/9XGlUETfWq4T1PT/Xzo+Hjtc6KI1xlfyhl0xRhqKLtZPkD2eCNMdn1D' + b'HA3cBTlRjd8REUMUUGNcWA0X2AbWVfe43woGKNuP5+O4unMT7yZbkBM6S7Gsu6mAo08moZ' + b'7rCBhWYCjdwaRpyaSqCRW8OQ+mqxOmAj15bj33y1WBOwkWvDifOnFGjk1jLc9f8Wmgg0cm' + b'sY/p1XCxUCjdyCIZ3qInG10Ru5IKN8Wiis+U5rTWWFpvJUU6H2emTcejx+1Qg8I24ERHmR' + b'j7E2xiTCU9IzpRoL74G0gronQJpVhPjnPRQs2zTBb7RwF1x6z0YeZwuE4T8T6n59Mq+wto' + b'K4W2PThSDRQB+8mlGLw2EbQzKQ5XxJ3bP8zbMe8tHUgVQjYNpY+BbkA5op+mBNdQxgLrr1' + b'6ZorjEtBWaWBKGVVwvVGqILH6Nz/ArTavZuA9NsbRSKbPjnxjdvwRKyOsCsZxt3IDK4dYc' + b'oQbkVWIJcJp2asYqtETdIcrfcNJ0l8NwdpbaI2A61N1DQdWRkgK9ZmQxBjo1nCVIu/KXjO' + b'SvSayRj3J7tTQuNOcx8ElYsy0W8spSD9rhamqcdgK4X5bnhLoUVcsVUU2WpHCYPKMZrTzw' + b'zt92GKJpByJqdAfnaYQ/L5J6PQQd9qCKGwgsJUChIUJsTdPfGBHTtPZRE6mpsALOg6IGZL' + b'YFVi0n1UKwB5asmgk08IjA4eM2BdbgvSb52x49UH5fL0btWucvxTt3fm3NwxMlVeKDoqXw' + b'plTrcZiU/b8bBq0Xhcre3IGTNCfz1my8hR27EzZoz8OXYALe0H19qOoYKNfDuOH15rO4oK' + b'NnJtOXGyqoCNXFtOGGJrO5AGcOTesWSQre1QGsCRe8uKM6sM2Mi14/iBtrbjqWAj15YjQ2' + b'1tR1TBRq7JsZ2tXezPeIsdoF6pdJUFaBS7VuVlcXWoyRxeOvIFHW9o3gZSXUNfoQfTCyaY' + b'eB3DoXkSA6cfKT9sOEv7GYyhGw3ou0AKMkbXUJiAzv0Dfbi5LATDfHt3tdiQOny02ODg8b' + b'JCbuHRTawTi46Pi881HBsNzhxL3DogNpJnf0X0yjxx4fFo1cIJN178gU5g8WjlI18oNA7d' + b'xRofZ19acLyOkbt8HZs/urQj5cd+ZIVZMiiurJuh2uyZ2bXs0THJmYOPvXfJgVCvjtSMRX' + b'eEmo46QjTXnlZ0PEvJL23ZXxjE7UVZNv06y1UTZ0C0RjeLOFr0RcQJa57ZMheO223ImjaG' + b'9Lm1WczSAWVkxbYCKQM/RydfMMs6aqPBAqlx5wzYqBZChYaGHIjmaYgoOj+A0ovOC2g6yn' + b'NUI4giJwQgnOj48KOVreWCtNewUhL6Cg1y9bVEqaFH9xIxyOsTopOA+u16BekteAXf2kKc' + b'3mD7rcRbPL2lCL7edoX4Z3/KdoZoQ9bPPKH7N/iOzh8gW6PzB5qO8h+hIRij+yjNLbNonL' + b'xVTrTnq90l+2Y53InIrw93NskoTycB0TfuBfRWjubJdzP0BkvnZ55wqbLCj1bY6+QkCnvj' + b'vrXOWBYAN0GnMqSrcvS7iZWzZk5svJbUMOTNaC2pWQDU+nlt6KCfk9Z3dDBqfQmHpiOrHs' + b'YGfRn/b4cLYnzbdq9rA+3DyX4Kuu+ejZaTuu+wnBIjQfXzeNAOiGBK5Btsnlna22RMHb/f' + b'8/+dXCmC6h/wS3hmLbfw3gfnaE9ODCmBW7Lv9enM0mHeS2Fp7cRB3oUVRc592hRcuk57qT' + b'3oPVUO0I485t1YUWRfxIUh9Cw56VkPSD/rKVP3HVVFBK+mQitQ29c1LVNm9lNf3OmgG2Zz' + b'y8ay/PO6qAhhSpVZQu6Yg5Z1iuZYGcWMpEoN7YcK6DpCRs7grUP13u30SIUm0D0Mdt8sd9' + b'+jx9nmib+bccL9tFPXqaetckOPmmBmwKs2aN2OGyHK3j9iUdrPNNfEoyKyB0WEebYDxgtE' + b'Dr5aH3K43j3PkhuPVtBdtBu8JKD6A5RjdK2WpqP+oAVj3z8MO7v41AQyrD4pMFosUrhsmU' + b'4N9nXoURs5TjgBZosbeDS2oMp2+m7NLEtGpjEspK/mgnU2MH6GTWUHqHF6aZFggFdq4NYZ' + b'lYl14Ed1F4B6QLO1iB7jlx4KhnYOik3tKg8G+zoH3bKwc6JqQw/nOsp/h2lzOgeJQd3c0W' + b'JS1wrgjeqcFzGjc5HrHTjnJD7EMgmgnGKZKkyOsdQOdIZ4COzxLHflQ3E7baNVs4qAGoVL' + b'0vrCtpoAbwSSa/NSh+jnkVaLMoLDnXqrBUvScPSzSPAw0bC+hK9wTyJZtr60D74yDUfRrB' + b'K538I64ikMo6TlltzZFUlef2Fo9kCXvXJvlQmTBVodcEDQBwyww1R+px4RMbHoUQRj2/Yh' + b'zkx0vduo25xaYNRvlha96jgri497ThaRvtKOgvDYoD0yaL+dmB4x6xLNxH5CVE1pIss00S' + b'kidI8OGPe6Dr7qdR0ed7EEo6xiH7rlzceSKlbd3pxvmJmvoCJpOihIGjVfwxlwtriGxU/M' + b'FC/LKzT4cLwh1INFaqCgl1lBlAhzDYSgHCzOGkUHV0StvlCj1vZP5jFRqtT8pCnKwsGmTi' + b'l6dzmsz91ooYU8PZKhhukJeaPpaCRDTvW7i3o7ZmmB6MCzAfe9tc+hijHKKcY+nK6WdKYW' + b'Hq3oWHRkPdI6MF7lKZNblh/zJDb6KAwdHyilxt6zz48WZmx4o/tLl8ktcxEmkqc82Ef0f4' + b'YhyZBqwDTuwnBZBPKWvfqKbD9UGq96WHRAGBQNEA+JpYXCgGiAW8OhEUUPhsZlNBQaRA+E' + b'BpBhcGYoGQSXjvRDoHEsA6CJTg9/hh0/MbwS6HLkfsDbBuPwHvU7NnefeWcyQuaCyPhYGc' + b'iNjojL2XBnK/sZ7TQRs4c3K/epFekZ6oq+bhz1K1p4QeTcDT6pVrIwWDwec0d19O4eyi+6' + b'E5KudKvUdNQqIeWw6zcXI6uxtV6/OQW/9ixjzh7zkCdcdBKTZGQk2l+4GIt+T35WNmlIhX' + b'UhJNudC80m9lPXPAduzE6w+4yeWVOYPLM2TU6y1IQWbnRSPVlpHPbwwAswpp7a89zs0lF+' + b'08vcyw394mHL1w4x2M9nzkV4HslzfEjPTzQSXHnKhNsK9bB+6eGJUXtwd6BxVOqpgf6XmS' + b'P3JjTvFDWGzMKTJvCFp5zs3E70oYXzCddJKZ2bcIHRYLYDzWqjd1RpR3ZJ1rqiB++odo68' + b'+bHHvZymbF5RQ8zcw5Ueb7Q4HYN1GMolWtKpSHu1yhBarTIAn6TQPTqHbaLxkjPXCYjGj1' + b'XUE4uO1+0zC8c9e+mCGNkP5haNR4bSgqO+nU1IrwMiGnsqgs+RMyccFd1BhlI0ZziuG2Tp' + b'ODfaI0RVFmH2Wx38recOCwdz2UmHQ7YcxS4PW6rVNEwjpbsTZHH0pqymo+5kmcSvhxYUht' + b'q9tURLkbgLLyPh0B4ZrHlKC90IqsRGHQg2ZUsE8zZcXtfRvU6LhLbNUAr04dw5yYdneyQj' + b'c5Q1VeB7UHJqNyNH2/JaOpjyklbbvhXJ0fvcGbGr17nz5BytCa5IjzTzBUPvmaYoRcvkHC' + b'0frhQdnUmegHF+7bqdvuf8vOZBZxP0V6qXc34Y5ZRab6C2IzJoxgYM+ilIe1kn5s1nbZUP' + b'hiyDFfjG6Mu3DdBXnMPqV4mMeNDPW6IqGiBe30eVNOjYQp7F+3D1OGTDPLLw1Wl7eDEXjy' + b'bnsFiWWyK+q6VKgUZWCZRVnX+CLnCOVsYaQ8sCGmTQBw6mqAjdrccG5nSoLimfkxw941AS' + b'u3Hp6zzzjPHFAZMFOVcPP1QGDQfcTcC3bjjAAOI5V0E3ZO35cO9ZvSs8U+hI/KlhxbV7Vl' + b'vwRtRT4VxF3ZJ1fRtChaKJ7sUpFR01CjrcdS9bngvNeGZNSK9TmDh2PSft3WbQd7BNPOOP' + b'jksHgcGkK4XTkLeUY8MQRXdpKFEtKUpY2aFTqpZ8KO1sXx1lhp3DhXOKDBfOGTBcOGfIk6' + b'6GDZpi97UPM+pZY4Fo6kUwOuJQkPa9oiF0t+iA0C8aIPQ7+cTQI/uXBUEuNT1jpBndwViP' + b'eNFFjJVm+tX+KLSrKxlRH3QvkzWGHlXTuQGv2ox1O66+jA99Qfdnfzqb+zdyCzzyMGLGd+' + b'VA2ieCavtpTnqk9ntkxE/U7KxfzWZnwhlNaIUxnr42yXiX3uSNgUYzU+P0GM+WFoLJPGgS' + b'IKmtTB60SqOvhLs2UybEHQ9Z8vPFnCYRdkaMVmOTVZtYb+r8SOUgASYWGMKBktoi6ogJS9' + b'Ye2tF302eCnsx7cpzrhens4gY3TDENGyXDeXhuP4NXB6i5+MwiIQczDdyaj7vw/YzcBaAW' + b'r50DPUufeSjM0x0Uz9RzD4a5uoNudUhOVD1fd66jGbvDbh0SLy1LT+eda+nnnJMwpZ8L4C' + b'f1zotb7TNHUdoY4t2aJ7NB7RjSU7o06MPkLjg/Tyeprr9E1Y3u5kKdje7m0nQ0dhgGmtFV' + b'I514xqiNenzcRLNkPDmoHDJqoHQoz7yFR7Wcoj+xkLNdyR01RORmuNzvnJPSeeARERajXV' + b'azUDSDmFrQz+Yciozv9506PEShedIxDBulQ+LBxKAv0YtmlERd/eBOlFDm6FrxCsqtNmAp' + b'QUerJJBUvwfNNhFdVYX+IrqqStNR2TIgxIPs//NMc9qnrbUca4uIIXdGs0FaXLktPRac1R' + b'7a9xsHVQZ67M29Ms3SUGbZjxNVEnw8GB2o8WrutbDShd01hkAzRn+/8ATZwmlgj45m22GC' + b'fUSf0Jkb5GiePf0uV7YCl991ok8Uz266sqZMOR+I/i5bImq/70bHhC4CqrWMGwjZHWv3o0' + b'uTnGWRB6mn/ZA1803ZqXnSW+zOFeRNdhGC3Efo18SR5cd+/bRBsHziwRC7R16aPrXEkTtA' + b'zdwSPMRPa1jagPLZWr4013NO5D7DRCoCwlTKwWEyRSCaNBjAGHZSceNnmmlCc7J7RYRVdA' + b'eMN1gcfLXB4vB4g4XgNrrIDrmnVzPQcvUEe7Yi7W/BMIS+lccB4coOAvoE9czQ8RyQ88vr' + b'KU3DJn41u2jYEcQa7MQAXoW1lNZhPRKUWCLeOKtG5NHNYKgP0c1gmo46FlSPy/g2D47Sl/' + b'F1HosrMDoZjSx67XZflZ7ROEQGWu8kaGm5Q2SwNH4O57ewNZw7RDSGIp9OHSYaYOUBCZkB' + b'8WauPONH0D8MqbSjmnSQOQ3kLc3IhOr1IuN1dLNO4bDvIboPmZCjdajaAkGDMkCsP2UWCt' + b'qTAW7pTiYpWnMyLiO9ySC3tCYjtNaZjEspSMMO+tLMkV5bMo6lSI0c8m5OY7JQK0PGtVeF' + b'HNEfN0bRnCa8RhnxXeR2tXlyMes5GaK9KLM/UuqylxqkuxqtXCYXubwMIYaFFUeEy8saDc' + b'hKS5VEz4HmyWWzDt1HkYIOt41VlpSzIZDd2yFCRH3b2CKQ3jMmxIJJ9HnAJBlzhQXRVmmA' + b'nQDpUkUjdxItS4DqpjAIKTeUQUptJmnI8C4xSH3tD8LR14lBd7i4C8qaif30V860M0uraC' + b'muvqCsbSwdhbi0mFxQtgIdX1DGHNeQzhDk3ZUdMmTUtxSVye3lYXjVt1Ogz7+EO8yQqZKZ' + b'6Ogu148YrzyoluQq43J08xOkj1RGlAVX4PytQcVK0eYS7QlTIJD2m2u3uqvJFe4vJ6Jb9x' + b'TxnJ/s7cyy9QQlJxdaMRt8u2eRvsgLPCTQiqMtbzQonsg2158tCk/ox4ebMeh1SBO44fgL' + b'HzAPc4jcn4bK8DI2xPeYO0kBEaL8ZQKsdT0v37+Mn8qGwnc1/E2L5Gr0m4+xaPBD3UAPtz' + b'ZW8GrldBXgq1czG5S7f5KY/qP7rCoPSCeA6HVvh6yRboXfusVaOjRZ0le1LgN4y+45wr3F' + b'cwRqW2cwbgWSJtdhaEwHkSZf2cWXyVfZSyvwrbfSLB0MlEjrW4or0NwsWJIRtgdyRZbFCA' + b'hLkgYMS5KWNKe4oAE3QgWt2GDaz2pC5G0IL7uhZ/sahhkEqXo9qEHRS88YW78q3XI+JTlS' + b'LRtiV5rlguhYsVwC1JkzA23ejeDuiu8TzAg6qRYCcBKrngabLCOOPo8yizjhjaI4LAfWAK' + b'Pbb9vkq5/LIE16WWMFt2iC+uEkNHcL+TrkaV1/iJ3WR31XPObpDvNNRADdTgBGHS+qoJ6r' + b'VxDImJjefGe8HTN1UjxTG602yf9isEoPOoB58lU6XVQlP/hVSGxQ+ZHjeiyeoeLogW01TV' + b'5ZyFXy6rsVJPl1re4snYHUhzdWoPXhDU1H8i7IkGBqUOM+tG49qAMkeFZ2uAWF+2ou1uME' + b'ncF+fbs9hCE169ewU8g4R89ImtBfw0uUYTV9GjNib3WZvKpnhpbJa2i5pSXETB3d8Ksaz2' + b'uSaosN85BX1dKhO73q3axZChq+OSbwFuo0RSqixkoHIV+Rnk7dmwrJvKZUwyFNFvTFkAaQ' + b'Rwox0CrAzWWAL2cOh07VHeOFmEn7HZ4qB2i/1278Cstk9T2mDmFqHaHb2huT/GJRRYi7NJ' + b'zn4LjlZSqRclw7x8PrwV+kY5yEk3g8kn7lRrOXls2kfS+IRX7tRrNTz+b94ryja7SmVX6H' + b'L4tRLs2G/m46Zjccab4LxPjzb+PxRl2H9jTYCAZcFhVnLgmnMw0Yy4mTWG0/lr48/7fFu/' + b'r7TiStLhnQF7+X0GLsQjNRFHpBfDYBrVuNoaWZQOaoW0ce6SXXWQZa+9Z0pNQhQwbzMMmM' + b'H5HdC1noSf1GUIY4pL9GeEbfTLmF/KrPysFV6L1RB98OZqK0Sjj3xHDzpxqB82Xypza3zp' + b'JgT4lZ1p+6F4LTqBdqkj+jEx3QCf7kBUpNm0SWjui4xawRmfynkrXNEz4EBD30bb3ehA57' + b'2ib6tnRouG8yM18mcnF6Rlz1ZFkSXaNuvOmlLNJ68JiC1uOGpqOByDAkmhTUfs3h1e+6Ut' + b'yroSn3oI7iCozqwgJcrdqXcB7Ko7ZEGCaq5E3P9JG8qIAsLdPgInlTCuB0TtLcCB+GsGUW' + b'wFg3ZF6Od4pXxvWtkbCMGaORcB5zxzvNqFgRf7TlDIXk7Xp7GlPwt6vdaegmb7eNKzD+vn' + b'3HuALV9e2WccXMBGa3LIezXTcJGYc6oSoi029MU5nncZsmokZbQ16dDq8ZwHG9RRN4Q9sM' + b'JhbzCI8fxjI8fXHZlBl5vLmCgwYHKDYETAUbH7VnVXasGGcFOPdhijKDDF55YIm4bYpmaj' + b'/9agumUm+91oGRC1rwgvxgdIhY+sMb+mmMFWzD8eYYhYi6G6RtMA9mm48wT1NkmJYZMEzL' + b'DBlNsTKH6PsyVk0KMaID4ag0QxC5Zji62deKjnqWkgypDSiwqzuvoe29XV163V6BUT+C/s' + b'g8VmLPJ6AgBt1PGmFVh2ZieJNttIxJfgtv72KWJkvgLMmX4alDIe9ZAryXaR5D+oJRlCtt' + b'4uZIpR+skDN6sIIoftrBShkGLiQhOvGNIC4qg9EJRAfAS0VHGVyQIVVpAup03z/pPrZxWD' + b'+c+8c+ejQDQxp4u/4MPUTDVYBv+ZqRPS7GwoNa7CswKkbGrroVdowX3XuwJ9Xj5HJF2i8Y' + b'r5JvHFvnyTd9WA36xjdZRCbPO2/wrS8cIK2MOmuSI6NOBnVt1FkZNBh1Gldjo04G16szXJ' + b'mhR0e4JgC1jSdD+qN7xIRbHVhFCRs0visQvfW39fEPtSnPGN/M2adlaT9D1xABoXNwcOge' + b'AGhtCSn1S+VVi28ZqWeWcCM1an0KwBp+8tO+sV4tzJcYVjraj9ezPPkWLeAgtpuWk2hS37' + b'pbJ6NRAaITtgg/OmFL+mh2rybmK2z/WFrtX5UG8FtSltJ7Sh4Jm0oWiXeVbLB6s8gi0W6R' + b'hfSukEXUzo8F9HkXi/jtHUuZZvT7wLfOqAusAngYDg7PJpNFwK0MwFD3ndEakhGdR0ShbD' + b'vdnOYEzKK/vko+I6oLj+HcLr3KcG4U3zL5Fh0rQwWOjpWRPgzqPnBUQW0lwoYRDYwQNToR' + b'A/fRiRjQ0s/D79gsABOib2GDDQmK7OEReGQPP0/+7a59v0z+H+SUGTTsMAEA' + )).decode().splitlines() + +%} diff --git a/fitz/helper-select.i b/fitz/helper-select.i new file mode 100644 index 0000000..44c8489 --- /dev/null +++ b/fitz/helper-select.i @@ -0,0 +1,393 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +//---------------------------------------------------------------------------- +// Helpers for document page selection - main logic was imported +// from pdf_clean_file.c. But instead of analyzing a string-based spec of +// selected pages, we accept a Python sequence. +//---------------------------------------------------------------------------- +typedef struct globals_s +{ + pdf_document *doc; + fz_context *ctx; +} globals; + +int string_in_names_list(fz_context *ctx, pdf_obj *p, pdf_obj *names_list) +{ + int n = pdf_array_len(ctx, names_list); + int i; + const char *str = pdf_to_text_string(ctx, p); + + for (i = 0; i < n ; i += 2) + { + if (!strcmp(pdf_to_text_string(ctx, pdf_array_get(ctx, names_list, i)), str)) + return 1; + } + return 0; +} + +//---------------------------------------------------------------------------- +// Recreate page tree to only retain specified pages. +//---------------------------------------------------------------------------- +void retainpage(fz_context *ctx, pdf_document *doc, pdf_obj *parent, pdf_obj *kids, int page) +{ + pdf_obj *pageref = pdf_lookup_page_obj(ctx, doc, page); + + pdf_flatten_inheritable_page_items(ctx, pageref); + + pdf_dict_put(ctx, pageref, PDF_NAME(Parent), parent); + + /* Store page object in new kids array */ + pdf_array_push(ctx, kids, pageref); +} + +int dest_is_valid_page(fz_context *ctx, pdf_obj *obj, int *page_object_nums, int pagecount) +{ + int i; + int num = pdf_to_num(ctx, obj); + + if (num == 0) + return 0; + for (i = 0; i < pagecount; i++) + { + if (page_object_nums[i] == num) + return 1; + } + return 0; +} + +int dest_is_valid(fz_context *ctx, pdf_obj *o, int page_count, int *page_object_nums, pdf_obj *names_list) +{ + pdf_obj *p; + + p = pdf_dict_get(ctx, o, PDF_NAME(A)); + if (pdf_name_eq(ctx, pdf_dict_get(ctx, p, PDF_NAME(S)), PDF_NAME(GoTo)) && + !string_in_names_list(ctx, pdf_dict_get(ctx, p, PDF_NAME(D)), names_list)) + return 0; + + p = pdf_dict_get(ctx, o, PDF_NAME(Dest)); + if (p == NULL) + {} + else if (pdf_is_string(ctx, p)) + { + return string_in_names_list(ctx, p, names_list); + } + else if (!dest_is_valid_page(ctx, pdf_array_get(ctx, p, 0), page_object_nums, page_count)) + return 0; + + return 1; +} + +int strip_outlines(fz_context *ctx, pdf_document *doc, pdf_obj *outlines, int page_count, int *page_object_nums, pdf_obj *names_list); + +int strip_outline(fz_context *ctx, pdf_document *doc, pdf_obj *outlines, int page_count, int *page_object_nums, pdf_obj *names_list, pdf_obj **pfirst, pdf_obj **plast) +{ + pdf_obj *prev = NULL; + pdf_obj *first = NULL; + pdf_obj *current; + int count = 0; + + for (current = outlines; current != NULL; ) + { + int nc; + + /*********************************************************************/ + // Strip any children to start with. This takes care of + // First / Last / Count for us. + /*********************************************************************/ + nc = strip_outlines(ctx, doc, current, page_count, page_object_nums, names_list); + + if (!dest_is_valid(ctx, current, page_count, page_object_nums, names_list)) + { + if (nc == 0) + { + /*************************************************************/ + // Outline with invalid dest and no children. Drop it by + // pulling the next one in here. + /*************************************************************/ + pdf_obj *next = pdf_dict_get(ctx, current, PDF_NAME(Next)); + if (next == NULL) + { + // There is no next one to pull in + if (prev != NULL) + pdf_dict_del(ctx, prev, PDF_NAME(Next)); + } + else if (prev != NULL) + { + pdf_dict_put(ctx, prev, PDF_NAME(Next), next); + pdf_dict_put(ctx, next, PDF_NAME(Prev), prev); + } + else + { + pdf_dict_del(ctx, next, PDF_NAME(Prev)); + } + current = next; + } + else + { + // Outline with invalid dest, but children. Just drop the dest. + pdf_dict_del(ctx, current, PDF_NAME(Dest)); + pdf_dict_del(ctx, current, PDF_NAME(A)); + current = pdf_dict_get(ctx, current, PDF_NAME(Next)); + } + } + else + { + // Keep this one + if (first == NULL) + first = current; + prev = current; + current = pdf_dict_get(ctx, current, PDF_NAME(Next)); + count++; + } + } + + *pfirst = first; + *plast = prev; + + return count; +} + +int strip_outlines(fz_context *ctx, pdf_document *doc, pdf_obj *outlines, int page_count, int *page_object_nums, pdf_obj *names_list) +{ + int nc; + pdf_obj *first; + pdf_obj *last; + + if (outlines == NULL) + return 0; + + first = pdf_dict_get(ctx, outlines, PDF_NAME(First)); + if (first == NULL) + nc = 0; + else + nc = strip_outline(ctx, doc, first, page_count, page_object_nums, + names_list, &first, &last); + + if (nc == 0) + { + pdf_dict_del(ctx, outlines, PDF_NAME(First)); + pdf_dict_del(ctx, outlines, PDF_NAME(Last)); + pdf_dict_del(ctx, outlines, PDF_NAME(Count)); + } + else + { + int old_count = pdf_to_int(ctx, pdf_dict_get(ctx, outlines, PDF_NAME(Count))); + pdf_dict_put(ctx, outlines, PDF_NAME(First), first); + pdf_dict_put(ctx, outlines, PDF_NAME(Last), last); + pdf_dict_put_drop(ctx, outlines, PDF_NAME(Count), pdf_new_int(ctx, old_count > 0 ? nc : -nc)); + } + return nc; +} + +//---------------------------------------------------------------------------- +// This is called by PyMuPDF: +// liste = page numbers to retain +//---------------------------------------------------------------------------- +void retainpages(fz_context *ctx, globals *glo, PyObject *liste) +{ + pdf_obj *oldroot, *root, *pages, *kids, *countobj, *olddests; + Py_ssize_t argc = PySequence_Size(liste); + pdf_document *doc = glo->doc; + pdf_obj *names_list = NULL; + pdf_obj *outlines; + pdf_obj *ocproperties; + int pagecount = pdf_count_pages(ctx, doc); + + int i; + int *page_object_nums; + +/******************************************************************************/ +// Keep only pages/type and (reduced) dest entries to avoid +// references to dropped pages +/******************************************************************************/ + oldroot = pdf_dict_get(ctx, pdf_trailer(ctx, doc), PDF_NAME(Root)); + pages = pdf_dict_get(ctx, oldroot, PDF_NAME(Pages)); + olddests = pdf_load_name_tree(ctx, doc, PDF_NAME(Dests)); + outlines = pdf_dict_get(ctx, oldroot, PDF_NAME(Outlines)); + ocproperties = pdf_dict_get(ctx, oldroot, PDF_NAME(OCProperties)); + + root = pdf_new_dict(ctx, doc, 3); + pdf_dict_put(ctx, root, PDF_NAME(Type), pdf_dict_get(ctx, oldroot, PDF_NAME(Type))); + pdf_dict_put(ctx, root, PDF_NAME(Pages), pdf_dict_get(ctx, oldroot, PDF_NAME(Pages))); + if (outlines) + pdf_dict_put(ctx, root, PDF_NAME(Outlines), outlines); + if (ocproperties) + pdf_dict_put(ctx, root, PDF_NAME(OCProperties), ocproperties); + + pdf_update_object(ctx, doc, pdf_to_num(ctx, oldroot), root); + + // Create a new kids array with only the pages we want to keep + kids = pdf_new_array(ctx, doc, 1); + + // Retain pages specified + Py_ssize_t page; + fz_try(ctx) { + for (page = 0; page < argc; page++) { + i = (int) PyInt_AsLong(PySequence_ITEM(liste, page)); + if (i < 0 || i >= pagecount) { + RAISEPY(ctx, MSG_BAD_PAGENO, PyExc_ValueError); + } + retainpage(ctx, doc, pages, kids, i); + } + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + + // Update page count and kids array + countobj = pdf_new_int(ctx, pdf_array_len(ctx, kids)); + pdf_dict_put_drop(ctx, pages, PDF_NAME(Count), countobj); + pdf_dict_put_drop(ctx, pages, PDF_NAME(Kids), kids); + + pagecount = pdf_count_pages(ctx, doc); + page_object_nums = fz_calloc(ctx, pagecount, sizeof(*page_object_nums)); + for (i = 0; i < pagecount; i++) + { + pdf_obj *pageref = pdf_lookup_page_obj(ctx, doc, i); + page_object_nums[i] = pdf_to_num(ctx, pageref); + } + +/******************************************************************************/ +// If we had an old Dests tree (now reformed as an olddests dictionary), +// keep any entries in there that point to valid pages. +// This may mean we keep more than we need, but it is safe at least. +/******************************************************************************/ + if (olddests) + { + pdf_obj *names = pdf_new_dict(ctx, doc, 1); + pdf_obj *dests = pdf_new_dict(ctx, doc, 1); + int len = pdf_dict_len(ctx, olddests); + + names_list = pdf_new_array(ctx, doc, 32); + + for (i = 0; i < len; i++) + { + pdf_obj *key = pdf_dict_get_key(ctx, olddests, i); + pdf_obj *val = pdf_dict_get_val(ctx, olddests, i); + pdf_obj *dest = pdf_dict_get(ctx, val, PDF_NAME(D)); + + dest = pdf_array_get(ctx, dest ? dest : val, 0); + if (dest_is_valid_page(ctx, dest, page_object_nums, pagecount)) + { + pdf_obj *key_str = pdf_new_string(ctx, pdf_to_name(ctx, key), strlen(pdf_to_name(ctx, key))); + pdf_array_push_drop(ctx, names_list, key_str); + pdf_array_push(ctx, names_list, val); + } + } + + pdf_dict_put(ctx, dests, PDF_NAME(Names), names_list); + pdf_dict_put(ctx, names, PDF_NAME(Dests), dests); + pdf_dict_put(ctx, root, PDF_NAME(Names), names); + + pdf_drop_obj(ctx, names); + pdf_drop_obj(ctx, dests); + pdf_drop_obj(ctx, olddests); + } + +/*****************************************************************************/ +// Edit each pages /Annot list to remove any links pointing to nowhere. +/*****************************************************************************/ + for (i = 0; i < pagecount; i++) + { + pdf_obj *pageref = pdf_lookup_page_obj(ctx, doc, i); + + pdf_obj *annots = pdf_dict_get(ctx, pageref, PDF_NAME(Annots)); + + int len = pdf_array_len(ctx, annots); + int j; + + for (j = 0; j < len; j++) + { + pdf_obj *o = pdf_array_get(ctx, annots, j); + + if (!pdf_name_eq(ctx, pdf_dict_get(ctx, o, PDF_NAME(Subtype)), PDF_NAME(Link))) + continue; + + if (!dest_is_valid(ctx, o, pagecount, page_object_nums, names_list)) + { + // Remove this annotation + pdf_array_delete(ctx, annots, j); + len--; + j--; + } + } + } + + if (strip_outlines(ctx, doc, outlines, pagecount, page_object_nums, names_list) == 0) + { + pdf_dict_del(ctx, root, PDF_NAME(Outlines)); + } + + fz_free(ctx, page_object_nums); + pdf_drop_obj(ctx, names_list); + pdf_drop_obj(ctx, root); +} + +void remove_dest_range(fz_context *ctx, pdf_document *pdf, PyObject *numbers) +{ + fz_try(ctx) { + int i, j, pno, len, pagecount = pdf_count_pages(ctx, pdf); + PyObject *n1 = NULL; + pdf_obj *target, *annots, *pageref, *o, *action, *dest; + for (i = 0; i < pagecount; i++) { + n1 = PyLong_FromLong((long) i); + if (PySet_Contains(numbers, n1)) { + Py_DECREF(n1); + continue; + } + Py_DECREF(n1); + + pageref = pdf_lookup_page_obj(ctx, pdf, i); + annots = pdf_dict_get(ctx, pageref, PDF_NAME(Annots)); + if (!annots) continue; + len = pdf_array_len(ctx, annots); + for (j = len - 1; j >= 0; j -= 1) { + o = pdf_array_get(ctx, annots, j); + if (!pdf_name_eq(ctx, pdf_dict_get(ctx, o, PDF_NAME(Subtype)), PDF_NAME(Link))) { + continue; + } + action = pdf_dict_get(ctx, o, PDF_NAME(A)); + dest = pdf_dict_get(ctx, o, PDF_NAME(Dest)); + if (action) { + if (!pdf_name_eq(ctx, pdf_dict_get(ctx, action, + PDF_NAME(S)), PDF_NAME(GoTo))) + continue; + dest = pdf_dict_get(ctx, action, PDF_NAME(D)); + } + pno = -1; + if (pdf_is_array(ctx, dest)) { + target = pdf_array_get(ctx, dest, 0); + pno = pdf_lookup_page_number(ctx, pdf, target); + } + else if (pdf_is_string(ctx, dest)) { + pno = pdf_lookup_anchor(ctx, pdf, + pdf_to_text_string(ctx, dest), + NULL, NULL); + } + if (pno < 0) { // page number lookup did not work + continue; + } + n1 = PyLong_FromLong((long) pno); + if (PySet_Contains(numbers, n1)) { + pdf_array_delete(ctx, annots, j); + } + Py_DECREF(n1); + } + } + } + + fz_catch(ctx) { + fz_rethrow(ctx); + } + return; +} +%} diff --git a/fitz/helper-stext.i b/fitz/helper-stext.i new file mode 100644 index 0000000..d96bb76 --- /dev/null +++ b/fitz/helper-stext.i @@ -0,0 +1,1030 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +// need own versions of ascender / descender +static const float +JM_font_ascender(fz_context *ctx, fz_font *font) +{ + if (skip_quad_corrections) { + return 0.8f; + } + return fz_font_ascender(ctx, font); +} + +static const float +JM_font_descender(fz_context *ctx, fz_font *font) +{ + if (skip_quad_corrections) { + return -0.2f; + } + return fz_font_descender(ctx, font); +} + + +/* inactive +//----------------------------------------------------------------------------- +// Make OCR text page directly from an fz_page +//----------------------------------------------------------------------------- +fz_stext_page * +JM_new_stext_page_ocr_from_page(fz_context *ctx, fz_page *page, fz_rect rect, int flags, + const char *lang, const char *tessdata) +{ + if (!page) return NULL; + int with_list = 1; + fz_stext_page *tp = NULL; + fz_device *dev = NULL, *ocr_dev = NULL; + fz_var(dev); + fz_var(ocr_dev); + fz_var(tp); + fz_stext_options options; + memset(&options, 0, sizeof options); + options.flags = flags; + //fz_matrix ctm = fz_identity; + fz_matrix ctm1 = fz_make_matrix(100/72, 0, 0, 100/72, 0, 0); + fz_matrix ctm2 = fz_make_matrix(400/72, 0, 0, 400/72, 0, 0); + + fz_try(ctx) { + tp = fz_new_stext_page(ctx, rect); + dev = fz_new_stext_device(ctx, tp, &options); + ocr_dev = fz_new_ocr_device(ctx, dev, fz_identity, rect, with_list, lang, tessdata, NULL); + fz_run_page(ctx, page, ocr_dev, fz_identity, NULL); + fz_close_device(ctx, ocr_dev); + fz_close_device(ctx, dev); + } + fz_always(ctx) { + fz_drop_device(ctx, dev); + fz_drop_device(ctx, ocr_dev); + } + fz_catch(ctx) { + fz_drop_stext_page(ctx, tp); + fz_rethrow(ctx); + } + return tp; +} +*/ + +//--------------------------------------------------------------------------- +// APPEND non-ascii runes in unicode escape format to fz_buffer +//--------------------------------------------------------------------------- +void JM_append_rune(fz_context *ctx, fz_buffer *buff, int ch) +{ + if ((ch >= 32 && ch <= 255) || ch == 10) { + fz_append_byte(ctx, buff, ch); + } else if (ch <= 0xffff) { // 4 hex digits + fz_append_printf(ctx, buff, "\\u%04x", ch); + } else { // 8 hex digits + fz_append_printf(ctx, buff, "\\U%08x", ch); + } +} + + +// re-compute char quad if ascender/descender values make no sense +static fz_quad +JM_char_quad(fz_context *ctx, fz_stext_line *line, fz_stext_char *ch) +{ + if (skip_quad_corrections) { // no special handling + return ch->quad; + } + if (line->wmode) { // never touch vertical write mode + return ch->quad; + } + fz_font *font = ch->font; + float asc = JM_font_ascender(ctx, font); + float dsc = JM_font_descender(ctx, font); + float c, s, fsize = ch->size; + float asc_dsc = asc - dsc + FLT_EPSILON; + if (asc_dsc >= 1 && small_glyph_heights == 0) { // no problem + return ch->quad; + } + if (asc < 1e-3) { // probably Tesseract glyphless font + dsc = -0.1f; + asc = 0.9f; + asc_dsc = 1.0f; + } + + if (small_glyph_heights || asc_dsc < 1) { + dsc = dsc / asc_dsc; + asc = asc / asc_dsc; + } + asc_dsc = asc - dsc; + asc = asc * fsize / asc_dsc; + dsc = dsc * fsize / asc_dsc; + + /* ------------------------------ + Re-compute quad with the adjusted ascender / descender values: + Move ch->origin to (0,0) and de-rotate quad, then adjust the corners, + re-rotate and move back to ch->origin location. + ------------------------------ */ + fz_matrix trm1, trm2, xlate1, xlate2; + fz_quad quad; + c = line->dir.x; // cosine + s = line->dir.y; // sine + trm1 = fz_make_matrix(c, -s, s, c, 0, 0); // derotate + trm2 = fz_make_matrix(c, s, -s, c, 0, 0); // rotate + if (c == -1) { // left-right flip + trm1.d = 1; + trm2.d = 1; + } + xlate1 = fz_make_matrix(1, 0, 0, 1, -ch->origin.x, -ch->origin.y); + xlate2 = fz_make_matrix(1, 0, 0, 1, ch->origin.x, ch->origin.y); + + quad = fz_transform_quad(ch->quad, xlate1); // move origin to (0,0) + quad = fz_transform_quad(quad, trm1); // de-rotate corners + + // adjust vertical coordinates + if (c == 1 && quad.ul.y > 0) { // up-down flip + quad.ul.y = asc; + quad.ur.y = asc; + quad.ll.y = dsc; + quad.lr.y = dsc; + } else { + quad.ul.y = -asc; + quad.ur.y = -asc; + quad.ll.y = -dsc; + quad.lr.y = -dsc; + } + + // adjust horizontal coordinates that are too crazy: + // (1) left x must be >= 0 + // (2) if bbox width is 0, lookup char advance in font. + if (quad.ll.x < 0) { + quad.ll.x = 0; + quad.ul.x = 0; + } + float cwidth = quad.lr.x - quad.ll.x; + if (cwidth < FLT_EPSILON) { + int glyph = fz_encode_character(ctx, font, ch->c); + if (glyph) { + float fwidth = fz_advance_glyph(ctx, font, glyph, line->wmode); + quad.lr.x = quad.ll.x + fwidth * fsize; + quad.ur.x = quad.lr.x; + } + } + + quad = fz_transform_quad(quad, trm2); // rotate back + quad = fz_transform_quad(quad, xlate2); // translate back + return quad; +} + + +// return rect of char quad +static fz_rect +JM_char_bbox(fz_context *ctx, fz_stext_line *line, fz_stext_char *ch) +{ + fz_rect r = fz_rect_from_quad(JM_char_quad(ctx, line, ch)); + if (!line->wmode) { + return r; + } + if (r.y1 < r.y0 + ch->size) { + r.y0 = r.y1 - ch->size; + } + return r; +} + + +//------------------------------------------- +// make a buffer from an stext_page's text +//------------------------------------------- +fz_buffer * +JM_new_buffer_from_stext_page(fz_context *ctx, fz_stext_page *page) +{ + fz_stext_block *block; + fz_stext_line *line; + fz_stext_char *ch; + fz_rect rect = page->mediabox; + fz_buffer *buf = NULL; + + fz_try(ctx) + { + buf = fz_new_buffer(ctx, 256); + for (block = page->first_block; block; block = block->next) { + if (block->type == FZ_STEXT_BLOCK_TEXT) { + for (line = block->u.t.first_line; line; line = line->next) { + for (ch = line->first_char; ch; ch = ch->next) { + if (!JM_rects_overlap(rect, JM_char_bbox(ctx, line, ch)) && + !fz_is_infinite_rect(rect)) { + continue; + } + fz_append_rune(ctx, buf, ch->c); + } + fz_append_byte(ctx, buf, '\n'); + } + fz_append_byte(ctx, buf, '\n'); + } + } + } + fz_catch(ctx) { + fz_drop_buffer(ctx, buf); + fz_rethrow(ctx); + } + return buf; +} + + +static float hdist(fz_point *dir, fz_point *a, fz_point *b) +{ + float dx = b->x - a->x; + float dy = b->y - a->y; + return fz_abs(dx * dir->x + dy * dir->y); +} + + +static float vdist(fz_point *dir, fz_point *a, fz_point *b) +{ + float dx = b->x - a->x; + float dy = b->y - a->y; + return fz_abs(dx * dir->y + dy * dir->x); +} + + +struct highlight +{ + Py_ssize_t len; + PyObject *quads; + float hfuzz, vfuzz; +}; + + +static void on_highlight_char(fz_context *ctx, void *arg, fz_stext_line *line, fz_stext_char *ch) +{ + struct highlight *hits = arg; + float vfuzz = ch->size * hits->vfuzz; + float hfuzz = ch->size * hits->hfuzz; + fz_quad ch_quad = JM_char_quad(ctx, line, ch); + if (hits->len > 0) { + PyObject *quad = PySequence_ITEM(hits->quads, hits->len - 1); + fz_quad end = JM_quad_from_py(quad); + Py_DECREF(quad); + if (hdist(&line->dir, &end.lr, &ch_quad.ll) < hfuzz + && vdist(&line->dir, &end.lr, &ch_quad.ll) < vfuzz + && hdist(&line->dir, &end.ur, &ch_quad.ul) < hfuzz + && vdist(&line->dir, &end.ur, &ch_quad.ul) < vfuzz) + { + end.ur = ch_quad.ur; + end.lr = ch_quad.lr; + quad = JM_py_from_quad(end); + PyList_SetItem(hits->quads, hits->len - 1, quad); + return; + } + } + LIST_APPEND_DROP(hits->quads, JM_py_from_quad(ch_quad)); + hits->len++; +} + + +static inline int canon(int c) +{ + /* TODO: proper unicode case folding */ + /* TODO: character equivalence (a matches ä, etc) */ + if (c == 0xA0 || c == 0x2028 || c == 0x2029) + return ' '; + if (c == '\r' || c == '\n' || c == '\t') + return ' '; + if (c >= 'A' && c <= 'Z') + return c - 'A' + 'a'; + return c; +} + + +static inline int chartocanon(int *c, const char *s) +{ + int n = fz_chartorune(c, s); + *c = canon(*c); + return n; +} + + +static const char *match_string(const char *h, const char *n) +{ + int hc, nc; + const char *e = h; + h += chartocanon(&hc, h); + n += chartocanon(&nc, n); + while (hc == nc) + { + e = h; + if (hc == ' ') + do + h += chartocanon(&hc, h); + while (hc == ' '); + else + h += chartocanon(&hc, h); + if (nc == ' ') + do + n += chartocanon(&nc, n); + while (nc == ' '); + else + n += chartocanon(&nc, n); + } + return nc == 0 ? e : NULL; +} + + +static const char *find_string(const char *s, const char *needle, const char **endp) +{ + const char *end; + while (*s) + { + end = match_string(s, needle); + if (end) + return *endp = end, s; + ++s; + } + return *endp = NULL, NULL; +} + + +PyObject * +JM_search_stext_page(fz_context *ctx, fz_stext_page *page, const char *needle) +{ + struct highlight hits; + fz_stext_block *block; + fz_stext_line *line; + fz_stext_char *ch; + fz_buffer *buffer = NULL; + const char *haystack, *begin, *end; + fz_rect rect = page->mediabox; + int c, inside; + + if (strlen(needle) == 0) Py_RETURN_NONE; + PyObject *quads = PyList_New(0); + hits.len = 0; + hits.quads = quads; + hits.hfuzz = 0.2f; /* merge kerns but not large gaps */ + hits.vfuzz = 0.1f; + + fz_try(ctx) { + buffer = JM_new_buffer_from_stext_page(ctx, page); + haystack = fz_string_from_buffer(ctx, buffer); + begin = find_string(haystack, needle, &end); + if (!begin) goto no_more_matches; + + inside = 0; + for (block = page->first_block; block; block = block->next) { + if (block->type != FZ_STEXT_BLOCK_TEXT) { + continue; + } + for (line = block->u.t.first_line; line; line = line->next) { + for (ch = line->first_char; ch; ch = ch->next) { + if (!fz_is_infinite_rect(rect) && + !JM_rects_overlap(rect, JM_char_bbox(ctx, line, ch))) { + goto next_char; + } +try_new_match: + if (!inside) { + if (haystack >= begin) inside = 1; + } + if (inside) { + if (haystack < end) { + on_highlight_char(ctx, &hits, line, ch); + } else { + inside = 0; + begin = find_string(haystack, needle, &end); + if (!begin) goto no_more_matches; + else goto try_new_match; + } + } + haystack += fz_chartorune(&c, haystack); +next_char:; + } + assert(*haystack == '\n'); + ++haystack; + } + assert(*haystack == '\n'); + ++haystack; + } +no_more_matches:; + } + fz_always(ctx) + fz_drop_buffer(ctx, buffer); + fz_catch(ctx) + fz_rethrow(ctx); + + return quads; +} + + +//----------------------------------------------------------------------------- +// Plain text output. An identical copy of fz_print_stext_page_as_text, +// but lines within a block are concatenated by space instead a new-line +// character (which else leads to 2 new-lines). +//----------------------------------------------------------------------------- +void +JM_print_stext_page_as_text(fz_context *ctx, fz_output *out, fz_stext_page *page) +{ + fz_stext_block *block; + fz_stext_line *line; + fz_stext_char *ch; + fz_rect rect = page->mediabox; + fz_rect chbbox; + int last_char = 0; + char utf[10]; + int i, n; + + for (block = page->first_block; block; block = block->next) { + if (block->type == FZ_STEXT_BLOCK_TEXT) { + for (line = block->u.t.first_line; line; line = line->next) { + last_char = 0; + for (ch = line->first_char; ch; ch = ch->next) { + chbbox = JM_char_bbox(ctx, line, ch); + if (fz_is_infinite_rect(rect) || + JM_rects_overlap(rect, chbbox)) { + last_char = ch->c; + n = fz_runetochar(utf, ch->c); + for (i = 0; i < n; i++) { + fz_write_byte(ctx, out, utf[i]); + } + } + } + if (last_char != 10 && last_char > 0) { + fz_write_string(ctx, out, "\n"); + } + } + } + } +} + +//----------------------------------------------------------------------------- +// Functions for wordlist output +//----------------------------------------------------------------------------- +int JM_append_word(fz_context *ctx, PyObject *lines, fz_buffer *buff, fz_rect *wbbox, + int block_n, int line_n, int word_n) +{ + PyObject *s = JM_EscapeStrFromBuffer(ctx, buff); + PyObject *litem = Py_BuildValue("ffffOiii", + wbbox->x0, + wbbox->y0, + wbbox->x1, + wbbox->y1, + s, + block_n, line_n, word_n); + LIST_APPEND_DROP(lines, litem); + Py_DECREF(s); + *wbbox = fz_empty_rect; + return word_n + 1; // word counter +} + +//----------------------------------------------------------------------------- +// Functions for dictionary output +//----------------------------------------------------------------------------- + +static int detect_super_script(fz_stext_line *line, fz_stext_char *ch) +{ + if (line->wmode == 0 && line->dir.x == 1 && line->dir.y == 0) + return ch->origin.y < line->first_char->origin.y - ch->size * 0.1f; + return 0; +} + +static int JM_char_font_flags(fz_context *ctx, fz_font *font, fz_stext_line *line, fz_stext_char *ch) +{ + int flags = detect_super_script(line, ch); + flags += fz_font_is_italic(ctx, font) * TEXT_FONT_ITALIC; + flags += fz_font_is_serif(ctx, font) * TEXT_FONT_SERIFED; + flags += fz_font_is_monospaced(ctx, font) * TEXT_FONT_MONOSPACED; + flags += fz_font_is_bold(ctx, font) * TEXT_FONT_BOLD; + return flags; +} + +static const char * +JM_font_name(fz_context *ctx, fz_font *font) +{ + const char *name = fz_font_name(ctx, font); + const char *s = strchr(name, '+'); + if (subset_fontnames || s == NULL || s-name != 6) { + return name; + } + return s + 1; +} + + +static fz_rect +JM_make_spanlist(fz_context *ctx, PyObject *line_dict, + fz_stext_line *line, int raw, fz_buffer *buff, + fz_rect tp_rect) +{ + PyObject *span = NULL, *char_list = NULL, *char_dict; + PyObject *span_list = PyList_New(0); + fz_clear_buffer(ctx, buff); + fz_stext_char *ch; + fz_rect span_rect = fz_empty_rect; + fz_rect line_rect = fz_empty_rect; + fz_point span_origin = {0, 0}; + typedef struct style_s { + float size; int flags; const char *font; int color; + float asc; float desc; + } char_style; + char_style old_style = { -1, -1, "", -1, 0, 0 }, style; + + for (ch = line->first_char; ch; ch = ch->next) { + fz_rect r = JM_char_bbox(ctx, line, ch); + if (!JM_rects_overlap(tp_rect, r) && + !fz_is_infinite_rect(tp_rect)) { + continue; + } + int flags = JM_char_font_flags(ctx, ch->font, line, ch); + fz_point origin = ch->origin; + style.size = ch->size; + style.flags = flags; + style.font = JM_font_name(ctx, ch->font); + style.color = ch->color; + style.asc = JM_font_ascender(ctx, ch->font); + style.desc = JM_font_descender(ctx, ch->font); + + if (style.size != old_style.size || + style.flags != old_style.flags || + style.color != old_style.color || + strcmp(style.font, old_style.font) != 0) { + + if (old_style.size >= 0) { + // not first one, output previous + if (raw) { + // put character list in the span + DICT_SETITEM_DROP(span, dictkey_chars, char_list); + char_list = NULL; + } else { + // put text string in the span + DICT_SETITEM_DROP(span, dictkey_text, JM_EscapeStrFromBuffer(ctx, buff)); + fz_clear_buffer(ctx, buff); + } + + DICT_SETITEM_DROP(span, dictkey_origin, + JM_py_from_point(span_origin)); + DICT_SETITEM_DROP(span, dictkey_bbox, + JM_py_from_rect(span_rect)); + line_rect = fz_union_rect(line_rect, span_rect); + LIST_APPEND_DROP(span_list, span); + span = NULL; + } + + span = PyDict_New(); + float asc = style.asc, desc = style.desc; + if (style.asc < 1e-3) { + asc = 0.9f; + desc = -0.1f; + } + + DICT_SETITEM_DROP(span, dictkey_size, Py_BuildValue("f", style.size)); + DICT_SETITEM_DROP(span, dictkey_flags, Py_BuildValue("i", style.flags)); + DICT_SETITEM_DROP(span, dictkey_font, JM_EscapeStrFromStr(style.font)); + DICT_SETITEM_DROP(span, dictkey_color, Py_BuildValue("i", style.color)); + DICT_SETITEMSTR_DROP(span, "ascender", Py_BuildValue("f", asc)); + DICT_SETITEMSTR_DROP(span, "descender", Py_BuildValue("f", desc)); + + old_style = style; + span_rect = r; + span_origin = origin; + + } + span_rect = fz_union_rect(span_rect, r); + + if (raw) { // make and append a char dict + char_dict = PyDict_New(); + DICT_SETITEM_DROP(char_dict, dictkey_origin, + JM_py_from_point(ch->origin)); + + DICT_SETITEM_DROP(char_dict, dictkey_bbox, + JM_py_from_rect(r)); + + DICT_SETITEM_DROP(char_dict, dictkey_c, + Py_BuildValue("C", ch->c)); + + if (!char_list) { + char_list = PyList_New(0); + } + LIST_APPEND_DROP(char_list, char_dict); + } else { // add character byte to buffer + JM_append_rune(ctx, buff, ch->c); + } + } + // all characters processed, now flush remaining span + if (span) { + if (raw) { + DICT_SETITEM_DROP(span, dictkey_chars, char_list); + char_list = NULL; + } else { + DICT_SETITEM_DROP(span, dictkey_text, JM_EscapeStrFromBuffer(ctx, buff)); + fz_clear_buffer(ctx, buff); + } + DICT_SETITEM_DROP(span, dictkey_origin, JM_py_from_point(span_origin)); + DICT_SETITEM_DROP(span, dictkey_bbox, JM_py_from_rect(span_rect)); + + if (!fz_is_empty_rect(span_rect)) { + LIST_APPEND_DROP(span_list, span); + line_rect = fz_union_rect(line_rect, span_rect); + } else { + Py_DECREF(span); + } + span = NULL; + } + if (!fz_is_empty_rect(line_rect)) { + DICT_SETITEM_DROP(line_dict, dictkey_spans, span_list); + } else { + DICT_SETITEM_DROP(line_dict, dictkey_spans, span_list); + } + return line_rect; +} + +static void JM_make_image_block(fz_context *ctx, fz_stext_block *block, PyObject *block_dict) +{ + fz_image *image = block->u.i.image; + fz_buffer *buf = NULL, *freebuf = NULL; + fz_compressed_buffer *buffer = fz_compressed_image_buffer(ctx, image); + fz_var(buf); + fz_var(freebuf); + int n = fz_colorspace_n(ctx, image->colorspace); + int w = image->w; + int h = image->h; + const char *ext = NULL; + int type = FZ_IMAGE_UNKNOWN; + if (buffer) + type = buffer->params.type; + if (type < FZ_IMAGE_BMP || type == FZ_IMAGE_JBIG2) + type = FZ_IMAGE_UNKNOWN; + PyObject *bytes = NULL; + fz_var(bytes); + fz_try(ctx) { + if (buffer && type != FZ_IMAGE_UNKNOWN) { + buf = buffer->buffer; + ext = JM_image_extension(type); + } else { + buf = freebuf = fz_new_buffer_from_image_as_png(ctx, image, fz_default_color_params); + ext = "png"; + } + bytes = JM_BinFromBuffer(ctx, buf); + } + fz_always(ctx) { + if (!bytes) + bytes = JM_BinFromChar(""); + DICT_SETITEM_DROP(block_dict, dictkey_width, + Py_BuildValue("i", w)); + DICT_SETITEM_DROP(block_dict, dictkey_height, + Py_BuildValue("i", h)); + DICT_SETITEM_DROP(block_dict, dictkey_ext, + Py_BuildValue("s", ext)); + DICT_SETITEM_DROP(block_dict, dictkey_colorspace, + Py_BuildValue("i", n)); + DICT_SETITEM_DROP(block_dict, dictkey_xres, + Py_BuildValue("i", image->xres)); + DICT_SETITEM_DROP(block_dict, dictkey_yres, + Py_BuildValue("i", image->xres)); + DICT_SETITEM_DROP(block_dict, dictkey_bpc, + Py_BuildValue("i", (int) image->bpc)); + DICT_SETITEM_DROP(block_dict, dictkey_matrix, + JM_py_from_matrix(block->u.i.transform)); + DICT_SETITEM_DROP(block_dict, dictkey_size, + Py_BuildValue("n", (Py_ssize_t) fz_image_size(ctx, image))); + DICT_SETITEM_DROP(block_dict, dictkey_image, bytes); + + fz_drop_buffer(ctx, freebuf); + } + fz_catch(ctx) {;} + return; +} + +static void JM_make_text_block(fz_context *ctx, fz_stext_block *block, PyObject *block_dict, int raw, fz_buffer *buff, fz_rect tp_rect) +{ + fz_stext_line *line; + PyObject *line_list = PyList_New(0), *line_dict; + fz_rect block_rect = fz_empty_rect; + for (line = block->u.t.first_line; line; line = line->next) { + if (fz_is_empty_rect(fz_intersect_rect(tp_rect, line->bbox)) && + !fz_is_infinite_rect(tp_rect)) { + continue; + } + line_dict = PyDict_New(); + fz_rect line_rect = JM_make_spanlist(ctx, line_dict, line, raw, buff, tp_rect); + block_rect = fz_union_rect(block_rect, line_rect); + DICT_SETITEM_DROP(line_dict, dictkey_wmode, + Py_BuildValue("i", line->wmode)); + DICT_SETITEM_DROP(line_dict, dictkey_dir, JM_py_from_point(line->dir)); + DICT_SETITEM_DROP(line_dict, dictkey_bbox, + JM_py_from_rect(line_rect)); + LIST_APPEND_DROP(line_list, line_dict); + } + DICT_SETITEM_DROP(block_dict, dictkey_bbox, JM_py_from_rect(block_rect)); + DICT_SETITEM_DROP(block_dict, dictkey_lines, line_list); + return; +} + +void JM_make_textpage_dict(fz_context *ctx, fz_stext_page *tp, PyObject *page_dict, int raw) +{ + fz_stext_block *block; + fz_buffer *text_buffer = fz_new_buffer(ctx, 128); + PyObject *block_dict, *block_list = PyList_New(0); + fz_rect tp_rect = tp->mediabox; + int block_n = -1; + for (block = tp->first_block; block; block = block->next) { + block_n++; + if (!fz_contains_rect(tp_rect, block->bbox) && + !fz_is_infinite_rect(tp_rect) && + block->type == FZ_STEXT_BLOCK_IMAGE) { + continue; + } + if (!fz_is_infinite_rect(tp_rect) && + fz_is_empty_rect(fz_intersect_rect(tp_rect, block->bbox))) { + continue; + } + + block_dict = PyDict_New(); + DICT_SETITEM_DROP(block_dict, dictkey_number, Py_BuildValue("i", block_n)); + DICT_SETITEM_DROP(block_dict, dictkey_type, Py_BuildValue("i", block->type)); + if (block->type == FZ_STEXT_BLOCK_IMAGE) { + DICT_SETITEM_DROP(block_dict, dictkey_bbox, JM_py_from_rect(block->bbox)); + JM_make_image_block(ctx, block, block_dict); + } else { + JM_make_text_block(ctx, block, block_dict, raw, text_buffer, tp_rect); + } + + LIST_APPEND_DROP(block_list, block_dict); + } + DICT_SETITEM_DROP(page_dict, dictkey_blocks, block_list); + fz_drop_buffer(ctx, text_buffer); +} + + + +//--------------------------------------------------------------------- +char * +JM_copy_rectangle(fz_context *ctx, fz_stext_page *page, fz_rect area) +{ + fz_stext_block *block; + fz_stext_line *line; + fz_stext_char *ch; + fz_buffer *buffer; + unsigned char *s; + int need_new_line = 0; + + buffer = fz_new_buffer(ctx, 1024); + fz_try(ctx) { + for (block = page->first_block; block; block = block->next) { + if (block->type != FZ_STEXT_BLOCK_TEXT) + continue; + for (line = block->u.t.first_line; line; line = line->next) { + int line_had_text = 0; + for (ch = line->first_char; ch; ch = ch->next) { + fz_rect r = JM_char_bbox(ctx, line, ch); + if (JM_rects_overlap(area, r)) { + line_had_text = 1; + if (need_new_line) { + fz_append_string(ctx, buffer, "\n"); + need_new_line = 0; + } + fz_append_rune(ctx, buffer, ch->c < 32 ? FZ_REPLACEMENT_CHARACTER : ch->c); + } + } + if (line_had_text) + need_new_line = 1; + } + } + fz_terminate_buffer(ctx, buffer); + } + fz_catch(ctx) { + fz_drop_buffer(ctx, buffer); + fz_rethrow(ctx); + } + + + fz_buffer_extract(ctx, buffer, &s); /* take over the data */ + fz_drop_buffer(ctx, buffer); + return (char*)s; +} +//--------------------------------------------------------------------- + + + + +fz_buffer *JM_object_to_buffer(fz_context *ctx, pdf_obj *what, int compress, int ascii) +{ + fz_buffer *res=NULL; + fz_output *out=NULL; + fz_try(ctx) { + res = fz_new_buffer(ctx, 512); + out = fz_new_output_with_buffer(ctx, res); + pdf_print_obj(ctx, out, what, compress, ascii); + } + fz_always(ctx) { + fz_drop_output(ctx, out); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + fz_terminate_buffer(ctx, res); + return res; +} + +//----------------------------------------------------------------------------- +// Merge the /Resources object created by a text pdf device into the page. +// The device may have created multiple /ExtGState/Alp? and /Font/F? objects. +// These need to be renamed (renumbered) to not overwrite existing page +// objects from previous executions. +// Returns the next available numbers n, m for objects /Alp, /F. +//----------------------------------------------------------------------------- +PyObject *JM_merge_resources(fz_context *ctx, pdf_page *page, pdf_obj *temp_res) +{ + // page objects /Resources, /Resources/ExtGState, /Resources/Font + pdf_obj *resources = pdf_dict_get(ctx, page->obj, PDF_NAME(Resources)); + pdf_obj *main_extg = pdf_dict_get(ctx, resources, PDF_NAME(ExtGState)); + pdf_obj *main_fonts = pdf_dict_get(ctx, resources, PDF_NAME(Font)); + + // text pdf device objects /ExtGState, /Font + pdf_obj *temp_extg = pdf_dict_get(ctx, temp_res, PDF_NAME(ExtGState)); + pdf_obj *temp_fonts = pdf_dict_get(ctx, temp_res, PDF_NAME(Font)); + + + int max_alp = -1, max_fonts = -1, i, n; + char text[20]; + + // Handle /Alp objects + if (pdf_is_dict(ctx, temp_extg)) // any created at all? + { + n = pdf_dict_len(ctx, temp_extg); + if (pdf_is_dict(ctx, main_extg)) { // does page have /ExtGState yet? + for (i = 0; i < pdf_dict_len(ctx, main_extg); i++) { + // get highest number of objects named /Alpxxx + char *alp = (char *) pdf_to_name(ctx, pdf_dict_get_key(ctx, main_extg, i)); + if (strncmp(alp, "Alp", 3) != 0) continue; + int j = fz_atoi(alp + 3); + if (j > max_alp) max_alp = j; + } + } + else // create a /ExtGState for the page + main_extg = pdf_dict_put_dict(ctx, resources, PDF_NAME(ExtGState), n); + + max_alp += 1; + for (i = 0; i < n; i++) // copy over renumbered /Alp objects + { + char *alp = (char *) pdf_to_name(ctx, pdf_dict_get_key(ctx, temp_extg, i)); + int j = fz_atoi(alp + 3) + max_alp; + fz_snprintf(text, sizeof(text), "Alp%d", j); // new name + pdf_obj *val = pdf_dict_get_val(ctx, temp_extg, i); + pdf_dict_puts(ctx, main_extg, text, val); + } + } + + + if (pdf_is_dict(ctx, main_fonts)) { // has page any fonts yet? + for (i = 0; i < pdf_dict_len(ctx, main_fonts); i++) { // get max font number + char *font = (char *) pdf_to_name(ctx, pdf_dict_get_key(ctx, main_fonts, i)); + if (strncmp(font, "F", 1) != 0) continue; + int j = fz_atoi(font + 1); + if (j > max_fonts) max_fonts = j; + } + } + else // create a Resources/Font for the page + main_fonts = pdf_dict_put_dict(ctx, resources, PDF_NAME(Font), 2); + + max_fonts += 1; + for (i = 0; i < pdf_dict_len(ctx, temp_fonts); i++) { // copy renumbered fonts + char *font = (char *) pdf_to_name(ctx, pdf_dict_get_key(ctx, temp_fonts, i)); + int j = fz_atoi(font + 1) + max_fonts; + fz_snprintf(text, sizeof(text), "F%d", j); + pdf_obj *val = pdf_dict_get_val(ctx, temp_fonts, i); + pdf_dict_puts(ctx, main_fonts, text, val); + } + return Py_BuildValue("ii", max_alp, max_fonts); // next available numbers +} + + +//----------------------------------------------------------------------------- +// version of fz_show_string, which covers SMALL CAPS +//----------------------------------------------------------------------------- +fz_matrix +JM_show_string_cs(fz_context *ctx, fz_text *text, fz_font *user_font, fz_matrix trm, const char *s, + int wmode, int bidi_level, fz_bidi_direction markup_dir, fz_text_language language) +{ + fz_font *font=NULL; + int gid, ucs; + float adv; + + while (*s) + { + s += fz_chartorune(&ucs, s); + gid = fz_encode_character_sc(ctx, user_font, ucs); + if (gid == 0) { + gid = fz_encode_character_with_fallback(ctx, user_font, ucs, 0, language, &font); + } else { + font = user_font; + } + fz_show_glyph(ctx, text, font, trm, gid, ucs, wmode, bidi_level, markup_dir, language); + adv = fz_advance_glyph(ctx, font, gid, wmode); + if (wmode == 0) + trm = fz_pre_translate(trm, adv, 0); + else + trm = fz_pre_translate(trm, 0, -adv); + } + + return trm; +} + + +//----------------------------------------------------------------------------- +// version of fz_show_string, which also covers UCDN script +//----------------------------------------------------------------------------- +fz_matrix JM_show_string(fz_context *ctx, fz_text *text, fz_font *user_font, fz_matrix trm, const char *s, int wmode, int bidi_level, fz_bidi_direction markup_dir, fz_text_language language, int script) +{ + fz_font *font; + int gid, ucs; + float adv; + + while (*s) { + s += fz_chartorune(&ucs, s); + gid = fz_encode_character_with_fallback(ctx, user_font, ucs, script, language, &font); + fz_show_glyph(ctx, text, font, trm, gid, ucs, wmode, bidi_level, markup_dir, language); + adv = fz_advance_glyph(ctx, font, gid, wmode); + if (wmode == 0) + trm = fz_pre_translate(trm, adv, 0); + else + trm = fz_pre_translate(trm, 0, -adv); + } + return trm; +} + + +//----------------------------------------------------------------------------- +// return a fz_font from a number of parameters +//----------------------------------------------------------------------------- +fz_font *JM_get_font(fz_context *ctx, + char *fontname, + char *fontfile, + PyObject *fontbuffer, + int script, + int lang, + int ordering, + int is_bold, + int is_italic, + int is_serif, + int embed) +{ + const unsigned char *data = NULL; + int size, index=0; + fz_buffer *res = NULL; + fz_font *font = NULL; + fz_try(ctx) { + if (fontfile) goto have_file; + if (EXISTS(fontbuffer)) goto have_buffer; + if (ordering > -1) goto have_cjk; + if (fontname) goto have_base14; + goto have_noto; + + // Base-14 or a MuPDF builtin font + have_base14:; + font = fz_new_base14_font(ctx, fontname); + if (font) { + goto fertig; + } + font = fz_new_builtin_font(ctx, fontname, is_bold, is_italic); + goto fertig; + + // CJK font + have_cjk:; + font = fz_new_cjk_font(ctx, ordering); + goto fertig; + + // fontfile + have_file:; + font = fz_new_font_from_file(ctx, NULL, fontfile, index, 0); + goto fertig; + + // fontbuffer + have_buffer:; + res = JM_BufferFromBytes(ctx, fontbuffer); + font = fz_new_font_from_buffer(ctx, NULL, res, index, 0); + goto fertig; + + // Check for NOTO font + have_noto:; + data = fz_lookup_noto_font(ctx, script, lang, &size, &index); + if (data) font = fz_new_font_from_memory(ctx, NULL, data, size, index, 0); + if (font) goto fertig; + font = fz_load_fallback_font(ctx, script, lang, is_serif, is_bold, is_italic); + goto fertig; + + fertig:; + if (!font) { + RAISEPY(ctx, MSG_FONT_FAILED, PyExc_RuntimeError); + } + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + // if font allows this, set embedding + if (!font->flags.never_embed) { + fz_set_font_embedding(ctx, font, embed); + } + #endif + } + fz_always(ctx) { + fz_drop_buffer(ctx, res); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return font; +} + +%} diff --git a/fitz/helper-xobject.i b/fitz/helper-xobject.i new file mode 100644 index 0000000..7f9c9c9 --- /dev/null +++ b/fitz/helper-xobject.i @@ -0,0 +1,285 @@ +%{ +/* +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +*/ +//----------------------------------------------------------------------------- +// Read and concatenate a PDF page's /Conents object(s) in a buffer +//----------------------------------------------------------------------------- +fz_buffer *JM_read_contents(fz_context * ctx, pdf_obj * pageref) +{ + fz_buffer *res = NULL, *nres = NULL; + int i; + fz_try(ctx) { + pdf_obj *contents = pdf_dict_get(ctx, pageref, PDF_NAME(Contents)); + if (pdf_is_array(ctx, contents)) { + res = fz_new_buffer(ctx, 1024); + for (i = 0; i < pdf_array_len(ctx, contents); i++) { + nres = pdf_load_stream(ctx, pdf_array_get(ctx, contents, i)); + fz_append_buffer(ctx, res, nres); + fz_drop_buffer(ctx, nres); + } + } + else if (contents) { + res = pdf_load_stream(ctx, contents); + } + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return res; +} + +//----------------------------------------------------------------------------- +// Make an XObject from a PDF page +// For a positive xref assume that its object can be used instead +//----------------------------------------------------------------------------- +pdf_obj *JM_xobject_from_page(fz_context * ctx, pdf_document * pdfout, fz_page * fsrcpage, int xref, pdf_graft_map *gmap) +{ + pdf_obj *xobj1, *resources = NULL, *o, *spageref; + fz_try(ctx) { + if (xref > 0) { + xobj1 = pdf_new_indirect(ctx, pdfout, xref, 0); + } else { + fz_buffer *res = NULL; + fz_rect mediabox; + pdf_page *srcpage = pdf_page_from_fz_page(ctx, fsrcpage); + spageref = srcpage->obj; + mediabox = pdf_to_rect(ctx, pdf_dict_get_inheritable(ctx, spageref, PDF_NAME(MediaBox))); + // Deep-copy resources object of source page + o = pdf_dict_get_inheritable(ctx, spageref, PDF_NAME(Resources)); + if (gmap) // use graftmap when possible + resources = pdf_graft_mapped_object(ctx, gmap, o); + else + resources = pdf_graft_object(ctx, pdfout, o); + + // get spgage contents source + res = JM_read_contents(ctx, spageref); + + //------------------------------------------------------------- + // create XObject representing the source page + //------------------------------------------------------------- + xobj1 = pdf_new_xobject(ctx, pdfout, mediabox, fz_identity, NULL, res); + // store spage contents + JM_update_stream(ctx, pdfout, xobj1, res, 1); + fz_drop_buffer(ctx, res); + + // store spage resources + pdf_dict_put_drop(ctx, xobj1, PDF_NAME(Resources), resources); + } + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return xobj1; +} + +//----------------------------------------------------------------------------- +// Insert a buffer as a new separate /Contents object of a page. +// 1. Create a new stream object from buffer 'newcont' +// 2. If /Contents already is an array, then just prepend or append this object +// 3. Else, create new array and put old content obj and this object into it. +// If the page had no /Contents before, just create a 1-item array. +//----------------------------------------------------------------------------- +int JM_insert_contents(fz_context * ctx, pdf_document * pdf, + pdf_obj * pageref, fz_buffer * newcont, int overlay) +{ + int xref = 0; + pdf_obj *newconts = NULL; + pdf_obj *carr = NULL; + fz_var(newconts); + fz_var(carr); + fz_try(ctx) { + pdf_obj *contents = pdf_dict_get(ctx, pageref, PDF_NAME(Contents)); + newconts = pdf_add_stream(ctx, pdf, newcont, NULL, 0); + xref = pdf_to_num(ctx, newconts); + if (pdf_is_array(ctx, contents)) { + if (overlay) // append new object + pdf_array_push(ctx, contents, newconts); + else // prepend new object + pdf_array_insert(ctx, contents, newconts, 0); + } else { + carr = pdf_new_array(ctx, pdf, 5); + if (overlay) { + if (contents) + pdf_array_push(ctx, carr, contents); + pdf_array_push(ctx, carr, newconts); + } else { + pdf_array_push(ctx, carr, newconts); + if (contents) + pdf_array_push(ctx, carr, contents); + } + pdf_dict_put(ctx, pageref, PDF_NAME(Contents), carr); + } + } + fz_always(ctx) { + pdf_drop_obj(ctx, newconts); + pdf_drop_obj(ctx, carr); + } + fz_catch(ctx) { + fz_rethrow(ctx); + } + return xref; +} + +static void show(const char* prefix, PyObject* obj) +{ + if (!obj) + { + printf( "%s \n", prefix); + return; + } + PyObject* obj_repr = PyObject_Repr( obj); + PyObject* obj_repr_u = PyUnicode_AsEncodedString( obj_repr, "utf-8", "~E~"); + const char* obj_repr_s = PyString_AsString( obj_repr_u); + printf( "%s%s\n", prefix, obj_repr_s); + fflush(stdout); +} + +static PyObject *g_img_info = NULL; +static fz_matrix g_img_info_matrix = {0}; + +static fz_image * +JM_image_filter(fz_context *ctx, void *opaque, fz_matrix ctm, const char *name, fz_image *image) +{ + fz_quad q = fz_transform_quad(fz_quad_from_rect(fz_unit_rect), ctm); + #if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + q = fz_transform_quad( q, g_img_info_matrix); + #endif + PyObject *temp = Py_BuildValue("sN", name, JM_py_from_quad(q)); + + LIST_APPEND_DROP(g_img_info, temp); + return image; +} + +#if FZ_VERSION_MAJOR == 1 && FZ_VERSION_MINOR >= 22 + +static PyObject * +JM_image_reporter(fz_context *ctx, pdf_page *page) +{ + pdf_document *doc = page->doc; + + pdf_page_transform(ctx, page, NULL, &g_img_info_matrix); + pdf_filter_options filter_options = {0}; + filter_options.recurse = 0; + filter_options.instance_forms = 1; + filter_options.ascii = 1; + filter_options.no_update = 1; + + pdf_sanitize_filter_options sanitize_filter_options = {0}; + sanitize_filter_options.opaque = page; + sanitize_filter_options.image_filter = JM_image_filter; + + pdf_filter_factory filter_factory[2] = {0}; + filter_factory[0].filter = pdf_new_sanitize_filter; + filter_factory[0].options = &sanitize_filter_options; + + filter_options.filters = filter_factory; // was & + + g_img_info = PyList_New(0); + + pdf_filter_page_contents(ctx, doc, page, &filter_options); + + PyObject *rc = PySequence_Tuple(g_img_info); + Py_CLEAR(g_img_info); + + return rc; +} + +#else + +void +JM_filter_content_stream( + fz_context * ctx, + pdf_document * doc, + pdf_obj * in_stm, + pdf_obj * in_res, + fz_matrix transform, + pdf_filter_options * filter, + int struct_parents, + fz_buffer **out_buf, + pdf_obj **out_res) +{ + pdf_processor *proc_buffer = NULL; + pdf_processor *proc_filter = NULL; + + fz_var(proc_buffer); + fz_var(proc_filter); + + *out_buf = NULL; + *out_res = NULL; + + fz_try(ctx) { + *out_buf = fz_new_buffer(ctx, 1024); + proc_buffer = pdf_new_buffer_processor(ctx, *out_buf, filter->ascii); + if (filter->sanitize) { + *out_res = pdf_new_dict(ctx, doc, 1); + proc_filter = pdf_new_filter_processor(ctx, doc, proc_buffer, in_res, *out_res, struct_parents, transform, filter); + pdf_process_contents(ctx, proc_filter, doc, in_res, in_stm, NULL); + pdf_close_processor(ctx, proc_filter); + } else { + *out_res = pdf_keep_obj(ctx, in_res); + pdf_process_contents(ctx, proc_buffer, doc, in_res, in_stm, NULL); + } + pdf_close_processor(ctx, proc_buffer); + } + fz_always(ctx) { + pdf_drop_processor(ctx, proc_filter); + pdf_drop_processor(ctx, proc_buffer); + } + fz_catch(ctx) { + fz_drop_buffer(ctx, *out_buf); + *out_buf = NULL; + pdf_drop_obj(ctx, *out_res); + *out_res = NULL; + fz_rethrow(ctx); + } +} + +PyObject * +JM_image_reporter(fz_context *ctx, pdf_page *page) +{ + pdf_document *doc = page->doc; + pdf_filter_options filter; + memset(&filter, 0, sizeof filter); + filter.opaque = page; + filter.text_filter = NULL; + filter.image_filter = JM_image_filter; + filter.end_page = NULL; + filter.recurse = 0; + filter.instance_forms = 1; + filter.sanitize = 1; + filter.ascii = 1; + + pdf_obj *contents, *old_res; + pdf_obj *struct_parents_obj; + pdf_obj *new_res; + fz_buffer *buffer; + int struct_parents; + fz_matrix ctm = fz_identity; + pdf_page_transform(ctx, page, NULL, &ctm); + struct_parents_obj = pdf_dict_get(ctx, page->obj, PDF_NAME(StructParents)); + struct_parents = -1; + if (pdf_is_number(ctx, struct_parents_obj)) + struct_parents = pdf_to_int(ctx, struct_parents_obj); + + contents = pdf_page_contents(ctx, page); + old_res = pdf_page_resources(ctx, page); + g_img_info = PyList_New(0); + JM_filter_content_stream(ctx, doc, contents, old_res, ctm, &filter, struct_parents, &buffer, &new_res); + fz_drop_buffer(ctx, buffer); + pdf_drop_obj(ctx, new_res); + PyObject *rc = PySequence_Tuple(g_img_info); + Py_CLEAR(g_img_info); + return rc; +} + +#endif + +%} diff --git a/fitz/utils.py b/fitz/utils.py new file mode 100644 index 0000000..c6c009b --- /dev/null +++ b/fitz/utils.py @@ -0,0 +1,5498 @@ +# ------------------------------------------------------------------------ +# Copyright 2020-2022, Harald Lieder, mailto:harald.lieder@outlook.com +# License: GNU AFFERO GPL 3.0, https://www.gnu.org/licenses/agpl-3.0.html +# +# Part of "PyMuPDF", a Python binding for "MuPDF" (http://mupdf.com), a +# lightweight PDF, XPS, and E-book viewer, renderer and toolkit which is +# maintained and developed by Artifex Software, Inc. https://artifex.com. +# ------------------------------------------------------------------------ +import io +import json +import math +import os +import random +import string +import tempfile +import typing +import warnings + +from fitz import * + +TESSDATA_PREFIX = os.getenv("TESSDATA_PREFIX") +point_like = "point_like" +rect_like = "rect_like" +matrix_like = "matrix_like" +quad_like = "quad_like" +AnyType = typing.Any +OptInt = typing.Union[int, None] +OptFloat = typing.Optional[float] +OptStr = typing.Optional[str] +OptDict = typing.Optional[dict] +OptBytes = typing.Optional[typing.ByteString] +OptSeq = typing.Optional[typing.Sequence] + +""" +This is a collection of functions to extend PyMupdf. +""" + + +def write_text(page: Page, **kwargs) -> None: + """Write the text of one or more TextWriter objects. + + Args: + rect: target rectangle. If None, the union of the text writers is used. + writers: one or more TextWriter objects. + overlay: put in foreground or background. + keep_proportion: maintain aspect ratio of rectangle sides. + rotate: arbitrary rotation angle. + oc: the xref of an optional content object + """ + if type(page) is not Page: + raise ValueError("bad page parameter") + s = { + k + for k in kwargs.keys() + if k + not in { + "rect", + "writers", + "opacity", + "color", + "overlay", + "keep_proportion", + "rotate", + "oc", + } + } + if s != set(): + raise ValueError("bad keywords: " + str(s)) + + rect = kwargs.get("rect") + writers = kwargs.get("writers") + opacity = kwargs.get("opacity") + color = kwargs.get("color") + overlay = bool(kwargs.get("overlay", True)) + keep_proportion = bool(kwargs.get("keep_proportion", True)) + rotate = int(kwargs.get("rotate", 0)) + oc = int(kwargs.get("oc", 0)) + + if not writers: + raise ValueError("need at least one TextWriter") + if type(writers) is TextWriter: + if rotate == 0 and rect is None: + writers.write_text(page, opacity=opacity, color=color, overlay=overlay) + return None + else: + writers = (writers,) + clip = writers[0].text_rect + textdoc = Document() + tpage = textdoc.new_page(width=page.rect.width, height=page.rect.height) + for writer in writers: + clip |= writer.text_rect + writer.write_text(tpage, opacity=opacity, color=color) + if rect is None: + rect = clip + page.show_pdf_page( + rect, + textdoc, + 0, + overlay=overlay, + keep_proportion=keep_proportion, + rotate=rotate, + clip=clip, + oc=oc, + ) + textdoc = None + tpage = None + + +def show_pdf_page(*args, **kwargs) -> int: + """Show page number 'pno' of PDF 'src' in rectangle 'rect'. + + Args: + rect: (rect-like) where to place the source image + src: (document) source PDF + pno: (int) source page number + overlay: (bool) put in foreground + keep_proportion: (bool) do not change width-height-ratio + rotate: (int) degrees (multiple of 90) + clip: (rect-like) part of source page rectangle + Returns: + xref of inserted object (for reuse) + """ + if len(args) not in (3, 4): + raise ValueError("bad number of positional parameters") + pno = None + if len(args) == 3: + page, rect, src = args + else: + page, rect, src, pno = args + if pno == None: + pno = int(kwargs.get("pno", 0)) + overlay = bool(kwargs.get("overlay", True)) + keep_proportion = bool(kwargs.get("keep_proportion", True)) + rotate = float(kwargs.get("rotate", 0)) + oc = int(kwargs.get("oc", 0)) + clip = kwargs.get("clip") + + def calc_matrix(sr, tr, keep=True, rotate=0): + """Calculate transformation matrix from source to target rect. + + Notes: + The product of four matrices in this sequence: (1) translate correct + source corner to origin, (2) rotate, (3) scale, (4) translate to + target's top-left corner. + Args: + sr: source rect in PDF (!) coordinate system + tr: target rect in PDF coordinate system + keep: whether to keep source ratio of width to height + rotate: rotation angle in degrees + Returns: + Transformation matrix. + """ + # calc center point of source rect + smp = (sr.tl + sr.br) / 2.0 + # calc center point of target rect + tmp = (tr.tl + tr.br) / 2.0 + + # m moves to (0, 0), then rotates + m = Matrix(1, 0, 0, 1, -smp.x, -smp.y) * Matrix(rotate) + + sr1 = sr * m # resulting source rect to calculate scale factors + + fw = tr.width / sr1.width # scale the width + fh = tr.height / sr1.height # scale the height + if keep: + fw = fh = min(fw, fh) # take min if keeping aspect ratio + + m *= Matrix(fw, fh) # concat scale matrix + m *= Matrix(1, 0, 0, 1, tmp.x, tmp.y) # concat move to target center + return JM_TUPLE(m) + + CheckParent(page) + doc = page.parent + + if not doc.is_pdf or not src.is_pdf: + raise ValueError("is no PDF") + + if rect.is_empty or rect.is_infinite: + raise ValueError("rect must be finite and not empty") + + while pno < 0: # support negative page numbers + pno += src.page_count + src_page = src[pno] # load source page + if src_page.get_contents() == []: + raise ValueError("nothing to show - source page empty") + + tar_rect = rect * ~page.transformation_matrix # target rect in PDF coordinates + + src_rect = src_page.rect if not clip else src_page.rect & clip # source rect + if src_rect.is_empty or src_rect.is_infinite: + raise ValueError("clip must be finite and not empty") + src_rect = src_rect * ~src_page.transformation_matrix # ... in PDF coord + + matrix = calc_matrix(src_rect, tar_rect, keep=keep_proportion, rotate=rotate) + + # list of existing /Form /XObjects + ilst = [i[1] for i in doc.get_page_xobjects(page.number)] + ilst += [i[7] for i in doc.get_page_images(page.number)] + ilst += [i[4] for i in doc.get_page_fonts(page.number)] + + # create a name not in that list + n = "fzFrm" + i = 0 + _imgname = n + "0" + while _imgname in ilst: + i += 1 + _imgname = n + str(i) + + isrc = src._graft_id # used as key for graftmaps + if doc._graft_id == isrc: + raise ValueError("source document must not equal target") + + # retrieve / make Graftmap for source PDF + gmap = doc.Graftmaps.get(isrc, None) + if gmap is None: + gmap = Graftmap(doc) + doc.Graftmaps[isrc] = gmap + + # take note of generated xref for automatic reuse + pno_id = (isrc, pno) # id of src[pno] + xref = doc.ShownPages.get(pno_id, 0) + + xref = page._show_pdf_page( + src_page, + overlay=overlay, + matrix=matrix, + xref=xref, + oc=oc, + clip=src_rect, + graftmap=gmap, + _imgname=_imgname, + ) + doc.ShownPages[pno_id] = xref + + return xref + + +def replace_image(page: Page, xref: int, *, filename=None, pixmap=None, stream=None): + """Replace the image referred to by xref. + + Replace the image by changing the object definition stored under xref. This + will leave the pages appearance instructions intact, so the new image is + being displayed with the same bbox, rotation etc. + By providing a small fully transparent image, an effect as if the image had + been deleted can be achieved. + A typical use may include replacing large images by a smaller version, + e.g. with a lower resolution or graylevel instead of colored. + + Args: + xref: the xref of the image to replace. + filename, pixmap, stream: exactly one of these must be provided. The + meaning being the same as in Page.insert_image. + """ + doc = page.parent # the owning document + if not doc.xref_is_image(xref): + raise ValueError("xref not an image") # insert new image anywhere in page + if bool(filename) + bool(stream) + bool(pixmap) != 1: + raise ValueError("Exactly one of filename/stream/pixmap must be given") + new_xref = page.insert_image( + page.rect, filename=filename, stream=stream, pixmap=pixmap + ) + doc.xref_copy(new_xref, xref) # copy over new to old + last_contents_xref = page.get_contents()[-1] + # new image insertion has created a new /Contents source, + # which we will set to spaces now + doc.update_stream(last_contents_xref, b" ") + + +def delete_image(page: Page, xref: int): + """Delete the image referred to by xef. + + Actually replaces by a small transparent Pixmap using method Page.replace_image. + + Args: + xref: xref of the image to delete. + """ + # make a small 100% transparent pixmap (of just any dimension) + pix = fitz.Pixmap(fitz.csGRAY, (0, 0, 1, 1), 1) + pix.clear_with() # clear all samples bytes to 0x00 + page.replace_image(xref, pixmap=pix) + + +def insert_image(page, rect, **kwargs): + """Insert an image for display in a rectangle. + + Args: + rect: (rect_like) position of image on the page. + alpha: (int, optional) set to 0 if image has no transparency. + filename: (str, Path, file object) image filename. + keep_proportion: (bool) keep width / height ratio (default). + mask: (bytes, optional) image consisting of alpha values to use. + oc: (int) xref of OCG or OCMD to declare as Optional Content. + overlay: (bool) put in foreground (default) or background. + pixmap: (Pixmap) use this as image. + rotate: (int) rotate by 0, 90, 180 or 270 degrees. + stream: (bytes) use this as image. + xref: (int) use this as image. + + 'page' and 'rect' are positional, all other parameters are keywords. + + If 'xref' is given, that image is used. Other input options are ignored. + Else, exactly one of pixmap, stream or filename must be given. + + 'alpha=0' for non-transparent images improves performance significantly. + Affects stream and filename only. + + Optimum transparent insertions are possible by using filename / stream in + conjunction with a 'mask' image of alpha values. + + Returns: + xref (int) of inserted image. Re-use as argument for multiple insertions. + """ + CheckParent(page) + doc = page.parent + if not doc.is_pdf: + raise ValueError("is no PDF") + + valid_keys = { + "alpha", + "filename", + "height", + "keep_proportion", + "mask", + "oc", + "overlay", + "pixmap", + "rotate", + "stream", + "width", + "xref", + } + s = set(kwargs.keys()).difference(valid_keys) + if s != set(): + raise ValueError(f"bad key argument(s): {s}.") + filename = kwargs.get("filename") + pixmap = kwargs.get("pixmap") + stream = kwargs.get("stream") + mask = kwargs.get("mask") + rotate = int(kwargs.get("rotate", 0)) + width = int(kwargs.get("width", 0)) + height = int(kwargs.get("height", 0)) + alpha = int(kwargs.get("alpha", -1)) + oc = int(kwargs.get("oc", 0)) + xref = int(kwargs.get("xref", 0)) + keep_proportion = bool(kwargs.get("keep_proportion", True)) + overlay = bool(kwargs.get("overlay", True)) + + if xref == 0 and (bool(filename) + bool(stream) + bool(pixmap) != 1): + raise ValueError("xref=0 needs exactly one of filename, pixmap, stream") + + if filename: + if type(filename) is str: + pass + elif hasattr(filename, "absolute"): + filename = str(filename) + elif hasattr(filename, "name"): + filename = filename.name + else: + raise ValueError("bad filename") + + if filename and not os.path.exists(filename): + raise FileNotFoundError("No such file: '%s'" % filename) + elif stream and type(stream) not in (bytes, bytearray, io.BytesIO): + raise ValueError("stream must be bytes-like / BytesIO") + elif pixmap and type(pixmap) is not Pixmap: + raise ValueError("pixmap must be a Pixmap") + if mask and not (stream or filename): + raise ValueError("mask requires stream or filename") + if mask and type(mask) not in (bytes, bytearray, io.BytesIO): + raise ValueError("mask must be bytes-like / BytesIO") + while rotate < 0: + rotate += 360 + while rotate >= 360: + rotate -= 360 + if rotate not in (0, 90, 180, 270): + raise ValueError("bad rotate value") + + r = Rect(rect) + if r.is_empty or r.is_infinite: + raise ValueError("rect must be finite and not empty") + clip = r * ~page.transformation_matrix + + # Create a unique image reference name. + ilst = [i[7] for i in doc.get_page_images(page.number)] + ilst += [i[1] for i in doc.get_page_xobjects(page.number)] + ilst += [i[4] for i in doc.get_page_fonts(page.number)] + n = "fzImg" # 'fitz image' + i = 0 + _imgname = n + "0" # first name candidate + while _imgname in ilst: + i += 1 + _imgname = n + str(i) # try new name + + digests = doc.InsertedImages + + xref, digests = page._insert_image( + filename=filename, + pixmap=pixmap, + stream=stream, + imask=mask, + clip=clip, + overlay=overlay, + oc=oc, + xref=xref, + rotate=rotate, + keep_proportion=keep_proportion, + width=width, + height=height, + alpha=alpha, + _imgname=_imgname, + digests=digests, + ) + + if digests != None: + doc.InsertedImages = digests + + return xref + + +def search_for(*args, **kwargs) -> list: + """Search for a string on a page. + + Args: + text: string to be searched for + clip: restrict search to this rectangle + quads: (bool) return quads instead of rectangles + flags: bit switches, default: join hyphened words + textpage: a pre-created TextPage + Returns: + a list of rectangles or quads, each containing one occurrence. + """ + if len(args) != 2: + raise ValueError("bad number of positional parameters") + page, text = args + quads = kwargs.get("quads", 0) + clip = kwargs.get("clip") + textpage = kwargs.get("textpage") + if clip != None: + clip = Rect(clip) + flags = kwargs.get( + "flags", + TEXT_DEHYPHENATE + | TEXT_PRESERVE_WHITESPACE + | TEXT_PRESERVE_LIGATURES + | TEXT_MEDIABOX_CLIP, + ) + + CheckParent(page) + tp = textpage + if tp is None: + tp = page.get_textpage(clip=clip, flags=flags) # create TextPage + elif getattr(tp, "parent") != page: + raise ValueError("not a textpage of this page") + rlist = tp.search(text, quads=quads) + if textpage is None: + del tp + return rlist + + +def search_page_for( + doc: Document, + pno: int, + text: str, + quads: bool = False, + clip: rect_like = None, + flags: int = TEXT_DEHYPHENATE + | TEXT_PRESERVE_LIGATURES + | TEXT_PRESERVE_WHITESPACE + | TEXT_MEDIABOX_CLIP, + textpage: TextPage = None, +) -> list: + """Search for a string on a page. + + Args: + pno: page number + text: string to be searched for + clip: restrict search to this rectangle + quads: (bool) return quads instead of rectangles + flags: bit switches, default: join hyphened words + textpage: reuse a prepared textpage + Returns: + a list of rectangles or quads, each containing an occurrence. + """ + + return doc[pno].search_for( + text, + quads=quads, + clip=clip, + flags=flags, + textpage=textpage, + ) + + +def get_text_blocks( + page: Page, + clip: rect_like = None, + flags: OptInt = None, + textpage: TextPage = None, + sort: bool = False, +) -> list: + """Return the text blocks on a page. + + Notes: + Lines in a block are concatenated with line breaks. + Args: + flags: (int) control the amount of data parsed into the textpage. + Returns: + A list of the blocks. Each item contains the containing rectangle + coordinates, text lines, block type and running block number. + """ + CheckParent(page) + if flags is None: + flags = ( + TEXT_PRESERVE_WHITESPACE + | TEXT_PRESERVE_IMAGES + | TEXT_PRESERVE_LIGATURES + | TEXT_MEDIABOX_CLIP + ) + tp = textpage + if tp is None: + tp = page.get_textpage(clip=clip, flags=flags) + elif getattr(tp, "parent") != page: + raise ValueError("not a textpage of this page") + + blocks = tp.extractBLOCKS() + if textpage is None: + del tp + if sort is True: + blocks.sort(key=lambda b: (b[3], b[0])) + return blocks + + +def get_text_words( + page: Page, + clip: rect_like = None, + flags: OptInt = None, + textpage: TextPage = None, + sort: bool = False, +) -> list: + """Return the text words as a list with the bbox for each word. + + Args: + flags: (int) control the amount of data parsed into the textpage. + """ + CheckParent(page) + if flags is None: + flags = TEXT_PRESERVE_WHITESPACE | TEXT_PRESERVE_LIGATURES | TEXT_MEDIABOX_CLIP + tp = textpage + if tp is None: + tp = page.get_textpage(clip=clip, flags=flags) + elif getattr(tp, "parent") != page: + raise ValueError("not a textpage of this page") + words = tp.extractWORDS() + if textpage is None: + del tp + if sort is True: + words.sort(key=lambda w: (w[3], w[0])) + return words + + +def get_textbox( + page: Page, + rect: rect_like, + textpage: TextPage = None, +) -> str: + tp = textpage + if tp is None: + tp = page.get_textpage() + elif getattr(tp, "parent") != page: + raise ValueError("not a textpage of this page") + rc = tp.extractTextbox(rect) + if textpage is None: + del tp + return rc + + +def get_text_selection( + page: Page, + p1: point_like, + p2: point_like, + clip: rect_like = None, + textpage: TextPage = None, +): + CheckParent(page) + tp = textpage + if tp is None: + tp = page.get_textpage(clip=clip, flags=TEXT_DEHYPHENATE) + elif getattr(tp, "parent") != page: + raise ValueError("not a textpage of this page") + rc = tp.extractSelection(p1, p2) + if textpage is None: + del tp + return rc + + +def get_textpage_ocr( + page: Page, + flags: int = 0, + language: str = "eng", + dpi: int = 72, + full: bool = False, + tessdata: str = None, +) -> TextPage: + """Create a Textpage from combined results of normal and OCR text parsing. + + Args: + flags: (int) control content becoming part of the result. + language: (str) specify expected language(s). Deafault is "eng" (English). + dpi: (int) resolution in dpi, default 72. + full: (bool) whether to OCR the full page image, or only its images (default) + """ + CheckParent(page) + if not os.getenv("TESSDATA_PREFIX") and not tessdata: + raise RuntimeError("No OCR support: TESSDATA_PREFIX not set") + + def full_ocr(page, dpi, language, flags): + zoom = dpi / 72 + mat = Matrix(zoom, zoom) + pix = page.get_pixmap(matrix=mat) + ocr_pdf = Document( + "pdf", + pix.pdfocr_tobytes(compress=False, language=language, tessdata=tessdata), + ) + ocr_page = ocr_pdf.load_page(0) + unzoom = page.rect.width / ocr_page.rect.width + ctm = Matrix(unzoom, unzoom) * page.derotation_matrix + tpage = ocr_page.get_textpage(flags=flags, matrix=ctm) + ocr_pdf.close() + pix = None + tpage.parent = weakref.proxy(page) + return tpage + + # if OCR for the full page, OCR its pixmap @ desired dpi + if full is True: + return full_ocr(page, dpi, language, flags) + + # For partial OCR, make a normal textpage, then extend it with text that + # is OCRed from each image. + # Because of this, we need the images flag bit set ON. + tpage = page.get_textpage(flags=flags) + for block in page.get_text("dict", flags=TEXT_PRESERVE_IMAGES)["blocks"]: + if block["type"] != 1: # only look at images + continue + bbox = Rect(block["bbox"]) + if bbox.width <= 3 or bbox.height <= 3: # ignore tiny stuff + continue + try: + pix = Pixmap(block["image"]) # get image pixmap + if pix.n - pix.alpha != 3: # we need to convert this to RGB! + pix = Pixmap(csRGB, pix) + if pix.alpha: # must remove alpha channel + pix = Pixmap(pix, 0) + imgdoc = Document( + "pdf", pix.pdfocr_tobytes(language=language, tessdata=tessdata) + ) # pdf with OCRed page + imgpage = imgdoc.load_page(0) # read image as a page + pix = None + # compute matrix to transform coordinates back to that of 'page' + imgrect = imgpage.rect # page size of image PDF + shrink = Matrix(1 / imgrect.width, 1 / imgrect.height) + mat = shrink * block["transform"] + imgpage.extend_textpage(tpage, flags=0, matrix=mat) + imgdoc.close() + except RuntimeError: + tpage = None + print("Falling back to full page OCR") + return full_ocr(page, dpi, language, flags) + + return tpage + + +def get_image_info(page: Page, hashes: bool = False, xrefs: bool = False) -> list: + """Extract image information only from a TextPage. + + Args: + hashes: (bool) include MD5 hash for each image. + xrefs: (bool) try to find the xref for each image. Sets hashes to true. + """ + doc = page.parent + if xrefs and doc.is_pdf: + hashes = True + if not doc.is_pdf: + xrefs = False + imginfo = getattr(page, "_image_info", None) + if imginfo and not xrefs: + return imginfo + if not imginfo: + tp = page.get_textpage(flags=TEXT_PRESERVE_IMAGES) + imginfo = tp.extractIMGINFO(hashes=hashes) + del tp + if hashes: + page._image_info = imginfo + if not xrefs or not doc.is_pdf: + return imginfo + imglist = page.get_images() + digests = {} + for item in imglist: + xref = item[0] + pix = Pixmap(doc, xref) + digests[pix.digest] = xref + del pix + for i in range(len(imginfo)): + item = imginfo[i] + xref = digests.get(item["digest"], 0) + item["xref"] = xref + imginfo[i] = item + return imginfo + + +def get_image_rects(page: Page, name, transform=False) -> list: + """Return list of image positions on a page. + + Args: + name: (str, list, int) image identification. May be reference name, an + item of the page's image list or an xref. + transform: (bool) whether to also return the transformation matrix. + Returns: + A list of Rect objects or tuples of (Rect, Matrix) for all image + locations on the page. + """ + if type(name) in (list, tuple): + xref = name[0] + elif type(name) is int: + xref = name + else: + imglist = [i for i in page.get_images() if i[7] == name] + if imglist == []: + raise ValueError("bad image name") + elif len(imglist) != 1: + raise ValueError("multiple image names found") + xref = imglist[0][0] + pix = Pixmap(page.parent, xref) # make pixmap of the image to compute MD5 + digest = pix.digest + del pix + infos = page.get_image_info(hashes=True) + if not transform: + bboxes = [Rect(im["bbox"]) for im in infos if im["digest"] == digest] + else: + bboxes = [ + (Rect(im["bbox"]), Matrix(im["transform"])) + for im in infos + if im["digest"] == digest + ] + return bboxes + + +def get_text( + page: Page, + option: str = "text", + clip: rect_like = None, + flags: OptInt = None, + textpage: TextPage = None, + sort: bool = False, +): + """Extract text from a page or an annotation. + + This is a unifying wrapper for various methods of the TextPage class. + + Args: + option: (str) text, words, blocks, html, dict, json, rawdict, xhtml or xml. + clip: (rect-like) restrict output to this area. + flags: bit switches to e.g. exclude images or decompose ligatures. + textpage: reuse this TextPage and make no new one. If specified, + 'flags' and 'clip' are ignored. + + Returns: + the output of methods get_text_words / get_text_blocks or TextPage + methods extractText, extractHTML, extractDICT, extractJSON, extractRAWDICT, + extractXHTML or etractXML respectively. + Default and misspelling choice is "text". + """ + formats = { + "text": 0, + "html": 1, + "json": 1, + "rawjson": 1, + "xml": 0, + "xhtml": 1, + "dict": 1, + "rawdict": 1, + "words": 0, + "blocks": 1, + } + option = option.lower() + if option not in formats: + option = "text" + if flags is None: + flags = TEXT_PRESERVE_WHITESPACE | TEXT_PRESERVE_LIGATURES | TEXT_MEDIABOX_CLIP + if formats[option] == 1: + flags |= TEXT_PRESERVE_IMAGES + + if option == "words": + return get_text_words( + page, clip=clip, flags=flags, textpage=textpage, sort=sort + ) + if option == "blocks": + return get_text_blocks( + page, clip=clip, flags=flags, textpage=textpage, sort=sort + ) + CheckParent(page) + cb = None + if option in ("html", "xml", "xhtml"): # no clipping for MuPDF functions + clip = page.cropbox + if clip != None: + clip = Rect(clip) + cb = None + elif type(page) is Page: + cb = page.cropbox + # TextPage with or without images + tp = textpage + if tp is None: + tp = page.get_textpage(clip=clip, flags=flags) + elif getattr(tp, "parent") != page: + raise ValueError("not a textpage of this page") + + if option == "json": + t = tp.extractJSON(cb=cb, sort=sort) + elif option == "rawjson": + t = tp.extractRAWJSON(cb=cb, sort=sort) + elif option == "dict": + t = tp.extractDICT(cb=cb, sort=sort) + elif option == "rawdict": + t = tp.extractRAWDICT(cb=cb, sort=sort) + elif option == "html": + t = tp.extractHTML() + elif option == "xml": + t = tp.extractXML() + elif option == "xhtml": + t = tp.extractXHTML() + else: + t = tp.extractText(sort=sort) + + if textpage is None: + del tp + return t + + +def get_page_text( + doc: Document, + pno: int, + option: str = "text", + clip: rect_like = None, + flags: OptInt = None, + textpage: TextPage = None, + sort: bool = False, +) -> typing.Any: + """Extract a document page's text by page number. + + Notes: + Convenience function calling page.get_text(). + Args: + pno: page number + option: (str) text, words, blocks, html, dict, json, rawdict, xhtml or xml. + Returns: + output from page.TextPage(). + """ + return doc[pno].get_text(option, clip=clip, flags=flags, sort=sort) + + +def get_pixmap( + page: Page, + *, + matrix: matrix_like = Identity, + dpi=None, + colorspace: Colorspace = csRGB, + clip: rect_like = None, + alpha: bool = False, + annots: bool = True, +) -> Pixmap: + """Create pixmap of page. + + Keyword args: + matrix: Matrix for transformation (default: Identity). + dpi: desired dots per inch. If given, matrix is ignored. + colorspace: (str/Colorspace) cmyk, rgb, gray - case ignored, default csRGB. + clip: (irect-like) restrict rendering to this area. + alpha: (bool) whether to include alpha channel + annots: (bool) whether to also render annotations + """ + CheckParent(page) + if dpi: + zoom = dpi / 72 + matrix = Matrix(zoom, zoom) + + if type(colorspace) is str: + if colorspace.upper() == "GRAY": + colorspace = csGRAY + elif colorspace.upper() == "CMYK": + colorspace = csCMYK + else: + colorspace = csRGB + if colorspace.n not in (1, 3, 4): + raise ValueError("unsupported colorspace") + + dl = page.get_displaylist(annots=annots) + pix = dl.get_pixmap(matrix=matrix, colorspace=colorspace, alpha=alpha, clip=clip) + dl = None + if dpi: + pix.set_dpi(dpi, dpi) + return pix + + +def get_page_pixmap( + doc: Document, + pno: int, + *, + matrix: matrix_like = Identity, + dpi=None, + colorspace: Colorspace = csRGB, + clip: rect_like = None, + alpha: bool = False, + annots: bool = True, +) -> Pixmap: + """Create pixmap of document page by page number. + + Notes: + Convenience function calling page.get_pixmap. + Args: + pno: (int) page number + matrix: Matrix for transformation (default: Identity). + colorspace: (str,Colorspace) rgb, rgb, gray - case ignored, default csRGB. + clip: (irect-like) restrict rendering to this area. + alpha: (bool) include alpha channel + annots: (bool) also render annotations + """ + return doc[pno].get_pixmap( + matrix=matrix, + dpi=dpi, + colorspace=colorspace, + clip=clip, + alpha=alpha, + annots=annots, + ) + + +def getLinkDict(ln) -> dict: + nl = {"kind": ln.dest.kind, "xref": 0} + try: + nl["from"] = ln.rect + except: + pass + pnt = Point(0, 0) + if ln.dest.flags & LINK_FLAG_L_VALID: + pnt.x = ln.dest.lt.x + if ln.dest.flags & LINK_FLAG_T_VALID: + pnt.y = ln.dest.lt.y + + if ln.dest.kind == LINK_URI: + nl["uri"] = ln.dest.uri + + elif ln.dest.kind == LINK_GOTO: + nl["page"] = ln.dest.page + nl["to"] = pnt + if ln.dest.flags & LINK_FLAG_R_IS_ZOOM: + nl["zoom"] = ln.dest.rb.x + else: + nl["zoom"] = 0.0 + + elif ln.dest.kind == LINK_GOTOR: + nl["file"] = ln.dest.fileSpec.replace("\\", "/") + nl["page"] = ln.dest.page + if ln.dest.page < 0: + nl["to"] = ln.dest.dest + else: + nl["to"] = pnt + if ln.dest.flags & LINK_FLAG_R_IS_ZOOM: + nl["zoom"] = ln.dest.rb.x + else: + nl["zoom"] = 0.0 + + elif ln.dest.kind == LINK_LAUNCH: + nl["file"] = ln.dest.fileSpec.replace("\\", "/") + + elif ln.dest.kind == LINK_NAMED: + nl["name"] = ln.dest.named + + else: + nl["page"] = ln.dest.page + + return nl + + +def get_links(page: Page) -> list: + """Create a list of all links contained in a PDF page. + + Notes: + see PyMuPDF ducmentation for details. + """ + + CheckParent(page) + ln = page.first_link + links = [] + while ln: + nl = getLinkDict(ln) + links.append(nl) + ln = ln.next + if links != [] and page.parent.is_pdf: + linkxrefs = [x for x in page.annot_xrefs() if x[1] == PDF_ANNOT_LINK] + if len(linkxrefs) == len(links): + for i in range(len(linkxrefs)): + links[i]["xref"] = linkxrefs[i][0] + links[i]["id"] = linkxrefs[i][2] + return links + + +def get_toc( + doc: Document, + simple: bool = True, +) -> list: + """Create a table of contents. + + Args: + simple: a bool to control output. Returns a list, where each entry consists of outline level, title, page number and link destination (if simple = False). For details see PyMuPDF's documentation. + """ + + def recurse(olItem, liste, lvl): + """Recursively follow the outline item chain and record item information in a list.""" + while olItem: + if olItem.title: + title = olItem.title + else: + title = " " + + if not olItem.is_external: + if olItem.uri: + if olItem.page == -1: + resolve = doc.resolve_link(olItem.uri) + page = resolve[0] + 1 + else: + page = olItem.page + 1 + else: + page = -1 + else: + page = -1 + + if not simple: + link = getLinkDict(olItem) + liste.append([lvl, title, page, link]) + else: + liste.append([lvl, title, page]) + + if olItem.down: + liste = recurse(olItem.down, liste, lvl + 1) + olItem = olItem.next + return liste + + # ensure document is open + if doc.is_closed: + raise ValueError("document closed") + doc.init_doc() + olItem = doc.outline + + if not olItem: + return [] + lvl = 1 + liste = [] + toc = recurse(olItem, liste, lvl) + if doc.is_pdf and simple is False: + doc._extend_toc_items(toc) + return toc + + +def del_toc_item( + doc: Document, + idx: int, +) -> None: + """Delete TOC / bookmark item by index.""" + xref = doc.get_outline_xrefs()[idx] + doc._remove_toc_item(xref) + + +def set_toc_item( + doc: Document, + idx: int, + dest_dict: OptDict = None, + kind: OptInt = None, + pno: OptInt = None, + uri: OptStr = None, + title: OptStr = None, + to: point_like = None, + filename: OptStr = None, + zoom: float = 0, +) -> None: + """Update TOC item by index. + + It allows changing the item's title and link destination. + + Args: + idx: (int) desired index of the TOC list, as created by get_toc. + dest_dict: (dict) destination dictionary as created by get_toc(False). + Outrules all other parameters. If None, the remaining parameters + are used to make a dest dictionary. + kind: (int) kind of link (LINK_GOTO, etc.). If None, then only the + title will be updated. If LINK_NONE, the TOC item will be deleted. + pno: (int) page number (1-based like in get_toc). Required if LINK_GOTO. + uri: (str) the URL, required if LINK_URI. + title: (str) the new title. No change if None. + to: (point-like) destination on the target page. If omitted, (72, 36) + will be used as taget coordinates. + filename: (str) destination filename, required for LINK_GOTOR and + LINK_LAUNCH. + name: (str) a destination name for LINK_NAMED. + zoom: (float) a zoom factor for the target location (LINK_GOTO). + """ + xref = doc.get_outline_xrefs()[idx] + page_xref = 0 + if type(dest_dict) is dict: + if dest_dict["kind"] == LINK_GOTO: + pno = dest_dict["page"] + page_xref = doc.page_xref(pno) + page_height = doc.page_cropbox(pno).height + to = dest_dict.get("to", Point(72, 36)) + to.y = page_height - to.y + dest_dict["to"] = to + action = getDestStr(page_xref, dest_dict) + if not action.startswith("/A"): + raise ValueError("bad bookmark dest") + color = dest_dict.get("color") + if color: + color = list(map(float, color)) + if len(color) != 3 or min(color) < 0 or max(color) > 1: + raise ValueError("bad color value") + bold = dest_dict.get("bold", False) + italic = dest_dict.get("italic", False) + flags = italic + 2 * bold + collapse = dest_dict.get("collapse") + return doc._update_toc_item( + xref, + action=action[2:], + title=title, + color=color, + flags=flags, + collapse=collapse, + ) + + if kind == LINK_NONE: # delete bookmark item + return doc.del_toc_item(idx) + if kind is None and title is None: # treat as no-op + return None + if kind is None: # only update title text + return doc._update_toc_item(xref, action=None, title=title) + + if kind == LINK_GOTO: + if pno is None or pno not in range(1, doc.page_count + 1): + raise ValueError("bad page number") + page_xref = doc.page_xref(pno - 1) + page_height = doc.page_cropbox(pno - 1).height + if to is None: + to = Point(72, page_height - 36) + else: + to = Point(to) + to.y = page_height - to.y + + ddict = { + "kind": kind, + "to": to, + "uri": uri, + "page": pno, + "file": filename, + "zoom": zoom, + } + action = getDestStr(page_xref, ddict) + if action == "" or not action.startswith("/A"): + raise ValueError("bad bookmark dest") + + return doc._update_toc_item(xref, action=action[2:], title=title) + + +def get_area(*args) -> float: + """Calculate area of rectangle.\nparameter is one of 'px' (default), 'in', 'cm', or 'mm'.""" + rect = args[0] + if len(args) > 1: + unit = args[1] + else: + unit = "px" + u = {"px": (1, 1), "in": (1.0, 72.0), "cm": (2.54, 72.0), "mm": (25.4, 72.0)} + f = (u[unit][0] / u[unit][1]) ** 2 + return f * rect.width * rect.height + + +def set_metadata(doc: Document, m: dict) -> None: + """Update the PDF /Info object. + + Args: + m: a dictionary like doc.metadata. + """ + if not doc.is_pdf: + raise ValueError("is no PDF") + if doc.is_closed or doc.is_encrypted: + raise ValueError("document closed or encrypted") + if type(m) is not dict: + raise ValueError("bad metadata") + keymap = { + "author": "Author", + "producer": "Producer", + "creator": "Creator", + "title": "Title", + "format": None, + "encryption": None, + "creationDate": "CreationDate", + "modDate": "ModDate", + "subject": "Subject", + "keywords": "Keywords", + "trapped": "Trapped", + } + valid_keys = set(keymap.keys()) + diff_set = set(m.keys()).difference(valid_keys) + if diff_set != set(): + msg = "bad dict key(s): %s" % diff_set + raise ValueError(msg) + + t, temp = doc.xref_get_key(-1, "Info") + if t != "xref": + info_xref = 0 + else: + info_xref = int(temp.replace("0 R", "")) + + if m == {} and info_xref == 0: # nothing to do + return + + if info_xref == 0: # no prev metadata: get new xref + info_xref = doc.get_new_xref() + doc.update_object(info_xref, "<<>>") # fill it with empty object + doc.xref_set_key(-1, "Info", "%i 0 R" % info_xref) + elif m == {}: # remove existing metadata + doc.xref_set_key(-1, "Info", "null") + return + + for key, val in [(k, v) for k, v in m.items() if keymap[k] != None]: + pdf_key = keymap[key] + if not bool(val) or val in ("none", "null"): + val = "null" + else: + val = get_pdf_str(val) + doc.xref_set_key(info_xref, pdf_key, val) + doc.init_doc() + return + + +def getDestStr(xref: int, ddict: dict) -> str: + """Calculate the PDF action string. + + Notes: + Supports Link annotations and outline items (bookmarks). + """ + if not ddict: + return "" + str_goto = "/A<>" + str_gotor1 = "/A<>>>" + str_gotor2 = "/A<>>>" + str_launch = "/A<>>>" + str_uri = "/A<>" + + if type(ddict) in (int, float): + dest = str_goto % (xref, 0, ddict, 0) + return dest + d_kind = ddict.get("kind", LINK_NONE) + + if d_kind == LINK_NONE: + return "" + + if ddict["kind"] == LINK_GOTO: + d_zoom = ddict.get("zoom", 0) + to = ddict.get("to", Point(0, 0)) + d_left, d_top = to + dest = str_goto % (xref, d_left, d_top, d_zoom) + return dest + + if ddict["kind"] == LINK_URI: + dest = str_uri % (get_pdf_str(ddict["uri"]),) + return dest + + if ddict["kind"] == LINK_LAUNCH: + fspec = get_pdf_str(ddict["file"]) + dest = str_launch % (fspec, fspec) + return dest + + if ddict["kind"] == LINK_GOTOR and ddict["page"] < 0: + fspec = get_pdf_str(ddict["file"]) + dest = str_gotor2 % (get_pdf_str(ddict["to"]), fspec, fspec) + return dest + + if ddict["kind"] == LINK_GOTOR and ddict["page"] >= 0: + fspec = get_pdf_str(ddict["file"]) + dest = str_gotor1 % ( + ddict["page"], + ddict["to"].x, + ddict["to"].y, + ddict["zoom"], + fspec, + fspec, + ) + return dest + + return "" + + +def set_toc( + doc: Document, + toc: list, + collapse: int = 1, +) -> int: + """Create new outline tree (table of contents, TOC). + + Args: + toc: (list, tuple) each entry must contain level, title, page and + optionally top margin on the page. None or '()' remove the TOC. + collapse: (int) collapses entries beyond this level. Zero or None + shows all entries unfolded. + Returns: + the number of inserted items, or the number of removed items respectively. + """ + if doc.is_closed or doc.is_encrypted: + raise ValueError("document closed or encrypted") + if not doc.is_pdf: + raise ValueError("is no PDF") + if not toc: # remove all entries + return len(doc._delToC()) + + # validity checks -------------------------------------------------------- + if type(toc) not in (list, tuple): + raise ValueError("'toc' must be list or tuple") + toclen = len(toc) + page_count = doc.page_count + t0 = toc[0] + if type(t0) not in (list, tuple): + raise ValueError("items must be sequences of 3 or 4 items") + if t0[0] != 1: + raise ValueError("hierarchy level of item 0 must be 1") + for i in list(range(toclen - 1)): + t1 = toc[i] + t2 = toc[i + 1] + if not -1 <= t1[2] <= page_count: + raise ValueError("row %i: page number out of range" % i) + if (type(t2) not in (list, tuple)) or len(t2) not in (3, 4): + raise ValueError("bad row %i" % (i + 1)) + if (type(t2[0]) is not int) or t2[0] < 1: + raise ValueError("bad hierarchy level in row %i" % (i + 1)) + if t2[0] > t1[0] + 1: + raise ValueError("bad hierarchy level in row %i" % (i + 1)) + # no formal errors in toc -------------------------------------------------- + + # -------------------------------------------------------------------------- + # make a list of xref numbers, which we can use for our TOC entries + # -------------------------------------------------------------------------- + old_xrefs = doc._delToC() # del old outlines, get their xref numbers + + # prepare table of xrefs for new bookmarks + old_xrefs = [] + xref = [0] + old_xrefs + xref[0] = doc._getOLRootNumber() # entry zero is outline root xref number + if toclen > len(old_xrefs): # too few old xrefs? + for i in range((toclen - len(old_xrefs))): + xref.append(doc.get_new_xref()) # acquire new ones + + lvltab = {0: 0} # to store last entry per hierarchy level + + # ------------------------------------------------------------------------------ + # contains new outline objects as strings - first one is the outline root + # ------------------------------------------------------------------------------ + olitems = [{"count": 0, "first": -1, "last": -1, "xref": xref[0]}] + # ------------------------------------------------------------------------------ + # build olitems as a list of PDF-like connnected dictionaries + # ------------------------------------------------------------------------------ + for i in range(toclen): + o = toc[i] + lvl = o[0] # level + title = get_pdf_str(o[1]) # title + pno = min(doc.page_count - 1, max(0, o[2] - 1)) # page number + page_xref = doc.page_xref(pno) + page_height = doc.page_cropbox(pno).height + top = Point(72, page_height - 36) + dest_dict = {"to": top, "kind": LINK_GOTO} # fall back target + if o[2] < 0: + dest_dict["kind"] = LINK_NONE + if len(o) > 3: # some target is specified + if type(o[3]) in (int, float): # convert a number to a point + dest_dict["to"] = Point(72, page_height - o[3]) + else: # if something else, make sure we have a dict + dest_dict = o[3] if type(o[3]) is dict else dest_dict + if "to" not in dest_dict: # target point not in dict? + dest_dict["to"] = top # put default in + else: # transform target to PDF coordinates + point = +dest_dict["to"] + point.y = page_height - point.y + dest_dict["to"] = point + d = {} + d["first"] = -1 + d["count"] = 0 + d["last"] = -1 + d["prev"] = -1 + d["next"] = -1 + d["dest"] = getDestStr(page_xref, dest_dict) + d["top"] = dest_dict["to"] + d["title"] = title + d["parent"] = lvltab[lvl - 1] + d["xref"] = xref[i + 1] + d["color"] = dest_dict.get("color") + d["flags"] = dest_dict.get("italic", 0) + 2 * dest_dict.get("bold", 0) + lvltab[lvl] = i + 1 + parent = olitems[lvltab[lvl - 1]] # the parent entry + + if ( + dest_dict.get("collapse") or collapse and lvl > collapse + ): # suppress expansion + parent["count"] -= 1 # make /Count negative + else: + parent["count"] += 1 # positive /Count + + if parent["first"] == -1: + parent["first"] = i + 1 + parent["last"] = i + 1 + else: + d["prev"] = parent["last"] + prev = olitems[parent["last"]] + prev["next"] = i + 1 + parent["last"] = i + 1 + olitems.append(d) + + # ------------------------------------------------------------------------------ + # now create each outline item as a string and insert it in the PDF + # ------------------------------------------------------------------------------ + for i, ol in enumerate(olitems): + txt = "<<" + if ol["count"] != 0: + txt += "/Count %i" % ol["count"] + try: + txt += ol["dest"] + except: + pass + try: + if ol["first"] > -1: + txt += "/First %i 0 R" % xref[ol["first"]] + except: + pass + try: + if ol["last"] > -1: + txt += "/Last %i 0 R" % xref[ol["last"]] + except: + pass + try: + if ol["next"] > -1: + txt += "/Next %i 0 R" % xref[ol["next"]] + except: + pass + try: + if ol["parent"] > -1: + txt += "/Parent %i 0 R" % xref[ol["parent"]] + except: + pass + try: + if ol["prev"] > -1: + txt += "/Prev %i 0 R" % xref[ol["prev"]] + except: + pass + try: + txt += "/Title" + ol["title"] + except: + pass + + if ol.get("color") and len(ol["color"]) == 3: + txt += "/C[ %g %g %g]" % tuple(ol["color"]) + if ol.get("flags", 0) > 0: + txt += "/F %i" % ol["flags"] + + if i == 0: # special: this is the outline root + txt += "/Type/Outlines" # so add the /Type entry + txt += ">>" + doc.update_object(xref[i], txt) # insert the PDF object + + doc.init_doc() + return toclen + + +def do_links( + doc1: Document, + doc2: Document, + from_page: int = -1, + to_page: int = -1, + start_at: int = -1, +) -> None: + """Insert links contained in copied page range into destination PDF. + + Parameter values **must** equal those of method insert_pdf(), which must + have been previously executed. + """ + + # -------------------------------------------------------------------------- + # internal function to create the actual "/Annots" object string + # -------------------------------------------------------------------------- + def cre_annot(lnk, xref_dst, pno_src, ctm): + """Create annotation object string for a passed-in link.""" + + r = lnk["from"] * ctm # rect in PDF coordinates + rect = "%g %g %g %g" % tuple(r) + if lnk["kind"] == LINK_GOTO: + txt = annot_skel["goto1"] # annot_goto + idx = pno_src.index(lnk["page"]) + p = lnk["to"] * ctm # target point in PDF coordinates + annot = txt % (xref_dst[idx], p.x, p.y, lnk["zoom"], rect) + + elif lnk["kind"] == LINK_GOTOR: + if lnk["page"] >= 0: + txt = annot_skel["gotor1"] # annot_gotor + pnt = lnk.get("to", Point(0, 0)) # destination point + if type(pnt) is not Point: + pnt = Point(0, 0) + annot = txt % ( + lnk["page"], + pnt.x, + pnt.y, + lnk["zoom"], + lnk["file"], + lnk["file"], + rect, + ) + else: + txt = annot_skel["gotor2"] # annot_gotor_n + to = get_pdf_str(lnk["to"]) + to = to[1:-1] + f = lnk["file"] + annot = txt % (to, f, rect) + + elif lnk["kind"] == LINK_LAUNCH: + txt = annot_skel["launch"] # annot_launch + annot = txt % (lnk["file"], lnk["file"], rect) + + elif lnk["kind"] == LINK_URI: + txt = annot_skel["uri"] # annot_uri + annot = txt % (lnk["uri"], rect) + + else: + annot = "" + + return annot + + # -------------------------------------------------------------------------- + + # validate & normalize parameters + if from_page < 0: + fp = 0 + elif from_page >= doc2.page_count: + fp = doc2.page_count - 1 + else: + fp = from_page + + if to_page < 0 or to_page >= doc2.page_count: + tp = doc2.page_count - 1 + else: + tp = to_page + + if start_at < 0: + raise ValueError("'start_at' must be >= 0") + sa = start_at + + incr = 1 if fp <= tp else -1 # page range could be reversed + + # lists of source / destination page numbers + pno_src = list(range(fp, tp + incr, incr)) + pno_dst = [sa + i for i in range(len(pno_src))] + + # lists of source / destination page xrefs + xref_src = [] + xref_dst = [] + for i in range(len(pno_src)): + p_src = pno_src[i] + p_dst = pno_dst[i] + old_xref = doc2.page_xref(p_src) + new_xref = doc1.page_xref(p_dst) + xref_src.append(old_xref) + xref_dst.append(new_xref) + + # create the links for each copied page in destination PDF + for i in range(len(xref_src)): + page_src = doc2[pno_src[i]] # load source page + links = page_src.get_links() # get all its links + if len(links) == 0: # no links there + page_src = None + continue + ctm = ~page_src.transformation_matrix # calc page transformation matrix + page_dst = doc1[pno_dst[i]] # load destination page + link_tab = [] # store all link definitions here + for l in links: + if l["kind"] == LINK_GOTO and (l["page"] not in pno_src): + continue # GOTO link target not in copied pages + annot_text = cre_annot(l, xref_dst, pno_src, ctm) + if not annot_text: + print("cannot create /Annot for kind: " + str(l["kind"])) + else: + link_tab.append(annot_text) + if link_tab != []: + page_dst._addAnnot_FromString(tuple(link_tab)) + + return + + +def getLinkText(page: Page, lnk: dict) -> str: + # -------------------------------------------------------------------------- + # define skeletons for /Annots object texts + # -------------------------------------------------------------------------- + ctm = page.transformation_matrix + ictm = ~ctm + r = lnk["from"] + rect = "%g %g %g %g" % tuple(r * ictm) + + annot = "" + if lnk["kind"] == LINK_GOTO: + if lnk["page"] >= 0: + txt = annot_skel["goto1"] # annot_goto + pno = lnk["page"] + xref = page.parent.page_xref(pno) + pnt = lnk.get("to", Point(0, 0)) # destination point + ipnt = pnt * ictm + annot = txt % (xref, ipnt.x, ipnt.y, lnk.get("zoom", 0), rect) + else: + txt = annot_skel["goto2"] # annot_goto_n + annot = txt % (get_pdf_str(lnk["to"]), rect) + + elif lnk["kind"] == LINK_GOTOR: + if lnk["page"] >= 0: + txt = annot_skel["gotor1"] # annot_gotor + pnt = lnk.get("to", Point(0, 0)) # destination point + if type(pnt) is not Point: + pnt = Point(0, 0) + annot = txt % ( + lnk["page"], + pnt.x, + pnt.y, + lnk.get("zoom", 0), + lnk["file"], + lnk["file"], + rect, + ) + else: + txt = annot_skel["gotor2"] # annot_gotor_n + annot = txt % (get_pdf_str(lnk["to"]), lnk["file"], rect) + + elif lnk["kind"] == LINK_LAUNCH: + txt = annot_skel["launch"] # annot_launch + annot = txt % (lnk["file"], lnk["file"], rect) + + elif lnk["kind"] == LINK_URI: + txt = annot_skel["uri"] # txt = annot_uri + annot = txt % (lnk["uri"], rect) + + elif lnk["kind"] == LINK_NAMED: + txt = annot_skel["named"] # annot_named + annot = txt % (lnk["name"], rect) + if not annot: + return annot + + # add a /NM PDF key to the object definition + link_names = dict( # existing ids and their xref + [(x[0], x[2]) for x in page.annot_xrefs() if x[1] == PDF_ANNOT_LINK] + ) + + old_name = lnk.get("id", "") # id value in the argument + + if old_name and (lnk["xref"], old_name) in link_names.items(): + name = old_name # no new name if this is an update only + else: + i = 0 + stem = TOOLS.set_annot_stem() + "-L%i" + while True: + name = stem % i + if name not in link_names.values(): + break + i += 1 + # add /NM key to object definition + annot = annot.replace("/Link", "/Link/NM(%s)" % name) + + return annot + + +def delete_widget(page: Page, widget: Widget) -> Widget: + """Delete widget from page and return the next one.""" + CheckParent(page) + annot = getattr(widget, "_annot", None) + if annot is None: + raise ValueError("bad type: widget") + nextwidget = widget.next + page.delete_annot(annot) + widget._annot.__del__() + widget._annot.parent = None + keylist = list(widget.__dict__.keys()) + for key in keylist: + del widget.__dict__[key] + return nextwidget + + +def update_link(page: Page, lnk: dict) -> None: + """Update a link on the current page.""" + CheckParent(page) + annot = getLinkText(page, lnk) + if annot == "": + raise ValueError("link kind not supported") + + page.parent.update_object(lnk["xref"], annot, page=page) + return + + +def insert_link(page: Page, lnk: dict, mark: bool = True) -> None: + """Insert a new link for the current page.""" + CheckParent(page) + annot = getLinkText(page, lnk) + if annot == "": + raise ValueError("link kind not supported") + page._addAnnot_FromString((annot,)) + return + + +def insert_textbox( + page: Page, + rect: rect_like, + buffer: typing.Union[str, list], + fontname: str = "helv", + fontfile: OptStr = None, + set_simple: int = 0, + encoding: int = 0, + fontsize: float = 11, + lineheight: OptFloat = None, + color: OptSeq = None, + fill: OptSeq = None, + expandtabs: int = 1, + align: int = 0, + rotate: int = 0, + render_mode: int = 0, + border_width: float = 1, + morph: OptSeq = None, + overlay: bool = True, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> float: + """Insert text into a given rectangle. + + Notes: + Creates a Shape object, uses its same-named method and commits it. + Parameters: + rect: (rect-like) area to use for text. + buffer: text to be inserted + fontname: a Base-14 font, font name or '/name' + fontfile: name of a font file + fontsize: font size + lineheight: overwrite the font property + color: RGB color triple + expandtabs: handles tabulators with string function + align: left, center, right, justified + rotate: 0, 90, 180, or 270 degrees + morph: morph box with a matrix and a fixpoint + overlay: put text in foreground or background + Returns: + unused or deficit rectangle area (float) + """ + img = page.new_shape() + rc = img.insert_textbox( + rect, + buffer, + fontsize=fontsize, + lineheight=lineheight, + fontname=fontname, + fontfile=fontfile, + set_simple=set_simple, + encoding=encoding, + color=color, + fill=fill, + expandtabs=expandtabs, + render_mode=render_mode, + border_width=border_width, + align=align, + rotate=rotate, + morph=morph, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + if rc >= 0: + img.commit(overlay) + return rc + + +def insert_text( + page: Page, + point: point_like, + text: typing.Union[str, list], + fontsize: float = 11, + lineheight: OptFloat = None, + fontname: str = "helv", + fontfile: OptStr = None, + set_simple: int = 0, + encoding: int = 0, + color: OptSeq = None, + fill: OptSeq = None, + border_width: float = 1, + render_mode: int = 0, + rotate: int = 0, + morph: OptSeq = None, + overlay: bool = True, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +): + img = page.new_shape() + rc = img.insert_text( + point, + text, + fontsize=fontsize, + lineheight=lineheight, + fontname=fontname, + fontfile=fontfile, + set_simple=set_simple, + encoding=encoding, + color=color, + fill=fill, + border_width=border_width, + render_mode=render_mode, + rotate=rotate, + morph=morph, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + if rc >= 0: + img.commit(overlay) + return rc + + +def new_page( + doc: Document, + pno: int = -1, + width: float = 595, + height: float = 842, +) -> Page: + """Create and return a new page object. + + Args: + pno: (int) insert before this page. Default: after last page. + width: (float) page width in points. Default: 595 (ISO A4 width). + height: (float) page height in points. Default 842 (ISO A4 height). + Returns: + A Page object. + """ + doc._newPage(pno, width=width, height=height) + return doc[pno] + + +def insert_page( + doc: Document, + pno: int, + text: typing.Union[str, list, None] = None, + fontsize: float = 11, + width: float = 595, + height: float = 842, + fontname: str = "helv", + fontfile: OptStr = None, + color: OptSeq = (0,), +) -> int: + """Create a new PDF page and insert some text. + + Notes: + Function combining Document.new_page() and Page.insert_text(). + For parameter details see these methods. + """ + page = doc.new_page(pno=pno, width=width, height=height) + if not bool(text): + return 0 + rc = page.insert_text( + (50, 72), + text, + fontsize=fontsize, + fontname=fontname, + fontfile=fontfile, + color=color, + ) + return rc + + +def draw_line( + page: Page, + p1: point_like, + p2: point_like, + color: OptSeq = (0,), + dashes: OptStr = None, + width: float = 1, + lineCap: int = 0, + lineJoin: int = 0, + overlay: bool = True, + morph: OptSeq = None, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc=0, +) -> Point: + """Draw a line from point p1 to point p2.""" + img = page.new_shape() + p = img.draw_line(Point(p1), Point(p2)) + img.finish( + color=color, + dashes=dashes, + width=width, + closePath=False, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return p + + +def draw_squiggle( + page: Page, + p1: point_like, + p2: point_like, + breadth: float = 2, + color: OptSeq = (0,), + dashes: OptStr = None, + width: float = 1, + lineCap: int = 0, + lineJoin: int = 0, + overlay: bool = True, + morph: OptSeq = None, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> Point: + """Draw a squiggly line from point p1 to point p2.""" + img = page.new_shape() + p = img.draw_squiggle(Point(p1), Point(p2), breadth=breadth) + img.finish( + color=color, + dashes=dashes, + width=width, + closePath=False, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return p + + +def draw_zigzag( + page: Page, + p1: point_like, + p2: point_like, + breadth: float = 2, + color: OptSeq = (0,), + dashes: OptStr = None, + width: float = 1, + lineCap: int = 0, + lineJoin: int = 0, + overlay: bool = True, + morph: OptSeq = None, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> Point: + """Draw a zigzag line from point p1 to point p2.""" + img = page.new_shape() + p = img.draw_zigzag(Point(p1), Point(p2), breadth=breadth) + img.finish( + color=color, + dashes=dashes, + width=width, + closePath=False, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return p + + +def draw_rect( + page: Page, + rect: rect_like, + color: OptSeq = (0,), + fill: OptSeq = None, + dashes: OptStr = None, + width: float = 1, + lineCap: int = 0, + lineJoin: int = 0, + morph: OptSeq = None, + overlay: bool = True, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, + radius=None, +) -> Point: + """Draw a rectangle. See Shape class method for details.""" + img = page.new_shape() + Q = img.draw_rect(Rect(rect), radius=radius) + img.finish( + color=color, + fill=fill, + dashes=dashes, + width=width, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return Q + + +def draw_quad( + page: Page, + quad: quad_like, + color: OptSeq = (0,), + fill: OptSeq = None, + dashes: OptStr = None, + width: float = 1, + lineCap: int = 0, + lineJoin: int = 0, + morph: OptSeq = None, + overlay: bool = True, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> Point: + """Draw a quadrilateral.""" + img = page.new_shape() + Q = img.draw_quad(Quad(quad)) + img.finish( + color=color, + fill=fill, + dashes=dashes, + width=width, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return Q + + +def draw_polyline( + page: Page, + points: list, + color: OptSeq = (0,), + fill: OptSeq = None, + dashes: OptStr = None, + width: float = 1, + morph: OptSeq = None, + lineCap: int = 0, + lineJoin: int = 0, + overlay: bool = True, + closePath: bool = False, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> Point: + """Draw multiple connected line segments.""" + img = page.new_shape() + Q = img.draw_polyline(points) + img.finish( + color=color, + fill=fill, + dashes=dashes, + width=width, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + closePath=closePath, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return Q + + +def draw_circle( + page: Page, + center: point_like, + radius: float, + color: OptSeq = (0,), + fill: OptSeq = None, + morph: OptSeq = None, + dashes: OptStr = None, + width: float = 1, + lineCap: int = 0, + lineJoin: int = 0, + overlay: bool = True, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> Point: + """Draw a circle given its center and radius.""" + img = page.new_shape() + Q = img.draw_circle(Point(center), radius) + img.finish( + color=color, + fill=fill, + dashes=dashes, + width=width, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + return Q + + +def draw_oval( + page: Page, + rect: typing.Union[rect_like, quad_like], + color: OptSeq = (0,), + fill: OptSeq = None, + dashes: OptStr = None, + morph: OptSeq = None, + width: float = 1, + lineCap: int = 0, + lineJoin: int = 0, + overlay: bool = True, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> Point: + """Draw an oval given its containing rectangle or quad.""" + img = page.new_shape() + Q = img.draw_oval(rect) + img.finish( + color=color, + fill=fill, + dashes=dashes, + width=width, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return Q + + +def draw_curve( + page: Page, + p1: point_like, + p2: point_like, + p3: point_like, + color: OptSeq = (0,), + fill: OptSeq = None, + dashes: OptStr = None, + width: float = 1, + morph: OptSeq = None, + closePath: bool = False, + lineCap: int = 0, + lineJoin: int = 0, + overlay: bool = True, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> Point: + """Draw a special Bezier curve from p1 to p3, generating control points on lines p1 to p2 and p2 to p3.""" + img = page.new_shape() + Q = img.draw_curve(Point(p1), Point(p2), Point(p3)) + img.finish( + color=color, + fill=fill, + dashes=dashes, + width=width, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + closePath=closePath, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return Q + + +def draw_bezier( + page: Page, + p1: point_like, + p2: point_like, + p3: point_like, + p4: point_like, + color: OptSeq = (0,), + fill: OptSeq = None, + dashes: OptStr = None, + width: float = 1, + morph: OptStr = None, + closePath: bool = False, + lineCap: int = 0, + lineJoin: int = 0, + overlay: bool = True, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> Point: + """Draw a general cubic Bezier curve from p1 to p4 using control points p2 and p3.""" + img = page.new_shape() + Q = img.draw_bezier(Point(p1), Point(p2), Point(p3), Point(p4)) + img.finish( + color=color, + fill=fill, + dashes=dashes, + width=width, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + closePath=closePath, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return Q + + +def draw_sector( + page: Page, + center: point_like, + point: point_like, + beta: float, + color: OptSeq = (0,), + fill: OptSeq = None, + dashes: OptStr = None, + fullSector: bool = True, + morph: OptSeq = None, + width: float = 1, + closePath: bool = False, + lineCap: int = 0, + lineJoin: int = 0, + overlay: bool = True, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, +) -> Point: + """Draw a circle sector given circle center, one arc end point and the angle of the arc. + + Parameters: + center -- center of circle + point -- arc end point + beta -- angle of arc (degrees) + fullSector -- connect arc ends with center + """ + img = page.new_shape() + Q = img.draw_sector(Point(center), Point(point), beta, fullSector=fullSector) + img.finish( + color=color, + fill=fill, + dashes=dashes, + width=width, + lineCap=lineCap, + lineJoin=lineJoin, + morph=morph, + closePath=closePath, + stroke_opacity=stroke_opacity, + fill_opacity=fill_opacity, + oc=oc, + ) + img.commit(overlay) + + return Q + + +# ---------------------------------------------------------------------- +# Name: wx.lib.colourdb.py +# Purpose: Adds a bunch of colour names and RGB values to the +# colour database so they can be found by name +# +# Author: Robin Dunn +# +# Created: 13-March-2001 +# Copyright: (c) 2001-2017 by Total Control Software +# Licence: wxWindows license +# Tags: phoenix-port, unittest, documented +# ---------------------------------------------------------------------- + + +def getColorList() -> list: + """ + Returns a list of just the colour names used by this module. + :rtype: list of strings + """ + + return [x[0] for x in getColorInfoList()] + + +def getColorInfoList() -> list: + """ + Returns the list of colour name/value tuples used by this module. + :rtype: list of tuples + """ + + return [ + ("ALICEBLUE", 240, 248, 255), + ("ANTIQUEWHITE", 250, 235, 215), + ("ANTIQUEWHITE1", 255, 239, 219), + ("ANTIQUEWHITE2", 238, 223, 204), + ("ANTIQUEWHITE3", 205, 192, 176), + ("ANTIQUEWHITE4", 139, 131, 120), + ("AQUAMARINE", 127, 255, 212), + ("AQUAMARINE1", 127, 255, 212), + ("AQUAMARINE2", 118, 238, 198), + ("AQUAMARINE3", 102, 205, 170), + ("AQUAMARINE4", 69, 139, 116), + ("AZURE", 240, 255, 255), + ("AZURE1", 240, 255, 255), + ("AZURE2", 224, 238, 238), + ("AZURE3", 193, 205, 205), + ("AZURE4", 131, 139, 139), + ("BEIGE", 245, 245, 220), + ("BISQUE", 255, 228, 196), + ("BISQUE1", 255, 228, 196), + ("BISQUE2", 238, 213, 183), + ("BISQUE3", 205, 183, 158), + ("BISQUE4", 139, 125, 107), + ("BLACK", 0, 0, 0), + ("BLANCHEDALMOND", 255, 235, 205), + ("BLUE", 0, 0, 255), + ("BLUE1", 0, 0, 255), + ("BLUE2", 0, 0, 238), + ("BLUE3", 0, 0, 205), + ("BLUE4", 0, 0, 139), + ("BLUEVIOLET", 138, 43, 226), + ("BROWN", 165, 42, 42), + ("BROWN1", 255, 64, 64), + ("BROWN2", 238, 59, 59), + ("BROWN3", 205, 51, 51), + ("BROWN4", 139, 35, 35), + ("BURLYWOOD", 222, 184, 135), + ("BURLYWOOD1", 255, 211, 155), + ("BURLYWOOD2", 238, 197, 145), + ("BURLYWOOD3", 205, 170, 125), + ("BURLYWOOD4", 139, 115, 85), + ("CADETBLUE", 95, 158, 160), + ("CADETBLUE1", 152, 245, 255), + ("CADETBLUE2", 142, 229, 238), + ("CADETBLUE3", 122, 197, 205), + ("CADETBLUE4", 83, 134, 139), + ("CHARTREUSE", 127, 255, 0), + ("CHARTREUSE1", 127, 255, 0), + ("CHARTREUSE2", 118, 238, 0), + ("CHARTREUSE3", 102, 205, 0), + ("CHARTREUSE4", 69, 139, 0), + ("CHOCOLATE", 210, 105, 30), + ("CHOCOLATE1", 255, 127, 36), + ("CHOCOLATE2", 238, 118, 33), + ("CHOCOLATE3", 205, 102, 29), + ("CHOCOLATE4", 139, 69, 19), + ("COFFEE", 156, 79, 0), + ("CORAL", 255, 127, 80), + ("CORAL1", 255, 114, 86), + ("CORAL2", 238, 106, 80), + ("CORAL3", 205, 91, 69), + ("CORAL4", 139, 62, 47), + ("CORNFLOWERBLUE", 100, 149, 237), + ("CORNSILK", 255, 248, 220), + ("CORNSILK1", 255, 248, 220), + ("CORNSILK2", 238, 232, 205), + ("CORNSILK3", 205, 200, 177), + ("CORNSILK4", 139, 136, 120), + ("CYAN", 0, 255, 255), + ("CYAN1", 0, 255, 255), + ("CYAN2", 0, 238, 238), + ("CYAN3", 0, 205, 205), + ("CYAN4", 0, 139, 139), + ("DARKBLUE", 0, 0, 139), + ("DARKCYAN", 0, 139, 139), + ("DARKGOLDENROD", 184, 134, 11), + ("DARKGOLDENROD1", 255, 185, 15), + ("DARKGOLDENROD2", 238, 173, 14), + ("DARKGOLDENROD3", 205, 149, 12), + ("DARKGOLDENROD4", 139, 101, 8), + ("DARKGREEN", 0, 100, 0), + ("DARKGRAY", 169, 169, 169), + ("DARKKHAKI", 189, 183, 107), + ("DARKMAGENTA", 139, 0, 139), + ("DARKOLIVEGREEN", 85, 107, 47), + ("DARKOLIVEGREEN1", 202, 255, 112), + ("DARKOLIVEGREEN2", 188, 238, 104), + ("DARKOLIVEGREEN3", 162, 205, 90), + ("DARKOLIVEGREEN4", 110, 139, 61), + ("DARKORANGE", 255, 140, 0), + ("DARKORANGE1", 255, 127, 0), + ("DARKORANGE2", 238, 118, 0), + ("DARKORANGE3", 205, 102, 0), + ("DARKORANGE4", 139, 69, 0), + ("DARKORCHID", 153, 50, 204), + ("DARKORCHID1", 191, 62, 255), + ("DARKORCHID2", 178, 58, 238), + ("DARKORCHID3", 154, 50, 205), + ("DARKORCHID4", 104, 34, 139), + ("DARKRED", 139, 0, 0), + ("DARKSALMON", 233, 150, 122), + ("DARKSEAGREEN", 143, 188, 143), + ("DARKSEAGREEN1", 193, 255, 193), + ("DARKSEAGREEN2", 180, 238, 180), + ("DARKSEAGREEN3", 155, 205, 155), + ("DARKSEAGREEN4", 105, 139, 105), + ("DARKSLATEBLUE", 72, 61, 139), + ("DARKSLATEGRAY", 47, 79, 79), + ("DARKTURQUOISE", 0, 206, 209), + ("DARKVIOLET", 148, 0, 211), + ("DEEPPINK", 255, 20, 147), + ("DEEPPINK1", 255, 20, 147), + ("DEEPPINK2", 238, 18, 137), + ("DEEPPINK3", 205, 16, 118), + ("DEEPPINK4", 139, 10, 80), + ("DEEPSKYBLUE", 0, 191, 255), + ("DEEPSKYBLUE1", 0, 191, 255), + ("DEEPSKYBLUE2", 0, 178, 238), + ("DEEPSKYBLUE3", 0, 154, 205), + ("DEEPSKYBLUE4", 0, 104, 139), + ("DIMGRAY", 105, 105, 105), + ("DODGERBLUE", 30, 144, 255), + ("DODGERBLUE1", 30, 144, 255), + ("DODGERBLUE2", 28, 134, 238), + ("DODGERBLUE3", 24, 116, 205), + ("DODGERBLUE4", 16, 78, 139), + ("FIREBRICK", 178, 34, 34), + ("FIREBRICK1", 255, 48, 48), + ("FIREBRICK2", 238, 44, 44), + ("FIREBRICK3", 205, 38, 38), + ("FIREBRICK4", 139, 26, 26), + ("FLORALWHITE", 255, 250, 240), + ("FORESTGREEN", 34, 139, 34), + ("GAINSBORO", 220, 220, 220), + ("GHOSTWHITE", 248, 248, 255), + ("GOLD", 255, 215, 0), + ("GOLD1", 255, 215, 0), + ("GOLD2", 238, 201, 0), + ("GOLD3", 205, 173, 0), + ("GOLD4", 139, 117, 0), + ("GOLDENROD", 218, 165, 32), + ("GOLDENROD1", 255, 193, 37), + ("GOLDENROD2", 238, 180, 34), + ("GOLDENROD3", 205, 155, 29), + ("GOLDENROD4", 139, 105, 20), + ("GREEN YELLOW", 173, 255, 47), + ("GREEN", 0, 255, 0), + ("GREEN1", 0, 255, 0), + ("GREEN2", 0, 238, 0), + ("GREEN3", 0, 205, 0), + ("GREEN4", 0, 139, 0), + ("GREENYELLOW", 173, 255, 47), + ("GRAY", 190, 190, 190), + ("GRAY0", 0, 0, 0), + ("GRAY1", 3, 3, 3), + ("GRAY10", 26, 26, 26), + ("GRAY100", 255, 255, 255), + ("GRAY11", 28, 28, 28), + ("GRAY12", 31, 31, 31), + ("GRAY13", 33, 33, 33), + ("GRAY14", 36, 36, 36), + ("GRAY15", 38, 38, 38), + ("GRAY16", 41, 41, 41), + ("GRAY17", 43, 43, 43), + ("GRAY18", 46, 46, 46), + ("GRAY19", 48, 48, 48), + ("GRAY2", 5, 5, 5), + ("GRAY20", 51, 51, 51), + ("GRAY21", 54, 54, 54), + ("GRAY22", 56, 56, 56), + ("GRAY23", 59, 59, 59), + ("GRAY24", 61, 61, 61), + ("GRAY25", 64, 64, 64), + ("GRAY26", 66, 66, 66), + ("GRAY27", 69, 69, 69), + ("GRAY28", 71, 71, 71), + ("GRAY29", 74, 74, 74), + ("GRAY3", 8, 8, 8), + ("GRAY30", 77, 77, 77), + ("GRAY31", 79, 79, 79), + ("GRAY32", 82, 82, 82), + ("GRAY33", 84, 84, 84), + ("GRAY34", 87, 87, 87), + ("GRAY35", 89, 89, 89), + ("GRAY36", 92, 92, 92), + ("GRAY37", 94, 94, 94), + ("GRAY38", 97, 97, 97), + ("GRAY39", 99, 99, 99), + ("GRAY4", 10, 10, 10), + ("GRAY40", 102, 102, 102), + ("GRAY41", 105, 105, 105), + ("GRAY42", 107, 107, 107), + ("GRAY43", 110, 110, 110), + ("GRAY44", 112, 112, 112), + ("GRAY45", 115, 115, 115), + ("GRAY46", 117, 117, 117), + ("GRAY47", 120, 120, 120), + ("GRAY48", 122, 122, 122), + ("GRAY49", 125, 125, 125), + ("GRAY5", 13, 13, 13), + ("GRAY50", 127, 127, 127), + ("GRAY51", 130, 130, 130), + ("GRAY52", 133, 133, 133), + ("GRAY53", 135, 135, 135), + ("GRAY54", 138, 138, 138), + ("GRAY55", 140, 140, 140), + ("GRAY56", 143, 143, 143), + ("GRAY57", 145, 145, 145), + ("GRAY58", 148, 148, 148), + ("GRAY59", 150, 150, 150), + ("GRAY6", 15, 15, 15), + ("GRAY60", 153, 153, 153), + ("GRAY61", 156, 156, 156), + ("GRAY62", 158, 158, 158), + ("GRAY63", 161, 161, 161), + ("GRAY64", 163, 163, 163), + ("GRAY65", 166, 166, 166), + ("GRAY66", 168, 168, 168), + ("GRAY67", 171, 171, 171), + ("GRAY68", 173, 173, 173), + ("GRAY69", 176, 176, 176), + ("GRAY7", 18, 18, 18), + ("GRAY70", 179, 179, 179), + ("GRAY71", 181, 181, 181), + ("GRAY72", 184, 184, 184), + ("GRAY73", 186, 186, 186), + ("GRAY74", 189, 189, 189), + ("GRAY75", 191, 191, 191), + ("GRAY76", 194, 194, 194), + ("GRAY77", 196, 196, 196), + ("GRAY78", 199, 199, 199), + ("GRAY79", 201, 201, 201), + ("GRAY8", 20, 20, 20), + ("GRAY80", 204, 204, 204), + ("GRAY81", 207, 207, 207), + ("GRAY82", 209, 209, 209), + ("GRAY83", 212, 212, 212), + ("GRAY84", 214, 214, 214), + ("GRAY85", 217, 217, 217), + ("GRAY86", 219, 219, 219), + ("GRAY87", 222, 222, 222), + ("GRAY88", 224, 224, 224), + ("GRAY89", 227, 227, 227), + ("GRAY9", 23, 23, 23), + ("GRAY90", 229, 229, 229), + ("GRAY91", 232, 232, 232), + ("GRAY92", 235, 235, 235), + ("GRAY93", 237, 237, 237), + ("GRAY94", 240, 240, 240), + ("GRAY95", 242, 242, 242), + ("GRAY96", 245, 245, 245), + ("GRAY97", 247, 247, 247), + ("GRAY98", 250, 250, 250), + ("GRAY99", 252, 252, 252), + ("HONEYDEW", 240, 255, 240), + ("HONEYDEW1", 240, 255, 240), + ("HONEYDEW2", 224, 238, 224), + ("HONEYDEW3", 193, 205, 193), + ("HONEYDEW4", 131, 139, 131), + ("HOTPINK", 255, 105, 180), + ("HOTPINK1", 255, 110, 180), + ("HOTPINK2", 238, 106, 167), + ("HOTPINK3", 205, 96, 144), + ("HOTPINK4", 139, 58, 98), + ("INDIANRED", 205, 92, 92), + ("INDIANRED1", 255, 106, 106), + ("INDIANRED2", 238, 99, 99), + ("INDIANRED3", 205, 85, 85), + ("INDIANRED4", 139, 58, 58), + ("IVORY", 255, 255, 240), + ("IVORY1", 255, 255, 240), + ("IVORY2", 238, 238, 224), + ("IVORY3", 205, 205, 193), + ("IVORY4", 139, 139, 131), + ("KHAKI", 240, 230, 140), + ("KHAKI1", 255, 246, 143), + ("KHAKI2", 238, 230, 133), + ("KHAKI3", 205, 198, 115), + ("KHAKI4", 139, 134, 78), + ("LAVENDER", 230, 230, 250), + ("LAVENDERBLUSH", 255, 240, 245), + ("LAVENDERBLUSH1", 255, 240, 245), + ("LAVENDERBLUSH2", 238, 224, 229), + ("LAVENDERBLUSH3", 205, 193, 197), + ("LAVENDERBLUSH4", 139, 131, 134), + ("LAWNGREEN", 124, 252, 0), + ("LEMONCHIFFON", 255, 250, 205), + ("LEMONCHIFFON1", 255, 250, 205), + ("LEMONCHIFFON2", 238, 233, 191), + ("LEMONCHIFFON3", 205, 201, 165), + ("LEMONCHIFFON4", 139, 137, 112), + ("LIGHTBLUE", 173, 216, 230), + ("LIGHTBLUE1", 191, 239, 255), + ("LIGHTBLUE2", 178, 223, 238), + ("LIGHTBLUE3", 154, 192, 205), + ("LIGHTBLUE4", 104, 131, 139), + ("LIGHTCORAL", 240, 128, 128), + ("LIGHTCYAN", 224, 255, 255), + ("LIGHTCYAN1", 224, 255, 255), + ("LIGHTCYAN2", 209, 238, 238), + ("LIGHTCYAN3", 180, 205, 205), + ("LIGHTCYAN4", 122, 139, 139), + ("LIGHTGOLDENROD", 238, 221, 130), + ("LIGHTGOLDENROD1", 255, 236, 139), + ("LIGHTGOLDENROD2", 238, 220, 130), + ("LIGHTGOLDENROD3", 205, 190, 112), + ("LIGHTGOLDENROD4", 139, 129, 76), + ("LIGHTGOLDENRODYELLOW", 250, 250, 210), + ("LIGHTGREEN", 144, 238, 144), + ("LIGHTGRAY", 211, 211, 211), + ("LIGHTPINK", 255, 182, 193), + ("LIGHTPINK1", 255, 174, 185), + ("LIGHTPINK2", 238, 162, 173), + ("LIGHTPINK3", 205, 140, 149), + ("LIGHTPINK4", 139, 95, 101), + ("LIGHTSALMON", 255, 160, 122), + ("LIGHTSALMON1", 255, 160, 122), + ("LIGHTSALMON2", 238, 149, 114), + ("LIGHTSALMON3", 205, 129, 98), + ("LIGHTSALMON4", 139, 87, 66), + ("LIGHTSEAGREEN", 32, 178, 170), + ("LIGHTSKYBLUE", 135, 206, 250), + ("LIGHTSKYBLUE1", 176, 226, 255), + ("LIGHTSKYBLUE2", 164, 211, 238), + ("LIGHTSKYBLUE3", 141, 182, 205), + ("LIGHTSKYBLUE4", 96, 123, 139), + ("LIGHTSLATEBLUE", 132, 112, 255), + ("LIGHTSLATEGRAY", 119, 136, 153), + ("LIGHTSTEELBLUE", 176, 196, 222), + ("LIGHTSTEELBLUE1", 202, 225, 255), + ("LIGHTSTEELBLUE2", 188, 210, 238), + ("LIGHTSTEELBLUE3", 162, 181, 205), + ("LIGHTSTEELBLUE4", 110, 123, 139), + ("LIGHTYELLOW", 255, 255, 224), + ("LIGHTYELLOW1", 255, 255, 224), + ("LIGHTYELLOW2", 238, 238, 209), + ("LIGHTYELLOW3", 205, 205, 180), + ("LIGHTYELLOW4", 139, 139, 122), + ("LIMEGREEN", 50, 205, 50), + ("LINEN", 250, 240, 230), + ("MAGENTA", 255, 0, 255), + ("MAGENTA1", 255, 0, 255), + ("MAGENTA2", 238, 0, 238), + ("MAGENTA3", 205, 0, 205), + ("MAGENTA4", 139, 0, 139), + ("MAROON", 176, 48, 96), + ("MAROON1", 255, 52, 179), + ("MAROON2", 238, 48, 167), + ("MAROON3", 205, 41, 144), + ("MAROON4", 139, 28, 98), + ("MEDIUMAQUAMARINE", 102, 205, 170), + ("MEDIUMBLUE", 0, 0, 205), + ("MEDIUMORCHID", 186, 85, 211), + ("MEDIUMORCHID1", 224, 102, 255), + ("MEDIUMORCHID2", 209, 95, 238), + ("MEDIUMORCHID3", 180, 82, 205), + ("MEDIUMORCHID4", 122, 55, 139), + ("MEDIUMPURPLE", 147, 112, 219), + ("MEDIUMPURPLE1", 171, 130, 255), + ("MEDIUMPURPLE2", 159, 121, 238), + ("MEDIUMPURPLE3", 137, 104, 205), + ("MEDIUMPURPLE4", 93, 71, 139), + ("MEDIUMSEAGREEN", 60, 179, 113), + ("MEDIUMSLATEBLUE", 123, 104, 238), + ("MEDIUMSPRINGGREEN", 0, 250, 154), + ("MEDIUMTURQUOISE", 72, 209, 204), + ("MEDIUMVIOLETRED", 199, 21, 133), + ("MIDNIGHTBLUE", 25, 25, 112), + ("MINTCREAM", 245, 255, 250), + ("MISTYROSE", 255, 228, 225), + ("MISTYROSE1", 255, 228, 225), + ("MISTYROSE2", 238, 213, 210), + ("MISTYROSE3", 205, 183, 181), + ("MISTYROSE4", 139, 125, 123), + ("MOCCASIN", 255, 228, 181), + ("MUPDFBLUE", 37, 114, 172), + ("NAVAJOWHITE", 255, 222, 173), + ("NAVAJOWHITE1", 255, 222, 173), + ("NAVAJOWHITE2", 238, 207, 161), + ("NAVAJOWHITE3", 205, 179, 139), + ("NAVAJOWHITE4", 139, 121, 94), + ("NAVY", 0, 0, 128), + ("NAVYBLUE", 0, 0, 128), + ("OLDLACE", 253, 245, 230), + ("OLIVEDRAB", 107, 142, 35), + ("OLIVEDRAB1", 192, 255, 62), + ("OLIVEDRAB2", 179, 238, 58), + ("OLIVEDRAB3", 154, 205, 50), + ("OLIVEDRAB4", 105, 139, 34), + ("ORANGE", 255, 165, 0), + ("ORANGE1", 255, 165, 0), + ("ORANGE2", 238, 154, 0), + ("ORANGE3", 205, 133, 0), + ("ORANGE4", 139, 90, 0), + ("ORANGERED", 255, 69, 0), + ("ORANGERED1", 255, 69, 0), + ("ORANGERED2", 238, 64, 0), + ("ORANGERED3", 205, 55, 0), + ("ORANGERED4", 139, 37, 0), + ("ORCHID", 218, 112, 214), + ("ORCHID1", 255, 131, 250), + ("ORCHID2", 238, 122, 233), + ("ORCHID3", 205, 105, 201), + ("ORCHID4", 139, 71, 137), + ("PALEGOLDENROD", 238, 232, 170), + ("PALEGREEN", 152, 251, 152), + ("PALEGREEN1", 154, 255, 154), + ("PALEGREEN2", 144, 238, 144), + ("PALEGREEN3", 124, 205, 124), + ("PALEGREEN4", 84, 139, 84), + ("PALETURQUOISE", 175, 238, 238), + ("PALETURQUOISE1", 187, 255, 255), + ("PALETURQUOISE2", 174, 238, 238), + ("PALETURQUOISE3", 150, 205, 205), + ("PALETURQUOISE4", 102, 139, 139), + ("PALEVIOLETRED", 219, 112, 147), + ("PALEVIOLETRED1", 255, 130, 171), + ("PALEVIOLETRED2", 238, 121, 159), + ("PALEVIOLETRED3", 205, 104, 137), + ("PALEVIOLETRED4", 139, 71, 93), + ("PAPAYAWHIP", 255, 239, 213), + ("PEACHPUFF", 255, 218, 185), + ("PEACHPUFF1", 255, 218, 185), + ("PEACHPUFF2", 238, 203, 173), + ("PEACHPUFF3", 205, 175, 149), + ("PEACHPUFF4", 139, 119, 101), + ("PERU", 205, 133, 63), + ("PINK", 255, 192, 203), + ("PINK1", 255, 181, 197), + ("PINK2", 238, 169, 184), + ("PINK3", 205, 145, 158), + ("PINK4", 139, 99, 108), + ("PLUM", 221, 160, 221), + ("PLUM1", 255, 187, 255), + ("PLUM2", 238, 174, 238), + ("PLUM3", 205, 150, 205), + ("PLUM4", 139, 102, 139), + ("POWDERBLUE", 176, 224, 230), + ("PURPLE", 160, 32, 240), + ("PURPLE1", 155, 48, 255), + ("PURPLE2", 145, 44, 238), + ("PURPLE3", 125, 38, 205), + ("PURPLE4", 85, 26, 139), + ("PY_COLOR", 240, 255, 210), + ("RED", 255, 0, 0), + ("RED1", 255, 0, 0), + ("RED2", 238, 0, 0), + ("RED3", 205, 0, 0), + ("RED4", 139, 0, 0), + ("ROSYBROWN", 188, 143, 143), + ("ROSYBROWN1", 255, 193, 193), + ("ROSYBROWN2", 238, 180, 180), + ("ROSYBROWN3", 205, 155, 155), + ("ROSYBROWN4", 139, 105, 105), + ("ROYALBLUE", 65, 105, 225), + ("ROYALBLUE1", 72, 118, 255), + ("ROYALBLUE2", 67, 110, 238), + ("ROYALBLUE3", 58, 95, 205), + ("ROYALBLUE4", 39, 64, 139), + ("SADDLEBROWN", 139, 69, 19), + ("SALMON", 250, 128, 114), + ("SALMON1", 255, 140, 105), + ("SALMON2", 238, 130, 98), + ("SALMON3", 205, 112, 84), + ("SALMON4", 139, 76, 57), + ("SANDYBROWN", 244, 164, 96), + ("SEAGREEN", 46, 139, 87), + ("SEAGREEN1", 84, 255, 159), + ("SEAGREEN2", 78, 238, 148), + ("SEAGREEN3", 67, 205, 128), + ("SEAGREEN4", 46, 139, 87), + ("SEASHELL", 255, 245, 238), + ("SEASHELL1", 255, 245, 238), + ("SEASHELL2", 238, 229, 222), + ("SEASHELL3", 205, 197, 191), + ("SEASHELL4", 139, 134, 130), + ("SIENNA", 160, 82, 45), + ("SIENNA1", 255, 130, 71), + ("SIENNA2", 238, 121, 66), + ("SIENNA3", 205, 104, 57), + ("SIENNA4", 139, 71, 38), + ("SKYBLUE", 135, 206, 235), + ("SKYBLUE1", 135, 206, 255), + ("SKYBLUE2", 126, 192, 238), + ("SKYBLUE3", 108, 166, 205), + ("SKYBLUE4", 74, 112, 139), + ("SLATEBLUE", 106, 90, 205), + ("SLATEBLUE1", 131, 111, 255), + ("SLATEBLUE2", 122, 103, 238), + ("SLATEBLUE3", 105, 89, 205), + ("SLATEBLUE4", 71, 60, 139), + ("SLATEGRAY", 112, 128, 144), + ("SNOW", 255, 250, 250), + ("SNOW1", 255, 250, 250), + ("SNOW2", 238, 233, 233), + ("SNOW3", 205, 201, 201), + ("SNOW4", 139, 137, 137), + ("SPRINGGREEN", 0, 255, 127), + ("SPRINGGREEN1", 0, 255, 127), + ("SPRINGGREEN2", 0, 238, 118), + ("SPRINGGREEN3", 0, 205, 102), + ("SPRINGGREEN4", 0, 139, 69), + ("STEELBLUE", 70, 130, 180), + ("STEELBLUE1", 99, 184, 255), + ("STEELBLUE2", 92, 172, 238), + ("STEELBLUE3", 79, 148, 205), + ("STEELBLUE4", 54, 100, 139), + ("TAN", 210, 180, 140), + ("TAN1", 255, 165, 79), + ("TAN2", 238, 154, 73), + ("TAN3", 205, 133, 63), + ("TAN4", 139, 90, 43), + ("THISTLE", 216, 191, 216), + ("THISTLE1", 255, 225, 255), + ("THISTLE2", 238, 210, 238), + ("THISTLE3", 205, 181, 205), + ("THISTLE4", 139, 123, 139), + ("TOMATO", 255, 99, 71), + ("TOMATO1", 255, 99, 71), + ("TOMATO2", 238, 92, 66), + ("TOMATO3", 205, 79, 57), + ("TOMATO4", 139, 54, 38), + ("TURQUOISE", 64, 224, 208), + ("TURQUOISE1", 0, 245, 255), + ("TURQUOISE2", 0, 229, 238), + ("TURQUOISE3", 0, 197, 205), + ("TURQUOISE4", 0, 134, 139), + ("VIOLET", 238, 130, 238), + ("VIOLETRED", 208, 32, 144), + ("VIOLETRED1", 255, 62, 150), + ("VIOLETRED2", 238, 58, 140), + ("VIOLETRED3", 205, 50, 120), + ("VIOLETRED4", 139, 34, 82), + ("WHEAT", 245, 222, 179), + ("WHEAT1", 255, 231, 186), + ("WHEAT2", 238, 216, 174), + ("WHEAT3", 205, 186, 150), + ("WHEAT4", 139, 126, 102), + ("WHITE", 255, 255, 255), + ("WHITESMOKE", 245, 245, 245), + ("YELLOW", 255, 255, 0), + ("YELLOW1", 255, 255, 0), + ("YELLOW2", 238, 238, 0), + ("YELLOW3", 205, 205, 0), + ("YELLOW4", 139, 139, 0), + ("YELLOWGREEN", 154, 205, 50), + ] + + +def getColorInfoDict() -> dict: + d = {} + for item in getColorInfoList(): + d[item[0].lower()] = item[1:] + return d + + +def getColor(name: str) -> tuple: + """Retrieve RGB color in PDF format by name. + + Returns: + a triple of floats in range 0 to 1. In case of name-not-found, "white" is returned. + """ + try: + c = getColorInfoList()[getColorList().index(name.upper())] + return (c[1] / 255.0, c[2] / 255.0, c[3] / 255.0) + except: + return (1, 1, 1) + + +def getColorHSV(name: str) -> tuple: + """Retrieve the hue, saturation, value triple of a color name. + + Returns: + a triple (degree, percent, percent). If not found (-1, -1, -1) is returned. + """ + try: + x = getColorInfoList()[getColorList().index(name.upper())] + except: + return (-1, -1, -1) + + r = x[1] / 255.0 + g = x[2] / 255.0 + b = x[3] / 255.0 + cmax = max(r, g, b) + V = round(cmax * 100, 1) + cmin = min(r, g, b) + delta = cmax - cmin + if delta == 0: + hue = 0 + elif cmax == r: + hue = 60.0 * (((g - b) / delta) % 6) + elif cmax == g: + hue = 60.0 * (((b - r) / delta) + 2) + else: + hue = 60.0 * (((r - g) / delta) + 4) + + H = int(round(hue)) + + if cmax == 0: + sat = 0 + else: + sat = delta / cmax + S = int(round(sat * 100)) + + return (H, S, V) + + +def _get_font_properties(doc: Document, xref: int) -> tuple: + fontname, ext, stype, buffer = doc.extract_font(xref) + asc = 0.8 + dsc = -0.2 + if ext == "": + return fontname, ext, stype, asc, dsc + + if buffer: + try: + font = Font(fontbuffer=buffer) + asc = font.ascender + dsc = font.descender + bbox = font.bbox + if asc - dsc < 1: + if bbox.y0 < dsc: + dsc = bbox.y0 + asc = 1 - dsc + except: + asc *= 1.2 + dsc *= 1.2 + return fontname, ext, stype, asc, dsc + if ext != "n/a": + try: + font = Font(fontname) + asc = font.ascender + dsc = font.descender + except: + asc *= 1.2 + dsc *= 1.2 + else: + asc *= 1.2 + dsc *= 1.2 + return fontname, ext, stype, asc, dsc + + +def get_char_widths( + doc: Document, xref: int, limit: int = 256, idx: int = 0, fontdict: OptDict = None +) -> list: + """Get list of glyph information of a font. + + Notes: + Must be provided by its XREF number. If we already dealt with the + font, it will be recorded in doc.FontInfos. Otherwise we insert an + entry there. + Finally we return the glyphs for the font. This is a list of + (glyph, width) where glyph is an integer controlling the char + appearance, and width is a float controlling the char's spacing: + width * fontsize is the actual space. + For 'simple' fonts, glyph == ord(char) will usually be true. + Exceptions are 'Symbol' and 'ZapfDingbats'. We are providing data for these directly here. + """ + fontinfo = CheckFontInfo(doc, xref) + if fontinfo is None: # not recorded yet: create it + if fontdict is None: + name, ext, stype, asc, dsc = _get_font_properties(doc, xref) + fontdict = { + "name": name, + "type": stype, + "ext": ext, + "ascender": asc, + "descender": dsc, + } + else: + name = fontdict["name"] + ext = fontdict["ext"] + stype = fontdict["type"] + ordering = fontdict["ordering"] + simple = fontdict["simple"] + + if ext == "": + raise ValueError("xref is not a font") + + # check for 'simple' fonts + if stype in ("Type1", "MMType1", "TrueType"): + simple = True + else: + simple = False + + # check for CJK fonts + if name in ("Fangti", "Ming"): + ordering = 0 + elif name in ("Heiti", "Song"): + ordering = 1 + elif name in ("Gothic", "Mincho"): + ordering = 2 + elif name in ("Dotum", "Batang"): + ordering = 3 + else: + ordering = -1 + + fontdict["simple"] = simple + + if name == "ZapfDingbats": + glyphs = zapf_glyphs + elif name == "Symbol": + glyphs = symbol_glyphs + else: + glyphs = None + + fontdict["glyphs"] = glyphs + fontdict["ordering"] = ordering + fontinfo = [xref, fontdict] + doc.FontInfos.append(fontinfo) + else: + fontdict = fontinfo[1] + glyphs = fontdict["glyphs"] + simple = fontdict["simple"] + ordering = fontdict["ordering"] + + if glyphs is None: + oldlimit = 0 + else: + oldlimit = len(glyphs) + + mylimit = max(256, limit) + + if mylimit <= oldlimit: + return glyphs + + if ordering < 0: # not a CJK font + glyphs = doc._get_char_widths( + xref, fontdict["name"], fontdict["ext"], fontdict["ordering"], mylimit, idx + ) + else: # CJK fonts use char codes and width = 1 + glyphs = None + + fontdict["glyphs"] = glyphs + fontinfo[1] = fontdict + UpdateFontInfo(doc, fontinfo) + + return glyphs + + +class Shape(object): + """Create a new shape.""" + + @staticmethod + def horizontal_angle(C, P): + """Return the angle to the horizontal for the connection from C to P. + This uses the arcus sine function and resolves its inherent ambiguity by + looking up in which quadrant vector S = P - C is located. + """ + S = Point(P - C).unit # unit vector 'C' -> 'P' + alfa = math.asin(abs(S.y)) # absolute angle from horizontal + if S.x < 0: # make arcsin result unique + if S.y <= 0: # bottom-left + alfa = -(math.pi - alfa) + else: # top-left + alfa = math.pi - alfa + else: + if S.y >= 0: # top-right + pass + else: # bottom-right + alfa = -alfa + return alfa + + def __init__(self, page: Page): + CheckParent(page) + self.page = page + self.doc = page.parent + if not self.doc.is_pdf: + raise ValueError("is no PDF") + self.height = page.mediabox_size.y + self.width = page.mediabox_size.x + self.x = page.cropbox_position.x + self.y = page.cropbox_position.y + + self.pctm = page.transformation_matrix # page transf. matrix + self.ipctm = ~self.pctm # inverted transf. matrix + + self.draw_cont = "" + self.text_cont = "" + self.totalcont = "" + self.lastPoint = None + self.rect = None + + def updateRect(self, x): + if self.rect is None: + if len(x) == 2: + self.rect = Rect(x, x) + else: + self.rect = Rect(x) + + else: + if len(x) == 2: + x = Point(x) + self.rect.x0 = min(self.rect.x0, x.x) + self.rect.y0 = min(self.rect.y0, x.y) + self.rect.x1 = max(self.rect.x1, x.x) + self.rect.y1 = max(self.rect.y1, x.y) + else: + x = Rect(x) + self.rect.x0 = min(self.rect.x0, x.x0) + self.rect.y0 = min(self.rect.y0, x.y0) + self.rect.x1 = max(self.rect.x1, x.x1) + self.rect.y1 = max(self.rect.y1, x.y1) + + def draw_line(self, p1: point_like, p2: point_like) -> Point: + """Draw a line between two points.""" + p1 = Point(p1) + p2 = Point(p2) + if not (self.lastPoint == p1): + self.draw_cont += "%g %g m\n" % JM_TUPLE(p1 * self.ipctm) + self.lastPoint = p1 + self.updateRect(p1) + + self.draw_cont += "%g %g l\n" % JM_TUPLE(p2 * self.ipctm) + self.updateRect(p2) + self.lastPoint = p2 + return self.lastPoint + + def draw_polyline(self, points: list) -> Point: + """Draw several connected line segments.""" + for i, p in enumerate(points): + if i == 0: + if not (self.lastPoint == Point(p)): + self.draw_cont += "%g %g m\n" % JM_TUPLE(Point(p) * self.ipctm) + self.lastPoint = Point(p) + else: + self.draw_cont += "%g %g l\n" % JM_TUPLE(Point(p) * self.ipctm) + self.updateRect(p) + + self.lastPoint = Point(points[-1]) + return self.lastPoint + + def draw_bezier( + self, + p1: point_like, + p2: point_like, + p3: point_like, + p4: point_like, + ) -> Point: + """Draw a standard cubic Bezier curve.""" + p1 = Point(p1) + p2 = Point(p2) + p3 = Point(p3) + p4 = Point(p4) + if not (self.lastPoint == p1): + self.draw_cont += "%g %g m\n" % JM_TUPLE(p1 * self.ipctm) + self.draw_cont += "%g %g %g %g %g %g c\n" % JM_TUPLE( + list(p2 * self.ipctm) + list(p3 * self.ipctm) + list(p4 * self.ipctm) + ) + self.updateRect(p1) + self.updateRect(p2) + self.updateRect(p3) + self.updateRect(p4) + self.lastPoint = p4 + return self.lastPoint + + def draw_oval(self, tetra: typing.Union[quad_like, rect_like]) -> Point: + """Draw an ellipse inside a tetrapod.""" + if len(tetra) != 4: + raise ValueError("invalid arg length") + if hasattr(tetra[0], "__float__"): + q = Rect(tetra).quad + else: + q = Quad(tetra) + + mt = q.ul + (q.ur - q.ul) * 0.5 + mr = q.ur + (q.lr - q.ur) * 0.5 + mb = q.ll + (q.lr - q.ll) * 0.5 + ml = q.ul + (q.ll - q.ul) * 0.5 + if not (self.lastPoint == ml): + self.draw_cont += "%g %g m\n" % JM_TUPLE(ml * self.ipctm) + self.lastPoint = ml + self.draw_curve(ml, q.ll, mb) + self.draw_curve(mb, q.lr, mr) + self.draw_curve(mr, q.ur, mt) + self.draw_curve(mt, q.ul, ml) + self.updateRect(q.rect) + self.lastPoint = ml + return self.lastPoint + + def draw_circle(self, center: point_like, radius: float) -> Point: + """Draw a circle given its center and radius.""" + if not radius > EPSILON: + raise ValueError("radius must be positive") + center = Point(center) + p1 = center - (radius, 0) + return self.draw_sector(center, p1, 360, fullSector=False) + + def draw_curve( + self, + p1: point_like, + p2: point_like, + p3: point_like, + ) -> Point: + """Draw a curve between points using one control point.""" + kappa = 0.55228474983 + p1 = Point(p1) + p2 = Point(p2) + p3 = Point(p3) + k1 = p1 + (p2 - p1) * kappa + k2 = p3 + (p2 - p3) * kappa + return self.draw_bezier(p1, k1, k2, p3) + + def draw_sector( + self, + center: point_like, + point: point_like, + beta: float, + fullSector: bool = True, + ) -> Point: + """Draw a circle sector.""" + center = Point(center) + point = Point(point) + l3 = "%g %g m\n" + l4 = "%g %g %g %g %g %g c\n" + l5 = "%g %g l\n" + betar = math.radians(-beta) + w360 = math.radians(math.copysign(360, betar)) * (-1) + w90 = math.radians(math.copysign(90, betar)) + w45 = w90 / 2 + while abs(betar) > 2 * math.pi: + betar += w360 # bring angle below 360 degrees + if not (self.lastPoint == point): + self.draw_cont += l3 % JM_TUPLE(point * self.ipctm) + self.lastPoint = point + Q = Point(0, 0) # just make sure it exists + C = center + P = point + S = P - C # vector 'center' -> 'point' + rad = abs(S) # circle radius + + if not rad > EPSILON: + raise ValueError("radius must be positive") + + alfa = self.horizontal_angle(center, point) + while abs(betar) > abs(w90): # draw 90 degree arcs + q1 = C.x + math.cos(alfa + w90) * rad + q2 = C.y + math.sin(alfa + w90) * rad + Q = Point(q1, q2) # the arc's end point + r1 = C.x + math.cos(alfa + w45) * rad / math.cos(w45) + r2 = C.y + math.sin(alfa + w45) * rad / math.cos(w45) + R = Point(r1, r2) # crossing point of tangents + kappah = (1 - math.cos(w45)) * 4 / 3 / abs(R - Q) + kappa = kappah * abs(P - Q) + cp1 = P + (R - P) * kappa # control point 1 + cp2 = Q + (R - Q) * kappa # control point 2 + self.draw_cont += l4 % JM_TUPLE( + list(cp1 * self.ipctm) + list(cp2 * self.ipctm) + list(Q * self.ipctm) + ) + + betar -= w90 # reduce parm angle by 90 deg + alfa += w90 # advance start angle by 90 deg + P = Q # advance to arc end point + # draw (remaining) arc + if abs(betar) > 1e-3: # significant degrees left? + beta2 = betar / 2 + q1 = C.x + math.cos(alfa + betar) * rad + q2 = C.y + math.sin(alfa + betar) * rad + Q = Point(q1, q2) # the arc's end point + r1 = C.x + math.cos(alfa + beta2) * rad / math.cos(beta2) + r2 = C.y + math.sin(alfa + beta2) * rad / math.cos(beta2) + R = Point(r1, r2) # crossing point of tangents + # kappa height is 4/3 of segment height + kappah = (1 - math.cos(beta2)) * 4 / 3 / abs(R - Q) # kappa height + kappa = kappah * abs(P - Q) / (1 - math.cos(betar)) + cp1 = P + (R - P) * kappa # control point 1 + cp2 = Q + (R - Q) * kappa # control point 2 + self.draw_cont += l4 % JM_TUPLE( + list(cp1 * self.ipctm) + list(cp2 * self.ipctm) + list(Q * self.ipctm) + ) + if fullSector: + self.draw_cont += l3 % JM_TUPLE(point * self.ipctm) + self.draw_cont += l5 % JM_TUPLE(center * self.ipctm) + self.draw_cont += l5 % JM_TUPLE(Q * self.ipctm) + self.lastPoint = Q + return self.lastPoint + + def draw_rect(self, rect: rect_like, *, radius=None) -> Point: + """Draw a rectangle. + + Args: + radius: if not None, the rectangle will have rounded corners. + This is the radius of the curvature, given as percentage of + the rectangle width or height. Valid are values 0 < v <= 0.5. + For a sequence of two values, the corners will have different + radii. Otherwise, the percentage will be computed from the + shorter side. A value of (0.5, 0.5) will draw an ellipse. + """ + r = Rect(rect) + if radius == None: # standard rectangle + self.draw_cont += "%g %g %g %g re\n" % JM_TUPLE( + list(r.bl * self.ipctm) + [r.width, r.height] + ) + self.updateRect(r) + self.lastPoint = r.tl + return self.lastPoint + # rounded corners requested. This requires 1 or 2 values, each + # with 0 < value <= 0.5 + if hasattr(radius, "__float__"): + if radius <= 0 or radius > 0.5: + raise ValueError(f"bad radius value {radius}.") + d = min(r.width, r.height) * radius + px = (d, 0) + py = (0, d) + elif hasattr(radius, "__len__") and len(radius) == 2: + rx, ry = radius + px = (rx * r.width, 0) + py = (0, ry * r.height) + if min(rx, ry) <= 0 or max(rx, ry) > 0.5: + raise ValueError(f"bad radius value {radius}.") + else: + raise ValueError(f"bad radius value {radius}.") + + lp = self.draw_line(r.tl + py, r.bl - py) + lp = self.draw_curve(lp, r.bl, r.bl + px) + + lp = self.draw_line(lp, r.br - px) + lp = self.draw_curve(lp, r.br, r.br - py) + + lp = self.draw_line(lp, r.tr + py) + lp = self.draw_curve(lp, r.tr, r.tr - px) + + lp = self.draw_line(lp, r.tl + px) + self.lastPoint = self.draw_curve(lp, r.tl, r.tl + py) + + self.updateRect(r) + return self.lastPoint + + def draw_quad(self, quad: quad_like) -> Point: + """Draw a Quad.""" + q = Quad(quad) + return self.draw_polyline([q.ul, q.ll, q.lr, q.ur, q.ul]) + + def draw_zigzag( + self, + p1: point_like, + p2: point_like, + breadth: float = 2, + ) -> Point: + """Draw a zig-zagged line from p1 to p2.""" + p1 = Point(p1) + p2 = Point(p2) + S = p2 - p1 # vector start - end + rad = abs(S) # distance of points + cnt = 4 * int(round(rad / (4 * breadth), 0)) # always take full phases + if cnt < 4: + raise ValueError("points too close") + mb = rad / cnt # revised breadth + matrix = Matrix(util_hor_matrix(p1, p2)) # normalize line to x-axis + i_mat = ~matrix # get original position + points = [] # stores edges + for i in range(1, cnt): + if i % 4 == 1: # point "above" connection + p = Point(i, -1) * mb + elif i % 4 == 3: # point "below" connection + p = Point(i, 1) * mb + else: # ignore others + continue + points.append(p * i_mat) + self.draw_polyline([p1] + points + [p2]) # add start and end points + return p2 + + def draw_squiggle( + self, + p1: point_like, + p2: point_like, + breadth=2, + ) -> Point: + """Draw a squiggly line from p1 to p2.""" + p1 = Point(p1) + p2 = Point(p2) + S = p2 - p1 # vector start - end + rad = abs(S) # distance of points + cnt = 4 * int(round(rad / (4 * breadth), 0)) # always take full phases + if cnt < 4: + raise ValueError("points too close") + mb = rad / cnt # revised breadth + matrix = Matrix(util_hor_matrix(p1, p2)) # normalize line to x-axis + i_mat = ~matrix # get original position + k = 2.4142135623765633 # y of draw_curve helper point + + points = [] # stores edges + for i in range(1, cnt): + if i % 4 == 1: # point "above" connection + p = Point(i, -k) * mb + elif i % 4 == 3: # point "below" connection + p = Point(i, k) * mb + else: # else on connection line + p = Point(i, 0) * mb + points.append(p * i_mat) + + points = [p1] + points + [p2] + cnt = len(points) + i = 0 + while i + 2 < cnt: + self.draw_curve(points[i], points[i + 1], points[i + 2]) + i += 2 + return p2 + + # ============================================================================== + # Shape.insert_text + # ============================================================================== + def insert_text( + self, + point: point_like, + buffer: typing.Union[str, list], + fontsize: float = 11, + lineheight: OptFloat = None, + fontname: str = "helv", + fontfile: OptStr = None, + set_simple: bool = 0, + encoding: int = 0, + color: OptSeq = None, + fill: OptSeq = None, + render_mode: int = 0, + border_width: float = 1, + rotate: int = 0, + morph: OptSeq = None, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, + ) -> int: + # ensure 'text' is a list of strings, worth dealing with + if not bool(buffer): + return 0 + + if type(buffer) not in (list, tuple): + text = buffer.splitlines() + else: + text = buffer + + if not len(text) > 0: + return 0 + + point = Point(point) + try: + maxcode = max([ord(c) for c in " ".join(text)]) + except: + return 0 + + # ensure valid 'fontname' + fname = fontname + if fname.startswith("/"): + fname = fname[1:] + + xref = self.page.insert_font( + fontname=fname, fontfile=fontfile, encoding=encoding, set_simple=set_simple + ) + fontinfo = CheckFontInfo(self.doc, xref) + + fontdict = fontinfo[1] + ordering = fontdict["ordering"] + simple = fontdict["simple"] + bfname = fontdict["name"] + ascender = fontdict["ascender"] + descender = fontdict["descender"] + if lineheight: + lheight = fontsize * lineheight + elif ascender - descender <= 1: + lheight = fontsize * 1.2 + else: + lheight = fontsize * (ascender - descender) + + if maxcode > 255: + glyphs = self.doc.get_char_widths(xref, maxcode + 1) + else: + glyphs = fontdict["glyphs"] + + tab = [] + for t in text: + if simple and bfname not in ("Symbol", "ZapfDingbats"): + g = None + else: + g = glyphs + tab.append(getTJstr(t, g, simple, ordering)) + text = tab + + color_str = ColorCode(color, "c") + fill_str = ColorCode(fill, "f") + if not fill and render_mode == 0: # ensure fill color when 0 Tr + fill = color + fill_str = ColorCode(color, "f") + + morphing = CheckMorph(morph) + rot = rotate + if rot % 90 != 0: + raise ValueError("bad rotate value") + + while rot < 0: + rot += 360 + rot = rot % 360 # text rotate = 0, 90, 270, 180 + + templ1 = "\nq\n%s%sBT\n%s1 0 0 1 %g %g Tm\n/%s %g Tf " + templ2 = "TJ\n0 -%g TD\n" + cmp90 = "0 1 -1 0 0 0 cm\n" # rotates 90 deg counter-clockwise + cmm90 = "0 -1 1 0 0 0 cm\n" # rotates 90 deg clockwise + cm180 = "-1 0 0 -1 0 0 cm\n" # rotates by 180 deg. + height = self.height + width = self.width + + # setting up for standard rotation directions + # case rotate = 0 + if morphing: + m1 = Matrix(1, 0, 0, 1, morph[0].x + self.x, height - morph[0].y - self.y) + mat = ~m1 * morph[1] * m1 + cm = "%g %g %g %g %g %g cm\n" % JM_TUPLE(mat) + else: + cm = "" + top = height - point.y - self.y # start of 1st char + left = point.x + self.x # start of 1. char + space = top # space available + headroom = point.y + self.y # distance to page border + if rot == 90: + left = height - point.y - self.y + top = -point.x - self.x + cm += cmp90 + space = width - abs(top) + headroom = point.x + self.x + + elif rot == 270: + left = -height + point.y + self.y + top = point.x + self.x + cm += cmm90 + space = abs(top) + headroom = width - point.x - self.x + + elif rot == 180: + left = -point.x - self.x + top = -height + point.y + self.y + cm += cm180 + space = abs(point.y + self.y) + headroom = height - point.y - self.y + + optcont = self.page._get_optional_content(oc) + if optcont != None: + bdc = "/OC /%s BDC\n" % optcont + emc = "EMC\n" + else: + bdc = emc = "" + + alpha = self.page._set_opacity(CA=stroke_opacity, ca=fill_opacity) + if alpha == None: + alpha = "" + else: + alpha = "/%s gs\n" % alpha + nres = templ1 % (bdc, alpha, cm, left, top, fname, fontsize) + if render_mode > 0: + nres += "%i Tr " % render_mode + if border_width != 1: + nres += "%g w " % border_width + if color is not None: + nres += color_str + if fill is not None: + nres += fill_str + + # ========================================================================= + # start text insertion + # ========================================================================= + nres += text[0] + nlines = 1 # set output line counter + if len(text) > 1: + nres += templ2 % lheight # line 1 + else: + nres += templ2[:2] + for i in range(1, len(text)): + if space < lheight: + break # no space left on page + if i > 1: + nres += "\nT* " + nres += text[i] + templ2[:2] + space -= lheight + nlines += 1 + + nres += "\nET\n%sQ\n" % emc + + # ========================================================================= + # end of text insertion + # ========================================================================= + # update the /Contents object + self.text_cont += nres + return nlines + + # ============================================================================== + # Shape.insert_textbox + # ============================================================================== + def insert_textbox( + self, + rect: rect_like, + buffer: typing.Union[str, list], + fontname: OptStr = "helv", + fontfile: OptStr = None, + fontsize: float = 11, + lineheight: OptFloat = None, + set_simple: bool = 0, + encoding: int = 0, + color: OptSeq = None, + fill: OptSeq = None, + expandtabs: int = 1, + border_width: float = 1, + align: int = 0, + render_mode: int = 0, + rotate: int = 0, + morph: OptSeq = None, + stroke_opacity: float = 1, + fill_opacity: float = 1, + oc: int = 0, + ) -> float: + """Insert text into a given rectangle. + + Args: + rect -- the textbox to fill + buffer -- text to be inserted + fontname -- a Base-14 font, font name or '/name' + fontfile -- name of a font file + fontsize -- font size + lineheight -- overwrite the font property + color -- RGB stroke color triple + fill -- RGB fill color triple + render_mode -- text rendering control + border_width -- thickness of glyph borders + expandtabs -- handles tabulators with string function + align -- left, center, right, justified + rotate -- 0, 90, 180, or 270 degrees + morph -- morph box with a matrix and a fixpoint + Returns: + unused or deficit rectangle area (float) + """ + rect = Rect(rect) + if rect.is_empty or rect.is_infinite: + raise ValueError("text box must be finite and not empty") + + color_str = ColorCode(color, "c") + fill_str = ColorCode(fill, "f") + if fill is None and render_mode == 0: # ensure fill color for 0 Tr + fill = color + fill_str = ColorCode(color, "f") + + optcont = self.page._get_optional_content(oc) + if optcont != None: + bdc = "/OC /%s BDC\n" % optcont + emc = "EMC\n" + else: + bdc = emc = "" + + # determine opacity / transparency + alpha = self.page._set_opacity(CA=stroke_opacity, ca=fill_opacity) + if alpha == None: + alpha = "" + else: + alpha = "/%s gs\n" % alpha + + if rotate % 90 != 0: + raise ValueError("rotate must be multiple of 90") + + rot = rotate + while rot < 0: + rot += 360 + rot = rot % 360 + + # is buffer worth of dealing with? + if not bool(buffer): + return rect.height if rot in (0, 180) else rect.width + + cmp90 = "0 1 -1 0 0 0 cm\n" # rotates counter-clockwise + cmm90 = "0 -1 1 0 0 0 cm\n" # rotates clockwise + cm180 = "-1 0 0 -1 0 0 cm\n" # rotates by 180 deg. + height = self.height + + fname = fontname + if fname.startswith("/"): + fname = fname[1:] + + xref = self.page.insert_font( + fontname=fname, fontfile=fontfile, encoding=encoding, set_simple=set_simple + ) + fontinfo = CheckFontInfo(self.doc, xref) + + fontdict = fontinfo[1] + ordering = fontdict["ordering"] + simple = fontdict["simple"] + glyphs = fontdict["glyphs"] + bfname = fontdict["name"] + ascender = fontdict["ascender"] + descender = fontdict["descender"] + + if lineheight: + lheight_factor = lineheight + elif ascender - descender <= 1: + lheight_factor = 1.2 + else: + lheight_factor = ascender - descender + lheight = fontsize * lheight_factor + + # create a list from buffer, split into its lines + if type(buffer) in (list, tuple): + t0 = "\n".join(buffer) + else: + t0 = buffer + + maxcode = max([ord(c) for c in t0]) + # replace invalid char codes for simple fonts + if simple and maxcode > 255: + t0 = "".join([c if ord(c) < 256 else "?" for c in t0]) + + t0 = t0.splitlines() + + glyphs = self.doc.get_char_widths(xref, maxcode + 1) + if simple and bfname not in ("Symbol", "ZapfDingbats"): + tj_glyphs = None + else: + tj_glyphs = glyphs + + # ---------------------------------------------------------------------- + # calculate pixel length of a string + # ---------------------------------------------------------------------- + def pixlen(x): + """Calculate pixel length of x.""" + if ordering < 0: + return sum([glyphs[ord(c)][1] for c in x]) * fontsize + else: + return len(x) * fontsize + + # ---------------------------------------------------------------------- + + if ordering < 0: + blen = glyphs[32][1] * fontsize # pixel size of space character + else: + blen = fontsize + + text = "" # output buffer + + if CheckMorph(morph): + m1 = Matrix( + 1, 0, 0, 1, morph[0].x + self.x, self.height - morph[0].y - self.y + ) + mat = ~m1 * morph[1] * m1 + cm = "%g %g %g %g %g %g cm\n" % JM_TUPLE(mat) + else: + cm = "" + + # --------------------------------------------------------------------------- + # adjust for text orientation / rotation + # --------------------------------------------------------------------------- + progr = 1 # direction of line progress + c_pnt = Point(0, fontsize * ascender) # used for line progress + if rot == 0: # normal orientation + point = rect.tl + c_pnt # line 1 is 'lheight' below top + pos = point.y + self.y # y of first line + maxwidth = rect.width # pixels available in one line + maxpos = rect.y1 + self.y # lines must not be below this + + elif rot == 90: # rotate counter clockwise + c_pnt = Point(fontsize * ascender, 0) # progress in x-direction + point = rect.bl + c_pnt # line 1 'lheight' away from left + pos = point.x + self.x # position of first line + maxwidth = rect.height # pixels available in one line + maxpos = rect.x1 + self.x # lines must not be right of this + cm += cmp90 + + elif rot == 180: # text upside down + # progress upwards in y direction + c_pnt = -Point(0, fontsize * ascender) + point = rect.br + c_pnt # line 1 'lheight' above bottom + pos = point.y + self.y # position of first line + maxwidth = rect.width # pixels available in one line + progr = -1 # subtract lheight for next line + maxpos = rect.y0 + self.y # lines must not be above this + cm += cm180 + + else: # rotate clockwise (270 or -90) + # progress from right to left + c_pnt = -Point(fontsize * ascender, 0) + point = rect.tr + c_pnt # line 1 'lheight' left of right + pos = point.x + self.x # position of first line + maxwidth = rect.height # pixels available in one line + progr = -1 # subtract lheight for next line + maxpos = rect.x0 + self.x # lines must not left of this + cm += cmm90 + + # ======================================================================= + # line loop + # ======================================================================= + just_tab = [] # 'justify' indicators per line + + for i, line in enumerate(t0): + line_t = line.expandtabs(expandtabs).split(" ") # split into words + lbuff = "" # init line buffer + rest = maxwidth # available line pixels + # =================================================================== + # word loop + # =================================================================== + for word in line_t: + pl_w = pixlen(word) # pixel len of word + if rest >= pl_w: # will it fit on the line? + lbuff += word + " " # yes, and append word + rest -= pl_w + blen # update available line space + continue + # word won't fit - output line (if not empty) + if len(lbuff) > 0: + lbuff = lbuff.rstrip() + "\n" # line full, append line break + text += lbuff # append to total text + pos += lheight * progr # increase line position + just_tab.append(True) # line is justify candidate + lbuff = "" # re-init line buffer + rest = maxwidth # re-init avail. space + if pl_w <= maxwidth: # word shorter than 1 line? + lbuff = word + " " # start the line with it + rest = maxwidth - pl_w - blen # update free space + continue + # long word: split across multiple lines - char by char ... + if len(just_tab) > 0: + just_tab[-1] = False # reset justify indicator + for c in word: + if pixlen(lbuff) <= maxwidth - pixlen(c): + lbuff += c + else: # line full + lbuff += "\n" # close line + text += lbuff # append to text + pos += lheight * progr # increase line position + just_tab.append(False) # do not justify line + lbuff = c # start new line with this char + lbuff += " " # finish long word + rest = maxwidth - pixlen(lbuff) # long word stored + + if lbuff != "": # unprocessed line content? + text += lbuff.rstrip() # append to text + just_tab.append(False) # do not justify line + if i < len(t0) - 1: # not the last line? + text += "\n" # insert line break + pos += lheight * progr # increase line position + + more = (pos - maxpos) * progr # difference to rect size limit + + if more > EPSILON: # landed too much outside rect + return (-1) * more # return deficit, don't output + + more = abs(more) + if more < EPSILON: + more = 0 # don't bother with epsilons + nres = "\nq\n%s%sBT\n" % (bdc, alpha) + cm # initialize output buffer + templ = "1 0 0 1 %g %g Tm /%s %g Tf " + # center, right, justify: output each line with its own specifics + text_t = text.splitlines() # split text in lines again + just_tab[-1] = False # never justify last line + for i, t in enumerate(text_t): + pl = maxwidth - pixlen(t) # length of empty line part + pnt = point + c_pnt * (i * lheight_factor) # text start of line + if align == 1: # center: right shift by half width + if rot in (0, 180): + pnt = pnt + Point(pl / 2, 0) * progr + else: + pnt = pnt - Point(0, pl / 2) * progr + elif align == 2: # right: right shift by full width + if rot in (0, 180): + pnt = pnt + Point(pl, 0) * progr + else: + pnt = pnt - Point(0, pl) * progr + elif align == 3: # justify + spaces = t.count(" ") # number of spaces in line + if spaces > 0 and just_tab[i]: # if any, and we may justify + spacing = pl / spaces # make every space this much larger + else: + spacing = 0 # keep normal space length + top = height - pnt.y - self.y + left = pnt.x + self.x + if rot == 90: + left = height - pnt.y - self.y + top = -pnt.x - self.x + elif rot == 270: + left = -height + pnt.y + self.y + top = pnt.x + self.x + elif rot == 180: + left = -pnt.x - self.x + top = -height + pnt.y + self.y + + nres += templ % (left, top, fname, fontsize) + if render_mode > 0: + nres += "%i Tr " % render_mode + if align == 3: + nres += "%g Tw " % spacing + + if color is not None: + nres += color_str + if fill is not None: + nres += fill_str + if border_width != 1: + nres += "%g w " % border_width + nres += "%sTJ\n" % getTJstr(t, tj_glyphs, simple, ordering) + + nres += "ET\n%sQ\n" % emc + + self.text_cont += nres + self.updateRect(rect) + return more + + def finish( + self, + width: float = 1, + color: OptSeq = (0,), + fill: OptSeq = None, + lineCap: int = 0, + lineJoin: int = 0, + dashes: OptStr = None, + even_odd: bool = False, + morph: OptSeq = None, + closePath: bool = True, + fill_opacity: float = 1, + stroke_opacity: float = 1, + oc: int = 0, + ) -> None: + """Finish the current drawing segment. + + Notes: + Apply colors, opacity, dashes, line style and width, or + morphing. Also whether to close the path + by connecting last to first point. + """ + if self.draw_cont == "": # treat empty contents as no-op + return + + if width == 0: # border color makes no sense then + color = None + elif color == None: # vice versa + width = 0 + # if color == None and fill == None: + # raise ValueError("at least one of 'color' or 'fill' must be given") + color_str = ColorCode(color, "c") # ensure proper color string + fill_str = ColorCode(fill, "f") # ensure proper fill string + + optcont = self.page._get_optional_content(oc) + if optcont is not None: + self.draw_cont = "/OC /%s BDC\n" % optcont + self.draw_cont + emc = "EMC\n" + else: + emc = "" + + alpha = self.page._set_opacity(CA=stroke_opacity, ca=fill_opacity) + if alpha != None: + self.draw_cont = "/%s gs\n" % alpha + self.draw_cont + + if width != 1 and width != 0: + self.draw_cont += "%g w\n" % width + + if lineCap != 0: + self.draw_cont = "%i J\n" % lineCap + self.draw_cont + if lineJoin != 0: + self.draw_cont = "%i j\n" % lineJoin + self.draw_cont + + if dashes not in (None, "", "[] 0"): + self.draw_cont = "%s d\n" % dashes + self.draw_cont + + if closePath: + self.draw_cont += "h\n" + self.lastPoint = None + + if color is not None: + self.draw_cont += color_str + + if fill is not None: + self.draw_cont += fill_str + if color is not None: + if not even_odd: + self.draw_cont += "B\n" + else: + self.draw_cont += "B*\n" + else: + if not even_odd: + self.draw_cont += "f\n" + else: + self.draw_cont += "f*\n" + else: + self.draw_cont += "S\n" + + self.draw_cont += emc + if CheckMorph(morph): + m1 = Matrix( + 1, 0, 0, 1, morph[0].x + self.x, self.height - morph[0].y - self.y + ) + mat = ~m1 * morph[1] * m1 + self.draw_cont = "%g %g %g %g %g %g cm\n" % JM_TUPLE(mat) + self.draw_cont + + self.totalcont += "\nq\n" + self.draw_cont + "Q\n" + self.draw_cont = "" + self.lastPoint = None + return + + def commit(self, overlay: bool = True) -> None: + """Update the page's /Contents object with Shape data. The argument controls whether data appear in foreground (default) or background.""" + CheckParent(self.page) # doc may have died meanwhile + self.totalcont += self.text_cont + + self.totalcont = self.totalcont.encode() + + if self.totalcont != b"": + # make /Contents object with dummy stream + xref = TOOLS._insert_contents(self.page, b" ", overlay) + # update it with potential compression + self.doc.update_stream(xref, self.totalcont) + + self.lastPoint = None # clean up ... + self.rect = None # + self.draw_cont = "" # for potential ... + self.text_cont = "" # ... + self.totalcont = "" # re-use + return + + # define deprecated aliases ------------------------------------------ + drawBezier = draw_bezier + drawCircle = draw_circle + drawCurve = draw_curve + drawLine = draw_line + drawOval = draw_oval + drawPolyline = draw_polyline + drawQuad = draw_quad + drawRect = draw_rect + drawSector = draw_sector + drawSquiggle = draw_squiggle + drawZigzag = draw_zigzag + insertText = insert_text + insertTextbox = insert_textbox + + +def apply_redactions(page: Page, images: int = 2) -> bool: + """Apply the redaction annotations of the page. + + Args: + page: the PDF page. + images: 0 - ignore images, 1 - remove complete overlapping image, + 2 - blank out overlapping image parts. + """ + + def center_rect(annot_rect, text, font, fsize): + """Calculate minimal sub-rectangle for the overlay text. + + Notes: + Because 'insert_textbox' supports no vertical text centering, + we calculate an approximate number of lines here and return a + sub-rect with smaller height, which should still be sufficient. + Args: + annot_rect: the annotation rectangle + text: the text to insert. + font: the fontname. Must be one of the CJK or Base-14 set, else + the rectangle is returned unchanged. + fsize: the fontsize + Returns: + A rectangle to use instead of the annot rectangle. + """ + if not text: + return annot_rect + try: + text_width = get_text_length(text, font, fsize) + except ValueError: # unsupported font + return annot_rect + line_height = fsize * 1.2 + limit = annot_rect.width + h = math.ceil(text_width / limit) * line_height # estimate rect height + if h >= annot_rect.height: + return annot_rect + r = annot_rect + y = (annot_rect.tl.y + annot_rect.bl.y - h) * 0.5 + r.y0 = y + return r + + CheckParent(page) + doc = page.parent + if doc.is_encrypted or doc.is_closed: + raise ValueError("document closed or encrypted") + if not doc.is_pdf: + raise ValueError("is no PDF") + + redact_annots = [] # storage of annot values + for annot in page.annots(types=(PDF_ANNOT_REDACT,)): # loop redactions + redact_annots.append(annot._get_redact_values()) # save annot values + + if redact_annots == []: # any redactions on this page? + return False # no redactions + + rc = page._apply_redactions(images) # call MuPDF redaction process step + if not rc: # should not happen really + raise ValueError("Error applying redactions.") + + # now write replacement text in old redact rectangles + shape = page.new_shape() + for redact in redact_annots: + annot_rect = redact["rect"] + fill = redact["fill"] + if fill: + shape.draw_rect(annot_rect) # colorize the rect background + shape.finish(fill=fill, color=fill) + if "text" in redact.keys(): # if we also have text + text = redact["text"] + align = redact.get("align", 0) + fname = redact["fontname"] + fsize = redact["fontsize"] + color = redact["text_color"] + # try finding vertical centered sub-rect + trect = center_rect(annot_rect, text, fname, fsize) + + rc = -1 + while rc < 0 and fsize >= 4: # while not enough room + # (re-) try insertion + rc = shape.insert_textbox( + trect, + text, + fontname=fname, + fontsize=fsize, + color=color, + align=align, + ) + fsize -= 0.5 # reduce font if unsuccessful + shape.commit() # append new contents object + return True + + +# ------------------------------------------------------------------------------ +# Remove potentially sensitive data from a PDF. Similar to the Adobe +# Acrobat 'sanitize' function +# ------------------------------------------------------------------------------ +def scrub( + doc: Document, + attached_files: bool = True, + clean_pages: bool = True, + embedded_files: bool = True, + hidden_text: bool = True, + javascript: bool = True, + metadata: bool = True, + redactions: bool = True, + redact_images: int = 0, + remove_links: bool = True, + reset_fields: bool = True, + reset_responses: bool = True, + thumbnails: bool = True, + xml_metadata: bool = True, +) -> None: + def remove_hidden(cont_lines): + """Remove hidden text from a PDF page. + + Args: + cont_lines: list of lines with /Contents content. Should have status + from after page.cleanContents(). + + Returns: + List of /Contents lines from which hidden text has been removed. + + Notes: + The input must have been created after the page's /Contents object(s) + have been cleaned with page.cleanContents(). This ensures a standard + formatting: one command per line, single spaces between operators. + This allows for drastic simplification of this code. + """ + out_lines = [] # will return this + in_text = False # indicate if within BT/ET object + suppress = False # indicate text suppression active + make_return = False + for line in cont_lines: + if line == b"BT": # start of text object + in_text = True # switch on + out_lines.append(line) # output it + continue + if line == b"ET": # end of text object + in_text = False # switch off + out_lines.append(line) # output it + continue + if line == b"3 Tr": # text suppression operator + suppress = True # switch on + make_return = True + continue + if line[-2:] == b"Tr" and line[0] != b"3": + suppress = False # text rendering changed + out_lines.append(line) + continue + if line == b"Q": # unstack command also switches off + suppress = False + out_lines.append(line) + continue + if suppress and in_text: # suppress hidden lines + continue + out_lines.append(line) + if make_return: + return out_lines + else: + return None + + if not doc.is_pdf: # only works for PDF + raise ValueError("is no PDF") + if doc.is_encrypted or doc.is_closed: + raise ValueError("closed or encrypted doc") + + if clean_pages is False: + hidden_text = False + redactions = False + + if metadata: + doc.set_metadata({}) # remove standard metadata + + for page in doc: + if reset_fields: + # reset form fields (widgets) + for widget in page.widgets(): + widget.reset() + + if remove_links: + links = page.get_links() # list of all links on page + for link in links: # remove all links + page.delete_link(link) + + found_redacts = False + for annot in page.annots(): + if annot.type[0] == PDF_ANNOT_FILE_ATTACHMENT and attached_files: + annot.fileUpd(buffer=b" ") # set file content to empty + if reset_responses: + annot.delete_responses() + if annot.type[0] == PDF_ANNOT_REDACT: + found_redacts = True + + if redactions and found_redacts: + page.apply_redactions(images=redact_images) + + if not (clean_pages or hidden_text): + continue # done with the page + + page.clean_contents() + if not page.get_contents(): + continue + if hidden_text: + xref = page.get_contents()[0] # only one b/o cleaning! + cont = doc.xref_stream(xref) + cont_lines = remove_hidden(cont.splitlines()) # remove hidden text + if cont_lines: # something was actually removed + cont = b"\n".join(cont_lines) + doc.update_stream(xref, cont) # rewrite the page /Contents + + if thumbnails: # remove page thumbnails? + if doc.xref_get_key(page.xref, "Thumb")[0] != "null": + doc.xref_set_key(page.xref, "Thumb", "null") + + # pages are scrubbed, now perform document-wide scrubbing + # remove embedded files + if embedded_files: + for name in doc.embfile_names(): + doc.embfile_del(name) + + if xml_metadata: + doc.del_xml_metadata() + if not (xml_metadata or javascript): + xref_limit = 0 + else: + xref_limit = doc.xref_length() + for xref in range(1, xref_limit): + if not doc.xref_object(xref): + msg = "bad xref %i - clean PDF before scrubbing" % xref + raise ValueError(msg) + if javascript and doc.xref_get_key(xref, "S")[1] == "/JavaScript": + # a /JavaScript action object + obj = "<>" # replace with a null JavaScript + doc.update_object(xref, obj) # update this object + continue # no further handling + + if not xml_metadata: + continue + + if doc.xref_get_key(xref, "Type")[1] == "/Metadata": + # delete any metadata object directly + doc.update_object(xref, "<<>>") + doc.update_stream(xref, b"deleted", new=True) + continue + + if doc.xref_get_key(xref, "Metadata")[0] != "null": + doc.xref_set_key(xref, "Metadata", "null") + + +def fill_textbox( + writer: TextWriter, + rect: rect_like, + text: typing.Union[str, list], + pos: point_like = None, + font: typing.Optional[Font] = None, + fontsize: float = 11, + lineheight: OptFloat = None, + align: int = 0, + warn: bool = None, + right_to_left: bool = False, + small_caps: bool = False, +) -> tuple: + """Fill a rectangle with text. + + Args: + writer: TextWriter object (= "self") + rect: rect-like to receive the text. + text: string or list/tuple of strings. + pos: point-like start position of first word. + font: Font object (default Font('helv')). + fontsize: the fontsize. + lineheight: overwrite the font property + align: (int) 0 = left, 1 = center, 2 = right, 3 = justify + warn: (bool) text overflow action: none, warn, or exception + right_to_left: (bool) indicate right-to-left language. + """ + rect = Rect(rect) + if rect.is_empty: + raise ValueError("fill rect must not empty.") + if type(font) is not Font: + font = Font("helv") + + def textlen(x): + """Return length of a string.""" + return font.text_length( + x, fontsize=fontsize, small_caps=small_caps + ) # abbreviation + + def char_lengths(x): + """Return list of single character lengths for a string.""" + return font.char_lengths(x, fontsize=fontsize, small_caps=small_caps) + + def append_this(pos, text): + return writer.append( + pos, text, font=font, fontsize=fontsize, small_caps=small_caps + ) + + tolerance = fontsize * 0.2 # extra distance to left border + space_len = textlen(" ") + std_width = rect.width - tolerance + std_start = rect.x0 + tolerance + + def norm_words(width, words): + """Cut any word in pieces no longer than 'width'.""" + nwords = [] + word_lengths = [] + for w in words: + wl_lst = char_lengths(w) + wl = sum(wl_lst) + if wl <= width: # nothing to do - copy over + nwords.append(w) + word_lengths.append(wl) + continue + + # word longer than rect width - split it in parts + n = len(wl_lst) + while n > 0: + wl = sum(wl_lst[:n]) + if wl <= width: + nwords.append(w[: n + 1]) + word_lengths.append(wl) + w = w[n + 1 :] + wl_lst = wl_lst[n + 1 :] + n = len(wl_lst) + else: + n -= 1 + return nwords, word_lengths + + def output_justify(start, line): + """Justified output of a line.""" + # ignore leading / trailing / multiple spaces + words = [w for w in line.split(" ") if w != ""] + nwords = len(words) + if nwords == 0: + return + if nwords == 1: # single word cannot be justified + append_this(start, words[0]) + return + tl = sum([textlen(w) for w in words]) # total word lengths + gaps = nwords - 1 # number of word gaps + gapl = (std_width - tl) / gaps # width of each gap + for w in words: + _, lp = append_this(start, w) # output one word + start.x = lp.x + gapl # next start at word end plus gap + return + + asc = font.ascender + dsc = font.descender + if not lineheight: + if asc - dsc <= 1: + lheight = 1.2 + else: + lheight = asc - dsc + else: + lheight = lineheight + + LINEHEIGHT = fontsize * lheight # effective line height + width = std_width # available horizontal space + + # starting point of text + if pos is not None: + pos = Point(pos) + else: # default is just below rect top-left + pos = rect.tl + (tolerance, fontsize * asc) + if not pos in rect: + raise ValueError("Text must start in rectangle.") + + # calculate displacement factor for alignment + if align == TEXT_ALIGN_CENTER: + factor = 0.5 + elif align == TEXT_ALIGN_RIGHT: + factor = 1.0 + else: + factor = 0 + + # split in lines if just a string was given + if type(text) is str: + textlines = text.splitlines() + else: + textlines = [] + for line in text: + textlines.extend(line.splitlines()) + + max_lines = int((rect.y1 - pos.y) / LINEHEIGHT) + 1 + + new_lines = [] # the final list of textbox lines + no_justify = [] # no justify for these line numbers + for i, line in enumerate(textlines): + if line in ("", " "): + new_lines.append((line, space_len)) + width = rect.width - tolerance + no_justify.append((len(new_lines) - 1)) + continue + if i == 0: + width = rect.x1 - pos.x + else: + width = rect.width - tolerance + + if right_to_left: # reverses Arabic / Hebrew text front to back + line = writer.clean_rtl(line) + tl = textlen(line) + if tl <= width: # line short enough + new_lines.append((line, tl)) + no_justify.append((len(new_lines) - 1)) + continue + + # we need to split the line in fitting parts + words = line.split(" ") # the words in the line + + # cut in parts any words that are longer than rect width + words, word_lengths = norm_words(std_width, words) + + n = len(words) + while True: + line0 = " ".join(words[:n]) + wl = sum(word_lengths[:n]) + space_len * (len(word_lengths[:n]) - 1) + if wl <= width: + new_lines.append((line0, wl)) + words = words[n:] + word_lengths = word_lengths[n:] + n = len(words) + line0 = None + else: + n -= 1 + + if len(words) == 0: + break + + # ------------------------------------------------------------------------- + # List of lines created. Each item is (text, tl), where 'tl' is the PDF + # output length (float) and 'text' is the text. Except for justified text, + # this is output-ready. + # ------------------------------------------------------------------------- + nlines = len(new_lines) + if nlines > max_lines: + msg = "Only fitting %i of %i lines." % (max_lines, nlines) + if warn == True: + print("Warning: " + msg) + elif warn == False: + raise ValueError(msg) + + start = Point() + no_justify += [len(new_lines) - 1] # no justifying of last line + for i in range(max_lines): + try: + line, tl = new_lines.pop(0) + except IndexError: + break + + if right_to_left: # Arabic, Hebrew + line = "".join(reversed(line)) + + if i == 0: # may have different start for first line + start = pos + + if align == TEXT_ALIGN_JUSTIFY and i not in no_justify and tl < std_width: + output_justify(start, line) + start.x = std_start + start.y += LINEHEIGHT + continue + + if i > 0 or pos.x == std_start: # left, center, right alignments + start.x += (width - tl) * factor + + append_this(start, line) + start.x = std_start + start.y += LINEHEIGHT + + return new_lines # return non-written lines + + +# ------------------------------------------------------------------------ +# Optional Content functions +# ------------------------------------------------------------------------ +def get_oc(doc: Document, xref: int) -> int: + """Return optional content object xref for an image or form xobject. + + Args: + xref: (int) xref number of an image or form xobject. + """ + if doc.is_closed or doc.is_encrypted: + raise ValueError("document close or encrypted") + t, name = doc.xref_get_key(xref, "Subtype") + if t != "name" or name not in ("/Image", "/Form"): + raise ValueError("bad object type at xref %i" % xref) + t, oc = doc.xref_get_key(xref, "OC") + if t != "xref": + return 0 + rc = int(oc.replace("0 R", "")) + return rc + + +def set_oc(doc: Document, xref: int, oc: int) -> None: + """Attach optional content object to image or form xobject. + + Args: + xref: (int) xref number of an image or form xobject + oc: (int) xref number of an OCG or OCMD + """ + if doc.is_closed or doc.is_encrypted: + raise ValueError("document close or encrypted") + t, name = doc.xref_get_key(xref, "Subtype") + if t != "name" or name not in ("/Image", "/Form"): + raise ValueError("bad object type at xref %i" % xref) + if oc > 0: + t, name = doc.xref_get_key(oc, "Type") + if t != "name" or name not in ("/OCG", "/OCMD"): + raise ValueError("bad object type at xref %i" % oc) + if oc == 0 and "OC" in doc.xref_get_keys(xref): + doc.xref_set_key(xref, "OC", "null") + return None + doc.xref_set_key(xref, "OC", "%i 0 R" % oc) + return None + + +def set_ocmd( + doc: Document, + xref: int = 0, + ocgs: typing.Union[list, None] = None, + policy: OptStr = None, + ve: typing.Union[list, None] = None, +) -> int: + """Create or update an OCMD object in a PDF document. + + Args: + xref: (int) 0 for creating a new object, otherwise update existing one. + ocgs: (list) OCG xref numbers, which shall be subject to 'policy'. + policy: one of 'AllOn', 'AllOff', 'AnyOn', 'AnyOff' (any casing). + ve: (list) visibility expression. Use instead of 'ocgs' with 'policy'. + + Returns: + Xref of the created or updated OCMD. + """ + + all_ocgs = set(doc.get_ocgs().keys()) + + def ve_maker(ve): + if type(ve) not in (list, tuple) or len(ve) < 2: + raise ValueError("bad 've' format: %s" % ve) + if ve[0].lower() not in ("and", "or", "not"): + raise ValueError("bad operand: %s" % ve[0]) + if ve[0].lower() == "not" and len(ve) != 2: + raise ValueError("bad 've' format: %s" % ve) + item = "[/%s" % ve[0].title() + for x in ve[1:]: + if type(x) is int: + if x not in all_ocgs: + raise ValueError("bad OCG %i" % x) + item += " %i 0 R" % x + else: + item += " %s" % ve_maker(x) + item += "]" + return item + + text = "< dict: + """Return the definition of an OCMD (optional content membership dictionary). + + Recognizes PDF dict keys /OCGs (PDF array of OCGs), /P (policy string) and + /VE (visibility expression, PDF array). Via string manipulation, this + info is converted to a Python dictionary with keys "xref", "ocgs", "policy" + and "ve" - ready to recycle as input for 'set_ocmd()'. + """ + + if xref not in range(doc.xref_length()): + raise ValueError("bad xref") + text = doc.xref_object(xref, compressed=True) + if "/Type/OCMD" not in text: + raise ValueError("bad object type") + textlen = len(text) + + p0 = text.find("/OCGs[") # look for /OCGs key + p1 = text.find("]", p0) + if p0 < 0 or p1 < 0: # no OCGs found + ocgs = None + else: + ocgs = text[p0 + 6 : p1].replace("0 R", " ").split() + ocgs = list(map(int, ocgs)) + + p0 = text.find("/P/") # look for /P policy key + if p0 < 0: + policy = None + else: + p1 = text.find("ff", p0) + if p1 < 0: + p1 = text.find("on", p0) + if p1 < 0: # some irregular syntax + raise ValueError("bad object at xref") + else: + policy = text[p0 + 3 : p1 + 2] + + p0 = text.find("/VE[") # look for /VE visibility expression key + if p0 < 0: # no visibility expression found + ve = None + else: + lp = rp = 0 # find end of /VE by finding last ']'. + p1 = p0 + while lp < 1 or lp != rp: + p1 += 1 + if not p1 < textlen: # some irregular syntax + raise ValueError("bad object at xref") + if text[p1] == "[": + lp += 1 + if text[p1] == "]": + rp += 1 + # p1 now positioned at the last "]" + ve = text[p0 + 3 : p1 + 1] # the PDF /VE array + ve = ( + ve.replace("/And", '"and",') + .replace("/Not", '"not",') + .replace("/Or", '"or",') + ) + ve = ve.replace(" 0 R]", "]").replace(" 0 R", ",").replace("][", "],[") + try: + ve = json.loads(ve) + except: + print("bad /VE key: ", ve) + raise + return {"xref": xref, "ocgs": ocgs, "policy": policy, "ve": ve} + + +""" +Handle page labels for PDF documents. + +Reading +------- +* compute the label of a page +* find page number(s) having the given label. + +Writing +------- +Supports setting (defining) page labels for PDF documents. + +A big Thank You goes to WILLIAM CHAPMAN who contributed the idea and +significant parts of the following code during late December 2020 +through early January 2021. +""" + + +def rule_dict(item): + """Make a Python dict from a PDF page label rule. + + Args: + item -- a tuple (pno, rule) with the start page number and the rule + string like <>. + Returns: + A dict like + {'startpage': int, 'prefix': str, 'style': str, 'firstpagenum': int}. + """ + # Jorj McKie, 2021-01-06 + + pno, rule = item + rule = rule[2:-2].split("/")[1:] # strip "<<" and ">>" + d = {"startpage": pno, "prefix": "", "firstpagenum": 1} + skip = False + for i, item in enumerate(rule): + if skip: # this item has already been processed + skip = False # deactivate skipping again + continue + if item == "S": # style specification + d["style"] = rule[i + 1] # next item has the style + skip = True # do not process next item again + continue + if item.startswith("P"): # prefix specification: extract the string + x = item[1:].replace("(", "").replace(")", "") + d["prefix"] = x + continue + if item.startswith("St"): # start page number specification + x = int(item[2:]) + d["firstpagenum"] = x + return d + + +def get_label_pno(pgNo, labels): + """Return the label for this page number. + + Args: + pgNo: page number, 0-based. + labels: result of doc._get_page_labels(). + Returns: + The label (str) of the page number. Errors return an empty string. + """ + # Jorj McKie, 2021-01-06 + + item = [x for x in labels if x[0] <= pgNo][-1] + rule = rule_dict(item) + prefix = rule.get("prefix", "") + style = rule.get("style", "") + pagenumber = pgNo - rule["startpage"] + rule["firstpagenum"] + return construct_label(style, prefix, pagenumber) + + +def get_label(page): + """Return the label for this PDF page. + + Args: + page: page object. + Returns: + The label (str) of the page. Errors return an empty string. + """ + # Jorj McKie, 2021-01-06 + + labels = page.parent._get_page_labels() + if not labels: + return "" + labels.sort() + return get_label_pno(page.number, labels) + + +def get_page_numbers(doc, label, only_one=False): + """Return a list of page numbers with the given label. + + Args: + doc: PDF document object (resp. 'self'). + label: (str) label. + only_one: (bool) stop searching after first hit. + Returns: + List of page numbers having this label. + """ + # Jorj McKie, 2021-01-06 + + numbers = [] + if not label: + return numbers + labels = doc._get_page_labels() + if labels == []: + return numbers + for i in range(doc.page_count): + plabel = get_label_pno(i, labels) + if plabel == label: + numbers.append(i) + if only_one: + break + return numbers + + +def construct_label(style, prefix, pno) -> str: + """Construct a label based on style, prefix and page number.""" + # William Chapman, 2021-01-06 + + n_str = "" + if style == "D": + n_str = str(pno) + elif style == "r": + n_str = integerToRoman(pno).lower() + elif style == "R": + n_str = integerToRoman(pno).upper() + elif style == "a": + n_str = integerToLetter(pno).lower() + elif style == "A": + n_str = integerToLetter(pno).upper() + result = prefix + n_str + return result + + +def integerToLetter(i) -> str: + """Returns letter sequence string for integer i.""" + # William Chapman, Jorj McKie, 2021-01-06 + + ls = string.ascii_uppercase + n, a = 1, i + while pow(26, n) <= a: + a -= int(math.pow(26, n)) + n += 1 + + str_t = "" + for j in reversed(range(n)): + f, g = divmod(a, int(math.pow(26, j))) + str_t += ls[f] + a = g + return str_t + + +def integerToRoman(num: int) -> str: + """Return roman numeral for an integer.""" + # William Chapman, Jorj McKie, 2021-01-06 + + roman = ( + (1000, "M"), + (900, "CM"), + (500, "D"), + (400, "CD"), + (100, "C"), + (90, "XC"), + (50, "L"), + (40, "XL"), + (10, "X"), + (9, "IX"), + (5, "V"), + (4, "IV"), + (1, "I"), + ) + + def roman_num(num): + for r, ltr in roman: + x, _ = divmod(num, r) + yield ltr * x + num -= r * x + if num <= 0: + break + + return "".join([a for a in roman_num(num)]) + + +def get_page_labels(doc): + """Return page label definitions in PDF document. + + Args: + doc: PDF document (resp. 'self'). + Returns: + A list of dictionaries with the following format: + {'startpage': int, 'prefix': str, 'style': str, 'firstpagenum': int}. + """ + # Jorj McKie, 2021-01-10 + return [rule_dict(item) for item in doc._get_page_labels()] + + +def set_page_labels(doc, labels): + """Add / replace page label definitions in PDF document. + + Args: + doc: PDF document (resp. 'self'). + labels: list of label dictionaries like: + {'startpage': int, 'prefix': str, 'style': str, 'firstpagenum': int}, + as returned by get_page_labels(). + """ + # William Chapman, 2021-01-06 + + def create_label_str(label): + """Convert Python label dict to correspnding PDF rule string. + + Args: + label: (dict) build rule for the label. + Returns: + PDF label rule string wrapped in "<<", ">>". + """ + s = "%i<<" % label["startpage"] + if label.get("prefix", "") != "": + s += "/P(%s)" % label["prefix"] + if label.get("style", "") != "": + s += "/S/%s" % label["style"] + if label.get("firstpagenum", 1) > 1: + s += "/St %i" % label["firstpagenum"] + s += ">>" + return s + + def create_nums(labels): + """Return concatenated string of all labels rules. + + Args: + labels: (list) dictionaries as created by function 'rule_dict'. + Returns: + PDF compatible string for page label definitions, ready to be + enclosed in PDF array 'Nums[...]'. + """ + labels.sort(key=lambda x: x["startpage"]) + s = "".join([create_label_str(label) for label in labels]) + return s + + doc._set_page_labels(create_nums(labels)) + + +# End of Page Label Code ------------------------------------------------- + + +def has_links(doc: Document) -> bool: + """Check whether there are links on any page.""" + if doc.is_closed: + raise ValueError("document closed") + if not doc.is_pdf: + raise ValueError("is no PDF") + for i in range(doc.page_count): + for item in doc.page_annot_xrefs(i): + if item[1] == PDF_ANNOT_LINK: + return True + return False + + +def has_annots(doc: Document) -> bool: + """Check whether there are annotations on any page.""" + if doc.is_closed: + raise ValueError("document closed") + if not doc.is_pdf: + raise ValueError("is no PDF") + for i in range(doc.page_count): + for item in doc.page_annot_xrefs(i): + if not (item[1] == PDF_ANNOT_LINK or item[1] == PDF_ANNOT_WIDGET): + return True + return False + + +# ------------------------------------------------------------------- +# Functions to recover the quad contained in a text extraction bbox +# ------------------------------------------------------------------- +def recover_bbox_quad(line_dir: tuple, span: dict, bbox: tuple) -> Quad: + """Compute the quad located inside the bbox. + + The bbox may be any of the resp. tuples occurring inside the given span. + + Args: + line_dir: (tuple) 'line["dir"]' of the owning line or None. + span: (dict) the span. May be from get_texttrace() method. + bbox: (tuple) the bbox of the span or any of its characters. + Returns: + The quad which is wrapped by the bbox. + """ + if line_dir == None: + line_dir = span["dir"] + cos, sin = line_dir + bbox = Rect(bbox) # make it a rect + if TOOLS.set_small_glyph_heights(): # ==> just fontsize as height + d = 1 + else: + d = span["ascender"] - span["descender"] + + height = d * span["size"] # the quad's rectangle height + # The following are distances from the bbox corners, at wich we find the + # respective quad points. The computation depends on in which quadrant + # the text writing angle is located. + hs = height * sin + hc = height * cos + if hc >= 0 and hs <= 0: # quadrant 1 + ul = bbox.bl - (0, hc) + ur = bbox.tr + (hs, 0) + ll = bbox.bl - (hs, 0) + lr = bbox.tr + (0, hc) + elif hc <= 0 and hs <= 0: # quadrant 2 + ul = bbox.br + (hs, 0) + ur = bbox.tl - (0, hc) + ll = bbox.br + (0, hc) + lr = bbox.tl - (hs, 0) + elif hc <= 0 and hs >= 0: # quadrant 3 + ul = bbox.tr - (0, hc) + ur = bbox.bl + (hs, 0) + ll = bbox.tr - (hs, 0) + lr = bbox.bl + (0, hc) + else: # quadrant 4 + ul = bbox.tl + (hs, 0) + ur = bbox.br - (0, hc) + ll = bbox.tl + (0, hc) + lr = bbox.br - (hs, 0) + return Quad(ul, ur, ll, lr) + + +def recover_quad(line_dir: tuple, span: dict) -> Quad: + """Recover the quadrilateral of a text span. + + Args: + line_dir: (tuple) 'line["dir"]' of the owning line. + span: the span. + Returns: + The quadrilateral enveloping the span's text. + """ + if type(line_dir) is not tuple or len(line_dir) != 2: + raise ValueError("bad line dir argument") + if type(span) is not dict: + raise ValueError("bad span argument") + return recover_bbox_quad(line_dir, span, span["bbox"]) + + +def recover_line_quad(line: dict, spans: list = None) -> Quad: + """Calculate the line quad for 'dict' / 'rawdict' text extractions. + + The lower quad points are those of the first, resp. last span quad. + The upper points are determined by the maximum span quad height. + From this, compute a rect with bottom-left in (0, 0), convert this to a + quad and rotate and shift back to cover the text of the spans. + + Args: + spans: (list, optional) sub-list of spans to consider. + Returns: + Quad covering selected spans. + """ + if spans == None: # no sub-selection + spans = line["spans"] # all spans + if len(spans) == 0: + raise ValueError("bad span list") + line_dir = line["dir"] # text direction + cos, sin = line_dir + q0 = recover_quad(line_dir, spans[0]) # quad of first span + + if len(spans) > 1: # get quad of last span + q1 = recover_quad(line_dir, spans[-1]) + else: + q1 = q0 # last = first + + line_ll = q0.ll # lower-left of line quad + line_lr = q1.lr # lower-right of line quad + + mat0 = planish_line(line_ll, line_lr) + + # map base line to x-axis such that line_ll goes to (0, 0) + x_lr = line_lr * mat0 + + small = TOOLS.set_small_glyph_heights() # small glyph heights? + + h = max( + [s["size"] * (1 if small else (s["ascender"] - s["descender"])) for s in spans] + ) + + line_rect = Rect(0, -h, x_lr.x, 0) # line rectangle + line_quad = line_rect.quad # make it a quad and: + line_quad *= ~mat0 + return line_quad + + +def recover_span_quad(line_dir: tuple, span: dict, chars: list = None) -> Quad: + """Calculate the span quad for 'dict' / 'rawdict' text extractions. + + Notes: + There are two execution paths: + 1. For the full span quad, the result of 'recover_quad' is returned. + 2. For the quad of a sub-list of characters, the char quads are + computed and joined. This is only supported for the "rawdict" + extraction option. + + Args: + line_dir: (tuple) 'line["dir"]' of the owning line. + span: (dict) the span. + chars: (list, optional) sub-list of characters to consider. + Returns: + Quad covering selected characters. + """ + if line_dir == None: # must be a span from get_texttrace() + line_dir = span["dir"] + if chars == None: # no sub-selection + return recover_quad(line_dir, span) + if not "chars" in span.keys(): + raise ValueError("need 'rawdict' option to sub-select chars") + + q0 = recover_char_quad(line_dir, span, chars[0]) # quad of first char + if len(chars) > 1: # get quad of last char + q1 = recover_char_quad(line_dir, span, chars[-1]) + else: + q1 = q0 # last = first + + span_ll = q0.ll # lower-left of span quad + span_lr = q1.lr # lower-right of span quad + mat0 = planish_line(span_ll, span_lr) + # map base line to x-axis such that span_ll goes to (0, 0) + x_lr = span_lr * mat0 + + small = TOOLS.set_small_glyph_heights() # small glyph heights? + h = span["size"] * (1 if small else (span["ascender"] - span["descender"])) + + span_rect = Rect(0, -h, x_lr.x, 0) # line rectangle + span_quad = span_rect.quad # make it a quad and: + span_quad *= ~mat0 # rotate back and shift back + return span_quad + + +def recover_char_quad(line_dir: tuple, span: dict, char: dict) -> Quad: + """Recover the quadrilateral of a text character. + + This requires the "rawdict" option of text extraction. + + Args: + line_dir: (tuple) 'line["dir"]' of the span's line. + span: (dict) the span dict. + char: (dict) the character dict. + Returns: + The quadrilateral enveloping the character. + """ + if line_dir == None: + line_dir = span["dir"] + if type(line_dir) is not tuple or len(line_dir) != 2: + raise ValueError("bad line dir argument") + if type(span) is not dict: + raise ValueError("bad span argument") + if type(char) is dict: + bbox = Rect(char["bbox"]) + elif type(char) is tuple: + bbox = Rect(char[3]) + else: + raise ValueError("bad span argument") + + return recover_bbox_quad(line_dir, span, bbox) + + +# ------------------------------------------------------------------- +# Building font subsets using fontTools +# ------------------------------------------------------------------- +def subset_fonts(doc: Document, verbose: bool = False) -> None: + """Build font subsets of a PDF. Requires package 'fontTools'. + + Eligible fonts are potentially replaced by smaller versions. Page text is + NOT rewritten and thus should retain properties like being hidden or + controlled by optional content. + """ + # Font binaries: - "buffer" -> (names, xrefs, (unicodes, glyphs)) + # An embedded font is uniquely defined by its fontbuffer only. It may have + # multiple names and xrefs. + # Once the sets of used unicodes and glyphs are known, we compute a + # smaller version of the buffer user package fontTools. + font_buffers = {} + + def get_old_widths(xref): + """Retrieve old font '/W' and '/DW' values.""" + df = doc.xref_get_key(xref, "DescendantFonts") + if df[0] != "array": # only handle xref specifications + return None, None + df_xref = int(df[1][1:-1].replace("0 R", "")) + widths = doc.xref_get_key(df_xref, "W") + if widths[0] != "array": # no widths key found + widths = None + else: + widths = widths[1] + dwidths = doc.xref_get_key(df_xref, "DW") + if dwidths[0] != "int": + dwidths = None + else: + dwidths = dwidths[1] + return widths, dwidths + + def set_old_widths(xref, widths, dwidths): + """Restore the old '/W' and '/DW' in subsetted font. + + If either parameter is None or evaluates to False, the corresponding + dictionary key will be set to null. + """ + df = doc.xref_get_key(xref, "DescendantFonts") + if df[0] != "array": # only handle xref specs + return None + df_xref = int(df[1][1:-1].replace("0 R", "")) + if (type(widths) is not str or not widths) and doc.xref_get_key(df_xref, "W")[ + 0 + ] != "null": + doc.xref_set_key(df_xref, "W", "null") + else: + doc.xref_set_key(df_xref, "W", widths) + if (type(dwidths) is not str or not dwidths) and doc.xref_get_key( + df_xref, "DW" + )[0] != "null": + doc.xref_set_key(df_xref, "DW", "null") + else: + doc.xref_set_key(df_xref, "DW", dwidths) + return None + + def set_subset_fontname(new_xref): + """Generate a name prefix to tag a font as subset. + + We use a random generator to select 6 upper case ASCII characters. + The prefixed name must be put in the font xref as the "/BaseFont" value + and in the FontDescriptor object as the '/FontName' value. + """ + # The following generates a prefix like 'ABCDEF+' + prefix = "".join(random.choices(tuple(string.ascii_uppercase), k=6)) + "+" + font_str = doc.xref_object(new_xref, compressed=True) + font_str = font_str.replace("/BaseFont/", "/BaseFont/" + prefix) + df = doc.xref_get_key(new_xref, "DescendantFonts") + if df[0] == "array": + df_xref = int(df[1][1:-1].replace("0 R", "")) + fd = doc.xref_get_key(df_xref, "FontDescriptor") + if fd[0] == "xref": + fd_xref = int(fd[1].replace("0 R", "")) + fd_str = doc.xref_object(fd_xref, compressed=True) + fd_str = fd_str.replace("/FontName/", "/FontName/" + prefix) + doc.update_object(fd_xref, fd_str) + doc.update_object(new_xref, font_str) + return None + + def build_subset(buffer, unc_set, gid_set): + """Build font subset using fontTools. + + Args: + buffer: (bytes) the font given as a binary buffer. + unc_set: (set) required glyph ids. + Returns: + Either None if subsetting is unsuccessful or the subset font buffer. + """ + try: + import fontTools.subset as fts + except ImportError: + print("This method requires fontTools to be installed.") + raise + tmp_dir = tempfile.gettempdir() + oldfont_path = f"{tmp_dir}/oldfont.ttf" + newfont_path = f"{tmp_dir}/newfont.ttf" + uncfile_path = f"{tmp_dir}/uncfile.txt" + args = [ + oldfont_path, + "--retain-gids", + f"--output-file={newfont_path}", + "--layout-features='*'", + "--passthrough-tables", + "--ignore-missing-glyphs", + "--ignore-missing-unicodes", + "--symbol-cmap", + ] + + unc_file = open( + f"{tmp_dir}/uncfile.txt", "w" + ) # store glyph ids or unicodes as file + if 0xFFFD in unc_set: # error unicode exists -> use glyphs + args.append(f"--gids-file={uncfile_path}") + gid_set.add(189) + unc_list = list(gid_set) + for unc in unc_list: + unc_file.write("%i\n" % unc) + else: + args.append(f"--unicodes-file={uncfile_path}") + unc_set.add(255) + unc_list = list(unc_set) + for unc in unc_list: + unc_file.write("%04x\n" % unc) + + unc_file.close() + fontfile = open(oldfont_path, "wb") # store fontbuffer as a file + fontfile.write(buffer) + fontfile.close() + try: + os.remove(newfont_path) # remove old file + except: + pass + try: # invoke fontTools subsetter + fts.main(args) + font = Font(fontfile=newfont_path) + new_buffer = font.buffer + if len(font.valid_codepoints()) == 0: + new_buffer = None + except: + new_buffer = None + try: + os.remove(uncfile_path) + except: + pass + try: + os.remove(oldfont_path) + except: + pass + try: + os.remove(newfont_path) + except: + pass + return new_buffer + + def repl_fontnames(doc): + """Populate 'font_buffers'. + + For each font candidate, store its xref and the list of names + by which PDF text may refer to it (there may be multiple). + """ + + def norm_name(name): + """Recreate font name that contains PDF hex codes. + + E.g. #20 -> space, chr(32) + """ + while "#" in name: + p = name.find("#") + c = int(name[p + 1 : p + 3], 16) + name = name.replace(name[p : p + 3], chr(c)) + return name + + def get_fontnames(doc, item): + """Return a list of fontnames for an item of page.get_fonts(). + + There may be multiple names e.g. for Type0 fonts. + """ + fontname = item[3] + names = [fontname] + fontname = doc.xref_get_key(item[0], "BaseFont")[1][1:] + fontname = norm_name(fontname) + if fontname not in names: + names.append(fontname) + descendents = doc.xref_get_key(item[0], "DescendantFonts") + if descendents[0] != "array": + return names + descendents = descendents[1][1:-1] + if descendents.endswith(" 0 R"): + xref = int(descendents[:-4]) + descendents = doc.xref_object(xref, compressed=True) + p1 = descendents.find("/BaseFont") + if p1 >= 0: + p2 = descendents.find("/", p1 + 1) + p1 = min(descendents.find("/", p2 + 1), descendents.find(">>", p2 + 1)) + fontname = descendents[p2 + 1 : p1] + fontname = norm_name(fontname) + if fontname not in names: + names.append(fontname) + return names + + for i in range(doc.page_count): + for f in doc.get_page_fonts(i, full=True): + font_xref = f[0] # font xref + font_ext = f[1] # font file extension + basename = f[3] # font basename + + if font_ext not in ( # skip if not supported by fontTools + "otf", + "ttf", + "woff", + "woff2", + ): + continue + # skip fonts which already are subsets + if len(basename) > 6 and basename[6] == "+": + continue + + extr = doc.extract_font(font_xref) + fontbuffer = extr[-1] + names = get_fontnames(doc, f) + name_set, xref_set, subsets = font_buffers.get( + fontbuffer, (set(), set(), (set(), set())) + ) + xref_set.add(font_xref) + for name in names: + name_set.add(name) + font = Font(fontbuffer=fontbuffer) + name_set.add(font.name) + del font + font_buffers[fontbuffer] = (name_set, xref_set, subsets) + return None + + def find_buffer_by_name(name): + for buffer in font_buffers.keys(): + name_set, _, _ = font_buffers[buffer] + if name in name_set: + return buffer + return None + + # ----------------- + # main function + # ----------------- + repl_fontnames(doc) # populate font information + if not font_buffers: # nothing found to do + if verbose: + print("No fonts to subset.") + return 0 + + old_fontsize = 0 + new_fontsize = 0 + for fontbuffer in font_buffers.keys(): + old_fontsize += len(fontbuffer) + + # Scan page text for usage of subsettable fonts + for page in doc: + # go through the text and extend set of used glyphs by font + # we use a modified MuPDF trace device, which delivers us glyph ids. + for span in page.get_texttrace(): + if type(span) is not dict: # skip useless information + continue + fontname = span["font"][:33] # fontname for the span + buffer = find_buffer_by_name(fontname) + if buffer is None: + continue + name_set, xref_set, (set_ucs, set_gid) = font_buffers[buffer] + for c in span["chars"]: + set_ucs.add(c[0]) # unicode + set_gid.add(c[1]) # glyph id + font_buffers[buffer] = (name_set, xref_set, (set_ucs, set_gid)) + + # build the font subsets + for old_buffer in font_buffers.keys(): + name_set, xref_set, subsets = font_buffers[old_buffer] + new_buffer = build_subset(old_buffer, subsets[0], subsets[1]) + fontname = list(name_set)[0] + if new_buffer == None or len(new_buffer) >= len(old_buffer): + # subset was not created or did not get smaller + if verbose: + print(f"Cannot subset '{fontname}'.") + continue + if verbose: + print(f"Built subset of font '{fontname}'.") + val = doc._insert_font(fontbuffer=new_buffer) # store subset font in PDF + new_xref = val[0] # get its xref + set_subset_fontname(new_xref) # tag fontname as subset font + font_str = doc.xref_object( # get its object definition + new_xref, + compressed=True, + ) + # walk through the original font xrefs and replace each by the subset def + for font_xref in xref_set: + # we need the original '/W' and '/DW' width values + width_table, def_width = get_old_widths(font_xref) + # ... and replace original font definition at xref with it + doc.update_object(font_xref, font_str) + # now copy over old '/W' and '/DW' values + if width_table or def_width: + set_old_widths(font_xref, width_table, def_width) + # 'new_xref' remains unused in the PDF and must be removed + # by garbage collection. + new_fontsize += len(new_buffer) + + return old_fontsize - new_fontsize + + +# ------------------------------------------------------------------- +# Copy XREF object to another XREF +# ------------------------------------------------------------------- +def xref_copy(doc: Document, source: int, target: int, *, keep: list = None) -> None: + """Copy a PDF dictionary object to another one given their xref numbers. + + Args: + doc: PDF document object + source: source xref number + target: target xref number, the xref must already exist + keep: an optional list of 1st level keys in target that should not be + removed before copying. + Notes: + This works similar to the copy() method of dictionaries in Python. The + source may be a stream object. + """ + if doc.xref_is_stream(source): + # read new xref stream, maintaining compression + stream = doc.xref_stream_raw(source) + doc.update_stream( + target, + stream, + compress=False, # keeps source compression + new=True, # in case target is no stream + ) + + # empty the target completely, observe exceptions + if keep is None: + keep = [] + for key in doc.xref_get_keys(target): + if key in keep: + continue + doc.xref_set_key(target, key, "null") + # copy over all source dict items + for key in doc.xref_get_keys(source): + item = doc.xref_get_key(source, key) + doc.xref_set_key(target, key, item[1]) + return None diff --git a/fitz/version.i b/fitz/version.i new file mode 100644 index 0000000..7c028e2 --- /dev/null +++ b/fitz/version.i @@ -0,0 +1,6 @@ +%pythoncode %{ +VersionFitz = "1.22.2" # MuPDF version. +VersionBind = "1.22.5" # PyMuPDF version. +VersionDate = "2023-06-21 00:00:01" +version = (VersionBind, VersionFitz, "20230621000001") +%} diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..2a62ff1 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "swig"] +build-backend = "setuptools.build_meta" diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..4b74c29 --- /dev/null +++ b/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +python_files = + tests/test_*.py diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..16e11f3 --- /dev/null +++ b/setup.py @@ -0,0 +1,907 @@ +''' +Overview: + + We hard-code the URL of the MuPDF .tar.gz file that we require. This + generally points to a particular source release on mupdf.com. + + Default behaviour: + + Building an sdist: + We download the MuPDF .tar.gz file and embed within the sdist. + + Building PyMuPDF: + If we are not in an sdist we first download the mupdf .tar.gz file. + + Then we extract and build MuPDF locally, before setuptools builds + PyMuPDF. So PyMuPDF will always be built with the exact MuPDF + release that we require. + +Environmental variables: + + PYMUPDF_SETUP_DEVENV + Location of devenv.com on Windows. If unset we search in some + hard-coded default locations; if that fails we use just 'devenv.com'. + + PYMUPDF_SETUP_MUPDF_BUILD + If set, overrides location of mupdf when building PyMuPDF: + Empty string: + Build PyMuPDF with the system mupdf. + A string starting with 'git:': + Use `git clone` to get a mupdf directory. We use the string in + the git clone command; it must contain the git URL from which + to clone, and can also contain other `git clone` args, for + example: + PYMUPDF_SETUP_MUPDF_BUILD="git:--branch master https://github.com/ArtifexSoftware/mupdf.git" + Otherwise: + Location of mupdf directory. + + In addition if MuPDF is a git checkout and the branch is 'master', + PyMuPDF is configured to build with MuPDF master branch, which may + have a slightly different API from the current release banch. + + PYMUPDF_SETUP_MUPDF_BUILD_BRANCH + If set to 'master', PyMuPDF is configured to build with MuPDF master + branch, which may have a slightly different API from the current + release banch. + + Other values are ignored. + + This is typically only useful if PYMUPDF_SETUP_MUPDF_BUILD is also set, + and not required if mupdf is a git checkout. + + PYMUPDF_SETUP_MUPDF_BUILD_TYPE + Unix only. Controls build type of MuPDF. Supported values are: + debug + memento + release (default) + + PYMUPDF_SETUP_MUPDF_CLEAN + Unix only. If '1', we do a clean MuPDF build. + + PYMUPDF_SETUP_MUPDF_THIRD + If '0' and we are building on Linux with the system MuPDF + (i.e. PYMUPDF_SETUP_MUPDF_BUILD=''), then don't link with + `-lmupdf-third`. + + PYMUPDF_SETUP_MUPDF_TGZ + If set, overrides location of MuPDF .tar.gz file: + Empty string: + Do not download MuPDF .tar.gz file. Sdist's will not contain + MuPDF. + + A string containing '://': + The URL from which to download the MuPDF .tar.gz file. Leaf + must match mupdf-*.tar.gz. + + Otherwise: + The path of local mupdf git checkout. We put all files in this + checkout known to git into a local tar archive. + + PYMUPDF_SETUP_MUPDF_OVERWRITE_CONFIG + If '0' we do not overwrite MuPDF's include/mupdf/fitz/config.h with + PyMuPDF's own configuration file, before building MuPDF. + + PYMUPDF_SETUP_MUPDF_REBUILD + If 0 we do not (re)build mupdf. + +Known build failures: + Linux: + *musllinux*. + Windows: + pp*: + fitz_wrap.obj : error LNK2001: unresolved external symbol PyUnicode_DecodeRawUnicodeEscape + + When using cibuildwheel, one can avoid building these failing wheels with: + CIBW_SKIP='*musllinux* pp*' +''' + +import glob +import json +import os +import platform +import re +import shutil +import stat +import subprocess +import sys +import tarfile +import time +import urllib.request + +from setuptools import Extension, setup +from setuptools.command.build_py import build_py as build_py_orig + + +_log_prefix = None +def log( text): + global _log_prefix + if not _log_prefix: + p = os.path.abspath( __file__) + p, p1 = os.path.split( p) + p, p0 = os.path.split( p) + _log_prefix = os.path.join( p0, p1) + print(f'{_log_prefix}: {text}', file=sys.stderr) + sys.stderr.flush() + + +if 1: + # For debugging. + log(f'sys.argv: {sys.argv}') + log(f'os.getcwd(): {os.getcwd()}') + log(f'__file__: {__file__}') + log(f'$PYTHON_ARCH: {os.environ.get("PYTHON_ARCH")!r}') + log(f'os.environ ({len(os.environ)}):') + for k, v in os.environ.items(): + log( f' {k}: {v}') + + +# setuptools seems to require current directory to be PyMuPDF. +# +assert os.path.abspath( os.getcwd()) == os.path.abspath( f'{__file__}/..'), \ + f'Current directory must be the PyMuPDF directory' + + +def remove(path): + ''' + Removes file or directory, without raising exception if it doesn't exist. + + We assert-fail if the path still exists when we return, in case of + permission problems etc. + ''' + # First try deleting `path` as a file. + try: + os.remove( path) + except Exception as e: + pass + + if os.path.exists(path): + # Try deleting `path` as a directory. Need to use + # shutil.rmtree() callback to handle permission problems; see: + # https://docs.python.org/3/library/shutil.html#rmtree-example + # + def error_fn(fn, path, excinfo): + # Clear the readonly bit and reattempt the removal. + os.chmod(path, stat.S_IWRITE) + fn(path) + shutil.rmtree( path, onerror=error_fn) + + assert not os.path.exists( path) + + +def tar_check(path, mode='r:gz', prefix=None, remove=False): + ''' + Checks items in tar file have same , or if not None. + + We fail if items in tar file have different top-level directory names. + + path: + The tar file. + mode: + As tarfile.open(). + prefix: + If not None, we fail if tar file's is not . + + Returns the directory name (which will be if not None). + ''' + with tarfile.open( path, mode) as t: + items = t.getnames() + assert items + item = items[0] + assert not item.startswith('./') and not item.startswith('../') + s = item.find('/') + if s == -1: + prefix_actual = item + '/' + else: + prefix_actual = item[:s+1] + if prefix: + assert prefix == prefix_actual, f'prefix={prefix} prefix_actual={prefix_actual}' + for item in items[1:]: + assert item.startswith( prefix_actual), f'prefix_actual={prefix_actual!r} != item={item!r}' + return prefix_actual + + +def tar_extract(path, mode='r:gz', prefix=None, exists='raise'): + ''' + Extracts tar file. + + We fail if items in tar file have different . + + path: + The tar file. + mode: + As tarfile.open(). + prefix: + If not None, we fail if tar file's is not . + exists: + What to do if already exists: + 'raise': raise exception. + 'remove': remove existing file/directory before extracting. + 'return': return without extracting. + + Returns the directory name (which will be if not None, with '/' + appended if not already present). + ''' + prefix_actual = tar_check( path, mode, prefix) + if os.path.exists( prefix_actual): + if exists == 'raise': + raise Exception( f'Path already exists: {prefix_actual!r}') + elif exists == 'remove': + remove( prefix_actual) + elif exists == 'return': + log( f'Not extracting {path} because already exists: {prefix_actual}') + return prefix_actual + else: + assert 0, f'Unrecognised exists={exists!r}' + assert not os.path.exists( prefix_actual), f'Path already exists: {prefix_actual}' + log( f'Extracting {path}') + with tarfile.open( path, mode) as t: + t.extractall() + return prefix_actual + + +def get_gitfiles( directory, submodules=False): + ''' + Returns list of all files known to git in ; must be + somewhere within a git checkout. + + Returned names are all relative to . + + If .git exists we use git-ls-files and write list of files to + /jtest-git-files. + + Otherwise we require that /jtest-git-files already exists. + ''' + def is_within_git_checkout( d): + while 1: + #log( 'd={d!r}') + if not d: + break + if os.path.isdir( f'{d}/.git'): + return True + d = os.path.dirname( d) + + if is_within_git_checkout( directory): + command = 'cd ' + directory + ' && git ls-files' + if submodules: + command += ' --recurse-submodules' + command += ' > jtest-git-files' + log( f'Running: {command}') + subprocess.run( command, shell=True, check=True) + + with open( '%s/jtest-git-files' % directory, 'r') as f: + text = f.read() + ret = text.strip().split( '\n') + return ret + + +def word_size(): + ''' + Returns integer word size (32 or 64) of build. + ''' + # Looks like on Windows, cibuildwheel runs us with a 64-bit Python + # interpreter even when building a 32-bit wheel. It appears to set + # PYTHON_ARCH to indicate word size (this isn't documented anywhere?). + # + a = os.environ.get( 'PYTHON_ARCH') + if a is None: + if sys.maxsize == 2**31-1: + return 32 + elif sys.maxsize == 2**63-1: + return 64 + else: + assert 0, 'Unrecognised sys.maxsize={sys.maxsize!r}' + else: + if a == '32': + return 32 + elif a == '64': + return 64 + else: + assert 0, f'Unrecognised $PYTHON_ARCH={a!r}' + + +class build_ext_first(build_py_orig): + """ + custom build_py command which runs build_ext first + this is necessary because build_py needs the fitz.py which is only generated + by SWIG in the build_ext step + """ + def run(self): + self.run_command("build_ext") + return super().run() + + +DEFAULT = ["mupdf"] +if os.environ.get( 'PYMUPDF_SETUP_MUPDF_THIRD') != '0': + DEFAULT.append("mupdf-third") + +ALPINE = DEFAULT + [ + "jbig2dec", + "jpeg", + "openjp2", + "harfbuzz", +] + +ARCH_LINUX = DEFAULT + [ + "jbig2dec", + "openjp2", + "jpeg", + "freetype", + "gumbo", +] + +NIX = ARCH_LINUX + [ + "harfbuzz", +] + +OPENSUSE = NIX + [ + "png16", +] + +DEBIAN = OPENSUSE + [ + "mujs", +] + +FEDORA = NIX + [ + "leptonica", + "tesseract", +] + +LIBRARIES = { + "default": DEFAULT, + "ubuntu": DEBIAN, + "arch": ARCH_LINUX, + "manjaro": ARCH_LINUX, + "artix": ARCH_LINUX, + "opensuse": OPENSUSE, + "fedora": FEDORA, + "alpine": ALPINE, + "nix": NIX, + "debian": DEBIAN, +} + + +def load_libraries(): + if os.getenv("NIX_STORE"): + return LIBRARIES["nix"] + + try: + import distro + + os_id = distro.id() + except: + os_id = "" + if os_id in list(LIBRARIES.keys()) + ["manjaro", "artix"]: + return LIBRARIES[os_id] + + filepath = "/etc/os-release" + if not os.path.exists(filepath): + return LIBRARIES["default"] + regex = re.compile("^([\\w]+)=(?:'|\")?(.*?)(?:'|\")?$") + with open(filepath) as os_release: + info = { + regex.match(line.strip()).group(1): re.sub( + r'\\([$"\'\\`])', r"\1", regex.match(line.strip()).group(2) + ) + for line in os_release + if regex.match(line.strip()) + } + + os_id = info["ID"] + if os_id.startswith("opensuse"): + os_id = "opensuse" + if os_id not in LIBRARIES.keys(): + return LIBRARIES["default"] + return LIBRARIES[os_id] + + + + +def get_git_id( directory): + ''' + Returns `(sha, comment, diff, branch)`, all items are str or None if not + available. + + directory: + Root of git checkout. + ''' + sha, comment, diff, branch = '', '', '', '' + cp = subprocess.run( + f'cd {directory} && (PAGER= git show --pretty=oneline|head -n 1 && git diff)', + capture_output=1, + shell=1, + text=1, + ) + if cp.returncode == 0: + sha, _ = cp.stdout.split(' ', 1) + comment, diff = _.split('\n', 1) + cp = subprocess.run( + f'cd {directory} && git rev-parse --abbrev-ref HEAD', + capture_output=1, + shell=1, + text=1, + ) + if cp.returncode == 0: + branch = cp.stdout.strip() + log(f'get_git_id(): directory={directory!r} returning branch={branch!r} sha={sha!r} comment={comment!r}') + return sha, comment, diff, branch + + +mupdf_tgz = os.path.abspath( f'{__file__}/../mupdf.tgz') + +def get_mupdf_tgz(): + ''' + Creates .tgz file containing MuPDF source, for inclusion in an sdist. + + What we do depends on environmental variable PYMUPDF_SETUP_MUPDF_TGZ; see + docs at start of this file for details. + + Returns name of top-level directory within the .tgz file. + ''' + mupdf_url_or_local = os.environ.get( + 'PYMUPDF_SETUP_MUPDF_TGZ', + 'https://mupdf.com/downloads/archive/mupdf-1.22.2-source.tar.gz', + ) + log( f'mupdf_url_or_local={mupdf_url_or_local!r}') + if mupdf_url_or_local == '': + # No mupdf in sdist. + log( 'mupdf_url_or_local is empty string so removing any mupdf_tgz={mupdf_tgz}') + remove( mupdf_tgz) + return + + if '://' in mupdf_url_or_local: + # Download from URL into . + mupdf_url = mupdf_url_or_local + mupdf_url_leaf = os.path.basename( mupdf_url) + leaf = '.tar.gz' + assert mupdf_url_leaf.endswith(leaf), f'Unrecognised suffix in mupdf_url={mupdf_url!r}' + mupdf_local = mupdf_url_leaf[ : -len(leaf)] + assert mupdf_local.startswith( 'mupdf-') + log(f'Downloading from: {mupdf_url}') + remove( mupdf_url_leaf) + urllib.request.urlretrieve( mupdf_url, mupdf_url_leaf) + assert os.path.exists( mupdf_url_leaf) + tar_check( mupdf_url_leaf, 'r:gz', f'{mupdf_local}/') + if mupdf_url_leaf != mupdf_tgz: + remove( mupdf_tgz) + os.rename( mupdf_url_leaf, mupdf_tgz) + return mupdf_local + + else: + # Create archive contining local mupdf directory's git + # files. + mupdf_local = mupdf_url_or_local + if mupdf_local.endswith( '/'): + del mupdf_local[-1] + assert os.path.isdir( mupdf_local), f'Not a directory: {mupdf_local!r}' + log( f'Creating .tgz from git files in: {mupdf_local}') + remove( mupdf_tgz) + with tarfile.open( mupdf_tgz, 'w:gz') as f: + for name in get_gitfiles( mupdf_local, submodules=True): + path = os.path.join( mupdf_local, name) + if os.path.isfile( path): + f.add( path, f'mupdf/{name}', recursive=False) + return mupdf_local + + +def get_mupdf(): + ''' + Downloads and/or extracts mupdf and returns location of mupdf directory. + + Exact behaviour depends on environmental variable + PYMUPDF_SETUP_MUPDF_BUILD; see docs at start of this file for details. + ''' + path = os.environ.get( 'PYMUPDF_SETUP_MUPDF_BUILD') + log( f'PYMUPDF_SETUP_MUPDF_BUILD={path!r}') + if path is None: + # Default. + if os.path.exists( mupdf_tgz): + log( f'mupdf_tgz already exists: {mupdf_tgz}') + else: + get_mupdf_tgz() + return tar_extract( mupdf_tgz, exists='return') + + elif path == '': + # Use system mupdf. + log( f'PYMUPDF_SETUP_MUPDF_BUILD="", using system mupdf') + return None + + git_prefix = 'git:' + if path.startswith( git_prefix): + # Get git clone of mupdf. + # + # `mupdf_url_or_local` is taken to be portion of a `git clone` command, + # for example: + # + # PYMUPDF_SETUP_MUPDF_BUILD="git:--branch master git://git.ghostscript.com/mupdf.git" + # PYMUPDF_SETUP_MUPDF_BUILD="git:--branch 1.20.x https://github.com/ArtifexSoftware/mupdf.git" + # PYMUPDF_SETUP_MUPDF_BUILD="git:--branch master https://github.com/ArtifexSoftware/mupdf.git" + # + # One would usually also set PYMUPDF_SETUP_MUPDF_TGZ= (empty string) to + # avoid the need to download a .tgz into an sdist. + # + command_suffix = path[ len(git_prefix):] + path = 'mupdf' + + # Remove any existing directory to avoid the clone failing. (We could + # assume any existing directory is a git checkout, and do `git pull` or + # similar, but that's complicated and fragile.) + # + remove(path) + + command = ('' + + f'git clone' + + f' --recursive' + #+ f' --single-branch' + #+ f' --recurse-submodules' + + f' --depth 1' + + f' --shallow-submodules' + #+ f' --branch {branch}' + #+ f' git://git.ghostscript.com/mupdf.git' + + f' {command_suffix}' + + f' {path}' + ) + log( f'Running: {command}') + subprocess.run( command, shell=True, check=True) + + # Show sha of checkout. + command = f'cd {path} && git show --pretty=oneline|head -n 1' + log( f'Running: {command}') + subprocess.run( command, shell=True, check=False) + + if 1: + # Use custom mupdf directory. + log( f'Using custom mupdf directory from $PYMUPDF_SETUP_MUPDF_BUILD: {path}') + assert os.path.isdir( path), f'$PYMUPDF_SETUP_MUPDF_BUILD is not a directory: {path}' + return path + + +include_dirs = [] +library_dirs = [] +libraries = [] +extra_swig_args = [] +extra_link_args = [] +extra_compile_args = [] + +log( f'platform.system()={platform.system()!r}') +log( f'sys.platform={sys.platform!r}') + +linux = platform.system() == 'Linux' +openbsd = platform.system() == 'OpenBSD' +freebsd = platform.system() == 'FreeBSD' +darwin = platform.system() == 'Darwin' +windows = platform.system() == 'Windows' or platform.system().startswith('CYGWIN') + + +if 'sdist' in sys.argv: + # Create local mupdf.tgz, for inclusion in sdist. + get_mupdf_tgz() + + +if ('-h' not in sys.argv and '--help' not in sys.argv + and (0 + or 'bdist_wheel' in sys.argv + or 'build' in sys.argv + or 'bdist' in sys.argv + or 'install' in sys.argv + ) + ): + + # Build MuPDF before setuptools runs, so that it can link with the MuPDF + # libraries. + # + mupdf_local = get_mupdf() + if mupdf_local: + if mupdf_local.endswith( '/'): + mupdf_local = mupdf_local[:-1] + + log( f'mupdf_local={mupdf_local!r}') + unix_build_dir = None + + # Always force clean build of PyMuPDF SWIG files etc, because setuptools + # doesn't seem to notice when our mupdf headers etc are newer than the + # SWIG-generated files. + # + remove( os.path.abspath( f'{__file__}/../build/')) + remove( os.path.abspath( f'{__file__}/../install/')) + + if mupdf_local: + # Build MuPDF before deferring to setuptools.setup(). + # + + log( f'Building mupdf.') + # Copy PyMuPDF's config file into mupdf. For example it #define's TOFU, + # which excludes various fonts in the MuPDF binaries. + if os.environ.get('PYMUPDF_SETUP_MUPDF_OVERWRITE_CONFIG') == '0': + # Use MuPDF default config. + log( f'Not copying fitz/_config.h to {mupdf_local}/include/mupdf/fitz/config.h.') + s = os.stat( f'{mupdf_local}/include/mupdf/fitz/config.h') + log( f'{mupdf_local}/include/mupdf/fitz/config.h: {s} mtime={time.strftime("%F-%T", time.gmtime(s.st_mtime))}') + else: + # Use our special config in MuPDF. + log( f'Copying fitz/_config.h to {mupdf_local}/include/mupdf/fitz/config.h') + shutil.copy2( 'fitz/_config.h', f'{mupdf_local}/include/mupdf/fitz/config.h') + + if windows: + # Windows build. + devenv = os.environ.get('PYMUPDF_SETUP_DEVENV') + log( 'PYMUPDF_SETUP_DEVENV={PYMUPDF_SETUP_DEVENV!r}') + if not devenv: + # Search for devenv in some known locations. + devenv = glob.glob('C:/Program Files (x86)/Microsoft Visual Studio/2019/*/Common7/IDE/devenv.com') + if devenv: + devenv = devenv[0] + if not devenv: + devenv = 'devenv.com' + log( f'Cannot find devenv.com in default locations, using: {devenv!r}') + windows_config = 'Win32' if word_size()==32 else 'x64' + command = ( + f'cd {mupdf_local}&&' + f'"{devenv}"' + f' platform/win32/mupdf.sln' + f' /Build "ReleaseTesseract|{windows_config}"' + f' /Project mupdf' + ) + else: + # Unix build. + # + flags = 'HAVE_X11=no HAVE_GLFW=no HAVE_GLUT=no HAVE_LEPTONICA=yes HAVE_TESSERACT=yes' + flags += ' verbose=yes' + env = '' + make = 'make' + if linux: + env += ' CFLAGS="-fPIC"' + if openbsd or freebsd: + make = 'gmake' + env += ' CFLAGS="-fPIC" CXX=clang++' + + unix_build_type = os.environ.get( 'PYMUPDF_SETUP_MUPDF_BUILD_TYPE', 'release') + assert unix_build_type in ('debug', 'memento', 'release') + flags += f' build={unix_build_type}' + + # This is for MacOS cross-compilation, where ARCHFLAGS can be + # '-arch arm64'. + # + archflags = os.environ.get( 'ARCHFLAGS') + if archflags: + flags += f' XCFLAGS="{archflags}" XLIBS="{archflags}"' + + # We specify a build directory path containing 'pymupdf' so that we + # coexist with non-pymupdf builds (because pymupdf builds have a + # different config.h). + # + # We also append further text to try to allow different builds to + # work if they reuse the mupdf directory. + # + # Using platform.machine() (e.g. 'amd64') ensures that different + # builds of mupdf on a shared filesystem can coexist. Using + # $_PYTHON_HOST_PLATFORM allows cross-compiled cibuildwheel builds + # to coexist, e.g. on github. + # + build_prefix = f'pymupdf-{platform.machine()}-' + build_prefix_extra = os.environ.get( '_PYTHON_HOST_PLATFORM') + if build_prefix_extra: + build_prefix += f'{build_prefix_extra}-' + flags += f' build_prefix={build_prefix}' + + unix_build_dir = f'{mupdf_local}/build/{build_prefix}{unix_build_type}' + + if os.environ.get( 'PYMUPDF_SETUP_MUPDF_CLEAN') == '1': + # Force clean build. + log(f'Removing {unix_build_dir} because PYMUPDF_SETUP_MUPDF_CLEAN=1') + assert '/build/' in unix_build_dir + remove(unix_build_dir) + + command = f'cd {mupdf_local} && {env} {make} {flags}' + command += f' && echo {unix_build_dir}:' + command += f' && ls -l build/{build_prefix}{unix_build_type}' + + if os.environ.get( 'PYMUPDF_SETUP_MUPDF_REBUILD') == '0': + log( f'PYMUPDF_SETUP_MUPDF_REBUILD is "0" so not building MuPDF; would have run: {command}') + else: + log( f'Building MuPDF by running: {command}') + subprocess.run( command, shell=True, check=True) + log( f'Finished building mupdf.') + else: + # Use installed MuPDF. + log( f'Using system mupdf.') + unix_build_type = '' + + # Set include and library paths for building PyMuPDF. + # + # We also add MuPDF's include directory to include path for Swig so that + # fitz/fitz.i can do `%include "mupdf/fitz/version.h"` and .i code can use + # `#if` with FZ_VERSION_* macros. + # + if mupdf_local: + assert os.path.isdir( mupdf_local), f'Not a directory: {mupdf_local!r}' + include_dirs.append( f'{mupdf_local}/include') + include_dirs.append( f'{mupdf_local}/include/mupdf') + include_dirs.append( f'{mupdf_local}/thirdparty/freetype/include') + if unix_build_dir: + library_dirs.append( unix_build_dir) + extra_swig_args.append(f'-I{mupdf_local}/include') + + if mupdf_local and (linux or openbsd or freebsd): + # setuptools' link command always seems to put '-L + # /usr/local/lib' before any that we specify, + # so '-l mupdf -l mupdf-third' will end up using the system + # libmupdf.so (if installed) instead of the one we've built in + # . + # + # So we force linking with our mupdf libraries by specifying + # them in . + # + extra_link_args.append( f'{unix_build_dir}/libmupdf.a') + extra_link_args.append( f'{unix_build_dir}/libmupdf-third.a') + library_dirs = [] + libraries = [] + if openbsd or freebsd: + if os.environ.get( 'PYMUPDF_SETUP_MUPDF_BUILD_TYPE') == 'memento': + extra_link_args.append( f'-lexecinfo') + + elif mupdf_local and darwin: + library_dirs.append(f'{unix_build_dir}') + libraries = [ + f'mupdf', + f'mupdf-third', + ] + + elif linux: + # Use system libraries. + include_dirs.append( '/usr/include/mupdf') + include_dirs.append( '/usr/local/include/mupdf') + include_dirs.append( '/usr/include/freetype2') + libraries = load_libraries() + extra_link_args = [] + extra_swig_args.append(f'-I/usr/local/include') + extra_swig_args.append(f'-I/usr/include') + + elif darwin or openbsd or freebsd: + # Use system libraries. + include_dirs.append("/usr/local/include/mupdf") + include_dirs.append("/usr/local/include") + include_dirs.append("/opt/homebrew/include/mupdf") + library_dirs.append("/usr/local/lib") + libraries = ["mupdf", "mupdf-third"] + library_dirs.append("/opt/homebrew/lib") + + include_dirs.append("/usr/include/freetype2") + include_dirs.append("/usr/local/include/freetype2") + include_dirs.append("/usr/X11R6/include/freetype2") + include_dirs.append("/opt/homebrew/include") + include_dirs.append("/opt/homebrew/include/freetype2") + + extra_swig_args.append(f'-I/usr/local/include') + extra_swig_args.append(f'-I/opt/homebrew/include') + + library_dirs.append("/opt/homebrew/lib") + + if freebsd: + libraries += [ + 'freetype', + 'harfbuzz', + ] + + elif windows: + # Windows. + assert mupdf_local + if word_size() == 32: + library_dirs.append( f'{mupdf_local}/platform/win32/ReleaseTesseract') + library_dirs.append( f'{mupdf_local}/platform/win32/Release') + else: + library_dirs.append( f'{mupdf_local}/platform/win32/x64/ReleaseTesseract') + library_dirs.append( f'{mupdf_local}/platform/win32/x64/Release') + libraries = [ + "libmupdf", + "libresources", + "libthirdparty", + ] + extra_link_args = ["/NODEFAULTLIB:MSVCRT"] + + else: + assert 0, 'Unrecognised OS' + + if linux or openbsd or freebsd or darwin: + extra_compile_args.append( '-Wno-incompatible-pointer-types') + extra_compile_args.append( '-Wno-pointer-sign') + extra_compile_args.append( '-Wno-sign-compare') + if unix_build_type == 'memento': + extra_compile_args.append( '-DMEMENTO') + if openbsd: + extra_compile_args.append( '-Wno-deprecated-declarations') + + # add any local include and library folders + pymupdf_dirs = os.environ.get("PYMUPDF_DIRS", None) + if pymupdf_dirs: + with open(pymupdf_dirs) as dirfile: + local_dirs = json.load(dirfile) + include_dirs += local_dirs.get("include_dirs", []) + library_dirs += local_dirs.get("library_dirs", []) + + with open(f'fitz/helper-git-versions.i', 'w') as f: + f.write('%pythoncode %{\n') + + def repr_escape(text): + text = repr(text) + text = text.replace('{', '{{') + text = text.replace('}', '}}') + text = text.replace('%', '{chr(37)})') # Avoid confusing swig. + return 'f' + text + def write_git(name, directory): + sha, comment, diff, branch = get_git_id(directory) + f.write(f'{name}_git_sha = \'{sha}\'\n') + f.write(f'{name}_git_comment = {repr_escape(comment)}\n') + f.write(f'{name}_git_diff = {repr_escape(diff)}\n') + f.write(f'{name}_git_branch = {repr_escape(branch)}\n') + f.write('\n') + + write_git('pymupdf', '.') + if mupdf_local: + write_git('mupdf', mupdf_local) + + f.write('%}\n') + +# Disable bogus SWIG warning 509, 'Overloaded method ... effectively ignored, +# as it is shadowed by ...'. +extra_swig_args.append( '-w509') + +log( f'include_dirs={include_dirs}') +log( f'library_dirs={library_dirs}') +log( f'libraries={libraries}') +log( f'extra_swig_args={extra_swig_args}') +log( f'extra_compile_args={extra_compile_args}') +log( f'extra_link_args={extra_link_args}') + +module = Extension( + "fitz._fitz", + ["fitz/fitz.i"], + language="c++", + include_dirs=include_dirs, + library_dirs=library_dirs, + libraries=libraries, + extra_compile_args=extra_compile_args, + extra_link_args=extra_link_args, + swig_opts=extra_swig_args, +) + + +setup_py_cwd = os.path.dirname(__file__) +classifiers = [ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: C", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: Implementation :: CPython", + "Topic :: Utilities", + "Topic :: Multimedia :: Graphics", + "Topic :: Software Development :: Libraries", +] +with open(os.path.join(setup_py_cwd, "README.md"), encoding="utf-8") as f: + readme = f.read() + +setup( + name="PyMuPDF", + version="1.22.5", + description="Python bindings for the PDF toolkit and renderer MuPDF", + long_description=readme, + long_description_content_type="text/markdown", + classifiers=classifiers, + url="https://github.com/pymupdf/PyMuPDF", + author="Artifex", + author_email="support@artifex.com", + cmdclass={"build_py": build_ext_first}, + ext_modules=[module], + python_requires=">=3.7", + py_modules=["fitz.fitz", "fitz.utils", "fitz.__main__"], + license="GNU AFFERO GPL 3.0", + project_urls={ + "Documentation": "https://pymupdf.readthedocs.io/", + "Source": "https://github.com/pymupdf/pymupdf", + "Tracker": "https://github.com/pymupdf/PyMuPDF/issues", + "Changelog": "https://pymupdf.readthedocs.io/en/latest/changes.html", + }, +) diff --git a/signatures/version1/cla.json b/signatures/version1/cla.json new file mode 100644 index 0000000..eff05a3 --- /dev/null +++ b/signatures/version1/cla.json @@ -0,0 +1,60 @@ +{ + "signedContributors": [ + { + "name": "jamie-lemon", + "id": 107279992, + "comment_id": 1346836521, + "created_at": "2022-12-12T16:30:10Z", + "repoId": 6105714, + "pullRequestNo": 2118 + }, + { + "name": "julian-smith-artifex-com", + "id": 83358719, + "comment_id": 1347087940, + "created_at": "2022-12-12T18:56:01Z", + "repoId": 6105714, + "pullRequestNo": 2120 + }, + { + "name": "JorjMcKie", + "id": 8290722, + "comment_id": 1347104970, + "created_at": "2022-12-12T19:02:45Z", + "repoId": 6105714, + "pullRequestNo": 2120 + }, + { + "name": "JorjMcKie", + "id": 8290722, + "comment_id": 1347107260, + "created_at": "2022-12-12T19:03:35Z", + "repoId": 6105714, + "pullRequestNo": 2120 + }, + { + "name": "arun-mani-j", + "id": 49952138, + "comment_id": 1374488387, + "created_at": "2023-01-07T13:50:31Z", + "repoId": 6105714, + "pullRequestNo": 2162 + }, + { + "name": "cbm755", + "id": 818622, + "comment_id": 1442341286, + "created_at": "2023-02-23T19:49:14Z", + "repoId": 6105714, + "pullRequestNo": 2234 + }, + { + "name": "kianmeng", + "id": 134518, + "comment_id": 1498410207, + "created_at": "2023-04-06T02:38:02Z", + "repoId": 6105714, + "pullRequestNo": 2315 + } + ] +} \ No newline at end of file diff --git a/tests/README.md b/tests/README.md new file mode 100644 index 0000000..4842ff4 --- /dev/null +++ b/tests/README.md @@ -0,0 +1,91 @@ +# Testing your PyMuPDF Installation +This folder contains a number of basic tests to confirm that PyMuPDF is correctly installed. + +The following areas are currently covered: +* encryption and decryption +* extraction of drawings +* "geometry": correct working of points, rectangles, matrices and operator algebra +* image bbox computation +* handling of embedded files +* image insertion +* PDF document joining +* computation of quadrilaterals for non-horizontal text +* extraction of non-unicode fontnames +* handling of PDF standard metadata +* handling of non-PDF document types +* programmatic editing of PDF object definition sources +* mass deletion of PDF pages +* handling of PDF page labels +* pixmap handling +* show PDF pages inside other PDF pages +* text extraction +* text searching +* handling of PDF Tables of Contents +* annotation handling +* field / widget handling +* image extraction + +This is **_not a coverage test_**, although a significant part of the relevant Python part **_does_** get executed (ca. 80%). Achieving a much higher code coverage remains an ongoing task. + +To use these scripts, you must have installed `pytest`: + +`python -m pip install pytest` + +Then simply execute `python -m pytest` in a terminal of this folder. `pytest` will automatically locate all scripts and execute them. All tests should run successfully and you will see an output like this: + +``` +pytest --cov=fitz +============================ test session starts ============================= +platform linux -- Python 3.8.5, pytest-6.2.4, py-1.10.0, pluggy-0.13.1 +rootdir: /mnt/d/harald/desktop/fitzPython119/pymupdf +plugins: cov-2.12.0 +collected 79 items + +test_annots.py ............... [ 18%] +test_badfonts.py . [ 20%] +test_crypting.py . [ 21%] +test_drawings.py .. [ 24%] +test_embeddedfiles.py . [ 25%] +test_font.py .. [ 27%] +test_general.py ............ [ 43%] +test_geometry.py ....... [ 51%] +test_imagebbox.py . [ 53%] +test_insertimage.py . [ 54%] +test_insertpdf.py . [ 55%] +test_linequad.py . [ 56%] +test_metadata.py .. [ 59%] +test_nonpdf.py ... [ 63%] +test_object_manipulation.py ... [ 67%] +test_optional_content.py .. [ 69%] +test_pagedelete.py . [ 70%] +test_pagelabels.py . [ 72%] +test_pixmap.py ...... [ 79%] +test_showpdfpage.py . [ 81%] +test_textbox.py .... [ 86%] +test_textextract.py . [ 87%] +test_textsearch.py . [ 88%] +test_toc.py .... [ 93%] +test_widgets.py ..... [100%] + +----------- coverage: platform linux, python 3.8.5-final-0 ----------- +Name Stmts Miss Cover +----------------------------------------------------------------------------- +/usr/local/lib/python3.8/dist-packages/fitz/__init__.py 335 13 96% +/usr/local/lib/python3.8/dist-packages/fitz/fitz.py 4183 740 82% +/usr/local/lib/python3.8/dist-packages/fitz/utils.py 2196 669 70% +----------------------------------------------------------------------------- +TOTAL 6714 1422 79% + + +============================ 79 passed in 5.76s ============================== +``` + +## Known test failure with non-default build of MuPDF + +If PyMuPDF has been built with a non-default build of MuPDF (using +environmental variable ``PYMUPDF_SETUP_MUPDF_BUILD``), it is possible that +``tests/test_textbox.py:test_textbox3()`` will fail, because it relies on MuPDF +having been built with PyMuPDF's customized configuration, ``fitz/_config.h``. + +One can skip this particular test by adding ``-k 'not test_textbox3'`` to the +pytest command line. diff --git a/tests/resources/001003ED.pdf b/tests/resources/001003ED.pdf new file mode 100644 index 0000000..a505dbb --- /dev/null +++ b/tests/resources/001003ED.pdf @@ -0,0 +1,1581 @@ +%PDF-1.6 +%%μῦ + +1 0 obj +<> +endobj + +2 0 obj +<> +endobj + +3 0 obj +<> +endobj + +4 0 obj +<> +endobj + +5 0 obj +<> +endobj + +6 0 obj +<> +endobj + +7 0 obj +<> +endobj + +8 0 obj +<> +endobj + +9 0 obj +<> +endobj + +10 0 obj +<> +endobj + +11 0 obj +<> +endobj + +12 0 obj +<> +endobj + +13 0 obj +<> +endobj + +14 0 obj +<> +endobj + +15 0 obj +<> +endobj + +16 0 obj +<> +endobj + +17 0 obj +<> +endobj + +18 0 obj +<> +endobj + +19 0 obj +<> +endobj + +20 0 obj +<> +endobj + +21 0 obj +<> +endobj + +22 0 obj +<> +endobj + +23 0 obj +<> +endobj + +24 0 obj +<> +endobj + +25 0 obj +<> +endobj + +26 0 obj +<> +endobj + +27 0 obj +<> +endobj + +28 0 obj +<> +endobj + +29 0 obj +<> +endobj + +30 0 obj +<> +endobj + +31 0 obj +<> +endobj + +32 0 obj +<> +endobj + +33 0 obj +<> +endobj + +34 0 obj +<> +endobj + +35 0 obj +<> +endobj + +36 0 obj +<> +endobj + +37 0 obj +<> +endobj + +38 0 obj +<> +endobj + +39 0 obj +<> +endobj + +40 0 obj +<> +endobj + +41 0 obj +<> +endobj + +42 0 obj +<> +endobj + +43 0 obj +<> +endobj + +44 0 obj +<> +endobj + +45 0 obj +<> +endobj + +46 0 obj +<> +endobj + +47 0 obj +<> +endobj + +48 0 obj +<> +endobj + +49 0 obj +<> +endobj + +50 0 obj +<> +endobj + +51 0 obj +<> +endobj + +52 0 obj +<> +endobj + +53 0 obj +<> +endobj + +54 0 obj +<> +endobj + +55 0 obj +<> +endobj + +56 0 obj +<> +endobj + +57 0 obj +<> +endobj + +58 0 obj +<> +endobj + +59 0 obj +<> +endobj + +60 0 obj +<> +endobj + +61 0 obj +<> +endobj + +62 0 obj +<> +endobj + +63 0 obj +<> +endobj + +64 0 obj +<> +endobj + +65 0 obj +<> +endobj + +66 0 obj +<> +endobj + +67 0 obj +<> +endobj + +68 0 obj +<> +endobj + +69 0 obj +<> +endobj + +70 0 obj +<> +endobj + +71 0 obj +<> +endobj + +72 0 obj +<> +endobj + +73 0 obj +<> +endobj + +74 0 obj +<> +endobj + +75 0 obj +<> +endobj + +76 0 obj +<> +endobj + +77 0 obj +<> +endobj + +78 0 obj +<> +endobj + +79 0 obj +<> +endobj + +80 0 obj +<> +endobj + +81 0 obj +<> +endobj + +82 0 obj +<> +endobj + +83 0 obj +<> +endobj + +84 0 obj +<> +endobj + +85 0 obj +<> +endobj + +86 0 obj +<> +endobj + +87 0 obj +<> +endobj + +88 0 obj +<> +endobj + +89 0 obj +<> +endobj + +90 0 obj +<> +endobj + +91 0 obj +<> +endobj + +92 0 obj +<> +endobj + +93 0 obj +<> +endobj + +94 0 obj +<> +endobj + +95 0 obj +<> +endobj + +96 0 obj +<> +endobj + +97 0 obj +<> +endobj + +98 0 obj +<> +endobj + +99 0 obj +<> +endobj + +100 0 obj +<> +endobj + +101 0 obj +<> +endobj + +102 0 obj +<> +endobj + +103 0 obj +<> +endobj + +104 0 obj +<> +endobj + +105 0 obj +<> +endobj + +106 0 obj +<> +endobj + +107 0 obj +<> +endobj + +108 0 obj +<> +endobj + +109 0 obj +<> +endobj + +110 0 obj +<> +endobj + +111 0 obj +<> +endobj + +112 0 obj +<> +endobj + +113 0 obj +</M(D:20070120104154+01'00')/Name(ARE_Acrobat Collaboration V7.0 P9 0000109)/ByteRange[0 10074 18512 149767]/Reference[<>/Data 113 0 R/TransformMethod/UR3/Type/SigRef>>]/Prop_Build<>/App<>/PubSec<>>>/Type/Sig>>/UR</M(D:20070120104154+01'00')/Name(ARE_Acrobat Collaboration V7.0 P9 0000109)/Reference[<>/DigestValue<5242B30DBC898D2B7EEF55B22022B28E>/DigestMethod/MD5/Data 113 0 R/TransformMethod/UR/Type/SigRef>>]/Prop_Build<>/App<>/PubSec<>>>/Type/Sig>>>>/Metadata 199 0 R/AcroForm 115 0 R/Pages 111 0 R/OpenAction 114 0 R/Type/Catalog>> +endobj + +114 0 obj +<> +endobj + +115 0 obj +<>/Encoding<>>>/SigFlags 3>> +endobj + +116 0 obj +<> +endobj + +117 0 obj +<> +endobj + +118 0 obj +<> +endobj + +119 0 obj +<> +endobj + +120 0 obj +<> +endobj + +121 0 obj +<> +endobj + +122 0 obj +[/ICCBased 139 0 R] +endobj + +123 0 obj +[/Indexed 122 0 R 194 154 0 R] +endobj + +124 0 obj +[/Indexed 122 0 R 135 156 0 R] +endobj + +125 0 obj +[/Indexed 122 0 R 1 140 0 R] +endobj + +126 0 obj +[/Indexed 122 0 R 174 144 0 R] +endobj + +127 0 obj +<> +stream +H‰tWKvÛ8kR},šÿOõȎT*•®”­´ÏI¥”‹h‘ vÇóZA÷.²„,&¯!£ ú>¤dµ;9'dDïwß}ŸfǼ©zqÇ_tM§DË{%®™lvüBgìZ³À/Ê"Hð,Ã,à¯y±›pnøY±e; ü0Ë ^¥{ ƒ´(Œ;ø1cËëYâqž²åý쏝Ÿ½^þvñúäW6ÿÇò—Ùùröi2–~‘DÌ=²0ò£"f,ç!¹|Åv£ÑGçaÊBx˜ÇSTðâ­3äI@Nxóå?ÉðïöœØžSšcJ–Ç~K)‰¹ÒìõÚ(Y 7õwՈëy‡½š©J÷l-8û0ly3?÷z†ulš= x¬YËkź5WìžK±Ás÷”½ÍFÀ¡åyɖgd0DьÁ{®n¸d÷B™Ó„dkü÷²W]Ëè´\ôþžé¦ím#ùa<ÍàéºÒlæäö²æmõÔtaM„-cz­¸€…SXPnl ®ñ˜x'ƒÞÌ©j^µâ?±+|!÷¼é7êû·ž³ßVX~Wõ@ôЙ,›Ë¿Pq4êj.ל½Râö–ßVxo±Tw÷ø×l*K?öþN–žÐ¢“HÂÝhö–K=–ëP-.$6}%“ Læ©ÇÕ|cK ³È¾Ø ¾9ÉKË⠉YZK' Šf ä¨Àö‡zhvÿEFíë‰ÚpÙ )àB%Maυ¤XR @ԃÜèS òù^(™§øâd˜æ%%faüY ­©~æ]ÍS”¢é«¦Ä}Dm¸î»oÔ!ƒZs^q[³gpì­ØªkGKÖœ­”È»AØK›ìnx¶Ú…§ñ>º +cL„qK·Š!„·ÿã1x+Ù.ü(wᣁ7?±ù"SdìmŠØ–mÜ÷4yqVÚÝû@¥ø¿!ãñ‹:¥¯Ú‘üiЫ €Þ?ôøm£L~.{Íóež8åD[¼DàÛ¤þÄnT¤®9Á4·µõõ³[öÑ»ôYh[/ú8÷ÙY7¹õŠ/že³$.ìa5­”{-@ŠÄ—ŽBšëÛsÊ"WÊ+Ú»ËCucÛ ÿ«áXݱ•hÖ¼ßg™ Y†šêm6¶]ú¤íä ö¹)Ër32(Ÿ’½ít%¶$¶ + ~¨«fÅhšXybÚÝØ@;Và²à$R“ì\ök$‹òDž‹Ë´¥Ö£Gž¨f#™‡^ìôѳ©I²ÐM(C3ðzP×6Âڀ{¬‹q6Ê Gð«AYFímêÃ"ð–ó0žE³#EÌ·™ÅÀî°,È\¢÷‹]ZÕêÁ¼–Rg4~žGì²÷‡×"¿=“|¨”£˜”ì ݂®F?HÚÉ&Ÿ °Gn\1â—û²É ú“¾ˆr†À iÐC–¹ …*Ž„<8"²Úa\Uw¢‡Të¤k òðØ8BL¡UII47ɛ†|rg™Œ¬0íȶ+ãÃÙºÒ}=ä¶1àâj#øÓr"qös7Q—]±xŠ RZÞ>Ö½Ù¦öᤉã+3_K¯k +öQqön« n'kP%ûo|À¯²Óæ`Šo-ª‡U§`jÑ.&qKµÂÊ·šÛ§ÁG@œ¢™M;¼—b¾ˆ€vïŽOìl†—õ‡OjLòÔ2«ZÙ=éZ`'òÓ4ÍDZZ…±ÑÕ#|‘kXÑ Æµz43‚ÄI:Ã"˜4ŒD +RòRUøL›ôdD²¥³À)ÈQÏY´iø¤n0ÐÀØF!‹®üåOóܞb„äÝN—Ü*¾ë×#r–|{*H!mÖ]Kd‡‘fmt¢œ&x¿›ƒ¹GQ±nu´Ï¤IâЧ; +Þpæ<¥A€Yäh\J3ÄënDtEs|ä7W;âEÍ^K;0#Ä‘`ó…1 ‰ê‡ÞÀå:M Úø%Çq{7Œà“k݅=äpz&¥KÉÉjÐÍwÝcG96ÿ‰ñ¨ÚIä03緂ä'®aTw#6>³jðÊÉÑwÍ£4çinòmiÉÀé©Á¨!cœxÞ4[i*:á¥Yñ ßH: ’up蘆b˜ºyäôÍó +oÒÓoшºkv@Z  l#VSÐp¸pUûá¼âÂ@ýM>ñUoǓÚf/{Fu“ê îå.&êJxÔ²ªE¨[êlTRìËðsõohêg¸°ì ±À…¶ô}†`F ¶»<҆ A•¢KÇ^¸͌pþº£åÖxÏÓçTaèÏÊж‘·tyÊÆ¹'%¡ÅL‚ÇžEþ¯[º«ôäÏð¼?M­Ð`Px¿ãà…!átÌÛfœ¨ùÁDåö.8 C;>À +¶;qu\S§›Ò&ci˜ã¶s­Q÷}¡”8•¶ãÏQHÖô(Ý ³M® »í²Ý§£mqdE:àFоn¬$Th¾sLiñdcT¸îí…Uéµ°,Œ,¯L‹:Ø q9 ºTM?ê¨vÌ ø­{{ó]Ýîü}VW„Y>am£ÐPRµPfh#h4Í­ýµýî aà^W=uõ“BäñÞ Á›b‹0Áç] Ðò‹’FßÉj”Ç‹ñ€ƒf‹£h” á½{i[sÑxÒ.|Tt-¨î¬¤ï åõŠ›ö.¼7ßâö ý@¼Ÿz¶-¨/jq8ڑ/#4ʯiJ±¯8ãÈ3ðý@‘_¨1À~øÁ].pÑ æ ä!÷\1ö0:ñéÏÐg»Š²×µ2«Î—³O³ 6ƒ˱8OsóÌ¿„°YP§ $ÏìŠÉÙñ«Ëm4öd‘Ÿ%I-W„~ŽWðmZÆe™°,ÏIɕ'×íìøuâÚ3ûO—;qæf7a‚ìd™Ù—V +Z¿/¯ëïÈ veó;-„•)NB֍éœ< M8çïbº9Ö„שÂYjÁ)áÁ{QóÅ×´¦´Ñ'‰=ôƒk–"%!Éw¢Ö÷rÓüÇô^b‰i_q(…ÿ²]5;qÄ0ø¾O‘#•Ø(±ãÄ9¢–ª-ĽHH•€ }…>M¡¯ÖC?'“ÌîÂiv¼“ÏŸû´ýÄ5vñïeá†Mç¯î +ú—?´?úexI\¤+økýæçëŸ|ê.çÔ¶R¬X/¸£p‚±Ož‚ÑùCFEýn?æ›çq¾ƒW‹8"–+ÌÀŽË©¸-ã@~›ï(¨)́UZ¨9ûTQyä2ÌK5AžCÅL¡!è’tI_Tâ.²+/V<bëÐÖæ©ïèdÅPU:O˜±&P0@"°³$k{!äE%©LࡈKš½#ÌÄdNš79¾Ckû‚C"´²FMðH:“ëHMl!¢¸’;[zJ»ÍŵºBHPÿ‰`Jßrª¹'»ïç·7w—îÓù»ÿºÛ_í>~9û|대ÈÕ0¬SFz{"~eTOöÑkŒ_«ÂdãkÉbO¶µêõá¥Ï×'×+>Mü#Ãíîq !"êæÀò#ò†¼xµ]ÛΉûvvuwvãÈ2´èd@֑:W2RÃÅ=or kÏSò´É(9…á‹g  +‰(Á9‹*žÒ$ rN{‡RPµ¾#ɼeíi*+YO­"…â1 +×dDJž*(`ýÏähzH;1וA Ô°–[œf×X4¢Åb…e$J]5§ÝËϛÇÉë#—ÆÙ->M±›Ý˜ÞQ¨Q‡Ùˆ§Ù\‘>F·šCÅì|\F‹:k¤Íý®M‘Ÿ§6$B3> +stream +H‰lWIŽ#I¼ç+t`Núþ¡ }Í¡ûÿÀ˜‘¾)¥ªƒÒ,|áîäãÁÿ䔯â#Æru͏×fJ¸Dõqƒ‘+VýÆd¹J¨“Á_'°¦€Ñ«÷GŽýÒ,€ñJU€ëU$-üüÉ1^ámE¼¤öGLíJZœ/ÉcƒrµÐlAfº”ìŠúÂÜѯ”Ò^KçŽzI‚8)GèÛUÕvéu1÷Á¸ÅîÞ?þR§Å%œûr©–Ç?­¯¹\9ÿ¯\ì#wç|EX¶Øäµ™}mîW_ˆ»¸^±À~„=÷«ÀZ%^ݬ›¸®“À Ea—²]‚ Ý̧ЄGÖÒÍë¡(Ï c+œ¹ÆU¹?”ƒ½pS¿B¥påj0Æë`Ú¥1? '¨“!ôËU.‰²0î®ñҖŽÀrÐÂ~ÇóãÖûQ5¤ÒÅD1Cel'¦@Ë(KɅzš ݖŸ0_šªm˜ 2 ‚b¿)2ÃtÞ°™ªd=V$(Ð=›#$©*ÂVj³ÉÁ3:˜dZäÖíÊ¢ ƒ0éÛ´f(‹¸7ÏFhQõª±Ú aµ…tìÖe†ÏËØ~âÆnéçoÓ{µt)¯Ýj2+ÎIT9 8¼¾åJ®ífæ&¯5¬ 5žŠ5qT¤ÉÌ«Š•»ûæƒÙå²Âïµ~cyÞõ´¤€D)7 è˜u¾˜{3,·%ž ìny܂ÇJ-ðCGBêÆÏŸÂÕ$-¦ñ{[`…çے;&ƒ³g÷÷´äÞ.ˆ}b$úÖ²1¼M9ZY qׄ·o,Ìß`ÄRžbHI_™å²µë“A¸¥|š •©ª¼1{×tÆ'3oÿóf!ö~ñ@fþéåñïó&[ÃßQêQ~üWQÙ+X…ÀCéO +Ÿu]K^vdiå ±“µ'þâälG&z=ºrá;±<-xST™h˜O߯¸)°.jTTŸ„g4Z}C4âdY9 7à(éÇ<0Ã/L2bϺ۬–P(µêÌmL²KHÝ÷ô¢^2)u¾¬›0Ä+ƒi3?ÃLÉ _˜øB˜„“H— òððÌl#F‰Ú1 [ñüÁ Q—T1Zô6˳A“…Ýjb93W@‘æ%¼ŸiÑtÈà¾ÉÀÚð 7/ҙ®+ÞVÔ%Ú¥e>‡˜mô¶b:›ÖÃÙ¿Ãá‰ðIxþOÑ"|,Ú¸j·Â¬!u8»ÿ&ƒ‚à ’¿ñQa~ÜPZ^ëák5抉) äڝ€žh=˜€¸„~ˆXÙÿï-”%d7ÅÔM<˜{3ˆŸÚ«1cצY:Ó´¼1, Ù ­àd Ãá‡æi»=Šö½¸ærU¼,0$DÊ´‚AãŠxܐUŽŒ ²3ç6{Ø^“¸ 3ÄO:q˜Ï^[”3´¨jޤð˙<š*¨œÐΕ³:6þå,h#5¦cš›ìZ‡Owa0ìC{9<™éñÌIv¢Mޘ}—ä<˜)Ï/†=T³TñsÈst.ïÌÒëË˜ÏB2 ä`q-0ðHýÆÐÐÓa‘C|¡£ £©bô4<È©. O´Šwô\_5Y¦Öm*5Ü-‚ƒ·ÄúîÉ>žöÜì>%Ä­ó>«²0@öäcÃn®‡q‡Þ¸º¥¯ÌòçÜôA@á’ÞìYP˳7½yÅ…Ž+J<=ÕY–Î]ƒØŸ{>™Ñ"L³*š:\Çñj–¬Ž½l†]z]½Ÿ¯¨öKܘù/‚¬…;&cՇ—æÕ˜²áÎ4ôÇ2©¶Õ•=坹?Ìa3C@¤1·ufòb©xñr(~2B Dô݁E+/ŒF;à<›æ +6>ÚuDד.6Næð«jûÆ Öj{cps­2îÝå“rôXŽ›ž¹x®`˹„ÚD]CêG¼o흹fD¾¿fpG¡È¦¿ ÿ™ÝV°ÉdK×uÇ'3üb¾R÷#4_›A¿Ó²M“‹Á¼ +[ÇäЖgL +"zÄ ï Ä+2¬ÚV3–55ìö;øH¤cü^ýeäIC‡:w³MÙôo NѪ'«°+ã)¹®†‚µš÷ƒMÉc¯û;ÂaNrˆˆÃ˜C˜Y܀æÉ.À¤Å°I(k¿@mÇù‚§0êþŽþÀ²Þw3J´ '!e1÷Á,7‹^ãÄM$ñ¾.`“á¶K˰Rµai1G8Žk>™'‹q¬)Û¹F Wõ¦1Û=eENCdÒzÄEÿ74/^Z=#)¸ÿVœL¼ãd0[ò_1Ï<ÿû<‹’ÌtF{G¹,¶I°kEȄµ"1¶ì͍öŽÛ(ù©‘] +c’’°Š„º½܋Ž–ê0a÷RˆÕ0Ï^˜>¨y˜t25¸ {ŽæÅàQ­ë~ýŠ¢îx>rıuÃÍah'AìêeG [­cè³XsãÑR}ÐÀÏóŽ ¡i&Äb6Ϙ6Cœ‚1j¡7Ž ]t—&[´ÙËfÇ¢†%ž=U¬sºL³OP¼Àõ¸Y¡su! {s¡\HµüÝj8ædX¬Ç +…:õ8w©¾ÝbÙâ!°È!åÆSÉLMç ÓóŽm«)Å0ærÛúôƓYÁCƒ€ŠèJe3·+ËÁmg’¾°Ð,æñ–úOÉ +endstream +endobj + +129 0 obj +<> +stream +H‰dW9²9ôuŠoQA\€ó(¢c éþî$QÓr¾ ÖdÖϏý“nû‡¹?»Éϟ_ÿüçטû™¤?Lò0ÿü…Až¹: ûÙ:°Ê,Øã–!úc¸;¦!?vÄbuܖþüöCi +,ëe¬À_^ÛtÌû…¶a>kô²`â¨GvÀŸãÆAÝ7Œ‡h¸EØ´¾"Š= Ö§MÎ Ç"GÒ¶d4;WYrZ^1¶ÌÀ2—»¤æ DH–Ä„sǺ÷wäuŽH6“cm'RñÛÓJ¹‚‡gur:0oTÄ\’ë•§¸ÙÄÆ‘îÆZßq-L=VPwÜgR•,dwÐ!zøý¯b.¡ì~0[sñ³Ö8”f¡£­A°»e¡Ç?K™| U«¸Çô‚·ÙY²­½VÝcãN>›Jç”æe¹–õPæ'PLxs×pÜT,Ÿ7nïÏØÂBÒþºem~-†Åòl= ¨?áé܄<ç©ıF:WL(û>;Ò²a¹'À¿µËú!ßaN ·„Sá$0òzÃ;hÏϊ޸œÐ@/ýsG{¤kñÍ7‹“/Žäl>u9n;^q:©ÝƼÖ+ÊÅÙ*þì[® +ˆ°‡~~ïšx©ª¦®ÐàvXÖ^~¥°a–𐽩 ùŠ÷g·C[B:Ì͉[úŒG­Ç­>þHEÈû†½-Ó¥¬@uFϼd“q<´­LqG“¨ïXÑ÷Ó^gGZØÇÕy;#:‚VàI;wtÈ–ÇaõÛÛÃXù´› +ˆls>Dq·CÙmL3u#šN}úèYѓ]V…øï)NdÇò#¹¢§9ùÄ¢Í2ÿ*Öðh¯«±,À*Ál–ÖŸ_’ñÚ£[àEœýKyސhX›P÷HcGa¤ßþ@é3µrTŒOå(ŒHá(qÞ¸…Iÿp”8ç_޲¢sá(4E +ŠÃQ& +E¡ÏF/…>\R)ÊÄü,<¥PÔŇ¢ŠÅ) +Xލ ,)’E™Â¥BQðq†²¾¥C!1É@ÁPšÏÄa(üåña(Ȗ-…¡Ð™C C]|êX’¡p@ãÂPjß… à~Â9*…vtT!(û"YŸGa(üÝýÃPP—½¤e×BP¦—÷‡ ð¥AýCPÐ_fœhZèǪ}é(k3¹¬°ÚìBP8)ÉãB_…Ÿ$¾_~ºøðÓµ?Ù/Tø ¸Ñ‡Ÿ¬­{á§ü’ô©šzÚ),’žìې =Yªç‡Ÿj¡ÂO “· +A]|êZ‚¢–‹­KQ¸“åCQ;äüKQV7º%*…t´Y_R(J<…¡ká'„šŒüTÉÈéIð6嫍~’áèý¤ÎO°,ŽîöÑ%öä ôŽÐOh[Þ²°Ì•MnSÜGy¶ÅÝ]ÇB  +Gàx¼¬f€Ð¢3®ääËä|SÙ0…á= ƼFދE8ŽœùQ7$nì-s“W ©d?—;íý™…@L­¯R(Æõwô­RpÔ²F=¯Ÿq°Ì\aŸ}–xN¦áù´Q(˜FÐ%·øG°€*óì867LÁŸ¦ìüD ZÐ}Êàâ7-¢Á &”=)gjÅ¡ôÈ{—8‚<Î"ž6ÿá¶'°XÐÇ6‚©Òâ<öî®÷LºQÅ}œ{ŽÅeŠŸy¾½"ø–"ejд„æ`úÜ]¹æÌ#>ß c=ús?œbî >; _oB*ZÓÌ ~d9´óÌÅC +K¦Òžƒ¤ùêy9[ªz³ÐS*ôIäÌ)¾–oßý±§BÁWÙF!ecv’é}ƒœ–÷*ÛÜÍ«dOnRÉÂâí(Y`*B°e®SÈ*S®´5låU²†iڄaOºJÖpŸWÉœJ¶ZlT gɼEìŠÓ©dÍ2õ*Ys›Š”µ(›V) ˞·u-·-×¥¬Ú§X¯RVíÕWʏ,kÄqqJÙ×RVG´T²V¼µ‹”µbå£ìZÖJ1¯võ44ª6,,üYÑ´_-«à’ªe՞Ò}Å,°©ÁWÌj jºb+V*ø³vKêbÖ¼0j9RÕ ~qöL/ PWËwšUËZè}_1kx+f N1[,.f “^1kwL­bÖ¼½bÖy¿jp +W5 Ñ+f‘_ZÖRÍTµ¬%î0¬ÏRŸòÁµlÁ©e‹Åµ¬`Dr´¬]©³jYsIKD +YC¢ƒÚî塱9XT?ËŬчîªfm ¦\=kC°çÕ³_J2A;›Ñd¾“Œ¦ì³0æµo1Õ^À$uïTe'ÍôḿÈܰoÙòóMd”2–c:ÇeA¶æ·,{Ã`"zz°®ü0ñÚþüO€¶ÝKr +endstream +endobj + +130 0 obj +<> +stream +H‰dW;’+ôçm¯Q> 8ÏDl¬1ïþîJJQEõ§'U€©äó‰?]ãêdáuÁ‰U5±¬¤ÆŸßŸÇ2/Å#¡Š%´Å‰™zN°«qXìZ+<Ø%špN@Ó~ã˜0/irŒOX²9^W‚K9c]«c†-ûü¥e,XtΜ#c&fÕ\sp«¼öÜø¸¦Ï L°õÜæ˜|c$byŠžã’ÚD‹]ökUØÊ”ú¥µ§)9€gF!Æôà´×ð2LßHÀ®¾!i—ª™çMÈÝimh4Xz·Ü2OK<+ Ø ²Ö?ß×eCj_ŸÞÚÅléÐ(RôߟÞ|éH½ˆ»àÏ?naOt¿-i[¹³–e262<¤Þäâu;h6žõ¦Íõ|׫ïék%®õUzŽïWëœL?i"UÓsybU«ÛÂ}bÄ Lö*HŒKÑfmÉ…öâ•AyÀ‹¾,Kpb]i[Lë )#]1ÇÝ6’&…ŸÇ"Ê![Ÿ+ñô¤ï˜a„P†×~îk`ë Å#,ˆñ”Ç\ßùZ£¦OJl†é]5'ðÕ .µÍ¡Œ˜ +!ê姈 \Ù3±AÔsž';!¬¬üÁ9ÁϹé+•¾y§û]yYqíQGæ;ój$¯î·Åç‘ÇñZlj@éË¢ÍpQ•ËBW—[äÈɂêfk±O^±3‘Á2;çˆ(ýM ½9õé<È#,rÐKoþ»]dÎFÒi`¯½œá÷}€'ó˜û5 Iìt@ß:¯*ñm #ÖZ¨=]éwe—5ìÊcß3É hNÜ»±*ˆŽFtkó¹µÁÎý¼Õ3Ëáþ¾îáA;¹ëM¿y ½!-ÍÖ°g +üV ¬P)z×ÃßÏÿ~pÒõDý“–ÞæmÉ}uàE8ÞeÕnt¯K³§…ŒÓ±Œ^ÔÝ?Ø(¨|ôš0:zäb 0F“å6^8"Ìm™mn-•Y³î Ü{Â=‚]Ï՟kíAŠ×>Øÿ ï”cŒÄjÀ”ÊãÜ¥ŸÚ—å +Ce´è⿈4®ª•œHå喩p/Ye%‹ãڇqܽ}êz%§AõÁv¸!ø#&8ÇM½-ç‚ 0)\rážPS~·ÊPïý…5ß/ 1F衑ÂCf£š1;|pt²Øt©¦<A6(’9vØÖõÖn‘Û¦ÅØÑo6l’»r‹Ô+$ÄK`m`»ŒãÀ й-ÎvX’«ï$Çé |Fÿ‹Ã²’E‡ÅùÀ(Í@3õð¡*Ü"ó5 Í0‚ÅœF¹nÙ˸ïòIl9‚ 2DF¤ÖGv°µQ3†á½×%>ÁkAë¯17I{¡¬ÞEsÝ%SŠ #öƒ.ƒLP5à¶3Á„\¶‰óãAՐߘ¡A‹Äc&ËØê͈Š z¶ÆÛ3ڄeTsU7³„}bÁxÅ" +.á=ž¢‡UO9”É.ýÕûN]ÐUtÔ¼|ž|Á]ŠÇȉK‘–i˜1J‘5¸XõŽÒ.{OkG•H¿ŸVã¸%ZÝö>ÏïC¤4'Τ&“ªa*•¦±ç¸Ùt(Ÿt_œ”oF&çÿ—Jg'E{Ñ{÷­ËÔÊ¢èf/Ëä—rç†þuX¤z"øì/½ó·áܘ4gïáûƒ(wÓV2i> +endobj + +132 0 obj +<> +endobj + +133 0 obj +<> +stream +H‰dW;’; +ôçm¯Q! !à<ñbÞû» $ªRÍk§#)}2õùďç¼DLJ¨]4ðÄS-ñꜘ­~ÂÂMÓ²Lr1VNعoãé2¦ç;ù_Mמ˜¦ûܚ@ [Nß(ðè3ñ {³ »LVMhÒ"ËÚJ<”kzùN1aøa|‡±.öùº¥_3Ž}XÆÕ$Üߟ~˜ýsܶÿ¹¥_–áÙ£ÂBÎÈ¥XX†¥¡õ Ø›¯Á‰›¬Ä¤áï÷ù2óó;^£'^qŽ8OBˆÍ¨ié!²‹Â+Ǎ# êÞٍc†^£öÀ¹dwæÄdØ#.7fÈÕ:ֈ{â¹ Çbœ˜ìmÏț̓"¦“…£SO·ãäáåܧjãùꗹ(áh8ã\†µÚ¿jÙ$G(a†{—ئ&–1*Ò­<Ô8ƒãžg’ÌáĊ«›¶÷<èØcè¼ógx2 ‘&y héÑ2›=ð xôŠ©bÏüÜ h­•Q¶ ™/Ó2[âòԚ7xez¹'äm‡õ1¬ ÚP€maˆj›¢åÑT¬å6˜ƒp¤È%Çs­:óìrWE`3qÖcM7DT™õü|º "V—ì‹Uˆ¼$GLƆd¨˜…õúì5á¬ÜïÏsWڝj̨f÷ÓߖÀU½I€‡T'ªæ&—åý£e†šÓjöÂ]¨fÀ¢Y³¹Bf¥ïEØ£¯ºÏ±H{zC1¯H(Í@5ʱEjϗŒhäÉBõˆb×n»Ë´e©šJöÜB*7#á1a +ŽÚF0`Ä‼jxáLš˜]ÐC×¥¶CdI­¼ÏH/tÔ÷2=îâ}[¿?ÿüÇ7ñâÏÄ"¿ð‘ ye{ږÀÒ)1[Be|ž~zE™¤…Z´gOÿ̧:±“ +eï<,I7÷ +£’qï1üÈ£öËP8'wú<ÇÆŒþá\ ç +•rnáä`‡|%©no¸ÏP†}Ț½cP«?AÚÛßQì‚|^ÃoTW,y0#®&;ÕÁ•¾Nuèb]ž—­ù²¼×ùâÞ½ˆD^‹{V2½×J¨à[Xœ2×Ê$ôÜTºÛÎòíýhK«%CßuÔæ¹zöàÄèÝné† kJŽ ‰Îè'K<v€Ôq – Ð+:ÎÕ£Ý;T‰­½ƒIõÞ<f܌c¦j•-¯Î-:ÊKŽ%ãʏCùó<Mž½ëù>3z¹ ­Ä³â<6pÅØWùœtB@jÔñI¹&lˈÊLˆmã-çt֌9^zͽɗ…CÈú,òY°¬(¡ g§õNCôàØwî³oËð +Ï5 =gðxÎÐQëѪ¡=Ò¯†B#q+r´ó¸FLjá7…ËO˜¬8§ïDVyƒc7ªVªšIb4Šc½Ï]IDÀÔ;’,UO(­ãë`úcI֏[V]ÓÌêX³ÜLUa»šÊ2Dr+ðœå·@\ÉÞª­ÄS[¤rŽÐ… óú5»áoø–2{ÀjV |žúj’š°Ÿ3YÏ»b?Â¥h!;CpnÙ|.IkC†˜šŒ+/#šÕŒ?±õæõ'¶ÚY¾5Vˆ¤…žlÑá$øwlÃ7G°¾k t×ay­’ ‹÷{Úp¬ì,±Nq–µN½-är3W¨šj‰SÎGZ¯­tÑr~©TŽ3Â,cÃßô{%Ô÷!;Ã{âVEÅ ¿JÕr!´¸@ û&¬"ýŠñ¼»GH ‰®9ò&½?MèÆ¤ú0ôwÒy¦î‡Ö¶¼sy/¤æÔÔn¢IÛòM âõd¦à_CIòˆ »ån‹dôOâÊ,›„ö¬•Éõ¶Ðëa1é/ËûTyRµJm?¨:¯¤ö–Tß´ŒýҌDñ)¦x‰FF÷•ÝN5”¡ÕÛÕïE¡J{ +¦a7Ž~ÖàÛ\3R'ªpBθ¥+,yó +ÇÈÜÛÒë÷ç´(aÉQ¹6•K韆-܇Q}#ĉCÏCQƒ©äè*¹öˆ—ˆJ/E\êX«‘1e6G\;ŒÇ•ÉÝ¢Í%?`¸²‰˜q½bä…'ïÛB~¤Ä¨–z, £ZLö-dJ—å KYsfF½ÝBthñb<Ýô¾HAr˜b„å¿'«›ß(†CDdl!ýßəÒÝM¢R׸2a݂>7°3U—¦‰&4m'g/®_A‹î‚–¨‘`¸Ø-“ðx §»¾F4®Æ†\…Š-vØJW"hñ‚ ©û¿ÿ:ÞêÏ ['êr1øâBeD7“&û€UõÞp?×”'+½žªGU֊ 8:¿§op²vl vӅ¤Ê¼´ÚÒvÀúÌ-uŽ#\ëQ>PcÊz "–hŁoUyψª„d‹œ­6ø‰ Od$³¸…ç«­z8)ó´œF;ôÌã×ËP-eÆô¨Ÿy +µÒÉ%@¬Ò+¨’üˆ¹5<$÷ç~Ç<õ‡E¿áSX¿ßI©?WƦy`)Ç$ ñ-Ab5åS‚ÄŽã-w1ß/‹t:%HXê͒$ðZ9pIÃ’$p‰Þd+ÿHk×<H¸EÂ°èW§Ñx>Éý]íæß j)<▻;„‰«5½ˆÃfãT ÚYâXã¶Hà6â§žWÚ ”þay']&¢­º€ÍËæ•izò²[z0ÐæeŸ¢l/;žQU/»¥m^6®f]¬ëx4>yÙ&^•÷ˆ¦ýæe Åg'/»¥­›–ÍI¯è#iùÆ7-–¤eŸß÷«G±á*•U´ì.õªž e‡G¥qÝð]jÞ m<ßC%õ‡” ÙIÊÔf7'GЋ䓔ó҃”Ý ÒR¶x-‡”\¤|X’”³<¬x<ŒÇ ï`å° yX9BB|³²Ã-ƒ‹•Ý‚vX¬A EY¼ëQêvÓðïÏçÿ ögPÛ +endstream +endobj + +134 0 obj +[/Separation/All 122 0 R 159 0 R] +endobj + +135 0 obj +<> +stream +H‰ÔWˎd5 Ý߯¸k¤¹$±ó’F³@ƒì-±@l(z@¨jÐÀ‚ßçØÎëVµÄlxu«ºÚNâç‰íì»üďDi§RA¦Ã!ËáÀšRTšh¿l?oÂJ‰ÁªGÁ–›rbÆ!r‡&„IIb#C`¥¹VQ-¾*'E¯Fä”Ρ(ÍeÒr"¾Æe•r“ dv©©ˆz€Î܌Р¡æf¢’>çáÃåÁ«ëöî0ëÁIN‘Úsےƒ>ŠƒsU‰+eøÃ8î¨Õ›±0Â8!ÒÂUbnHGE…QŽ˜Ò‰SWâéöø;Fâ´è–C!ø;Îê<Ý>ý⛰ÿôûöÙþõ»ßŸÞmÞíò‹/Fò3C†C”aäÓmC‘À´»ÃÕöw±”ý÷ËûÍíOùóÇöÝkç;ÇŸèðó&9ð(ͲæñùÁ¹Ø÷½ÃçyîéÄûþé«ís1óß,ž†F„žò¡©¤ÅАÁ’üû˜ÕP‘Nø¸fM³"º(ôož~iÊfxÂ}x"! åãÃóºù]MÓÔ,ÚFøÃAÆ·Æ BÁ-,*âÇb5žG¨^yBH€ŸWMÅík’5+>¾žQæË¢fY—X±|Ê\Ó½ø¸Ë"'µø=OOïL3y˞ÁKË^²ý—YݶfGsӉXöâÄV8LQò£Çc’… v3æ¾~.,‘^C0ÌÏØô<¡â R *EoR%¿çŒr†¨Hº}Çå† jªsÅLQ)|ç£t2¹¬n5Ó>lÉ£0óîsµrŽËà\'¡ˆy)ƒŒ‚ˆâ}Os>¢“ªÔdÞÓSâ·ûû-¢laGïq#w”¤ã·g]þ°Q6C(Ý)Y-ä+NłB_Yº…ðÀpÞ/·íÓ/oyûëö5~ÿEÇp¨;v/QQ;}ítF½¥`&a~£¡ÏÂ_oaY †“Jµ”î5z³y]¯‡“°¼J'ÖJîÕ9×Áù+·õ§K5×QöÐùññ`÷™Äh'@-y¤Vø%¹ÊY]Är@j«]†èÜ¢2²Œn^Öÿš— -ÜçîœÏtÌÓI@ÃGO–EY­Áy^}„„‚”ÆžHtŽáãy" £\¼ +.)ßB¥¥ÈEqè7+-¸]Qé±¶š¢Û<7ä6Ïè5Ä¿nó@n•V/ý$æþyCùµÄ:õԕ"„â¤ëïêÔC+¶£Ì|¼‡Û©Ss[ëqϳ…¸w÷ {Ôâ˜%Œ ß¾–€Äƒb(±‘’ƒTb”Ì0ص€ð2êa€¼>È1Ø +˜ƒr1šcN†ôŒó`\?JݽÞ3úAÚ.nn$­Êìð +ð2c`&‚6Î:pÈÙ⒣Ç0qÿ÷Ag_/Ý=i· +ñ$霗N>ê1÷‚›:P(Õ{Žòº‰­ á£'‰K3=£Èˆé)–f¡#ŠLއX^Õ&Ä3{Ø_I À/“wrã›f7•‹þnúçÂ}Þ÷b¸"÷(½ §“ð5%ÚYîm¯†±X(Æ “t³§M v*ø—ã]lô’Ì¥ôo@ ˼Yz?FËX´gÂÐbzå%Û4»‰mþ?›CÆØ‘՝„ÐxÌ\0La•-ܟÈ6ŒnEЌÈɸøú9r£Ñc ³Œãߍsfà ›—#@ׅ†¥ˆL.åð5¶ÿ­X‰4ˆ.w/4Þc •|9‚¶ÍÎÊ!æ–3 Ý‰¤ûÓi°aåü ÕP•‚z(o¬ ß"‰Fäæ;5¶ ¶ߺ%¡ÓñØrÁõSŽå¢ÌM9䋆*fLOV %’‘™2yÁ~HõË:r,PH††If8d»1ŠCªdÃ'¥«IK’˜œ$sj¤#DqPf£‹¤€…eYg³”öæËM•KÛþDeQy%ñ‚”>'Ì~/á0ÆUD¦/±ÈGΖ*Œ¢j s'Ívi®·àxÉi¡‘8„õ²¤ÙöI%Ö`¸äæ ÑxA„Ð<2N‚ óX3ˆ¾R’FL‹Š‘² ,é´!x£3)éÄ#(p¾¬9‡\ËZ(–fA_7T]i˜PÝih9qEZE‘šHJA8‘–øðqYÇe‹iHkŒŽ4™¬Ë‚´A¤-¦úP¤.qE,ôë:C Ò@s]‘FÊ Ò@ç8Ö©Ž´$/˜i`P^–ìqБ$ñ*+Òà󂴖€´NO¤õ5¤AbM Ò=68†´$UgAš˜D+Ò¤IˆãDš¾£NHë9éH:-Hë Òäa%À‘Ì'í ƒõš{Ф¯(Æ,Cä  4#â.-ë8–-¼&^U¢óygíþߌÑr/²<ÕfªšÂ‘(¸—D&ô•`¸à4i/ÉÂÞ9' •½Ý ?îØ[Ê\,IêàZ¬ÝÖvkCôÃç-æ!©×oÂÔäë=Uú:i ¤EÏ0F¨¸XbÊ`Œ<™¨IjT-Ðe`P-V—θNFoõýÄ=l¢¢œ82‹¸uÉ~=£y~.ƒ¤Èóô M¾ u~Ññ,<ÒEêʼnAR’ + tHý ++zÀðq^s˜›ÙÀD@ ÐÔ ìNÉõ6N¡Yp÷Ø´£u¸MÙðÐdH*zD¶ÜĪ—lpŠ>F„Žz™Û]¯¡ÁhTƒŠë^— ¾1k›tnûã`TåU$Œ ²ß¥ˆ©Q¯©æC§÷É!-˜B—À*U=-3#ÏuÆm¤±”juÛʪ$¤¶„6n\6ÔiCˆÔÝAzØLA¨ep® 'hò„–Á¹wšN3ÐeÛÿ`Ñ8x1 +endstream +endobj + +136 0 obj +<> +endobj + +137 0 obj +<> +stream +H‰ìWË®]· Ÿ¯Ø?YoQc Š;è èÈ-ç6H<Èïg‘)ísý­=°×’6Eñ±¨s]ü§ÎÒ¸•+ ̃€s¨ó€)ŒÔ¯/“‰©^O01̬D»*Qh“×™­f"¶àL¥ðŽríÀ-Č#Ù°!>0‡i/ç@kwâ`¡.Æj4kÎÎ<…iE¾È[ ¢;†ŒGãàߞ·¦É.ýüh1‡’ê²ñ\ìÙm2¶3Ƭ—ඝÚxÀž#ª!Où|™ÿ@p_ D•!gæ±ùJwÈ'ÄÐڱŠÿzCX×íBØùuŽ@¾Aà'Âu9bf±Œ±±TúÎ({ç^Fi e‰z)0øš¤`ö]•yJâ¸.)è!õ¡ ¶3P²u3Qtx‘rˆe¯'R‘32{aF¦nWe=ÄK»à^úauhÈ7Ӊā4²wãQò q/¦rêŠS\V[RÓÍH(më‰ýXÒF ‹Ëù$õ$ÄSˆ,Ÿ—Ð+ÛÇÂÔ +DWI°z5¨—)¹ïõ}|N£8¡É«|`ù™¸@´mg¨ë‚ŠGh9¯)ÓaC6ԕt-ÙÉôYÒsíސVÑ#®ìñœÒap€Î„€)q¥¬ª2 ©[ג’>ÏIÊAÒޥӝIXNS?#=gé¼qj†øô~lP1âLÔª}=³‡(Í4êó]ˆ´r‰.#υh©JSJƖ….ڂmµo ëÔ5ðÆh¤ÄíÆXö½oØãJ{¹JB¥}åB —ÎõAƒ}ÍdUïçаgv¿Ëá¨ç¾ìWÇύQ(+~Y³Üw¸}ÑìÔº—W²Ô؆Y öMØÞ7aJ^L_02–ڍù'°ñ.hP–7NžÆÃI[àZØ ……AºžøÀeiAR2üÜxi3nUÊ«:‚¹©c©7u,ýE˼©c7u4èê¸SÇÒîêèØÕñ`D¿àÀMKQÇ2ŽÅqWGà»:‚¸©#𡎆Láü]AÜÔ±ÐM9X7uÄeNu´è›::vuôü,u„G7utìê茪#àMKQÇÒïêÈñ=Ô¿¨£%Äԑñ©Ž¾~¨#âD¤µ>²HUÅ#9óÜ ?5´¡k‹ o—©–pµZC™Ù §%ë^G°û‚ÜÀÃT·uóÑõ@Ô<ùՁjºAÕù{$1ë¬oû©t@RÉ1æ¸2?¿EØkÎ?ù@t±z‰{qþ·}ÅKšnÓʙ5­=ŒZmÝÓª±ӊqÚÓÊà1­TªžÓ +„(M«†.ؽa'ZëTàÑZ•ƒ~¨§á=­œYÓ +RßBµì»Ž±Ç[ØZËh¨cZ5ÖÐÛ´3ʞV¸Ml{Z1œç´‚ý–iexM+À²Ç‘9gÓ +Ù(y/[²Ö´r¸§Uƒ›õœVFØtj*õØ§•:­Y`<Þ™m·iåŒM«†wkÞÓª‘”¬O+à2ÏińãçÆ»l;?Äԉ9¥†œØLñ»C¿8T»ª}óŠù‡è”Ü ´Pëßbì›Qu4¿â}ªÛøÈ¨÷r#D·4M]›ÒˆÎ`”p!rCμõ>ÅI§‚mUۘºVϺ …m(3¥‡·I8†|uJ +ŠGTÕÑÄ¢÷~0E^Œë‘nÍñ|n²›Åëo ÿå(¿æX'‹BޏFZõPámÃýhjÅñ¬tˆ¾Õ†çòäÌó`²LQƵí×ñ[d?%¾Hðó¹aß×y@ü•Év2ü6•#½e¸Y‡ŠìDý9ÆÅ¦ŽRgð¨T‰<–ñП‡DqoҖ(±«»%àÈL×ùāUïZÌÎ<…aY¶ªÅ=çù„e¼Þz£Ñå)6ÿL^¦¦4t“žç&ü”é¡­4Ugš3—Á璽ÊdÖÐ|F3jÚÖ:`Y•"Ÿ;Öôa̅‰/Kþˆ5ñ'ÑGjgB•z)ÃZ¿=ó@ÈCÅZB¸p™¤þç5µˆI yâ¾CÔ§‘b_¼‡–‚EHђIZÎKŠùòøÎ6tê13UŒ±1θÔ#²ëˆ#ôÜbÇ;€ñêµ0]Kì c–s=êÌÇû-¸ÆzD¸§UÌûÆ[[ý؏ ËR7fEÑ(¿¦AÎMGJ-žRü‘RÀÑÏk.Ì׌´>PÝÎ*60vóN}=ïæÆùù\§0×óƒGüÔ÷¾ý®Ò0VªÎ<F‹˜ã†Ÿ9,ªü® ‘ûûõ_Ô¿Ñ.M R._rÌ+%‰­! + #¢Ñêb¶!þÉk™m}úþ-]ÿùúøí‘pT-9ÍK&aƒp}—øY[@ .=·Îi'LjgôûãÓïxqüå×Çßð÷Óç¯ýúüv¡"—O¦õŸ„ç]oŸÿúˆ×W¼~¹øÁqýqÕë§ëÿŒ×¿ØÂîԈ†AG¦)zõß{|Ã7>*éYbT”ÿ+EXcC• -”ñsi‡¡~>zÉòkeïèuܘ§É$/'³±à6áëfÁw¬ÿ¨ó|#ø÷JË{‡ßwa= Gdü>Eéo θ c܆fä YF:P,(L›Yéx¶BG¿[ð÷KU$͒ôÿ1¼ÅðVlÔ5n=“ÐoFð͚†b+?Eú±gYñS—‘…·íÖ2á;,G¥¬CŒà_)íØ±Žò ë¨#‰n·aŒ1­¼½ÖÎÿT|®?Å(\ +endstream +endobj + +138 0 obj +<> +stream +H‰ì—Én7†ï| +¥ÃPÜ-A €=cä`F0J"’ƒhøõS\ŠÝ¬iŽ|¤Ë¨‹ÅﯭÙݜç?o´PÚsåE´š?24ì”P.ò¶gï™1R¤èx4QH—Ý‹:q££ÖÃ¥)F»¶\¯Ìh#´6+ò†f±RDexX"a‰zB0Tãž•Î2Sƒå-J}zzºûDÊé/LroJœKäzüVÀ®•‡&}V#´$dä`©‘ê;ZÝ#B$'¦øþö&…vü…ggaÏnnOÊóã‰+~:~}ÁÃñ'“e8¨úãYÎëÒ©³Ò¹^:EÆLiaìzʒpQõ! eAõj0\wàe¸”JÚ>[jD«…]•q݌» ¡/cµÊD­ KeTDfðÝ!¤ðÑ÷ñICÈ©G\«zܪi9šÁˆ”RŸÈo(‡ëîUº¯Çqwé³ÓäµÍeú_N£éÉpžÑYY)KêeÁC¡5ƒJ\»A kzw,´:¯7 ¼<²:x=îÑG†×A$ÛPY3‚ ‚Q×Ç͊²ï™§‰ÄÁðÀº„Bf€1kŽ2Ð`„³Ø˜œÞP !×±@CZïM$^¼†—ÄLƝq$G$£r[wãnGàôHxmòªÉûÓf<Ò,Z”Æ]ŠÒRX^üö+Üb©‡#K˜ûBY›rÐRÆÀà ™Ž¦x)‘î C˜aåà‡ýýÒ­JZ-uúþHb¯†Ðaë§ÊNÁfùÓ ú½^xÌÚžåUy¡m¶~ÒÀ®]ݹÇT>½žÀ‰j†£i .ø Žèdš6q¡à’œàˆNÆ¡iç ž˜à6xE( <4mò\å©8áQ¥ ì¶M¢­D£gD¢UˆhÛ$šJ´~F$Z…ˆ¶M¢®D/gD¢UˆhÛ$ªJ ³¾P­Bô:Syqڗ°Ñ—0ïËÍíÉóã‰+7Œv6ñª`CJë­ûƛa¥Ü8hÛ¾s¢×¸0!ÚÚ5›ÔŒO” m›éWÉç bç;éf|¢\øhی_æøà¡1!¯S&4™|ÜoÊвL·MÒpކœ†V*ÔÚ3ãåÂGÛ3mPêUùvÖfª\øæR›+ʕÚ?JºXOMçgݦ;ïv™ÓÖ/Mªw¬ ³nSåÂ÷—º¬S«‘ +.¿—‘8íuØèu¸Ðë6AГÒé, +G]}^ÉiÇãFÇ㥎ûF÷Êù”µqun½šõê—™œ÷ýß|ƒ8ø6‰°œ 7Ü;ùð]»¯_ùWööÀnØËÌcÈ=7ù͗¯Ÿ|ãàóƒžÚ F~dÀ3^òÑ}ºúðñí‡þüîîËÿ<}ùýá³T;éŗ¯ww×»ƒé®Ìõo‡ŸØ»{Ïn~Øþ×é{UáTíÌÕÙAJRª,\þŸoìRœss}ø;ko= BÛ"ĸ¼x(ó³Õ9žOW2±ÐR:¬€òoœy#ýõNɯ>Þ?-5¹¨´‚A2ÈáPózÏÀÎÿ`¶Jø +endstream +endobj + +139 0 obj +<> +stream +H‰œ–yTSwÇoɞ•°Ãc [€°5la‘QIBHØADED„ª•2ÖmtFOE.®c­Ö}êÒõ0êè8´׎8GNg¦Óïï÷9÷wïïÝß½÷ó '¥ªµÕ0 Ö ÏJŒÅb¤  + 2y­.-;!à’ÆK°ZÜ ü‹ž^i½"LÊÀ0ðÿ‰-×é @8(”µrœ;q®ª7èLöœy¥•&†Qëñq¶4±jž½ç|æ9ÚÄ +V³)gB£0ñiœWו8#©8wÕ©•õ8_Å٥ʨQãüÜ«QÊj@é&»A)/ÇÙgº>'K‚óÈtÕ;\ú” Ó¥$ÕºF½ZUnÀÜå˜(4TŒ%)뫔ƒ0C&¯”阤Z£“i˜¿óœ8¦Úbx‘ƒE¡ÁÁBÑ;…ú¯›¿P¦ÞÎӓ̹žAü om?çW= +€x¯Íú·¶Ò-Œ¯Àòæ[›Ëû0ñ¾¾øÎ}ø¦y)7ta¾¾õõõ>j¥ÜÇTÐ7úŸ¿@ï¼ÏÇtܛò`qÊ2™±Ê€™ê&¯®ª6ê±ZL®Ä„?â_øóyxg)˔z¥ÈçL­UáíÖ*ÔuµSkÿSeØO4?׸¸c¯¯Ø°.òò· åÒR´ ߁Þô-•’2ð5ßáÞüÜÏ ú÷Sá>Ó£V­š‹“då`r£¾n~ÏôY &à+`œ;ÂA4ˆÉ 䀰ÈA9Ð=¨- t°lÃ`;»Á~pŒƒÁ ðGp| ®[`Lƒ‡`<¯ "A ˆ YA+äùCb(ЇR¡,¨*T2B-Ð +¨ꇆ¡Ðnè÷ÐQètº}MA ï —0Óal»Á¾°ށSàx ¬‚kà&¸^Á£ð>ø0|>_ƒ'á‡ð,ÂG!"F$H:Rˆ”!z¤éF‘Qd?r 9‹\A&‘GÈ ”ˆrQ ¢áhš‹ÊÑ´íE‡Ñ]èaô4zBgÐ×Á–àE#H ‹*B=¡‹0HØIøˆp†p0MxJ$ùD1„˜D, V›‰½Ä­ÄÄãÄKÄ»ÄY‰dEò"EÒI2’ÔEÚBÚGúŒt™4MzN¦‘Èþär!YKî ’÷?%_&ß#¿¢°(®”0J:EAi¤ôQÆ(Ç()ӔWT6U@ æP+¨íÔ!ê~êêmêæD ¥eÒÔ´å´!ÚïhŸÓ¦h/èº']B/¢éëèҏӿ¢?a0nŒhF!ÃÀXÇØÍ8Åøšñ܌kæc&5S˜µ™˜6»lö˜Iaº2c˜K™MÌAæ!æEæ#…僰d¬VÖë(ëk–Íe‹Øél »—½‡}Ž}ŸCâ¸qâ9 +N'çÎ)Î].ÂuæJ¸rî +î÷ wšGä xR^¯‡÷[ÞoƜchžgÞ`>bþ‰ù$á»ñ¥ü*~ÿ ÿ:ÿ¥…EŒ…ÒbÅ~‹ËÏ,m,£-•–Ý–,¯Y¾´Â¬â­*­6X[ݱF­=­3­ë­·YŸ±~dó ·‘ÛtÛ´¹i ÛzÚfÙ6Û~`{ÁvÖÎÞ.ÑNg·Åî”Ý#{¾}´}…ý€ý§ö¸‘j‡‡ÏþŠ™c1X6„Æfm“Ž;'_9 œr:œ8Ýq¦:‹ËœœO:ϸ8¸¤¹´¸ìu¹éJq»–»nv=ëúÌMà–ï¶ÊmÜí¾ÀR 4 ö +n»3Ü£ÜkÜGݯz=Ä•[=¾ô„=ƒ<Ë=GTB(É/ÙSòƒ,]6*›-•–¾W:#—È7Ë*¢ŠÊe¿ò^YDYÙ}U„j£êAyTù`ù#µD=¬þ¶"©b{ųÊôÊ+¬Ê¯: !kJ4Gµm¥ötµ}uCõ%—®K7YV³©fFŸ¢ßY Õ.©=bàá?SŒîƕƩºÈº‘ºçõyõ‡Ø Ú† žkï5%4ý¦m–7Ÿlqlio™Z³lG+ÔZÚz²Í¹­³mzyâò]íÔöÊö?uøuôw|¿"űN»ÎåwW&®ÜÛe֥ﺱ*|ÕöÕèjõê‰5k¶¬yÝ­èþ¢Ç¯g°ç‡^yïkEk‡Öþ¸®lÝD_pß¶õÄõÚõ×7DmØÕÏîoê¿»1mãál {àûMśΠnßLÝlÜ<9”úO¤[þ˜¸™$™™üšhšÕ›B›¯œœ‰œ÷dÒž@ž®ŸŸ‹Ÿú i Ø¡G¡¶¢&¢–££v£æ¤V¤Ç¥8¥©¦¦‹¦ý§n§à¨R¨Ä©7©©ªª««u«é¬\¬Ð­D­¸®-®¡¯¯‹°°u°ê±`±Ö²K²Â³8³®´%´œµµŠ¶¶y¶ð·h·à¸Y¸Ñ¹J¹Âº;ºµ».»§¼!¼›½½¾ +¾„¾ÿ¿z¿õÀpÀìÁgÁãÂ_ÂÛÃXÃÔÄQÄÎÅKÅÈÆFÆÃÇAÇ¿È=ȼÉ:ɹÊ8Ê·Ë6˶Ì5̵Í5͵Î6ζÏ7ϸÐ9кÑ<ѾÒ?ÒÁÓDÓÆÔIÔËÕNÕÑÖUÖØ×\×àØdØèÙlÙñÚvÚûۀÜ܊ÝݖÞÞ¢ß)߯à6à½áDáÌâSâÛãcãëäsäü儿 æ–çç©è2è¼éFéÐê[êåëpëûì†ííœî(î´ï@ïÌðXðåñrñÿòŒóó§ô4ôÂõPõÞömöû÷Šøø¨ù8ùÇúWúçûwüü˜ý)ýºþKþÜÿmÿÿ ÷„óû +endstream +endobj + +140 0 obj +<> +stream +xœûÿÿÿ÷ïß¹®í +endstream +endobj + +141 0 obj +<> +stream +H‰b`Ã0å +endstream +endobj + +142 0 obj +<> +stream +ÿØÿîAdobed€ÿۄ    +  $$''$$53335;;;;;;;;;;  %% ## ((%%((22022;;;;;;;;;;ÿÀ"ó"ÿÄ? +  +  3!1AQa"q2‘¡±B#$RÁb34r‚ÑC%’Sðáñcs5¢²ƒ&D“TdE£t6ÒUâeò³„ÃÓuãóF'”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö7GWgw‡—§·Ç×ç÷5!1AQaq"2‘¡±B#ÁRÑð3$bár‚’CScs4ñ%¢²ƒ&5ÂÒD“T£dEU6teâò³„ÃÓuãóF”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö'7GWgw‡—§·ÇÿÚ ?èF?P~I½·=Õ`i"6ĝª'Øò¶†‡ÝfòZé‘ôàöÜ­3*¡]/7WéÚ-£·tÛ]<·Ä+GªÔç¶Í¶è€@|TfëÅ!¾Ò­ÿ–‹Ì‰C`4œüv^+ %÷9¼¸LN“H Ù;}Û¿v ýܦkØq¬Çw¨ÑcšíÌò Â;rÚ2þѵämÛé}²œ£›ŽÜ'‰OÞ¤qá¯?Ù) +k)­¾«…N.Ýa.qÜg”õ3 +¶Ç䰝²ö¸‚v‡4N§³“€lù ݍ »÷_þk”Ãmý×ÿšïîS«!•µÍ6äXdnԁ`j "gíy¼ÌnÌþïÉ®¿ºÿó]ýʕ”uÇ9á·5Œ$ìý‰ƒ1Øq¢¾^Ò÷?íRòLH=›§dÃfݧ#,‚ ˦RSOÐëñ$™&¢`yhÇë[œï´7P6Iѧ˺½W¥]¾¡»%ä ­<„ÁŒÐœŒ²áùÛ¿×Á%"ů>¶‘‘o¨dA pÓYuQ՚H~D°µÃè8¸8iÝNªÖѺ·}£(šÛ·_ÎÖ}Þ*.©î>Ü̦‚LØk£~ô”֮޲ÊÃNK^øÕΩÜü!HÓÕËvý¡ e²ž#NÊè'sϯxDqe¾¢¶¡Srr//åĞĞÞI)¥èuSyÈ4 ¸;“íå/G­sö†éÀôÝp}¾*Ëê.&s2¶i ȓÊL©¬qwÚòÉpÉ·y)%5E]gsšëZZXv¸Vá‘F¢%3±ú±c@È÷ÛÝé ÆÝ#²´ÊÚȌ¬¡‡¼yB›Ml&oÉpw;Œ{I쒚gë{?Ÿný`úN%f¦e6¦¶ÝÏxúN p_D?¤¼È ;€:×âc”ª8õZ,d8´ÈéÁÅ%0w¨9GåQ‡»èî?OäNÿGÓ{&Çïvã¸Fƒú½üÔ麊íeÇÔhÚæ·‡º{x¦Ä’á=®Ó  x‡}‘l»÷_þk“l¿÷_÷;û•×u¦A¸w êê•U2-y<Ï$ä4 /ý×ÿšïîQ5ä~埿»û–§íº?Ñ¿îKöåèß÷$§'ÒÈýË?Ír‹«½¢\×´xà¿íÜôoûî딾§±µ<—=ÃMRS“ê[1¼ÇÑç´JJ'ƒÏ‡’H)зêæM»wZÁ·ÂOp{´ø'ÿ›—š}cvžòAæumÚâ" !ïïtC†~ªÞtûK¢u£µÿ¢Œÿ«·¾C¬D{ƒœ§}ÛV¶÷þñK{ÿx¥¢œv}[½§ùòáÇÄ~ïš3úï§Ñ/hl!Ν<ö­-ïýâ–÷þñKE8ÿójáríê;Ëù>Jlú½s ý1sHi±Ð;§è­]ïýâ–÷þñKE9·ô;ï{^môÈú7–Èÿ5E¿W­h \ñ=ýWxÏî­Mïýâ–÷þñKE4ièöÒAÜ@ ØçhLø!žƒnç8Zæî$Ãlp™àmóWNv0{ë9‡Ô&Æïl´x¸vLzŽ §×9Uú3·ÔÞÝ»¼7LJVp˱ûC \"÷û¢Ã®¿EÝ*ÇTú‰l$»ÞéÔ΄Ž«H»"»,׎+>³œÐ×z­.~IéëÓ]Ì˯e£sw= ÄN âÓÁ?Ý=?B6tkíÆÇ<‚7Xâ$OƒFš£ý‹#ùyÿÈ©[›LúÙ ¯oÒÞö¶4žþI‡QÄ-{†U[jƒc·¶Œé(Ø[F®wcöä}çÿ"—Ør?‘÷ŸüŠ©…׎a¦Ï´ÙŒ\ëZ4¬Nöϟ®¥†+u§.¯M°ÿQ»Aóÿ‘CY©I¸EÀ1ØÇ+í%í Ûþ¦Qqú®&NUؔ^,¿4ÚÐF›µãæ’˜~ËÈñgÞò*'¤äÎgÞò*ö÷þñK{ÿx¥¢š²2yŸyÿȦý“ûìûÏþEhoï·¿÷ŠZ)Îý‘ûìûÏþE1虼ϼÿäU“Ô+k‹Æ½®ÚZZéݦƒÇ”ªêÜö×^C ÝÝ®Q0RSWö&Háõýçû“ž‹•ûìûÏ÷-=™_¾ß¸ÿz[2¿}¿qþô©N_ì<¯ßgÞ¹7¢ØÌ[_}µ²–´›$CG&v­lŒŸ²0ٕhk4÷y?z‡T;ºMÄë-”# FÀºy³tݓöºöýŸ÷¬þot~ç2’&ÖoàýûâJOj>??ыÝ=‡ÉÅõzÛ¿7憛¨à³?ãXâÖ8‚Kb}¤;¸> +–GÕ¼LœA‰}–º–ØëZöç“«ÓÞtQS+{˺K8ýXÆ8ÿg}÷½¾§ª\÷îtí,Åº=’Äú­ƒ‰‘^EO´ÙS·7s¤NÏO]8õ„©NŠJ~‹¼B^‹¼BT¦ )ú.ñ z.ñ R˜$§è»Ä'ôâ¥Àæ½Âÿ[Ðs¡² |¡t=ʟ×z…ìkvf2«*sKLllXÃ̟‚º~¯^@n~šýÊ6'E³!·»$Ûµ¥ihhéðJʨ6ÒSô]âô]â¥0QÞßSK£«¥$Ý_—ܐ{¢áeÝÑ1-6’ûm½¹D‡ 4@ƒ·Ëî„6}]Ãeç Yo¨ââùpÚ}G¼m üâSµêŸÞ?Š^©ýãø¬!õkµšëËËcI$qÑėÇrTYõ_»K³3c@þ±|o )Þ/i2LüPº™ÿ$Ü“ÿ~ &ê?òE¿ÕõA:0ó—Ê|žz}ÿOó?ï‰%'Ôå¿Cþø’›ÿV0ê·°?H|כuoVþ³{=gÐÁmšî ¯ˆŸÅzIúCæ¼×«á5·fä¤r¬..òy†Ÿ%ŸÍšŒu«4†Wü€ûjÈ-ûKšÐßm é¯;îˆÚ}G9†×Šk²Dø“ñA¥¸âǧk˜Àã2â5ç˜Q5W[›¨y>Ò5 ¦žeT×`uîéÖÕù$ûmÄW/u¦²í7è'OÁ/7"÷óc`†¶I1ùÇCòWŸÓj ²Ö.hs^@-–ùøêŒñ¼>÷jæ}'ÀnáÎö}†Òûl¯w´´͟nº¸£êðAöºqþ ?nô™’À×@kÉЗO樳ëHxþÖóÀ‚@;dîxA³§Œ éøÛ€'S£]/04ߊ‹:uŒ!ÕôÜf:¿ wx;w¯þ§´ÿÅݥѕf%÷zvÔvà@‚¿Ò +në])»g*±¼ÃuçR?ï¥WÈÿ"Âûz~5„‘/s¥ÄÝߛü‘÷"ct¬gKòpiªÍÁÃg»X×X%/W‚¿SCIÝk¶íì|Šrimô8>§‰k‡"!ÑE8õ6š+©ŸEꈜÆjÍmÒÖ<.Vޯ՛s™]Umi ne³¡ j4â5]Qà¬á‡Œ@s›«¼Ï%Cœó`c&ý^é W‡ +cÃúDýGuްٝ$ƒ+¸‘TŸõ›ë#^æ·¦5í€àÛ#ÇRºo³`zއø]»öI³¾ô™Óì6 +áæ—l´5ÓµÐµÞ +¯ÇñÜå¿ÆŸð]X»Ïì/ÿ:>²÷é`çkô<«u}`êV>Àê«­ÄTç2Â^ÏÍ|i+d³¤œOµ¹ì8Žlú¥ÞÂ×{yóQ·¥tfš½Zšâ+¦Iԁ-k~A:2çÿJ<¸þé™Ecèeø4:oXê9]Oì¶Ðю*Þok\ßwfÉ[=GþH·ú£þ¨!³ ÝÔÖòÐÒg]£@>HKþH·ú£þ¨+<¹Êkݿк¯ªÉÕ½º¼ôÿD}ûâI oú'èßV¿õk_ÿU½‰úCæ¼ÏªS˜î§˜Ïh¬äX|\Fçi—¦¤>kÌ:«ïoS̼^à[} Ólo:mø~+?›ùc¶ý]†_J¯”n¦Ý•sšXêZ@cl¤4Á3ێÊ-x.­Ââͦ=¤ö>Zh‰èØÇ8Ðæî­ÆÓüfRÜÜVºáXÞH5v½Ý*•êL×§ìe`ed×mƶ€IÑ®n¢á +%ÖÙPcje•6Ƽýï7\ܐCضP @ñæ‹ëÄ®–=§u„Àð:¡ÄE=HízžÍgØ^Òˆi ‡MP ÿ&y]WÔ¯Dâ享 …ö5ÏqnÝKDø,‡lÇ}Œp ík|I÷-Ϩí3h†úà7àֆÿc•70j·jóµ÷yUGæ;ºùy=¹–cémíÿ¤€@Ô.ù¨>΀Ãc^ìpXÁe€–è×‚æ»æ©Ö+ÎØqpnßX÷e<–´ÃlpšÓVö4‹Ý]€±ÀÄˆä”ï~Õé„}ª­v×;{`:7A3'«ªt۟éՕSß»fÐö“¸v|—?f3þÉLãôÚÚÛfE[ÿD§í#Y'_+ÊÀuwŒ.—"Àâ¿q÷‡I:mi))èñú×IÉ 8ùtÛ¸Ãv¼ nü‰ßÖ:K½Ù”íÐÓÉpOŠÃ}f—Ôêpú[r=&›H[»sèè÷ù!ÑÓîc+¡¸=5õ°¹µäÈÞ#™t‘ø¤§¥·; +—–]‘Uo-{ÚÒ' PwTé­­öœª‹+n÷¸=¦NÙ0O}?[/¾êìÌÄ閱ûXË=@ë[K‹Z×;’6è‰f6cnÉbôÛ0¶¶ Úç5Å®sl#N8ÒS¶Þ­ÒÜtêv€lh3¨ˆ'É?í^™öº"HŸQœˆžþas¯®ÃÆÓÓºcœê˜ûžll¸¸·¹4Ñ9ÄÉ2NK$ïáÛÆã»á¹IOM••X·ÆÛYáÍ3䊨tfµ¸,heŸá,zdòøƒ*úJXðU DV'UtðU u¬O TÐÆºÇõ §«~ÊÞֆÖC˜óÖF±à‡‹œ×àä^Ëê/mµ¶Ë›YkdФ8m—O>~H˜/²Ìܶzö¸ àVúÃki.€æ¸t"×Óò™‡f;³\û^æ¹·–6ZÙ¼k·¿ŠIhdeÖ΋Š}zYUípÜêI­Ñ%±VÝ ù(7ë Y}Bªñ‹…,¸Tðú;Á,~×oöƒ½ºÂÒ~Kñ¨¤æ8>E—·säDø6[Ò·gU•U­¦ºÈsèml÷‘ºKŸÎ²8ðI §#Nɺ—üoõGýPR{HvéO +=KþH·ú£þ¨'Cæh—Ê|žwMü»èßO?¤ú™ÿ|IMÿ«?õ[ؤ>kÌzÖ5lêYW1Û_ö—fZÙq÷í^œ~ù¯3êØwZʼ;ØëÜLƒ&L§eŸÍšÖµv>@œìפ}|´[ŽÖ^³;Ç&{=¿õÚÇ7ÔÇ³è¸ øýê;[Eߥy}NH;~> ¢W°8c¼5„ÀD> +ÒëÕÔÓpÆê«©à’Mgè%Íw†¼Š¶Ïq{ÚI%»cFÈð:ë8õ:½,v÷:ð> +vUsí.©á»ƒd8LFš $.¯ü$H_Vy0}*Xæ´·V ‚à yi+¥ú– p¯­Ì5½àˆ×n§Ís7†7.ÒÀ Îìꩆpn}F¼6ÍÜîϲ±ÊüãêÔçoîòóšÝs܎ ëÓ±³Z*hs­~ÇÌ»Ûô¸ð0ªþËyÜÃÐðÁq—7íí'Ý:xü–ÖoKèÙyN~[Z쇰Ve头ï ?yЫ»ê÷Õ @uLio¤p<‡~÷‹BÐqÜëñ³® Æ# zD¾¶>àçáén®{FßÁEý2À+ÝÑ0õöÓ†ñ¹íøëªÕÈèŸW²=?P2)kjhm…£c$µ¦ç*óoêËÚڅ,pü֋&ogk¢Jhet×2ÖzñSK¦ðÓ%¤=ƒkN’¢Ì ݎªÎ‡†êžýîgªÞX®íbÕ=#êíÌe{+p©ŽkyÛgwd Uõê½OÞ6¹ÖìÝi04#o»’š/éu²¶UGJÄvklÛ}^®Ð*iÝ[š7Ï»N~å:±mªü{ðú>!º‘úÆÛÛº«7ýòo»…§Jèä¾ÛÃ{¶—Xd–€Öîð ¬èÿWß}—X֛ïVÒltþíFèƒÌ$§"þ–çºÒ0‹-ý­‹¡ï}z?QÇnò‰‰cÃ1nè˜ØØæö—n´v{K˜4.!Ž0´õêۜZê˜\Ò ¡ ÷x6ôO«ûōc šïPP3#ݤnIN]}*Ëëk踎c˜E%—¢æà‘$¸Á +G¤^L; ãEšY ~SÛEÐÑn 56šl­µÕì #²#²qÚK]k‚à!%4ºÙ0È~%XwØâ뫤îiw§à´Tu/vÖ=®tLž:)¤¥Q Ӆ}r”Ñs›[Û‚ÜšÞ u¦Í®ß½Ó¡<’ÈòKo’Â5fÍoH`scSn‡I1¨”Wbd:¶ìéõWs‰–=îs@h½¤~i%»|’Û䱩ÃÊ.ý> ,d´{Kœã/ƒÃÌÝV‡ìÌô-ûÏ÷¤¦ÌGdÝGþH·ú£þ¨ 3 ›eU†:bA=ÁóGê?òE¿ÕõA:0ó eòŸ'ž×Ôüߣÿ|I(ý'ÑCþø’›ÿV0ê·°?H|×/Õ>¦äg>çW˜Ú½{ ßì$íݸ6wÓ9Ík›¸2˜”ûÙûÃïUgŽ3®!u¨nbÍôÏ»býßij{˜»ãÿšb¿©ùmìÆ8ö>™ÿÉ&§êUµ¼½Ù»÷Ip-1¯€Ý¢ê7³÷‡Þ–ö~ðûÐû®};ø•}û˜ýÿù¡æmúsÈsr˜×‹ 07…­Ñ:Cº]©ÖúîyÏ ‰ GrV†ö~ðûÒÞÏÞz|0ã"«Å'3–qᔬçgՖrŔáאGéý¤dGÁ'§ÜçÚYƒKƒÉ÷nØçO»{ºØÞÏÞzBÊÉ 89ÕHÂá»¦ß h锖°Ë@²&¿4vbdו[êÀ¥-`}¡ðZcÝ൷7Ä%¹¾!%8£6‹qð1þ™Ùfã1ôd‚{…°f9î.éøåö‘¸ƒ‰ÔýËss|B[›âS‹f]®&ώâ`r¼ø6a]ux +ƒ6~㺠%kîoˆKs|BJs2p÷Ø÷}†·5­» ——{AG€žê¨Á̬i@ÂXót l~õ»¹¾!-ÍñWŠñ0?F'íj3¥à0»›š&9‚¤zfžë /y—8ŽJ³¹¾!-Íñ¬CF&=޲ššÇ¼â;€Ž›s|B[›âRëàÖi7gًia›L3g¨ýN‡“Ýu¥ÍŽBÌf ,cX!ºÀ$Ï$y¤§ŒoëVšôº#h“&>hµÝÓ×µÝZ×ïid¹ú‰“#ÛÌ-²W°G†Öÿr_d¯÷ŸÉo÷ §/$Šqúöº€ o3ÚH0´ñqÎ=^™±öêHu†Lx|“qšÒ\Ñ´ ù½7ø»ðþ䒳¹oõ¿OԿ䋪?ê‚^“¤\cQ)u-:E³§´Õè|ÃÌ-—Ê|žzÿ¢~‡ýñ$¤oúGèßSêÖýVô]Oùü.ÿ¦ÿ¾•–Ì ±^ÁÖžææÖïÑÎá¸s:íðòZKúFüwýô®ZŸ°YT}“¦[ºAp¼Ô %ÁÍÂ~‘‚«¶zfS-prÇç“Y"\“ǧFPÂzɹ•>mdžI»c éô‡Ég:®œÚšY‹Ó^÷z±|݂3-䩌ìßfK±°õØIfDͤµ¤Ÿhf²’ž‘ù2wØÆÀÜeà@ñOêV\ZÒæÁ#p‘@ %?Û³#úæ?xxOåÑ/¶fënӉpø,Zp©yc*ë}v8S¤—;Ûæ'è¤Ìvûvõw³cvÒpÛìôIt}((YìÉíãÿ:?Å.×Û²ãúûO¸x ûŠNÎÌ a=ÍýíÃò,¦†ú[ÕM¦æ×é¹íy 5Içó»©kìºöõ’¹¯{CNÖ5Äh5Ð#g²8!þp}…èR\Ëñj õŸÕ\}Ì­îû\êÏ©î÷÷ŽÈØãì™4¿'«Yh$¼5ÁÛ^Ò\Ý{sù4JÏeB´È… IsE—6½îë/ ¸¼´lp Í}Ò*wµ·¿:©kñ™é“µÞë@s þ—:ꕞÊà‡ùÁö¢Is¤Z÷u§‡5Ü:„@ 'Nþz¥èµ„ÖÞ²ö6 `pÚaÆtçµðÖ»ìyÖd·gw×]Ô¤awý7ýôª£u½ŒhêfY$¸ÔÂ]¬‰>J»a˳FMæ¿ ¶ÞæRð@ÚKƒe°LGdéã +6ÙӜÐZÛ6cX]¶ {4þJ×+¯ïò“aÆcìõÀFš©?¥õ§‚?hÌAm‚ ÈñIMJp:®–㎞j²_[® xâ?tê™ý¨XßMÔôÖ·t‡6“¸A–˜ˆ‘ W?guè1ÔZ±ú +à ¤öR§¯ +KnÎa¸–Í¥°Ã‘;´IM&t\æ×y5à ms @§Ød–óÄ"ãàuZYcšÜ +ïôöÕeT–Ãá¢]åÊ0étIJ\z–ò¢Óôìðü‰)©„Þ Ö«#Óu@#]ۻςXv2»º…ÑŒxs»è%[ôßáù]‚™ê8ÇÔO¾­ÛšZf7ӅšÛ UoÛ1šm0ÚÀ.Ùã"{ëÄ)ú¹¶ÛéÎ¥ÕI€µ¯Ý°Sgö×F;fÖË»™7k¦œ§=c£ì.õ[0=„É>ÝeR³7u,&†\×VÃQ!Àñ¦Ý<>JnÉ{Áu9xÛkc §¸n#nð@ýóæ’›Ÿµ:VÒMŒAöŸÍ1à¥noK¦ÂǺ¶ØàDjAü㧒¢rë·n8ËÆ{ÜmöH{¦!£¸ì…ö««¬zù¸­ –Š÷5®háÄq¶S¢Þ«ÒO¨EŒËHÄxñ)YÕzUo ²Æµä i’$¢¦rK+‡æc›^àIËKaÒÓ·ÄÁNì—>‹m˜ÒÍƒÔ ­%ÄG”Ú·«tº½'=À gc¶ÍÐΚr¦Î¥Ó^ֆ½¥¯y©£iúC‘æ³Ý—hú¹Ø­4–83bDñ¨…;òžÊêÊǢʉe­{~“†“Sr¾§Òì¹Ô1í6‡m-Úy$Ìë]0ÈÄvÚáçàKï²²ì|œ{,m„Øí£è‘¼3Ú½ÐNUŽ uyô44~›Ôd:\d{H@ˆINƒz¿NyvۇèÀsÌ$2<Òý­€j76ÍõµÁ®  ¾0¨Ý–NëéÎÇf4†—l’$KuáKž¡}[©Ê¢Æ6C`-'¶í9I)ÑÅÍÆÌkŽýía‚`Hžè[þK¿áÿ~ø•[U[ns]dêæ7h‚Vÿ’ïøߑÌ<Â%±òyÈ÷ýÌÿ¾$›Mü;èßSêÆýVô}HÅø]¿MÏöJÉo[ú ?¦Épik‹F…ÐL´óku/çð¿ã¿ï¥c²ì¶€ßµe—K œiO•XšmÂW­WŸìLz×\ôÇvY:’üÎî†:×Ö=ÛÏL>œ³yß&4±¤ø%f^[…a–åKA.pÇ;qK{mz“2ó œï[ ŠÃŸ°ã4o ¶‡+€—‚ÿ`÷ðdzÇ\Þ:qæîqy ´€]´ìäJ_·z¨}"ΝcuŒ¬»t–î0çÀå ÙY…ñëe Î03`~pßÁ!—“-Úr\Æò(÷A$Ì}Éqø+Ø=ÇØƒ¹ê;÷’õûË>šòrªsٓm`ÚÐúÃÒIo‰TŽ'SÒ3µ‘ºjlDë qx,àöÿ÷¨ïÞUq‰Ýԝ¹ îúOú#Ù˼‘èeŒ©¬µþ­€C¬€ÝÇÆX,kïÏcĵÏÀ÷ˆÚÒÔ(½– pI:XðݬøÏhSe9gÔõ„âÚÈ­»D28>2®;¢ô§v3ËM|”‡Jéíqp¤nsK “;O#”PÑ{rúCf ·8’Ùè5ñ€¤ád° 0‹€smqOòå\gKéõ×é×KZÍÁÐ$jxøãôÐý㻏}$¤§=ÍÍ``œւ熎4ª›*Ì/Ø×áAstkD’#Oš¶î‹ÓíΡ³3Ƀ¤A›zWOmž«hh³pvá3 @„”Õûséöý”\Òd5€´°dÏàÃêÕϧöPÒ5nÂûxÚ…8¸ô9±ÖÖ>‘ñ(©$×G1¸YíÈ­Ín0©¡»ˆg¾tÞ[re@àõ-ÄbC‰—zzI:赒IWغ~Å ™ÍHîÓœ. q]YͰ¹¤³ÙfwµINK°z“ˆÓØl×>âgÃ@¦q³ý]Ƭc¼<—êÝÎ#]VšI)ÍÝSÒx»´Öû NòtŸpÑ Ý;¨9Ži¯ é i¬ëÉ®’JsoÄÎpuu3µO±¥“ð1!hW]u·mm Ã@~JI$¥*=[þK¿áÿ~ +ò£Õ¿äËþ÷àŒ~aæ-“ÎHßôÏÐÿ¾$—»Ôüß¡ÿ|IMÿ«X?õ[Ñu?çð¿ãOýI^1Òþ´ý{êýPàãuk«Üë°5Œg.>ÞËÙúŸóØñ§þ¤¯ŸºT¯¦u‡Ûk¶So©MKCÏÓ¼ îØzNµÖþ¼ôºYÞºüš\C\X \Fá¸m:Óÿ‹N»ÖzÏOê®êyv侒ÁK nn漝¡ºè¸ž¯Ö:_ì‹pi¼fdäZr ·ÚíC¸]Oø¡ÙÝcMÞêô'Øþý“mAì=LÆì ·0 é4÷ä‚å:í½Íµ¦Ì²ÐÀ×hýv™Ÿ-{*ía-sPcZœ>Õ«@çó´ì¤ê*ô=M¬Þ]zòÂÂ5—nÊ*Lë2÷švLÀ¶¶û´‘®‘0§ŽûòA©·ä2Á.> ÖéíÛáù߂­erÚ=•¹ ?{]“îî‡N£Úik¬&¦V×Xw8±¹:Ÿ¥§'„êS‰—[ìË}v´å)b¸²Î¢ðàÂ×Hs¸ÎJŽ& +"ç5ì·÷ Ž{Gnú'Çh{º“ KÌ‚ÎE#ōy™4úy¸ä´·VðA‘Ük%§…6۞ýFf< ¯»V‚r¨ÕÑðS[gO¾±@i©›÷>ÆëÛy)~ÈÁpeŸ³®cÛ»Ýí÷w|Põv „aé)Š?‹q¹Y.±ôŒÊ=Z‹Zð{8¡ÐkäœÝšƒ³(k÷4ɏJþ™•qÉ» X÷‡ÜÐæÁ†‘Þø¦ÇéxMsvtüŠŽýÍvá¤øëà◫°Ua¯šW_»Õ¿öŒ»+m´æcìxÜ×Aîù£Q}•ŒÌŠµ¦I‘Ýe~ÇéÂÀGM¼–äm »ù3óÕ7ì—ÛgOºË@õ@ŸkGïhÐõx"±_Í*þèþ. ÍÄ5…Ì5Z\‰o#â<»/€—ZÀ:‰‘΋1Ø8® Âû®¨Xç‡8Ãmi8í¼üÕzº?Ou ýƒ"½îs·@â]º0«°PºÊ_c¹öœoô¬ä¤9o?r‹³°Ûo¢ë˜,-ß´¸NÒ ÝðÑsôôIRA¥~Éχ†Û¹Ž¬;Û> µ[£§µ¸ìªÚXKI0‰'åÙÔwï%ê;÷’ “"z ¤³±æF;ÔLâ +zð)©þ¥t5ˆÜÐ…?Qß¼—¨ïÞJ–²ôßáù~œÊΟôÿ©Þ£¿yWÁ±¬»>ǟk^ã΁‰)ÑIfäuªi49­ÝEüÚNÝ¢vðBÌ·¬ÙeWÓs·2ÈXØhÏ2¡SÑ:ê˜æ±ïk^ÿ¢Ò@'à‹š€OqÏʲÁK§¢®ÆÈ<—ý"J–F}·¾—»KñÚ^5$¶]2æ¡jz|¬›é}mª‡\¸·±Žò‚zŽPh?aºN6 uÇ׈m±ÍŸÕŒÌÝ»ÉU`ÚûškÎw¨ØŸ¢4pŽuӔTèÛÔ2ëc0¬yvíÌlˆòÕ+:†S_±˜V¿FÜ @$O”ª5¹¬4¸Sš0Ö ÑÆKÎða+h©–Y™`ö–°8`:L ùø¤¦ýÝC"·–7Û9 p§ò˜ͼÝ]GÀÐç¿M¬$A=áfÚhº×¸Õ™¸:\À}²Hqê¦µ•z5X)Ìk`6Hq ˜6u3ÞS}C)ì±ßb²²À CûɆÉï)3©d¿ +Ö9ÁÄ{DďŸs«Êpuõf°kpfŒöΰÔZÚÇå»'ÓÌkŽèlûk"6öòóIM£Ô²½Ñq 獵É™™OcÈÅ{Æî yú\ûFÙ×E˜Ìv[siol×=πïýéöÆêÜ֗K·Á Ç*‰ôE–>¼ÇzijÓw¼ÇӖ´Èj¦‘Sîmyæ°¸‚é‘ æJtߟ”ËŸ_Øìp齤ºFºö„C›g­]mDZ̰lÈ!ßYµúwç6ßO:·Ø[ µ»~—ݪ¹ûYÜKÃàÛ¤žÒ’G«Éwü?ïÊÎ=Þ½-·c«Ýùá¬jn­ÿ%ßðÿ¿có0‰l|žnFÿ¤~‡ýñ$ò}O¦>‡ýñ%7þ¬`ÿÕoEÔ¿ŸÂÿŽÿ¾•‘QÎmV9¶æ5»ƒKZwj=­s‡?%¯Ô¿ŸÂÿŽÿ¾•™èô°Ý¾…›Dûwïçæ«ڌˆÛ¯ql-9Ñ>¶ti-©‚Kоz‘cv6AÙɐßê n;ÍvÄ9®Üùu“à§^.´Üê«°l´<@‘·]8J•)½iØù6¿h¡Ç OmšéÏt¿hÖc\ýík´oݏ˜Yí§Õ ÁÊÚ=ğݍuðSf56NKN×\A$íçYì’Ö×íHsXqoÞív†ODÀÍR-^Ý´×½÷gæÇ`Ÿ£øwDûG­@]–Æã’òâýͺŸvºüëÎ¥¹€ÙAŽd4’ ùmø)UT8?/%ÖY·kMd‘îì#ܒ–6UeÌÇæÖæL</?IÇËrž“ÁÝ~qcdÈb#žèõäUS›cóoy}bư´êíÞY’0ß]—fßc\„9“2Kví™·Á%-kC]T]œçZkô; $G +O²›œêk»/ÔÇmÖ4Iö’O»ÇHí?£!C KÃwÏ´$|tD~e”´f^׍ÐÿMۜ\GË˄”½&·—1·g£pÝ ‚c_Be­eÁÏ¿;`—ú`Þf77{)ەP,`ÎÉa¡§|Vâ]©$º~å;ì`hȯ:úYsœv–9ߝ:Ú8BJGꗽÎ7çµÎ— ^ÄøÂ&6UT¸XÛ3no¯i! {~yI×¼a7veçsþžÆ±Ðր[úBÞK¥\gUâªÙu®/†´¹Í!ÄíݸÀóIM +C±µ_žÖly8 üÖÏçk¢Ð¯ªÔ iôo% 9Ìî` Õ0ë˜$“¾›§^;Fªî=õäPË띖 ͑æ +Jf ÁáRêßòeÿûðW•­ÿ%ßðÿ¿có0‰l|žs_S†ýûâIG¿è~gýñ%7þ¬`ÿÕoEÔÿŸÂÿŽÿ¾•„Êz‹mk“¸¼k®‹w©?…ÿÿ}*›zŽ ¬¼Õp"InÂN“ᦰ«¶J³²)fM¹Œ&š9wœÇ,k¾z+Ý «cuL,×Wéì¬CÛúMk¾Ÿ¨Ö~ +ðê8¹h3é¿Ä7ÝTŽ~+ƒª­–5öÔH’;ïINpmPw3&}gyáø)µ¬;詘ÄÛ põœD̀í±e6VúÙê1þ±ÛíÇnïåm UZòlôˋr ¡¯áÍ ´KGd,.‘ÔTß²šƒ¶P,/7×x€wI—Šhé;'#ÒªÂLµ¶’#±äj‡‰k™]¯}VTèp¬ÔÑ¡óÒ=7(µÀ]_¸M-'RN³>)"›˜Þ€¡­ÇpuLZAž4å7Nþ•›ÿßú¡…FU;Åö¶Æ˜ØÐÐ#¾ƒ’–^뺃kvǗ€×sf…‡AÍk„8<ªbãÞÁ]Õ2Ææ‚mUYÕ€å3Äû'¾ƒ_$ÿgê~m XÐ}GíIˆ B*I—Ó03Y[2©m¬«ù¶øDx)daQêÝ`3Q–A#Àöø n¬}@ü¶Ã˜á^ÖCŒí$ù!ŒN¸6þ»Y 5G÷¤¦}ZÇV*Œ¿² $ûwD~Eœì›«mO»ª–›A5í¨Ä 5Ò{w Q¸Ý@[^ü†¾–½®`.s¤“¯d…ÖfUE­ú;«ts©Ñ%!»5õRrÞÛ)>÷zrl×k'B›#(ÔÛ ™ïcrSöH­¤“ÛY쬜.ªD}­F¤Ô &{ñ"4QûZ$ÎM1¨£ý©)¬ü›j~ÇõC¹º¹¾Žº‚ñÀýÕ;re#¨¾°Ðns6FÖm݃‚³^Ti©”Ç‚ÂÖMcG~ññø&]Wq.Ê®;Xâ#i™Ñ%5j¾ÇU_TsÞó p¨`4*#0±¶4õ'½Ïi™gçߜ4WŽ'S°3&±`õj’dG•8}I®q·%HlVÐw ò )£eï§uwõ7—°†€ÚŒÈÖ$w)ÎS±©Τç:óú'š¸Úè~ž +ðúÛ·~·PÖX} tçUm´e›*6Y[«kvÚ͚¹ÚênÉ$yӜܶÔ)µýIÎ¥Î" s¹Àƒ´é#é%FSEë+§ô«ÓtÁöêc]]ÂÙôëˆÚ#˜ÊfÕSI-cAq—§ÏîI#³ ‰gíGnÜÙÛLm®žIÅ·¿×SÔ\úÛ`.öm;\t ݯx[~›&v‰ñ„½6Dmá )u•—Rýû`<†¸ òÜVÿ’ïø߂º'S +—Vÿ’ïø߂1ù‡˜D¶>O7¦þô?ï‰'‘¿é¡ÿ|IMÿ«?õ[Ñu/éZOé¸þÉLÞ±Ò\ nE0 i÷°jXF§÷„'ê_Ïávý7ýô¬A‹‘Qè÷³d§’[·žcº®H ¨ÄËjÓÆ±ÕúIã"ƒþŸÇ~é«ë.ë[MWUe¯$5sI%¼ñðY-£¦»sF;k;O²½ Fäl_³µÆúza¦ÐÒÿPz`DmiåÐ qûrðÿ;^¥¹ùßXÐ3ò,¡Ôs6îŸs~.d ~*_´¯Ü°o¨>Îs¯šA^ܼ?Æžúÿsò%¾¿Üü‹1½G$°8àäOp61â—íþ‰Ã¸;pV@“¦³à—W·/ñƒ§¾¿ÜüŠžØËú…ŽÖ<8ÌJ±j¶ °¿¨¶£C¦ŽÙ¡×DV³ý­…ˆ'èžÀ;š—íLNC^[$nÚbB¡W֚k÷>»œCD<?œ—LsôL)>¿­EÌ‡Ñ %Úi>ßk]å»Á/É싯rkq½W Çk[aw†Ò8ø©ÑŸ}­Œ²]0KNÝ'_š§s>±º¬_MÕ rg@^>ˆïíñ…&Wõø7 ¬­¹$0Tx Ùî÷jD€•ø{B¾xo[øÓ«µ¾ mo‚ÅeZ V_ULôÀõ õ´ÏaßÅ;õ¦`¾Ñ² L7Ý1â丼 +}¯ëÃívv·Á-­ðX–­ ý(58µö2`‘!º\t#²&úÅeô¿0×]kžÆÀvµÁÜþyK‹À¨âО8iâëío‚[[à$æ%¶·Á-­ðN’J[k|Úßé$¥¶·Á-­ðN’J[k|Úßé$¦;[à©õoù.ÿ‡ýø+ʏVÿ’ïø߂1ù‡˜D¶>O9'ÔúCèßJ©Àú÷Ĕßú±ƒÿU½Rþ þ;þúV]X‡6ÆÕŒ÷ƒî>£ùŸ"G S©?…ÿÿ}+›0½Ak>ËP¬¸ÌØ×D¶AŽ] +¹l6‡snõÛN9±Òéõ]ˇ¹Ñ<{”N­ÇÇ$‹ë]­cF¸{¾*¾ÜbdŒ=í#Bë57]áNçPZ ᝞֑êhqƒ÷”Ø·§º=«ÇuNk Úç¼àϏ²³VªªòY¶¶“´8ÀÜu‰<î²ËqÜÍ®û HØû €ÈÖS¾ÊÜö;[Z7<™®£T”ê³§ôÖÖê˜Ð`8žÚ;¼ÒwMé› K»|n#Ý?{ÁfW^;Þ[XÃ.²CÚaLkàÕôó\1¨ ÖAy!¤c—$§n¦ÔÆ6ªˆÛX `Z›Ž?í;ÀÜj;ÃN“µ“ °YÒ½CfÂý°âÒO´Ÿ?‚L,ê¡¿Hè>>š5{§0}pÍ72¿±5­{ƒ7—º  ì„ù[:•;Ûö Ýâ`´Ë4ŽW?Ԟq*o¨is_¡uÏØÀ;51à&õŠ„ÝµÖã° 5—>\7x*|¼ùŒ±¾*;kU¦ú*º½—NúՑxwÛñ«Äpq`Ýۈ6¢Ul¿®Yµîf>%/±±üíޛ;µs¶à._7¨gÌwè\\Cϵ§]°GÑïæ´2‰}Ç4o¡Í-{Iö6Éì‡1ÌäÇ8ÄKPGO5^Ñù™çê)®Ç\Àûâb³¦îJa›ÕN¿b#ÔÓßvŠ»ñØ00^!Éc)kÐèsµ'û!SvEƦtûÚ+úOÀ˜oˆƒ;@WÁ°ì;'¨Œ†°cR훟¸ ²=ßò +?iêrg­ ïŸpÆöåg *ì{kýu!ÞÍþ †utx¢W‰X¯ìíÀµµÚá¹Î°Ï·Ü ïÊ*m;?© +KÎmÀ +͍պ˥JÌΤÐÀÜ9{Øç»ß£Há¤Ç*Å£ÑvîeÎcÀãv௴¨ &É`é֊›¨wªwÂJo37«þ~`“óåK#7¨ÒçlÃõkæ¼ AüUzkk]gìëY`ŠÝXq#k˜AÛۍ݃Sk[^ik „N%6ÆoW.hû}Òñ|Sý¿¨´»¸k¡àêN¤:F¼ +¥¬wO¹­?IÂÓqå&p›# ¥Çöu¯¢’E/e„Ht9Óߒ’›îÍêM/œ@Z'm›ÃG0$tóKí½PÎÜ1 ŸÏx‚>!P¢—1©:æ6á¶Ö¾Í‰öÈ<8D§ +§5åøW³ß>¡.s„ġlêÇgê!»‡º^ xpžÜ®¦ÛžÊñEŒm³töΜøªUàU£>ÁfÚĀë&IpxmĨszuívùký^ îÜgÍ%= +£Õ¿ä»þ÷à‡‘}ì.º‡c‘×IŸ‚Vÿ’ïø߂1ù‡˜D¶>O9¤ú™ÿ|I6›ÿ;èßSêÖýVô}HNFé¿ï¥ô¼Ó'í^â"}6Ôô¿ þ;·õJÎnW֐×nÅÇs·±k‡´¸Äü§ÅWl6ÿefoe ·Ógtõt̶X^üŸP;Mmà©Ñ›õ˜å2»ðêm'i²Ñk´ûƒGr¶çx»ñIHþÃo®Ûw[ZA¤5»I3¬Ä„_²´ôÙ>0;|“nw‹¿·;ÅߊTJìÅk>…llÉЩù%öfÄzlˆŽ&Üï~)nw‹¿É´ýµ¿ {ªu´õzA-sÁnæò&¸Ñ^Üï~*®9ԉxZߤ}œ4$,7¢§Ë~±à[]NÈÌßí!¶^â ·Ìm{G%ÛgNڔN“}­Æõ‹Øçûm†na::OÞBïíé½.Û¥ôgØÆ¸¿ÓqšçÛOq)ät®“s=+1³\$3èÏq$Dš­|Ä@ˆá®¦öò +ëÙåééß³3T^oÆcg&­šîờÞü¯»(\+­­mL0×v܅¾Þ‘ÒChû>Hú%:´KC>mã^êèÛ+ £-»ˆ$èÐmǟ¹E—”É9^ƒëø¨ø:Œ\2Êò.-g³ìæ?5º¼ëòB²¶WD}“)Í·e¯3ïiltçMQ^“HcY}C¢+mf=@ F£ŸÑé(- }n'íÕº¢ÝÌsˆ.ÝìÚ™æUð(R–eu[ˆ\(˃Iª·8î°=Úö3óìõ·eŒL±é‚¶˜pÍ‘ÎãÉS¨‹³YnܚÉp.Öa¿œ"LíCŠ~Îܟ×ݵàŸ¦wyxEIŠÍ×X1òd7Ôkw´¹ðâֆŽFä1w†ãf°‘$5ãoyóåâ~ËLœßM®t–Òk´ñ25M¹úå¹µÙK÷nsA™3¯t”ÅØuÓkªn>e›´kšóA2d¤§²£]¶¿.¡[ç;@d6U²PÔûc] tüŠÎÖÙ{ŸšòÀHm€û¦Gµ²d„”ƒÐªÆµƒ 4oÄº$ úGçÂ31Øö1ÿfËo¢[Zçí%£wº„«t¦XÊîm×Õ#paöA=Üߌî”×SeG"ïҐ\íúéáà’œÖ2»Y]ÂÌ c·¸ë¨h÷9Úü”òqoºœ»\ßÓ1ç¸çHöWOHa­¬u÷9Ì%̳¸9þʏìVÿܛç]wë'ºJi^ÚËÞû0ò½KÞGèœuÛ Íì%5•²ÆVçaf5Ìa¬Ÿtxüuå_wF ¬†´L€ó©$rTJ¥ö‹€w{ˆ‚ žü¤¦]*ªëÇ.®«i?Bï¥íÓñMÕ¿ä»þ÷à‹…„1X-²Ùïk·`…Õ¿äËþ÷àŒ~aæ-“ÎO¿éþgýñ$µõ8Cþø’›ÿV0ê·¢ê_ÏáÇßJå.ë?lv+.}f³úG:@1wÕõ/çð¿ã¿ï¥g·§ä—o³§b›]ô¬Ž@¶zcÅdo2Ù¸ÌiáâœýaêUÝc]]edɭĐ$ɒ ×Ü;.¬cdúrì*\ýÀmbIÔxè£fW°³ ËI±¦twfƒy§{2ïø”q•?X¬¶»=·2ÆèÐ^ᮼƟïZ};7/#=æÖ6KFòwýå®1óp>€ ˜Üuÿ¢‘£¨ˆáÐÑ̒tþLF©{2ïù«Œ!Æ²ã’Æ—¸Žà“â´zpýg4ÿÂ7þ¤(ÑE¾£Í¸ì`iŠ\É$·ùZh§Óÿ¤fÿƏú€¤ÇÑ-ÛÉ$’‘j’I$”¤’I%)$’IJI$’R’I$”¤’I%)$’IJI$’R•lêF ´8,² '™VPÞ ¨©ÿjJy¯ÙØ;'í/þ‹ên“ôwG©üß=¿‚KcÑÍÛüøŸOӝþv~ŸÒùmI9w;ñ}{£‚=†Ü?F}CúNüwýô«ËåTKõRKåT’SõRKåT’SõRKåT’SõR¥ƒý+7þ4Ô5|ĒJ~ªI|ª’J~ªI|ª’J~ªI|ª’J~ªI|ª’J~ªI|ª’J~ªI|ª’J~ªI|ª’J~ªI|ª’J~ªI|ª’J~ªP?@ÿŠùa$”ý3ù¿™Çýù%ó2I)ÿÙ +endstream +endobj + +143 0 obj +<> +stream +ÿØÿîAdobed€ÿۄ    +  $$''$$53335;;;;;;;;;;  %% ## ((%%((22022;;;;;;;;;;ÿÀ90"ÿÄ? +  +  3!1AQa"q2‘¡±B#$RÁb34r‚ÑC%’Sðáñcs5¢²ƒ&D“TdE£t6ÒUâeò³„ÃÓuãóF'”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö7GWgw‡—§·Ç×ç÷5!1AQaq"2‘¡±B#ÁRÑð3$bár‚’CScs4ñ%¢²ƒ&5ÂÒD“T£dEU6teâò³„ÃÓuãóF”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö'7GWgw‡—§·ÇÿÚ ?ä”È$Ÿ’£mîs§îBu ä¦l½Á çh֍Iøo75)Ы³S,#v-РtîåY¼3hfTÜ£aª¦=Ö¶wÖ֒ᷙi +LÉ{¡ÄÈì{iæ§ÃÍÀ@DX²òÓãâ¿Hm¿÷Fß3¢ª}*õ&J9´Øßq’ªÞæŠ lœ@ñQêwzß©ßQ:wSé®ë=w"Ì,0â+¶¶ØÐ>Ÿªòt™/PèßVþ¯ôªÚþ—‰U{€"ð7½ÀŽ}GKµø®êßJú‰GÕÜ.­×2…ÅÍ$cå[º¶<8‡¶¼vóîò+Ò1,Ç·›1cìï­®§h°€[¶‹9¾µ88TM4WY¼—\ZÀ ˵q| gÍr?ã+ QwÕ[–Tz[ÅÌem ³í°ß-~KµCÈ¢¬š,Ǹnªæ:»â× ¤$§ççDöLIqV:–#úvvOO³éâÚú‰ñ 0Ì*íáNeÅBô­ZÜ<6k®õC©ýIÁé˜ÖþÎvw_váeTì‹waÍÝì`!z/DÎê؆üütח‘]{^ïN×;oÑ>K…ÉÿWzEGêÏLAah¢¯Œ½ß8\ðúÙõ›ëOR£§Yž1•kkm“Md«\ön~¼r¡l¾ÆÞ¡‚ì—b7"§dÖßQô‡‚ö´ÜæÌ€¼ûübýzkYoC饖 XÓfu7™aÝ«§ßMdðPþ¹etÿ©uÕÓþ¯cU™™[ŽUöÖm{©w·Kl&eÃPW™ÀØß¢‚­g8—Lù©h¢x#ˆZV/'”õ[eV¶ÚÜYcsÓ8"†%͹9V=ù6¾ëlÔÙc‹ÜOÅÒ«Â#>˜ø¡»é;âRIláÑêØxZoé¬,–…K¥ÿ:>oSô‡+í{ZðÝú¸šYøýε^žïÿÙ +endstream +endobj + +144 0 obj +<> +stream +H‰ÉÝkRaÀñ¡çUϛçèyñù=çEWFƒÅZ›‡Îˆð¾‹5ŠîŠp*nT›N¤‹`í:( ÖU#=:.ڊØbA‚÷y/yoŸ«ï·E€?E‚OA‚J)q]ג¶V³ó¿£Ö ­0´)øFB…Vi–8IPwxýëtQà38Ì·".“‰§ ÞÖvµZÙÙ©?ß^{°ÉèÏ¢ÉÏ"͛'Vªš4/0Ñõòæd29 ú” kqí;}윘©šaR W(”ZMŸdSÑÀéƒãƒs N@B7 ­j†V´WÈÞGvƒ…w ø¼Õá,IUEÓ9)%.+ڑ;¶|-`p—€. m‰¼¬'Œ¥%×u½›+·rÞÊ~vþÃâÕF4zC£,´à"Bʞ‡ãñx8ü3—Î 9¡s1-S¹˜ÂÅê±äwÅXÜ&!íÌ:Î%EV³®·œõn¸Þ–}å½ ^³è £ÑÈ6S6vÆÇ¥Gù8Ë[ «ÉºŒ¿ˆØg X,3 'ˆ2ÃK5Qÿ3;,4 TX/‘³·»w~þkcõÞõˆôR54ۏà^z!¢øÙ5òE"Dõ{ý‡ó33«*úi¥ysE2<ÉÈ©æmÕz’ÉüÎe_,ÌQtt0œžžmܽ¿(È»ôO€à ’ +endstream +endobj + +145 0 obj +<> +stream +H‰b`F&fV6vN.nn^܀_@PHXDTŒ…Gœ‘B I)iY9yE%e&<*ùTÕÔ54µ´´´utõô™ñª4042ª4153·n*-­¬mlíˆPiïàèäìâ +Š%^^̸B¨ts÷ððôòöñõóǧ2(8$4,<"2*::&6ŸÊø„Ĥ¤ä”Ô´ôŒÌ¬ì›\Ü*󒴴’ò B +‹ŠKJËÊ+øyqª¬ÔÒª¬ªNLªIª­«ohljƯ² +h²VKk[{GgW7~•@R«§·¯ÂÄI“§0€£b*L›>c&¦ÊY³çh͝7ÁB>°q‹-Z¼dé²å+VbQi¢µjõšµë¦‚U5’tž +endstream +endobj + +146 0 obj +<> +stream +H‰ZÇ.¿ŒÖÁÈu¨"Ȃ˜"£©« À“únk +endstream +endobj + +147 0 obj +<> +stream +H‰úÿ "`” +endstream +endobj + +148 0 obj +<> +stream +ÿØÿîAdobed€ÿۄ    +  $$''$$53335;;;;;;;;;;  %% ## ((%%((22022;;;;;;;;;;ÿÀrW"ÿÄ? +  +  3!1AQa"q2‘¡±B#$RÁb34r‚ÑC%’Sðáñcs5¢²ƒ&D“TdE£t6ÒUâeò³„ÃÓuãóF'”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö7GWgw‡—§·Ç×ç÷5!1AQaq"2‘¡±B#ÁRÑð3$bár‚’CScs4ñ%¢²ƒ&5ÂÒD“T£dEU6teâò³„ÃÓuãóF”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö'7GWgw‡—§·ÇÿÚ ?ô*œû+Ü@jO`à²uUs2®¨SÇuK'¨ä¶‡™Â®a" W՘š:5zô6—è 5Õ£àœ|ìkZ§kIùÂç2þ°çÙaÇ·k˜çhcU›Õþ¹çà†ãàZ‘^ž ‚Y=„é*žqå×ÍŦÔÞãu‘$ â|lhÏë=XؽW*Ë®meö¸ìüãò ôN¹õGåýKí:}¡­öú@•ÆÝ“~EϾûe¶çØâK‰>$¨<¼¦H¡§–î@…u}L}fú¥}€tüÁ‰¸}Xæ ó<~+C©bßQ4–¿d‚öAóÑxÜé +×NËËÀȯ.’æ5ŽÑô\ÙÕ§ÅVɀÈKÕ¾´[²‘cì}»êùe™7Ø9Øû֕‚‹Z…sßRúÅEÙ^c+¸y΅¨uŠ*Ç'Õt·Á q‘„uÑ9«Ü•Ûd¹t—ÙK½w¥²jdAIr¶g¶ë™xÊ{Egyi<Á˜II÷L÷ÅÇ×åàkqÇ·âúU«ebÖ7A£¡ad¹¶VàyŽí¹G!¤¢{,»ª`k´'èێ;ПSçfó]Ù<9‡e#ÅîÒ~\¬Œª½oª7Öª¨3ºÃOuÙõ>—þRÁuûM>¿©·Äµ¤÷®›ìohŸ§(¨ŸÉ›‚Ð(ÀÎ;¾e“þ.úö;7Ë\1„’¨³êw_}Ÿesw;@½±– 82‡{ƒ¤žß‘’@]þ ^Çí|Ó§}AsÇæ¼˚?"»õO§ú.Õìq-•Ód½»ô:!=Ìhj{B®rL›¶Ç³ŒF¸wxª™Ù=ëE5Rc0Œ{ë?D‡è>mré>°âz¹Ì¸yiäŸ%‘ˆúò†K¼ã^ð;ít´…­Õz³sì ²¶5“ô¼î¯à„çDm +2sL£‰ù‰ô¸¬ªç0½­.­³¸ü¿‚Kw#¡Ùeu>ŒˆÂ¬‡>;·óŒ¤ ÿIsŒpß·^]۟tý_ z·âÓw¶oRÀ­ ýšöê’œd`d°Æ6D¶[Ú¦æþè8}Z‡(Þ"@ÿyóï¬ø¸øõãdãU{2Xm$ˆ2=³æ´ñ`º՟®9Mû=/gÓ°Ùà?E‰\¿Së9·h¹˜l’â7¸˜˜k@Ön`qLÚåäD 7®¯_êR‚{ ­ª ÐÛÌ/>ÀúϗfUNm¶_Qv×½ì iK{«ßXúÍøå”Òlcžd¶$í?ÃÄ Y£ÂGºz[̂%S¶·×©î¹gå⾛-~IϤ4µ ƒá¡ZØ=_+-Ωîk›[¸ã™¦f:葔úiI1šwåÓ´±»˜|CŽŸŠ¿…Ѯƭç"É`Ýc ,&5Úegu,'YŽ,²ò×É{èùƽÊÒú«—}Fޜv?¢ÆvF¿9Vqg1 }CZ4ÔÉËq “Ò¡fŽþ)\ޗV;q‡OÍôžvíßÇT—Ië\*ŸO´Ä$ŸíãââáWÅ}o»¿:®)UVçfž'øÀé9}BŒª·ÔÈvÆçª×Êê⋬¬²TK‰ªò_«Ù'ëž­hc_‘¸0p$¨f›>אZÐÿ¢=ôMæyg ê[ø±B$š=­Äê]^ž©V ¨¹Œõ?NÝk—¶ gÅd_…¹÷UêØÖØ4Ò{ǚٺΡ‹NEŽÃ¬ÐeÐ×èû•{ójmr5‘¸| +¢y“’F`‹­%ÅÑÑåh~ºh\¦tº¬´—“©-ò”ºÿNª÷´Ý?¢?£p×SÛæ‰Œ2æÌ†A¹ä@|èÆðÑS+ªu«r}6ÒaÅÃr–6umÈDzjѳÀÀúÚ×¶&wOÝþÔlLÇ»&ÖŎâGÙAž¦ÁÍ´—;ÉÇUsí®´ì<$¨ç#±QŒxAï ên-•m.{Ä0tW¾©Ý^%weßc7–¶¦¹ÚKQ¼=Ìq ÜÆƒ¸ŽD­<^‘gTèn˜­íoèZ<¼R‰1áaÌb1˜ŸÒݺ>¹ôÙè4—7÷À%£ÆR\oO³"—߃íkì®ÊMd{‹ËK@ûÒSû“º³æÔö¡{hëáýY»ëž&f1ê“íüÈ•¿õˆ:þ¤ül\¿²fl`‘çUž…sþØæ8¯b¹ï®+?¬ý²‹Ew5­k1 ¬§òò÷ñ*Ðõ·›xˆd#Á溷Ö>¾Æ?¦äÛ´ÖK^@Õß5«õo(ç`7Õ;­¥Îc‰æ­U¯ú·‘N5G¬±î®²s ’u£Ò(éâ?'§o <¸ÈÓÄ&s0Çf0‡·q-w)"& =+S­2ʯ6¬ÊÈyQ•Ûwv÷;HRÉ(RNþ£ëÐ́î׍J»c+¶‘äs*¸6 ½Îo +‘ÖoÆiµø9ÔV5/eU·Öhó÷˜XxõbçÖËj>×j ÉŒ@è(K•Ë)@-c½µÎu™cÓh Rk,c q:É<aøõâ<ɀ3üWMõK¡½½_)…­ÿ´u¸jGúWúŸ½2â5«.LƒLälôÛ¶`žŸõ_ìÏþvÂÓg“œàcä œÝ <°AW¾°Ýº–R;¼|!e_g·fèÕl€‚œ|’&\Gsª+ñ)¿*³cÜ~ÎàæÄ@ðI ×kç‰æe%½‚¸n^ ¼íSâtCe’ÉžÜ|TlqÑ­ÖtV)e³ß¹Ä“j…ætÐò|‰˜Âm›j:ò’›Tp®Ãé'ÖnÛoȾò<¬y,ÿ£ œúÏõjޙ”þ¡ÑÙ¾¬§݄Ϧ×þu”³’ÓùÀpºÜ¼Çàà5¸õzù%¡´ÕÚ@åç³Bñ©×¾¶U×rò²²¬¯¨Ô]MŽd4VÐ~…ºß‚l¢$8KcåŽ\AôªÿU³:ŽQêf·W‰[¿E`ÚëHãsO Šï4¢òÿª?ã]Ôµ˜?X‹­¬CYÔÙ{ãš>—õ†«Óq²±s1ٓ‰k/¢Ñ,¶²Ò<ˆJ—,²Jåô×ØEqô‹¦~`Øá´i$êïÖ+ØëCd¾±½ÀvÝÇäXNÜG¸mP‘`ží; W¸ûˆ!£Ì§â’Žk^_I¯WWk'ĵÇc¾é”¥¯OôGÀsðQwóƒúDžàI) +ÏЍäü•>£Áþ»*ðt\ý7ûÜr¼?üaÿâÛ3ù®jþ¯ómþsÏÅr‰#ѐ%wÓwþ‡/%ìÿâÏþAÆþ©þkèrœþRñ4)}Óªʹ¼ý*þ?Í·þŠÌ·é¿žOú%ãÉ&±OwÖßù¿/ë|Ò^H’ÿÙ +endstream +endobj + +149 0 obj +<> +stream +H‰ì×=’ܸpPP‰¦„ ¨DAÎ&˜u_AGP¨`KD—‚ }ŘhaT9p +—,Òÿ÷²ìéùД×nni¶šù#ññÞÃÔ ñª2cGµíÌP†Ò °Œ"ƒþõ4)!8"›±6@õY¼eòÉëÛ¼ÁHJτEµ í¬¹-tÈx|ŽÉ¡þ4T?͐K æ{ÑkÌeP=¤Ý%o' ÈÊrÆjþå÷ Cýt M²x€U%$ÑÃâ1¶EßÓlu*=s´E6*¡!»N«›턴½L Ó¡×džê×ßþuñ€-ô"@Ý\=vl„†#ô¶„èß_’ã«ÓÐÎúb(I7îmñ†þþ ›ô㉠dêÛë +¤ÓscŽÂh/ª3”f‰¯}JÏùG@ÉD1_jÐPŠvëåP˜‘±ÂsCހ¡ìIïNCÝ]PȌºÌ[4õ ¨{0ÄÅr‹ƒ-œÕ+w@»ç R T¦“ ä…‰bù¯Ü@} ½}$櫼-l ›“P:ÆéK3Ô%P™·èt1±·Á`(D‡I$mòGhj‹Lγu´@Ã\=è|¬'¼P' 2ÊÓÌ¡7wB1ûº´$(t7s›Ë˜•jîÍp +¢q‰U•I+Ü*,×o÷…lôD7 –WsöPÿ˝ €Ø@Ã*,ǧû@®4owƨ¹õž1@]y'û +“}¢å&Á(è^Tî]…§”yo[2äÕ h +oü²) wݗw.¡{‰¶žÒ¤mÊà›@£PÜP¢Ûm +%‘Â}Ø@ËÕ¥&c»³H® Õµ€ô²+ ûe(ï\¾w)xצOA.ŽŠÂŒ;_d?o¡ø%bèŐíÆv£Õ¥EÐç͝± +Y#$AÓIȆ¦¶ó*×2Ûm” M×Rw†ø»ËZÓú6áší½òk³ÚînbY³n%éQcã–'L*kA®sk%GP’*PrH{2Ž +Ïlg׫¥eÝv/ûl­×j×¢²éêÍrW é +¾~­L †Š=Õ~6%†šá¿õZ UJtQºÙ(€²"˜ FÍ ‡c1]@¢…–ÓmøB‚Zê´vv*ë<ÒãCÌ æuÒ”n4DúvñiÿN"D‚$6¾Î)ŸBc}k’²&ƒº*bùF“oˆè¡† [g7󫜶e#ܱę ê鯦%á0©}€tƒX¼F¹ôåMa˜¯ítܹ¸ä¨bpxƒn)óŽG˜š–¡eh ÷Õ †º-„´ší‡v<¿‹…*=Í6¼ ]1! +BÐAoàŸ ÐA[A} ;»zNIö½òuHÈki\ ô}ÕçžÊ™ƒù$GmÓ@wLô&˜VtÉð¥4سIvš|/µž¢Pjñ`†ð#ºã¢œyhX†zWz4hhFm ŽA©I|äÕMË$ßáˆJ:ô±;öª‹ƒ©56Òeª!x ²KƒÅ{‹þÐ'D˜±aڋÍI¯ºX¢òòÀiz“Ó !Ôø—n1•rš tço)„ë£x'©¸ØZɅ‡(“_€ºQ¹×øìõº÷ ÉòÜk’þ¿4 „ãE|y Ño/ú±u¯qý;Cí1Üâ£b&¹ £" ¶Ûƒì;@Æ~³»dh[´ Íg€|c?t#â~ˆã †ål#iR%û‹nœ²[ßz¼ø(ø`š››6” €¾ºÁ¼#(¬K!#)QI·Ý´®õݢݶV_«Â×dĵˆû!^1 41$ÓÊÅ] æ­ùªœr¨‘júÐ,!ª´"Cþ qâ|HPå2ɱÑ"ó€< ½ LWC•'C­ý… ÷‚ƒ– ©ïð%¨N&é…ùØÔ!TGÇ¿"2ðlA93*a(jS` ¾ m¸¤±a{B«ZàÔÔbð®\‡šo2 +ÛÎ#IO‰ Ö~ëq[夯·a¤¥»’ˆ¦&Ö×|ïÊJˆ‚!C}OCŽ ‘!Á]'C[«´PÔÏ(S hˆ +±;¬›*„ÐrõBÄÜa°òXUÍ ښ޲ָI"-u\X&M%b€®ËJˆ M^žIµ†b>oŠnÛêÞpePRƒÐ!Rc5ú’\û¬¿’߈¡Ö‚¶@Ë ”<¨NÔÍ[ˆ+@LóY/Sû!辡An2È*CËæ8}&Ze-ߘo „ ¡™¡O]QрùH-P?µ¦Å`c@šÆ¾J¡&:ô•º½’˜ä€DaÕ =<÷FtKX;Bº&–z˜r9MÕB™^R½G£Û¥¯õ9ôRŠ^K!Tjâ¸Xê¶=¨%Hêc½ÌcÛqFÇ5u#?Çý R¯_ˆA·¹ïx‡æþðšë©¤o"D¡L zÐ'd™‡>Š+„OîFŠå“Òjz‹ÔXŸ}òn¡R·¼õ¬@¦%È3”E KÐÀùÒ,½á䈾Že“vB¼žR4A]€²õ: +þD*¢õg¹l V Mp Ô&Ð4äPïRÙ-š?‘!Ó.c½BÈ£=¥î~ÑÅó=@‚!,i'D~dJk’Ê0zt€ÚÂú4ù=Ñ7šR9D±Ã½/«:~9@%œ±Í#ˆb ¢V7gU17à^N»š¢¬ï~-³\²¤¸ËF=|ƒÜƒ¨PV ŠU!,=æ¤E5©;Þ ÝV¡°‹ÜB´9¹á°QQ0‰Wܳ¨P;^Æ}¸¼f•Bt9@þ塞ށ0Íe(€ ˆÐp|åʱBý +]’Shށ0D¢ A3yb¨Uù)¨Ï!ÄçÉ]u˜o)HÐ[âÃôÏå=NC–BqbßR91¶(˜c°ªAßhí5ó¦±Ë‚ü4”¹&ȟ†0†´a¤ñP ƲÌÈ!½Bú«Â0`ßB𷤎Wûâªs}߯/Šÿ§™¶-ú+=*2‰áÅ\tet˜]GU  Ê#âFåÐT‡þ]B“ ý*2†h‚•·„…BBçßJ¯wUhy&BHx 䄨ƒø.º<µtòX  6jJ÷´Q£¾Ù„!Z ñ@ސݞ ñ† ÖI†h‡ËÁS”Eq +!窹¥6ÔFH•@Ô7]Ù6I8T‹Îò>%¬8÷ZÌ”à6C”e6Ê}ÍC ™êñ«Þ_5€Z^÷[¨xîá>S+4 Sò”;K|S}èrŸ{¤i7ҍ÷Ì¶wO@]„Œ P‹ý¢ú OYÄ1l›»Pyo5áïffš³=Õ w&ä +Mu€ºó ¿YÒ§ !…ª4I8 íMšý›#D‘–>š<B™ñ¹©mãqŸ!©ßì²Sf³ÿøIýÐ$kЩ—zBhwy< +j+ÐÎò˜î³l΁öÖᣠ±íÔg„ÔšûŸù´s<êîÝ'´ý! {ëòÀ&â=ì%à™º@)4Ÿí•{ÿ]¹@=nïu€ÿ×ÐÞ&ã¬ãûÿôÛsA¿?4_ gúg‚ìº@ϹᙠÿhçLè Ž ôZ4jѨE£Z4²,0-wúÍ +endstream +endobj + +150 0 obj +<> +stream +H‰ì—MŽÜ¸Ç©Èpea˜³Ì°æ^z1ù(>‚—^)|Ü W¡ákr²!`‚Êû )Rzªêò”wE4ººUäOäûø¿ÇeyŒÇxŒÇxŒÇèFš,?èË÷§û±¾ +ÏÜx7¼UËލ•RÓî©îÅwÀßoö~||µ{*<ú΁x5o&õîNøH|½ã»;ñ½æíw¾ß¡mÔ֛a¼×þ-¢ý֛át¯ý‚¸u°¿Ÿsk›a^ûûð“"ŽÕýc7݉Ùòn#—w㎜° P;߉ï7jÏwâgäM€š#þ$1¿0¬æOÕãÌ"Çg-¹©²Ù){œ:à[Ql/ 3ç…s÷x8à“N·ð3Æu‹ð¥Ù,¶·¨Ö÷üQÞ¿#þ-¥­Ìõº}O2ŸÌ¿¯FÇ#~ßO!_šN•Ú(-}'óK4ÇΨá´Xi6ÅN¼ÁG|-ò£ÒøqCo‘ +6uiyÀ÷lù]5º™Ñ$ñƒã&ٟÂ_¶ü/?Ozz!Ÿ_ÑÙ²í7a¶=-ßðÓL7ð“ÄŸE>p)l¾…?îù FŸ%þ …a¹årðxÿÌüøô8àŸ©Œ˜ånütğ—m0_ŸËf:¾¹#?Hü&1h¤Â77ò+Êy™æÂŸà3>ÿeÃw2ŸËOå?Ý¿_Oürd®—õÀ·Ù>á ü\ØÿÈKS'-;>—g—û˜'äW–ZXÒñcá÷õ,+ÔLµþwqØÌÿ3ó³I?—0õӉè3ßö_ +#–&àOÔ²¥š”•øE´ìº~ºÒ$}+|ùç ü8s?pUÿSÓ¥2ß3ßÑzàûaèë4?]­_¾éñ¸3ÌýW(~‘¿0?^­¿¶9aÏ'ξ½Êûù7…™or]4Uj{`RÜÒßòkù¿“2ØÕ¸¹cٌÐ^˜ŸÓ|+òYyšþ*ˆ½–m[l&e—ñ¶wüÔòÛ=;ñ:£Ú0 X>«ét|ׇÔLwæaR /ócÃOíîŒt›i7ùùšºòûmÅ&`¹®ÿoK¦Ëâçkêâ—k|£Êé#Ùmsq[úä]n¤ò5äˆù› +Vþzÿ5‚L®¦:Y·kY±ºGøÏW‹€ðÆa ´b0ÿ¼ã÷wBàoÊoYžWó ?ŒëÅÓ“šÞ%8=ö¯ôSÜòÏ+®³:~ÅnRb¤#[_ws|å¿\W;…²Ú§„íF2ÈÒrí¥ÁO›ò¸òõ: øðü¯Ä)ó½- ×>øKá›: bäø¦ã»Þü)ß8ۤ󓟺INâÃm +±É59–¸5½ù±Vá«­Z·¼çç½ÖvK΅Í?¨Üˆ4zïè)¤MÇw¿Îí¤†ïê4Ø(Fem cnDúPÇsQH«FÉýdßv³þÚ[™ûì•Ï-ßgâk˜ôjµð?v§lù¼&u|ħJ›:aP˜Á˜Ä•¿uïÂޅÐñՆ_Œî55?°hLƒùuÀ öG™¿u/„6·ÄãÒòwÄ')´PdÒh_˜d(•6î-y“þ끕zÓðñoôY$¾Õø +äOÄwÛzœ+ ì·å”FuÛ¦¼øà ЉO£{6àiàÄ9'íÆ½¥—/üoDy5R‹\+måƒ.Á=äŠz{òàÈ|C"á5µ”fÛY^ +û%¾køk›WrØ?X؂‘í›È"š[5ü´uo¹+ؙ㇠ÿ÷΋µiùP‹ð\î'H…ÑaúÎÈå˜w…ê€ÔKä‘¿¨×(%^A¨ŽŠùŠ”éŒK6øÂ‡L'¾mø~å—]ÿ…Ò™¯¨0e>.õOä¿@¾“ù–*I€¤Œä¼`áÒ]xîø ñÿAY4kÖ&ÊÓÎ)áÑ­(º¾ðq’ݵә¯ò 2óßÿTö_“ùܾ"ßb͂Þ:;œ¨ŸÆƒ|óRäGªØ‘ƒ XñÖG%…gá™o–?ð'ª_Š}ª×Ï‰?P7‡|ã »ð\ùÜ2|2?­|pSTxÃIêcmÉÚýóˆø#'ys×¼Ÿ¾ù ùAä{5>Fqk‹OwêÖð½f¬ƒ=½Áæ¸\DSgG²‡uŒO£‡XT6£øãÂíŀ|ØÓüQâÛªáû±ðµ>ùÆ |þcHÌÿüÑíøØ˜×èöÂw'Åf±ZP·†Ïvá{âçlŒ_Q. +ßjÅ=ƒÑûâ(ðÑo`Ӏüœí¿w|8 ÚcMj&Å?ð…ð\ù¬1çå|P,æ«ê†µ;zåC5H°JKá™ë#ðÑ.P<ñGb±d¬y€|v¿òḊ_"„gÃÇùiÄۙS§ˆü,4 F•?¢‰à-”³xCëPÂáKvÅw)ýCQ£ {"¾UùÏáȘ =ßibg>¤ˆ"™@U³òñ¼øË©š`yx–‹ŒR-œ› +_cèê¿©™ød³4 †Xà俙‘OuÊlùœCåC0ß>’þÙçi ÌÑ â?C9ý„’NüDø×â€ø'Êf0¡Q/ðŽõrˆBø—˜ÐNhvõaBW~èùq¤\(’€Ÿ¨JâÕAMþyXêÕm°šøÏà•¨1ßnøú /]Ä?EšòËH]‘ÂË'~(|Ê0Bƒâ†9â{*Ÿð°gå„ð_;Ý‘?@#Lüøø¾¿A3dW~™ï‰·!§PÝA…œþð­f>ÊðFÏW,3![8õz`mZJ_SmÁ¨FŸX¸ì½“ù\×µd…ùŠhÀ¶ï±E†2ßònGUþÈQ„ÕÑòN·ü÷ä3ЫÀDÐH"ßÂ|‘Oc˟ŠäCð2Ra£e¥ôZìGr0¥^Út–Møgô¨P†GWø ½ +µ$0*|H5)½ûIéÜ3)JqMMvƒ  z£ +'Wwö™j ò'âž¶.©âd/'âƒ2Ц°w +rP†>• Œ¡8?CðENJ_h˜6™aIv€­÷{¬'N{]çSo[Ý •_3uÇg=V@lñ5ÈÇ~/©Ÿ5©á“<øˆ%â)¥ê…|–¨z +ë+òéœÚx¬wɆWþÙ"9Çü,a"‘͍ŠÙnˆ©V3‡åaÚ@°éXDyÀ‹Ò_ +ŒÌÏF¥V +ù'37|jg‰tÄÇèð%§Ä©¼ÎsJnÕ¡$kbY±ã¡œò“užGþsˆ©Ž/µQ—øÔWUvåtý/AÚÍX]çµØåoÖmøôY‹ˆŸê—1óSÇ·b \á{%ì*RÚMƒ‹™¬DG^á'iU"þØ\0ÜÄÁ.ñ§KüõŽÛŽÊ/_ډ‚ê;øâàf±y9LWrxñåÍäaÎ\@MA‚VHŽú«üzJûR¸÷»øöÌæ®³•*™?KOíÁìü-kÄ{È9ágñ­ùÿË ’oùs“øTVvÆf­(^©k næ/ME ‡æùKü* bš_þ*Õ,ʒö„eêúÒwóãtOønþõ œñ÷o?ŒÏã¿?˜ÿñññññqqü_€éC7Œ +endstream +endobj + +151 0 obj +<> +stream +H‰ì—»näȆ«!À GLíÀ¨`§›0\väñlä10öb€UHN¼À`ø ΍…Ü óÔr/ÌPÝî@lLm•ëœ*Þ)©n”Cÿ`Zd“uêëºü甔¯zÕ«^õªW½êU¯ú¿Óç¿Óo®^b ‚Œôã—&’u“¶_nðž½”܍‰z*z°¼vOŒ +Wϟ‹¥'…µ~ênñNd©¥EmÞãϼÔ6vX â;-k··CT»ŽAñ<ÓÉIâÚä`?óþâ>xügm¨O+¯Ÿã¤Âs-MvXù¶ä‹îMcñ8&Ú°€ÆEbF» ‹M&÷^`ZõB“þ‹ c&lAù¼bìª 1R.0dANÑê@cDDŒSî‰Ø3Ú²=°HŒ¢môÈCÆã…Û¥ÑB)ñ¢‰˜g3X1VÔáçI´PQG_ʯ£EмÅã™ìûXŒbmñšF +Ô(֒ÝÅ Ó)Ö&»ôAql‘³Q†!“Q1‚ŒÅd·1‚ŒT'‚Ð1&Šà@< 1UÏ>†SÌ(BY¶Ä“1L6 ‡˜S°cbN¡5B1£Ðc™µ +\þ ­}¼ü³(s +[þ‹­}¸ü—ñ}­š4>Ç¢˜SH¡wŠF1£Òx¹M©Ä©Û<Ĭü­lÉM)C¬lÉM©$ߖ‹nJ`e‹nJ¥CêÙ°ŠI1#ï¹Ì"BÌêÚ³‹ 1'ÏC‰H£RÌõx5[¬|íä7—K»…ôݗK»…ôË¥ÝôÞ§Q9ú»„¼æ27ËhSyÕ>Ìü-—d»toÒÚX¶d¸uoÒÚX ³º”wxœIZ”K;èëè¦ëa­- +&?9·hmì=€}Ÿâµ#јP²¹ø3€ý'Åk.9aYÅÂòYd7Í‚ýK·ç²~K Bé3 ³r>øæÍÅ·p1¼æò(3ApÈ +琳r®0Ø,§ ÷ƒh;BÒ`µc®Lü`,7`U÷¬ˆB&¨Ûû1h°l +&¿$ŽAg嘕:°¿¥2i9ÆDr sÌJ]ú.•W|LÞìh0˜ã"ëê×ï˜dÜø-—û˜ü7a²s\dS`u v‚U;B‹$ŒÌm‘u…5£’ (ǤÙ“„}˜¥ÜÙ]–ËôЂñt¦TÐ 0·EVv`™[ ¬ˆ˜x·ª×ÜJŸ¼+Eº#¿0`ÛÆ`é­¬ÀÜYl+Òj·jÀ®gÀŽ8ÃÞrZd¬»ªÀ^I¦N\=°Áj)ò0§EÖÛ Õ¿Î5ž¸&`ê!E°W¥æâd]—ìÄÕÄj0Xãë6]õÁ2l³ö8õH§EÖ;˜þÀ® ÿ‘Êá쌱Õ+̋ Ӊë ˜ª×ìØlVóHŸûW{Ç!ÆÏưºyX¬/äÁ>á‘Å€„õ~–]oÌL­ ¯ £Œ7EzÂÂX¼K¤ü+‚Qu›mW0lç&¾—¢3`:‰_+è¿à}&ÁÝÁìW—bk.ãžÕ” Àþ©ªkñuóí f– V¨ìWL&pò¿Î†`e±’â·êæ#îÛìÆ̾î%åÔÜi°RýïƒÉDŠß7ú.¤Ö<)(—–²~±W'‚>ÃGŽ_ïÕ¿˜ŠúÈ6e<~²³õ G°êýˆ…G»P{"Ñ`i»ávécø£O½ÁjÆd»Áju"Ijuº“;°ÇO¶…Ïì 1Éÿ?û `PXæjÔ.äë,Jm_•Ú] Lüvç©“êv%{>`÷Pß2XÅ,‚¤Ã;‘¥Ãú€Iíï‰ÃDÀª6—ía¶´nØ´©¥ÃÎжz?;µ`—˜:LMtž~¬hÀ€[0 +`&n>mfy´´M]3:Š`èLÊ» ˜òײ]"t,]ŒC!`—ê3å Xã¤ÁælÞ²† S«\ƒ}0‘š9ºíÍŽ[̬1•ÙÁ*˜ÛöÀsñs«ÐŸƒÀj짦–˜íF±ë;¼ÞρeV¡-7ï£Î'—:,«¼I<—s`峁Éͅ»ÕgY=ˆ¦w +S:öp»ª? +˜\+°Ã^î5X×jôL ’¦]²ù! +xOð*ëÕRƒÕ¤Ÿ4íö›¥?©ÔüU•R1¸9JíµG<ç½ X#˜]Œrp=Øé!°Þ\ù°9‚ݍÀ²ØM G Z U+KÛÉ«FwÛF ö)«ÄÛ o°Ì lXÝ+´ùã`ƕH ²z °JÝ÷Áò ÷»™\<ªaÝS)¿jÀv°T}]AÑ}á 6%|TÃò¢R‹ÀÀNwjl²LeÏZnÍÛa`¥O[#Xñt`çg–•¬3ؚ$S_–c°Ãr`Ã_¯Ìƒf +¬6Or)÷í{§ ˜ É –] è v™7Յ«Ú÷ö0N.ú­O£²3 ,}B°tFÁâÆ`r7³;Y‚ ^SGl¬œ€Õú´`?, VS}JƒåX»”Îú4ڏƒèƬݪϕ÷°| AÛÚ,·ä$  ptz¯NƒC°l¬ßK\°A0M¡Àvï謄r +6h½4˜„#W +³”õÀÎ<Ø×mk»Åo 6Øâ-˜XÉ9°C:ö5iÝÛõh 6për`ہm9€Ñ!\ì~fב+ØÃ9Gªæ8%OÉW04‹kG.ëCþøƒì `탡Y$Aöb–ï•=ȏ÷G«aȺey‰¾÷Ü`åàñéô?æËç7r¤ŠãÕêCïJh}à4BÂÜöŒFBŒQâ/àoXqXnh51Äå ¢\@‹Æü üé¨WdWZÅÛ8ô_q%b“?Þ«²Û?;ñïÊW‰ËõlW}ºêÕ«W¶YÛ)?k—Utóï|ÛI0aÁu®C°–+² Xxÿã lïCÇr0aÊX†`´RPëá} \ßd¯á*XWÁ`%‰Ø¿qÁ¢v¯Åë&°Ë[Œ VJ©îsšÀ"風צ³^«–Zƒµ|Ý©LX)X ÑJ˜m6Ëvi.‰·ãM`À`{ž= Ìv•í,ÜOyí»¨êyíÁÜv`YÕ¦WbjfGó]™€™å†ÚåF¤"lA®‘b€ÉÁŒuÙ!¦ P¬ ô[Úe“¤Ýïd`ɪ†SK03½ ˜¸T¾L?zíۃmUš&çNÈÆ©{æ1^x÷­b°¼kÝ]Ë ”y‹09lœH0C‚mزð +9fáÇ·ß¼ZFX©„1£ FŽ`s¸£p˜œ*07YR!zR iÖz»'lu‹©ÓP!T`ŸËæ¬Â/ùqûµÖ: —Jò‹h´i’"¶e^vúsÉÁ¡&x‰¦˜Û rôGGlIë.9ý‚8ä&·—a"»Ê¿îÐ[ûx¡¥®->T ñBýš8¤ÓîóS +/Ñtk/R°lÑqôdZ1ÅQ¬‹ä¥£g± )Ñ[:€µ©lÆ,*×4ØFLœ¢Ç +,[í¢ V¸}H]⅒§À¶t’:+ñLîO ¬Ù ¬S¼(ñÑÙÍûÉ*£¦x¨À²f·åºtæô‹hà"ff`¸ibSVl ‡Áë +æöKdµiûÛ¥`kôu_¥44ª…,¹Ó´k¼¨ +Á|¹ñ X9˜Bˆ¬RrÑ ¬k¼¨ +çnGâ°ír°”æʧ„. wŽM`‡-¢ÀfydÛg?w[î }:eßì RDpc9ƒ›¥´¥².Ót+†™Þ`àà!çê+Ú¿C8ÏÛî—õ€úUCÁÎqà)Ü p)Œ„i~-g£@Óm¡mGÛ⠏b‚Áf¡ÌÂÓUØ/ëá˒äáރdʃÜ„iÖSs;5:tYÊ6ø%­c…:)X…ÆéÔh÷m¼¡ •t_… +ÌO dÜZ÷뽍eˤ{—¥0ò7h:ö´Œ%ýÁ?s•UúXNÓiG‚RN>DÏHÁR‡—á‚Þèê4c,ËÄTQËÏ/ S×e6Ʋaæ8õ–6±Õ(õ†‡6à1”Aw6rH°P¬,ó¯Æà6aX°È Ðˍb¡Å ²ÐÍ'›‚È$y{†“Ã1$‘©R½`èM–o†}ž£P4h`xœ.i¸Ú§Ë †XÄÇ¡hjzÐׯF¢hòñ”û 0{,ŠE ïý­0Fè+‰ÆÀûE½S³I ¢¸wz0é€!XßCδq,Ì~_Ú֘5Å1ôs²À£®xßÓW#ƒT‡à™=¾ëõQ!XÒ#EÓ¥©Â[\÷VçÏÆ8_ݯÐï³{Æè UXçß/¦ö|HÁ¼Ž1k +”²üºv –c|Žš|—®¢Kµ{oû]¤Àº„ËÍ †ºrdáYm?æáבEët1šÃñI.WeËóN2×,hçÑ̜Œ¤"ÇReÒªKfLRÕk+½ ZtÊ&ߺsýùpç=Øí<,U†cÆîíٞq¼žî#ƌãoz³rÁ³R-`Ʊí™k¦§åê±<Ãc³°T0$hHû|˜b—ÕcµA ê¦ÉUÃ쁱³Bõ¿ŒÍîs%¿l°Ùù¨!%›þàÑ á6šÿÂ2Í?‰Jâí{Ê5+L ÀÑÖõýŠ/tQÌuQlé&8¢÷–n‚#z¯à˜ÝÇôN7À1]ë8¦otÓV7À1=Z°³‡_Ñ£sÝÇ4ç¶“ ÝG”º Ž(1uÔuGÁ5S4hOai¦¨+ú)]!ØþcºFº1êò¿4!ÏÇ%÷G&0ŸÎ©¤ÕÍ7:°÷oÏ:Rœ¼Yi¤ÕÍ;¡»f9¸Ä³‰çÀ¨?ËV7ÿ±5!ÜÕv?Á¶–¤Òê¦f¦ëK·S]LÙ/A#Ž­nª5ÓÝ´h„1ö,<ƒv c2ҚÆŠ®¦âóŒÒêfçnA£½XB‰ß'q¼husô¥ýš§ÏýòxQë¦÷Ëïg‹G°PÌÒH­›ñ[þ"X•ŸHmVwNoÀžÀƒFÈjÓ®Üò +,u¥8ÜPð¤ëºNƒ‰³$¨€H„£Nç'Hâë÷Ç-ßòY­ƒFa0µcé¾ÔA#°~1n;vLfÔA#°þxܚê(wɌ:ª,;NpÜv_˜Üv +¬Mf€q•›ˆæÜQˆK`„v,Â…€GgMQMO9£tÜ)®¿ FúýE€ñdJUºÐ?”^ã¶ûtì$©¤^ã¶û$ÒÁèrR¼‘qÛE°Áõr§§ù=Ÿ”Aã¶û$üâCáŠÇ}œN6² rÛE°¾( +£ìÁ|A¥%ó~(ܖíãü™ÿåðùsÛE°cWüÌp€¨JV^Dߏ`¾ê9NÅ÷B+{9›•]ÀµÇ- ÀüÉÁ<Ä ´”Qå¶û °:?“è2Ný$4¥‹e}WpÓ ñõq7²r\ 0ët¾î ñuó0±Z*/`òî­ÀÚ¨–[É[9,äߨs0˜]óI´‘)ˆýN®Ø_f`€$…° õüNyÜVmÙ¿ÁÒ?"Wрñ Ý«kÉAŸSñs°ô¿Ì×1ÙÁ@2ÒR1½“§“ Ø?{Ý:§;‚ ¯¬“:‰*D<Ç ÿDÛҁÅT9ˆÿ©@/ËQÅ€°ýÎì©cÙ‚­—jÇäTN²¿`Sñ`‘¥¢‹9üá`¼Ia‘“û‘Ïl—ÁžàÌl')—g¼†€V7¨¬‡Y`Å3 Yvæ4 X¥›×ÀÆE(-½OÐ×kÝ ·`ËÁdäþœŸ|1X¾ ›JÕÜά"êµn^ËLFLêEb…` ³ ÁìÅëI§›‘%VÆ1‹„%P—ÉʝiS'ûR°»H– 4Ý}jQç˜E‚(c)ÖsÛ l¾qÎäŒkIãV`Ø ®Š–±c°Ïx?Z&½—°¶û žÅD‘ï~40§¤NƃfÁ곯oÏZ¯!pJêdf“ß—p„#™¿kb0>ë~Q\Œ£MÂéҗj,¼Á*yì‹`h“pôðq á+´ãSó>Þ¤=¼ù"°ÝÎå†ë‚áMÂ5™;KæýMÁð&áë +¡ `"Ãy‡Xw– >uQ½ÏVãMJ +ÿFÌÀPÈ2•X› oÎãC©Û§WÁ VeۄLšTº{ì¤}¡níž^,q2†wü"ØAÿφ þãò"˜ªG‘¬J+ê[ü|•‚½À§ªhl•.òêÒ¦ur»ìÊK`êÉæ ÷ò9 ˜ÏQÅ@ D¦.ˆv¬RïÒlÌÄû˜`ÆT@ E6€Ö +ôŸ&ÌV«ˆæB c*°†"Ò°zqkiLµŠXÕìÄðÊ DªW”|Û.«1ÃES?"2tôÙÖæ ÁºAþyT0³}(2ßúEƒ¯Úõ¿µ3òO£‚™íCŠ7Þà^Â'¾ ËÀ®°`uO+X=¯Žû÷É‚¡èõI¼¥„ `”á"ƒ‘Ô ²ÔÜ Xš8!i¡âQþ+šã«á‡'²Áðã üV©ÉuÈ3ñؐŸHd…3Ö d‚n}*ô(4Y‘â؅ðMö %v X-[¸GÔ­Ï@­mÙ*Ã1ðK1+HÙªVËMŸ”»À…m²be*øJØ¥ŽfYêÁÄÆ‚5gXÜcX¯­8cÙ‹–Õ¹+%–£¢ðð꟏fteªz<0›¼Þ(×PëõA°KÕ<ò¿t²ýö—ö1XgfÀo÷R§)ӊÃ)•­ùª÷“ñ—¼%šEÂc`/Gû!…5(xômZ8çWコˆÉÓX0^r|á«1·Ä3f‘ŽÁ`¨áЍ`T¸vqSÄÌYP#uÁ¨.â¦7H(f÷PdÃ-d>‚Ñî£Z0/¨Þ™ +IšZP¢¥}ï½=ÊØA`6«¢È¨^Š´ïѶΡŒfïm(2ªQŒ^è;”ƺ˜ªDF v®°¡Jv‹JfŒ?-ÞK%5¯0ÿ²30k}péºdȦ׸h›WâÇgúòEé}³0¯uôÜ;cÆ0£Mœ4ui +fv|0`ŸL;…O>ð2$ÌkΛD¼@0›Épn—À·éÀº$.L5f礴¿¦c(Ìk¶µí8±qžûlx0»DKwK)ނØ*.Ü%mX0?“mIs¼†g4z{ŸÏÌf\ºæÌÃwïœQ¢p&°`ö‡"$'Ìk˜`›]œ¸ÍFfÞ ß(²S˜A0°Ûèõ(/†Û\zh±0ÝîWiT*ÒÍvn›0žÀGô"~v•îÅkœÛL\dÊžq°^­²¨PGÀÌ^¢'“Á Z%‰†­Û°Od-vÛ#»PÈÆmاÞÝË{²…ˆ†x÷ÂiÈñÑܕ aäð°4ØÛg΄r|4™ªR‰Î{KÄÛ§bl”㣵±9X)ÆFé ˜äŸ3}w'ÇFé ‡‘sUÅØ(ã0&°VÅXKg`Tlì^ìPŒ^Ô­›t«Ä I ˃ÝÙ»‡F"k$X[ÁnQx`$²½d{ÿ›J¥¦áïN2‚å¦á‘?ÜH&°ïW•º2ƒ7tyÎößç÷ãÝÒ«3ØÏ’ ìßÇÁÀ“u¢ä{y?J˃ÛQT%¶²%±4XÿµÚ™Æƒ?¦Sìž ì?Ì.Tëé»ã;ÿZ/K·žÓ)öŠ ì_ç¯4OódL×Ʉ ì Ö¨¶žSë^°Ä+Õ©C' ‚Ý67ÕË.ÜîXûŽÀrh„`ÍïÁþüJõl3‡USÝq€uÅAÏy8ìæÀ†Kå>g‹r©î+«jL²!˜|`}¶°íK17¹`€ýãy:7¹@0,òKƒ©t£ç¼¿zžÌM.Á.=ÿᯇcم“ F°m +`y `E8¹`Ãù5ؙþ^…“§ü`ßk°þi8¹äûð–ØoÃÉ%;Ø·¿ÿ ¾$;Øû¯Áà‹`@rƒ=”þ—$ HLû|`²­¬š«˜ÁÞi°vÒÁ7XwùLƒ•úZäÇG¤º‡G°¼û‚Õ"øÈ Vª_ŸŸk¾½œ z5•- Eó‹þÅ+Hëtá]I¢ç +7›%Öj•ù`X•$I¡Ü»l¯Ÿf>ÚX‰W:xªf~i‰hVç™1Ñ}¢\°ry&ŠKH +.V%‰TBq¥1Œ,‘I &ç±!0™4`T–xB` 2«±nî7 +®Ë%Ô!È»‚/A\‡`[8‹ºXWƒ!˜Y‰Ku­Ô†ƒh‚i‘•¸T×Jå @&ääì]i0Xªk°²|!'`7,Qg“-Ê X•¸Tä1Ø¢œ€ui9”ò†ÇF9Íï뻌3ÌC°›»&UƙÆÀq…`õ¶g +À8ÓØXó«ÛBXÉÀcã0ë×Wä1$0µNÈcˆåqÆhgÀþ”‚ÇÈ9ÝØ> +stream +ÿØÿîAdobed€ÿۄ    +  $$''$$53335;;;;;;;;;;  %% ## ((%%((22022;;;;;;;;;;ÿÀ86"ÿÄ? +  +  3!1AQa"q2‘¡±B#$RÁb34r‚ÑC%’Sðáñcs5¢²ƒ&D“TdE£t6ÒUâeò³„ÃÓuãóF'”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö7GWgw‡—§·Ç×ç÷5!1AQaq"2‘¡±B#ÁRÑð3$bár‚’CScs4ñ%¢²ƒ&5ÂÒD“T£dEU6teâò³„ÃÓuãóF”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö'7GWgw‡—§·ÇÿÚ ?àXôVØ©5å–)ᖴ-\˜oPÙÝ*¢¥ô”·Ä+» ‰íH [œ´ðñZƇ8jPª¨4nw)íËôÛêø±C9'¿EeœòýSݐʴïà©]q°ù*îµÎ%ÄêRa%E.lÌð@¦<¯x‰Ô.êùVRF­û•ƞƴ„%‚2‰#uñÍ(Öºvs“€§caÒ8V0±Å¯×Žê´1Êsà¶%Fgj¶¬&p[néµYQ±œUøÖRý®v*\œ´ñÑù‡ƒ3Æz`NÖíaÖÒÆ4Dʳvš ÍY]6ó™:Bèpìm­ÚyZwÈm[5ãÜ¡!­Ý÷rjŸìx[8Þ”;ä¬3 „î(–ÔØ d + ‘ £d÷jº½TÒ_nÝ +‹ni<¤F‹c1~tuŸ¤"âºJ«ë†òyXùØnÇtö)ЕèVäR LD¡¿Uøt)ÝH­s’ãGu떐VíõhÉ`‚´úuÚm)™GVnV{Äù´º…;áØê³™o¤c²ßêu3p\îCâ<2"Cp¿€J;‰zƒe™á<rec‡t*ëmq¬Od±å»Cñ¨ìwz‘aôÜß>·TÖ<º-’~!^ê õ1œ<N“ú³€N:ëÃ_Pño÷<ù”Fb»• Ù²ç·À¢ã^Öû_¯‚‚!ïºIÇ[֝6’[¥ ׳l„dêD«¤ÄNíA² ´tz5sœçyþE«ÔÏèñYÝ ùÂóæ®ç¸\¡—ÎPäGÙ£Ê]a/pó)±ÎëÓ⥖ÀË w)±éðPú½Ðï(†Ÿºêká¾eBë@Ð*·äGŠ·)ƒֆ9Ì~Qû«×å1œ‚u]wJǧpqܐxYÿWÙëdz¤hÕ¥Ö/Š¿êQ E~–¥±!ÅÄ{-eÎ$–´J>+Îy(Xe»Év²®´µÖ{{#Š7ë'½,ϐ߷[Ö®¾5‚Œ_2©_~àOޙö’ÐÞÁQÏ»e[GÒvŸ$tˆ”Ïš¤uŒ#ä?Š¿Õ¹Ïí0¬¶*¸³Þ w’l&F>*ÖEyÆ%(ÞÑ71İy­œF¶Š·žy+"ˆ o’³vQ,ÛÃG*I‚@ 8æ#)KÄÒ<ì’÷—xèXíÀ*¯»Ô°Ç@¬RD#*‡Dg‰¡)|ÇùRvžÃ•µÓú{YW«x’îÅWé?ÔwÚ-ÁôAî¯æ^OèÙۘL™èc2‘Û©=¿µÌÌ¥®ÈãðyòV¾ÇF17s +uÒ(a¹ÿKÍgçeºçGdÑg@ÎE&§¢-´¨E­EL¥‹.§ìvåv—YyšŸ;#Á‹D5)å÷:4W)¥µ ÎQ¬²–ù¨ºÂó.ã°Q3Û»;¬.i<Ë$ŸQÄ­<¬¶±°«"Ñ©äž~1­±ç¡¹s/q}„•:±Ÿf @ñ*Þ7N}¯Üá3Ùk7 +ª«÷¦p &zÙÙp³¤ÞéôyËh}fޅµÇ²Û~'ªèÛ ~(©º´Q8bN’«[Ç1Llú£†S+u†<®T #²ýu¤’I5 6µÇ*cpä-faÕS<\«ÜʁÚ9î™.NQˆ&@Ì£šŒ‰XAHÞ@VÙN×ëÀC¢²]¹½•‹Zö°ø•c—Ä8ˆ²5bË;é}[t¥k‰(¯cÇÒ +,®]ª¯˜äÉ*"™qp@^åÕ´² MH1‰ŽN̒—2SJsŽÑª¶Ìf–)Æ |Âм´³§V‘ÉH܆¸wn-ÑO§XÚÞZí'EÑcáWeO¼¬Ìޕ²Ííð@aÈ'Œê7«!”Œ g\'¨èéb=Žg§¨dàÀ‘àSt‚æú‡ø­ÇÔlj +YDH”#Å׫ʿ ­v捎 +Î5έàñâ¶²:uOl·éwYva9Ž„c AV°Ä‚%Mwv1î`*v¸²èsêQÎH#”Þñëuª ߢJ -p<ð®dØ>k6æ¸{Ûې¤ˆÒ‹÷¸õÝÕÀÍ-pi*þ]mÈ ÷0¹ªr=ñ]OÈWæ™8Ѱˌñ +õG'žÉ­ÕXAS©Ûà->­ˆ ¼ +„ÕnÒ¤Œ¬_ò¶)â~ï¨xÅÚÈé-n ±ŽÜø’;,ì[œÇAúL0VæÞ¦0iÖ4+©cœlPiçà™LK$àâˆFÓòZúËOuž FáÁЫ> ÊeNZ'JPÏ' r$Ê+ntB5/' 0~ªöÒú,5¼C‚¤HêEܸô:õvz=ÛvŸ2µ—5Í{'²çzuÞàÈW§tÎ¥[€âˆ>“òʇI_Û«—Ô™¶ùýáùIZLKZï³Iªs'†gƋ6ñÆüӛNÈCkõÑ1á³îQO!⎬‚7¸ÙèúU¾•mqîòr=FB£Œb†üÜñ°Ÿ´„v>M)eÜxÈ~.=Ž&Ç®¥rå\»Ü~*ÍЦ);½‰-ž +Á…Öñà‚çï2âT±ëõ.k|õM‘3•Ò)ØÑz~ƒO£ˆ^y!Wëo&²;ÇåW©skÆkBÆë’ ~q’±·èXtˆREþeË­å§E£‹£ <•›X—a×ìpÌF̎ˆœ á@Ö»·ŸkX$•—‘iºÒO…3k­½Ô]VÁ%,¹ ÅGå«näz¶1ÊÄ»ºw½|3…D¸¢ÒvÉ(C6Эå<ŒÏ["›þ¨k@ +¶FIpÚ + íqø!N¨äÎN‘Ñl1F&÷=ÓP%môžžü›£*—LÁ~EÓÚº™« 1š5O1…u*˜â k¯Šù9 ǬU^‘¦ˆÀßßR³ìÉßl¸èšüÂ[µ¦<$ýUù ÏrØÍÏ;G²Ì{ä’{ Ù|>ôª—*HÀ,¹$FÜ1LÑ%^«!•0Ê  'ܔ£e§ÇCêt~Ò]«Žˆvæ@Ê¦^c”2âý÷¡Áö.÷¸võH²¶×½Ð5*Æ'OsÎ÷÷Œw -V·AÙ6s­"»>"g=|] ¥š PY°£¸îeWPóQ lô]í®†IÑeeä‡Iá¡K'!÷;ËÁ tûn÷’襌@ԝXrNDðDêGìr2mõ^Hs3Vdh8!S*¾PxîSá;mö)$’Q©;²ÎÓz¨ç8»ŸŠ­'AÂpt•Lòɹٱ aznèaÞÆr=·1ÐÈï(µØUŒ\á¡ròÞ®;Ù¿pi®¥R{Ë~(ÂÀZ@º Ñfv8£¡Ù`8¨ê9ÎyÕŸiÁD1M¢\`ñ[á :¸ÎjÓ«>Óî<,\W9šÌƒÙládÁ„èxZœDÄI¥ÄNQ?¥¨bÇۋdˆôòjŸSYŒÛDÂ(²“,û“I×bË`(úƒ]ø†»77…§i5Á(EáÃÍ=. t Iê"E±žç²ÍNˆï©—7p÷@y}ə{«0Ž¥‹HìÂê A|Ö]¯sBß{EõHåd?œ€Çè å:2ÐÚÙDÊc£L¼“ª²1Øê ‡ÒZÎéØ£Ò`Ñ£»Êư]‰aªÏ—š ñmÑB7ý\œªßK÷·è÷òZ=7.6ºt<¡ÜÑ`>jŽ3ýMG蓢Qþ¬ÿ˜ÈýaøÅêm{l¯ið\öuE¤–ò +¶2^Âߤ{¢!V; å²îþM¾‘ƒùÂ~jßS©·PO$,.ŸycË{°ÏÉlý¥¯lá4D‡òîÊ$+„ÿwéÑçŸafêÝËSU‘¹»O*}R­¯8+<>¢ÉžXçDéû +£ŒLm¶þaìz¡´<tY?Y±öä š4w?5gêý³Y#ò#uº½lBG!:^«¼?ÞHˆâíû4y¼[MvƒØè­]“f±çwÁNǓ©Pcæ qšèWOL…îÛÊ´YJÎi’Œ-ŽÊ a™YärJ2ðdÂŽÔÈð†Ï¤ˆNˆmúA24Wc¾:Œ»e!»"k:ªî°ìCvŠä¹’=> qŠ7ÄÙüX\Ž×C +«¿Ý¢1vŠ®)Ä[]–%ZéMÝkŸÙ¢¹jtÆí£wrTœ·¯7÷A,yªµêC¦ëœtìOS|ØÖø ûÕýË#.Íù#ŽµÌ Þ 5ðŸrgú ŸàªôÈotê¤] „O$¨Sj²;uÛ±Ò§vFñ …Xr¥ÉQŒÓ1BÈqÆí»K)4É#påG—ä^Ú«'ð@ݤÒý^éâ–}¦Ñî:…fáBÔ{µæ$,^²>Ÿ%ò~¯†ã´€Ñ.B-xdä–rÖ÷]7PÎ!…­åÚ|–U@0—w&J² $8ˆØèÄO „Aԍ|<],c^%ùÅVÈË}„ë¢ ísŽ¥VÈÈŽuN«”‘,ƒHCR‘öµ¼•K#4¸íg*½¹vƒ„:À'U ù‹<0Ó¹] 5FGˆþ º¤‰r!ÈŽUg]´@As‹Š^ÿhj{¯ž8ÈÙÖº7›”]Ýdª™Ý†9t3HÐÞØå‡]P ¦—Úuá^ÄÄ}΢⧃ӝdhßÊ·±ñÛ[CX#ÍK)փRÆ >s鈨#£¼zõåû+i.r~ ÷RíLÊÏ/6"¢¢wlDˆ‹í+òÜïkW…e¦^‹C*f¦%JÞ¡]m†jQò +»ÜýW…Cw>4Urú“ )5Nü»®:˜ ªÅ}‡Ȭ2–Еí~I€ L—ìéÌkw8æVå25#æP³Ãi;”‰‰–‚üÒ DH2Û]?7صŒ†ö™Óä’;Üß´VÙ×_ÈRNà…ì>oÙ³ønÍðÝWØóÁLLB‹B;@XÀ[¨M ‡#V×§´)5£U$1€nF©ŠY,TEÚÄÙL:•[¸Çe á™DËÀ FB;zŠFµ9‡ê€yy€¤÷!°ÝÛžçFî5Ò{-*ì±ËkZ@ñGÃËxv×j¼y†˜ä=Œ û‘Ö´!ëñîhG®Ð¬,l£Y +ãsÛ¦©ÆÓî‚ œª!²ÎVhË{ä-µ1íåef€^KSà:ʏO›u¹{GˆS¬j®3B­ e‘ÜpR1H‘6zõµY;xá>C[kw7é“^K›¡áX¯3n„èšbz.vïÔ7ñ¯=ùLÌjóiŽ,oÑ+h”×ë=¬ñFê•þ˜;Ä ã?e­> +¡€Žs |¦_fâØÎN)`-<˜]gNéXáVXsìh.'ÍsV5áÜ`ÃSýQùÃÆG„VÛ5£Å08‰»yΔ[ŸsvÖ×{VnM'í‡^à®§?ٜóâü;Õ¿¤´ù(yœPøÀõX7æÍ‡$ŽNtÔW’jˆ>Ô¬0՝IŸWџj?Vxtú¹ ¯³Ž«k©ôŠèÆ NŠ—D¬;0ú+s¬ºqÈðò+\¼·¨ŽþÆ,¦äMŸESÉ9§E©E¢¬p`qs€ìŒëÐ<\¾AŽ^œðãá‰óm;+ÚuTÜâäž\Áz6R˘ÎQ½£ªq〉:hÉ΄2e':Jˆ* ψ²Â!tµ[} œÊ}š3óeRËÁôò½w*O»O„KMkÓ×]–{ðºéߦŒúN¾à÷}t]CîeU +7­ì!=ג qÐ+øðˆÄG¶þmc—S3×o.kl6XICuo'äªYZIJ¬ì‡)òÍ +ޘ† JFS5}íÛ²ÚÆéÊÏ}Ž{¤”+-.<ÊL$ªy9“’\#fÌyqŽíg ‹¡MûE`Ž{ jL•CÁ@n¾âÔê°¸¤1*@ÄÊÌaðWñÛuƒÜa¾ +Ç(bnÇ=X9 lÅ8Ï{àhÑÝl`a6D ?*=;œзq+ª†ySðÇ¡¿u‚G&µ§@ÙÇÇ h‚YL¥¤«dõ°@0±²s ŽÔÀðB1'oµêÈo÷`ååº÷êt +¿¨GXÒ¤¥Ž\įALͯwtšÒãR™­$ù­<,2`„ˆ ˜Á:É6 qü-z(ª¶Žèo5PÉqYÏêE²¨ì¢ÖE”Ã¡áñu­È®±ÊÂê7¶ë%½­Ë±òIÑgäfDµ€ÏŠp>©c<$cõ¹k’~Ô ë) î;·wñICÇÿK‰š‡áMP“\'HM0³®›ui‹ÐœâSI)’2%B .¤ IŒ.0cDÁDBDX€ƒ(ƒD£WzmaÖëÛUHLÁZ* ë ˜à}ênR7”Ú±s&±‘ßOµß§¥ q…“%ݖuý+ѳvØñ…Óã±µÔÖ°È+*eƒQ_â×]|ÚÆ#P==,~חˆòH> §™Óâ\ÁªÌ{ ÁÑM$²‰d,#ƒ dP] *î¿i‚R&1Ô蘉KbÊâ[.o(˜¹aãk´pUhr%u0V_Ä(øŒ§é7՛„Ž-ÐÝ'Á éT¨Ìü×|”ßfß{Sã8Ê6Œ|«¿ØÛWp²àìqøŠ2AÖTٖ„I†H ±ÜXëOYUÛ~ ÜDÈ:‘Fl°uìQ™š…0IJDÆüöu«µ mwrßXº'ÙÞr±‡è_«š?4­·¼‡ƒíå‹êË­ÔX%¤DÙÀJ4u’ÁéÈLt—æð th¬â󱄯£õ¾”ü ‚Z&§ + Çj©ÀË@OÄ6ä#’>­çݲÄ,ȱ¡Á î$O‚M~æÂšy¸¸±¥¨a„8}c¦ˆñ,5ä5ËZËÀ"‹ûlV_asG’o+›Û„âz]Ìb”eáö¦êÞÀGeB³G..dTHr‹™ŸA1Ô~L˜" ;7¨quՃ®¡w$ÅT. ““_Åvg zLð±‚FP¿3y6‡Zx®ö¼þsBçrCòoâ<çÖn¡®–I ;œïRO×Èè+ˆÖú1Á9N;؎»jåä±õ×ˆ>j :+ýn=FGšÎiT3ÄC,¢:WäÛÄx  ñüݞ€?Hçù€´z›÷VáàÒ³º9ÛQ=åXÌyuîH…¡†5Š'ú¶ÔɐqÊ=åN ~’Óè8çæ»ù¶êâ³CKLv_WqF& µÂõO—…È’4Ž¿^ŒÒÐï/ɧõ“ +„¹‚è·¾³än“ßý«œÕ.n^°?ªœ¨Ÿ6|«=?ÙY `'USUÕ}^Äm4úÏãªg/ŽvG¦:ŸØe‘Do->K­c™‡†ÚY¦Ñ¯Åsý&SîwmZ]G$ít•ŠëC[§+HT}RójJ<@Æ:*üÛÆÖŽê–VX'kxA>½Œ.h;GuUî!CŸ™":D‹ê¬8ñqߢG<IA}‚i%D…By ›‘€‰SN¨¥àp…ÂZòš . Ù—xR~Š‹[¸«1¡>3?µlä# ß²\jG.Z4j«P:®ŠìeAÎañæµpDF?š²G^¥= +ŒŽQ,Îy4YÌc~*¹Ìuƒ£|”ñƒ©² 2WËÂ<[y9nn§ÅTõ\ã$©]}E  J UÙk¡ƒO̒ sa¢0VÝV¦³k¶¢ßU´Žeª¶úvqÊÓ-m•$™Š–3­IvIÊ3=.7š³hp‡!_K«>EPtPqË õaž‰Õ-„¼(îP3£¸¨e”ÙÒ¯VHâo³¡…™°ŠÞ}§ƒà´wH•Ï]£-á»IÓ²µËs úgÓbÁŸ†°;ïnöHÒÑwwCÁÆyÂɪÂÖèS³LcÍvHÄ%“†Çˆ~ z­¢Ë (—¸¹Aœ¬üÓãÉ)wn⏠{:X7 +© ®Ÿ¡àÖü1•sCi;¸w\ºÛS9y 5è˜ÇÆ®†ñ[C~à¯`ÈN0:DW›K>0rP<2™Ün^¿ÓèvN=•47s½Àa^sÅt²± hU³¬œºÚx¬OÌ _‘¸¢7Zo©H…ÙºÐ}7púí»ïhøŸ½f´+y¯Þ÷NƒAòU +Îæ å‘ñ¯³FÞæÇŽ¿n­Œs~@Ÿ¢ÝJéÛx®¡[{,Ž™O¥FóôŸ¯ÉY}íkK‰Ð+ü¶ @8½E¥›=ä":פ5ú–N¡€êu+4ÛÍJË=W¹ç¹U^è*Ÿ1˜™\O§§“g1^¯›¯šgdÜt/1à„çJŒ•6ScÁpi-˜P9nL¿zŒ{Gðb )r£¢Rš”kc^Rh!îD©¥ä$§ H éd”Œµ:I¿$ ø=¡ÍuœþEÑU}xõ\h´1aàˆ2.ݚr™œ ¡ß»,.‰Œí÷8}È=k;´ô†ë§h@Íë’LÉ\ÆVe™6ù£€—(Æ;þˆþ]cÄdN·ß´oó,,qs‹¸Ì(K‰Ñ!¹Ú+TÐÑßr©K,´Ò÷-›@•cbºÓ.áiÖÆV W×[a ùÿº¯ÀcŽzž­){³ðÐ^ód7Ù:7R³ÎSœu(õôø§C$  ™â™d<º6„€$ÉSð‡Ó¯º7hÑðR™ØÑdqˆÈñ_ÑÈ4ÝAƒ08*Ö>l ¶jk9Àn÷)íÕ¥C b&Zè[RׄÄYßÁÚ»£×›¹Õ‚ÀÊé÷bZ[hÐwZT×f†qØ­‹7Qªä'-N½¤GÊ+¼W‹°èa +O c©t{1Éucs|3¥®ð!PÏ FVv;³k¢cCq¸;¤un ܓlj˜\HƒÂA ÉL±wÝWՊŸà:UœL£S¶»è|•\vÖ]0?Úã)a’pv7b–8Șìí:æÄƒ2†2„Æ‹6»Ü[¶Sn>*ßÞÁ€×û´A"W"ÚËÏxåj}YËÔÆ'Ÿpk¶¸s¨Dļâå×{x_L9Ÿý-%üY@?EìþÕé¸,¬ý82ÆçT=—}0;;Åm]¡¶7Pá!ìveâ?ãG š|ãåkœOàùácœã·¶ª.áXÌÆ~>Cé~ŽiŠ­=–lãÂH;‚Þ‰°سÇvÛZ|֎FA‘¬ÆhàQ­°©°æ0Ç :Å“âz»aחTZU)€¦×’ +L˔τž‚—c‡ ø©ÆRjI#»õk]Ô÷}†ïšê²l$v\ï@sqè6ð¯[›3 S„G…ý­)J"fG¹ KÞÖ½ÆIòJ§‘–Sˆ:­›”KËAÐNë h–^f0Œu Uø¬† NBR؛¤$™“ÝN†z–´vB%úcwu›ŽŒÅív[Ó5Nù ©‚¶s +†NC‹v¡>âçL ¹ÒuÕXÏÍJwé˜pòÂNû¨8ö)Œ’Z]#¤ÙŸp‘OНJr‹4æ ,³èî£h$m¤}'.Ÿ¨ÑÓð:y©Œh-ueŽ£UT44jW)׺“²-54ûG?ܯ $tëûdzP‰åŸ«KÚ=‡RäØéy#‚xP:èˆÆ%JºK'…C†R?ÞnXɍ4:àZøxÌ«SôihhÚÑ.> c§ôkò}¾Ê֏-†ÇÖ_“K™œ¤8o„!„;óYSeîùwZÝO£Çy\]ÖØã2'DìüÀÇ@²M üÖ`Ã)2ô€Ÿ’\¼Û20p„¦FևJÍ3”¤e#e¼!DŽh´€e;¯1 +¯¨Jv‡9?ߖÀ-€Ü¦ÜHR®·Øa¢TèÇ.Ü-:jemöUœ<¼²T¦h5ófŽ=",ãà5°lÔø-|Jh`Ъ1–;F´•cì9B²÷J·ÃŠ`Lˆ$|´nž¢ÊÄ5SÈêYÞ¨w%H7ōÕîÎdð +ñ)ëçæ—£cÌ"1ìcTNYD!­èõÒՄѭ‡ä­6Ìz 6c¯±ÝÔC‰)HîVÇ.1¤F®Ï®Ý³Ù%˜,¦BI¼“Ý—g;§VÁ@=Ï%X³ +«GùŽVEÝOx-kJ‘é†@!< ¢ƒN5Äkrq³l¨§ä¶qzW ¶hP.é4Z7T`ù,ûp²qÌāÜ"De±¢´NpÓ$lwíõw즻µR¹¶SÆ¡Q£¨ÝV„ÈóGQeÕ ¢¤cW /µ—<5ÚI‚V§ìŒ* l‡þø:¬; ‹‡ucª>“±ÇsâñZ eoioéù˜G{=õøâ†s£^VÍõÚÍ ù!dtœ|€\Ó²ÓÜpš$ Ô(Œ«P6qnx´ÉUÒÃ#…w+ 'æË{¨ºÞÄ}êAæ³s¤QÛS/sNÒ­¿Ü4U-¹Wæ7ñmâÒ#qæÄµÿJ9WúP{ãîà"ª>öšöª'‘ÈQqˆIâp“ðñ{VSfF { î ê]49Î-^?g¤õ—¶*°ñÛû–ݍÆÌ¯ÜžމF¤8¡%„O‹Š¤:<Øæ8´ˆ*2àAÕ:S«qs}ñX—·nÂ¡›—8õ»BÚŜdÒª]B Ê ²DH(’…2àD„Ñ0q*A ÃÑàD*ÓÝM®„ød1+gvÝêºg©‹é8û«ã൛|kÜ.G¥dšï€~—®ì§vZ8ýq c1éù5¾²c6ïÖ+ð5Íì'UÓ½åà‡j +ÀˬÑs˜8&GÁWç0ÕdÿþÅü¾hȘýaûZìä)ÜfÁÕH™…Jô¦Íj—›.pem.qЬæôLü:…×VZÃÝtßV0h«¹[A¶Ã£hðZyÔ7+ +ìnw°Äþ÷!^+oÔ}GðiKšÉïpÆ€o#·Ñóe60¹Á¾%'4µÄ¨(ø€:Ð|5U!)ˆž¦›r•D‘ÙØc›UmdðP»)­cˆæ o%ƁËÓªÑÉÍF ˆ¶hC—$‹&ZëÙ ì.y$¨9Ò£:¦Yd“©t@­Ssô„>””¼¦N8D¦—XðÖ÷HHRTHÏD¸8nɰ>‘]n#ªÃ 1šÕgôì'Ãi¡›ŸÉ?ĕ§•Òl£Ûêo{ç4ÞKSŽ4~i|Çö9Óæ£)Ýø@.®oTꕘ>ãôBæœKœIԞU¬«k˝ÏxÀ级Ìä㘈ùc·ñmáDÈüÇùRõÖF¥\ÅÆ²ç{DåK ØCŸ£<Ű0:üǾåÛ6ãP$@T3:¯¨ÒÆèu…U{þŽžeUvSÜdºJ„æÅ©âòٗ†rØpøK®Þd&7±¼¸,‘u‡º_¤qîSþó´mˆ`1ÐÎN‘Êi0ÝSµ®~®:xF¢æ˜" +¸ËˆB—ø†º,͋†¸uóÝ;Z8‰U¾ÒÉóR =ӎ»¸îX:nHIUõ›I$ÞËïÇìj;wgƒ-ЮžÜ {„³B|º]¬Õ£pB±Ë ü–š¨ÕΦ״CõóW*Ésx20ðEÖì·Ú*ͽ u‘àqؔ08€®.ƒø"¯¨XÃ*ý=J›FÛ9óYaåSô˜cÅU7¹‡Ü@Â'PS Ò>“üÝüŒ!ddãÛI;L¥Nyÿ‘S~C¬íRëaà&ë„øèæÙ’öx!Cí *õ•²Á¨ß†Zeœ&ÏÝÄØíÕv9Bøh[w)àîiáncu’0|×%Žëp`1& +Ô;€–P…dÑt¦qš«‰Ö»=O«]¬Û`iñY]K¢2ƛ(Þ*¦'S->½¾õ¯Vh‰AKÕpB^¨ý%uPãÉ嶺ü¬\|¶ÏгÅs¹Ý9Լȏ0›(ñÃþ)ýŒ‘É(žƒ}¤å–SÌS²Zv¡:J«!Ãt5e܀K @‡¦Upö8ÿ±¡À©€F¥ Y¤ƒN¡SÄ+}z=]4eç0¹¥¡€ÀݤŸ²ºŸL²·Q›øg¡õ³OêöŸi஋ÔÇÊ«m 9…\2ŒãD\KTC,e`Omß:²—48CØNuWêï´Ûˆw7«ÌKêi{™¼ª99R ÆÌ[æÐè|thAƒÙ2›È$•…Y°ÉŒ{†‚@M0ž»œÀ@à¤à¡T+~¨föèÊ»KäjÍy}!ÿÝ`Ìj«FÝ¡Xåsœdއó`æ0Æ`>^ݝ¶yU³ˆ¹‚Á©ÐTƒ´ŽÅK<þà”%Õdq1òi§U'·kÒ,pԍ:ø6ìiâ÷ ÀtLƒ§üâ­ aÒ³>¯ZÑØÒ~ƒœ?V͍iÕjCXƒàDpߛÇuzEK"¾y-øB9ØÒ{•¡õ‘­=CÕìö‰¢Í'mj”½f{kö¶.á:éö,ç¡=Ó¢iÕDò¡$–@Ùe0DB„•&Çt¢%EO„ÐIIK±¤˜ñ]Béäàm¬}+ðTºGMvUÍÝ£'R»š™C+nŒ`à+ܦ.ÖK¯ËüZ<æI˜˜bGo ZŠ)Ç«mcmœ9ÇÍe}`êÍÇÆ4Ö}ÏЬçõ2²é†´.+;-ùwºÇ'O‚—˜ÍÃïòÚÅÊrb&çëÈ~ivƒ\îs§¹WpñÚ=ÏU˜CF©ÎDp©a”#.9ú¼ÙDä8c îìý¢¶ˆ7u1§n¥d¿!Ü"÷J±>|ÕB5âX!ÉGôÍø%Èɶ÷’ó>jRk T¥##dÙ-°Æ$£WQ<©6¦O(’|1õ–dòV‘Ô¤®¦%ÜֈjªmŽèn´öSûр¨þ %.ŸRÛ9D(;=ü©—8¦ÕG.k!ÚD.c×T¯Éµü¸¨o?4Íi&rÒÓBe)jI>l€  Í¼jSAåV&¶Ç¡!dZ'F…Щµ‡ U֚ä,VºÂQ˜Û®áæz53`¿Ò¯ßØoµԍ°ª¾-oiWUØj,Š½Ú™¥! ùjœãA.Ы Çyhi0Þ®UHs£@´)á±»UB]YÉ:Ú?‰q¾È#“>).‡ÓÅl RCŽ?º¿ÚŸï—*ŒÇ³ƒ#ÁhÓÔýϚÄôÜÞ8N-#”ùcfsHq~o@}'{›Ê¬úN‡E•E·¸År|‚%—Ü4±¤|Sx̞ìt#Ku«ÏªÏm€×tü,Hà°Ýa™•fÛ_. –™b–’вÊ褺£÷*/¢úLZíꤶ|Œ–ÛÙ’7BVƦcDzÝ?óª?Ù[¿W]n[¤O‡u-ÎýB€í±côè²z¬™#M<“_’ÛYµÂ| +Ö»/P6?÷›¢ËÌé9xใÕ`ðçîN ÙiÈF’àæYY$þ 2ðÈ“¯ƒAî +«ÆX$r›ž*Ã&;©ÂvhÉܤû%1¨ƒª¦af8‚Vð‘òJË:B×éýaՑ]‡C߲Šæ¤ÖžêLY§*Â̐Ít^Ö¾¢Ýƒi‘à³ó½ÁÛ¡v†.>{é†;ÜßÄ+ÎiÞêî9㠞*î W/¹§§~¡ÎËÄ}.0%½Šª¶h±¹7¶—ðó®–ß«]Ú=&³e€ikOºU\œ¬dL±HQèY¡Ì˜Ä ‘$÷@L [ªt<Žœço÷Õù¶>k$…Vp” HSfŒÅÄڈTZ˞è<%) Òâ-9x&RނҤ +6QA%š€à›Õ.Tâ4Qk’â:øî®§†ÏGвx¯¯¶â­Yyq™XÝ6í•<U‡fˆÑjà0ö I­?&†opÎ@G@wüQõŸulyäYO²@ +î]ž­.ò2³Ulƒ–㱵˂1#dø¯)’NÑ*»2¥ ‘RIJî­bc›žnåª÷º;-ž›e×7/<žÀx•?-ˆN`Ëå‹i˜ÄðüÔêtÊl6 +±ÛÀ÷Àx•¾Üz¶†™|òRÆÄ§[uî÷wq@ÉÎmU=î;XÑ&4ZR1í与c&‘—·Ž'Yu—w˜úÌ÷Uy¡–sþٹZê9nÊÉ}‡¹ÑS ¬¾b|Y ÀÐ}\1"÷:®^šS$¢d]Ä @ðI2“Z] RRÁ§àšx…c¥_qÞÖ®‹¦tJáÖûš±•É-O xïö0ϘˆÒ>³á·Úác`få8 +«$ç@¶°þ§Øèv]»Gîµo6ìLVC¨åõ²%µóâ­C—‡c3âÕɓ1ê1ÇùmÕÌë=©¤û£¹ð\ëq/°ÃOšè,±Ùu§tø«X¿f`÷)Ù9HJ‰ô×îÕ‹™°5ésp)è¹oˆW¨ú±cµ{¡l;:¦ˆhT²zãjÓpŸ©Lû¾(‹1w™e9$M’{@5óz>%[ƒ¥ÐOܰœ œ®fukre¢CO$òª°’«ç–9pƦæ"ƒ&8È\¥ô6T*ÔÅm ÄöF«Ë€NLj£edò©4[+U– D§§°DêUúz}}à+¸ãÀ5 Ã)‰ŠþMXGQšK»-&bc7•0üJõhŸîvÇ÷kÖču^ukJ%§"° ô•eÝF¶hTòrÍñØÈí¢ó۝šÙ‡¬ù™)!HI?€0{òfJƒ{­;º5ÍՎÜnFS Rt6¶D“êôø–çNίđ;»ø-†dáe6gÅq¶«:´5*zƒ˜}ÓñQJQâÜÄø³ˆÜ@50?wàõwt\KA5¤ø,܎’Íkvà…‹Ö,Ü< +Ó§¬µÚ?Dx¤³gÈ)ŽéÿzÒÁñY÷puª#£Õtþ°Û@kÌ?ÅlU—:çâÇ4‡pBÖé½^ÒEOàŸ ±™áÚGñT¢xncŠ#¯Pôý7 9¤Àeœ4\ÎKÊÄqüöv!tÕQ•cÀ Ü%¡Æ U2캲YsŸ†<[ý½X㠝¼nÃʗ¡P-JÖÉêßs}®ñ +ŽF)¦©'UL¢HvÉ £ˆ ©u ¾ê€Z¥[öò©Âdium©Àjé™:ʇªà|”Þ9BÛÛº°t?bcDj>…#.s\Ó «é`dÖó·B¹#S™¸*U\ú,¬Á +\YN3R„î:³€¸ïø¼¾ê®¨²ÀÓ¦«ê}0V÷YF¬[à®SÕYmrâC‡!ìàd B»8áž?T…PÓÉž˜Ñ؎„8§Í!ª·}l³ÜÍ‚ªZZ`ò³g[Ž…½ 1áN§†<8‰²‰Lš 츋Ñ=ä<ï<‚„h™§²n +$Ù³Õ@P¦æ+ˆkÚ{€˜¹ +‹ ‘⋵Rûžˆ€v¶# ‘°êÂ)±«[^´ºWF·éj¾—UacŒ¢Ð +Nsœâ\d•*Z睭R:ËÒ§@¡ Ôù”øÍs¡ŒI€»N‹N4m~¯wð\÷NÅ´Xï¦áß²Òû]‘´°a”`/sø4²dÇ)Èރóu23{JæºïSsÏ٘t¿â‹ŸŸèT\L½ßD.zǹÎ.q’u%GÍdàæ;øLãõ~€ÛÅEÐTK‰M¯)Dª ¥'”ᨌ-iIL©Åsȕ©‹F=<Á+<]M}¢;«xraÅ­qK¹ke†L‚µˆðw[kGIýHTßu€~UÏ». „ç¹ÆI•$ùèþŒ,øì³)1¼è~.µýYïќx•[íO&K¤ª;ŠmÎ*Íd'zð ׈ï}úº-ÎÛʓº¹Ù+4á/¾fª¾ˆû¶;²-±nvMÚ@ð!N¥@8!d(ŒÌÌ™2ˆŠ€HÚÚZÖwUýb—¬¤ŽHG£Ç3¹¶ã Wiµ€jV1¼§¼üØùÁ ¡k2ræbŽžNðȬ~pLz…Mÿ ÷,"ç‘Ê“Fš§}öΐûJÁËpr£¨þªÎ.üe†*¾ô±ó)øód‘”BÙcBOŒ‹£Y.L© ÔǏ‚:ºÑ˜© ù1ÙãD°ž þµ=ZD;E`åS`Ôˆê^èM½ÃºÛ cݱ¨º9â¾N€¬œœ +I<|B!´÷(n¸Jw”u,\Q½4?ÕiŒ‹4t7ÅYû5­lÕaøB‹²š·2¾æxaê¼’ Hé§æûmÔ¹¥¿ÊoõõØ=¶^܊žØ:‚³îk–h£É3 t˜üW@hnðû ´ëK¹2†ç´ò±Ùu­üâ¤rŸ”#Íã«¢K–™5Å~Aºìzîtp•Ýlk¢x•M™kƒFý©a6Y9lŸ0 àÏè'ÃûZ¶ã[Iƒ÷¦f.M.cd{r̓Tln ÚÚ+st€bÀfGèËîçùA—W=Í{ =¤5w¤Ý]YÏñ?<¬Œ{Dr|U$¸‚BB`$d9aF&%ô*³*±‚ÆF£”W2«˜ZàÓË]¨ù.§u{±œŒ³¸]>P«!Õ;^íVa“ˆ\MŽÇ£Z|®=«Û“Åçņ_D:»ÁçÓðrÁÌe•»ÒÉaað!uŒË ÁM13+Ù{CñRé_šÌx§ Y'ˆÐ¼¸úû5žÉ?öWê8iÝlõŽìWz¸ÎÜÏÝ+>ì·½†·7iîªË—Åê2°OË[6½ù’+O˜KpÐx€„ZàTîwº<÷©Kæ5Ñ·b/ªk2M€Ù +gDZ±-¹®s†ò…t!ñš”¯Õ±=Qc4Ü-¹Ì:)ú…ß3©I ƒ%2×WW§èW«ËÆ99NpkŒ1¬ÐüTúŸÕ Kð¬õ#üà;äîþ¯uêŒ9¿CÌx-¶e‘ô–ˆ†9c »F³ ’Ÿ”+gÏ/Æ»Ã]Ì5¼r!ͅÜõvcd²ÐOeÉæà¾—K}Ì<áVËÊÊ#Š>¨õîÌ –’;v-ü¤D%®̻LR™*J¡/k|HHvSÑáQ[1˜Ò9|åfuš«cë,hlƒ0µÛ£@ð +­jêþjs1‡îÕ}­õ‘ï+'ìr»©œØQYmåãIW:}'w¨t…Z¦ov¼pY°CtSòñBrÚ?›YiÃÝÑv[[ʝB#€¨¾ß«Ûivƒ…c':u`‡) ø~Õò/}ö8üÄwL¤6ª2‘‘$›%¶†€+pÛ00™$¾ä¤¦…&Ö÷p +JZJBIV©éÙð x•¥‹õrË~“´ò +Xòùe¯ òюYñ.Ïa«‰ ÃJì1¾¨bÄÚça^«êÿF«AG¨|ä©cÉd=C_/Ä0âÒVððb²í$ø U†t¼÷´¹¸öŽûJôpñèþc•ù€¯Èm¹ïp04hRDš@Á‰œ’NJD_ÌFŸnš>·°ÃÀ¨JÑ닲 ¬ý²ªe€„ÌAºtqÈÊ"DU±’””FÕ(ŒÆ'Í60”¶“(Ê S«lÀ{‘GIyï +QÊæ;@±Ë˜ÅåNz pÛ +÷ìw~úè–žø#÷\ãô¼b=Ód)ˆV¿bååKöF`×l§ÇXþ‡âNxåú_E[˜9V鱒«»§f7ó²§íp->YÇ9CxÐcŸ Ç ýލsO:¢Û ò¬2вU¨ÈH5'†P6 ù'ܒ®'F‚?\éّSÛæ¯âdaššÇ5¦º,³)ŸJ´dØÎk!Fc]i”dŒìp‚õ€zÓON°k[PŸÒ:m£èGÁs,ꥺn!Z§­¼¸0:O‚h'¤ú¨á†õ(þ.¿Vp_«\B©oÕ!Ë,G¯©Ýæñƒ«65ïR!Û#”ÿ«™>೯éy5•Ò¿©´ŽeR¿+ÔÑ#Š2c^Z+Šq6%~aæß{9iF·“·­scU^»)e²@Pϒ‡ï˜ù³Cš™¿Ouc±rCwznA!ãBí)긏`c«lj¤êºeÿš5L<œzH1kÇ1=tŒ¼±âšòŸÔ+¤Ìé˜?šù,»zKy­ß"™.O(’%ø*<Þ3ó=\×XIM¸…¡OH¶×ms¶ŽÊ*êI‚G—Í\F'öýŒƒ˜Ã` víö´Ãʱ‹›ma‚«º·´ê}ÁGÊÁ †IF2uY‡Õ«ÉhkŽÛ°rËt\c,{\ÓwZ4ur=¶‰þPW±sP–“ôžý¹0N:ÃÔ?nì§)Rã[ƒÇæêv[ò$BŒ8I2"W ¥HψSh÷Ó`{ 8j +ßÃë#"°×¶Aî¹÷’9Piƒ3 øsœg¼z„dÇÆ45.ïMn@‰s•¾•Ó©Ïc­¸î­¦‰ù®Tä ’V·B꾋t1ü5vÜfx@«ïù5'ÊʸŒþðéãMî©õAŽk®éïןIüƒ¿½r×Qm6:»ZXöèæ»B»Öf–é3*¯SÅÄÏ«ô ׎GÍE—–Ö>“ø3Brˆ¢xÇâñ£a·vUcùA/ÜWôëìïïOÓZ˜Ãá%V†9 Є… Ë9ƒŠR‰½ º²ú£7Ù\˜d­7,ž²âX G™¯bDë·æÑÂIÏŽ€þM,–T×[·Õ 0˜©V`¬©I5Ã}¢ +»l2ØQuˆn|¨‰™ª D]õdçʂI&®RI"±  ))ƒZJ+)ÉGÄÂÈ˳f; ÏsØ|JÜoÕg³¾Û¢Èú-~*lXe=kÓ݋&QH7.ÃW •7Á\Ç©’k&‹ ¡iLÜ͇B¬ãžf™* ¹ƒåÄ¢Éú×k¤T>k/#«ç_ ¸Áìf†£3lêš2dŸéu$B:p™yíö5½;žf$•6aäžV%‚4 +ÐòSGÔ̟&œäâxD@r«ÀÉýÕj®—–u_­à:JЫ>¦€ƒ!°'Ì NY ™äÊúNof«-éYÀjÉWÇX©ƒP“õ’–´†¸ñRqÈ €+%ŒHêx¿?°9ú´–åY§!¬åaäõg:Âkà÷ñBý«—Ü£—5†èËì/† Àh4éf=SsjSôɎ©wˆû”¿h^{¹3ÞÂHýŒµ˜oöÿcÔ?¨RA€%dß[.vâ;ÈY£6ãˑêÈqԕ.9ã:½Ø²Œ²¨ë¦í_‚=x›¾ˆBm"I„jòv SGÊׄ¬ÖB~¥'ØlŽ$¥öó¢I—6n ÇÚíéx÷U{hÃxú %e›~‹ŽEŸ¼€„ºb?4BL¾ŸÈ…@a×[ÃÙ¡︝\P_’НÂ7•iÔ±ÝØÆ òÙ°ÌË)fMf]Oí øð³ìËNÈÜ×4ˆL–XFÈݖ8¥(ÔϗVÍÙ`8únÓԝoxUFªa­J¯ï䖠ð¯#(î¿&ÇwB.%;›EΞY2Kô‰,°…èџ¨æê |«ê73“¸yªÎÝ¢ƒ‚‹ÞÉé$2ûPõ]OÚ²5n¿g8ŸiYìtr¥¿À)G9’µ#ìc<®;Ð}º·GP¹½Â‹ú•Ž`*Ž‘ô÷–~éûŠiæ³t%#—ÅÔŽ´8ʉppPÚáÈ!D’ 2$ÙfŒ@.à8jŽÒ©Ú@:«Ý?™Y-kŒ4‘2Œ g!¹Tæ" ;›wŽ8D÷%z=7 +Š…L¡„ œ”+úK»ùÌF6ßÉ +ß܍i;?cŸþ“…ëŽqøN¯¼•uØôŒah~½×C“õ7 à»ÇÒ{5Ðáýë7êçPÆ9¶0xHMŽ<˜ø¸±û–4£³7¿‹(‰ŒÌëc7&ÛG-¢Û}fÂe$ *´„̍ƒ~M˜˜(Šóbó ÝD8„Î2å6Tû4`˜å4MkÉY`I'Tð“ØæŸp„Ó()E&¸Ž‚œ )ÙéQÏ‹O¸}ë@ØïÊA‘ÈWjê7m s¸î¯`æâK±´¿‹W6 Ýã Çö:öØÈÚíAä*¸˜íf[¬op©œ’u.V:}ÀÚA<…0Ϗ&H +ØèXN)ã„ΦƮ“uÖºÖù£uÀh‹#=û¬.v@b1îB¹HsÜ=Aj¤’p×;P8Yn‚É$’JRp“Z\`+¸Ý1ö¼n0Îä'Cæj"ÖÏ$`.FSO¨ö¶¶—¸òÐî@¸Y“íoú6ÿ‡E †4ãñVŽ^Ñ Wñò¢#ÕS?ƒRy¥3¡àãýêF6a•44T:—YmL"džËÏë;e•Ïñì%×Yk‹œd”2çŒ4©~~å’RAaS‚)FŸ¶ÃsÞQ¼}©'¤ò¦Bӊx‚Üý§•ÆèQ9ùg›ÉV#Ÿ)ÞrûT0ãB?bG]kþ“Éø•qHF8&Y'Sk¨ ‚?MÉý7ø+5í?¡EUÁXÃÊûŸ¥L9ž ôìãlpì¦%núþàI¸´ô›î~ŸàÅxKN㲚Ë9 TtÌw †¡]Òêcdið*Ha1ª þ ÷7&$ Ú¡á1 r«`s[<íÞ{£ï¥P½4lzb9)(Ù´ê’uÜ;'ëÕÚ·êùªX×öZ7uñ Yö\Ðu<ò§9ä*»DޟÔ2$ÐÂà9)ÏÕÞ°íMcæå§ÓºµXÌ,&A3¡Z´õ6Ü ­¤Ç*‘ëk`{ší±Ðý0>¬uSËZ>jCê·RŸplxJê>Øîì)Îx´„ÏnŠ¿_ý_´šVó 0|J ¾®æ0˸]5MƒËâ©duá 2S¸bkÐ4ú.{Ê_‘p,é·ÃD'tÛ?x-K-.2Uk/c{ÉN8q‘êŠÃ–wú²e/&ƒð-±Bv-‘[³4q*÷KV;vI–øy¨Ž $ Û£4reˆU»ƒög4û¸Zý¹"̀Ðx<-·SÑÜ4kPŸ_Nh;@É,|¾8ÊÅýB²eÈc¨ÓûÎ°ÈÆpÑ¢;(â6€ëÅfZKGÅ õVùR˜‘©•~ q8èŸÍ×ɧ À’À&^#æ$î©K†¯?O'<8CNˆJx„}f2ðЭöägxâaåa©v-m0ҕv;÷0¡ºÂR‘¡ç•œe>,c†µß<53`÷z,¬.l2ÝG½mUԙceŽŸ.ë3êïJ¡õ›­`yø•¸zvǺ¦´ÿ'E¡xnuÅàОaðd7Ô^žhþÚ#Ul¬ÀZgYF·¤0ÿ7k‡ÇUBþ“”ÐK\ñÑ8D^ëŽX¤>ÐÇN腝“C;L|œº³«;_Y„jÿæ÷W}^®Á¨Òèw܆R$ xL¼QˆÆŽS£òƒ¥¸/¥Í>!J»¬ªv•c&›±Þk½Ž­ã³„*¯áeËÑ/I"¾×B'Ž:ѵÝkÎ©¨±Œ°9í;%Sš¹3ö—Há6͉]›êº†±­)-÷µçÚØncöLh ãÂ!È{™°ð‰—&G_€8@!Õ$å"W/¸£â<µÒ«"Ð`§@ԁìQ!q!¸ûK•<ƒ/G.…ZÓ.RfÉÅ£(l°Q á»Z†’…•IÚÒã;X\UšØŒx-”¸E²Æ nª×¦,¶Û³TÏÏsDª¿‡.,1 õjeÇ<ÕEØ~Eu6^àËÌêO¶YWµÏr¨¾û,2ó)¥C›œ”ì@pG¿Rˋ–£3ƈ%![ʐ"QÙ«Âv—tû )Ÿ‘û§î]mÁWi¢’‰Dòx‡Y&<ÆSÒ/q/o,?rªÁËOÜ»æãP0)·ÇZÇÜ£<¦>’úZÿ{ DOÕóáYðSø¯FoNÃhŸE®?¢üL'èüfæ1äo^+úk['Å!Ã!Gêcç£xNpì<ÚçtnéÅ4†Úޙ™K¤ÖK|F©Ñäñ eԌDºcGؗÍ(~•­å¤.‰ ÄH1ŽäeÈC¤ÈúZ!ÎÊF¸Úósš^SÙÂèÇMÆ´j÷ ÛÐ)wÑÓàTc–ÉèÈ?&_pHz±Ÿ¦®[:Ÿvu!ùÍù…;>¯ÚÝX~õŸu6Pý Ç/1rÔwÝaņGÒ8eÓô]!ÔëþPIý@9§l“ÚVX*Åøg2#aãKN 4•ù[JגèÔ¢WUä+´í#ÍL0Dkd°ž`‰PÚZ¡§aÑ%ohIK¢/'a¿íיظªÖ3+óšãò]˛ˆ0 ¾¬CùW˜Œ¿JCël‘GèÃòx‚-o-pù-Nµf;vÁ1Ým_^'Y]$èÑCðÌëÒQµÒ‘И]c&LúÂ.ˆ„_ÛMwç7ïT]AüÀ…f QíRpLt‰ûC¾†Rü jüö?RñðQ·8þiUíÆ à üÔ93厂}YcƒæG^ãDÏÌ´òä[aæSCæ›rƒ™´VšƒŠr3Èc[ 횣‡}i¦lyN`à™âJ ÉÑW—Nåš<$hQ™Þ~ôAÔ²G/•PÊm®=‘²§/µG3¼GØÚ~]¯åÄù ›^¢Ö?ÁÖO‘HÏ$÷$¨Gv#.qM‰)Þ LwL\è…ö;”˜ò*0ž!%=Mëb–Lx‚µëêÂÁ-rᤎjʶ³-q +ä9À~xýcüç‡È~’þ/pî #R¯4÷:x.^Ž®áÑ#Ǻ»V}}Áð:+0ž)ü²[Äú£ûCÐSe7^ãM[»Å]7³p÷n#Iì/ëCƒ ö2®QžÄáHa 4a#L‚R»Ѿþn¦n&uF»Øáâ<Á\‡Uú½“ŠK蛩H|GuÑýª;¤ršDL"cÔ>£vxƒ qʼÅá `¥ £ê}; —×ì°÷þ+Ì{(~ËÀö*–^^xõÞ?¼?kca-¦]¿‚Vû^Á.*OƲ·mpÕ-Ï¥ûë0|TêÌ-y}ƒq=ÊdF:©%{ô¥ÄÎìQ·[BZA‚‘¬TkÉÊo©ìaç²>{ðë!”Hæ8N®2Ÿቡ{Ÿ¢J”aÂI"ÍlâS¬Â$§i…"bäŸr‘*””£’¦I%%k€HÜP’JÔÉÖ8¨¤’JRI$’—˜à©¶ÒÒIIÆO’˜ÊUU$á’cªßn= +ó¶kà­3«Sù߂ƓÙ2š<æXé`ù†9òØç¸wÛÕqx’+zÕ âßÁsiå;ïÙ:Æ?ŠÑÊÄm)~N:õcü!û‘×XLú‹–Lç%ÖüW{חàöõÂxp?5f¾¬]ôŸµpá®<"5ù ú/#æ¤Ø;ã#û¥Œà#ô£/ïEïñïÆßêîqî{+~µ.¸¼á¹Ù¬áåLuŒÖé¿DáÍcñqcÉÊ›=?vnÿ]ˬ=ÖVÚ â±YÖÑ͟†Š¦F}׈yÓÁVÜ¢ËÎJÀÄj u²Ã–ަcSµ‡›¹_Y¯ÁÁXgYgï˜\óJ3 )Cœ™4DOÑ2À".2ó?Åßý±Yiÿ²3rÖK~ˆÐ6Çujº+nTǏ,L#Q½Ø›Ùôš~)V÷4šÌ}2‰ú¬ž(d£uâoU‘ô’T$m%%/¼;èû¯õÏg`äX8y*.Ê·»’³æòaU³ž\SøG›½)uáú['æÕ0ç‰Cve#‡J­f#AúJàÙkƒj‰Ð(ŒòÄéá« „$,ÊD};óÄh«¿,ž•?Us¬‚÷6°~e[gÕljàäZçyjŒÏ<»Gê¾#tˆ³å‰y×\çr¢7;è‚~®¹#¤ãǰ8ö*kF4kGÀ±4b‘Þ`\rÜûeûË7%ü0üôF¯¦ÝËÈ i×Uـ!>ÝÞJHòðÍËÌ­–IU\cåý®qéaÚ蝽2†«o±£“ +¦F`5<ãÃT£«žYza#]ú)ØxÍ…et´.É|x » žò ÉŸ"ѱN²3ü”û5ö<Ô=îäÂMm·»eL/wƒD«ôt.¥d3ÓÊçîUÉè ‡†ÌÆXñüÆ þ.qh˜ïâ¢Xàº<_«6\}ǎ]ÙlcýZ­£Õ¡<)G'/ҐZ±žr=>z<+(¶Ã iqò +Ó:VK„»ÛùWtü\:Yµ¬k@ìcæ>°âI“Ç~¢eø“ærՀ">ÒóÀý-T ÞÊÎf@. pÙ*,þÔ$cɇܘ‘!w¡¸ÎŠ%Å6âªÛe/­k4ÜQ+ͱ¦A ªÄÊiN&6‘Za¸þ'Tm€2õݏewÔ1΋”ÜUÌn¥m#kŽæøÊæpm—üaûZÙyyxÏø%Ü.B½•ÛYk ø*G8*¯˜·±’Á”¨„ã–Ïp…Ž×ÚþŠ´û·UH2Ó ?(ljq¿•½)Ö·c•[j´µœ ©XIvº§ª«.~ÊÆçx(ªG„nt”iNÃRÁ8)ßSë$Ƽ¹^\MyêˆiP~5N×hUY‘iʗÚìo,Ÿ‚›Š$xx„pLoçE'ØÙ>I!þÑg]> !Ǐ¼Uëþ¿Úó ù0²˜…Pyaáçæ‚ïŠŠ|ÁŽÑm¦<¸'RÖÙk¶²I<¶ºVu;,4™uÑetìºq_ês›ÝE€ë0eõd0?¢i{Œ{GÍ MÏsÜuQÈÙ$/ˆ z^‰Ôp°qÉpn÷r{£Ýõ’—˜a\˜$èˆéÑYÙ ”}Ï×À=®Z¥”»™Üòžï¬8ÍIq¬q8è™îètRj<7ïn&1˞#¶ºñpê]쿬u¸Ù+##ªÝl†€ÑøªN*1ÝA.k!Ðý_âÌ0Cyzð]ö=ÇR ŸD” «JIá4I€’”œ4‘*þ?H¹à:Ó°ÝÔ²0ê¤DñʘrÙxxÈá:1}ã*3B ˜›ˆABˆ°R]m–8—òuBNI<¦@’M“v’I$¥Á#á£„¡)‰±Aˆ;‡G +ª2l÷ëaªcSCÛéi¸jÕR»Ã,0|B{m‡sÎãâ¦9 qp;ù˜†) ¢Bg‚«…‹aYª íX#²6>A©ãv¡E‡ˆqmՒ|\'‡vÙ鎱²Ý +¥~5´:,i²è1mªæ0Èî<ìú/«kÀp#PUü¼®)DÐî O|Öx…€vêñ@Â3iiÕhu.…f+͕˨<øµSo¥I’©û&2©Šïòlœ OÍÚ¿6Vô»«§ÕcR%¡‘Õ µza±<Ÿž–qˆH{D‘Zù«»Â}Ú»Ò»3kÈFeƒº¬$(Á#fB[­pEd8ª ±Á¼ßB§†a´˜Ž"5޾‹)­Ñ!MÝ6‡‰âP±ò`NªûÙ FŒ#Ó4³äË \Iˆs,èǚÝò*¸95}&ñ £j c8Pääñ®L¸ùŒ´,‰y¼Œ8y)¸pWOoI¢ýKDø +¡‘õvæëQŸ"ªÏ”ÉbD¼´,ñæ"~`cø‡:¬Ûê:9]§«N–7æ+ð²h?¤¬ãÙØç͈Ցá$œX²k@øÅèhê 1µÄ¯Wžþú®Q{L‚´1ºˆÛGÍ[ÅÌ㞓ôËðaž,˜õ‡®=GW£oQlqªÇꗗVâyy€ŠËY`–G’O­–p§ák¨ X¥3*ð6FÅç÷TÚàV…Ý9º–ñà©¿ àûUËæÆl!àØqä}%VAW¨-v„,’-¬û‚³“´‰SrùÀ— ½>ly± ö!Ð8õmÔ$œ\ÒÂï•ڏ‡vë­û¿ØÝUë +_óc¿IÄüÕ§õ#Ûò »=Dz¯íƒúû@¾IÿMKº.CA?z¬p1ÑªÝ·—j`Nܶ7ƒ)ã0.Qˆú3)N9JG¿büLfŽ5A5Óà¬És¦ŽÔ3͈h">Æ|P©OÙ:!9ày©wVûLU\³5`o՚Zé]¹ÎwS¡µ‹³P9BÜT«e–;mm.qà%A긍ìÏ 8Hºm=¬¾ÂêÄ4pÂ]äµ°:6vÒ^̀ö<£;¦6¯¦5W¾îrDHú%#r ?~05ÇB‡WÑqà'n!:Vµ•VÁ T¬¶ Ùò°†¤Úøs`‘¢SX81IöZJ†s„tŠøÂR6n¼Ws‚zŒÊJ¹6ÏLIKT||[²¶¦—ç°ø­Z:;)‡]ïwà¥ÅËÏ&‡ï˜²f„7ÔöMX–Û¨<ª]e¦^âeA%&LÙ2|Ò'âÈb„>X׏U$’J5êI$¡%)$奦 +d”¤’ÝKÚ;$¦)ÃI‘'à¦S¡$FV^CGt†ª` M»š­_ÒÍté-úAScöó¨Ož)ã LUê²#0L ѦUUeŽÚѪ•²‡ì° •WuŽÝGÒòCµ·z„Zþò— BêVN’ýWάP~’Þ³'è§Òy‚æ`¥´¦‰Æ ÷tpX´<4Σ‰[Tæ7À½Ð×i ”· +™ºÃ£‰ù«Xù±˜¦ÍyòÆSŒÄȧ´¿7ô:!ÁÃð\f@h¹Û~Œè¦Ûì Ú`öQ,ÝÏ)™óG$b"µÕ“%";h…%7VáðPUÙT’I$¥'‚™LIK5ÎiеÔn¨ëî! §ôAOÇ9ÄÜ KgHT€#ÅÖ§ªÐýì>|+µd5ÂXàáåªæÝK‡¨µïaö’Ò­G˜Ó$oÄhZ畎øå^Pöä4¹_­ìpЂ_R˯ó·=UÊ:ãšFöǘRŽgº˜Ÿë¾ÖHï/îÿ­4S`‡4ÝPËú»ƒx.hôÝâÝLn»Y||U»:—©Q :žá>¸…i1ö„­ðñKÎftË1žCNöƒÈUÃ{¶çuÖyP8Õ?–‰M—%¸šðèÆ9ɍÕÏ®›¹®Ú|•†ß•X÷7xñîöm¼qàˆ¦†!¡#Ëo±&azÆü÷iÙÔÐ4S¢ö9Ìj¬æëâw`=‡ugä—ëbzLvب{RŽ ÆúŸâÚuUX5ª–ôáÍFE­ïf¬”ùc„Ǫ?ŏ&#¤¬}¡Ëôò}5-RVìsŽeB=¢uù•h]qKæàúU³û²«á'Öé‘n[»†¨:‹ãÝiü‹˜ILxK‹ñcõ~ҝëZËÉ*»œÉISÍvxiµ8­Óuž 49ÿ˜’«¯®é²+‡ÑNæ>=pÒîŽþ‘“k[Cwç²çW±ìú¯«TûÞ禿µîp¾§É|ø´h>õ»‹Òðq07ÌW”¤¤ÇíWêëéü­¯—ßãwÃã¿ðûX¾Ú«iÔ5açdÔâ`è9+ƒII õ_*àøîô99{ݵ‡æª\ZÎ]'À,”•ng^*¾«Á›§†ø–íÇ>Rd¯ +šJƒqÑy`lDÂÑèýÙÄÙc¶RÞ|OÁs©)±ð{Ÿ¬ª­;_‹¸¸=7ãßèú« Êš‚ÊÎêu4çvø®E%¡êáý]qt½¾Ypuù:ÖÿWK+&˜I?%YVIfdãã*öxº¬ËíqφýޏGÔ:púµY©K(ä*)(ù¾sÑW^ªÙ“•âöýW]/wSÐnnþ'º¹ÔÆ7 6Æþйô“ñ¹òü¿µf_÷F?›é³h«x+=%LnÛ.°…ãnÕº˜’°}ºõ~ xñÙ«ý·1Ì0á ·né×ÁTIWÓ£`_VÊ@¬’Jo1Èíà,¤”˜·cÍò»´´8êÝÓªµ²=¥si+ðöx¹Ãõjd÷}>Õ߃¯~ ÔêFæø…X€¨¤¨eö¸¿W|>-¼^çë+‹Á½µÝ‘kȺ³É˜’ã¿E߂éðת«ÅßÇÎ7-ìkÄ´ÊãÒZ¸=ÞÖׇw7?·Æ=¿›ð{V»îR,c¸Ñq 'ù.zª¼^ÕÕ8j5 +3â¸Ä“¼Ø_êïëò½™k]ʏ§·VýˎI!jŸµÅýW­õ«:þstÁ%É$޾ ?WýjýŸï¿ÿÙ +endstream +endobj + +153 0 obj +<> +stream +ÿØÿîAdobed€ÿۄ    +  $$''$$53335;;;;;;;;;;  %% ## ((%%((22022;;;;;;;;;;ÿÀ9&"ÿÄ? +  +  3!1AQa"q2‘¡±B#$RÁb34r‚ÑC%’Sðáñcs5¢²ƒ&D“TdE£t6ÒUâeò³„ÃÓuãóF'”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö7GWgw‡—§·Ç×ç÷5!1AQaq"2‘¡±B#ÁRÑð3$bár‚’CScs4ñ%¢²ƒ&5ÂÒD“T£dEU6teâò³„ÃÓuãóF”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö'7GWgw‡—§·ÇÿÚ ?¾’I)²I$’’I ¥Ó'LŠT’I …$™$”É2‘å2)Y$’A +I$’Ré" +­pÑ¥Eõ½ŸI¤|Q¤°I$B’I$”ïú h’U,ÀßH÷ƒ¢¹sÉ ?:`x)z ÃD¤’J$©$’INݎ…G<èЮ½…¼ªwQuÎԀ +S¶ˆ´“+­Àñwܝ€ÀÓîí)œ%6ô”¶Û{Ì$š§yÀ;”ÛK” •-!'¨ÁÀB¶æ¹ðLªgý ‘ÐZ¼Ûǯ¿´ÊHi(í.çµ1-P‘â¢n¨rà¥B­µµ7s¾Agä^ntÄÂ.^C,¬Ôꮩ’—@I()&)×·À”ã§ü·¾±ÀLëàh·à†¯Ù›ûLcØ’si3âPŸ’Ö}7ǒ*fiÀ@×É%_íÔîãñI ›¥2™EÏ 1¢ÙwÑgæ3ô„«G!­äZ×zŽÝ ²ª¤†®Ä‘cT”t—RJ'²e ÙL¶–ôç²cHðEhw‚—»°JÔÕû-{ÛªJǾxI£…‘Ù Öo‚cpìF½›Bæ§õš¨;%Ãóݗo`Å:º^¸žY?i¿ÇDâŠHþêbØä"úñÙEöneíiì¢j¬òõJR/B¾!$OšIW‚¾¬×ø¢|P«sãP‰3Ê*;¯(ocÝÁD„’Pjúï™Ñ%kä’mڀÑ1§”´NZÀ±ÿšä >Úߣ+Z%ªZmÎûfXxd{’JåoÚ*wçkù +I´x%Õ-; 4[ÙÊN9 à'+ê訿8¶Â–F^CZ[³l÷T$“ªŽy+@nÍ&æ¾4léñ *‰&{…4¦5ÍSBnUN0҈ÓÝXc7ÕD ‰A³‡öƒäùu±ÛJväRþ†‡M¨jžœw}§’’½¹ž)&ðG²l´-õš³Üw_s  aBÊk°CÛ>iv4¡'&wÇ”ÍÑÊåøE.¯P;*Äʊ@еÃTåçpÕ%_zHqªjs+~„ÁV74‰?¹À颛rl'Dñ›¸Aƒ«fm '…’øs§ÅD’L¥¬¦O'à)–Ñ0’x;|ÒMS'§´‚æ}Ë82Aì·Oógà±_ôÏÅ?' éõDnµbF°Ã@! }%7} ˜R²â’úa$ÿ±nÿÙ +endstream +endobj + +154 0 obj +<> +stream +H‰’W’‚@ï‹]vEâ‰2 QrÎÊ^dµªÿ_uõ›ö¿q?¦ýÖ׸¾†í5nG÷xög·<ûõÙÌk;oí²uË>îe?—ýRK=>šiMª.©»´î³ºÏÛ±è¦jX‚´ÒÒOË )ƒ¬ ³**ê¸êÒ¦/‡¹žvÛa| çžzq~Ïë¬Úy×-O³<ÝruûgÜ|à ww£,®Z¨›P3¡Ž$ Iº–bÞ4˹:¾åGN”…yÕ, )ª@RT¸‹ÆËº¨’Žü$Š†ä ɉ4)^bÄ ¯è’nªÈ1½ðžUY3à ‡3ü™H ҂$(†|µŠ~ú¥À/Åþ’àDœg†'€È^Ö4Ûõ“".[Œ`0‚ÆÎÌɞhÀI*ò£¼¿q +;¿¡I²Pµ«gQÑ~ãäNâ4GpPT¯QÙ~áԉáX(«è†¼pÜ_'šcEY5o‚rÃ)Q35äxIŽ‘,F¾Ùм¤Y®—'š=Q,#ÊVûiA`…3há‚üP AÑõ-e›nh‡‰lڊiýlºòïõ¸4󿯙çQÙ¤UŸ5}ñé;æÝÔLú󊵛÷a}Û1mÇ¿¿º¹ +endstream +endobj + +155 0 obj +<> +stream +H‰ìÐkWGðÙ¤,K$7³!Ô#Ø `ÐT EcmDШE³M-­IHlpm±xk½+xkª¢ ‚¶^Bë?Ygv³!%­}×sþÏî<3³ç·s²Aè=BQ2Jˆ,«)$Ôû`i7‡åoæqÊKoiüŠ„ó™üL˜üì]~Ã(äÌñ挲P¥b„« .ʙÅ­RÅHZ§×ëuz]:z’ÌÚ`Xhd‹LÚB¢ .1“”šÍ•𥔦GYyù¢ÅKŒ«’hB9²Ô¶¬²¤J·ÜbýkŒíÕÕv)ՙ²ÛW¬°¯¬©]åXZW¾ÚhÒÈÉÉkœÎ5鈫Ìö“µëê] Ÿ6®ß c5…îøßó4y½/NssSS“/…òzš=ÞM-­m®ÚŠÏ>_nR2û6ûÄ´·û2k1[::›·nóo¯Û±Ó‚?ÿ]ïÌ.¡º{¾ðô™u{ȏV£à¸/…ÈMÖ$\ øÕÞ¯û¾iû¶b_ˆµ +8 G"ýB‡q‡ÃýB“D¹ØÞŽýΆïö…â"˜7ø0×u0qÈïøþ£U•Á?¾Mñˆp‡J ù•G0vÓj48oŽD¢Çڏ·žøYĿ䃟ì95t0`À€ 0`À€ 0`À€ 0`À€ 0`À€ 0`À€þßb>z²ýø™}òa_â¬Ë‘Áó‡s]¾Äó<ßߏǛw„çÏñáh`—/qþ‚ãb(n-Sj4<2<2rIèaÜ$BGG¢—¹X÷•ýۍ×$ƒc±±`,6ŠÁXßøŠëêö]oi«ùõ†Žqò7’¤4’xêf2™ìé8µék•­L_¤eî»Õ÷ÎÜê켞ð¶ Õ×Ü®«ZbQ2ejÔ;>~§WŒ¸ï•öw[õ+'ŒÅSM̓û\.CM­Ãv±Ê0¥Q¹iŒ§§§'É5)Îé&¶;mu7â~gä´ ¡?räqåµ² !kÒªyÆOr$Zýô™±ˆX9Mp*ΦffRƙ?ɔb㩾YÒ3F–²˜´JÁªBU#ĚÕÒ¤ùëù‹—ŠW³…?!Uî0n…;Xr0’ÿSfi™x,M¿¦…¼Îj?¦gñ7QMáN[DÖBdYM¡¹gj5úoù[€ûÃF +endstream +endobj + +156 0 obj +<> +stream +H‰ÌW–¢@Àûßb—Œ ‚’ƒD›LCt2³ÿõjÄ´¿ÑöÖ£›qƒ–¢CIÙinù‘jº²fpòí$È_¬HÐ> +stream +H‰b`dbfaecçàäâæáåã&€ê3aQ1q I)Ááâ5˜Ï¤9e¸Åeå䇀úLIYEU gêšÃ€|¦Åª­¤£«§o`hd¬./¯1<Äg&¦fæ–V<Ö6¶’vöÀ|æàèäìâêæ®æÁãiíåÅ;<ƒ·¯Ÿ@`P°Jˆ%·•¸x(Ïð@Ÿ1……GDF™¹F[ÄÄêssÇÅNjÅ#€ˆ‡…À.Ad"$Zw"H@ BÀÐg ‰,¬IÉ):®*©iéª\Ã0dú3š[vNn^~®ŠŠJ!©" QXT¢ŠTDT€‹@B…X PH XDQaQQ!Ø &@B*…3Á&‚TB•ƒìQhfŠ "@L„˜ u$H5@l``ú¬8¼¤4Ù4(—£¬ÜÕյ * ‚ÙH<ŒrE¡*Pµ! ¡˜XáŠE3>«‚âÖ +(€>óaJô«,)­ª6Í ª©6€¡®ÞÇ'¡X+œ5¢”-²!9pØp¤1%:„7ú³fe ôYÐkMÍa~-­ámþþ•Ã0€@f;33sqqØ0`Ÿ$(I&€ópŸË‘aAæ@»†š€aØ€èU©ä +endstream +endobj + +158 0 obj +<> +stream +ÿØÿîAdobed€ÿۄ    +  $$''$$53335;;;;;;;;;;  %% ## ((%%((22022;;;;;;;;;;ÿÀ1ó"ÿÄ? +  +  3!1AQa"q2‘¡±B#$RÁb34r‚ÑC%’Sðáñcs5¢²ƒ&D“TdE£t6ÒUâeò³„ÃÓuãóF'”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö7GWgw‡—§·Ç×ç÷5!1AQaq"2‘¡±B#ÁRÑð3$bár‚’CScs4ñ%¢²ƒ&5ÂÒD“T£dEU6teâò³„ÃÓuãóF”¤…´•ÄÔäô¥µÅÕåõVfv†–¦¶ÆÖæö'7GWgw‡—§·ÇÿÚ ?ègS~H±·Å2Ù®;Aßó˜…"sƒ d¸¹ÀI#è»v»²­QÁ„ìqu¸ö9áÞ¥@=¾ïWÕ(sÚàû2ÐÑ|uL1²†û댬U «m\ÊlÈkbËK̝®ï¶}²Š.³çïNۛéä1¯}fóí{A7aÍ:ƒØ¢·&±•UĹⶆ¸‘&$øø¢4]ù¡¹çóʗ©gï¶³Õ²â,u›¬@éöû|%J¿I›ËsokÞ~—<;ƒ§çBpª: Þš1?÷А±ß¼~ôjraÁùvY$m–ÁŠ¶âNÞ£pvI†éø •½W~ò¥m½`½í¨ÒÚçØçLÁŸÄ-d<½ïûu :v°VØh#ãªBÛ;çÚOy­¿Ã„Ðë¿N‚ãßÝûÒëR㾈ÛíÄþU~—–X׿:ëÖ´´q.ñ)M‡SŸtëÃ9IM|Wç~ÒúŽ£nß tüâ+wTû*5ÚþpÐíˆs +æû‰¬ýºÉ®w~Œ{åÛ½Úü”^r‰;:ƒÚ Ðz`ÀøžJJjTî°ÖRÌw¿¹Ô ŸîO¿«m‚ì}ÄjDètñ•|Yps¿[qhh–üð˜:öÖX3_%ûͅ€˜ýȘ„”ÐÝÖ7l¢#hŸvœŸ S:ÄÎìxA®£À•qÿjq#öƒÚÂ;V&dù¤Ó{^\z®‘komÚÄÇ”ÓõmÅ®4Áa-sfämSum‹)ß.ß¡ˆÓlkÏ*ã}fÁý¡fšÆÀG]›,sI.͵óâÆƒ¬h?ÖRS›¿­lúToÝúµM—ŠÇ®àl×qoéø#‘?®ZénÝZtþV§®Æ×h°æ\à í#H×M\’‘ûÊ&ÇþùS±ìuoi¹ïÞíÖÀ×Ìë¯e*.¦»Yoªö@‡±¢C¿×à›H¹Úít€âñAê?÷Þ›Õ³÷Þ´Ôi’[sÀ™³ùJzz¥ ŸRǾ{mÐä9¦Ûxýê&Û}ËcöÎòÿÍKöÖòÿÍKê§Õ»÷Ýø¦6Ü?=ßy[_¶ð¿—þjGYÂ}/`kž\Ú[¦©}Tä}£"cyðçÊRAÒ{óà|JÔÝ» gÚÐÙcbOҞÑàœtáQªZ7O¹®‚'ÃEÐZâÐ#I(~£üRÑ>~­õ" d¸ãpˆðÔcгŽ?¢_¬GªæfCVרÿ½Gø¥¢œ:úQaÞçØ¼GàÄcÑóM>”öÛ¿|»ã;yZÞ£üRõ▊q?`u!¹/v.iÿ¾©³¢u“7½Ì »À È2o’Øõ◨ÿ´S•HεÍs,4–èv8jžJ-è™íŸÖ,“ßsò+_ÔŠ^£üRÑNm=+.² žë géóÿ‘Kõ¿ä}çÿ"’ØùÞ ÿ;ÿ1AËèùcÃc™[eÏßýOéÓ¿6x%%'´;ŸŸ…‹Ý=‡ÉÄöpߏðB‚—QÅ9˜¶c6ÃQ´úúM‘ÈóTnè6]‰f+ó.k,³Õ­Å–4ÌíÔˆ²· §ƒà³[õzñC©P¾Â^NjFæ†v4€ PÆú°ì{ª·íùz.cƒ^ù`p‚<ä©N¢J~“ü’ôŸä•)‚J~“ü’ôŸä•)‚J~“ü’ôŸä•)ãz›©fGQ†{2MŽ·næ¸×´­i—ñ°ðšÓC®õèh£]SAcIm/c¬ô ½ÞâßÊ´òþ¨?'"ëŽC@¹ÎvÒɍǎQ_õk-õšÎE[H}3"5Зy(ø dsD(mÃôþ.F5UÕ~=¶‘n%UÒÛ1ìs$Ë­ƒí&àíðó +ØøŽÄ¹†º™s:{è¯qcfòû>žH#Ü·]õ5ßO"—xMZúJ'êæY$›é×·¥ð‡%í¨ós×AÓ¿G8Y›Õ*ºÆ³ú”6í£FSs\KIà9Ágåb3öuU㵛ýÈ­Žhsöä²Í‡_qØ .‰¿W³›;rjˆÝékô’ÿ›ÙƒèßKOˆ«QÏò¼Ñ ߍ­—0LLj?`#ö¸8cr){Ú)]Ù-asÊ p@vƒ² p«¯¤ãh·í¯·6±²Çº°ë=2k{¡Ànátóo µÍuÕ斈® žü¦?V³ i95{ ·ô^QûÉö{0PyŒœ;¥Òʱ-UeŒÜ曅Ÿg.anoð[ý¸ßó‡7"ªšÆdÑK¨svûv¶,aÚd%Z?WsHäS#ƒékùQðz&F6K.}Õ¹¬kšÆlÕÂ'’•žÊ ßIOÒ’^“ü’¥0M¹¾!Ò’¬8J”È׎NâÆÎâÐLøÌy'mt5ۚÆ5Üî þE™H6›Hʺ³m̼Ÿ¢X#ÓÈòCoCso7ý¶ò^^^Éö~‚àéÆž )Ûõï%ëÞXC¡e2·2¾«–%ÂKLKŽ±ÄžÐ3×?«åºÀsš@6Κèv¤§uæ»4³kǃ „>§ÿ$Ý»ÿ~ &ê?òE¿Õÿ¿'CæaùO“ÏIõ>ú÷Ē÷zœ¡ÿ|IMÿ«?õ[ØGúöTr??ó"J´›pÝúi¼7æ›ü¾%|̒k#ôÓ¾‹~)ÝÏúø/™IÓ4}%k‚¾\Iº'±~ªI|ª’{õRKåT’SõRKåT’SõRKåT’SõRKåT’SõRKåT’SõRKåT’SõITG æD’SôÚKæDKôÚKæD’SôÚn¥ÿ#Ýý_â¾eI:0ó eòŸ'Ý?û÷ԗ…$§ÿÕ­oýTÿÿÙ +endstream +endobj + +159 0 obj +<> +stream +H‰úÿÿÿß¿ÿüþýë×ϟ?¾ÿöíۗ/_>úüñÃÇ÷ï>¼{ûîÍë·¯^¾~ñüåó§/ž>yöäÑÓGž<¸ÿèþ݇woß¿}óÞ­wn\»}ýêÍ«—o\¾xíÒù«Î]>æÒÙSNŸ<êø¹ÇÎ?rú術Gž8´ÿ؁}G÷ï9¼w÷¡=;îÚ¾ç¶}Û·ìݶi÷–»6¯ß¹qÝö k¶­[½eíÊÍ«WlZµlÃÊ¥ë—/^·lњ% V/ž¿jáÜ æ,Ÿ?{ÙܙKæÌX<{Ú¢™S̘<Ú¤yS'ΝÒ?{R߬‰=3û»gôuMëíœÚÝ>¥«mRgËĎæ mMý­}-õ=Muݍ5] ՝õUu•í5å­Õe-U¥Í%MåEe…õ¥u%ùµE¹5…9ÕÙUùY•y™9éeÙi¥Y©%™)ÅÉEéI…i‰)ñùÉq¹I±9‰1Ù ÑYñQ™q‘±é1áiÑa©Q¡)‘ÁÉáAIa‰¡ !þñÁ~qA¾±>1ÞQ~ž‘¾á>îá>naÞn¡ž®Áž.ÁÎAîNnŽ®þ.ö~Îv¾N¶>Ž6Þ6^ö֞öVv–î¶n6®Öæ.VfÎV¦N–&NƎæÆfFö¦†v¦¶&ú6ÆúÖFzV†º@di c¡¯m®¯e¦§eª«i¢£DÆÚêFZj†Zjšªú*@¤§®¬«¦¤£¦¤­ª¨¥¢DšÊòJòêJrjвj +²ª +2*ò2ÊòÒÊrÒJ²Rв’ +2@$!/-!'-.+%.+)&#)&-!*Dâ"’âÂbÂâbBâ¢Bb"‚¢@$, ", ,ÄDB‚|‚@$À+ ÀËÏÏÃD|ܼ|\<¼\ܼœ\<@ÄÁÉÍÁÁÅÎDœllœl¬¬@ÄÂD,Ìl@ÄÌÄ +EŒ,LPÄÌÄÀÌBLHˆ‘` À4S ƒ +endstream +endobj + +160 0 obj +<> +stream +H‰T–{lS×Çç>|¯¿?ãØ¾vüHÜà8‰2b$”Á(*kC ¡y@I4„Áx´ÐjЀ²R@CH­ D(C°D×Ril㏭PyŒmr5uiD!¾ìwí ²+ß=çÜ{Î=çs¿÷÷½@@ý@ÃÂW‡ +W´.Ûàx{kÛ:÷¯Ú«ÈÂ>òׯîõ®=Ìtáõ;ôŠ•«ÚιyÀǶ{՛Wޝ‘_‹cñºùØê憦·gþ«Àۄ}ÑÕØ¡üB¶ Ûb;guÛúžÃÇ~mÆöe™ë͎ƆÇÑžXúq>}[CO§üåÞÅû]í mÍÿØ]ݐ½דÓÙñÖz\7/IÁÕ¹®¹3gka¶qô à(b/â]̌ ¬,D`¸ +– Ñ4e“˸+/ß)4¾m kÇËj’eµÚ‰²m² Ê˒eR éËQfïèäwìÅ'³G™š§gp!°„|KUSð)®¸B4±±`eØQRqFølž4é}íCÕ$ÂÁ(,!¢‚:pGãð~¸…#}qÉ£¬Ùl#MÒè&¡±9µžšdíÜæ9¡\_Rdô ߸qëàîâϾfììAЀÖÆÍ;XRÉ#ÖáTúÝa‰e8ª²µÝc–¿'’ (O”ã ³7Ƌ!Kå#^›Oîe}&µ%™ ,kZÖÌJc€( V…=:C|~ @=˜M:-G .¿OW\¢ôQ]1åqSºL³©ˆŽ÷¾±t³ø(nn)ï&‘ÝÇ{NÞš÷){ðÁ)ñºxç’øýÝ dÆø0©|òà1Y4Nfˆ7ÅonoÿR":†Û»ÉþxðœâÉ()Š+†S2Ü Š*¹´¥±›ÉR(/ÿ +¡Ff’’"G7veÈ·÷2=±Û°üø“vzgBe2ö¸áx¼6ÊT2KÙ5ÙíŽMŽmdÅçñuÖ5Ö^k¯ý¬•7Ñ0vµUàìV†ëÔhÜEÄÀºœ]‚[)láb¦·Ú¯pÆÜ9Už4Øñ„öÇÄý´P:}iHo.%x֗–ê0@} +¹±*½:_†^y&‡`•V ¼²ÕjÉÖ¨¾œDK¢‘bŸÇÍÉ8օB½1““iˆ ;P@ó·ÿñò@ñ¢Á¾óU>æ]ÑE÷6VžÝµ"Öd£Õ“¹ç‰¾³£:²xMß¾w«·_èþ‹8ñû›ªšDÃK[?A*aTŽ‚0ŒÅ/+ç7ç6æwåvåË}¤š*,ÁLýS83¢rOÔ&´‰L +H4T`õœõe{Ý>Ð`h¾aØ=ÎØ¼–a‡°BBóe¥P¥e¨-Kñ@b¤ž¡"E&T]a›Œ‹8HQá ‹%ˆ& "ç ÆLðӃ?(•çöž8{Tï5Ø}¦æYë6Ìõ±§ãíÄxû?U/U®Ý"þðØO̟¿S¾ö`ÏþnBӔ+öþšõ=›Žt~~åü¶EEÙÎSý_‰¢¤ÚŠgwð{ì\ˆçéw:©Re¥a©a•™Î+U(µºKo0èÕ—ÞÀÁ¬0G£Ä·©¶¨ÕÙú醉¸®e«t\ÌÖ1—»JH3ý11†8åIäyü9KIf;¦û´Waª#\`A¼‹“È)í€.Ìébí¨9¹q2eaà­iíI<µeP‰f½áÿHú¥ŒE£‹ +c&%¸süI}_ü—G†Îõ×o}ØF=JþEaþ–«DÿTL ‹ÿՒ¶¡Žë½ƒÇæÅå4ý©¸ÎgÄ+_Š_\½ŽY«æÙmÆÃ~Yà‡ã¥lÄÌ{y¿u™õ7°ƒì”sU¼Bð µ:“¾ÆE²Xµ˜K 8bº³‚*Sä„͹U–dioõ¢žM! +nJo’àÏeVìõÙ]ÈXŸKãŸ1'vÖ$͆vj…ñšüÈÖc4—úIZ`’ÂH=f;“Ñãó#êg7贩̗†eÌÄÄWuñ´Ö3kہӊ™¿zµu„(ÅÿY¼3«,سùøúáÃ{؏~Ú¶¤ Nü§8ùZ~àáý+âßH˜´ŒÏHӓo.mm¿6ô»’;Tcî +¡ž4°8e3¬T,cº²T5_µ„z•YAã½ªÕUMɉJ=4Œ\I©x€5“ŸP몴)Hø9>¤é³J†`Rª'FÅIÅ£7DK„šû`ÙÒüìi׿<Úu`òÛh¶8rùÂPã2D¿?yVòt{2Â)G-ŒÛØ<ŽÎ§£q±¤‰‘¬´IèéM[>nʼн´’ÿ¡é°Ü»#ÜJîC'£áú4ƒ>­# +vkýåd/ŽˆŸˆêa|4®¢ñÙ×ÿc»Üc›ºî8~÷Ü‡íØ¾~¿çÚqœ8±“œ˜Ä©(¨Œ©Ö†´Ã×0yãîÍÞR÷]ÄKñ’KD³:¦Ë`Pe +D¨ÈQއ~á0dñGpRp꽑psFËèhÕFŽ9ûûûçëÄ oÁؙM1jäHa ÏT–'3„Ul}•öÐÓôš`ÞC÷p„± +1)í•þC˜À‘Iô"ÁŒñ¢(H„TPª:8NâSðã$}¡  ’Ègâ(&&ŸãÈ_ç)(Á&Ռü–’O••sïw˜Šýj»øJ-o°7'nœœN10¥i_гë +}Ê.æEÀh´tI;^ÒÇŠ„,ȱ׏“³ØYÜN–kÅ¢vã8ë-֓³Åwï¾L¾øŒb¨Ÿ¹Î,B¬,¸ñhÐ.ìÅ <πãaBzð¶‚×ÁóDعȋ&j2a^$Tvˆq‹ êÿ2IÈo¶ìPVv<Ìûż^fÍӚ‡[œuãÚS°i(g–!¾¯ß W{?/ڎ‘&Ö{g·gh÷ƝÙFŽ»÷ »½c ¡Í…ª¬õáä´Øoâljî& 6 Rˆ„BfG†"¾ŒÙŽ|¦,zæ»íðm' {tbáõ¬ÎëYáx=«ózVçõ,ðz0 ¼ËC¼þ?¸®›r4ؑÞ.‡B¹mG»Þ<©ukûïßr /ÇÁÚ×7®h—¿Án+ûrè„vA;|éºüžˆ+/bûÐ.üƒ[˜â¼Ö§ÔÞcs!K!K«,%Ж‚C(iÃØL<ƒ-dO»V3Ñs×b? +âPalL)?éXâXᢎHÔrS%âqqqG¹A’"fÅRÕU=4c[ $Ÿš0ù+’Ÿ*ÝKüàÀEøEXVŽœì½HípMej1¨ú%Ïê +®ã`ƒ˜{Ý 58n<ŒÑñ»—ŽyF ô‘½{Ÿ_<ïñéL fGzÐdá,ÂÓ¹5Zs utmÏE`”Û™™[\··.¶ôÅSßO>âRœùÇoý",n†˜¬ºü^ߥ»ãÿµ} Ë®‘€ð­Kã~Ìåù™,ÎΜiÛ ujëÛøÂÝå;íØ?÷×´ónŸöu—v›º°æà¤èâ–Ý›Úd;„m ðCTÂ"7(nx„æÏ×ì{0½ŒçÚ@¹šQýµ0º²›ìæ %”¨›`_(-² 9Ña‘h0+”Ka»%ܜ"édóáfҜ­Tv‰¡D™7ԃ7ñÃQ!N›I¸ÁœòùKHVî-´“¡‰¶Ähÿ˜–ñVHÃøôPá^)ž¼/~Íë«·pz = ·4t¶Qº£ÝeûU<ʦ _ÀÀSêR°R†‰‚a¯i‚=àù‘±ÜˆëlÅ6 ÆïÆ£¾3,¶ÀìU(»ô1¨ÑmTHÄú*vT£[—Nybæ+ʳÙÅó2ÓðÁ·å¥5?oVL{Ù7»{W®ðª–ˆ\Yo¯ôHç~ÔÝ{dëæó³ªÚöüÒâ­%¡šøy±ÊW=gÚ£•Ó>Ú6a«ŭ¡2J×[ø±±Â„E‡6uÿƉ¯èµ4úÞ%:ÀŽƒ×‡ÑªB¶Ñ:Þ:Ýú&·/ÈTÑEla;ÃaÁi"a¯™¥i{Rv¢æDÀ‰nT–Ž}PZP¶W® ­Zåœ<Á€/$™Æ>sI!XŸÄ‘)(ƱaF´¦A7˜ˆWwÁ=¨¡ÞQw»kçڝ{Ölڇ7O«³WëÛ/Іþõ7üÄõ?õÿîą3¤±>2‰„‡ZºŸš«‡þ‰§CM®ƒ‚ÓÏåÉ¦ŠÑ+xÅ—p®Vˆ¢³„8ÝÉa^p[L%It‹;‰<Ð/=˜? Ìû0߃î§Êa‘ƌ¦'1&׫‹[w°P7ý'_M«þ ’ÙØñþAv¼ø—©Jn÷Ì_§’Ý+g¼ö_ö«>(ªëŠŸ{ï{owùrùÞ]@—®| +~!q…]Ԁˆ86`ýØQ ZSKi›Ž”M‰c4R3ɘ£Ð†B*$‡88jkMªil;‰£&ӎŒýˆmZa_ïíŠâ´cf:í_y¿yï{Þyç{ιçÝsaä”îuXÇ +¿¹‰fº“MW%˜¬£ÇQÏ2 ½Ëé¼mÇàHÑàh}@§ܶé}ÎÖ£8¤ì›ä·Ï@Éh¾ßÐœåÆ E˜ •ÐHÂ.ɝc&RTµ£·WßÿmCX¥vrÐJ÷´£ÊI…KJœ’׬4™ä¸g³¦È&Rláa“ÃAYG2›l˲“=)ù.^¹£ê!7n;QßñÄßZ +!/buD1ŒØÖ®òÎú+•9GS¦¶¸³ÌÏMêe¯HSö¯¬zþ¡to®.Z™Pœ÷­†‘÷`,æ[¨}$¥Jèkl°}·{Æ~s‡õ鄗¥ÃæCÖ# }æS楫Qˆ‹˜mVRl¦ˆ”˜p»ÉnçãI–Œx»#©Y^OÝ´â?o“r(QJµ ‘£y:3%‚’#A…ÅE¤³âbNPҙˆRÒÙí ’|bL^(O°êcÐpTlf˜ø'Û¦–¿ùrGNjØøáÀßf1Ÿ)MlܡޕO ÷t]ןF¯²IÃ()n9i¾GšGѴȝž!Ò#g‰y’e¶ò(K´%"ì‡(:Ììˆeúª%{Lló"4-w„FO€…sGõL°s¥[aÁ–EobôÄÞÑÿÒò-Śdݾ§WšÒ?óÇïÞ4²_ÿ×%j×¹E^ÏW½99l Šõ±9î4)¡ Q(QaÑ$¡`JÅGŏà v»c8uÝæPK²¢`ÐhǂY9%ø²Ž\1ÒyüU…ªez*ČÃ?ïêJŸ9>nBIF˲'Ÿ”—ÎïñæÇ†3¾ËbþÑ:~b/v÷3îJÚüŽ1ÖÎg¿«Ä¿ ©ÚÀé€\ 4Wt(ÊQ¦¹æHóló–ƒs‡ÅgàLaIaó 4'Âï7ððþWø +ÿK/3îq¨œ ˜§B÷<ĝk4ÅÆÅ'ÍŽ·SBÌ4¢LʦšL4m:QÞÌYù£o“‡hˆ²r¢E´¸Šh)=tïÏþÿ‰úpÍ&'¨0\'£>Ì¢Ù°Ü ÃPÌ^J«ÈOk¨Ö£nl¢&MÃ;NÊE–7F¶‚ªB²õýfPV»üåŠÐ—?Ì´6ôŽ +Qˆ–@[C´Ê©G\²€ã„½AšSª`௠Ñè–­€~Î[RRì)4¿n}s]SC­ÿ^cx¢Ðã^Švo>ÕÁ͸6Á{µðLèuôpýðν¤ÿÛç˜ÛM2™%Púè֝Öò8lô¸Ûµsph2ÃYtÆ|ŸØC${­L¹•Òš€Nè#œõ{`©ö©|’¬ ڟE!Tôë'Ì)¢z‚P7¾-õÈíò~%§2ìQX2Kc…¬˜­ZÙÞϲ~^ÉØYÞÉ>f—Ùuv“Ë<‚ÇóI¼‰ïå]ü8W4ˆ}âiñ±¸!= sù |UI3ý6°:°#ð®V¨]Ò¾À:2£-€+Œª°‘¡û臘ÅπnDmNÐi—Y2 Ñð±æ`ÓÙB ‚-bkY{޽ 3lùG ¸…GóDžÌ—ðÕ|oåçy«HÙâA±Lt§ÄqSܔd)VŠ—æaËÜ.mžI‡¥é=¹@~@®¿.·Ê;ävQ+Ÿ“/(-Ê.¥G¹®üɔi*7m4µ#:§‘³ïŒ­™l"¬ŸŽúVËD¦¬‡®VzE*¦ù'ˆÎÔ'rgegef¤§Mt}-Õ9a|Jr’ÃnCÇmŒ³˜MŠ, Î(Çë*õ9ÕtŸ*¥»æÏÏÕÇ.?þ;>Õ VéXÕé3Ĝc%ݐ\{—¤;(é•dVgåæ8½.§ú+ËÙǖ-®ý„ÇUãT‡ z¡Aï6èHЩ©xÁéµÕ{œ*ó9½jis}›×灺~7Ü–›£ 7…ëŠU*ño®·á¦KxU‡ËãUí.ñL¤yýkÔÊÅÕ^ORjj x`UUã¹9 º´3bkÍÎ>7­öé”yµ*ü5*÷麢'©‰.šøèUÛíá-ÊÛ~ÇC•§•úëÚJႝóƒCŸ>ò·cT¶Ä µ|[MµÊ¶…ŒÐmlôÍ­syu–¯Ñ©Z\Å®ú¶FœKUÕ=·Ãëò{jTª¬î±»íÆ 7§ßÖR˜ŠÙ÷çÎ͝«ß Sm-Áûgùï„rƒŸà^V5ê¦ɵvªÎZã#.›¯_êò©­6b8j¦Ù{JTŽœiªœ¶À¯¶.¹eF½'hœ¯ÑÓc±;ô9øŠk ïk³ÎÆg ou9ÛnBèº6–ãq”4ë ÒI=У¹‚ç·èfÃ1úçl®z=¾ÍÞÐØeóÞÁÀXwn³§N/«¬NU5`Œþé©ê§óªqE73ëC¶ªb[«N¼ñ/Æ«7¶©*ŠßûúÖ÷`€‡ áín™¥+“,eƒP@Vš;6 N†.™eŒ NÀ—1²@$ÄHÖm¬k›ù`…˜à>ðÅÄLL\E!&ÓjöüÛ?Œ1Ð&¿÷;÷{ϹçÞsn﫚ç‹þó¶6ªW¢9%{³Û]c.\ÄXt,¡N¼sðƒøûŒ$œÅ=åÛ¦¶£ðۙW3˜/§ž5ðYH‰²#€ßf°uj/«Cß(Ú¯€ûi,õ¾N³¨|ÀK@mZ®Aß[ؘAv$·³.]°ø*N`^[€³ûÕ[¬ßîa[i®7Œÿó¼ï¢1ö¨ìÛýzê+ùKBn¸bÈQȕÚaæ/$à}9ìD5.·]a/bYބácx@Ÿùà€üÁúúäø?@ïr\;[ ;Ðû0nÚȹ˜Ç,b`6P©ôZß)ùlPéÅ|z™’Ž»Pƍ8²1aþé9=ŠÔ㟀‚Ø~獛ÛDD&`½­‚½ޖŽ»V¹Ž8ê¬?°^×rn³ÙÀÓȼûˆï +P›Y)Úy˜§/'‰5D;‹vkDí²†m£¬ ºn{'{_m¶~SÜV£ò'[¡Üen»ƒmB~ÕÀþëÀEؼ#ó¡Ùºÿ);·- òU|Ý£<ˬ­ÞÕ`o_„¿¿!ÿë©}ÂÚÁ氒֝öž×EsFY3ú$(Þϗ@ü°}‚ÆÐxؚ‘öÓ?ŽûSë›Z[ZgšƒÄ֚ٯ  ¿[…JÔú¬¹À]à=€ê©8C}0ÿ;È«2o‘;”£2O#°ULù's7Çy¹g©Ú¡X^MÛÖì½l ð9@õƨnh,æ’Ìئ£ÜÉ0í7ÅH9õOè+k2 û÷S¾ýgüOdê™ò3ÅÖ¶£lØö.»„<¼aX;[‚5¹‰1ö#ÈcÔ=Ú`Û©2ë/ªE GÁT‹²½ïS ¥í—þ•ih¯¨&2T¿àUàiÙvfíìO±¬oâ‘4esÒõù¿,Ï%œ ™½Íì{†ñGu‰s*]ƒ“åV _Rï‘çZHæYúŒ§|·´vûåyÊä¼2kFqï§ ~ßnÓv\Eék³Œ1ín€vhmOþh]]€sòû8ØX+?ÄöÀ'€š•º üP\Õ½ùö,_ëÍUŦü¹¢`j®øÖäöäiq³à֟˦³>7>MY=•ŸáŸ²f&øYæà{ñ•QÊO&\;DTݬ ˆ6ùä¼;^¸X\æÏ3‡Ê1f²œŠ_܋Äm·©ð¸¸ê4UЕB´¼O‰¯ŒÓâKãMqèI©¢.“Æt;ÄñB“ŸŒ‹ “Cq,E» o¹:E³[êë:M¥'.<Ð×{sÅÒeÅb‰ñ“(wš:G{‘Q'º¯‹ç Ù­Fޙb¾q\,‡ªÐ¨v.†x”ŸÂ v*îX+.BD¸‰ײN“ïKJÝ“ïõ. ”vºN‡«N8\kœNÈõßh´×´ÕÚb­ ýZ±6OË×géyú }š>U×uÍä½ñ*aâ=¬ +ËғÐízŽÉÏã¥:Äûä˾/tUWt¦ç›ÖIº]䛼'™G„A»”ì&ïK¤^õy…J’*y +=•ÔµDáºÂÖâuØ´³ž WTÍZ5Ó³Æÿ¸GÓCϲÇÿ +¸ë¬Ý¸95p‚` Yå¦~»vãò••Õnؓ·mo‘×’êÐ;Æ5=,*ØÞ–¾ó.h +nÝFüF(ÖVòǶ—ø‹Â-“¨[H.ñ°–êM›Z¼!<ì Ëq"èÛÙø¯Ž¬¯¾IŒùÈØNòlœDÝHê ùj$_ä+è J_guëF_û.dgQukmQ¬tc¬æå-›ñ…Ôà7ù9¼ôïfÿ +0÷5 +endstream +endobj + +161 0 obj +<> +endobj + +162 0 obj +<> +stream +H‰lU{TgŸÌL0ã 63±¾£P«¨kD©‚ÈV¤Š@ ¯°áuP+Ö'!Tߏ¶¶§«U{ +"¸–*Eŵ®â{ÕvOÙÒzõÞxÎ~ƒìÙ6“™oæ›ûøÝ{÷Ž‚RyQ +…â­˜¨Ä… ‰æ”B[™ÕQ<%Ú^h±[-Qö|‹,`’‚Ò•4J£Ÿµ¯Ö¾ZAC½\ô‡ï‡wÑ­£¼н'£íE[ÎG%ℬ‰bhČé“É5"TŒ´Ø3­brEq‰µ X|¿0Ëî(²;2J¬–`QŒÌÏ—ÈZÅâk±ÕQFv‡ˆ¶b1CtXslDÓaµˆ%Ž ‹µ Ñ'Ú³ÅE¶B{IE‘uJ¬•˜ˆ\ fZBìÑF,—fÛ,¶ ‡ÍZüÿã¢ä |½¨áÃ(‘¢FûQa4¥¦ÑT²šª ¨µ‹¢.PÔŠºGQ)ò›OF©(–2RS©XªŽ:C½V”(¾õ½öyµ+(ە¿¨¨œª~:Ž®¡ÏЏ˜pÆÂ¬aN2dg³l ‹ê,õ-õÞ!Þ˼ ÓËö·Õç€f”Æ¢©ÓÜÔôùÆùn÷½­¢ÍÐÞôá·Ìo³ßþ>þþ™þ»ü¿>oøݟtŸqÞøP;øŸŠ£0WytßÇô@Œ ÁÞ¯8Nå¸Åg?KD]¨h² y  ²ZqC¿´³_qL “ð8·`*Fû^4A"$î%ïìp¸àgœ+ŒUuÕdbælY™œ¼¥r ¯¦©KЊGÊÊ0 ÌÊsR:ßôiÓ§þªöœó@؜—ç¦ÚÕZÑ”´¹_ñ±ÉÒ=PxˆíùÄÁ|5@ÀŒsè‹|T4êwÁĴԞïlܓûQø +a«ýAC(!¡_·L8Lëaæ +r§÷+L“ŽðuMmÛ¾Ò:ÉþIH‘û•,×ÿûכÇ-ߒ—àþ®U@ƒ;–qrç{ó`íÍwÝ,÷kOá»?`dB;êcR?)Ê3æÙ׬ˆ6ÈABPðäwl£òSã!ȉ°ó@-•¢1ž©®v9q¶GP•ËOh7³ŸÐ2è± Yÿ(ÝWƒ¸$× ÑÒSʁò(<as|wkS÷ÖÉ+'oZ•läΖ®«ß³QàVoÜãØyË\l'òÈÇ&!y0é^¹ÑÉrÏCóÖ͛fÀ€K Ð8T”?úÀ9˜& Qæ%wÂ¥5|³ëÁ¥.÷œôUU3/vÝm6re€=<׌U72Á7VÕâ΅e5Y)Lh*ñ­à&ó˖o÷?.àÌú%1F®¹lÝN¹ˆerÞt#l†@éHnU›äþoKrG<Þ*ÔÈOŒÜ/ҟºo@ã ÒÏëÀÈ]‚ƒÂßdϝ:qò áö©æ“í /g]‹Éµh˜g$š­®‹gò²Ò‹Òÿ’NTŠÈMºa ËmSxb‹¬¶‹P:œð*®r]½ô wBõMNC&ӈd cpÙ@6i ~ø:›–(øœÇ1 ×{®jk»\D=ƒœ‹@©» +fˆ%LÀ̽€|¸ÎÿÄô|n‰]'Lg¤*'õ2m§,ŽYQš+Œc@íyç^tCX=Ä=³_÷É”îoêzd€àÉ_#kÄ#¯³í¶J29¾$s¦ÀT˜@fÓD®:+yÒ¿—!EÀ‰ÌgmhLлëïC<Ä7Ü,pÿn9c`!Ûãcpº]¸“®“\¸ÜÆÁ¤*P*~ èã`Š +$àï2/\è#ñýŠù!BƒfTòPÍ@ÃjØAÂ-͆@\.à6F+g ‡œó‰+dJV=(±Žì•µ„ìo¥Çø:›!׈h<í"¢£'ϹD,—ìq,ÖUù¢ˆX é#9Þ8Ùj£TŃygÈ#œ„® Oùò«Ñ°£ëùþú&AòQùÃٍ%E›AÓJ¡ ¸ÝoHŒ­‹}›X=]®¸FB”ýÛ¥N£ùu6Ù¹ ‘x›LìlìiòÅø‚LõìòÁ˜ W/oËàH!˜rÅ>ùMë@6ï¹ÄæçÛfïyÒiÄ`gì ƒœ—ý 3Jm¬vÛC²x–Ê;áò—ãf ÒÃÐ0‡Á•ÒhX whœ9DügåŠn9òx=ák™ôì8I&LDŽ—¦ÃxbR/u¸YôtÐ0†Aê ÝžOW2¨ñTÒNi©’Æ­2•@ýN7:»ä^†^:JôS=GiH#J&OQX*UѸJVèâÞ`-–Ê éìãÿG³Ç ³H1â×ÏzW@Jné…$HvÁBtñmˆl‚œ 8KjI@b#ÍG¬å¾õ0×ö§&¦åÿ‡ójjêJÃИ{ѲAsM´‰Ük@Ad|`5ãTÑ.V+Å'±ÔêªØHZÄ* X% b- EFc‹Š«VºSw7ÅívšYG«ëÙ?3»ÿ ÈÒm»Ûٙ0“Kî9ÿëû¾óSCÝ!4kzs‘õ ý +ÆÇ|¢„Ç8Ïi‹õD8ÂL‚i8‚‰p¿¡}ù÷ɉôÅë3—/—–p¨ÑÆÑO>¹‹æ¼Y`:šÈȰëƒB˜pNڐ²´væÉ}Õ§nÝׁßÒîI"~È&3ŒqŪ| +1›‚ wä2ˆ§ó +¯ìÝùÇ;DÁÙU¾&F‚`B+uc°£bÂP) wðm¸’‹Zi{Z‰ÕÜ«·$ åpLn ¾ËõÂàÁßË%Íõ!øzôCUè óÝ %a>0µ„«z%Æ$½"~Þo—®ÙØò(QçÊyœpw¼Q]¤å^¢\>ÀRùŒ–i!’ƒÕLÔB8T»í #d»Ã÷Þ,ú‡Ÿ^ÖÂ\ÆÈÇHè@ÉÐ1ƒÙlá.Ì]ÂÖyÉ%J\Ì!çyœÆžfpªlW¸æõ€²GÝ"ªé „;.A¡fÍ!1wýÞ³§z/î½½_ñ±æ¾Ù‰¯Ñé³>睷¥4ó»¢u r§KZ;.؜$nàédgCMÎÓ½|jQ_Wû±‹ˆ6NÈ<°D ã¸Â“žÔ Ä}â‚d ɤFRÌ,8èÔ¬ËéáQÑí«]E‘A(äÄÆIh +׀Ž:.ÛxSC[æ%=åƒñÉãÀ¸”¬¤-’jÈm¨²{¡¯—ܤÆ2Ëf‚/50~áUÔb´G¡ˆ³êQ¯Ãø?~ +þ"­Xš‘œ¸öý&˜þ´´óÚÙ#Û#Äᙝ£nŒ¤36 +š¸Á̺®f¶Nʌ‘k¬ü™-)Uqz (Á°É’ИFºÔv¸£ŸµšgÉMí]ê³@zˆäçïÑð™æLџ¯ß°Fšè?ó5`,Í?\¥ÿaҒ wŸ¯9Ù$5Ÿ¬qØ~Ü[Ùëâß°ö^=(] ½OH:ÑПª)ÎÜYœ©ÇˆÊpœ\,-›ý· ²±Ûµ´‹»Pi¿R@Ëb{@å—3œe%’+ðy£ek`zykçÅÒ(TL̋{íàƒEPC­FèYaå?¶$Ùãõl¥òÈåïfœ;}´­V²Måâ2ÓRDáގåH9•…5µ:9ôÛß»í©b­ ¶HrÏhѐîÚg‹§Ø•WhåþöŠÓKÒÍÙñݜ̬]f+Ô¢øN¦²ñ޲‡£,%AÄGú/Û,”B} ã`#s)·½= Œ¡T(XHh<\’ѐ;,mQwRÈ#«¥MIYÇa#sÂ2ËM;”Ì 7PÀT¥U^Ä2à†Ž +¥¸I{³¬ú™Þ/`±f:X-"•3v9Õè¡là7,qêö +¡ñ„ðB+}e²%1M'¯ógÉ+Gxú56>éDKÖ=@ØcIp<)#ÅlÈ4¥‹VNp^¦An¡_u¶§D¡õ2•ç)®‘n·}£s ƒüªu™+Ó©#Bëur*”Ë)˜1¿‡®*b߀ˆ í'ÀHÉýj3øéaäÝßJ¬¸à®®ùðѓ¢à¨ª´æ”K‚³"§$#…I˜àï²ÙçBõ3Ñ+ ½%¡ç@qýD]ê;ïmݚUV+æUًë|hþù½Ëְ߮€kû£”8[€@^`Àß}@©Bã¾^ø²×»(àŸ ç߄ÁjMxS z1‰'gqS©’cè†âýŒî$©c·=ƒñÏo)ªýÑ7š »ù©óAÑ +¡"èöGw?xñB'¶k¿«J³Á‹r~é;”HŸ7AœNè2Ó´Dù3®êÐõ[çŽlN2™Ów˜ TæfË&]bsrs²(´5ohÞ´AÿV^Ò®Å%»Da;ÝjÚöÚív=Œv~þôFêÙÔV"Bê©ÔÆ]˱j{!·Ö^wü’TÁ'&¡zZ"Äçvâ)¥LŽÂD¹ š@™?ûÅÆSè&ö¼Ä)ÃJü·øm?hŒ øDÁ¼çXí{þ‹hßóÓf «s¸À™ò£¡ÜFz OŽiˋäj>(úÀð«úÿ­¦þ¸ŸìÀŸz.m†™ ø ñ¨Âë—ðrמáÄ´ô뇢Œ^ËîÊþM•ø^,+ç î{KŸUÕ¿© +wWÁ«U>p¼Ôån÷‘®Q`xÑUïë † +ß_ûªä±ò'š Q' +endstream +endobj + +163 0 obj +<> +endobj + +164 0 obj +<> +stream +H‰T‘Ënƒ0E÷þŠY¦Ê€ i$ ©¢AÊ¢5i÷Ä ©ːEþ¾3vš¶H0ǞWwdµÜÛaùê'sÀºÁ¶çéì  ûÁBšA;˜åz +_36$5.ó‚ãÞvh-ä%çÅ_`õžu®“;/¾E?ØVÇôýƒ.gç¾pD»@e -vBVO{nFù§û7u¼8„,œÓ«Œ©ÅÙ5}c{%%è®(mû?'Ò$¶œ:óÙxK“„‚Ð* LxyËl"WÌ]äš8ߦ tk +®)0òŽUà"fEÄuQ +vU ~ôD}ZqƒŠ•Š»òŒ8cˆi<Ë,îã?Xë†+6y¸ ŽÃã86€wt³Ôœ½'·Ã"ƒ›ìã`ñ¶k79¶_ñ-ÀØÙ™ +endstream +endobj + +165 0 obj +<> +endobj + +166 0 obj +<> +stream +H‰bd`ab`dd”pu +ðòwÔ.HÍ.)*ÍuÉÌKOJ,)ÉÉýfü!ÎòC†G¬Öù硟ëX¿Ûó÷|w\þ½Eˆ‰‘1()ÌPG!F#9FSÁÈÀÀHGÁ71/­(5EÁ/5#§X!-ÆÈؼH!8ÀÕ;$(ÔWÁÅ5H!Ü38ØÕ/ØÙÃÑ-DG!¢Þ!¤^/9?Ã!@`ÄÌÍÀÂÈÈ"¦iþ«†ï疟iåŒë~¦‰þMû „ì@‘ôùŒ?}ÿ+úwÝE֟ëØùÞw¿gþÀò7àýÏv>¹ÇU +Œÿ{åJ³$ç÷²É-NNŽœÏÃÙÍõž ÀЭ^ +endstream +endobj + +167 0 obj +<> +endobj + +168 0 obj +<> +stream +H‰TMoà †ïü +;õ@iê%BªZMÊaZºÝ)8ÒbCù÷uóúËSwîÈEoìMG–qö „+ŽŽ nÀ:·¬¬fÒd‚ûuŽ8u4xh[!ߓ8G^aw,cÿÔ쫐¯l‘°»ÔŸé _BøÆ )BJÅAÈÓ³/zBè_鲄¦äõfÃ[œƒ6ȚF„ö±Vд$û_͍¸æK³¸UVU +¢i8*‘¸­"ߐ?y÷dæd·t¢ØÉFá½YÁ‡ünžâG€ïny +endstream +endobj + +169 0 obj +<> +endobj + +170 0 obj +<> +stream +H‰T•yTWÆ_QÔ뒥»¬VºµªÅÜ¢“#Q!*h5è¨ÁӄÅtC#`¢‰xFmÂh\piEƒFcÍà‚ëȘWЀˆ€àÞj_;3…cþ˜:ïܪzïÞïÝúίª(äî†(ŠRý1lÖôOÃFÍK5YfK¤£¤îÒ@/ ™H6½NÉ@– +ÎûÀž‡ö§Ôȍ¢vì O[že6-û"]?B?>8è£Ñr ÿ6¾ôSҖõs²,鯋~Zj|šyyšÙnLxO¯Ÿ’œ¬éU°ècŒ£Ù*ϾkDo²è út³!Á˜b0©OKÔG›RÓÒ³–ÇL5ÊuS"õ†Ô„±if½I.·d,µ˜L³Éhù]!J~ԇBÞn¨/4 @Hë†ôù©P ƒB(Æ¢hÍaQB)nhBkڂЄ. T…PB|̔íBîHÆ¢t]AôŒŠ¤’¨³nﻸ ýé(:ƒ¾ã®t_é^Ãø1Œ‘¹‹)ÜÇâµx¾­ðT$)N°»•=Áþ»ÏÈ>}öö¹ìçqÓsŒçNOð +õªòVyämò^éýL9A™¥üMå¯Ú©ªõêå³ÅçºÏ›¾CúÆöýE­RQǪ-âb¸Ò~)ýþɏwE+åAêm]N*S #zF·p5°Ãiä‰;Þí:ÇÜÀ܃q$˜ï錕.ž¨œÿ¡nµÐ°…¨øÙó(ÀO{µðªý±`oÛ[]TÁNÃÙK¾ŠJϦOÊ +¨%Jâ+ü^ ¡-t­3—¿Dbެ‘m'XGgDæ,`Å;Ë\;øˆÝW»ë^†O‡àyÐ[MÔ°ª€©40àÅG‘ L5wØÀÈʐQK•B>] +<äג|¹ÙhxQ i‡hð"4V~Ý UÞ§ÊÛ¡¸ƒ.‡žøìºo±0³¤jÞcÝ¡K…§*ĪË?>† -hÌ`&ñdù*²k¨Ð^ÀƒO‚w÷_åžCˆO&MX¿ð‚÷¥ÝꜚIAq; ŚvçÔ÷qBR”aº‰õWTì«ù¹ì«´ÕI¥ªªÒ;és)ºŽÔcðŞ=M-{öûÞiÛt¤\ UŽ›ýÝ҅§±ë¦m0±ÝøÀÆ6 ;·N4%®NŽŸ+$ŒñÆD2D Pf×Ãú‡u_-ù=ßŵ‡>/5oÁOqìüò[Ö[:ø îUÇvø’»È={U|¨þ¢¶}r 5‡|˜M&¯žá‡›¯í;««9—JÜ ûuÄ÷bˆ‚k=š·?ïX.{fu…1L;s錑.ùå×Û¡ ûYåüÿ‰¶vÒ­šN‰ÃðÐÅ1 Çäµ4j¡–!ƒ0©$5 Ô(”¶f§÷»d)²™ÈwX Vpƒ­Ná¦úQ‡tñ)·â‘¦Ã©Å°â–iÃð­S`ÞO‰$XÁ$F×ãÿÒ\¥QMdYؘ®J´…2èTi•Óڂ¶Ë™VÀimqQDB³Š"’„ˆ‚Ð҈¬ŠAE´ÙĽ‘Í•VTÜ0 ¨è-慙ygΩuN½wë½ï~÷»ßEÛùwZñV€áŒ0~˜ÿÚÞäµUkÜCRþü°ˆê† ˜4ìýIÒiDx 8¼+¢›ùBpsˆwú,¼ ¼¹>EËqE§u‚l.ïIhë*ÓH+vɏÑh¼%=t\¦W0ׂåU4-v=ŒÈõËYÏõ‘ë™ ö¿Õïà\îD¿zBLÇyøté3rñ³mxcH0®Ë//ç.^̺ÒD_¨õ)c]ŽáRJ ¬$´h쬄Õú–Ôc OºAúB𲇄/ûqZp3)øîW.eSÚÂL7ñrïÓø;qÝqoý‹m³r«ÒŠ‹Å*ÕéšzúFp“S [â¼Táýßz]$dÅ'ãà}ˆ‹ Ú 8À øW’®‚–ÏðâÖy‰o’c¥3»ì:|s°‰û>˜T”бêçYžŒl.‰æxæ6Ó÷oÕ Ü{,ŒýDʊdµ^—Ä8š¥º1h*âÖ"·L§›^\YHm,Œ†Ü´¢}Âç"3duX]'cõ` v ³_ÿ26„„nÝÓ!aÄPdp+êÍðTL—~jŸPy­³¡Qٞ¹{Ø9­0.¼i€Ñéoîq7î'u]¤yG3ƒh³Y Í.+Òoy°Q9ªNšß(Âz7´HOT£ŒáñTÐ0ƒƒÍ"©2ä¯AžüeÒF¼ ùXª€ ߇ñ#C˜T©ÿô§¶¤âåà©ÿ<Ф|a%?Öºð¦ÝX\g“ÔI´X׋ñ½šH"]šÇ÷ÿ¯¨B•ñ ¨H(ål¸?ú‚üà‹~=”êÖzùá«_ãŽX!,5æ+^ëlHޒWýºÜ…¼™H÷7]1aùÐ; wá¯ÍFZˆ}GEñûŒãëå­©t2ÄyòüÈ"¶>èïyËçŸc=wq~‰¾‰Áb ™t쮖9[y(ä4—µ_dO+£’ö1^։NÜOÛ£×zÑTíâ;k@TՖú¼†¥¢‚•ûRBؐ(¡je+ä{Ãüľ¡>ánÌ"i“êÌP”sTT^†\vŠ3˜ß¶î6O7ƒ—Ôèµ.ôSµ°ÃfARQ#ãD²ùØPµÊÇiQé…á ¬®IÌgÚ[Ï]¯à +Îç5öÑ0Ú¯.äÛàn÷« ƒHo»Ð. Ÿ$–¸&ÄhºæŸW)` ̄Éiè{wט{ü”v£š~ˆÐÎÓRƒ¼hôr[é]-nr·IÙ͘ Iöh׌"ç"_αñÏø†RÃÄÏÞçÌIWYƒ5cÒû±\sÀÛ¦žËP%¾M\ËÆkü´ð5À U˯âm%9±>ÍhƒÆ"Áb´Álß֒Ú…•ˆ3Yé9©Jë)RvFVNK,DGu^¼ +´™"QèÇ7% +°¶±,$8#œtÊkmÕ«[À´UР†¬^!b$ϔÇÀ ígýܸ%›¤–¦4Zøh6,¬«*Èe/\L®vô¼è i0ºGº‰ÑK¿ö>è[™z±¾já4Y¬¸’YÈæŸ,­î``”ẪCu½Ð*zŸ—ÍÞ\zڙYaàìÏÁeó¯Ý—Kùhôˆ™jð ?"â#"°UJÙ)Ñðr sìë:™TÐ¥Æã…òIpGùL Ñ~¨_ð#~_< ~ÙÜÿõÙ]RÁ[ð­±fx—…¾Ãá ëÄH^áëŸIS©®WøÜå¤*©2çû[ziE3óð‚›=‡ºµP"ÊU4å³Y'ϗÞf~W……fr'bR÷Ñè‘ފðôðMÑr$ܱzo[ö‹ïu$d¶¯Œ_âÆQµÌEÊUÄW”ÄIz4 è¥:{Œ5PEþQÛ¦: 4ŸÍ MØM£û½ Á\4í”"}V„`©yaA&9âü´vAý{¸6 ¬™™t!XÁÒç`&C°¹#G4 Mdßã9¿¯üÖë«‘!šcµåçéÛ´õܱ ƒeí<}Ǿè[ åï©®nä$Qƒ’Üvöª?]Ɓà8Àº™Ѩ‹c—lÅv\öœ0'•nI±ûé·ÈNBuÕ(ðPf³ó}–û¸²Ûö¸$ìæM9†·9<Úâ+ۅ=—æKÔZªSñÍ&_žiaú …<…M‘—“‰ß‰"“Â’öSjÏῬõM1'îVúúÑ8ßÑ÷ùÉí¬®;„Õ0[‚ÆËµž˜†Æ”³—¸²¼â߯Ѫ¸Ò½g؀€Tĝ§x¤_¦ï¿Vd”´^šÈö’9Šü˜BöVÀÂ,;‰­~øîuØÐ>îQDéAW:âpD\ " ŒÿëåÓVÅñÖöÝ7 íõu±¥vÌºÀ0ÇԘ%3¢1æBk¶I6™Ë”ڂÉĘÒ"Fk„Œ Ê`1˖ †?¢Æ¥›©“ ¶W¶bó6ã¹õãy-Æáfâ¯?š&/ïón{Ïçû¹å浞ñZÕ!}ë<ÓOicÝ~ö”4ÇgIáñš‹CÁ¶¶–ØÒ\ï­3ùG{³`ôS\f1·J°=$fZßmS7¨!| |Qz2Ì|¿,®þd#ú´fhrø …Ö½ó°°@|ý-“&¶ÆÎGP£jüåî*ËKÕe ÛÌ6}>–ߢžÝX—¬`=´jŒ(_ÕÌ'¬R“rmÂÆ ˜M°ávýÊz7˟Ã| ÄÏPg›Âú„OMDÌǕ,ÒùjZ“ø¼]ÍGCÂÊ ³&g…&²ù,[ÈLΖ6Àºt0 8¡Ebnå%Ü%êÍP̛ùVÞ¥F3Ø`;¬âÛ¸ 'óqw¼H}¸Gdžðr/ÀùØã~»Þ'ø!J¦ažÃ‚þüòv€eèŒ,#Ä €"|Ï7Âû"?Ä/ ™ã̤ý­ÔAp\zœQ(#\ÇKAǝ1Þz19fˆ×ÊÓK€…2ÐçDè–Ç~•@¯`ÖÓ¯®#O˜/7á UøžOÁ÷ٝ® +×߀ïïßëéKµØ=õ1Îzù"Òs¾'Ñ+ÿ¨(¦µ~æÕAƒžy§^’\ÿ ®†m’¸ÃÎÂw½ŽV—.†ÕÓ­w¡C@Ú±EE땣*+yJr¤ˆ’‹Rùæ×ÏA6;ü¥áòü9¹òÚ Üx•ºЬ‡ü4r|þÒȎv¿¾·ÚcyqÏ®×6›Êz+OWZè©Ï^޹‡kKMõ›7lÝÓ¶ÕBëQâOyüµ*Ë3Á©ÆIó ù ÐZûaщ,z¢àc¡±£©ó ©ÿƒýgG-Ã}Ý}c&HãiG×ðŠF~w¡%yN*žŸô{®*†öøó‰}2LGh¨Ýx„¡X€Yæø¥%ü$J•й±ÕÜËÑÑúÍð²ÞåËKé)‹Ä&d¾w±Ø²TjŸ¸°¼ÅºSPøÞ!OnŠP—æµ¾QÍÍÚçÒÜä}£š¿?—&e~4½‰ûYÚò÷N‡¢N/tÊàŒ½‚?Í¿Ìjïdwü©úóÀ¬ öðo\m‹ÐÁÛ¡¯âVô Þ}TÚâò.uÝñòC¼ùlè"l‡ûZÓ»Å‹é± 6~O|§ô›ë/w +endstream +endobj + +171 0 obj +<> +endobj + +172 0 obj +<> +stream +H‰T’Ënƒ0E÷þŠY¦Ê‚7$’…”"eчš´{0CŠT 2d‘¿ïŒ’ |<ö•ïeìÇÃQw3xïfP'œ¡ítcp®F!Ôxé4!4š—™ýª¾Á#ñé6ÍØu;€”Âû Åi67Xíì³.£µÿޛiÐtú«sðùE…Óu°G=ƒy ¶Â+^ªñµê¼?êÇÒù6"„v,6†§±Rh*}A¡ŸƒlU¨›ÿk"X$u«¾+#ÜVß§AÈ(±Lqæ8#N*ËɎxÃ{B?ˆˆ«ÚÖw{âÚñž¹i-JbŒ-?ÇÄmj¹L™•ã"drqó0çÌÊpçmÅ ÉB–…è +ûdÿQäÌÚ¼;rnˆ…Œâ؈©Ð'.w|à|!\`b!S–¤îؔ%)Ë3'I9bƒÌý—Œ%[¶°å°åÞ–P.7ïɽ­êj uÜ^&ÛQîe§ñ~߯aäÖñ+~¯¼µ› +endstream +endobj + +173 0 obj +<> +endobj + +174 0 obj +<> +stream +H‰\TkTמÌL´½ŒA›™Ô'*à«÷* o- ¥J+*bE1<’˜ð劕$…[*/EDP(Þ¢â£K*òRTTꪢ’Šh×ڃG—w°÷×=笽ö:çìo}ûÛçl&²ÁÁoÏoÖxzÏû.F©S¨5.Ûw*ãw)ƎÜ8ZÀMí±µálíÑ”õnÅ;_œ)|41%Ø3ƒ>~òM¶$”L„«“àúߪāv˜@ðS¥Wl\¢Z¥•;…Í‘/t[ºÄ™·n ?Ùşì—rðØí +y`¢F«Piä+cÂbÕq±êP­"ÜU.÷عS¾~ A#_¯Ð(Ô:~÷<åJŽ tuâÐéDò Sž:ðׇ9‡!¡e4BúޟDXqè±hf^;Ø&tЗÁ&ïÙmö×;™½çdܺ¹ïÈ rAA›¼ónleRJ’ªzd\ )٘|7B¦[ðE«]óìé§R8}ú%cûaY6¤ã¥Æ²äJæRÜ?K=é +ö„*ØeFTF¼ØJg¼5@Ÿ¨Û§.b w™âÖÈÌ1)™:Ì=Cõë±m÷ê0Õ´ô·U@Öw~ÜÈP)ñfMŽšÙ RªÉàmŒÕ)ÅQÚȄ`z‰þjÕL7Õ²TJi¾ÑPÀJ²Ó;Ö¶p²VÓÛõYáÌ+ª Vñ¹ºTÊ[Ò°APMæû¹)yI ŸÇ44f”ѝí'¯ŸgËO—6ÉÀFyQ}’¹âÿïM4""üµ©,œAR·„#1’›`áé*,ƒ0%ÍÜÄHä©·¹©v¯ i`ÁõšS‚u¬ôu â«!_çì g£Ékbzå†Ê(v]óƒôjš²À¤îß_b‰Í†¸wz¶zðM­ æ=K½~‘ã¦5óɤµ^ã»`«Þ®yÎX©&Ώó‘–ì‰lEÆ#ÁRˆâ`NT{uӑŠ:^‡ã…y%‡óÍîS­„ḡ!O6*u!³Þ‡ñڅ›Ðä„Hí›gÕ&p¦ù¬:¾˜¸$® ÌS&٘Ò+ÛÀ©]pÙ…ƒB. R¥¿›B0ÚEÇd(ƒÙeßêݜdhñ½9°èõÅúòc̙sÙuYåbÂkßæèµôŒm=oÍlv›„æ»g[h™ld%èD‰ŽóéçGéz; -TôFK)K–^•G¯ +ÓFëØ¤øjÙz'ßWï},œŒë@ã’5¦ÚûEÅFãQF’BZaV|lçQ.A••z.õÍ>ä!tçËÜGõlqmvLQ½™Ðn~/ ¨!£~\qq&Î^оòË÷kØÈÕ·¥] +̏ϳÈÁàŒ¦,ÒÔ_;À€ÆKy‘FzZ‚-|Öp>)®œ-Ïô—-اgTÚhý·´$›Ïhõà`ßü0(ì³€"â”é?G+˜²kºhÀLs·²èâ ´“ÝéeiÅLKÌò¢ ´÷Ն,\p&%<†Qô»‡€Ï°R¯r¤€¿A8AYr?„K­œÑ…ÈÿºfÐ z-|Ó±@!¨ ‡‰ÚõJñþ7(ç<]Ç`GzÁs«ð¹½uTÁ#ÀsbmwªÞî)OvlMv zŸò”k‰ªÌº’“ÌOy5ç[é»g‚װ菨&™®;Åþxº¦…þ¥J§=ÊI=œð½ Ý€ç$8!Q¥n_›ÄœMŒºŽ„ô6ßôeÁ,ÕÔíLšýð¿êNÓÛ½°‚iêyao…zâõͦŽªÓ{µeL©6[µC†î ‚•WäX°‘Þº½ ÕhìÅ]ˆÌu¸Åð þºÇú2½e€ê±ðŒ‹‰'ÇÛ@˜wÄdÌarŒ‡Œ â~29S—{@LYB÷h½ CS‰¬æ(¥L"ß}‡›Ò)à;Uø°°æHÑã@è}¹9çÄÏìÙÒS¿\“U¥ÕÄgTªÃˆ=-ŽÏٚwAv§Ï”_ݶy3H”˜ÊR+˜ªÅ…þ4»̟֧ûSÃÞKªù×fYÒþ¤´TF­I§è›k3Ç|ú]NÔ)¶ó8_é3ÔG¸Õ©ŸTŸÈÊ*`ºIþČÚÏT_ÎÂu"Ф« +Ÿ¶ˆ”dÿнe¬ÞvÁ8D5>àŒRôœtúuL¬úZM!C%˜ÿŽ¿$Œ†k¹2Îy.:KRjS¸NńÅ}Ÿ¼…ž·÷·&î“îÈ £È"Ìà_VÿÈ|Ö͔ZF\ I6¸›`l‘$†— }àq”€GÀ1óÃø"òÉøáÏàÔdî­ô¿ ‹ý? +endstream +endobj + +175 0 obj +<> +endobj + +176 0 obj +<> +stream +H‰T‘MO„0†ïýs\³‡îBL‰¢›pð#²ë½”I¤4¥ø÷N[\•¤ÌÓÎÇ oyY=Vj°ÀßÌ$k´Ð ª58O‹‘ öƒ‚8vvÛù·…NÍõ:[+ÕMçŒ¿Sr¶f…ݽö§Ã>ºþjZ4ƒêawŽ/tP/ZáˆÊBE-vŒ—ÏB¿ˆÿéþMWø}¼}ÆÔ⬅D#T'Qy' @Õþϱ,t4ü†…Ê(¢@,—Ž1ðñ!óL¸ ü@|> +endobj + +178 0 obj +<> +stream +H‰tTPußåØï*â·íр³»ZÚ¡pŠé€šb©ƒ¥ #zwyrtwž0ÉX4ÇI¤i`Ӏ3ÊL¤5ôk ¥üúÛ2ŒŠ~àd ÎYo×w7Ó÷´þlwg÷;ûÞç}ßû|Þûò\j +Çó¼òhñÊÕ¥¥s+ë}aO ˜Wì¯w{êƒw¹ï™m¡¤ÇL#›7ì©Æôt;àÞÛ»n?.ÀiÐwôgœžN +l\ +Ïw+ö74’ ͱ5GË_\XËދóµ"·‹G[ß y¶µÕõ[ý &äq;5­¨®N«H¢‚Z…'è „ÙßsÑ|A­F xžñ1dÀãÖB·g{M Vó{µr_½?ÔÔàÉ+õ°E%ZM½{ž? ùX¤àŽ-AŸÛWðy‚Îÿ©ŒãÙÍepœÄq™7“ã°p9ÎÉsù÷…+äØeg$q©œÈ=Íâ3ø |/ÿsʤGʺoʾ”Ï,ËRKØr-µ(uSê«©?âQ+{ž× Ðù÷)4R pF“ ùöõ]܍ù +1ßµ»ºÚuò F‹Œ®(æa¦ŠsEÌdKp+ +y©À\2Ù]Š5ªã:ÿÞP2¨¡è2¾C &>ü# ¦Ì9²d‰³Ueˆ\yçä×'Oïv:1âøò»#O §HÆ \–T "—ÎÚ\¦b£ˆê!,W6ïL†éÝ`.]óTàÉgÕÈásñ¯d@:Ã_êÝìjW­CÝa¨ÐÜKѝ¶×#Ò0´š^^’A¨à9…Ây‚;L¯€…ñ€,Ý«‘+ÄL‹ç +ÖöfÝ´ë¶A +Ý´ü‚ô;ô6']V„eŸ¨%ŧ®¨0s# ÃØcŸ\Uúý—žG–ª”œ}Àb¬‹`)VaU K¡N±1.oëüU +'˜@ª9_%àê]‹[q ± ++Õe¤¬R†Åê߆4h=HÃj×&œÐápm¹ÊjIxá'à h×±Ã!]´j¬ìñ;^¨å‚Â8„t 1k«Ž§ÈZ˜}x'…Âq–Ãt;s9ŽãÔô–á0‡á²„Wd½Ô6½;ùo¨å›;{1»he„,ßÉ÷Ph –Ø,ß߉ô€3Ê´1ΔÅu1·jûºÙ/ŽŽ(Öö»®±MfÚ ¡3N„‚o2Ë纰œà~"0ʶ$Vx;zicØt|a»LW¨ÔÙvÓÁ`õ ‡ðm¦Cˆ%,‘YDêCoB"}¦& /F. ›ò·Lña +ßQéi˜a—êLIŒÅ ?! ã¬F"½‘H#üÀ´ +9IcdL‡·tžRh¢ÊH+{úÏÁ,XêØ·Vō„2YÒÈ©O_Ài(l®-Uq +I–:¼¡Û.Ph¡ë¨d@LÈ †Q­kSÛÜ{1½#<ésé}©·7 ´œÝ­ÄDéÖxO×Ðõ,¶}Àf71‚a‚ÀåTÈ$WßmtàÔêÀ\õ~ò_ž<·žBììãT6ÖÇä?koàL{¸ +3ڔ¶âŽÍ -š$ýYv֟“U¸¥ºåÖïG”Õg´žY3²Ávî ëPmþ› ¨* />]¹÷ï¬>…¬_+\ŽŒ•„Mûä5S—Ú <*3älÓ&”XÉFq„H !Ç´'gnVÂ.°i(aÓXN¤rt&¦&žeNks§éíÄç^ƒÇ£°31¹]ÜÕez»pwwM‚£ûÁ¶?ï˜<64e #=}¨=}ª±î^vÊý#À¬ +endstream +endobj + +179 0 obj +<> +endobj + +180 0 obj +<> +stream +H‰TQËnà ¼ó{L•~Æ9 KQ¢J>ô¡&í݆µk©ÆÛÿ}YpÓ fwgaàçêRé~þjGyÅÚ^+‹Ó¸X‰Ð`×kˆP½œ·“_åPà®øºN3•nG‚ñ7œf»ÂîäÇþ1ßGÀ_¬BÛëv·øýÃ×Ř/PÏAY‚–ñóSmžëÿ©þ ÝVƒøs¼]cT8™Z¢­u‡ ’¨‘% Vÿc¬M+?kËBf¹‰4àÔã$à„ð!àƒÃYì±Û˜ÈC~NùԌpQ2×sSÏ~z…Ö"K]RÔvđˆ&G"ˆ éÒ=ò<ˆ'›x£·‘ýw·äb­3Òÿ‘7Š,ê5޿ь†¡É¾úµ +endstream +endobj + +181 0 obj +<> +endobj + +182 0 obj +<> +stream +H‰ŒV P÷ßåØÿBBÎzë2-«» ¾y) +¨Uh4*ò0ñ”C. gïð”VɨpàA[#31Ô D¬h•Ág¢cÔzH@QôXˉíhEùÿ8Óÿ¡iÌL_;7{óí|ßïÿû~ßc—¦ÜÝ(š¦}çEE.Š;1!Ó`ћÌþQÆÌ}¦YŸâKþ³ Y9þ1.¿@UPGy‰ê(Z½xUòòÆÓqñóÜç¿a`£×hh}ÃᡗßHÏ/_Š¡i¤­<Ñ4'ŸFÿT”qCŽÉ°.-K·v¼:‰ÜÃå!/yiŽ9K¿Þ,Çf®5š6MÉYú”Yž“‘!ǹ¢ÌrœÞ¬7YÈÓWteƒYN–MúuiÒ§ÈY¦äýúdSºlL•2Y9ôþ1z1'ZNÎL 4šdA2o\c6¤’M½9 pþÒxâ'‡È)úÔÿªES^¥¥)_ŠCQ“ijŽõCÅ1Ô5Šj¥4ā\%u‰åCM "©ê8ý3:›îr›î–ïæÔj24Nw{¨{†{žû3&”ÉfšÐ(U¢.v8›È¶{̐µ2æ`3}5G0Ç_Cݰ™ñEZ¹¨¾Ö«É}¼ÿ/{‘vÙ@ª•6ÃE T¤ò1oyq…iDZ¨)rªWœôý(#îQÞ<¼…žJÀc$<†Å3W,Ç#Ä8ƒ‡ét·ªÇ¥Cg÷uB€Ïðͼ*6¯‹ýS €MŒ-·À}'ä:éïz40ÝîÃ.'Þ¹°ß‰Ÿ¿Heµ`%.­NØä¤ïõÀVrälבºypI҂ô¸X ‹¨|õÍü)¥Òî‰Ûý¾çñýöT0…Õ;Ào}UØ'v¶µìLC«Oת.BÛ_¤ö@+!`ÍuªNÝÕî9½`z8¥—ë¼ +é¼m~ƒÝÞ|z&IÜ-ÈÁÍY0Åçî×UE®¹½"if¡ôÁ؏ç\Ã?ð[AÑxñÃë¡ùSâ3uiF¤Èu.^Xýw©œÕæ5®³@‰d‡®A™× a +w†äZÂãÐÝ 3ÞâÍ'Ïõ?Ývw§ÄY츄ïBۏçמð|¶ÕЈ½åE)˜É;х_»(tFÆØ$î f‹ñ„mØßgBõüþúã—+D;"‡Å]R…FXkÕÝS ©;Sá ßXTwÇÞyX˜]„Ý|Çì©YÄw;ZQU“uAhQƒ tùà‡C—§YÖ¥(G\î‹TŕÔ}×s[ÃÛ¸Çj8<äÙµøÊGmÂZ<›[Óû%îÁ×ώÂèi* +*޳f Ó,W ĺCõ÷¸ºvc˜Mzáå6HRÃÂЯ£@ß"p»ÂÞÝ%˜Š!Ö½0¦•/ph>öÃ:lRÒ[.Ô8S%Ù ½EÊ@j¸‹ üÊáݵˆë +kçžÂ$¨åAÞYWq^øKm+µ/äëX˜±îö}´È©Ï®î;¥`´¬ sÕRá}¼-i—•[ÔE˜ì(°êÚ®yjçÞ\¤¨‹þáðc¹mlmÙ±öí¤#®˾%oç&!2Êê'gµx©ø&uØå%Mº«m¯p-Ð »ùyÅØsÜ»©†iÅàþàÉì!Ú`1oC粒$ +k°g.ÎÅ*ñ0êäé'Ê¥âÑ()#-nUÆÁ3…b +Øv'ù¯hnW#qS/ü Õx—¢ôMá ÏY@Ëë¦Æê7/þṖšpRIøµ–£ÜølõBÏjeg·Âø]COˆÂ=€I$ÇðA»‚¸ -ßÓørN]^yVú[Ep”·#ˆÁG™>„߇Z˜ 뙱àLƦF»ÎA.d×¹A&‘òò¡:jˆgžUפÀ,×oÄH®Eã Øý%_~þ™øé÷쬾ٷb™„Ÿ(êޜ¶7qåÊ­ Fñtvâþ•Ââ²̒ qçêCØâ4{ZšÏ ¯B¸–ý%‡Ê*Äý{”UüñR™KqºoWN¹”yÂ>ÚЀ©_ësâÒÅ|Ä57¼ŽtžåN]ÿÄÙucÿêх0Ôü4iÕ´6 _âìéúºõXÄa–ÅÓ¤÷þÖÃ`†q›@‹£~2Z<˱ÄEãåL¶ÀžK|P~cóG ?~R„5³±°eþRÎIj?Iºú|•1±PêFJgÕ xtú´Dג›»lÁ>IzÓûdDž6}! ìQдh»õèNhÈðloTuºEÑØ\cscvIMúÝÊʏí•b=kËùm»QT{X¸9°ø°á©Ì"w}º«ÝHF° ¾‡Y|hü‡«—k¯Šð$|g§ž]ÝqálÅéJ±qÙï¶³5¶š՗L°EÍK‡ö©S¸ æÝ`²jg¦"îÖF06„5‚ tõcÑ u¸å¶•Þ3¯ÁÜ0j$Cú›Œd\¦@BCÐØÁhç«F3Øa÷!+ˆXAèµí~»oh¿/ëUzI«ÕFü‡oŠøÿ—|mÄ¿Ýòœ'…‡ ÐC/kÎc+Ì}õÞ~¥ÝÓ>¢^2¡ñÂúŸ~" 7ÿ–¼DÖý Øõ½†ôìÖå– ¤–áÜßCôÐ?u’ë.÷ɹ<> +stream +H‰b¼ÀÀÀÀÃðï÷_ÀÁÀÀ`[8- +endstream +endobj + +184 0 obj +<> +endobj + +185 0 obj +<>/DW 1000/Type/Font>> +endobj + +186 0 obj +<> +stream +H‰„”kLWÇï°ÎŒuН¤`gg¤Zì‘B‹1.…bi‹ ҕ•U܅ÝuWM³‹vaÙ$6‚*‹à£‚® V,¶¥©b) %´~è6¤ªmÎàÕ¤lýÚùp3÷Ì9¿ÿ¹ÿ9¹š…(ŠJÌ^—õvn¶a‹ÍêVΤuv›Y±9s–½Üœ”KÞ]VWURÎtn’¯.ª×Šê ”*jU¯ÉÀjÓ¿jÃÑðp!ü¡]²ˆŽÒ¾ˆhŠb¸O.ÍöÊ uöŠ*‡ug™KJ,]&¥¬ÊXù +YW¥H3YÒ¦*§KÙã”rm¥vG…Ýar)æå’d,/— +¦«œRâTný·aÉê”L’CÙi%•Å,¹&³²ÇäØ-Ù-ÒF«ÍP’r‚0nL6s²Ý!Y ɹw‡Ój¶šVŹƒ _}8‡†X¦®óйθ§º#PêÑ]#šs@æk nL(ôõ?ü*½ó¾­EzؾF ‡–ñq?{ÅVv¬0/á”%z¾/á N‡¥7?kíkŸ–¨ÙÄrÞ xyæEtWa1,€,ô_ß +W?Ýògo'‘Õ¤è†h\ó=x.ñ«g°ÞÔóêëÇ;»ôݝÇÃÁ/gãÆä-Úö–â‘+<åVƒЁFwäXPÔ €åÇA~–§,AÈ a9ò/;ZCîv7ìõ€×£ë'Mœ€­1üy貋;hHeø8º öÒø%†Â<¶Ò~5 ËO, a¨kÛ+ǚ=ºÿ&€.ØöL&™pÓùp¸©¬Dē «ËXþ—áw.çTºy=â>_uÍû®Ù~† Ìó óÎPø‹„0SßÁòß_ŽÀ‚ZtlAQõ»•z?Ã_¼ÍrÀz#p’Wë΂xÿéXð=g!ˆ×Þ+‡èx˜3rç7=¤ã†C?Çu·žìùð±£~ß=?Öì ¹Ë¦‡‚ՔvÍ¿£]˜Òó=ø5h8#ÅYUv{MSK­¸¿%ðqÃéÙ\áÁ‰<·š™i²" ¦ ·ðw¤›ò–ׁë>Ù> +stream +xœcdÆ#/.GE +endstream +endobj + +188 0 obj +<> +endobj + +189 0 obj +<>/DW 1000/Type/Font>> +endobj + +190 0 obj +<> +endobj + +191 0 obj +<> +endobj + +192 0 obj +<> +endobj + +193 0 obj +<> +endobj + +194 0 obj +<> +endobj + +195 0 obj +<> +endobj + +196 0 obj +<<>> +endobj + +197 0 obj +<> +endobj + +198 0 obj +<> +endobj + +199 0 obj +<> +stream +xœíWÝjÛ0½ïSïfcؒìüØÂvIꄕ‘2ڎ^E’[QÿMV·¯¶‹=Ò^arÒ¤qҕ1V؅ ëû>st,} à¸. ½ãʘ󑇿Ïï?LC°Ð¼êÏà¬<á·âÓ£äg—ôñŽúÌ<ŽŽ‚×Y™qEŒ:Kó +סIX1çX7a`«uš®¬!tÌèÈ0Œ@²ŸÇÓ§yzš·J•€åri/]»7ù¾ ÇÒVõ+R[yõn ²Á‰yE¥(•(r£“y±P¡i>Õ¬Ÿ5Q¹C”WöJ¬M‹ è@¶ ¶È ¸â/²` Êe4¢²˜eÄ¢R"M¹4†6´ûÆû+‘³bY}@«~£ìIü[é5)_–®Z:lK×A|"9Q<Öoä@8´ ²{‰|Œz!Â`¯l`V0‘<´x‰ î!ÜoaìTîcèmÀˆ"„²[ûþÍf¯;˜e&Îf8.è"ã¹:£ÅB0<öFÓÉ ÉȍÇùýoäyƒÉ4ž¸îp¼ZK{Þæi^)’S¾ÁDnÏ$óÕ÷`bõ†ˆZžãÏ-§çúí»t0äܝ¹‡Z¹÷œMe‘EÏlídÿµ½Œn½-2]_FOyãA¥ýEmsÅI!3¢"R–© ¤aiŽPžSízÚìÔBF-ú•Ð þ­Ý$Ré• +n\Ð[Â.×K։6 8ÄY)9$m¤(¡Rþ‚Qª~'¤1 +§$¿ ÍÚb>/ExtGState<>/Font<>/XObject<>/ProcSet[/PDF/Text/ImageB/ImageC/ImageI]>> +endobj + +xref +0 201 +0000000000 65536 f +0000000018 00000 n +0000000085 00000 n +0000000165 00000 n +0000000274 00000 n +0000000373 00000 n +0000000473 00000 n +0000000545 00000 n +0000000616 00000 n +0000000740 00000 n +0000000809 00000 n +0000000919 00000 n +0000001017 00000 n +0000001104 00000 n +0000001222 00000 n +0000001306 00000 n +0000001425 00000 n +0000001529 00000 n +0000001652 00000 n +0000001761 00000 n +0000001863 00000 n +0000001969 00000 n +0000002064 00000 n +0000002181 00000 n +0000002297 00000 n +0000002402 00000 n +0000002488 00000 n +0000002536 00000 n +0000002639 00000 n +0000002687 00000 n +0000002791 00000 n +0000002839 00000 n +0000002890 00000 n +0000002938 00000 n +0000002978 00000 n +0000003086 00000 n +0000003126 00000 n +0000003232 00000 n +0000003272 00000 n +0000003386 00000 n +0000003426 00000 n +0000003526 00000 n +0000003566 00000 n +0000003673 00000 n +0000003713 00000 n +0000003812 00000 n +0000003852 00000 n +0000003961 00000 n +0000004001 00000 n +0000004052 00000 n +0000004103 00000 n +0000004154 00000 n +0000004205 00000 n +0000004256 00000 n +0000004307 00000 n +0000004358 00000 n +0000004409 00000 n +0000004449 00000 n +0000004500 00000 n +0000004548 00000 n +0000004596 00000 n +0000004647 00000 n +0000004695 00000 n +0000004746 00000 n +0000004794 00000 n +0000004845 00000 n +0000004893 00000 n +0000005005 00000 n +0000005053 00000 n +0000005141 00000 n +0000005189 00000 n +0000005286 00000 n +0000005334 00000 n +0000005434 00000 n +0000005482 00000 n +0000005533 00000 n +0000005581 00000 n +0000005629 00000 n +0000005680 00000 n +0000005728 00000 n +0000005817 00000 n +0000005865 00000 n +0000005961 00000 n +0000006009 00000 n +0000006060 00000 n +0000006111 00000 n +0000006159 00000 n +0000006210 00000 n +0000006291 00000 n +0000006370 00000 n +0000006419 00000 n +0000006519 00000 n +0000006568 00000 n +0000006673 00000 n +0000006722 00000 n +0000006821 00000 n +0000006870 00000 n +0000006977 00000 n +0000007026 00000 n +0000007133 00000 n +0000007182 00000 n +0000007278 00000 n +0000007328 00000 n +0000007442 00000 n +0000007492 00000 n +0000007544 00000 n +0000007594 00000 n +0000007636 00000 n +0000007691 00000 n +0000008885 00000 n +0000008978 00000 n +0000009057 00000 n +0000009113 00000 n +0000009313 00000 n +0000026893 00000 n +0000026938 00000 n +0000027079 00000 n +0000027291 00000 n +0000028328 00000 n +0000028708 00000 n +0000029756 00000 n +0000030811 00000 n +0000031212 00000 n +0000031250 00000 n +0000031299 00000 n +0000031348 00000 n +0000031395 00000 n +0000031444 00000 n +0000034824 00000 n +0000037259 00000 n +0000039674 00000 n +0000042138 00000 n +0000042279 00000 n +0000042424 00000 n +0000044890 00000 n +0000044942 00000 n +0000047205 00000 n +0000047585 00000 n +0000049732 00000 n +0000051084 00000 n +0000053754 00000 n +0000053838 00000 n +0000054009 00000 n +0000072256 00000 n +0000073647 00000 n +0000074232 00000 n +0000074675 00000 n +0000074886 00000 n +0000075083 00000 n +0000078703 00000 n +0000081795 00000 n +0000084766 00000 n +0000096872 00000 n +0000122201 00000 n +0000123798 00000 n +0000124349 00000 n +0000125425 00000 n +0000125844 00000 n +0000126517 00000 n +0000129853 00000 n +0000130772 00000 n +0000139003 00000 n +0000139210 00000 n +0000143519 00000 n +0000144004 00000 n +0000144385 00000 n +0000144558 00000 n +0000144900 00000 n +0000145099 00000 n +0000145400 00000 n +0000145461 00000 n +0000150679 00000 n +0000151215 00000 n +0000151657 00000 n +0000152056 00000 n +0000154201 00000 n +0000154505 00000 n +0000154862 00000 n +0000155014 00000 n +0000156566 00000 n +0000156848 00000 n +0000157211 00000 n +0000157347 00000 n +0000159586 00000 n +0000159684 00000 n +0000159924 00000 n +0000160276 00000 n +0000161569 00000 n +0000161657 00000 n +0000161900 00000 n +0000162181 00000 n +0000162259 00000 n +0000162335 00000 n +0000162410 00000 n +0000162487 00000 n +0000162526 00000 n +0000162560 00000 n +0000162583 00000 n +0000162625 00000 n +0000162678 00000 n +0000163433 00000 n + +trailer +<<3B3A82A0DCB87745ADACA2FA7987F854>]>> +startxref +163894 +%%EOF diff --git a/tests/resources/1.pdf b/tests/resources/1.pdf new file mode 100644 index 0000000..ed945a2 --- /dev/null +++ b/tests/resources/1.pdf @@ -0,0 +1,744 @@ +%PDF-1.5 +%%μῦ + +1 0 obj +<> +endobj + +2 0 obj +<> +endobj + +3 0 obj +<> +endobj + +4 0 obj +<> +stream + + + + + none + 2016-11-10T06:47:57-04:00 + + + none + + + application/pdf + + + none + + + + + none + + + + + + + + + + + + + + + + + + + + + + + + + + + +endstream +endobj + +5 0 obj +<>>> +endobj + +6 0 obj +<>/NM(05b46baf-8873-4881-b973382e67de9b94)/Name/Sold/Rect[38.308668 747.53 283.51768 811.89108]/Subj(Something Special)/Subtype/Stamp/CreationDate(D:20161104051921-04'00')>> +endobj + +7 0 obj +<>/NM(04e49b05-c0fe-4e18-a442eb62db809d70)/RC(

this is a comment

)/Name/Comment/Rect[339.5959 772.56729 359.5959 790.56729]/Subj(Kommentar)/Popup 8 0 R/Subtype/Text/Contents(this is a comment)/CreationDate(D:20161104051939-04'00')>> +endobj + +8 0 obj +<> +endobj + +9 0 obj +<>/BS<>/DA(1.000 0.000 0.000 rg)/IT/FreeTextTypewriter/LE/None/NM(7f35f3bb-14ee-4900-b2ca8f24a6d68ea2)/RC(

typewriter text

)/RD[0 0 0 0]/Rect[396.85334 754.1775 476.07408 788.5933]/Subj(Schreibmaschine)/Subtype/FreeText/Contents(typewriter text)/CreationDate(D:20161104052009-04'00')>> +endobj + +10 0 obj +<>/BE<
>/BS<>/DA(0.000 0.000 0.000 rg)/LE/None/NM(75e29b8c-3e27-49f5-9d1691add0d9e97b)/RC(

modified text field

)/RD[8.56139 8.564017 8.561391 8.561395]/Rect[49.683259 675.1755 166.80605 710.3009]/Subj(Text-Box)/Subtype/FreeText/Contents(modified text field)/CreationDate(D:20161104052030-04'00')>> +endobj + +11 0 obj +<>/CL[212.24744 704.70046 231.19762 699.76559 243.19762 699.76559]/DA(1.000 0.000 0.000 rg)/IT/FreeTextCallout/LE/OpenArrow/NM(46f3ee9e-f8d1-4348-a57b21efbbded44c)/RC(

explanation text

)/RD[31.917883 -.000018 .00002 .000019]/Rect[211.27973 690.76559 343.19764 708.76559]/Subj(Erl\344uterung)/Subtype/FreeText/Contents(explanation text)/CreationDate(D:20161104052053-04'00')>> +endobj + +12 0 obj +<>/IT/LineArrow/LE[/RClosedArrow/Diamond]/NM(6404d971-e409-4762-8f4157381a50c0b4)/Rect[385.39955 696.20608 477.3782 730.7424]/Subj(Pfeil)/Popup 22 0 R/Subtype/Line/CreationDate(D:20161104052133-04'00')>> +endobj + +13 0 obj +<>/BS<>/IC[.752943 .752943 .752943]/NM(55705d19-8e3a-4d87-884ff485467f387d)/RD[2 2 2 2]/Rect[65.779178 605.25326 202.0124 651.6411]/Subj(Rechteck)/Subtype/Square/CreationDate(D:20161104052208-04'00')>> +endobj + +14 0 obj +<>/BS<>/IC[.752942 1 1]/NM(2a1c3549-37e0-428b-93ee9b69a3ce9df9)/RC(

comment in circle

)/RD[1 1 1 1]/Rect[247.78653 581.0016 343.54469 671.71279]/Subj(Oval)/Popup 23 0 R/Subtype/Circle/Contents(comment in circle)/CreationDate(D:20161106044139-04'00')>> +endobj + +15 0 obj +<>/LE[/Square/OpenArrow]/NM(d2a8b21c-d4c1-4719-86a11a64a4e0b6ec)/Rect[393.5004 632.15127 504.1475 662.9966]/Subj(Linienzug)/Subtype/PolyLine/Vertices[397.84056 656.3386 404.75093 632.65127 438.3156 632.65127 447.2004 657.3256 479.07295 635.72817 503.47065 662.2605]/CreationDate(D:20161104052251-04'00')>> +endobj + +16 0 obj +<>/NM(a60c24b6-1dc6-4e09-bdec495c6894fdc0)/Rect[69.89996 524.0557 213.81572 586.2813]/Subj(Polygon)/Subtype/Polygon/Vertices[70.477687 567.9854 78.37527 526.5325 158.33824 524.55856 212.63411 550.2199 135.6327 585.75106]/CreationDate(D:20161104052317-04'00')>> +endobj + +17 0 obj +<>/NM(66b71715-3eb4-4097-b2e2f5c4eb0c4d52)/Rect[283.2077 522.9725 404.2485 571.8722]/Subj(Stift)/InkList[[283.32566 563.5629 309.97999 557.6411 340.5831 556.6541 345.51908 551.71926 341.57029 537.9016 345.51908 534.9407 367.23744 536.9146 393.89176 535.9276 397.84056 540.8625 400.80213 539.87557 403.7637 549.7453][395.86616 558.62808 385.99418 557.6411][367.23744 550.73226 337.6215 530.0058 321.82633 524.0839 310.96717 525.0709 308.99278 530.0058 305.04399 535.9276 305.04399 549.7453 308.00559 553.6932 312.94157 558.62808 327.7495 563.5629 355.39106 565.53689 366.2502 570.47177 383.03257 570.47177 387.96858 566.52389 398.82774 563.5629]]/Subtype/Ink/CreationDate(D:20161104052347-04'00')>> +endobj + +18 0 obj +<>/NM(3cba4ea0-034d-4505-bd857d15063c675e)/Rect[108.57578 435.81523 178.93265 476.0276]/Subj(Stift)/InkList[[108.70982 463.30189 128.95235 469.29933 139.44847 475.29676 139.44847 463.30189 143.94681 458.05415][121.45512 454.30574 130.45178 454.30574 132.70096 456.55479 131.95124 452.0567 134.20041 448.3083 136.44957 437.81278 144.69652 437.81278 164.93904 436.31343][161.19043 469.29933 168.68766 469.29933 173.93572 465.55094 176.93462 461.05287 178.43405 446.05928]]/Subtype/Ink/CreationDate(D:20161105124026-04'00')>> +endobj + +19 0 obj +<>/NM(6fe9e08e-7ff0-45e0-8fc78c7d63822c11)/Rect[161.19043 454.55543 209.92243 455.55543]/Subj(Stift)/InkList[[209.92243 455.05543 161.19043 455.05543]]/Subtype/Ink/CreationDate(D:20161105124033-04'00')>> +endobj + +20 0 obj +<>/NM(1d9e69b5-b0bc-43be-8817c8e56e66d22c)/Rect[188.24126 442.2735 193.92711 474.60557]/Subj(Stift)/InkList[[189.6799 474.5471 188.93018 464.80125 191.92906 458.05415 193.42852 442.31086]]/Subtype/Ink/CreationDate(D:20161105124035-04'00')>> +endobj + +21 0 obj +<>/NM(83464ab8-6479-45c4-83e04e59025f9b68)/Rect[88.038318 436.9787 245.77399 474.80393]/Subj(Stift)/InkList[[235.413 469.29933 240.66106 465.55094 242.1605 459.5535 245.1594 458.05415 243.65995 453.55607 243.65995 449.80766 222.66771 440.06184 211.42186 438.56248 209.1727 437.81278 214.42076 452.0567 218.16938 455.80509 218.9191 462.55223 216.66992 464.05158 186.68102 464.05158 143.94681 468.54966 135.69985 465.55094 125.95345 464.80125 118.45622 461.80253 104.21149 461.80253 94.46509 461.05287 92.965648 466.3006 88.46731 474.5471]]/Subtype/Ink/CreationDate(D:20161105124037-04'00')>> +endobj + +22 0 obj +<> +endobj + +23 0 obj +<> +endobj + +24 0 obj +<>/FS 44 0 R/NM(0ac8a6cb-a18f-4526-b5e2ca95632913df)/Rect[313.39085 381.29405 327.39085 401.29405]/Popup 25 0 R/Subtype/FileAttachment/Contents/CreationDate(D:20161109142650-04'00')>> +endobj + +25 0 obj +<> +endobj + +26 0 obj +<> +stream +xœ+T0Ð3T0A(œË¥dh` ^ÌÈh{ +endstream +endobj + +27 0 obj +<> +endobj + +28 0 obj +<>>>>> +stream +xÚ3T0BC=CCK '9—Kß-×@Á%Ÿ U¸ +endstream +endobj + +29 0 obj +<>>> +stream +xÚM‘9R1 EsŸB'PYޝ’‘pW±U“p}ä/yèšäý-϶P¤Ÿ÷iÿ^ŸƒÐW(ô"7úÕϟA&·Dk¢o•{",‰ +§FÒ8 Ã&Ǿ¹Ò +ÏL‘sRn³Õ\!s?(<š±Ö5 ÂÆÀR8w-¶ÐX:¡O:·L{šÑ +¦bI‡Ãђ©{³1¦®û ¯@áɯCï%óè»®ˆ{À™+PWÑI2ÏjRÚ¾‹ÇAø‰‡ ²ç/å"ÞëìCiÌ: a£€ë.Š3¼ùᔏ:Ø…Ï!â~¢c¹73v±ãà1~±æä~è}ðÌÿæ{™7@Á&×]ó.>8V÷¯ó”C½k}¸éÎ¥š3H÷'ł*gŽbAQªõĸ&B«±Ú¦×MÎmÿA‹œ´ +endstream +endobj + +30 0 obj +<>/ProcSet[/PDF]>>>> +stream +xÚMÝ +Â0 …ïóyeiÓ® Áé•JÁ{ÿ܍>¾é6DÊiÓöœ„©Š2Õ"ó>\Á ëÚ­Éã¤dX0xGÎ ‘‚5(Bµ©p8Þàeú`»éà5ÇÕ ¹èEœ&ù ?> ÈÐBìV”Pî‡ÚãÚ>/ProcSet[/PDF]>>>> +stream +xÚ]—ËŽ&5 …÷õyÉÄqn–‹ ¬X€~‰=ÍMHƒÄlàñ9Ç©îvZ£_ÓN9Îçԉ’$©ä5WúúÇUÿýôý¥éß«¯P‹$,3l¹óªF³æŽ\²uzKK֊Ç6à6S¯Ú,‹{k¶ZÓ,ȱՄ¥¬4˜‚”±ÔÊEÅMÓA÷ ¶¾DˆÚй9[ãìÑ·{kr“¯ža®-F_HT7§*ÜGÇÆ-XÐRnÂç‰Â *LÍs2:¤ðy˝3L÷ÅêâVçÚ¶iÌt,ÊÅ 9!ãæÛD“Û4—ïâ,3«Ý-W݋7@®’EÄ£‰¹Ù h`Ál ,Ô@dYدÀH`Á›¬‹+áoV‹qó# z`)ÁFd±š-° ,a´FðvG ‘Rs8´íàáÈ @´51âŠHG LBÁD(‘;‡W(©yE(ÁâJñ„‚‡E(Ø=BÁ.”@ÄJ ç>@Õzgñ +Uq4#l‹Pµ¿S4G¢¤9#jšö!j©ãPµh9d-ÚÞéR8„-:eÓ>¥Í‘¨mFˆâ¦}¨[tò–V}Këï.m +—6‰Ó>5Α(rÚQåŒxÈ\Ú:t.¨|‡ÐYËN(”+µ]½ÖDÈþrŠ¡ÔaÚ,Ô,weªÏÀˆí¸ÖžQ+#ì@»TñÍXO5pXAI”À¡^ž'À¬…żûŽrBË K ۅ4Àzóæ0Ðud I̱»ƒ×`ÀëÓ=Êä¨'»{€æ¡ÜåUþ ý ?‹cAzÜ·¡.ó&íŽßÙa¼„N$É7ÁÆ7sã>ßý 5pº?6¾ ÆGIŶAÈÀÜ  Í„¿"Á=¡HÛ2ýÍ ·ŠšÇ¯³zü>}OÑ[ÍGðF¹§”ÊÂïïKZCFÝpmèkŒR¦ V—úÕ# b΄æä3 éÛÃêžÑ{ßI×Ð׬û-ñ’gÝ·ƒÆmâ©òm•ú™p¨}ˆ*^›n&tGQì÷\›i©Ÿôy2ñìG&ÌÐȤ»Ì¦j÷«»™X~*Z9˜*n ‘ %Ð"ì~2±HF&DÈTË}‰ze’µ/,/L¬ÓIÊÄ “ŸßÀÄÛDd‚­'»Id‚m‘©ìs˜P$2ñ<¿1ñ°F$ý%¡£öSv܀s[·.^`P +,°°Ù¿¡à̶ˆ²p_ (¸hh@Yý¬y (¼œ;5’%Í;Ð +Žÿ!h8E=Ã)Ê×±SͼŸ¸G-s‰eê¡d^ ßPÆ8uÌPPÑ¢Šaž"Æ@Ô0fG Ã<h²ïðv?Í«£ÛWêRm»/\Чßêý«·üwÏWúóúåúøø/=ýðéúçÿ"RTtÎf»W¨u <¹>hú`‰eµ¦¯¿qöÏéïëéqÊÄošO|²ôôøõúø]çŒÇï×7¥ô‚_-(îø}ÂÏîÿ×+ŠçsÛýiÿ]ôÛôøëúü¸~¼>ƒô²©ä +endstream +endobj + +32 0 obj +<>/ProcSet[/PDF]>>>> +stream +xÚ]ÍJCA …÷yмÀM“ÌOf@º¸¥uåBpïo+ؕï±½](!$9ð$Ɔ8¾‘±"î®ÉÍÅsph–ày2±Þ¸ö.Q+çôO¹Õ¤– +­IjþËýñå*Ý3æív¦àTON&§Þق/Ä{z¤ÕøæùfC_Ëqƞ 6G5‡MÅòRrá§MƓ†•Õ…àOšé ŃzyPy꒣ðx¦Õï;WºRÍMµÌȄ~‡Ìèm©ºAõ³¦iÑ.L¬y¼ÓvÐ-mqóàEDª +endstream +endobj + +33 0 obj +<>>> +stream +xÚ]޽ CAƒ{¦`‹ŽŸ Òç-‘âRe)èIi"*[ŸËÜóAÖŽÊæC.ã7yJ7§&V5ºÆ´¥3¿)E'§ $ö@žÑZ÷[;¢ÆpØôâ}È*°4æÝFɞ¢ÿg˜Bôâèý‹~Hå#9 +endstream +endobj + +34 0 obj +<>>> +stream +xÚ3Ð375V0@"‹Ò¹ ôL,Œ`$H0ȝËD¡œ+ÚBÁc R¸ÌÌõÌÍ-Ì ÌõŒ€J ôŒŒLŒôŒ-,ŠR¹2¸’¸ªÏK +endstream +endobj + +35 0 obj +<>>> +stream +xÚ]O11 ÛóмÀJÓ:齀'оO%hOBYb˱C²j™óº‹©Íåz×·Ôæ`£†*SŸ'CCgÓêdh¤!KU?ˆˆñ8ÉSÑ:²çvØø—1/c=t°;Ì|G,ä&,4/³ +endstream +endobj + +36 0 obj +<>>> +stream +xÚe»‚1 ƒûL‘ tNüŠ' ‡%( bÿ;ä¿âŽ&/’¥VÅ1®}¾ß†VâØšá՚ïabH'эàþ¦ºâ—XbӓÐ}ς¤RáÈ}H\–œ—ä1¬ çP䏎ú¹,¸Jú!/›Ò¶:Œ%òÐ¥åüCWfÕaÖ¶hbØU$ZÅ,ˆ7˜Ïñ_tõ2‹ +endstream +endobj + +37 0 obj +<>>> +stream +xÚʱ Ä@Dќ*¨`ìÎ\~U8ðE׿d¬ÉÞ|W›}?ÂF¬PžÆ.êON£•Q°9nñ°ž$Œ5æXUJ:Òûmb¤²Á½ä/àD­ +endstream +endobj + +38 0 obj +<>>> +stream +xÚ]”Í‘\1„ïŋ€B ôïv>;ÿ«¿fìY—kkë H@74ó÷ýÛ+NZÆzj¥ñÿüzÅ]¶*ñ¸-Gö<õT]‹uŸôk÷`n[s¹Q–¥œÓöyÛ5.ö°ÍWä®ñßé‡ÃjcNs}ÌÜv¬ûe›¯ +…c{ÆWœvß$)-O%€ÖÐ7Ÿ\Û"wÛ’DhÍÙEÇádŒÎªq!y£kÞ8Š “'²s¤qAªqÉ4°·)’ng5{@E„èPuºÓZÕ<Ü̶Gg8{1=Í7ӂqÀgzðÍîԏŽ÷"Á¼¶§hüx ÆYH Ž-àüÂãv³#àæÐÿ(€ÛSnò䨰·É¤dSŒÀ³¡ú…,m[Ý.7wAO~т§ÐÓOBÓ lé'cÐ>pÆ4?­£±¬„$È-卂ìh["à^{ÊNßD‰º:΍(@`иί¨~rˆ Tf‹<p”ª m{«à3eµ¸§H¦n¤]í“&òuÿ3yfªhU@»µØA”¢¢Xӧގ´ê…6©‘tn^5F¶–ƒ‡ÉUò+â04)ˆ”W †‡2 ½ÖËaû×}CˆÖ+èý¸Qk¼W2õv- Z¬×Ñ~ˆÿxý¾1úç +endstream +endobj + +39 0 obj +<>>> +stream +xÚM’M’[Aƒ÷>Å;Õ4?MŸ û̲Îý·ùÀה.ñjôYü~ÿzé*9úxšØÚÏߗjIYW’Š>ªWô\pˆW>ºKnlð•}ïóç¥æ^¥Ãµ”Ró¦ÚŸïã4÷¬DãÍ(Y»51ƒ›ãÛU3–ìñ¹DãQWÉ[óØäúy< +‡ðõҍjŽHöj;°nŸŠîÄ*a‚9Æ.ûó½WS¹ïB°«Ú»-pdé|ÈÈ#%ÕÌP ©Û»«DÖ(h¾ñf@L­©Ý±dYÏ0Ò¼ƒ-»àÀ K5ý!/ÎÊÛÏô{uǒN‚ð¥íð6ýdŠ!haDM[&räÝIòü$`µvNe#ذýW".ŸÎ Q§ŠÞÏIôÿ¦ïŠr<{œ¥¦yè¬úyDOZÌ xûlÜQça]ó–s†A€Í bÍ9’ì£áÜÌß§œøo|GQ‰z6;Å3Îs3[ÏÁƒŒèþž ö¤ÿÎbÿ šï +endstream +endobj + +40 0 obj +<>>> +stream +xÚ3T0 w.#K=K##SS=SS…\.C S=SS3¸ßÌPÏÐÎMæ +æI‡Ü +endstream +endobj + +41 0 obj +<>>> +stream +xÚ%»Ä@CsWA ± \~.áâë?5‹‡€y’@JÒóý\šÅ+ ØôÁ%« ÒL¶DÕ`y#8Eé7y(ZQ¶pÒê­>¼üå:"Y碜±¬aÏì„uS k.`ìz:î빺!Õ +endstream +endobj + +42 0 obj +<>>> +stream +xÚMSK®AÛÏ)êˆÁ ²Ï;BÖ¹ÿ6†nE£Y´ ¸À†‘ÃøýþõQ r±ãÙ¤Ýç/"El‘K®q·ðÄû¨3e +pP„œ?u%EÄH2·âò`¡.?“—aDƒa/£c"E˜Nå½ÚóBDožÃ—n=‘DE-ƒïࠊ™Ê0 ¡E.ÁÈt +„®½O*„†óúOð¦â‚9¥B÷¼ÌÝFŽé…53ª‚ ŽS‡!—,ã8Rè̌ÖämÀuóñ2˜ãMÄð–2¼EKKêiÉMrçK%öüî 9CãE®ÁAVP-Žã£Â§»øT˜)±3IÙc cRØÅëÇ20­éz}cTGì.’m-`¤ÂÚG*9wßWs+V7pÏPI9÷Ÿzªq‘;¼NϽà+BÉCp[:Îâk7Ï`‘Ø›ìaÀ_p“1"–§úò¢¸ÇèLˆ+à ûu²bŒ|oËH;ƒó> KQ¯¼m¾x:¿"ñ<+[ÌãLðí±<»,+dþ8Œã†`ÓXã+ì8ùbt“Ùbho˜:·0kÓXô";jí.t Ís›®pHv>êm§oÐfë—!·Nã? ¹ÐáZaôÙØ…>Wôóùېʪ +endstream +endobj + +43 0 obj +<>>> +stream +xÚM;Rƒ1 „{B@cÉï–&U8B +H&4ar{vý»È¸YÙÚO+»:Îï—$åù8‰ëMŠž%YsýÃýUšµ¬ÍrÕ閊&ËMïÐ帾˻¸Û¬êÓê@›=¬²NëC®O9ŠdA€Ë]»9Bd«Z }Ð݉ö¦¾fD`f ¾»^ä-±¾® äÅ«€Ś8YGžX¤%6}ȝšY_ò_D¿±¨øB¡{5Kk—àø-™xXŠUpÝVwÿS>a¯+ v¤AÀ°Çܒö²ô„{´ÝN÷?"¼N¶ +endstream +endobj + +44 0 obj +<>/UF/Type/F>> +endobj + +45 0 obj +<> +endobj + +46 0 obj +<>/Filter[/FlateDecode]/Length 16/Matrix[1 0 0 1 -.001419 .000039]/Subtype/Form/FormType 1/Resources<>>>>> +stream +xÚÓwË5PpÉç ð +endstream +endobj + +47 0 obj +<>/FontDescriptor 53 0 R>>]>> +endobj + +48 0 obj +<>/FontDescriptor 55 0 R>>]>> +endobj + +49 0 obj +<>/FontDescriptor 57 0 R>>]>> +endobj + +50 0 obj +<> +stream +xœåZaoÇý.@ÿá/Ly±3»;»c  +»R4­‘äCÃñd³¥H—¢ã8Eÿ{ßÜޑ}”iG®Øbó4Üۛ}óæ½9±ËN›/gíÕôÕbsÑ¿i®Vëfó¢m¶AÓf1ÿq=]¿¹8?ûñÕ|±iŽ;Ê¢^ +î¬çg¸ç?¸ßj=k×Mýc—þØüç¿çg—«Åj}sûò¤OÊäa󔜿h|ÿ¿gÍäj¾XØžáËW‹éóá»ý—ãùÙ|yµjšƒ%/WHÚҖœL°Êõjöxºiññ‹Ç-D>úDÊ၏ï'_ êrÝN7óÕòH(í….§×2ù~µ˜Ù 6óÍ¢»ð—ÕúŸÍ?&Øíb¾lÿ¼œÝd`Ý^n¶{5ßüâ¾Ãõ/CqÁaVŽÅ§‹†Õ©ŽžC Ù.•àe•œsˆ@FâRH”„ÓWçg›7/ۃ=%Æ^¿ßL¯_N€§ŸÚõf~Ùîïðé³ßèt0py76/æ7 þ›6—«ëk»xAGàñhwûBHP—4•"|”bxHäsNA=Žþ¢ފݏ9dzòCûóæ“ƒcr³yS³e©ûi=Í_Ý Û«+d¢Ã>½¸ýÃ×óÙæ>t2™Mo^´7dޅ7ï)ËàÞp¦Yؐ§Áþ¨22꥔±k÷ÃTv$¯×ó ²±±S8‚Ì‚îb:™ìý2ß’8ŒéºÛàßV˶;å¬[ã`Uq ¤Øj)3Ç +ꢢq¸äÅ ÂØ×ϒBŽÀÕÈìëuÛþÐ'kø÷Û<þ^€LøôhrœÃÁyè¥t_½pœ_ÍÛY‡Ot[»ßJ“WRHSQü§BiTÔ= Šv++…R —ŠQàҁ’XöâcJ R–$õZ&`´¤™@´§ õScò½:ï½A§ýùåbºìp'»èîÆM +÷…›¿¿l—Z¯W¯ß½^áŸïvâPA +N‹ä©c´ÀNÉÃ[ˆïŒÄE‚¨€Ÿ®$‡0ÀA"û¢%ÇmÀ–þ:¯dl×}A+ éJ.$KN¹¡5€^]ÕHFÌØL ª( +j®#áCLÿ.Pè]N¬È £çàaJ²S®Ý?~ N¶ñTüâÔ~ßKÈ¿œ•2€×›R&+ê è¨DFœ{‚ÿ5%Ÿ£d$ÀÆåNÍÿûÕtÝ~ò^üëß§2ƒàV|0ðz§Ý̗Íå|}¹hït6¤§ B,Ò¨ç¾G$¢{º\Ð>µ'H0eï(£ß‹ßuöcÅÔùŒxWJ‰Ü_9Ãd¢¿æãSÃð·ÕPe‚ç“ )MæîÃ:ìờnvA` •”М (Åæ% ©US‘Щ¨ ÅÅÀB™͈ èхµêX2võdµxÓõÓ£-4»m6 )ÂA«YiqØÃÞ (Z»4s–€t}ìœc"êù5Ú(ˆ'ˆ[,UŽ…¡BÍJ ôÏ …èà¦ç’ ŒL=ÆL(_‡*õÁÑÂÜÖBÖb7VIh÷1û=]0†E­kß >ӊ0¶'ŸZòGnÑꊱanœ¶_ Ùd뙡u¡ÖÜ@ô‰­¹"—Úµw¤üóÕòÞÁÄ1g)}ù@1&ÉN ȚÌJÇרB ‰%{ –Ũå~àD©X•ÀÃíÜMâè À»+ ‚¡’€N Q»/#*A™ªW‘¬Œ) øÈ= wë èÈc'u_Ÿ)Ž;p¦Óq?2ŽíM +û lÈNYVä]‡<ÜS&ä'è[@¥}ÁÆÃ'e ¬u×HÝ^ïM¾Yþ놟ڌ.Sð7ÂQ;ÜY¹WUðê4«n%qËJܛO\]*ˆÓê÷ ÂÉæª +Yºñ«E%…åÁüEì«EpY0°e»¹dåBZÌñَêª;—ÝíÞ^(D§°iy·ʑCŽ\]ÿ +±l\õtAÁûÉ´¨äðøè>Û¾5ÒÜ­-–\{”uKžiïm Œ%š“†áŽÑŒ¥žn±‘d€=r`¤~w@È´0îècu1ö$:§–´2Q`4T`–¡;¹Ô”¯|Áó„]”‰ àU‚M(êî990¨¸·H-@ْë7ãø=¾ c¾>ãÈ!D&¶ÞË'°#kÚQ¦à”…·™Û"©ë%Ôxî=êN‘ŒZSP2©¸Uí@š€”Ýl,ˆ8NÀN¨jÉvÕu8QŸ+ØYç÷äM<…z{Ù{Ë!6mÄ¼Ý˜#ÌDámÛ9Üþ³Ï´U˜ÎD¢Oi](ËÇmp§`EÀç•lPeˆIÿìUtýŒ3Fÿïô± æ"ª Uaö¡­Â¶€Ú- §QúREKrÁúZÖ%Æ ÖR|W(¥Žu ÀnD­.F+@² ŽcîÞ»Ù`Pü£a#w„V»Ìre<„¡VÁÖÊ0ñ6$WDÇ!âm‚'±`Ê(¾*Žº£¦Òí.<Æ6ûÓT†7Œ‡:K8õÃ‰õm(‚+mǚ‰±?ÈBڍ: lßÍt%v¿“ñ8¼ã±8AZ4 d ÷… Ö8¢`7–––RƒcÊ㐽‘0AßDßÛ6j‹‚mÃnv&­KžØ¬ÞÇátÞ>U?¢—€åKæcA‡ Êcf£ßl1 C¤uëV-J̘ +j¢§[ åŸÐ8$œfpfë(ÕÑ\—0À=)Žm˜Ö}Þvҋ·C?…àÇæÿBNcŠŒMg¢MrÜƚ٭°–º#‹ƒ €ð$H_\‡(?ˆø¼ÑtÒ õš>2šJqöÖJ¡2D'qÁ† œr%7ˆ{ˆ´®,Q*UUgow~âƒ;bÁJ°EžzÚ ›`Üaî‡_²Q\±®ìwª;MÀwúW +£è î³ ]¿Ñ·{X} hêJß^ˆ1F[Ccsßó?oŒÆ“^ÞWŒ~ds÷èihÑ4Èè/Öޗ¾×q´7NTRv¼ϯ€'%8ӎ‰>˜õ`“P&0ˆ¡ŽƒÆš2rá`4·äñ¦ ¸94d€ŒwºÃ~ÙÖ[3Óúr}쾨dÌ”í¥¾$lŒw® +`9ñâ³öï„GÃàê ÷v:aÌx|†¸ð½5Šx,«(.ûCAH@0yÒ\g"Öi3ƒ´Äaä`͂Ðp´èÖ/ˆ'¤«Ùè„všxDÚá†Hž»ŸJ¥ØëûØÛá¦è> +õX;vŒ”0CŒåíØ^ð¬Û–etb(ïÏÑ\'ÜßÓ@|£qcÚ:¾YRÿZ öSEµçƒøu³c² þGd~»ÿµŒq²ÃcFó°ŒXªÜä]ñŒ{檥fƒ/Ðö ãí0ÅHêËiˆ9PŠ +3/ö¹é†$ùá! mÃP°Q2ô¶&™e„΍[ÏϚM{³q/gWÍËéó¶ñMó i(5Óårµéçæ¢i—³f…ˆõêùzz}~ö?;ÿ +endstream +endobj + +51 0 obj +<>>>>> +stream +xÚ3г0UHç2TÈäÒwËUpÉ2 ôL €0›«PÁÌ2TÐ5Ò35Qɹ +¥ +\“ò ë +endstream +endobj + +52 0 obj +<> +stream +xÚ]‘OkÄ Åï~ +ÛCɟƵË. 9´]šRz[²: BcÄJ¾}ÕÉn¡Bò˜ßøô1&‡æØhåhr¶“hÁÑ^iiaž+€^aPšd9•J¸­Š1v†$ÞÜ®³ƒ±ÑýDªŠ$ï¾9;»ÒÝùxú:|*ø›§$y³¬ÒÝ5´Sn ´]Œù†Ñš’º&zìKg^»h®¹<æi™1^Ìç¸õ?Vë ƒ‰IÂl:¶Ó*õ«¦Õɯš€–ÿú]×þoû“ß~—<­,žcµÉž!Qöe„ }›p´3†…ç9B^ ì♜ÇÌ·t!~˜ø}2b±Ö->KœG˜„Òp93™àŠß/_G +endstream +endobj + +53 0 obj +<> +endobj + +54 0 obj +<> +stream +xÚ]‘MkÄ †ïþ +ÛC‰I󱁐Ë. 9´]º¥ô¶$: BcÄJþ}ÕIS¨ /óŒ3¯Ñ©97JZ]ÍÄo`i/•00O‹á@;¤"qB…äv‹ÂÉÇV“ÈßÖÙÂØ¨~"UE¢7—œ­Yéáz¾|ž>$|ƒI؉^#Õ@e¥]=½-ZÁèe¤®‰€Þµ}nõK;ü3÷DŽ•qVdّÑ=ÿ¾j IˆcŒOfÝr0­€TÌ­šV·jJüËXÕõןÜõ]V{˜!JË yА#DÉË3¢Mr0Aˆ’÷±ç&öÌ:„(Å1Ìü;ß;¾;Ãcœiá[‚Þ ©`ÿ9=i_öˆíŸ +endstream +endobj + +55 0 obj +<> +endobj + +56 0 obj +<> +stream +xÚ]‘MkÄ †ïþ +»'“l>6rÙe!‡¶K·”ÞJ¢“ 4FŒ¡äßW4… +ú2Ï|¨3ìÒ\%-ew3ñXÚK% ÌÓb8ЩHœP!¹Ý¬pò±Õ„¹äÇ:[ÕO¤ª{uÎٚ•î×ÛÇå]Â7˜$:öb©zh(+íêécÑú FhDêšè]Ù§V?·#Pæ¯ùŒÓ8IËòTætw¿­hìßÅ'³n9˜V @ªÈ­šV7·jJüóŸ1«ëÿÂO.|—$ª=LÓ`m’Ç „%Â!Gˆ’—{„(90‹ƒ•áE9 ̛X3ë¢çð‘ß'û?ù)ìÝâ‹1®‘aT¡I¾=RÁ>M=iŸö½‘ð +endstream +endobj + +57 0 obj +<> +endobj + +58 0 obj +<> +endobj + +59 0 obj +<> +stream +259.521 417.906 m +257.935 417.332 256.569 416.132 256.599 414.411 c +256.635 412.353 257.791 411.113 262.822 409.268 c +270.304 406.543 272.053 402.625 272.12 398.761 c +272.234 392.252 266.891 387.916 260.678 387.682 c +260.635 390.118 l +262.64 390.783 264.346 391.737 264.389 394.048 c +264.427 396.695 261.301 397.732 259.27 398.579 c +253.94 400.712 248.985 403.02 248.87 409.613 c +248.758 415.996 253.309 420.024 259.478 420.342 c +259.521 417.906 l +270.765 411.465 m +270.781 410.499 270.632 409.447 269.414 409.425 c +268.406 409.408 268.014 410.199 267.874 411.037 c +267.358 414.136 264.698 417.282 261.41 417.939 c +261.368 420.375 l +264.014 420.379 266.856 418.832 267.318 418.84 c +268.409 418.859 268.299 420.369 269.391 420.389 c +270.693 420.411 270.634 418.982 270.639 418.646 c +270.765 411.465 l +249.081 397.519 m +249.065 398.401 249.213 399.58 250.389 399.6 c +251.229 399.615 251.828 398.953 251.967 398.242 c +252.613 394.892 255.037 390.776 258.746 390.085 c +258.788 387.649 l +255.133 387.669 254.31 389.125 252.798 389.099 c +251.454 389.075 251.604 387.692 250.555 387.673 c +249.253 387.651 249.226 389.204 249.222 389.414 c +249.081 397.519 l +f +287.488 418.394 m +285.527 417.646 285.381 416.383 285.399 415.333 c +285.779 393.581 l +285.797 392.531 285.987 391.274 287.973 390.595 c +288.016 388.159 l +279.099 388.802 274.817 395.91 274.671 404.267 c +274.525 412.624 278.557 419.877 287.446 420.83 c +287.488 418.394 l +289.863 390.628 m +291.824 391.376 291.97 392.639 291.952 393.689 c +291.572 415.441 l +291.554 416.491 291.364 417.748 289.378 418.427 c +289.336 420.863 l +298.252 420.22 302.534 413.112 302.68 404.755 c +302.826 396.399 298.794 389.145 289.905 388.192 c +289.863 390.628 l +f +318.198 389.316 m +307.112 389.123 l +306.944 389.12 305.557 389.18 305.535 390.398 c +305.525 390.985 305.896 391.37 306.439 391.548 c +307.901 392.035 307.855 392.286 307.828 393.84 c +307.444 415.844 l +307.417 417.398 307.454 417.651 305.977 418.087 c +305.428 418.246 305.043 418.617 305.033 419.205 c +305.012 420.423 306.396 420.531 306.564 420.534 c +318.448 420.741 l +318.616 420.744 320.003 420.684 320.024 419.466 c +320.035 418.879 319.663 418.494 319.12 418.316 c +317.659 417.829 317.705 417.578 317.732 416.024 c +318.198 389.316 l +320.045 391.785 m +323.439 392.306 325.319 395.279 326.133 399.2 c +326.327 400.128 326.826 400.388 327.708 400.404 c +328.674 400.421 328.772 399.624 328.786 398.826 c +328.948 389.504 l +320.088 389.349 l +320.045 391.785 l +f +344.021 421.188 m +351.159 421.396 357.208 416.545 357.901 405.719 c +358.535 395.859 351.54 389.898 344.569 389.777 c +344.527 392.212 l +347.082 392.593 347.167 394.989 347.139 396.585 c +346.827 414.474 l +346.799 416.069 346.631 418.461 344.064 418.752 c +344.021 421.188 l +342.68 389.744 m +332.013 389.558 l +331.089 389.542 330.038 389.607 330.017 390.825 c +330.007 391.413 330.378 391.797 330.921 391.975 c +332.383 392.463 332.336 392.714 332.309 394.267 c +331.926 416.272 l +331.914 416.902 331.979 418.037 331.176 418.275 c +330.331 418.555 329.531 418.667 329.513 419.716 c +329.494 420.808 330.542 420.952 331.382 420.967 c +342.132 421.155 l +342.68 389.744 l +f +191.56 374.148 m +186.591 374.062 182.49 378.02 182.404 382.99 c +181.705 422.984 l +181.619 427.954 185.578 432.053 190.548 432.139 c +414.512 436.05 l +419.482 436.136 423.582 432.178 423.668 427.208 c +424.367 387.214 l +424.453 382.244 420.494 378.145 415.524 378.059 c +191.56 374.148 l +409.058 382.81 m +414.027 382.897 417.987 386.996 417.9 391.966 c +417.368 422.42 l +417.281 427.39 413.181 431.348 408.212 431.261 c +196.865 427.571 l +191.896 427.485 187.936 423.386 188.023 418.416 c +188.555 387.962 l +188.642 382.992 192.742 379.034 197.711 379.12 c +409.058 382.81 l +f +endstream +endobj + +60 0 obj +<> +stream +xÚí]y`”Õµ?ç~3Éd’If&“Y²Î’I„Ȅ$ˆ ’ HØ£ÈdqE¢Ö¥€‚;UZ­Uq™$XhAÁµ"ZŸK µ€m5U[é†É¼ß½3BÐÖ?ޟùNÎÝ׳Ýs¿L2ÄDd¤fÒÈ=oõJ÷“© l(yŠ(qÃÂe‹– ûIõ‡D†J¢„Т+®Y8ðÅýz¢Ô«‰Ì¡¦sæýl^3Qn;úT6¡Àø @þ+ä󛖬¼ú•­¿L”‡1³¿½bé¼9tâ'ÍDÿX2çêeé¹I­úíÝ˖®XÙU@ӈVg«ü• –=»Ï1ù y@÷‘înÊBœ«Í¥\¢ÈÑ~ÚuêPßÕ‰ˆÐ{j £ÏTÀ}*œÊ¢1ͧô„î¢P6˜ß¦'(Di(?L×Sî¡«è}šù¥z„¾¤NM‘.²ÐZêâ5ô èUIïÑÚ$‚š_÷91ó@mßD¥e*ÝO:„‹#FäÛDŽ¢×Túµ6ÛPù+ïÓ½™K?ã 8¢{†Þ¢öê¨ëæÈúÈÖÈ6J¥o´œÎý‘A‘%è5i]4ÓCtÄH±7òc¬©kXKÏÓ¯Ù¯#]#Yé"´þm¦]ô+:DÿK'˜9‹¸™ßãÃzê<Ðu r~dnd)Õх4‰šQ›Ãý¸ZÌÐfhOktþ¡ëX$cO¥Õt5]Gií èCú˜5aSÅ4íiÊ¢‘4ƒæ‚š÷`MOÐt” \Á#8Ä·òSbµNë<ÓQ(8VQÿ.Ú +šþœž¥ô½‹1¿M5v±Ÿ§ñL^÷ð|/ÿœŸâgøs¡ÿ«iڍºWuŸw‰#FžÀ¼Y”MnêÎTÒàçAú3öWÌ%\Å¿~Q¢±.¥³«kpä¼ÈÚÈ+‘ÈG…h;’j±ç 4«¾†n¦=ô*ú¤·é$ýTÒØÈVÐÂÍ>¾ˆ§ð*¬âiþ’;…ü«WˆVqXókuÓuÏtîìÊèjíú²+Ù GöGÞRüŠyjÀY´ŒV(Ž=‡y^¡ãô':…98kËã±ßÍÿ( q2ˆÄS"¢Ô6ioè\ºÍ]v-éÚÜÕ©ˆL€li¤'UF@š¦Qƾ Ô|„žgÚ =Gè/ìä\ÈçóÅ\ύÜÄKy/çëøzPõ ÞÉ{øÌ:‘ 2@'¿˜'n÷ˆâ€8"Žk¤MÑêµåÚuÚ=ÚNíí:³®D7P7Aר»Fw­žôZ‚ÝðÖ·Žo—tÎí|°s׀®Ú®Å]ë»^ê:Òõi$9²7r‚h ÖØ@‹°Æ5Øÿ­t'= ùxkü=}FŸƒç-4NâL¬8Oñ­란•Oç^hâËAÿfÞÁ­üïã—ø þ5ÿ†?á/cõhÁ4±{xPìañ!à”ø—V •håÚ`m”ÖˆÝܦݎý< }¢Ð ]†nnŠn­î5½¦Ÿ¯¿_¿U@ÿºþÏ æ„Kb6âŒÁ£½%^ҍҮ í4IhڟÅoD×ˆÓü ‘Ã/a¶m’6IԈ Þ)_B¶Ä­ ž°‘9±QŽ!¶ˆRmº®@K¡•Ð73Ä­¢‘ãè´ I[­ÛÅlm«înÝ(þ€ÖbN&þ;US5ïÞ£åàP©ö¬îm9¢Þ }«_"L‘ÛtŸé…öØÁ‘,´7ywð$aµâNò!oæÄçC?„äïâéT©;¦mãÄÇ(»‚îᗰÇ=t…ØÃ?_*¡Wò$Þ¦ ¢x9¨1œ.÷’W,^Èó4úßÄÐÜÓàM¾XH:Í$æÑaÑ®¿ÃV1€o€œ.¡õ¼ŽJ¸“÷Ñ[â.Ê ´_}ëê,üm·hc©…OëÞн!té%Ps ¬GòlÄ4h¦G+€ÔT’^”@þgÁ^@qНWÐe¼Yûÿ\TÓDZ ­cøþ®Sºjm0(¶Ö¤&a¸ôA}Ž®ÿŒFAáµëºÔ 붺4–>b;_ʓu1^‰\L;ijºO"Na½†u=ÇAΏ¸yy$™'CÂ/Mx¢s‹n½îÝ*Ýõ8›NÃjÞJwӃô2N“Gqn‚Ž€š3a{.Ã1Êiv7ŠFÃ*ºIt1ìi#¬äBúZËûœÉ-8¡Æƒ—¢ßBºå+pB]G7@ÿo£ °÷Ócô®xR<¬yÄíâ±Z\FÑGÚkZˆ/¦ÃºëÖÒʧɜŽ™‡Kyè·!òfëOY°þÐRÈ}äóȑÈ㝇0ÞcXûÝ £éó„*¢‰üw]&ëC£§M U Œ^9lHÅàòAË”–ø‹ûôË÷y=î¼Üœì¬L—Óaϰ¥[-æ´TSJ²1ɐ˜ ×i‚©¤Î7¦Ñ.h ë +|cǖʼo +æô(h »Q4æì6aw£jæ>»e-öjж u·d³;HÁÒwÏ>Xës·óŒÉõHßQëkp‡;Tz‚JoRiÒ:¸ëœMµî07ºëÂcV7­«k¬Åp-ÉÆ_Íci µ“‘LF*ìð-kaÇ(V á¨Ñ"È`¢™¾Úº°ËW+WÖúÕ͙ž4¹¾®6Ëãi(- sÍ<ßÜ0ùF‡Óüª Õ¨i 5áD5û2¹Zïn)Ù·nC»™æ6úSæûæÏ™YÖæ4È9,~Ì[v\{Üy&‹Á­5õ·õ¬ÍÒÖÕ9/sËìºu·¹ÃÛ'×÷¬õȰ¡c ¯è7¦qÝL½D?ōÙÄ- õa¾SºåN䮢û[૓%—»ÃI¾Ñ¾¦u—7‚5™ëÂtÑ5žÖÌÌЮÈ1ʬs¯›Zï󄫲| sj³[l´î¢kÚ\!·ëìšÒ’³%JؖԴX"ÅÔ3± »N¥Ts™Q7eY®Èw>"ìžçÆJê}ØS¥ TÒºy•h†§Ñ+<¹,œTӸΞûýáâb)"‰5à)Ö8J凔–¬n—ù–™Ýˆ@>šÚÎiQò{<’ÁëÛC4™póäúhÞMs³Z)Tæo‹FY³/^“1MÖ4Çkº»7ú É;IzýaCA÷OšÙž^×4"ÌöÿP½ Z?~Šoüäõîºu1ڎŸzV.Z_Ù]K…Ókêµ,K‰,MÕB(gv7–™ú”°®~”PÏoO4@*U »Ç„͍c£aƒÑãùÚ#_É^*:Ó-¶ÌðÿÙùÀYù³–—²NÂubüÔëÖϪ ´nÝŸ{̺ÆusÚ#Ís}n³oÝ.x ë–Õ5Æ9ÚÙ½>+êêw¹‰BªTÈRY(3n™¡ñ Ao…ç(Ûgí +5«Z*PùyíLªÌ/cš×.¢eæèDj¢|×yíºhM(ÞZ‡2C´¬9Úº(Öڀ³¬Ù ·¬Œ>ÒjÔL­ï)JÉJьš#GueZ3|Y7½ºÌhߒ!ÊÅhq<ÑWÅ«éoº>²~äú$ëÎyÿ¶›\ÙÅÙ¢2w\Öy3³fä-ͺ" Y[²·ä>¯O[eߝ}@;`}#ûÜÃ+–L·›˜-9G¢ÎcIN™šØN¼ »nç!‡×àÀv/µíµ²µél.OñSÎv.¹Å¡ù›YË'tt˜ÿ>kyÇqªê¨ê°/ vÍƒÒøpʔñá|l¨ÕnK€Lí̲ååŠöÈ•òi YË?»}pùÐaC—ãôKLHðyiH .§Ä‚Ÿ7!QWúíãöO\úvuzªÙixêÆÿí:Êi¯¿ÍÆé®÷ï¹çp&ÿä‘×F NsY,æòéœõÆóœÐõ·×?óÔ’z/Âϳë-ð†²im¨Øë*w…\¹æ¹Vº~äJL7™ëm6¯)!%©^¯÷¦Ø³]÷edx³µWD;ßûËìSŠ‘p˜þ‚…Ru:½;c¢m®œÉkŠÁ Øq'¶MUÁª¿w˜;Ø|êpzŃò,Îð I·xäN|oÁŠ¡ƒ=_4!6]¿–Çåefæu:s33syܩܬÌ<½åû&ûWGºÕé´¦;Ä.„N¹³Ò®tÓ!ýh—†ªӆ‡”Næib–i>/ ›VòuÅWHޟ°Ïøaâ‡I~8èd £Á‡ÿºÄ Úí)ÜP²ÛÅ3¡dWYŽË•ãµg¤©¼õõT«5-՛Ñ/O櫽e¹^o^®·›ú—¥2²v{Fj™'ÙØßÃ÷è)/Ð/¡À“f`CæàJuç¦åL̙³4G—ã*¿ôÎ3r#‰¼’3¡‚¬:ᩂà˜;,ÖáÃ͝#!INî›ô½™³…¬(eàîÈ7Tùf§?ÅäæÝ‘OiPä·-…¾ÊØÓÀËgÑröû9c¨”7°|ÞBËà³28ސ˜a“òÉÅãŸZuýoVtu¾øû oIöt-•áòå2Ô~òÞæ-‡oyà°6wË%3Wºò¹®Èó] ’gŽôt‡.àDªë²»½³é®wA·v@*ghW–²]ŸÊ%I—[¯±þØzÂOÒ³½Šüy¯ûòò¼>ovVÆnñ 99J²96o–¿Ÿl1±èÂü¢¢~ù^rªMùŒúD\V[ªÙ˜ß/@þc•Ù£KÌdyÙÙYƴįEbf)ÙÜùi¾I¾fß&ßvßW¾Ÿ«¤óÎ3ò|¡ùä,ˆóɐªp¦3¾HÖX¬Žál>œ~Z¤eÛEÙז•_Áí‘c­–Ì +òûû,1ö=—nKµ[³•aXŽ x”C’-ނ¨%è¥8Ý|âÑGêÆßèJ7¦¦û*\öîå•J–äfºò~½U†ÚÜÃ÷N[™÷eÖïèªP¬±ZâÉpänXـîn*¡7Bù§³Ø”•™%5>g|Ùøžñ¸Q¿:õÖÔûRK}5ùHr‚ÃÀ‰’#:¾2”aÐé ^6ے2,if‹Õ¦w¥ôoçGB–Ü@~~b€™R<®dÛíºv~"d+)1$¹ <¯R¶9۝½,{o¶Úw¢­´øi§qÄš¿QÛÒ!‰nÙ­Ã{)@Í5¡ÔÌ,crrfR³Rò"å¿ñFZ> ÂÎqŠYl½lÁ8ùË弛_W‚]¹jù´W‡ÙLf§Éýå÷<³U–n•VG›+‰Õùîùs»M.KšÉ3aÝ*Q& ÿ)èX‰èîÕæR‘va(\d/tܪ=iÿ¹£]ì²ïtGèZûFû³ö_ُڻì†í", Í 3d8uΌ"Ñ_W”Qè¨ÔUfŒÕÍ˜®›n«Ï¨wÕ-䟦ŒEŽE®EE×é®ÎØl¿ßñ˜Ø¡{³×göT .gOE¹Ïl5»¹ÜÆ\^QcµZݞ +›ÇS!àd¦ÌczÀð܁ÌAò@~À(¨ T¡ššªÊÊ*Ÿ¯pÀ€Âª}E;Øé®y°ÊÜ©ÎbÖ§x<ö”=ÙÙnÏáÓôK!™uå¨oó=XhUí<6¤å”ÅÜ}Ž«ÖhÌ4'NîæDȪÂÃò,œpÜõ³ÃeF ׄãNpm–ex™ h‘Díņ̃ù¸,”±8“œæ<½ýmü¸†í"käÍ6Wy•µ=r¸ÍQ*ã§ÚlE2þ{›Õ'ãOÛR2þ]kVpTTf vRú0BÈgŠþæt6‡ÐÓlD7s.ú˜sMö*³·»—ê–†'z?gqšÒ*ÃÆµ"ŽÅÒ5šµ\-lpäÓP’5¹Ê’›l­B«OC㐰íŽQ£Å:ª¦:×ZÅ2¨–m©bÔ Ë2#… ÆæJ«bxŒ9îQiÊm®¬Qf›9£ªîbk,®ih3ÛFÁ/82!á "ðȀýç<Ý Í‚øØÏò˜Ïrâ༱·à,­`>KM|b;ßX`KËÌëú«TŠõ]»ºölPà—¹™ié|cדùé¨?‘çråÍç,Ι/U脬ÍçWº6&ÚMQŸœ‡w½õÏMöD8/c ªFú|_²%ªU)v´ê>øì›¡UåÅj'9­N¯ßäq á!–‰¦ãtú¿¼ÉIéãÓÇy›¸ÉruúÕÞÛÓo÷î²¼˜¾Ûûª÷oª×™”6zšO…¤Ž«ÔÑÓ@½?†ÒT±µÜj)O7šå1’k2•YL&³Å Æeys›s9w‹7N|–×ç„&; ÀÚÙJ.0`P¹×_žž$Ô¡§×oa½^°7‰)Ó¦N%Ç@;ÊÒ[º73½¼8_–.),,óæû¼Å>ozy¹Ûçµù|^ ´›ØFÖtârTX-L†\½5‰ŒÞ@V–-™ …cRB~ xPÀï/N¥ÜI¹bYî±Ü¯rµÜ̊Iz&½YïÖ/ÓÓ¥OлïV†^Ý"ŽÏZ7uy·ŸìÞ’ü©RΒcøm†~}Ì`#úOæzVÙ9Vù;«ãYsï։sÐä˜)_>˸ÿ ‚g í`¸¢ëZWn¦)Ã~R].x:_¤.'ò2Ͷ_ܬä3[]a¿­¦Œ$eÀ'Š–¨˜AO¿¿wÈ_{ÈWT 'Þ|ñeg§O’ êw4ô›ä{ÑÆ¢~S]¯fš‚‘ÎÀô‹ÂîÈÑâpI ¾åÉXÅõ´‘—ªU+²ð®ÂÌW +î¥òjžÆób—|Yƒ1eëµØ§Ä"'Ä̹IáCÈwböf…Í9Ž@7‰M Z=úI”ãlG$N%b +×b‡ò7åÏqm¦wèºÈ ¶"J‚×ÄQ†ôhµ…6‰) "GäÈ0Šñ‡× V¶VÏ÷¥¿ÿ‹â õ)†(>~¦Þ US;v)°¿‡9 ëNWP ~íAàËø2z²!i§\œJQJ­éÆÅÝÅ4 +tÞÓ_D¬½ UœžÍ1zÆi¥çµÝ´Œc?È»äéj~+$n-ƒVÊò8¢ò¤Û±ú´K¦,a€|ìa…"ßb?Ց¿Sqä0ýUiêÌø¾ÒÒPCêè=XÇ|ÈͬafÈ¡ jçÑ\pm=ï¡é¬£1|1­§6‘I©¦©4Žë°ö7±îéàa­â"¤î®R’¼°KÉñòþº +7ôùjÒZŒ£úÈiº’ŠW¡…+Š®b-VQ¢ÖÑ@ýI¼›é¶c½›@»ë W3ې¸šSúß”–ä1¬ÿ*ìs!`X[ Ù4é¿Ò¿éwô3z…ž¢·h;¸| j÷Ò?è¾ +íï‹tD:Ðî-ÐKâ; +â#/â=ƽE)Gìc<„ºÓS¢†7p#çókü–oªù~ø ?|“?æy>,Û7¼–§ò06p"Òýü5ãø]þ›¸-àìý{S^JqEý?Ê;x _„²m<—!{ýT“dJP-ÍX‡|6òR·H}îʨâ'a)¿¢€_¡ÕCÐV"ít´ü¾™ßÇÊç7Ñ>|ðwÇñôÿõoc#ËUÙ åFú5(ô$¿ÀÿTëTÆéØþøuþQ÷^ãe±½ž?ē%*HLˆÒ¦;îý¤Äè‹9“³{ÆqÚBz¨x'ô]Öh¹Š[¹U•wAªeþoX«|°µ—'iµÊ/‚ŽÞD?¥m°$@á·!4‡.=>†l˜ €³ÈMzðáMÀûàÆÍ¨•³l£müg>ŧ ß‹ù9þ†ÿÀb¨†ÞTSCÉø/üF| Txs}¿ám:ȗóJ¬ð ½€5!Ë?†Zè/ö¯Ñƒ°·ò,À¯/ðƒ|ô µ»© %塘TÈݞ¨§¿ÑGüOðK~VDÚSØM¬a oæükÞ;ø +$wû¡N¾”kµ5ôºêÿ0¿È?çýü<À¯ HA¤€=óg`4Z»ÏÏŠ=ώï°Jò̈Ÿ?{Ÿ=qžò;¢(× çøž>\Çÿ¶öÙ;zµÂŀ¹è/q$»?l«<ïFcÍ ò°/á±¼0VÁUJ‹¤$Æ¥±—ýÐø{µí¿háwâÀ­=4ôû°·æþ >Gcÿ[,5:Žz€òËcV3¦åçÄqkú_ânëð=qÜZü·¸›ž°*ð:ÿ¦Òˆ¯wóõû0 Z³¦1þ¯Ž«qv+'nõ8Uöñvèð +ț‘ÿ"l°4ûx¿Å+OÓ eþÂûzs!NuXòVE= 'ý6z>nçz"Æó×»EXEÖp'ý‹MÊy@ù*ðƒ¬·Éð>t@éEÛQ[ªP¶ØÿX–4ÓsÐÔ+1m3î#Ц?(ïn¬`J¥g„vÙѯMyvà;Ý Ë*ýå ´l$ZIOù§ +>†7r2w7•âNó-ÀÂ0b=èk"@~~šËeÝ~`Üç”3ÇmÀOid%ÚWÖ±émö¶=Q³û,TbÜĽû€¨O{+}¦VEj|ÑYöGږ&Üንv9Rò>w¡:á›è6ÀÀzm§©Ï§½_RzÈ{p«´€r1ê@‹ qÊÜE+ì…>Ax'àîYޕŸØÂJÛÁy'¬F®7³ ô4$l§úœhüÄÛÔvÑÿÀ³kV5ÆÌíN=Û¤°„K¹PJÄiÈðpkãN‘*Rqß +©[àµt­Šå„AœS/ȳ@µØ¬ 9NåÁ<x‡âö‡w yw«‚î8ˆÞï#sôÓ\j¬èŸMîUö?¿›«9=r4Õ÷AyšFcìý$úùIÎåý‚0ß XgF7È~ª÷1bô|»œŸ)P!ry°ƒ‡³N¼ *p ‰î|¼YRŸ#/ÇY-y½|x`Neɹ¨¬¬­wá&²_ÝÙo„Ô¼ R;Ñoý ²S„üèùýðˇ+ûi‘7.XÀþòÓ¢ˆ¯FæàF!gÊw%æÂ¿Ñlô³a§²÷ZŒ¹T +“0©wEw:-TšÛ* ¡›ÔÉå€ß/oäFèÑtè·¼Ám„ÝMÈSL[%ñD÷yçÃ}bq d 'åÁ k‘Ô>©8ùT9Ï~ÐAÎ/1®7Âã*VÄQŽ$0ÖJh†;’Z=vШôÕ¦è„uÁÏ~œ?vx]÷óùZ{©ûµðË_|=Yø%uüÂíhÿ ïÖFD¾æ—Ñ« yü[¾.f-â6,jÇvțþ9ø]žÈð›gnµg£ôP¤‘Ö'Ž=ßHtB*â‡Ðó]BOlS¶²´Ûõ|ÏÐãïz¿è‰fÈŒÄøYz,¥•Š¿§8 ý+Q¶ {Û z<‘¬H÷€³<€õ½ W?aâàÒý +½^ÅI¹½«È>Û"ÔÙÔ(²;(òEäbÀ €¬H¢\»Z#Ö"?Q®Æ®îå«þÛÿÛ^~ÈÜ=@j¼»[ £CAÈe±E «Ó½ئ¨+_WÊ÷¨‹ÖtSàM€ŒçdOx4°nE=Ö3(Š`¶@Vã|§Xû6‚NÊw¸ÏþŒZ`'€">ÈЁ´°çóqØÓ¸!ÈVN‘GJi÷~DùAÂ&zžztVJž^òov@Ú|œ¯¨ÿÝ xŒ.Ɗœ8…ä‰Õ^aÔmFn1êr`s~OGpû¶°ÖØ¡nç ቟f¦¯á)Yy<_ÀCÙÇÉô[¥åýFýEÉ@ØëA ¶¼6<‹ 6€±.€|ŸBÏê„gîÆ)7 vށ2Y2H–ôÊMð«nå»ùô…{á¯D&|ûø½6þŒ ìV.Nüø:¹ðNJ%€6mTÕÝJþ­ÕZiAáùž°(Ô Í=\­­²x;Zù”—%a3?*ì"7ˆzúwÁOÕ­â dá#¬óÿëÑó®ó+{ß¿¿×«{ê½âø}¼÷½üÏ:î‰÷¾mν_!”'úVœw öº]ð9 ~æqHßÅ4á àhZ÷[òR%‹-¥h?<¹<¨Ä؉êýc zo€t ç4܂ñ|€Oa’È«sáé(üÐwé}”Û ;6žÊ*éË鸭Ÿâå +*¸FJ ;¨ü‡HßðTž‹kq*ô²2) +)QèmÙXèY.=ö¡Űåiê,’ÄTÄiHI¾CÁõÆ.nÛå9Œ“›§G^¢—À_è.ö.uu%Ú/ƒoR¯|íÅêïŠöªS z»½Ž_åc\¢´?ˆ¹P3¯¾E竹 ¶ôj@3÷ÉլN•Uô)Òx™ D)ÿ°ð¹‚ Ùø5þ%·£×B~§önÌ|þÍ5”D) ÷œâŽs +ç7ðÎÞ|Ä'ùcœú"|NXá]þ»·ÜvË +ä¦»ÎÆpXéËîWï8~++ƒlÏ+ÇošçA£G‘ƒ'Ñø.ÅT*üˆÿÁøÝ7¹Þ:ð=sófHßlèûvº6£¥·÷<͇.%Ár¤"N'oá %£f±âŽ|{ô RTþ¾§VÄ;bŝà.X°%t +þÀû1›µçë'ˆàEêÍñ#òý§|ƒ üø/¾KÁþ#š/ãeô8} hM\”u> Þ1EÃ^DyŽ)羝;ç­\Ï;¯|nÄLòM£xÿ5?Â/óS 9y:ÂöN¾ÅÛøwü;ÁQà-ðt÷ó£ê+ßîžû^ó5œðW°šê½1m‡/¸ú³÷NN¤gQ!Õâ¾s7dö|èÓëðñ\ð–2Àƞ‚•ZpC†” eËéúBýN¤‰¶ðKÿ§<Š_€ /]¿—jp¯š¨ôu9¼óº‚spG¾3N£ßÂ#¾}ÿù=ý5£ÁÁ¿âÔ¿ÚfƒÅZ ·Ñœ€·`õòÍ ¼·ƒ ‘`¶©³cXäÍnKÞ¦ìu÷o1c6w™(Vºðnè»#‘˜Í¯Q^|j Ò°J+vþ2õ=?ì=ù¨³Q8¿µÃQÔ/ŒbÂ3gÐÉ7½EdՇ}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}¨‰R¯Õ~DçÓ&J Af*£‹‰´?ë§«ÿ+ýËeêò©ïAìýri7MìÓöµNjG4BEm©ùåÍ2N6©¸5ipUu™¶–Ÿêh6µ±òVeéFU¿]ÛCaà>à;@Y²%»Q²%»QR¥µkÏk¿lÍÏÃÔ;Û\ùå_Vgjm +í.m=y0ö¥±xv,ވ¸ñ¦X|‡¶¾5—V„<ӗ#@½mk=obù.•T‰­ñ’­m(É«viÛ°ªmXÕ6¬jVõ%Bƨ[Q¾å[Q¾U•o•ß‚¡<ýcCÅÛZÓì±$ªZƒv1•cˆúX<]»¸µÓ´e¦é¾™¦ú™¦ gšÆÌ4•Í4µóܐÃoúØoÚä7]ì7 õ›†øMƒý¦þ~Sµ…x:™èW*­ÂrzU˜ÃÓ[M”ô_B4€ wznÌ;ái×qkÞ͞v¢›¢¹K¢Q@þ2o gQ^I´¤ å{^ÔašÆOQ"ûC%‰o$ÎN %OXšX”X˜èKÌK´¬³!Րb0 †ƒÎ d°µGŽ…üò±%˜e” “¡N¥Íò?ÉW$ÁAã(œ®㧌æñá}óhü\wøïS|ílœ<#¬÷æ°u<Ÿ:Úæߞ¹(\éNštI} ó ȅÅííLSëÛ9"‹nɒß|¹‹˜Kn¹#+74È>õ-:¾ã޲¯®rVYGY†©ýŽ 1öøÎgÏ/p+É ß?~J}øÉœ†p¹LDrƃrò‹2w‰J1´®v—&£†ú]ÆfQYw‘,76×6œiGn”×î"ŒT;rËväîÕ.W “íúÉ(Ú.WµË=«]ËHO]m‹Ço3Rµyv›Eg·Y¤Ú,ŠµÑ¢m<=Ú$#jãIû҂ƹóšd|­¬ç«m¡kë¦Ö·\ZPÛ:.4®Î7§¶¡í¼9ÅOŸ5ݏãÓµÏùŽÁæÈÁŠå\ç=ýÕOËêóä\O˹ž–s:OÍ¥¤bi Ñ 53£q›H6B€³< £íæe£”4<βv눧dC8Å7:lʪÒêÒjY-“U©òËdcUΞ¬Ýüx¬ÊŒb‹o49ë.«Åϊ±ÄüY!Ÿ•—®¸TÅêgÅÊU@õ­&+hÅJªS”U΃}Ê2K‹,­¶¶bEÃÊ藟¬XEr¼•283|wjFæg}gʊޏ” ?EíXÅê›Uˆ Žü§~ Cr‘±QþÂa©¤ +endstream +endobj + +61 0 obj +<> +stream +xÚí¼ xUÖ|nUuõ’tÒÙW蝄¥Y-Cš„Mv ‚HU@ÐÁÅ("AqcpWÔ±"t>˜qqÁ™QGqßutÜHýï¹UšÅù¾ùŸÿyþ'ÝyëÜåÜýÜsϽu;$ˆÈC ¤R`Þ«í{ëj„>bÚ¶Ðøˆã™*HA­ˆî£‡ÄBzˆöÑÄHõ0í¡ú#eP9ÝBÑ ´žtš+i2¾„ß ²ŒêC·£_n§À;ÖÐ^J™ÆÇt ­Sÿ‚TëÈK]h8M¤e´IœiœO3é-m- ¤3é\Z.Œjãjã:ã.º›ö¨4ŽQeÓ<|_0>süÕxƒz!ōt½%®s?Ba”ÒÎ[é<Ú¦Î҄qŽñjG¿D4G/ˆýJ¹×Ӈ"S\¤Ž@.wãIpåÒ,Z@Ûh¯ F*yŽ™Æ8ãJG«‘ëMÔL»ñm¥ßÑk"Þñ…q—ñeQOö´Ð‹b¿ÚvìÒ¶Rô˜½Ô#fý=C/‰ ø½²Ìï(r„¿2^¦TêGÓPÛ{‘òñoe ¾—¨Ok•F% _®åÞ¦§èm‘-úˆ ¢Jé®,SnSÏ#Jì‡o-DoEîoŠØ­Ä+Õ;µ´ïõNm‡ŒH!ÝL·Òï…- ˆ•â2ñŠxW¡ÌVnVÞQoÐî×þ윃VŸMKi=@ÿÉb˜$Î ÄEb½¸VÜ$^/‰”áÊTe±ò¹º@]¡þN+ÃwжR[ë¸Âq•þQ[uۓmjû·Qd\A“ —¢ö7ÒmhÙ:HÃ÷-zG8DœHÀ7 òÄ4ñk|׈MâqŸ¸_´ ”—Ä;âcñ¥øJ|¯¾º’£ä)]ð *ç)¿TnPnQâû’òå[5Cí¢†Ôj‰Z£.C­Ö«›ñ}D}[ËÖjú¹È±Å±ÝqŸãÇ_èñÎË\äzþ‡;õ8öfµmhÛÒÖÜÖb¼MiÃlô‚ŸJPû9ø.ÂxoÄ=Lñè»lÑC g¢gf‹Eb…Xž¼\lw˺ÿV<Ž^zU|Ž:{•\YçÞÊ¥L™€ïÙJ½²BÙ¬\§´(¯(ß©N5NMTÓÔêHu–Z¯®R/T·¨õyõïê;ê×êøšGók]´B-¤Ôfkçk·ij:f:žs¼¯{ô¥úz«þOçÎaΉÎIÎYÎkœ»/»j!OÐ#ô(E}ÄaõRµB}„®Vе,åEåEÈólªSÇ)Tå>±A¹X´(ùŽÕúPe¨O_h…è맕íÊ×ÊPuœ+¦Ð"¥Ÿ™›žªí)ў #ÚãhۋÈyµ/Ö(ŸëñÔ,HŒ2ŸRûj!õ9zM}K8µÛéuÍ#2Äå^u"¤àwÚ0G5å©·ÐoÕâbzD©€ +ýÞµr<^ì„^˜*ŠÄ7ªAª2R4P}—ÖÒbå¯tóxýFÔiçÐÕT,.¢é̊îŽsõzšxVY¨5*)¢…í~´n°Èª#•.³ÔmúçÊßè|:¨yèMõAÔþ ò[uœö…c²X€p1]A+ŒKéBGµögq©¢Š +´ÃÐn©EZè%Ð*3¡Óvcvï…®ŽCH&$çLÈÅ4hˆmøn…žÐ A 1ǧC‹½H-úT¥•Îq$h"í¹¶É4ø‡n2Ρsë¨ôÁzã"äx½O×Ð}b]Û¯i9uÆÌySœé¨T:*^J£ò7eвåÄñEoˆLúßßÂ3Ìñ5j¯Ò*56‡ ÝÝ ao¢¹4†ÞC+?C £ÔýTÜ6^i2*Õåhï[4ɸ×ð -0–Ðzœîv:hŽ3„1Žˆ?£½¿¦ze²±J­o[ˆ~¸½Foýs¥¶B[«}K1ç·@ßìÀ¼Ù‰™ÃsŸÂg­[µò¼˗»tÉâE œ3¿~î¬êéUÓ¦N?<\:ì%C‡ 4p@ÿâ¢~}ûôîÕ3Ô£{·®…ùÁ.yçN¹9ÙY™éi©)ÉI¾Äo|œÇírêMUõ¬VÖ"…µ­08jT/öç `NT@m$€ Êy"ZÉ8‘3 Îù1œa“3ÜÎ)|*éÕ3P D^(ZŌIÕpo*Ö"G¤{œto–n/ÜyyH¨È\PˆˆÚ@E¤ò‚µåÈ®)Î3"8¢ÞÓ«'5yâàŒƒ+’\Þ$2† éP2*†4)äò¢R‘ì`yE$+XÎ5ˆ¨sê"'UW”çäåÕôê#æçF(XI I!‹‰è#"NYL`!·†® +4õÜ߸±ÕGskCñuÁº93«#êœ.#)„rË#¿z/ó¸™'¨^›£6Vd. °·±q} ²cRutl?kjÒ*•µ•(z#:qì”JSÖÕTGÄ:à–p«ÌöÕ+8¤vQ â–4.ªÅÐd7Fhò…yÍÙÙá=ÆaÊ®4N­æEJs‚5sÊs›R©qò…»²Â¬czõlò%™۔h9â½ÑŽúö8é’ìì;¹½g×(8 Ì  &ÕA´i?êQã¼A`çF U¤#²0âQÛèÂáœ>â(ð_$ xä'†Ì±BôßWÄN–“vQC¼íŽ„B‘=XDœ#0¦¨ã0éЫç­J0¸ÜA÷ÑDô휚!}Ðýyy<ÀWµ†i.<‘†IÕ¦?@ssš)Ü'TQj9f¿“6cì˜öäµAHr ±É›q¶ÿ%úÒS* ‰ˆôŸˆ®7ãÇN Ž4£:PÑXkõíØ©'øÌøAíq–+’2¢ZÍQ,—’£ÊXåÌvföTÇG´üéR¨ëZ.H¥ Êˆ¯v”ù¬ñäåýÌD­ÆœJ’ãɬjF†„Nô=ÁBõâUTËëØ©3='ÄAÔÌG[OS«ó#"4 3³­ÆþAŒšœH]6‚ få=1Çr×àÃÒÙ«g%]cce0PÙXÛ8§Õh˜ ø‚{”?(h\^Qk N«±÷ªœHåÆôÕ1¤WÏ Ç46Ö5‘Z€bÂ9MB:ޏª&2!TŒÌ ó‚ÕõhKÓŠÏ›Z;.…Êš‚b䦰Ø0eFõv%¦V7+BQ[VӔ¸ê=,2TáPdO€=4V kš—äÏÙ&j±š þy­‚d˜Ë4¯U1Ã|fA…² 0 Ëy­š¶¹5„¹Ì°“»›ÅíBŒcöV’‘æ§ ž©ÕaÏÀððÐð0¥TApP3Bö‚w¨ ]ÃD©ÈiBž“ep«hhÎÙ#sšlq6€“ÃÚÃPsf‹Êå™ Ÿv¼ÓfTïFÈ_>ÁQÆÖ´¨Dô’Љå|z¨:^i;ȑžA9ž¨è'Œˆ`dvpu·.R¼0ÁHÚLM42·¦±1€o½2¯ªÚ|r”虋œj" smޜ\ÈÄqo<’J¹Ú•Ë:¤½´_Û¥‡ÒØÑh™wÊÒPûˆ8‹ŸòOV¿é +šåc•6 mœÙ8ò˜éÄ[õ€7!·F怚l•5rqš›`>Ï¥+9¨Éà˜&e|HR!iã˜`E8Xt`°òu5ÌäIÂÿ£L"Љ™y£o¨í–Ïœ¾‘sNô.h÷V2`£ô6ÕÚ"§l^dQNdIM¨e·¹s{Oð!2ñHF-–‘‘†ysPE¬7£ç0ê¹fòBÝȖӼ9Hƽl•97tB–Ð * +qs" µ5Zè1 ˆ8@óa>ç°Þ˜h¶g"”?ȜÆ)HK‘¦&Äù¡9âœIäIËUã2‡{ŒoÈK^¦*ʄ;žâE¸¥*³XÐcƗGq쥂¤Äá¾(ÖDã¨É*Ý_8¿*±Àç{)Iø’ÂIµI Iš?§Ló‡½^eZR²Ï‡g«q4œ”˜—ž€g¦Œk5¾kœ>-)ÁçÓÙÿYK|¼t|ÓâõÂñ˜]»ÝUI«’]^o+×,9>>Þt$Äŵrdr¾Óg…9}’+<´jŸó ó-§áÔüÎRç§êìÌõrfÆÇãٙkàŒçҝñ\–3›‹vfuî?134Þw4$?³V„BãŽÀq,tü3kE‰Ã|ÇB%ï…BTz¤´„‘48)yp¿¾4K¬˜E+ršÔ´VµOسD ä‰Ë·eIœ3‘(³´4TZœ<¸4Ô·_MÞ=Ø¥°p@ÿä3Š‹Ò3’Š“DjzqÑú»èê ú'/9tþ¢—×Öné³ëXàÁó/¸û¾_¯¾ýŠÛ6~çv¡6N®$|W©$?à÷O¿öü“Xi¬ñ‘ÖY†j'%]J\†ŸrӔiê,Ç,÷´¸zu±c™»>ΕÖj¼gv5áÉìê”ËÏ®És|—úu¶Ö/yHV¿ÜáÉ㲇çNJž™59wNòÒì9¹«õÕi_+_gú(]$z32&¦×¦/OWÓs7ûvøŸOËÉõ8i¯²“„±¿…Å@Àn +Ë¡ö !nLÉÕâ2 a_´KU†-UÒmIÞ®ªŒ°·Õx£…GÌË2Âõƒã),^ÎÔݵGÿˆWx³ýðí*(ìÏôÑÎÁþ}ýŸþ˜ñƒ™ÕîªôbŸËŸ)ÏKbsÁŽ¥ ##;¡`s’HÒ¤]•)mªVãá8iW¥²üÀÿQ8ƒe(I‘ÖU¼´®tV^ˆû.ÆÆj³veڋb¦­[3͹öVelÎ9²¤œö’rdIðNâ’r4.)ÇÃ%!´-ÇyçÄs™ðÿ ËÌAQ»I)څm´xjU°@¼Db3í ÅO¥4ª‰³ë$5´OjhŸÔÎñ\,¥[:ú[G §J%-g%p-(+¿ U¬Þ•dz%4þh(z¾˜+³/*öÑ(E~l|E}ù+Î#Vߘ[ã|G|G’2óK6yB|jJaj|RŽHö¦å‚]j¯à?:ñŸ'ÛC¡ÇÁPì»–¡[Õ;ÃÙ¼D—‹vQô|i©©æ‹Óΐæ!?Ғ‚Iý ¥²—.8àZ{Ñ=‹.ø́Ûvî +ζü†–êº3/¢Þ8~öÜê½ï>ÖU¹uÉì!7Þuì7JóêÕ·]{ìo¼”Ã^ìŠyí¥,aðÌޝ–É]—Q’R’È2UÏ®,‘ìôdŏÔG¹ªô×9úB—«¿oHòô™¾±ÉcÓ+2g:fº'ûf%ÏJŸœ¹Ô±Ô]ç[š¼4½.ó—"Í­;¼g©SS=gÅ/Qëõž%ñžŒ\͙™O­FR£ö©ö´ +ûªRósäþ"Gî5œÐ’æþÂ)wNŸúE‹´ÎØ!M3v°xH‡4Ù¤Iš_п¯SÓç Àì{ eZ梳ß[}æ‰ccî[dlë0Á”æððª„|ŠO`{"YÊi¼”Ó\)§ÒJ´ÄQNGJ—FÑ,ç +Å[ù’/K¸ÜÜP¿l6(¥žŸÕ.>Ò®XšõuhÖñÀeU–ÖçMȈ™Õa÷Ç÷\Ç\·&fՐ½¦¸$s/§eH#Q³ŒDVþ¾1JK•öaJÔN¤ü®+Ÿz]¤ÿúÓ«Þj;²§yýͻ֭oVRD׫/h{ûØ Ÿ^&: ïóÏ=ÿ§§ž;€Õ"ßøRéḠ6à¹rµœ ã¢Ü®(·3Ê­G¹=0肅ýÝ<ùp4daý÷z„Jé>w(Ñ£§cw›èëB]„÷ø^ÑÖ5ÉSÅt©J.ˆ†ÓUᮨu.w687;5ÂÀïpFœû/9u'ï1X½:YgK©‚QØÂÈijMË!w¬Py@Y¤ÂqR¸tK²Ì©ãÜ«,¢LqFÓüèµÊçè{¾#%¬~J|ï-‘»ÈcØC&N*.ö=Ë˶­)šTXùE-¼‘$8Âî%Âãõ&%xܰ aê—èN §ÎVSÜiÞlß1$îRquœ«Oòt­ÆYWð±Õ³5îQ¥5þqž÷½¦rÿÉûºï}Orr“.ëOÉI‰™^ Î*,]‰:)^òx:ÛvI!Ó^Ê Ï×uÕér»…®»š +JôaڊÄD¯½ïV¼qj¼Ï£'*‰ßÓô´[ñ;•È­*Þ§±-ˆW±6ª7ÖqØõ^o|vŽ\“àuƒ_•Î9¬m<ðÑw÷+Yáíß&Ý +â{"Ê«t¶¶’Ò€ÑÎNôKGU‹õ4CÙI1ÔNÖ¤óÀ»þá {9-ø§o%@m…æSØÞ=œy,ç|$]I3\~Zæ¨2Ž¡¼-Žgh>pÜwhïÒ}ú`Z +ÿ]H·O#Ȩ;ªn±XƒÑj15€.r€IÊ ´T;“úë&Çû¤2 yÜOo¿Ðêh<üõœâh¡mìÆI¬4Ži·Ðõ( Bܯô-hGú»ð5õQþA½ôºòUŽü/nCžIy¨£©(¿7h±ö¾”¡+€(ës»Ÿ¸oà¿ã:eýÀ3é§#1. À®ÊïÃ}Îã.ªÚƒ÷=ðÌd ŽgЇ¨›”Y³ ·Éñ4çÌíV^\Nžþ -¶Çyò|a™E]šì¼yN±ÌØTÊ÷b)÷Ÿq;Y¦Ú)æžö)ä:È9Ù²)Ï;ԙçÃp$}Ö²ÌrýlÊý²&ûs¢%Qmí+ç¨J´d}­Mí¾h§ è.äY«Ï…NÙA£´U4J½–æj_P¹Úz;ú" íoDù”&»öS1Ærü7ÅЭ ç!±È±í|ýyˆnEŸ®Ð)]´CÂáxÀøØAâYÇÊé>‰ÆBì7ã˜2¢ãþÓðÿ ”W@g>`|â8dhÏu<'œŸŠ¾@À¦o€®ØêZ,ZØ8êDGeZ˜†8Â4PۏñIƒžÇ\@ø4ÇÛ´O݄±>düM4Pƒ‚<œi4Gٝ†²”Wh-ƒó]%G'È\¬,ÙԖ×XÊ:ߒ)?¨Žù÷¢…÷,| |9 ™Ìⵁõ³\ £+,y]Ô.ŸÏÒÝ WÙò#§‹bä3>V.c©\[ ßíyв®´ÛÏú‘uëHÖs¬glþX•¾QÙ 9f=üͰæu cPÇw¬¹=Œñžnz¥q¯Þbܧ&÷éEpÿp÷¢/V·¯©ÕF›µžv·×R3œâìuÔQLK-}v—Ô7_Ò r­’õsëÓ%Žï1îЁ²¾;¬9ˆþD½kµèóm´íÈR×c>"˜É}"ǂ(“×^ÕÑϼm¢µêë°8m1%Éõ¢”¦£îÏÊ0¬©L9Ì1îÐ?¥"mtí~ªã±âvp}xì]ç“ו=qˆúi÷ƒ'<àÛ!û L÷J¹à´‹‰¸/œóÈ ™Îïv™&LÉVÜ%ûB¦‡-Â2Ì}<õ4š,í‰Oi»cMǺÝÙ@·cãJ˜÷!»‘n ×é²åz}#…ùµºitIùŸa|¯>€ö¬†^ÔôÑ”éh@.–m/×L»žçº“ +YFô¡‡Ùž¸‘µUè‹iÂ69 'QîU»ó7„¹{%Òû-½M(ûJ„sÚR¶eØFàùâ SŠÞ í’u`;å«ÓíêÚ9îºý°ŽzÑÏúZ÷Ea`/ª>ºt RLF qpóºG»”jUT¤öÃÜM¢^ڟ0W¿¥›ÕDš­ ›µVÚÈ~-…º©´¿¶%‡¤‰®üþ­4C+Aú t®6›VªM½—É£ÍÇX#ãjÈI>Ò‰|-ˆwi†Z…¹uÜßbŸ,£ÅÍÐFQ/™. +²®6bꬌE«Æ`LQ_vŸP_Ôµ½žvOQ?ÙNÎé˜G»™JÐOo&m›¤l¢€Êk4BGŠûŒ½èäÊŒŠökÄE@om= +\ +wOÐÿ6ý°ÝÐëÀ:ä½tï JÁa·[çì¸hp9§ +†#ÇØ{‚ÿ¬5€8jìeÄò£ŸÏ@ygh¿0ö2 ‹cú%”ê¼€RÕ®ïŒt1~GæÓ#”¯’ñïÓÕ駀O¿¨~ G·ÑÐôŸ7¢h€©µ6Ðÿ¥~ÿ`|“€¾²?£4S†(E¼b¼ +Z%^¡$õ|È oøSìþ´Ç á×Ëð˜ñSʌ6îóØðX츞ίì¢ÙѰå ]®£a ­ü@¬ßõ, cèO!ýÚ½§Á ê¡nã:A»žì×'PW†’ºfsÌ9 Ý:`^™ÞK#9‹Öƒ.ù¡pôÐTÐW†Õ»­;à…; aç€ÞJôýWpŸ‡ðC& EË¡–]™…°ÝVZ—•ß3ý÷$úî(ð°™þûÀ"¸ÿ `=ÿþï ¿Ý +þOîrÐ?˜ñÇfÃð8üŸÂ¿¨†{3hhO HFú- ¶GNڇþ×é©÷?—Âf™‡zúùÌ ô¢Ø=ÄϦöxž†Æî5ìñ?:3ˆ¡f?`Ïôì¾HôÞç§ö86Åx¶EC›fƒMÏv4Û²l?KûÑ¢rÿ&íX”K”jS¶Ù~eۙíWÐÛ噁CÖgïóe½¬u#Z·Š£tàr,º<ß*]¡{!ß_aot~Èßp·bíJÄZ·z÷+Ðàïú•½¦Ùºõ${š5í¿íÿO×ÈÿŚZdav ~,ÜÆ £±kñŠÓ­ÝÿëµüGÖèèuúÿê·×yîaTÄp†½ŒX»ô$;à4þÓÙ¹ÿ©?Öîøý1v‰íÅIñ±²gÛ3ٔݎ˜y÷Ÿ‚÷Ú#Çm»±ó¸}¾Y~ôQE4 ºYkèÀ¿ 3:X£Œëà_ãúŠ\Qü¬‹F)PÇq gˆM|¾mƒÿ2ø}Ú ’·ÚBÝéä9VnÙ>—ö!úLêÁÍ\ê ’&`©=Ö¼‡Dه¬º¼ÏÕf_i/16àiéZ<"ü‰ÐÅ©zôv˜îåóxP¨ú}Òñ3>ã˜þ+É3Fž-¯¢QÐóçj‡øìËxRžéµQ¢3^¾GY‹5ÔoŸÓÁŸÆgCΟ—­Öù\­þ%ÖÁéXݼv Ü*ùNh±Æç¸_Ò j•[gÈ©öY2ŸOñz¥÷&Ÿ<Lj>G~¶ñL*J­÷TÓøüE}_¾«YÏçîêxzÜz¿ñì¤ÛÜÏÐm®:ªt]"ß7mQo¡µ»Åy5Ý¢‡äû•iöºÊkâ)Îþø,3»ýLÓjs¬M ë7“Îäó˜èrít®J¬¥_Ês(óó4¶ ÖøF Î|_a|}êóNãyëÜsµÆ_оæÇžÓϤIêìûì3Ù{@_¡³µ+«cëb—…~9öc¶m›À=]žõ™ï{ø *%ê=\¥ìçåxæ1sx1‡yü=Öû¹2m5øÊÒ>̳ÇõÖ{»,`ºò7ð߆9z.æ +dP»^¾Ã»Üx{dº%æ{3} +PŠzÍGºüîÈ­;ã=m5JÈs5ã%ÕØzžòœ|ǘh½ ÌÒ6ÒTy¦yü`¦ÖMž[wÓ¦àBøóeÛ-*û*Œt‰4Z¶‘Ïæz!Î¥µÎH-^ç£Té C^ã¨Ò±‹òÕe°_öC×åbìÆ`\i­úuÖÑ<5‰ê¢ÒxQ| + +K¡|‚p¾u-üüî÷U:Û~¯fžOÓ÷`+Ö»\F=CÙ)ò¬÷„5–»“éFØ`Ú-a籓î‰øŒw€ï•PvÕ)­(cê‚rTæ_ f®…nV9#µé˜c'bD,–iŸX œiA,¬ðìX œiY,^vŠzüߏÕãÇ cðÂÿB=~,ß`,ü‰úÂÇþõø±~ΏÂó¢ãcðñ±õ€~Â>¶íiìMý«µÞ z&(¤¯íI>Çæ[þ¿Z|¿°ÿ5n°W6Ê,@ç¼^úûjcÒq´= škž‘Ûå×=€*³,NÛö˜Y¶„UfÛ.3ý±‡@ÿãO>0˓e³îÝ ¶YíÛ`•1ëÞvýqþ¶\³2]ä8 ˜Œô~Ð)ÇÑöˆ ã Ðß|.úŒU/vw¶úƒÛü(çu\/ÐwÚ6èŒZ"¬Õ©Î&Õ~MgJ{ð„µj¹Ô‡ïÒ}RßÐ}%T¤{a‡ÜJel7°wÔKþ«uX›ö li/&‡öe9Þ§ÙÚ¹T®î†]<úeÈ÷2țõ6Ûê•4ï*å;!~w²šÖ{Z¤ýâOªö!ê{íÞmƒ£šÒëÎÞðoƺ~;­vüš~åZJûô/ø)ÍÇzå×gÓ`Çe4ÊÞÛêKÉ툇]`Q×Všçì‰ðÐ> \÷zØu/ÑDôÙ@»ìöw÷NJEø=æùŠ”?à‡p¦¬3ê ;LÃÞ:Õ¾7à˜…>©“õ/ß9ÝOöèäøk÷hêætÃöêCܙ´CÿíÐa§†ä{ùùVß÷å÷OÎs¨Ÿc=Ú{wý=ôóTòؔßÇÙç°Ýn×H{1Y¾×²ÎÚ©¿ok |W"Ö®±í¨v›Â:#h?s°ÛÊëg{û-eo˜g +ûaŸ¦QˆßãÉ3‘XjÕI¾ÇÛY²ìYç>ãTAï¡ùú4Å1ý’BSœOP²s$e²}ætJ»n)¯ÑŽoa‹N¡BŒÍ{ +c‘ù^̨±æ8Ÿ¹½ +ÌÄd<Û +ã³ +Œ¹‡ðiVZÄç›û ÉÃïÏ-÷ u&§=öw‹ÿ¡¨³š·LÈ}H ÚNµîR]q=þîžå§ò´ôgž¡ñæ;U§xÇK¯]`ûa罅9zÒݶ£c©õ¾I¥mÈôn‹Þɲƶ^,½¿òc÷Y~Ž5ç™MO¼÷bÓ³-ZØ~/ç44úžÌqj–?áçžÝYgnÙ6=ÅýóLî8ÕOÚ?ES9&¤Zv,Ûïcä{~¾›óh¿ÃudàDT1ø>Á© c%a8—œËÎÿQè× àòÇÂøu¾Ô„q³…O-ÜÁPöҀvm,ŒIœú~]¹~+Ê\½L8Ÿ5!íÿŸú€œXI]ɒê¼þ$`e0œŸ[¸Ê†a0ì~·ûÑî´í´{A{íò­|ÿ¯ãø—ÿV»ªîѰîèٔïî駬7ÆGâ_&ä]š”bAG¿><°p=s%›ï*©õ§zy_±=ÍIr° {S†å·îßè:,;g¦9øî ª9Uÿ8ëMùsv5ûIÞÛ1m¯÷ѯuÇv¾¥ûòÝév랬Ÿu Ö]žç}µßÓüm>cйŸ6îÀ:é’cU*Ïw:~ð…ñGÇ%°”u¹…g-ì0m?ãaë¤.ïï¤û£½mgó˜ë¤q·eo³{ž‰¶Íðãõ²u¯ú Úñ=eÉû¥a¹¿ž¨-Ğ~!e©Ÿ"ö¿oRçÐp^3Ô3`[ñ›ÕÖ}Y>{xԄý2Q½/j~óý¾WÈ;9UªkAûÀŽnìS‰6ƒ’0¦^`ÆúEkÿÀû¦ƒz˸ þ'Ý °ß“[Ôñ-tü‚z9ŽÁ>xrp˜J_Ó͎Rê¦OÄ:ö }s‰ï˻ćŒí³oz5¥¹Ÿ¢‘Câû6Uà‹¡½Óäzdþ_Bì¶è3OyڜkÒÎu–ÓZÌãJ`”uï{¾ù~ 6(æžfÞSí¦ÝM“bî¡ÚÐ[χ)Ð íg¯LùN˖e ²ù ògÞ×¢.ÙÆ^e"u¶ÒžeîK >¯¾à3Ë[¢Þ?maüý~+ö=ԏ½/:Ý݌ÓÝÕ8Éÿ¾S‰½»qº»§õǼs9Ýû2È*ÛȕXWöé;Cð? +\ ýzC#Ð磦½v¥‡¹½ +{Ðєo‰ò9igè¯ÎÚFy¦…™¥@7•™góÆÖïäy*ŸÍ±]ªfÊßAd[¿kèfý.a”ý»‰ösÚþ4u-ëT¹fðÝnìÓ oêX·(ÏR±òƒ©ƒÄ! b]$Ï%ËPÇ2I¥[éaé”2r+ÅhËõ&ÔDãY©“L¥òke}†õ×ÔWÔlS)/›:Hy<6ŽŸð»ÞOË=5ïÍî—kÓw¦ž”ºÏ!ᖿG1÷O‰<ùw0§³—,Ûòú˜MOgZi°ÒœÌo½»ÁZ’"×äg¨;ßímßwË»ÑÈýÊ(ij rÜηÏÛå8aŒÌwû"v_Àïsxlí=½ynÖörmB®Ó܏Â.ó`Ý=S–'ß÷¬4ŽZõäýIäôªö½Ÿ½—³÷DCµÛè.õØB}ùN’\ïÚßÞŐwHž¥»å]fP„½¾Qæº!א§€—€?Ÿ¯˜çTÇþÆ¿â~ißmçûm{o ¿ž&·ëLÊÒ÷šöŠÚ@çñ¹8ƒWÀ¿²±“ïÕÈ»PC­{„¼¯/·(t.–z~¥|¿1SM†}0rRI¿€¿Ü¿Ð.†­ÞU¾§ªÒ.”¿‰™¦f¡Žÿ¾ªHþ¾j øúÈû½S´_Ó4ÇS´Èñšçø†îq¡{@oQênþ~B;*yŸ»b½âÁ~m%ÇúÛg×EÖü'çí +¬iWÒVí Ä}º paëÿ´U|B[Օ'ð¨Ë{Ó[µ€öCüR‹¾Ž°¥Ð>ðý®ÕK¯ÎYF.m @~{*虙ÈcÒô“å|ˆ5ñ Ú,ëp*p–Yu² >1Ž¢NWƒî^³ë Yhp=bóŽÆ‡V}bÊcp_DƒûEû’z£ü-ÀQ§aÀzLjû+\×v|ub½eÚྌ÷­«ŸOî÷hÈv/9>í@ð˜È±°d@ý-Êf7·›y¾0ëÈ2 ed)öøC&ϔõþ@Öw«V@‹dÝPŽ£ºc¾`žÉíyšòtµLÇ|ˆ“cÈuã~~˜ºË:<#ek —ËñܟúQJÔwƒç5”‘žy”esÞW˜õ“iB‡!/}*âýX«ÞG#݌“õ·ÚÕ^w®;òtxͺÖ܊9z¦Þ yuÿE°+YF¦OS¥þ°«d5H[¡ºDÿ^ È°Âø·a£@±ågÚEÎ㟠žï?ßH[NÖ1(Š Óҍ£ý¬?€ñÊBл¥Ûyº|XG±~:°ŽÝkë¯Ø2X—1`$´ëµhì éQý/ûžïBkGèV†›æ­s¼Kë”BèõBä[H=ÎÀ< /t²Ð݊+´ü. [üªLˆç5À؛ðgIÙöƌ2°1n> këÙ6`,ìÄ'Ä+ÆlЏA×ýØ]—óÇÞ¥‰½sºzd“ÆÞkj1;È8¬m1>ÒÞ3>r΄Mø9@Shpœüãm=å!"Z ¬d[ϟ{ïÿ綛ï J›âsÏÅûxùÎàAËþXI3°/åýþø;9KÉzeêãé6ÇÿÐzçýäÖ_k¿Ãr…k#y)”éNÀ:û¢õŽû|ǝ°¿–ɳÒù[b¶¿»Ð>µds/ôÊ*ØR5XWn&Üò~ð Ø0›ù7¢ŸÕ”³íÄïèÙnµ~ÛÌ¿a^¨—Ðθ*ã®1Fr\<AÎÊOس¾BЏOÞå¯4Ã([™„}Ø}Ô=*l”E»[Ô?Wғî[ß*½i“¼wyö +ûä>žm‘DØÐ) ­«ño÷ùOAk |†ú>Æ0Ê}ÚýbÌýÓÞÁ?͝ûÓΑG1÷öü=ì¥JÈz¥t?ûᏠ ß«è.WAwkƒéng=Ý ™¾2{3th‰ãj”é¶Ñͺi£k;8nÂÜâ¼¶Òz}:ø>B|'«,èKÇHØ:Ká^L µò̤‰Ž‹iŽ=Ÿž…úö£ÝòÊYƅb›q»â'¿xÍhÑr©L¿Ÿ.ƒ]¹^»vôý Ksh”ú9(ÂÓ­8¸±'\¯?ÿtø—šñ°W*¥{5] ÿeâƝÚRãIõ쏯YFfkke.ï2ý«ÜT†¾\/ýçßhËў¡-rîïQ¶R—BKŽ—¨Òuˆ.“xɤqiH·’r]ÇC7.vˆ³ézûL$öîàIgeHw= ±× ¾S ÿwÂ3Æ~m±q§çN"×ÕÐ'Ó {ցZû9áúØ8•”¢ë&´±ØS~I¥úYüŸÎ:>?ýQ°ïՎ¢·‡ëÀ‰ˆ¿ÃDÂ7'Ã×|"R +;Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@þ+D¾MÊ#TBÈI +ù(LW92ÿ üªõ_/ºšÿ·4öÓ4uÝð8µ'•.ԉüjHíÌüjf½“¿Uí¶«0ÓÿÒãjw: (j÷æP'ÿµ«Ú©y¨?ܪw%§%ï¥P•>òÀsð0°Ðh¶Úá>U#¡*ª :òûUÑìM*îQ åsJ&¿ò™rČQŽìJH*Ú>|Œò= ìTå|ßVÞ¦K”ÃÜçx–ہ}ÀAàs@Wãû¾o*oR¢òwꔳíÀ>àsÀ©üOŸò‹š|²»P”7ðô)¯£Y¯ã™¨¼×kÊk¨Ú_š.Ú#¡>–Ã_`92r,GrzQ«òçæo»C¢ +1Ґ¨ÇÔ.4ŒŠÕ.Íý ~™Í% ý­Ê»»!ÿŽá}•—)(¨ÉË(ùe +Z`9 Ãõ +\¯P°ØDHž>  ž^¡¾@˜¸”—šQL«r°¹°Ì?<]yQy†2Ðã/(”ôyåiIŸSž’ôYÐΠ”§›;ûixâ i|ü_m@û Þ¡ü~W~²ßž¤ìCßùñ씀ÙÀ5€®ìSº4×ù“‘ÉctÀEàl¦%½‡îpQx‘?\8àGá_À…ÇöÀöB%\¸å&xùQxõupñ£ðòpñ£ðW—ÂŏÂ%ÀŏºEpñ£pÆl¸øQ8a*\x´*·=šßÕ?pÂbž¨ü½ôKôÒ/ÑK¿$Mù%é[ëvssè±máP÷þ†½¢áqÑ0Y4Ü!êEÃÑp©h( g‹†hÈ ECX4<&¡+D¸åïàp¦h8  +EC¡h( ù¢! †[•¼æÑŒTH²k8O:Ð_ ƒöITòУyù<è„}x é ƒ)ÐÅdÎêÌ´Ë®¥¦¿÷¢e˜>O á†'è-@Ã=1z™< ñ,fûÏÐÁÝ¿F>ñ씳K€Ï]Vçs@¡eV–ãJ÷±*>Д'ðí‚ož’îäËõ…|£ÔkrEbg1¡³ÑYHééPÙÉI®¤VáÝýoï7ÿö’{¸[¹Z¹†U·²Ù¢×4 Õ-¶6>æž&~C5HžL…¢t­”þ”ëbڟr•@‹šs«,±¹°§¯HàT»ýßæ¾çÿ8·Uó£ÜÇü¯Z5Ñì?„vû_νÒÿlŸVB/l {’uOî ÿC$륈ØÖì_Ãd·ÿâܑþŹ2¢ÞŒ8{%|áDÿäÂþQȯ·› +$_Û-ù4Á|M+ó+ʛòó%OF€VJž•hžà)(<é t@òHo`žÈ0ɒ› –ι’EdS®dÉْ¥ê8K‹åÊv–+eIª8ΓkòxÛ<ÞÃà ýÜO}Y($v ­™7³¢>XQ¬¨j#W]° 3Ò07hšWÁˆZX;wÞ¦sê#5Áúòȼ`y ièÌSDÏäè¡Áò&šY1µºif¸¾¼yhxhEpNyÍ®‘û<¡¬+ÛËê?ñ™MäÌúsY#ž"z Gä²rY¹¬‘ᑲ,’¢>±ºÉEe5#fšt—çØÖæäՔ¥û–“2<4/sMÎ^˜.÷Q\¨&,‹xŽê5¼×pŽÂÔâ¨'ZQ™k†æåì÷YQ>'Ë(´êü•çSfÅÂróo%>Zu>w¸ù ­ü±â*"á9å+Wô˜26R:iFu“Ó‰ÐZnRdˆWÑjì7{#pªj;#‡•p˜Ûm1ž<þç[tςå±]"ÜY¬¢•5j¤óØ© +4ÂÔhëÌÕ{aXñZ±² \)Bb¥‡UíPˆL?q›m¬:ßrY}±Ê¢fJ$YiwIû‡;+ÔÞc«d¶²;C3«‡'¨g¨}h8lç¾ ½@{©}ÂɅ~Uèw»úã<å~§^î·s­ Ñÿ1˜Ú +endstream +endobj + +62 0 obj +<> +stream +xÚí½ x”Õõ|î»Í’Ìd²¯0& ˁ„-É’°É!A°Ê* hµu‰"‚—j¥¸TpWÔ: Ú?´î+¶ÛZZw-•Z¤u!ï÷;÷}'ûÿ÷{¾çùžÌä÷ž»œ»Ÿ{î¹÷½$ˆÈM¤R`áëì{÷:„«Õ˜•äE¿ü”{›ž¡¿ˆQ,&‹Z¥·²Z¹C=œ(q ¾‹hú{rG„Än%Q9 Þ­=¤}ctk?dz1"Etý‚~-n¯kºý·íÿ2KÌ«h*äárÔþfº-ÛCèOø¾KºH^|"_Ì?Á÷Rq­¸K< ­(å5ñWñ‰øB|)¾Q_CÉUò•ø•ó”)?SnWàûšò7å+5Sí¡†ÔÁj¹Z¯®F­6©7àû˜ú-G; ™èç}«¾]@Hÿ~ÄHt\á$çËßÞ}¼ÏñwÚ©}sûÖö–öVó/”Ž1ÌA/ø©µŸïrŒ÷VHÜ£ô{‘ˆ¾Ë}ÄHq&zfžX.֊ ѓWŠ[޲î¿O¢—þ >G=Jž¬se°R©LÆ÷le±²V¹A¹QiUÞP¾Vj‚𤦫}Ô1ê\u±º^½HݪFԗշտªÇÔoñ55·æ×zhEZH£ÍÓÎ×îÐ>Ò>Òçè/énc•q•ÑfüÃ1Ä1Ò1Å1Õ1×q½c·ãug¤ó)zŒ§˜8¤^®V«ÑuJ©–­¼ª¼ +yžG‹Ô‰ +$Uy@lV.­J~¡1B!&Ñ­}ý¬²]9¦ŒP'Š b:-WZ¹iÚNrí):¬=‰¶½Šœ/4Å¥ÊçF"µRÊPæ3ê-¤¾Doªï +‡v'ýYs‹LqX¹_)ø•6R¯£|õvú¥ºV\B)ÕP¡ß8·@Ž'‰Ð 3D‰ø·j’ªL‚ Uߣ ´Bù#Æ<ÞL?‹´sè:*ÓGtfEoý\£‘.^P–iMJªh%E{­+BÕÓèJ1W½Õø\ùO47½£>ŒÚP~©NԎèÓÄR̀Kè*Zk^NéuÚïÄ9¤ŠZ*ÔA»]¬–hù —A«ÌNۍٽz`”:!Yœ3!3¡!nÅwô„ Z†9> ZìUj5f(mtŽîÐ:DÚKíÓh¶yÝbžCçš7R?èƒMæÅÈñú€®§ÄÆöŸÐꎙóŽ8S¯Qè5f?¥Iù“2]Ùzâø¢· E}Šï/á©?AMÚh:U˜[̃î^а·ÐOG cÕýTÚ>Ii6kÔ5hï»4Õ¼ßô 7-5WÒdz’îuè4ßÂGÄïÐޟÐbeš¹^]ܾ ýp=z!ŒÞ:úçjm­¶Aûж`Îo…¾Ùy³3‡ç>…ÏÚ¸~Ýyk׬>wÕÊ˗-=gÉâsëfÕΜ1yÒ¨pÅÈ3ÊG /6tð Ò’Šû÷ëêÓ»WϢ‚`ü€¿{·¼Üœì¬ÌŒô´Ô”d_’ד˜àv9†®©Š ¾ÕÁš†@¤¨!¢ǎíÇþà|̏ hˆTs"O$Ð Ù'r†Á¹$Ž3lq†;8…/PNåýúªƒÈ+UÁ@›˜=µîk«‚õÈaéž(Ý7H·îü|$Tg-­ +DDC :RsÁÒ¦ê†*dלà½ØÝ¯/5»àL€+’\Ó,2G +éP2«‡7+äô R‘œ`Uu$;XÅ5ˆ¨…ÕóE¦L­«®ÊÍϯï×7"F/ .ˆP°2’’,4Z1FG²˜À2n ]hiK›4„͟SQç×sÉ!”[ÉüñûY^dž2ºnSll®ÚTµ,ÀÞ¦¦MÈŽ©u±±ùü¬¯GH«Ö44Õ è-èÄ Ó(MÙX_Qd€[­²Ú·8XÍ! ËW°2¸´iy†&§)BÓ.ÊoÉÉ ï1QNu iF]0?R‘¬Ÿ_•לFMÓ.ڕdŸÓ¯o³/ÙêØfo’íHôÄ:wÄI—dgׄi=+¸FÁqˆH`a5© ¢MÃø±x5-6|êREaD–E\£š|Ã9œÓGôB_0Ðô%A‚‡ÿvbÈ|;Ä(ô}Iìd9é5ÄGݑP(Ò§‹ˆc4Æu)ýƒûõ½ M ×ø è>š‚¾_?¼ݟŸÏ|M[˜ÀiœZgù´ ·…ÂÅ¡úˆÒÀ1û£1é39¦1ӑ¼!In%6yÓ#΢Ž¿$_FjõÒá‘ñ=ы­ø Ӄ¦Î® T75Ø};aÆ >+~XGœíФޮSsÛ¥äª2B9§ƒ™=u‰­†êEm'¤R†ˆ@MÄ×0ÖzÖ»óó`¢6ó§’¤3™]ÍÈðЉþ'øO¨^b“Š +cy0cvS“û„8ˆšUà8›@âiF]~`t„fbfâ¯ÍÜ?ŒQŸ £ËF3äÏ +²½'0æÚîz|X:ûõ­¢kjª jššæ·™ ‚_°iòå7Mkª¢‚Ófî½&7R³¥}µT ï×7È1MM‹šI-D1áÜf!CG_S™ªF„‚ùÁºÅhKópJ̟Ñ0.…*›ƒbóÔæ°Ø<}vÝv%›gÔµ(BÝPYß\€¸º=,2TáPdO€=4A kZ§äÏÝ&j”±š þ…m‚d˜3&ha›b…ù¬‚ŠdAa– Û4+&åÖæ´Â-î^6·1>ŽÙKXqHFZŸfxfԅÝCÃÃÃ#Â#• +=ÂA-Ù Þ‚v"·yN“Ám¢±yD8wÌišÍÙNkìC͙-&#”g5|fg fήÛ5’¿|‚£’?¬iQ‰Ø9$Ëù¬P]¢Ò4a:$#ÝÃrÝ1ÑNÁȼà…ùܺHmð¢|#hk05Ә¼ú¦¦¾AôÊÂÚ:ëÉQ¢orª4.ˆòææA&:½‰H*åjW됎Ò~-í<”ÆŽ¦hq‘…§, µˆ³ø)ÿdõ›‡PÐ*«´UhӜ¦ÙÇüH7.Ø®¼Þ¼z™j²MÖDÈÅi!l‚%<—¬ä &ƒã›•I!I…¤MãƒÕ‹ÀÁÀ¢;ƒ•XTÏ\Až4,øßÉ$b˜x!‘™7ùFD}ÂöYÓ·)rΉޥÞl”Âþ–š@[ä”͏,ύ¬¬u°Ìç67anç >\&ÃhÀ²3&Ò¸p>ªˆõfÜ Æ# P·ÀêA^¨›ØrZ8ɸ—í’"ç†NÈ:A@E!#nN¤qJ ¡>Ð"¦¢³s4°æSp>ë)V{¦@ùƒÌošŽ´ÄÖq@Ÿ-™¿8ÈÊ5Âònõ>×QCíhz]„r›š‚!T±°ÌȾ(bc‚¿5¡àüÅlÙ-aÃn±er º²w8·Üê`~=X”Bٗè8L´üXØÄvã܆z"¹)¥)Pք ?ºJ+ZXÛ½ðjr¨çç‡NǾzdd1º +™éå_QdU¨y®£°3Dþ­YÌN™«4""S¢,ùÇÚPDɆHn¼˜6[® (î<½pº7 ©ÊåԘE3ìeÃJ?Ž“æFÌJ†úèyo.›§ÄjÂ9‘” ÓÎÊEÇöCØxóc-O‰]ÃPµGdc¨.Ü×åqõÉöäôéíéÓ§Ì3$}hîð>ãúÌõÌí³Ü³¬OÀ&ÏU½o͸-çAOz¯6óãքcfO8ÂÙìº/{g¯ÝÙOôz:û@¯ß¥¿ÝËY•!º·™GÃɉ‰ÆÌ”~ê‰üÜf +Of—?ӟêÛgP™VÖwœ6¶o­³>´Ä¹,tAâ¦Ä¿ò|J:È+4_qÁ Ì’ü´¬y½W÷Vzç{+¼×{·{M¯¾Ýû¨÷s¯ê}Âüš(A„¯õ&&%)3½mæ§­>Ÿt §ù|ÆLo¢Çƒ§‘”„g‘ÇÓ&­µÞ¬ÄD8«õzóÔÌ6e箬¾  {k³úºÝ•3³nNËËsPG[¨º§»$OMè=ß7ŸFùÌcVÁTKdþ›)ÑvKò Ÿ„Z2<e&æ´™“•bG8C ´ÄDé*GeÏÂñV8«] + ÿ·­\|A›rVØÛ3LE¾¢@р¢G‹ô2,þ­^¯2³¨Í|Ãr8mpÞ,e†{ZÚô¼s”Eúb×´†¼ýþ×õƒ©ogúAÚ癟eÐíßôgøý¡œòŒòœ 9kü7øý•OÿŒáÊ`Ï¥ÚS“6.o–»ÖsŽç㣌¯ÅQ¯O¤«Þ_,€G2¹Ó1%²F¹1¤Üa +dE§CkmV© 'Ì/¬™ÒZK…ÉI˜9¬I×èÌIŠò… j“ +}¾×’…/9œÜÜ˜¬ùÃ,Jþ0 Wr +Ϥd9/Yä’ –üä,×f~V\²—Åþ¿ËÙÇ¿[Y“ŸˆÖnwmòú”¨à§D?Åüݵ)ŸæðI®ðˆÚ}ŽŽw¦Có;*“ª£;×Ë!ÅÝѝkàâî*ǑÃE;²»š’šä;ÙPh"‹üñPì$(÷q˜ïx¨ü}H.Ľœ‘Ì¢ÍB †87«éذÒ+¼^r'äÂÛº2Á‘D`HoiJYE‚›?˜’›2‚š yR:bùU‡-~ú²ƒç/}CÃÖâ]ÇŸÁ½üäÂ;¯ºcË7wojÓÔQŠ÷ë%ååýì›/?Í7+Gw¬鐸 )q™~ÊKWfªsõ¹®™ ‹Õúj×âg:ë4ÙÕp„§±«[?{¦üIÿ:íXŽ60exöÀ¼Q)sFåMM™“=-o~ʪœùy¦SŽeù(C$y23§d4d¬ÉP3ò’nðíð)>Ÿ–›çvÐ^e' h>Á*NµOqsjž– ;Ò!U™Q©’îG•cfØEÛÊ#æaáúyxéàóp¦®ž}E<“ã‡oWaÑ ¦³2õ ÆQ5¿»6£Ôç´Åçµõ£Ï’«pj­¯À.è3(*/Q1ca†P­##ByR„¼R„ò¤ðdHA‚ !ÈKh"‹Ïûƒ8[ËaR¨ 9Ç¡úÞ¯8 -9·üøÚrÁʑ…H̕ºQ¬=/7܍h +­¡Fºô¶c?½FŒ"_˘g¥||Jªêsk©RÌ´w®Ô—n©/+B)eóΞ[J.-ž»B'2Y]R²JK(9͑ŸÁ"'ò‹¤ÒTÏÞÛ÷ï{>iÿ\¤½uPxÅ·»[6.ÜrüMejâ°Ú«/~PÔfÞÝ*üB‰¢Wû;í_ùî]*n¾jôÒû{¾Tˆa£þ{ÊgKùëžæIÙÅÙ²ÃÙk²oK¼Ýó Ç™ãéå‰dïÏÖ²yý9þAݜ51)Ï-ҕPZª¦äޞ&ÒÌTk°¯M k™ÑËŒ*„Lk¤°Šgj¤*7 +A,‡ båùÝ@";Ì*(;ì +¢4–HêÅ!ԃ•õ•€TJ<¾”Æ#K–‰"¶ò"Ç׏Kãî¬ì'Å^ʧcÂMY¡Ð±XåÐw´ÜW.ÅáÐá¹TQQ^^~š¢,<ú"<ɆËa8±Vû\)¹”l$劐õ¹ür‚ +9/÷1rg¤ª +ÖÀ֕ªáNjSK0 ÂÐҒ +ŒcirppéàAC‡@wd:xìÒÓKӃÉ-Û·§æl¸àÌ9¹ÃJ¦U8 ÞºeíŠA5³R~á®iX°åÛ%¬'6c¨Ê±2©äPÜ8¾ »Yþ´IÎ1X«:ͳ¹kσ|±œUIMÂ`]rÞÚÐw}r×ܺAaÁ‹RÎ+,S©ƒKÓU,C›[[[µÏø&]+úæMÌê —¡,/âò“¥e”ç»e#N:XO1âq#“ëIãøx­.‡KçÉ>tØ I ¶è€íQ(i¸0=sP’î×·ëïêÚd<Žèª__£7ꦮ¡õnE-´ÔÆP[m¤c^m'±ŸŽ`ªP÷i6ÖánRsÈa#9l¶ÖpÚ*Ã38L¹XQÇàÑ$íÄÁãÑcšÇ‡Œ}'Õ.rËq²&~iò†V}ï×5¬o7©bÅÏJ}›ª«Fªò€¯Í÷žúQêõXªÉ9î‘àt‘Oló½–u(ËÌÒÎ4oZFJžîF†Çíñ&zO0ú¼1K²7j†ój½Yaî…,iì%ôbwB÷Fo›’Y—&È~Iè!9Ø”Æ^‚ÜçÁÿo®àr³V…ÿXXn@Â¥C™  “²xr É:’¥¬ÉڑÉڟ¥e©JizFTýgD„ŒèŠ!GòXkr²5N–yҀi¶šÿš-T¸9<š=nûÃ)¨Î>G +À¤Lß±¹1Jn{Ê¡ëyG4÷ęÆÚ‹aJVN.)–æÏ0’]n§ÛáV _lâ\‘äNÉåíÁ'ˆCnØåqg¸…¡ê)mꀖ•ºµó©ˆú{_“LT$Uò¦»Î»áÎ)>wkŸc×ݯýüÑê5K.9¾N¹êÜU£n|ùø“æ*؇=!-Ê&ËËîô,np*Ÿ°#‰Ï ³+[F¤8ÜىcŒ±ÎZ£ÞyޱÌéäž2Ã}VâJu±¾Ø½2ѝ™§9’óÒ n"–³ÇH‹ŠaØW›V+÷¹RÜЊÖ~Â!wŸzDnË¥CšbìàA•i¢I´ pЇ ‡Ï€™×± ß]ëøn®Èež6áöF…Ì•-¯½Û…9@‰^¶R¤å(…*O +•´ +Iî£)Qê‚ )Vaí§ + A¢/Eó¥D{3Cs؀”ªaî ’óqî±Ðܹ'Êï?Ceð¦côœº°kº>ݵ@_àÒÄÜz’z£9!ÙÚ{$h™Ò(Ôl£•½o(äˆÒÓ¤=˜³ó¨ºçêgþ,2~òÙ5ï¶ÞӲ骖]7µ(©¢çu´ÿåø+Ÿ]!º ÏË/½üÛg^zMÚÔ¾LˇT¥PwqPj¡õ‰¾~¾3||ZE PüÞ‰Án%é%Ý*»­ ÜpÏž;>s|n½ó¬Ä9™sr—;W$.ó­Ê\‘»?ðû´·³ÞÎù}÷÷ÓÞï~(`2‚ZÈJ¬ ÷Õhã}³}$|֭ݗìŦ#Ï`ý•çM oö •#PÙ•W›]ðš[øÜawƒ»Ñ­¤X¤ˆ¹aè…X¸ÜY¶ÿëVZ7ï=x\Ý|þÃbåæY’Äë^/RK•ÒÎ-jT%Ù{ÕpvmJ!Ñ~!n;DDš_TˆÉ°ŸyIËˆðqqÂÇe )ÑBîOk2–!ɚÁ yH'RX¾D¶ÌÐ,»á–ÀD륣ïûŽŸxxƒõå°TFö^¼Ø¬¶’7ÙË{‰ÇWz rŒ<-+¨Ê •IÀ•ŽÔ:éi +oZ{&«1³éžá7.ÝüÚòóßýÉìëû'ßwÁ…Ý¿~]sû2ýWMS§n1·ÝÝþÍ5g?þzÏ+O¿tð¥ÿ½Z`~¡ôÑoÁNá\–™QJ4Û;Æ,!ÆíŒq;bÜFŒÛõ;X4ÈÅs¸ŽÆlXi‰·P)Ãç +%¹!%jB’¯õžS —Û®®Da:œÕ®êÇG£ã‡FP;Ç~ÇkÃÁÒÀˊÒéøBì9¬ó Û!÷ŠÖd)".¸ [Y +×±WYNYbHó’8£xØZa|ï-—g ÇËyð’KK}/°¥5šUŒ_I+7a×Jáöx’½n—J·ÁCYZRRlOüÂLëØ7ÉC“yӐƛ@ŗsfù‚•}¯¼r×c¥†zu¿s»oä⻔…[„ceûµ[Žß4±oÛI°9þ¡‘O ñÈ=IžÎÓ#Þ]ÿWg ŠÀ=جÈ7véI"ÁÐ6@7¹“²¸“ŠYD+*’ù€0÷ñ¤‘Ô#»Ìh3ß OÉ.›´UÛê¼Å{kÒ~}¿±ßñR’+)œQ–£¦ºÒ=9¾ÁbxÂåâºgqÊ,­ÞQŸPçý¹ØæÞ–ð¸Ò–ø|‹ޗ}oª]¿õüÙ÷;%¥ÙõHH¤”ä¤,Èà)íeW’AЇÜnÅàÕ»œMéeU熗†êpº\Â0\º¦B ’|Pö")ÉãCï»O‚šèsIJ’Û÷,=ëR|…äJ#r©ŠçYð&ªi‰‰ªÛåRUìþ<žÄDrON)ã<—&öp'Í7\—†Ým"÷ñ°1ÅhÄ|lSF‡½õR¥Çdtý¸ä‹ŸfA™{ôpNöñ¹Çs²û>ð=ü!¶™°7|åÖs“Þ?4wÓ%OoꟚ{‰ïiâê'%mr>½Éë{Úz‚8¼¾òrgy=¤ H«7«[Y÷wB·²Ä™e*Àþ–ü2Ï+wz™è‘_æ +ç•Eå´^š,蟹õRɲû0¨<¸l±b@Q6.B”ffd:® ÚS$‰+ÛoùËÝýóúîúCûOÅ5o¿9¼ý¥—hÿj̀ÊÒoڏ¿*Æ×·Ï%y›ÁøpÉòOӃó’Ê¿tæ:å­«»ÞëهéoñÞñ¯=~ŽœSáu_Xײˆ#Û'Ñh}ýè×?ö‘Þññ]kØA|gÎFDù­­£t`œ£ýH¯¥:±‰f+;éb†ÚÂÚÃtxwÂ? +t/§ÿLà] ¨r찉À|`:ûÁ»‡Ó"5œ¤ëh¶ÓO«õZó8ÊÛª?GK€;à¾K{0Êhü÷ Ý>h(ó ÍVc'mCøíˆ_ˆ°;@ëà¿î9H7Àv»×R6SÀ@xoäsÝޞê¯iˆ¶Îü ÚR<ÇW¡Œ) 5Àð¤‚V›Äs´Yçqµíeà}d~ª4M´çFžŽÏÄ ¥o>ÎØæ\!Ú3ÉgVka®‡i¨¶ã“=¹€ð™ú_hŸz-Æú ù'ÑH +òp¤Ó|e+tÊRÞ  ÎtMŒ sñ²¥Qy§¬óm™òƒ˜¯Úx߯1àKÈÑÈd6¯ ¬Ÿåú \eËëòù|î½&*Ÿqrºãå2žÊµú=:OQÖÕÑö³~dÇ:’õë™(<Iߤ섳~…fÛ󺇍ñ¨ã_í¹=ŒñžešFy¿Ñj> ¦˜%pÿÐÍûÑv¬©uf»½žöŽ®¥V8%D×Q½”VÙúì©o¾ ŸÉu´VÖÏeyœéÐi ö xÒÉ ¾²Ât¿” N»‚ˆû±ÙIàáüî”i”b÷Ç=²/dzØ",ÃÜÈÓH§iҞøŒ¶ë3iæÐŽFºÓ˜‰9—N {‘n<×éräz}3…ùµºi3tIùŸm~£>„ö\½¨è£‡(KoD®m¯Ò,»‰çº“ŠXFŒ›¡‡Ùž¸™š´U+èZ„]«CO¢Ükv%æos÷j¤÷Ûz›PöÕç´l˰ÀóŦT£QÚ$ëÀv +ÊW?¡;Õñ´r<Êy3úa#õ£ô1¶Ó|UõÑ% C•RúJH€›×Ð=Úå´L«¥u æn2õÓ~‹¹úݦ&Ñ<íEºMk£-ì×R©—Aû[a[røšÂáÊïàßF³µr¤ßLçjóhÚ Ù{ÜÚŒ5Òé×AN +þ äkC¼G³ÕZÌ­«àþ +ë ød­æ8†6–úÉt1u"®ÎÊ´j<Æõe÷ õE];ê­ã)ê'ÛÉù"óh·Q9úé- Ð¢íS•ké!`‡ò&V'ÒEâs/:¹&ccýÚ`q1Ð_L—ÃÝô€G-?l·Áôg`#òÞº‹÷ ¥’†0EØÀ6à¥h\,¸œS…ÇBÏ5÷žà k Žš{ñüèç!(oˆv†¹—YÏ0.£4Ç”¦öDxw¤‹ó빘OQJæ¿NW§ï>cú1ÛÆèx€fü¼CLíµþ/õûß㛠ýûwJ·dˆRÅæ@kÅ”¬žàïj´?£ã„ð›dxÜø)•f;÷y|x¼?~\OçWvѼXDå Cn¤‘ ­ü@¼ßùdÏ î™“ýÚý§Álê£ÞÊu‚ ö<ÙoL¦ž ¥uÍá4˜s@‡ÿtÀ¼2½‡Æ0xî2”Vì׀ŽøÁTÍèìWÂýªÞjÅGÇ':.ñãƒú Ô^¥Q =A‡ƒN¥±s6~ÞÆ‡EuÉ©xâæÆÀïÊóÿOÀÜyxxöÿí²AV`¼;¤väAØ'gñéãÐ%ß÷AÍý°z·÷uƒº¡ß§vžñ™ÇKžñòly=…ž?W;Èg_æÓòL¯’‰ò=ʬ¡þè9üé|6äðy‰ÙfŸÏ5_`œ…õÐÅkÊ­•ï„Vh|ŽûýLM *û 9-z–ÌçS¼^ýÉ'Ï1bϑ߃m<‡ª€ +û=ÕL>Q?ïj6ñ¹»:‰ž´ßoEÜ;é×st‡sÕ8/“ï›¶ª·Ó„Ýn7BòýÊÌèºÊkâ)Îþø,3§ãLÓns¼M ë7‡Îäó˜Ør£éœ5XK¿çPÖ9æil¬ñMÀ"ë}…yìÔçæËö¹çR{¿ c͏?§ŸCSÕK±ï‹žÉÞú­]Ø}_—hYè—ãße Em¸gɳ>ë}ŸA¥Æ¼‡«‘ýü‰¯qΟ|h•'ËfÝ»4Üj·o³]nĪ{ûMüíyVeºH'L˜†ô~Ðéĥùè/>}ή»»ÛýÁm~œóêÔ ôµv+tFÖê4ÇN‹j?¡3¥Î=pÂZµFêÃ÷è©ïLè¾r*1<°C~A•l7°×KþkôEX›ö li/"]{†²õhžv.U©»a¾Eò½ òf½Í6‡z5Mä»JùNˆß\H›Ü­Ò~ñ'Mûõ½…öa϶Y¯#ô†£?ü7`]¿“.ÔB?v®¢}Æ~gJK°^ùyT¦_Ac£{[c¹ôDØ6un£…޾ßIíCÊsm‚]÷MAŸ –ÝñîÞAi¿Ï:_‘ò|ΔuF}a‡iØ[§Eï èsÑ'‹d}&ÉwN’†=:éŸcíG½.Ø^ŴٕE;Œch‡;5$ßË/±û~¿rœCõMTÝ»g;Jù}\ô<¶ÛÚRi/¦È÷Zöy@æÁïÛi ߕˆ·k¢vT‡MaŸtœ9DÛÊëgGûmcoXg +ûaŸ¦SˆßãÉ3‘xj×I¾ÇÛY²íYÇ>ïPAï£%ÆU4]Ÿˆ~I¥éާ(Å1†²Ø>s8¤]·Š×hý+Ø¢Ó©c3ÀžÂ\n½3ëí9Îgnæ`2žm‡ñYÆÜL@øL;-âÍó­}†äá÷gM¶{´E§=þ¶ÍÿHÌYÍ»ä>$k§Úw©®:‰v¾»gù©9-ýgh<‡ùNÕ)ÞñÇӛ@—Fý°óÞŽi€µ£ã©ý¾ÿR‹Jېé½6½›em½xå»î³|kͳ(=ñÞK”žmÓ¢Ž{9§¡±÷d:©iÚ~ï=»³ÏÜr¢ô÷¬3¹Njœ´Š¥rLHµíX¶ßÇË÷ü|7ç{Ðq‡ë +ÈÀ‰¨eð}‚SÁÀJÂp¬<¶ÿ0®G:À鏇ùOê|¹ó6ŸÙ¸‹¡ +ì¥í§ñ0ÿ)qêûuUÆ/P.àìgÁñ‚iÿÐäÀJêL‘Ôàµð{+ƒáøÜÆ5Q˜&#ÚïÑ~Œö Úö!Ú½´£ÎÑòí|ÿ¯ãø—ÿV»¿¯î±°ïèE)ßÝ3NYoŒÄ?-È»4;)Ն~}xxÑÆM ̕¾«¤.†<-–÷;Ҝ$×boʰýöýÀeçȲæßý±@õ§êÇbKþ=­~’÷v,Ûë´Ãcß±]bë¾×ºÓ¾'ëg݂u—çùí×´äD›Ïœní§Í»°NêàOÖ×Sò’y·þcè„#æóúe°”u¥lì°l?óQû¤!ïï¤c½mwóXë¤y¯mo³{ž…ö¬ðÎzEu¯úo´ãʖ÷KÃr=E[†=ý2ÊV?C<ì~ߤΧQ¼f¨C`[ñ› íû²|öð¨úeŠú@Ìüæû5|¯wrxœžÅÀüÏÊôÑý}/y¾´züÏä—w'ïô ¾ëÄv‘Š…>r1¼SÍߪÛ@ÇÚø7p.ê[K˔+©Ÿºûá×`ï¤#|-°î,Ð$ ¸¸€Êðo '_ƒP5ø_Õ±·×ö•-8^î·wÓ"ØÄ‹ŸÅwP¦±`Ð"ñYÖ"µùOÁNI…E¡¦Ûnñ‘nŸµçsæ—qQW'ãªq/¡uh1ìˆQæ^ñ •k³)cêc¬_µ÷¼o: ·Ì;àñ¤{Ñ÷ä6Õ¡eúÔO?ûà-ÈÁ!*׏Ñmzõ2¦`{˜Î‹½¹Ä÷‰å]âƒæ«Ñ³ï(Œ:Jw=Cc0†Ä÷7¢TyˆXŽöΔë‘õoÐb·EYyÊûÓÖ\“v®£Š6`×cí{ßK¬÷c°A1÷4ëžj/í^ꆜkՎÞ2y>L‡nè8{eÊwÚX¶l[ṁ•ßñ¾uÉ1÷*S¨»ö,k_jòyõÏ>³¼=æýÓVÆÿ×ï·âßC}×û¢ÓÝÍ8Ý]“üÿá;•ø»§»ËqZÜ;—Ó½/ƒ¬²\ƒueŸ±Ó<ÿãÀO¡_ïahdšò|Ô²×®V0·×c:Ž +ì3Q>'íýÕ]Û"Ïô¯²ò£Tè¦JëlÞüÖþƒC»¶zOùžªV»Hþ&f¦š~èü}U‰ü}Õ¥à+–÷{§k?¡™ú3´\ÿ=-ÔÿM÷¹ÆÓ} ·« +ÐGY¿ŸÐΣÞ§Á®Ø¤¸±_[G“°>xaûläºÈú€Ÿãä¼]‹5íjÚ¦=…¸@WN¬cÅð¡mâSÚ¦®Ã8G}Rޛަý t âWÙôÏ[ýàßÛôSm)9zèœÕäÔV^òØSAÏÌAÐf ,ç#¬‰OÑ ²§×iµ]'âSó(êtènàÍh]â!ë ®G|Þ±øÈ®O\y î‹Xp¿h_P”¿øð:ê4ؤ>±¿bÁuíÀ—'Ö[öaܗñྍÂk÷ó)Àý ÙãÐô‰ [Ô_¢lvs»™çˆUG–)#³I‰Ž?dòLYïe}·i…´\Ö åè5Ð{ôóLëÈӒ§ëd:æCœC®÷ó£Ô[Öá9)[ã¹\Žçþ4ŽR’±èû{- Óã߆ú¥¶Ÿi9(x¾ÿPü[ê„XÜ~:°>ˆCI|˜–a>ëgýLR–Þ+ݎÓåÃ:ŠõÓé€uìþ¨þŠ/ƒu6€·C¯Åb͊éÙ÷|Z;L¿` °iÒFý=Ú¨A¯!ß"ê t€\ ›Þv\‘íw½·R7‘×s¯÷w’²íebcÞv:8Þ֋ڀñ|°Ÿo˜ó@?Ýø]w]¾Ë—&þNÌéêu’M¯©Õ<¤“yHÛj~¬½o~옛ðM*qxAS©,Aþoí}å¿pD´XÇ4¾ž?ôÞÿm7ß”6Å+֞‹÷ñòÁöý±Žfc_ÊûýKáïæø%¥é”eL¢;ôÿ¡MŽÉe¼Ùq‡å*çò8R)ËåÅ:ûªýŽû|ýnØ_«åYiªü-1Ûß=hŸZ ÙÜ ½²¶T=֕ÛÈ-÷‡¼| 6Ì üQ“ÏjªØvâwôl·Ú¿mæß0/3ÊigB­ùçx3%!‘J gU'ìYß E< ïò×Xa”£LÅ>ìê6Ö¦½m ?Wғî[š_)ýéZyïòìöÉ}<Û"I°¡SZOó_ îóïƒÖH õŒaŒû´ûŸ;ú§½ƒš;÷§#cî1¢ódÕ0 CÝ-û’©<š úècÖûRsLœ›¬÷vLåYV0ËÆq(¶Þ™ƒ€Qûž÷ö±¿=âßÙ{þ>ÑßiU°ÏâiuŒ›ûüjOèÁ”&~Lý‘Ç|Æ¡ +ù{ØK5õé~öÃó /B¾×Ó=2®šîÕÊè^Çbº2}dö6èÐrý>j’én¥Û ÒF|7»,èK} lUp¯ eZyfÑýf`Ïgd£¾i·¼ƒr–y‘¸Õ¼Sñ“_¼i¶jyTi@£yjw„ûð¼ hö¯žVہC£vSóZ~ߨžj6Òf£AIj&}˜€Šzf¢ÔLš Ì®¶†äãÕÀeÀ>àˆŒ «™-7–¢î™-×H²kùÊéoyç̕Þ]³ê-:qªE«ÆYlÃ-¶ƒ¬àþ•íÙ×¢)…%Lݞ’ý£2Ô 42_ƒ§Pž¦$!ÈO;ÔtŠŠjØ!a5eWAQÉö}ªFBUTA‹ÈoîWE‹'¹d”[1•Ï)…üÊߕÃVŒrx—7¹dû¨ñÊ_éQ` *Å÷/Ê_è2å÷9žÀv`pø0”Cø¾‹ï;Ê;”¤¼MÅ@0Øì>ÊÛxú”·XÔä“Ý€¢¼…§Où3šõg<“”7ázSyUû}Ëв’=Ò*¶þBۑ™k;R2Jڔߵ|ÕU„‘†D=¡ö ‘Tªöh)ñËj)_æoSÞÛùwŒ ¼N@AM^GɯS˜4k®7àzƒ€@€”áéʋÀËÀ4S§òZ ŠiS´UúGe(¯*ÏQ&züåyI_Vž•ô%åI_íú¢òlKw?J@ôÏb ˜ Ì® eŸÒ£e‘?™!†¡+E¸õoY8K4¾(ëDc‘h,¢1 ††Û”ü–q¥’TK²kO:Ð3FBû$)ùèÑ|È|>tÂ><¦ô…Áèa1gwgÚcWŸ +ËßxÉjLŸ§ð) ÃSô. a€ž‚=…LžBIxVó€ýÀç€ à_/ŸIxÀ<à2àsÀÕùPhµ]ÅGeŸÒÅvÅ'šò¾=ðÍWòÃÝ|y¾o¬z}žHê.&w7»+C)#*;%ٙÜ&<»ÿåù÷¿<äåR®S®gÕ­Ü`Óë[¾‚êÛZŠžðJ?§î$O”Q‘(Fë¤0å9™¢<å!В–¼Z$Kj)êëß+¼œj·ÿ«¼÷ýŸäµ)p~œ÷„ÿ6M´ø"ä¡Ýþ×ó®ö¿PÜæDȓEmdo@²îÉæäEÉz9"nmñ_Êd·ÿ’¼1þy2b±qö:øÂIþiE³ýc‘_UÞxòÜí¯È;Û_nq æ4»ýP…åìƒÊöΓ…»#¤Õ?xæÌ¡mbi¸¯c«£Î1Ù1ÄQâèëÈwøÝ¹Ž4gŠÓçô:n§Ói85§â$gZ›y(" `šácÂÿj¯ Mº} +?å?AŽy-œ +§Hª:A™0½RLˆì_H"Ǧۄ{ê숬‘” 4aFedXhB›ÃœšqL9«®YˆëêQ6· šQ×&LژI]·‡„HÞxm.Ó^¯­¯§¬Œ *²*RF&—ÕTâÑ`?CŸ¬ÜÝ*#['L¯k¼sg·ÊúH‰t›&Ü"7MÌ©Û#¾Gª«öˆ0©¯Û£Ž_TOãpudU}ý„6Q+ù( þ>ˆÎ?$Ÿ«4óQÀÙÝâ»Õâ+Dzð0ŸËE…’¯Ðå’|š`¾æuÕUÍ’'3@ë$ϺÌ@,ϋ…à),”<ô¢äy1£‘y"#%K^XºçI‘Cy’%OäH–ÚN–b›åê–«eIªèäɳx<‡¢<žCà ýÐÏâÊPHìQ¿pNõâ`uC°z1й悥Y‘Ɓ@óÂzŽDÔ¢† —2¿8R\\Y¬ +4˜sŠè9="XÕLsªgÔ5Ï /®jQœ_U¿k̔ACO(ëꎲM9EfS8³A\֘¡§ˆÊÑc¸¬¡\ÖP.kLxŒ,‹¤¨O©kvReýè9Ý¥$¸!¶ ¹ùõ•¾5#¥ ÈϺ4w/L—(!TI VF<GõÕoGajq”ÁIvTÖ¥#òs÷Šì(‚“ƒ•Zþºó)«zY•õ·­?Ÿ;Üz†Ö}×qՑðüªuë‰&DúLŸ©˜:»®Ùá@h7)2<–PÝfî·û#p8ªj#‡•s˜Ëe3ž<þçÛt4ςFå‰]"Ü]¬§uõj¤û„ +4ŒÙhëœÙu{aXñZ±® \'Bb]4»Ú¡Y~â6G±þ|Ûe÷Åz›Z)‘d]´K:>ÜY¡Ž[/³•ÝšS7Ê«Q‹ilç ý@û–€–¨Åá”"¿ª õ»œCý î*¿Ã¨òGs­Ñÿ@óª½ +endstream +endobj + +xref +0 63 +0000000000 65536 f +0000000018 00000 n +0000000263 00000 n +0000000324 00000 n +0000000376 00000 n +0000003436 00000 n +0000003713 00000 n +0000003977 00000 n +0000004663 00000 n +0000004842 00000 n +0000005610 00000 n +0000006384 00000 n +0000007213 00000 n +0000007548 00000 n +0000007880 00000 n +0000008591 00000 n +0000009006 00000 n +0000009358 00000 n +0000010147 00000 n +0000010761 00000 n +0000011056 00000 n +0000011388 00000 n +0000012070 00000 n +0000012249 00000 n +0000012430 00000 n +0000012724 00000 n +0000012905 00000 n +0000013008 00000 n +0000013042 00000 n +0000013298 00000 n +0000013777 00000 n +0000014190 00000 n +0000015646 00000 n +0000016087 00000 n +0000016412 00000 n +0000016700 00000 n +0000017032 00000 n +0000017388 00000 n +0000017678 00000 n +0000018505 00000 n +0000019105 00000 n +0000019367 00000 n +0000019686 00000 n +0000020390 00000 n +0000020777 00000 n +0000020880 00000 n +0000020941 00000 n +0000021218 00000 n +0000021572 00000 n +0000021920 00000 n +0000022278 00000 n +0000025321 00000 n +0000025581 00000 n +0000025933 00000 n +0000026132 00000 n +0000026486 00000 n +0000026674 00000 n +0000027032 00000 n +0000027220 00000 n +0000027253 00000 n +0000031015 00000 n +0000042377 00000 n +0000057909 00000 n + +trailer +<]>> +startxref +74555 +%%EOF diff --git a/tests/resources/2.pdf b/tests/resources/2.pdf new file mode 100644 index 0000000..1e10b45 --- /dev/null +++ b/tests/resources/2.pdf @@ -0,0 +1,5211 @@ +%PDF-1.5 +%%μῦ + +1 0 obj +<> +endobj + +2 0 obj +<>1<>2<>3<>4<>5<>6<>7<>8<>9<>10<>11<>12<>13<>14<>15<>16<>17<>18<>19<>20<>21<>22<>23<>24<>25<>26<>27<>28<>29<>30<>31<>32<>33<>34<>35<>36<>37<>38<>39<>40<>41<>42<>43<>44<>45<>46<>]>>>> +endobj + +3 0 obj +<> +endobj + +4 0 obj +<> +stream + + + + + GPL Ghostscript 9.19 + + + 2016-11-05T03:15:25-04:00 + 2016-10-29T16:58:19-04:00 + (unspecified) + + + uuid:eb5b3f92-a075-11e6-0000-cfb413626b8c + uuid:eb5b3f92-a075-11e6-0000-cfb413626b8c + + + application/pdf + + + PyMuPDF Documentation + + + + + Ruikai Liu; Jorj X. McKie + + + + + Release 1.8 + + + + + + + + + + + + + + + + + + + + + + + + + + + +endstream +endobj + +5 0 obj +<> +endobj + +6 0 obj +<>>> +endobj + +7 0 obj +<>>> +endobj + +8 0 obj +<>>> +endobj + +9 0 obj +<>>> +endobj + +10 0 obj +<>>> +endobj + +11 0 obj +<>>> +endobj + +12 0 obj +<>>> +endobj + +13 0 obj +<>>> +endobj + +14 0 obj +<>>> +endobj + +15 0 obj +<>>> +endobj + +16 0 obj +<>>> +endobj + +17 0 obj +<>>> +endobj + +18 0 obj +<>>> +endobj + +19 0 obj +<>>> +endobj + +20 0 obj +<>>> +endobj + +21 0 obj +<>>> +endobj + +22 0 obj +<>>> +endobj + +23 0 obj +<>>> +endobj + +24 0 obj +<>>> +endobj + +25 0 obj +<>>> +endobj + +26 0 obj +<>>> +endobj + +27 0 obj +<>>> +endobj + +28 0 obj +<>>> +endobj + +29 0 obj +<>>> +endobj + +30 0 obj +<>>> +endobj + +31 0 obj +<>>> +endobj + +32 0 obj +<>>> +endobj + +33 0 obj +<>>> +endobj + +34 0 obj +<>>> +endobj + +35 0 obj +<>>> +endobj + +36 0 obj +<>>> +endobj + +37 0 obj +<>>> +endobj + +38 0 obj +<>>> +endobj + +39 0 obj +<>>> +endobj + +40 0 obj +<>>> +endobj + +41 0 obj +<>>> +endobj + +42 0 obj +<>>> +endobj + +43 0 obj +<>>> +endobj + +44 0 obj +<>>> +endobj + +45 0 obj +<>>> +endobj + +46 0 obj +<>>> +endobj + +47 0 obj +<>>> +endobj + +48 0 obj +<>>> +endobj + +49 0 obj +<>>> +endobj + +50 0 obj +<>>> +endobj + +51 0 obj +<>>> +endobj + +52 0 obj +<> +endobj + +53 0 obj +<> +endobj + +54 0 obj +<> +stream +x‘ÝN1…_e.Ѹu:ýÙöÖ &*Ö¾.…,fÙ°°ßÞ)JBʦ™É´sæ|íPHÀ´v±¬¯ ‰³—¼ö$ý‰¤Bn1òÜ P Z§:´¦— à;bî䉤¸?§ñ’\‘ÑÎqB–¬7œ(‹N'K[çûPÖp’{ „¦ðE$AæFht<Æ ä1¡î ?ݰý¦ìê¸X×U³¸s¸ 0ú+¹>vMú@þûÿsI󓋌Úð·ÇÚÄvÅ …û†Ù®3ºt¤kðVƒU”D‹®zWðTuI3“ÂÚÜç + aÒ{hÚ9 ÊÇ*òyÒS;½ŒH(ÃÈïŸ NzÏÍ&Ö¯±åºâ-ÍÁæèkBd +endstream +endobj + +55 0 obj +<> +endobj + +56 0 obj +<> +endobj + +57 0 obj +<> +stream +xœ+T0Ð3T0A(œË¥dh` ^ÌÈh{ +endstream +endobj + +58 0 obj +<> +endobj + +59 0 obj +<> +endobj + +60 0 obj +<> +endobj + +61 0 obj +<> +endobj + +62 0 obj +<> +endobj + +63 0 obj +<> +endobj + +64 0 obj +<> +endobj + +65 0 obj +<> +endobj + +66 0 obj +<> +endobj + +67 0 obj +<> +endobj + +68 0 obj +<> +endobj + +69 0 obj +<> +endobj + +70 0 obj +<> +endobj + +71 0 obj +<> +endobj + +72 0 obj +<> +endobj + +73 0 obj +<> +endobj + +74 0 obj +<> +endobj + +75 0 obj +<> +endobj + +76 0 obj +<> +endobj + +77 0 obj +<> +endobj + +78 0 obj +<> +endobj + +79 0 obj +<> +endobj + +80 0 obj +<> +endobj + +81 0 obj +<> +endobj + +82 0 obj +<> +endobj + +83 0 obj +<> +endobj + +84 0 obj +<> +endobj + +85 0 obj +<> +endobj + +86 0 obj +<> +endobj + +87 0 obj +<> +endobj + +88 0 obj +<> +endobj + +89 0 obj +<> +endobj + +90 0 obj +<> +endobj + +91 0 obj +<> +endobj + +92 0 obj +<> +endobj + +93 0 obj +<> +endobj + +94 0 obj +<> +endobj + +95 0 obj +<> +endobj + +96 0 obj +<> +endobj + +97 0 obj +<> +endobj + +98 0 obj +<> +endobj + +99 0 obj +<> +endobj + +100 0 obj +<> +endobj + +101 0 obj +<> +endobj + +102 0 obj +<> +endobj + +103 0 obj +<> +endobj + +104 0 obj +<> +endobj + +105 0 obj +<> +endobj + +106 0 obj +<> +endobj + +107 0 obj +<> +endobj + +108 0 obj +<> +endobj + +109 0 obj +<> +endobj + +110 0 obj +<> +endobj + +111 0 obj +<> +endobj + +112 0 obj +<> +endobj + +113 0 obj +<> +endobj + +114 0 obj +<> +endobj + +115 0 obj +<> +endobj + +116 0 obj +<> +endobj + +117 0 obj +<> +endobj + +118 0 obj +<> +endobj + +119 0 obj +<> +endobj + +120 0 obj +<> +endobj + +121 0 obj +<> +endobj + +122 0 obj +<> +endobj + +123 0 obj +<> +endobj + +124 0 obj +<> +endobj + +125 0 obj +<> +endobj + +126 0 obj +<> +endobj + +127 0 obj +<> +endobj + +128 0 obj +<> +endobj + +129 0 obj +<> +endobj + +130 0 obj +<> +endobj + +131 0 obj +<> +endobj + +132 0 obj +<> +endobj + +133 0 obj +<> +endobj + +134 0 obj +<> +endobj + +135 0 obj +<> +endobj + +136 0 obj +<> +endobj + +137 0 obj +<> +endobj + +138 0 obj +<> +endobj + +139 0 obj +<> +endobj + +140 0 obj +<> +endobj + +141 0 obj +<> +endobj + +142 0 obj +<> +endobj + +143 0 obj +<> +endobj + +144 0 obj +<> +endobj + +145 0 obj +<> +endobj + +146 0 obj +<> +endobj + +147 0 obj +<> +stream +x¥V]s›0ü+÷Øv&D!oMœt2“Çv§}UA¶Õ"H$q} ŽmäÄnÇc›»{w:݇©?휝Ž)!0׸±È¥‘墜ƒGâ0øÄñ"ð]ßu«×¡0û¼ #$dRËÅøÛG“:+×gaˆnà‘^@BVgð´É¼û‹38ŸÖÙSp Lg°ªˆ#Y…‰‚a¦Ù§ •‘ýyú.§ðÐîOhä{ˆS»¸tâӅ€Ñò¶ ¯`¨â*ÃHÜH•×Ñ|9€"É'Š+µ¨×Šž°pu÷„†õýëܔ*©âŽÜÝ^“‘l1î” r0˜ÃÏlWøNiî:‹®4xØp,Í%]i3iþ¿g»Â>‹œÁ¦l[ÊnñÚð4åÖâ][ñ# + g¸é/yªxÒu¡a±Ý öªƒÔ à{gp^É4S˜SÇ¥Ðu¯Ò2ŸÃ÷ÑÏ}´íšVF•’§½]òlA®³B•¦9!ç2O0’î…±ï ‘_éÃH…‡°£@&ÌB%xž7¦”¿*# ÿ5Ž…Öp+ æÀ ?ŒõC•ð"Íî+“Ê܍íÍ»¥š6{ 'òΎ_T¦¨ú6aÅ^¤J‹t`ƒ^¾ò¬Hõ2癌ñ±„8<¯M-7VeYj÷NZçÑû´Ã4¸ÖíIÚ4ÒȚ³JU© ·‰3|[éžÍ¤%¬ÖºŠgiS+xÕð©MŸáZ[Ƽ ÷lðë±Ò,ûðÀ +ãÔô±öçª mÐz‡ÂVcdƒßr´ˆ×Øm·m' 93ë㺠÷lð«TÅáðÉÇÁ +g6øXÕ/6¸¿6Ӈ¿Ž¡ä} +endstream +endobj + +148 0 obj +<> +endobj + +149 0 obj +<> +endobj + +150 0 obj +<> +endobj + +151 0 obj +<> +endobj + +152 0 obj +<> +endobj + +153 0 obj +<> +endobj + +154 0 obj +<> +endobj + +155 0 obj +<> +endobj + +156 0 obj +<> +endobj + +157 0 obj +<> +endobj + +158 0 obj +<> +endobj + +159 0 obj +<> +endobj + +160 0 obj +<> +endobj + +161 0 obj +<> +endobj + +162 0 obj +<> +endobj + +163 0 obj +<> +endobj + +164 0 obj +<> +endobj + +165 0 obj +<> +endobj + +166 0 obj +<> +endobj + +167 0 obj +<> +endobj + +168 0 obj +<> +endobj + +169 0 obj +<> +endobj + +170 0 obj +<> +endobj + +171 0 obj +<> +endobj + +172 0 obj +<> +endobj + +173 0 obj +<> +endobj + +174 0 obj +<> +endobj + +175 0 obj +<> +endobj + +176 0 obj +<> +endobj + +177 0 obj +<> +endobj + +178 0 obj +<> +endobj + +179 0 obj +<> +endobj + +180 0 obj +<> +endobj + +181 0 obj +<> +endobj + +182 0 obj +<> +endobj + +183 0 obj +<> +endobj + +184 0 obj +<> +endobj + +185 0 obj +<> +endobj + +186 0 obj +<> +endobj + +187 0 obj +<> +endobj + +188 0 obj +<> +endobj + +189 0 obj +<> +stream +x¥”Ënƒ0Ee–É"ԏeHmÕ4//ºÈƂIBFBýúbBªFLQÄÂâêÜ;ƒ=øÌáÀìÓ­±zXqÆ`Wv²ÏøÔw¡ØÁx+—XÁ“°´k±…S‡9ŒûLÂÀaî„Ì+sH5Å7xçð±×"Œ+Îڈ‡V]ȁNH4­•ÌûpHÂYªMuÅ®0&PN¡kC¶ìº×ðõ‘ˆ÷N|xÁ?gº4R›¤N ҕÂBš´Qۀð€Oü è$ù^ ÊrÙãcž£NÒºßyjQ-U~@hÆ"¯š˶`w¢Ú2¶…a†fŸ%Dý.Ð#ÀÁS‚}ß 6ÃÖîÝl³;ìïëùçö¯kÅCzPˬ*b„«´@…úÆ ©¦Xl³BIc¿‘)åx‘FÂ:ýÁ’6Ø«‚wWÅÈgÞ7`;>/Rect[335.636 709.839 372.306 721.839]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +191 0 obj +<>/Rect[40.0158 583.839 100.586 595.839]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +192 0 obj +<>/Rect[188.056 203.039 231.396 215.039]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +193 0 obj +<> +stream +x¥XÛvÛ6ý$»KBy'•·¤i:™•¦¹xVæây€HPBL ZV_úë³@J¶ì´YÉʊM‹Îmï}ô™gƒdõ,f;ì(‚ /ÂÞÿòWg| ¯¢4) +¿Èy¯ +¿¾ßۭ²¿Ÿš†‘tæ9S† ԋֲµê*Õm ±»áñ4Wiü§QÆ'QÆqʳøN”‡¿6s÷ÎÌ#éí3—ì±`ÚlíNÒOF&DW±½ýÀ®•ÜÉá1'“Ë8Žx´b˰ Ø½/¥@"ËRÃjÕHÃTG,h÷‚ýÖËÎ=üôü?ìò¬Ô­*ÙZë+$ŽLüüöŸÏñ¹\NŸQ…„5ÎYyÌåEœçìbGÄ0’ °³´SvË䍕œPÌ'?¾Ž'1Oˆä뼯ê“*Ɓ·ð(8îÌxœÄy:ï¼éÍ7îÔl;nk¹þÝÛJ©˜ö{€Ý‰NãC¼²×÷ÈÝ:áòÌh**'»R.|:íxža¼×£¯{ð$Qø;iûîŒê^ÉkÙèþ”E)õ6|]'yhx¢œ8F1§=œóËsg6,@ó|‚iR±Ê‚ŒŒÏdï}­*@aB ÕL4 Sm¯+:Ëê±sZc˜®™ßSºu±Â›qew­ݑÔröa v+1ƒ´HÆÈ‘ŒKÝYՍz4PyÓ Ÿ0—½Ù&3Òòó'Ç[ ¼ªàÌh™h5v»FµªECÒhœ|(KáôÐ㮒^Š^¬U£ìÞQgìÌ8ôÕ«h'ÅO«L/eåÌò0ÉÈíÝÙ¿õèxZneyå$PÞÁȦ~Ê~ÒmOa!hv-…¸')‰ÉË©zœ½ê˜•CëréÌ8?Ž~yÛß§{³º§E:kԇ²0ˆï? <-î<ˆnCø±.vJøåòÁ‘[*uàÅA° –•¢ÓDs’Gc±|c·ÔÙ\¢Á   %cj3'ŠDY‹éŠ&¬AxÏtïš)ÁléÐâ'Á»h x–d«„l½­Fmqä!7¯A>>o¼jvôt¢‰¢‚ŽšF‰Aé6§MgÎ/ªÔ KמjD´"Iw͚¨@$¢>„nÑú)s1†ÚxnPž ìCXwšxå‡t®ÓN…W7£l;ÁFÂÈ>ÂØÙ¿d‰rójöIC´&Ԑ®(óÛ¥ÕK—t43t´®é»¤Gägê9†aâ`ôŒù:ŒŽN“ tøÉºvÚ°Þ;ϵÆa“öíÿwêJ±^@´ltCÄÎ5‚9…K/Ð.<šháÁs3®[eݩʘQz¦¯Ç™­xöè0|ÕÕ5ÊÓ;—n áäêú…ßvuÒ„á¡$ÊóyRx£‘`”ƒBz# W_=ˆ8Dß9>Ì"Ž˜oÿÀ5ˆÄ2L¼ wDó_ 8¡ó3. %V:\(Ýhרõ <N Ä|5ñiÚÿ€ÙlZÁÙß©úŒdfwÛýSO›lEg›Õªðœ'õ 6Š}g¯ÈMß¹h>)!Ñ Ç©Ÿ1ˆ::7^«5„î ×(Î?«1°gƒUµ¼atmwŽ­åçQ¹^·»?xÿ ò°ðþ牳1ßixÅa͝fºÚL™-GêĪžúþnPð‚uZèÛС/Š~«ÊcþŽîRY1}Åðò~ö£dŽŒ^º$ÍÙtúïÕýßßÌ(Š¢˜£y–å¹»"A­Þÿíųk«·ô¬òÿ—­6֔ƒêí=÷AòÑ·!ê¤R€ÓÒui†¹¬h%³“ÓëëòÎcèy7j×ã¨]}÷!, +¤)ðœÎ_,}„Œõgñ×’rçX>݀°Êy\°,Hx˜ÇG*~á[º¼H0é§ÔûÃ9Úwÿ„Éø® +endstream +endobj + +194 0 obj +<> +endobj + +195 0 obj +<>/Rect[256.773 648.239 377.88 660.239]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +196 0 obj +<>/Rect[83.0157 482.239 241.959 494.239]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +197 0 obj +<> +stream +x­XÛnÛFý•y³]X4——% /AÚ¢…Óº±zC4¹²6¡¸ ¹Œª¼ô×;³]hËi#¢œå\Μ93« Búç>«ÕÅ+†p7àŸŠ¤ˆXñÀCqˆ¯¤2žåA\@¥Q$€Ð X|1¬ñ< ³œ=ððê‡ÏÙ¸¦¨¢4És|ˆxċbæ Eð˜‰ÜT+x>§è0ó،$a7 Bt3_þغlšRK՞ÍßÁwsøÕâð¨å˜þòå”¹µ<_Êj1T½¼,Õ´iýÁÕæåxõâû`çó¿Ì²ð (÷ ?~âÿA¥°äÁDYá½Ö¢ö ^¨uÛ¨²ö¹=œá¶P[ëšpíE§©U¿²­al?É.îJ­º^ H€)ÁZ"Üwò£€¡„…jjџC#4ŒTT ©£Å|·‘$è9â̼>½Ú|/õ's`jìþóK+È9¯à‘O]AÎ ­©`´WAS?@*?A yŠá?„‹’8…ùzWÏ)ð9‚ÅÜÕØÕ‹Æ1ÔØW"ÐeÜ}‚I-X’eû΢W+8Œ;HCV¤ñ…¥<ȲøXø¡¢P8)°~`q–XÜ#Ï#úåŽËÀ,ŽÑOa† ÌkÂe}ª—i4Œ–íë‚)øyJ”È÷Àgùë$Úã=Úς0ÁÏÜ4ʨ©•Pãd-¦¶Ó$HxQì·±]اÃx[Ë^TÔäç ¦}×e«IFß d¬@ø7(˜ŽlAµº¦D’˜È +nÝÙШBEÊñ£1ÕÓa½Pý +P$\ Ìã€YmIØ* ȶVëa/OF7ÇÎM€«qÐSk˜yZp—ùhE_jáz¬Uë)™Ã4H2æPF óGkß1²­š' 8[ntÍb†š…º´_õ®W•¨G,<²fU¾ Ùà3æ í¸½BÚx †`8MÏËÓ_ZŸ¾MqY¢–êµՑëáÙÙÉ¡0H¼(¡ðÿ¡¸è{h¼Bì+ŠËÕsâÙÄ£0³¹?n^ +Ô¤kø]cÙÀµk©¦°áä-bæk1ù>Ïó"t…kä픈QB¡ºú[İP¶e‡¬¤òû5 ¾ïð˜C¦ +Q^`ÛÆ¾ +,`IšQÊ%Šx¥Z]ÊVP§AaûÇ<óíua[äÂuá…/ÒÅZ¶q4M>÷ӐÝzYjX«þ=ºY ³s ÞŸxc,8¶îQRø”€¡Ol͒0ÂNÈ\Ο þâ•hD9ˆ©ÇØFêuˆs 1 à·®¦2N§D¤¹WüAè± ºÍ4Tp4äÔ E£ ы¶v‡Øª ”Ã4¡Yæ‘OñéË~sƒ¯ ß¾> >“ØÉÛijE²¯š4éS/š‡=ÂpèÐ(ÛµˆiÒ ]+ô`+»£µ2½HöÀž>y%ùú)™ÇvÈ'y⇠ƒ,ÉL“»E~1B¿Ähz‰_1&£Ùñ°KâçÑA9˜þ-a¹+ÿ´ö,Íqo‹ï‘Ùe3S.›ÙÊe3-¶ë*ªeÊCfylvQÿ*øW½ ¶(nòü1•JÑAZ¨”5u½ê ђ6Äè`"ø¢ì†Ç—®°I„Ð?ºÂ9ñÔ+l3G»ÂÆÏàù(›ë~Mšð„7‘„a"îʸ@˜+dTI‹IY¿£–¢òmuˆî|¶#ã»à.0ƒš^m¦pܔÄÁP¿F I0š›í{â$‰µ=}k6䂸;lÚ +/6zi7™ÝáA÷c¥ÑÑÙÛùO@s óo<¨[3°*…^¬®I§í;7µ ¹Q#3*K‰e?c{u¢7kS9å+Ç»»•±Ó÷2܋·ˆ8íúB¢Å,ãíȉ§&Z…¾ú?+TLmhïüíêÏ' WŒ—Mn¥Œô”–ÚS·{Ö{Ðbe÷¯Þ³ÚƒjzØ^s)_g™¡—½v/y6­TœEAáw̛ÙJ}sƒušî·ÛÕvº?iÆýâ»@q|hú2JÅM_”ä{;CVxâµ)2w„¿që©In9Ša< ™1Èç!³®¬Þ—wb˜Ëùž±mÙïê¯9j¾+Cå“^\^n/|ê^m‚{c$pÉ<Þ㉅~å57ù )¶€ûÇ8yùÜ.xÎÝghZQh®†á\¯Ê‰_~¹¹Å­¦ëš ‘”`òS‚µl¤ÞyÍORú?¢"Û¶òØý·)ØÆ> O†,Ïv×ÿ“Ë6KBôdwŸ'?Ä¥¼UxsÄ[%jM K5¢;#ö‡¹Â*¾9ÆƁ¿²ÊU§zZÆïß-ýb-ƒåÂàͬ—Ý%Í_T¿ÅØVvNn£g䯏Hû˜ŒL5ÌpÜ¥tO¿<ÎN¬üú/Šë¬š +endstream +endobj + +198 0 obj +<> +endobj + +199 0 obj +<> +endobj + +200 0 obj +<> +endobj + +201 0 obj +<> +endobj + +202 0 obj +<> +endobj + +203 0 obj +<> +endobj + +204 0 obj +<> +endobj + +205 0 obj +<> +endobj + +206 0 obj +<> +endobj + +207 0 obj +<> +endobj + +208 0 obj +<> +endobj + +209 0 obj +<>/Rect[177.272 257.039 424.789 269.039]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +210 0 obj +<> +endobj + +211 0 obj +<> +stream +x­ZÛr¹ý”_,¥¤Ù˜Kªò ßR›ÄŽ×æ&yÐːűI=k™¯ßÓÀ€sáE¤Ärí’6ºƒîÆé†~0ßã̧ÍçdùËîûì¡ÄP"Á“=_Šø˜¢B…Qì SB OJ†Vh6û Ø#fľÅ|ϗ/JÇWZ•P2ŽñE„"L¾¡KZÁÆÍÊÝÇdÉތhõœñfÌîˆ3é{¾1qχ™ÑòjTWy‘¥‹ëÑ7ö~Ä~³ÕÐÈ>­Š{q£už•¬jT³Çl±`å<d›¼fÕ\³ºÔ,Ÿ±õçwX¶bŸ7Õ<_±²Òk6ޘOVÄc6ú €ŸH6z¼z£')M¶3Ëz½Î‹ªd«¼bùj±a½aãºbé¢ÌÙÿ>½aÿ^ë¾°t5eï?ÿþ†Íòb™Vå ƒÄ4×%¬uÞõkÆôÉÔ'ýSXìB—%{ÔvÆH•.¾³tœÃ ­b–A„´š­•éw³·q¡fՆ”Z\½$vÑ9yòƒZÈÂ81'K@*üÆs^p~ÐÌIÎ#‹ß]hYÙêÍÉ_n<Í×)ö;N'Øâjã$Ӓý>úp3½šäS=e·Àœé?&z]eùªôØçk®èàÇÙjj՗æ[LÓ)͝ի ÍJ膰­Ë:]Ù©&íÎh•³z•шÇîV°–.× Íȵôõë_¾_5û¶ÈÊôê]>©—zUy£üíýÕý5Ã*ºX%¾•[jxÝÔk]ÿ¤8Uä÷bTÄ.ΏK’Œ‰Å§ ¤D?1´N‹*3\ô()Y™b¬šß_·ÉÕ¯‘PSb¬ó²Ìˆñ —O@„«¢žB—Ä٬ȗl©—yz›V)²º>ëL×¥¡×feûª‰×¾Ð/pËà°àþöξ„/È8ò’¸§–J‰©®@úJƒÎ­”‰p q>²©Ûî.àï2ƒ C,DvœÑY¸ÄQ‚‰$ŠXÀñb««’•T¢®M+¸)ŠM8"—®¬A1t·Ì©~™ë¥%ÇÀe®-hÔ±ð#TqÚ,õY±15Y×à¹5Ž }ˆ3 ہù—&l_WüšÛs–íjŽ…çúš÷8ÖE6Á‘ƒ0©õ¦Ö¹¶v¹ùgˆ\³o›Š[˜yع8Š?Üó¥3?Hxþ–î˜ +¢à€è‰¦”:ٔsžçšâÉÁùDeƒ¸û%Ÿˆª  .bËo“\‡QŸÐYxfŸn˜‡|Ä$ÙqþGãìvçü¢ÒGv%d„J6Ø®9½÷Dtñ5%Þ]ó;]NŠÌ$£'K“Ðy¤´Èúñyd{§âmŠpBq?=J›oóڔu=Žæs/ä où´Íµ&ËҗÍAµ>Åqìï÷WÙªÒºhXÄIHDaƒ„¸ˆª!t1)9ÄÇFÆ\;óÞ xM’ô;o‡˜f¶õWlÎÁBÅ‹$¼Ð;„¢¹D!ñ!+ÀSÝm‹I t{ÝB¡ÏÒääJÿmà#;•bg§MWôØNG¦‰‡½çÁœCîç<õà àç¹hR,¿¨À†H,òtúÑýomŗ!@”#ŸZ!R4Ü~Ðtš›Bi»yá)¼ qöü÷­Ù”i°ôo %ÊWÜAíÀ‚š,ƒË{+äzBÍµÝ +5=¡ænn…šžPs+·BÍ ”Útɸ«w¹‘bW•k··G¨UåìñäˆÐP7I &î5wñÁž?½íá}:Ǔ¸ñ]—æÆ"Þ¾ÜÙ#%röÎfû'ú„ÍJÃĶñü òP«¿( ݲ¶OŒ{‘`"âm{! ‰û IÛh¢ÚÛu +Úûd_oô»Þ ûЖ©Í%–¨ÉҕyÆ0¥›}4u—3å +/û8ª°©ËzaßYèác™®Ð$@¦½ï8GϾ‰©kþnš­á/z†×¶ÕOeÙصžf¦ä¤ìSþSê"|?|I½„ n}h1€µ¦çÑ¢WØNÐm¡¾ˆA2@EA‹¤/¢žÙc:¥Èãñw2O×ô\?Ü +M?pÞaÓï@p´@ý¸«µ·Ñã˜)ñç¢e´ ®0hµYoß3³™}¨&Ô3¼bÓKÚ$¥ž?9á2ÝPl™•Ô²MÀvÄ{]@ö8Ï&s€]ҋ«Þ°¦51¼Ÿ8GS²¡³ù.·iÂÉ;\` +î:ýÏ+0yܟø«>®‰žhJœl*1’êÙ¦Â#óO)0£ê؏ë1¯ måäò&—Ê ¤";.Pþ©7§V”‘Â*‚v‘‘¤†ïå+J\ÛQvùŸtQë3^‘AŽwÍFåºÈ§õDÃà@*Iya¸]#„Ɲ•7où¬zD˜7ܱµvë&ßJ3Û¦ÇãèïHŒXs™¼ÆÈ-^ _?¡»í÷Õÿ¾m Ú&ý Îô ݶ;yTï]+rXÏ2Ÿ‚v裊H€òå"E‰… è¼OÒ^çà Îì‚rËòÜ@—/ºÈÛ +¹®KZ[!7ÐRC!µGH …Ä®P2\xÒY¸¡`ˆµvwnDˆhG•k··G¨U5œÖáÌ;hveN¾9éò"1(•Àšo¨@ыJà^=~û¨ta +endstream +endobj + +212 0 obj +<> +endobj + +213 0 obj +<> +endobj + +214 0 obj +<> +endobj + +215 0 obj +<> +endobj + +216 0 obj +<> +endobj + +217 0 obj +<> +endobj + +218 0 obj +<> +endobj + +219 0 obj +<> +endobj + +220 0 obj +<> +endobj + +221 0 obj +<> +endobj + +222 0 obj +<> +endobj + +223 0 obj +<> +endobj + +224 0 obj +<> +endobj + +225 0 obj +<> +endobj + +226 0 obj +<> +endobj + +227 0 obj +<> +stream +xµZmsܶþ+˜ñË‰Æ ét’8MÝÆ©b]'ÉXù@ÝQ-y&yQ”_Ÿ@ð'žŽ±Æ6,v±À.öÁ.¾ ì„õOõÿzûö#Á݈˜Ÿüùz¹@°Çâ„SÏ÷ºGëò Û«š}»B?"êqôûñøû¹ÚǏß5†(í–oç°éMC‚®ÖÞñ"Ú'¢C"gÌ5‘kè‰>‘hˆ¨P@Åj[ÝÖ-c3o¨ú +k*ǞÍXƒ‘kÐDØS¾¢D|€Ï0ÇûÔ¸µîW¹ƒ-°EXo‘'1$ù€}{†—ž +¡Ü—>¨€¥áðÁ–þ!ïå +¿ã½Øz¯Àóޟ²ü=Æå=úß¾Lâ4*j{îthlµÏŸKéQËÿÛ´Œríå}„6Ùz¿Ò²@™†Ê<Šœ Jâøý>.þÞÌÀSŒqå£Öj™ƒÍ7«ÅE íÁæûzµˆlÎP!&¢½Ú\)•õ’ƒãÁ:ü.¬4ø¡c.E··Ó©d·Sólz Ñc'†J;>íôJÙé¥:ˆ5ÝÏåƒÍíL•'ífeÉû2Ú¢ÖúƒTÊèÈÇ1’¤HúÇl+²StÀN[ÖÒ³†½91Þ ³†Ýí³sNZ“ç˜(ÎF>¦¥1ì÷¥ö¤½ê¸àëÝÆyQ֞ÃÎ/áú¾ò|+p¾lCkoM+,P˜¢®œçõŠ#܇a¼-®:¿Ð JèUS¢£Dfâ®×>’fÄ8QÄxL˜&Æ/8 œžWÙ6:z_†œ%ÀBª‚6çá¾,¢aú ¥mIÛ¨¼Ï6ÚÂ6(,Ë<¾Ù—­x6ƾ¢ Œ‰8ƒûBvÇËI îs4TJ°o*(ãC]õ¥îíV_Ñé¥D,S}¡„¸ôe™­/½9sJé%Îú¯(½¨á¬O*½`ßCZìVÙ7:yƒìŸÅ«/è |…>}2Y†sdîõç*üzŽ<σ³}úõÈ"я0N(2LŒ_ºÈ@5,6_‚òKTšÁ «J3"`i6jõ`¬xÐy„"?”›n"´‹òÛ,ßF+>\ Rh´¡1óø´‚§”¢(”7¸~þ1Ê=„}åôYÐÆ„CuU¯:ñÃ$.Ÿ<ôÕ6ƒ0`ƒ&$VÒ»â=e{½.&8Ï8$©/Ì;}°+€K É£T£%ÖJ‚°â×.- ~q©|:€y6;sÚÙUHžÐY¶ÍÏQž•öq¸[q…æaB\zè»"è1‡,´À:Æ[½ƒ‰-Ô¸>‹StùÃwH›QX^¿9GAÌCû&ç¢CÀiP/£žƒÍ\çÙj‰• ¢¯ï'üfÓÛQ™g°]¥-˞ò›»„­?¢íîó`Ƕœº™Î á'=‘˜߄pG@ýv< z½£ï'\g/WQzt(ÑÛÞêô»C¹<0TâàéNÎÅ^^RÃÁ]÷¸X&ŒC9§‰ìþ’wU´n‰9)øÜg—dáF?×gi ÇNö例ÙåQaл͸Šú „Deêá\ ü·Î©ëXQèLcù´‹×a¢o …–»À+Œ€{XtDž÷ BT‡¥=»ÏÂæ¼Ëãú!oà lÑ1ðhbü ðˆŽ¾Åk}Ÿ;}Økƒ€'@¸Ù…/{•Ñ‘EUh[։áЉ™½ÄÊ£„H½/³<M0€ƒ˜3ôý™ÿæµò㟥€Y´ +endstream +endobj + +228 0 obj +<> +endobj + +229 0 obj +<> +endobj + +230 0 obj +<> +endobj + +231 0 obj +<> +stream +xÕ]sÛ¸ñ¯`⹉ݱU|’`güÐܵi¦™K.q'u熖h›™Ò‘tdå×wA$@‚Œm™;å!²VûÅ.v»Ôï/Âê_óÿòî¯Æè¦¨?Fù ú‘ú}óßò½ºP_c꓋kT£Äñ 0 ˜Eèâîø_Iž ´@q†’‡øn»NPy—h•Ûu¼ÀzÖiöY}e°$ÍQ¹ß&ÅßN.~Cÿ¸@¿€lc"âŠÏBbJ¢Äâ˜/7Á¸ Âù‚÷" b'èú/ˆ¡ A€IÐáⅈ¢GèÃëoùˆ8¯¿Šæ˜ÃZ©fт9¯ÀTiÏft›â +¬ 4 640a•Z„#9J°Âí€ÂB%BZP[¦Vb,§Dµ¶xèËZ[dNj ö³3KÕÛP[îˆÚPK!A£­€Kª ¦ Qשi ´7‰PiAmT6‰ØP[&"é.ìë8[JlT.-h€'$& ]8¶Gù]Ä ÇA÷¼˜j!IðÙPFJÿ àRÊ-ܞÜ )³¥ìñs5”ËqœV'N)hH'PC›®½O O 2l£é4hÊ' íé4ëi î=v&| +•O¢†6´'SÄ'p)‘ãl[#i€öa×î€ µ=3}‚Ak§náO°hÞC~”EkleÑÚ,]8°‚ )hŒ£¶6Û{M§PÙjkÑØî¨ un£r‚°ÎÜg¿†.?r£j?r¢j?Ò@Ú3I$˜ãÍx>Æ0ïçcÊÔ©‹\u>vtæÿ¥5BÑÅÛã#ô:)”ns­2:´¼Ïó$+Ñ6¾IN^Î#ÃË&WüFÒJ‡I«V¥ZIë É'‘’2êxó.p9ïx%(Kà¯rƒ–·Éò3Jâå-Ú,k‡YV΢€E’ *›ÒìzƒvàQPOÝ$åüÎÔªrŸÅˆÞƒ ÝçÝmºN0pH­³ªH—ƒ|7è`Ž*BŸRýGŸÜ}žVg —3åP6¼’õA™@Dg0¨P$>òŽb}Xr9ßY)"Üéª(_ßóˆ°Ë{á5Ãý© œÎ=èÝ|¾<Ìàâ#3ÈeɃ顏(‰o€ 4¯êf‡n€êƒQUŠr¸à]kJHôß|xý-üU°|/(dUø`´)§)¢êæt8C”´^ä‡$[%9\¡Úºûö…ðz§DYÔß[×æÏžy4ZȚâÅmZ´Ó.Ë<‰Ë¤€IïTLßܗª/Wþe–›¬„æÈ£Ç]†bӘT¡Ôh=fÔeŒ@7ê"pÄ'Æ/xV`xïK1‰2>ìx…=hàj¹QuŸÍ‰ªûlØkárêuá*¸L,ˆ×Úb܍ÍÂIl*øø,‹†:û™ØWG³&*¡ÔöÔ!lèˆ:›\&Sê`rÒ¶XØØ–sœAC—®4°§ŽfljªMG™s`ÁÚèj„k8…*ihµ×9GÕón®º©ïF åÄZ±Õdo½nÍ·¸–Ä,˜`Ûnl \Km¨s*K{cj);Qõ†›k»±.Tʰd6n»Á¦ U° ClÊP›0Ô8b¨NTm¨NIˆÇQ[Ut;UÁ Ÿ«ÐPçð¢ÚBéΉÉ)T}ú9Q[«p +Ü*ʉ*lÔÞXžàÚj±åêv:}0’pòXÅΠ빏.Yðï7H¢fGš,)O–e¬èØù +­%¬÷ +(ò9÷Г6ÈÍ2A¢k ƒÈ wµ¹ÏVsNð€}/«¯«…Êð‹»x½† ´M’µš(‰«Z!N³4»©¾ò'p +jÕ¥³xEkÆ£ƒ¼"«´ìß+3îø,<\YäòÙ½‚ˆïìê’»m¹‡böÕß·¸‹·zʰóˆEúõë,Œ´ê@hÿ³†rÀæÜÃä•An–†”82ø)pŸÜûôìåòxé›6 /é%í“ûq³ÞäÅ6^&ó¬@7A<­@7AÌ|ü\÷òäUÁ >›Î(kÛöà s Þ²ÀËIÛúVGn¹NâüSZÞ^÷.(%#õˆþãǚšÆ AŸ +á¡ n¼Ç#ô&KË4^éŒv *ÈdÀ˜Ñ‹ÝmZ&/ªGÿ^dTæq®f_÷/<ô%˜hcÔ*ù2ßA+„׃¶#7ïA+„׃¶#÷Sò%UÇø³al³ZÇøýØä +h•Ç;´ªA×`k]Š1À‡mñ¶*˜£njm«c³ða[¹ü>»Tnq:‘5ƒŽžä‡û—>¹7+h›¤å¾²°Ùl«ãwzÊ &ÛCEP5¤ôõD}¢ej†ª±¤h¦]ÕæN³ÍNY¶®Í^Óu+ä)H¯öj´ä*A÷E²š;=~T7’†ÍSyÏìFŽàûîFÒî‰öñ—¤ÛúZÏiÎõSpo$Òîa­OõƒE +Ä=*ÊM^s6˜¾ÿùuÅøÑíCÊ ³4t—OëŽàwÍCý÷E¿†:/ú5;¯t¨£aÒÎ#ž‰Ô}'£ç +¸eÖ½æó[ƒ‹üÖ ·Ë!G;3Ó[_SpuŒ0˜½€^y¹Øf7/<O]ëäQnÚ¶Ï=˜ÜøÞ&܎üTÿ¤†}B¬ éÞÜ@ʓÁyñ­S‚Á4X7#$OM>ÞÆ6鿁Z:4Iˆ.vÇïòꁶMö%ÉKKxU1ºJKuÇs_ÄWë]íû+Z¨ÐoVR4Š" 5„Þºâ_Õ,FQµ“0ªäê¯æÉ6O +H)ŠJˆê¾âçÕ¾l":t®ASQÕa 8g%j jžc½îRÌ¢~äïý¾¼Ýd‰8ÏãýU£!éËcÈIÓ²Ðâk™ÔÓê!“µLE™7Wx¦Ø ×Ð3ÐX‘n²ª .@1ª¨†@ÑéJ­èþþÚ£»MQ6º««î°æ£W£–Ñé´úÙõc¹ÛèQ–bÑÈAôK•ìêež¼4eä!¨“ZÐG'‚6ƒ âhË~œàÂãÃ'ê>ÑÀ^¿±éÚ:Qu×VíÖ  +ßÕÙ#Т¶|[©¨³Eªîçҝ¨:بv/3ÇՓ ­ÄØ5œàFm†ܨú·.¦%nöÞ=ᣡÎÖx‹ÚӐ㨠 h[…þA +7jóƒ؛áà6*qÛŒÆ =%)„à®â2ž­{iðñѽ4È¡’+}Sb°ñqSbÓ‘iøò^Ñ6sôó#Tíõy¡TÄS÷&õEI½> +endobj + +233 0 obj +<> +endobj + +234 0 obj +<>/Rect[238.456 117.039 265.689 129.039]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +235 0 obj +<>/Rect[277.361 117.039 314.591 129.039]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +236 0 obj +<> +endobj + +237 0 obj +<> +stream +x½[moÜ6þ+ÄõCœƒ£ŠEIîÃéµ)Òkšèuqwµ^5»+UÒzíûõ7C‘¥¥”¸+5áõŽæ•‡3$õñ=J|ü¯~¯ö_ ¾Oîkø* ‚0á(^ìûQLIuO¸Ï= +q.¼ !!åÜãœpæ“*#›¿“€œ€CáSѱú^˜$ÂOȇï>'ã#á\= bùã>¡FA4  +›‹>‘Æ6•úÈ;ÂJ©ß#¢9ÕØ +‡`÷©}›ü>1ðj]¬”Š1ìE‚FS¬8„œr_t`iԗô˜FFQ6®ˆ1>A ú&ö}gbÂ&Äk"ÆY?ž` ‚x‚•ó‰øqß`ÚÃHô÷1Âé„Åœõ#܇õ½³­§6a}bó4P±p±RáñýqV拱'ƒ'«ÌVÉ?`2ËÏê×jO¾¹ùúóüæfCڌF æébY ÄÝì¯Öi“’—7¿“ooÈϘüâ˜Ìñá9z¢s=ÿèt<ÛìØ?GÊüqn«1ùpÚSã]`5$³¡¸:ݗ»¬F¡¯„ ¯(#7ë«|?ûˆX…,Œç3 8òvŸÞg³Z§è²1Cq›ªØß=5Y}{e±8”k4eI„݇gi‹Î´ýíÃwßüóoxËÍPæ5ù5¯²U3wìaš3ö°f ŝòu³½&‹X+ٜÖœÅm³ü~ÛüvMl±µ&äqü<%əÍuS]`5?Çû­Lñ·/o_Ê´DLçr[4Åì™Ç$kX±æÈýâL\›yn>͞{5䝪‹r÷‡âÞcÄ¥õ·˜õÛ!Q.$Iu|Ešm&ƒ÷¢&yßí2‚H÷»Y°EMÙ¶Ä™|Eg)x£s««ãáSíõ2‰+T˜˜Ç~h¶†âÞ®³C“7OŒ-†-j•é;ƒ/R´Ÿ×&k½bÜÜ[øŠŠ“y~ÝVõde=9åÍÖF)AÎÌ0 »þqîa ř’K†U×y–¸…a©Îg²¶ï†â4,Õ ~ïør@šqެdÇÉ>k¶Åº†3>ÄVÓö!Â䲊@\‡&ÍÙü¢+8yÎÇ8¹yÒn¯(Ì*@*>¼n±ºFrKõ'¨ÙÕ>m8%”–Eãù%ˆÞ/_\Œã ë0Ðü¥î_Yzæ¸e‰ƒzj©ÃÒ2G‡a‰S°A\àú½z©…ÞÛ+xÛ6ûÝë>ÈÒ]~ØÃ„º&÷Uq,5ÔFϐ@yb2=š´ð,=sÏ· ð,-sϧ€÷ý͏Bxï÷º8¼&›ãngKf1x {\ee‰sWT5IkxæÐÔÀ¶.NL´u™ê9p'LGô¸ìtEi©™£¶Ä-;v¦åØ%ç1P°ûáãOÿ^vAbÎ~1Ý} ê,¬å‡uþ¯éެ¶)žU³`E öX4+ö:qKbE³b¯§°÷“ñì;]¯°å^Ò;4xw +NИ 7§«›‚Üçy*ސ‘H¾ÎR’BËÝ{ð»<Ê»]ðW »*;É!m€–Ôv!k]Ö¯úƒ9ôÅêˆk-´"ïßü‹<²°šƒ”‡^c;óŽpùq,¨3xpX€– +äµ×¶=ò1ˤiYf€ÖGD74GPvîjeeæ«—ìÛ§²*VY]#þ›|ŸµiÒ*´S/®ªì#”ÒhY홲ò‹®¡/.¹ +8Â?÷UÀ <ߺ +ø1K«Õö9ÅÌøM@<o/Vâ5¼8 +‚ïí¦O…7ÿYÝt=A±Z«ì°ÊCeZJL`ÚµäÂû„0 Yµ‡Zí‡~ÕìžÈ p »au¥0%«¬ÂnC]%4(ºBh¤UýÚÍ©«…Ђ3ûf`ð̛…#üÝÅÂÀgLÝ·r:k~÷©³¦:O «óÔÙɚ ä.©Wlœ¬c¯p©(š§ÿÒÎHðiÌ&%|B"ã¬0o&ˆÂ'μ"ã‹fL¯Èµœr2U¥ ÙÐBóÅ»B'*Ù×e¶Ê7ùJvҘ’fX|¬4ʘˆ‹-¾–š9_K܂‹¯¥eŽÅ××ù/¸¨-&KՋ°¢¾¸¤_ŠÙPè5ÙæÍ÷éã쀁ù;Tv `‡âH_œS¡rÕ¨Ôp(5äC©ü¢qÎFí¼"c²3%õ‹*+²-ž.Ôǝܧ²ðăú %°¬¦·-”…(EÑO9t¸Ow— åÀùk õܯ¿ ÅÀît'³X»Ç ë6ìì‹ +k2X¶ù0òŸM ç¥E@C/dxAŸª— ìH|蟳=»p °rI؈ts‚S£?ÞϔeK[e5Ù<c2ˆá+,ìüHoÎc°NWÖ,²íáqK¨Ò=;Е¢âXAýUuÉ]Vlä v.J\# ƽëMŠ8€o©¸}é=óõÆB¿Ws²øY5ëÿ5+wՕ0"ú “Ÿd¿1C­ÊüDÕÁøÊ˜ œF8@­|ˆz™Þå;ØèËd=*‡â]aáN۞å>;@O‚CÂÅ-²PÉbu +Í +ž»ìs(t=ò}qÂöå‹Ô¢|RÍR÷x×ñHŒÐÊ@}¬;$YÃm­×ÞgêX˜aƒIFñ-Wð/ñŸsSp<^42ñ¢P'¡,íl˲¨š¶­3ÇLí Î+&ŸBb1yårЫӇÌѓ&~ûœGÞ¶3Ù¦kˆHµþq…½îkA«lƒW*01CkÓOKÍ[ø +i{QaRËÖКos€ò>ý„•B çû,æQ›ÿÒuŠGÙwOªñ~ÿþ$úëbӜõ׀óO°€¼¸0Ñ3|[4 Pk^v¶”ëMuº$Õ3ƒ iD>ÄòòÅ +ÖDÈrÔÒqõþ ‚Å.Y¢(‡.“ŽÙ “ã£xx’ Å8$ú ‹Ôût·Ëªöäªëp°@Ʌ[öåU~ŸÃ"ã}þ¼v-ˆe¾ Ÿ™pÎk-pbéo(¯ 3Åp¶&¢­Xͨ|TËÛ¥²Ká°ÝTÚ¾NwœüLñOÃêKRŸ¶÷Ǧ(víT(TŠè"Då+&"lcÒv}¤Âj„a㠈í±Xü…ü\¨'âö ÀMë4< ¦>ö2‘R&û:=?•ýG'=¡I5nbì B0< ³Û„0õBnÛùífc¿Õ쐜àÉàçw…K¹oÓõŠ_e÷ÅÊG­æ!ýML»§ÌL Q®‡:QC6|j`ƒSҟ)wÕԂڇXpð]婼»ÁfÎ⦙Ðùüçÿ±w•Å +endstream +endobj + +238 0 obj +<> +endobj + +239 0 obj +<> +endobj + +240 0 obj +<> +endobj + +241 0 obj +<> +endobj + +242 0 obj +<> +endobj + +243 0 obj +<> +endobj + +244 0 obj +<> +stream +xµYkÛ¸ý+Äî‡Ì,f‰z/ši²mS¤ØÙŒ~hŠ‚–h[‰,zõÇùõ{.IɒÉ$ãMŒ‡ûâ½çÒ¿3×ñ˜KíÏlýüçºlÙ0Oÿ­—,p1F,ŽR×ñSz!w‚€y‰ËjÉ?aw'©ø0Þzgö«òý§©òÂǪŠÒøIª¢8}´ªÈ{šªÀ´*~.ú´iéïÌ3ŸílÍþ6£Ó÷id¶`&#<DŽ«ƒŽ ™³õ¯–¢ž‹¥d/˜w}›úaäðtÌTYʬe]Õ52gjþ¿5×ÿ›ý v°Ù[6ûi,ï%y^´…ªX«˜wY덀¬Oµ\°VÌKiMEøgEð¶–5åݦ,2Ñʉ=$&+¥¨Œ'žù¡ãÛ¡LU­¬ZÖ´µ놽¿ªÈÙs¿Ø¶hW,µ|=ˆÊå¢$V˜Ë}뻊œ©eCA±2‡¢ÉŠÂîó\®PՃ¬[–‹V+/ï_½yê^‹vØW•u¯0L\ÚÑÐ'˜™,>C_®²nMÎ@dƒØ 䧍¨r{qR ¹™|¸³¨°ŽdŠÒl“0¦íª*w¬Қ¾-[ѯÙJTK™k ¿ÌØoŒ;!Û"ŐìƒMÍwÿؗqbjsÍÂy¼(Ùý,íفÉ"‹=ûEv`²È¢Î~‘˜,²Ð²_dƋzPõ“EñáýÀdQt`x?0YÞÐ"/‰9Ne€„õ0r*šûU‡‡@«zñ<üú¹-êh‘뤾¦Œ“¸nœhøò8O6,r¼­ÈãO€.×@W䦺˜çD¡—„l¶½ºC•£+ÕÊ“¥×³Ïßq7´bî §CˆšåW¯m9”Ùï¯Põ´x¯3uͺ&²LÕyQ-©jÿÝݽþû³f(@AÈtÊP”%›KZ¸èêv%k܃,ÕFæ7¬QPÀn½˜ŒˆðC:©ØR;šrj ›!£í(ˆ‘m5X‹H»‡[œ3j| +o;ÕÕlS«e-ÖûîWwU… vö®œ!±;èÚ‚&QìQxrx£Đêê‚×åíQù"[èÌ9›ï´·<ÁóQzÀtFQ8'ð{Òc Iå{¹àú‹Z­mÔì:bz£qbÞ-`3àh•¾ÜÜœÐۓЂà ÝîIÒ[C; KJ(~³˜N¯ ~.T搛Ör Ðӄž?.3¹¬Ëæ.0‘N´¥2öŒ° ӘH|†é‹)³¾ÔÒ00mQ+@N6¥ÈÐ֟Q¸¼¡ÃAÚÕ+ÑQñýi@ ¼J©-Îd¬ü$֛RþüµÖ¸&6æI =Ćî¦ýë]E®‰Q˜¦¸tšÇŒ/î¿ÿjÈøqîC€ $´àfQ´ŸþPp ø¿*s$ßþϏ¥éeÇ.àúê/ŽãX¦ƒKª4·3Œý•MöKgéлP½Ów»ëgã[’î܍[sXv³¤‡´ÏKFà¾yžýó£Iz@Цkuæc¯jñ±Ù=j}¯ÝTBwÎîï/×Ô''œDG–Þå‹wx–õåR"‰.gr_##™^Œ”›¹\DâàÒE2’øçÉHIvßÒëë›_Ÿ’~<ÑéwÚöï.?8”úôB‰Ý#KGó›[Wº/ +¾äð‰éÌðíI jEúãoUôuÍÛ«¸ç¿ýÌQÃà +endstream +endobj + +245 0 obj +<>/Rect[381.318 436.239 401.328 448.239]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +246 0 obj +<> +stream +xÅZKsã¸þ+Èø`9esð&¸U¹¤6©J*³³®Ê!΁–(›YIԒ”ç×§I$BÏL4«¦Õ_7ý&E8"7ÿôßõþã‚1zªà¿ÆDÂP"…q¬*ŸÇ<"B".¤ŒX‚á<â1le†¶¿E ½‹”ÙóâH$‰Ä úò§¯‚üŒ8?ÿ"–ªýå@!ԈèÈ€0å%s>ÉÍè™Ûb–ʦ29¤Ü0÷T›H°Ed–XÂmjCñˆ¥ØÇkÄjª#VZD8®O¬—•Û¬ŽÔXN°6.Â]ö_ÀAˆRÊ\ð*JèeAe°š²Œ¦ú-ãgår‚UP‹è5¶Yí»¤ÊÖ)Á¾óÈDz”Jª×ûü¬„ÙT¿ƒÁߖʩÏR†j‹¥Ê"r峅ŸUQ‹hklîÝËj¬hˆ„pïyb_+iS-ä„j¢Ä¾7fÔTaß¼öoCµY›Õ¶1eò2kwZMŒý—ÇÕDR2T?°&RJ½¶`ÒcÆÎ-˜œp MtBDŸUج¶GA˜`õçÎ•Ì90}Gâa„_–ÔAR¯ïr›êg%>ç¤ [Téõâ+"TÚTâé¨{ïcï]k­ðD1Dñ³^(\:<ï)X^ԝÞ/æ¾t/”ò%@׆êk/‘b<Ái„ÆrJh,'„úˆP/Qٜàä>7Òç×&ª Uú.Ú7|‚õ‚ÄØáy0ü͂L¼úU¿$H‡›WPgfM~+ :eeA'¬ìgñ«.ƆhŸ²ó1•= 5ñ&8¥÷<Úº_.X…M%Þú$¨7j¥èD ×±*¯-¨7¦¥Et’0“¬ÆŠšè´-Ýyˆœè= Õß{ª?]9Ñ{xY;•‰i¥o^xrœóNŒS†j!+l™·æxYÃù¥¿ð³r×mí§Xã)•”£õšIú\ª³„¦ÚÇ1C¡z»?.}“J'Ö?*ÃXŽI"˜ç ¤¿_¼] è?ë=úýýÇ/}›ÿ¹ß¢óڀ f~og´&r¿_]ݽïssÿ/ÐÝÿeu…®ëb—•顾FŸü#¨˜n²òæúýX×è÷è§v£aŽ3qŠÅ¥s0nαɶŠ÷+€Ì”tˆ†p’¢Y˜¨„OZ}¬­T#pm²ïW¸i9±0VÛCºÏn~hpïÀùîš Û¬|òm¾ËGŠ IQF=_¾K J¿›s(‚]8û8K(ì¦@W*¸Rß.]ðâ˜æ¨ËGêê;¸Z:¦J´+6B–ˆþË7ÉRʕõ¡|ü0C{˜}FÚß´žC¤²\g“Öér®CpP×éáÎ>\o.]AѽuášÜðwœÏà$AŠAÓí9¢¯ÐñT£ü€öÙ¾(ßfœËdh™¨î>L +}%‰Ɋ¤ÖÜzWTك‰'³>ž¶Û¬  :_5íBˆ¡Ô…C?×e~xúô·‡Uã–Ë-åmùWh]^²²Fuªü~Âãb>ô8«~|W³aÊ÷¸žJŽÏ%›Ó¡c…Ñ>ѝ”´O0õ©œUøè¹ 0ÑçÍöKÛԀ3#¢s§Å\©…vYNªÃd-ò¡8û÷:;Ö!òTùšú,vbX@Êâq—탪,ƒt>¿ýõÔ ëô€ž³t‡ò:Ä1«¬4ŸM±^.÷â‚äàöÛ¼þOpµÅHμE¹p?ëÓ>;ԫЍ®iKÂ>¤Çã._§u^>7Û9]/xÐìÓ7ô +{©¦¡Oiù˜>…Ÿ6õØ?í HÁúõüèäÝÝì ]T6ëê± wٍÑ.åTØ×íÒz9ƒÅj ƒõ¨$„Áz8gÜ_*Z!C‚Yï²ômLbT؁"¥( íõ 28«?c܅ ¸ÔÛÂøÜý£©»@½ˆGj‡©l„~k +“GÊÏܚJ9²ù͂ñ*ےÆ]ŒOBðQ€.³å5Ú  ý—Ùò¯ïÍêîbéÂù¶¼‹y‘àËmx§Þdޕt,ó0 ·ñÌÎÞò*îö.‹mzMe`a…èwqÓ»˜Ç1¼ì–WÑ(Ü®tlW:Tøÿ°+ˆ¿B‡âµÝ—B /_áñ½^šÞ5¯$hŸÿ–×f^%W]6û€ÐÑeV¯1s¢+k­_€xXUoÕR¹a . +piùôò#RÐb’æ& Â5Ö¿œÑKe²Á>ÿ EU¾Ïwi 9 :íêö1Ç#LÕëç<{É6·íc'º¾jÆñ ™"#jpÂX/›e3@¤ê’Í"ô©Fp‚ªNÇcQÖ:Wª +m‹Hh#µ]ˆF¾c²QÒ<ÃlÇeÎÎpÕ3R% ׆.N5FèÏÅ+-Ñ[qBûSÕtª/Å/ð ©Fi»Š*;¦%¬Œß —> +endobj + +248 0 obj +<> +endobj + +249 0 obj +<> +endobj + +250 0 obj +<> +endobj + +251 0 obj +<> +endobj + +252 0 obj +<> +endobj + +253 0 obj +<> +endobj + +254 0 obj +<> +endobj + +255 0 obj +<> +endobj + +256 0 obj +<> +endobj + +257 0 obj +<> +endobj + +258 0 obj +<> +endobj + +259 0 obj +<> +endobj + +260 0 obj +<> +endobj + +261 0 obj +<> +endobj + +262 0 obj +<> +endobj + +263 0 obj +<> +endobj + +264 0 obj +<> +endobj + +265 0 obj +<> +endobj + +266 0 obj +<> +stream +xµX[Wã6þ+z+ô¯î¶ÙÒöl{ÊBy޾dm³$ýõٖodi¶VON°˜Œf¾‘ææùŠp@¶Ÿî™dn Æh[)æ1%ñ‘E¹E Ã!Q(Ã(`1TЀsTj´ù1ô +;"ŒÃˆYÜþño2î,**xÁ‚J*c &qÄ-‚¯ˆ4ÈÝ#ÉÐǕEO‘hµA­EqàFMH jVÙÙ¯©ª*]¯žÐo+ô¥=‚w…2K9&T j…®5JMU£bƒnŸ_n®~GI«è¤’DW•yH5úfªwWêÙ#ñá–ö +(§«õÙÆÔíï&ÛÍŠ—™lW”5ªjUëLç5zUÕ\&¡aÑn_Ëoe÷æ“ææsw#´»ÒÞ‘¸»U¸¿0б<²ïÇñtäö¿Q%còÖÓTɐ¬JˆeªXx²*/S…É©ªDÄ©Rœ¬Š‡ËTѓÝBenÁã“Ý‚‡Ë܂‹“Ý‚³ï»E,1`/"Éhtb\rÙq0Üp†%$&d“¹Óð“é$ï~LBjõ¸¼×$Ó!•¾k‰"Äqˆ@ï0dC}òq#©G ț’_êÔðáòàњ¿õ²¥¶ 2/Û3”5’ÑK¥×MÄCÎ+Xd>Ã]ˆ>hþj̇=o„"¶ïn÷g*xT€Šâ9Så³×ðl¨îjëÅ6Šóu—ÞÑ„û„M¤·¦¤NFqÝ&% ç?)õ^OŸÇ}HÞÆK‰¼Õ0N¨àô+hwV>2y;fHUîó:x„¥^ªÅHâ´ZlÒBÕÿ[µàr¨ºÐËy +‰™Ô¦Ë²AÑtêž#ƒs>Öu÷¨µ—ûà|Ö*ø¦å­êôSmæ÷šDC?‚þ„ïSæ|ó·× R äÀÂ¬o>:˜:Â˜É z&G˜0u“¡©#L˜º™ÐÀÔ&LlÜ&LdÜ&Lx¿î&:=“#L˜ä ¸#L˜ø ¸#L˜è ¸#L˜È ¸#Œ™ÜÐ¥gr„ S8î&1î–‰D´"¸iJÖSBþVŸ£ u„iåô±ø¦¹lË5ÛxTݼ<·á8 „ÍfÖaÄÁZ]ŸÅç?µ±÷åùh" +endstream +endobj + +267 0 obj +<> +endobj + +268 0 obj +<> +endobj + +269 0 obj +<> +endobj + +270 0 obj +<> +endobj + +271 0 obj +<> +endobj + +272 0 obj +<> +endobj + +273 0 obj +<> +stream +x­TÉnÛ0ý•¹Å.j†»ÈÞ£‚¶@"ë €¡È²« –IF›¿ïP¢¼0îb¤Ð„†oÖ÷æ(a@ÝçÏlu3JaYã/+-göÄ¥Z‚ Q¢ˆr",(®8‘87På°ø~ ÂPvâ_ÿÍÇÔeŕ4/\sm^„¦Fº ^€µ™÷G¶‚«ä2æÊ“, «ˆ¤„va¡&Y &ëçuUoÒ,&Oð9»® ð˨pNùU’ðÎoœoª¼Î˦†æ{™ mX/ …ãhDQf•8+(3‚Øè(èmñs•nÞQgìÈ'Awî)óOG,Â7xHæƒÉsZ×0¾½i_ mü+Ž}ÿ:ÈZëðgAUoQDiÊ1œƒuļmäàóÁlV”E3›A‚’úaÐø® A2ÉtÖ:^@î!¶q±î֚&u×ô_àE•gM€ù0ô±ï&]áÉ»~”uSm³f]ùj÷ömÉNP–X!”•p +'M© Á¼´0­¬P1À8íeiAÑÞ*ù¡U*ePb¿±žO-í5¢Q,žZc(·«Ç¼‚bŽB)¯E¹Ü«¥­Àt»Ù¬«&Ÿü­!Å4‚q£‰ýd:‹¯¯‚V LOšB¯ãñ=`é=§å<„3KäžÐÓÙäÛý—í­o´$ŒÂé³^MÉë&ÿ¦mˆÖÖzn•Í[^°v—x²o7ÚóküöŽvõâ ²Çò&Æ ð޽cqF©CŸëÇ'ç´ê÷hÏ œ|êV¨»«t™‡MešDÖê3zZ7iÙo \€®›\ºå*v+.¯92’(¡ÜjctxÑU{÷ …¹c +endstream +endobj + +274 0 obj +<> +endobj + +275 0 obj +<> +endobj + +276 0 obj +<> +endobj + +277 0 obj +<> +endobj + +278 0 obj +<> +endobj + +279 0 obj +<> +stream +xÅVÛNÛ@ý•Q_šTÉvï^óF¡ª*ñÀŏHÈØb”ØÁ»åò÷±D,(¢PUQdÇã9sö̙ÝÜg8}†k±þz*8‡«€RJ‘>sÓ^â˜b,$ —L¥`¤‘LkÒAëañÜa†ãÄeU_áÛX¿Èk¸D”¦öpWÅ%V b{(u]úߛAô÷ðÛæE¬š2Ÿ·Át<6°|é‘ùºª} —ÓÏ(–&vÁ¯|ñqÑÔ!¶¿zÀµË¦dÓÏ$ªØŠêûä`•‡ûÇ?)¨¬‚RS°è‚‘Ül#†Ë%•,Ÿtt.†çsJ-'U]ŋ ÀàÓªF÷™ç“. {¬˜Bgº>ŠËXŒå8ƒQbWWØ>Ú\^ãúG©jžO‡À6u.ú'Å«¤øÁV³¦¥ƒÇ~ù–:VÝû¬ó 4ô³¬Âf•?Àª +qè H™Gd¾Ub‡Ò££ù¶è~ ýèôb@ï’yÈ/Ív‡….R£Þ43"ULK VØaf&ÇÕ=®ås(¥e‰ÝÁœ‘4ïg+µbiºƒ|Ø«}„b¿²ˀV΀IÝ8ÃaÚõE9fmšö~©ÐyÝù{Ô[Ñmis™üG«Ó®–ÞÇʾžMÑ(Y»GÅi¦ÍK£’÷›!¾>cæÏ ÅXɦL[<,ŒTÇÂgTÞa~!æän?Àl¤åÜà±Ê­$íhQå·Qëþ­zÚÚ§êSƒß%ža\ïÀ¾ žDa¸1#íëI§þmai¢:ÇÒYæ…´²Q†N8!ðôìÖròth' +endstream +endobj + +280 0 obj +<> +endobj + +281 0 obj +<> +endobj + +282 0 obj +<> +endobj + +283 0 obj +<> +endobj + +284 0 obj +<> +endobj + +285 0 obj +<> +endobj + +286 0 obj +<> +endobj + +287 0 obj +<> +endobj + +288 0 obj +<> +endobj + +289 0 obj +<> +stream +x­XKoÛFþ+s³T„î“dniR)4qôPõ@S+‹) +IYq/ýë™Y.õ ¤$Ž ÃЊš™ýæñÍò3„ŒCHþ3+Ÿ_ó0„Û%*<9³hnA†¸Eˆ¢P0™€Z0¥@ˆ Ë_@ÂwÄaÅüÌâú÷ïéøH^ ­â“h\HƊ<ø Üy>|d%ü:{~-t\Ál ý‰8¨…½ÉB43+'¯óv]¤÷oó¶›Î>Áo3øÐÃð Å<äôäœb­˜8Q y )´ÊêªKó*¯naѤ[úÌê²L«E óIg¿tÏ /Ó[Û>Ûel>e0[YÈ«ÎVNQ·­ƒe],^ »äŠô®HΌ ¼ø{ÂÙ4@X')ÙÎÒl…¶‚Òf«´ÊÛº]l2 ë´iɏz‰‚k4=ýgöðfo'‚M¯€s&e‚У^„]E‚K³m¯z‘v)´]³ÉºMCŽB¹)º<èVM¤¸µÝfçÙ®, +ԕ…þ7gÙâ‰VÖÙDÿküÞ8±ÆV Û´.È’jÉ¥>L,‘R' +r‰òQ&L,].Ì-Ì.BŸŽB*ÁB&âNQ:øõûÁ—‚ob탏þ`jrn¥—à"%vû¦wx¥É(¢‘ -½~]¯7EÚÙlónõÍ'›u-,›ºôƒ›{h6•Ë.oS{]‚Ǩ,ꕽGY†‚óÉ|:6jÌÞh]¡âãª`:䉖ÃGÄy€Ïäµ½Ë3û€‚;Ñ©c†U| sז`u¹å +oާé- +±ˆt%ՂŽºÔ `¢|^ÚQA$óê®þ— $‹>LN×U;F—'ŠQW$Ð.à*MÿórSe]^WlwxîþððÊ7Aƒ­Õ5AÞ7Anv¹Éâ(N0uO‡û%?Þï÷'&Ld|¸ˆñÚWÆKhá$xL]'â‰ÁÂÃûôi;åÎ<¶ã}ªÖ+FS§þke«¾%÷‘÷Ê<ærdQì(ó¤…÷ú£K¬ØÞƒÙXY–V@f¸¡¡eÓ"G»ùŠÜ|ÚӋ3^o:X¥wŽ_H4  ¯Y7¶ÛÏCŽÚñÙ'Ë:۔4.óëðê(ˆ‘J Ð×8s5ˆÓýÚ¾x<ƒs,ÍPkÀ­?Éà§}JûòP‘:èSp)±~6#Ÿ§/W(ܳ1æÐhd_֕?µ/ÅüÿVé@œk8âø>mÒÒbf´ãy?6 Ë*!¡ÿG* 6æáxã9Dìáa?67pږȟJEO6Ý ±Déĉ* ×Œ³/]¢°Ê\…<þ RÑx`žô ÜDLÓD2è¼'L§3qÚ7þ§ŠS¢Žüy—vMþåQq¢âHg§Y“V-¶ôÛÙ —®×Å=-Ž{gín¶­ëöŽ˜¡ìôÙ 0º…je"w Œ:O‥¢§O`ã߯JÎ7×xûzt +¯ÏԃøgUÜ{2À‘¯ç´ð¾y—·ùMaÁäĒôâ¡o9ˆ-]ݕÙ]{ðNƵrŒæÈòÆ)osä,ºc4[øԜ‡YpO¿ôv\ÔHkxsGæ¤ ™wÈQ î.YþÊê_‚ •¼:9±mÀixBX”à,Ák4Y÷2 Š ;M3Ãۀ_—í°Ç +endstream +endobj + +290 0 obj +<> +endobj + +291 0 obj +<> +endobj + +292 0 obj +<> +endobj + +293 0 obj +<> +endobj + +294 0 obj +<> +endobj + +295 0 obj +<> +endobj + +296 0 obj +<> +endobj + +297 0 obj +<> +endobj + +298 0 obj +<> +endobj + +299 0 obj +<> +endobj + +300 0 obj +<> +endobj + +301 0 obj +<> +endobj + +302 0 obj +<> +endobj + +303 0 obj +<> +endobj + +304 0 obj +<> +stream +xÍZëoÛÈÿWö[ä"ÚìƒËLJ"uš6‡êsÔOuÐÔÊbNu$uŽ?õ_ïÌ>(ré‡+¸ƒaˆ^ÍîÌìÌüæAÿFå„áû,ª7—œ1rÓÂRe‚g÷<47D2Ø¢b’$LP™%” QD„HI£Éê/D’[ؑ2–¤üž‡Ëi–s$úÆÐ.Й8‘bèek$\>¦Ã?ó¶j½%Fõå_Ö„%§µ›Æx½ï6åV?¦Çû²i;b(dœz +A¡Î,Å¿ì!$8EFT–¦ìtu¬â*µàœªU6‡m^=ª5V£Hƒaô\T w.x*’)"lµ^¶Pl?¦Å‡ Bq®·&ç<Õ’'VEM!Çy½7ü *‹¼jÉv_]ë­ƒ»F8%OŒJNÑ¡‚6e™wù3ÔÁ-Äí±Šª ƒää'øýΩ1¬(+¼QA hÃnæ@ä†D¾é‰üˆÈ5+"·0"rmʁÈ-Œˆ¢@p¿0"à~aDÄBÁÙTpßdôD~aD‚û…‘ +÷ H$2iJbß(TýJMùùµƒ]î!:åùIñQx6Rïewt5î;~Åcïñç¦Ù{ñ!Ì0€ø8³£€³WÃðŠª>' Çsãæåìóçr[vŸ?û¤â¥vÀ콚™/Ä!Û@×ÄXj¿mõflnãkl4|y<†ý`säΝT-sì«Ò ?MÝâÇX†…|RšñPõ0gzõêë/ºè†£ˆ4‡¤'ÐÑÍyŠ;.òäîtÓþ€÷gÔÊ @’eq’Åí!©\Ìdg£›=Xs|³Ð*noBdƒL¯ü ‘ùœ¼%–Ž\Íþ½x?Oq®³ß–E½Ô@€eiæ…ï±ÂÛåݺPŸmaÉ8DA™¸›0OTœ¥ÕèSšu5”öPYQ‚¨joËÍ—ëÞˆ)ß.é+`jV@–®Ü]¯†è¯»MY@²¹CÆÈï#*s;³(Ñú1HoT1ÛáPÌ?M}WOÑÑæI ý¤YŠŽm›¤nßlIw·Ó?ŒÇnT1ž)ù¬éWœ2¥ˆœ¶mϘé…]|$2ígzVlãR#7•”³Ì¹ÄÛÐÓ³ÞŽssêö«>¨bp(Œ*°ÿ‹h¯°àñÍàÿ:¯~”~¿ÂõGø½šá9=øôvãÑC°$9Ö'ü»â’Ñšªåyà4<8èr$ú_xQÔzáTEÜÁ!ÚdÒkd`r‘~kwº(Ww¬ óAOàÁ `À%¸Z˜|QÉÅТ¡ÜÎÜf¤=—ÐðEB0O ΔÁ<3\¯ò;5‡[m Wº¶‚åÙÔõ¯-ٔ¿j—1  ^Í4½¡Á\éûJwËU(ó—ñõº]—Åf÷­…fÉìv'§¢I”ÀÀÄüyñî=‚±mÞõW˜?´0¦C”^¢TØ(ú‹|`š“ª¬ÜÅ¢ô¨Žgù1:ljß”öÕP،ùL—2¢”\ꢮÀBK´L "y öWÓk]Yôï|âã¼ð¨A^Òò¦Üæ›dÌ+ çF4²Û7;œ\Xs¢»xзa.šÆŠ›ôl=dâڊfʃÆI]Ý‚÷ð¶Æ_‘›!ANÚýnCW¸Æa<‡!U"cŸŸgh¾gh}PŒ1¬4³£Ê‘rÒ4r6¨0Â[ðáNß`̌£˜ß@ñá¬kš`W M½žß'C˜‡<âü©òøp<æò$=&—ï £¸­›åñå9ÇÄpT¡X +ð¾{çïP7‚;8_@' Á(\”¨{…‘0ðºsÁSºmWûÍæõˆôü&öÜûÐܐ™ZÓà5@‹ &²†ÎûÄ3ìê,tSaf\>ôL„ýg”wáÆz¡ÉÙ?\8‰¢ÌÂTöŽÙ FŸDW”R.(“‡|AÄ}÷åÑIÎãÅý¿hö˜!9Hü€c—‘ÖQލw&0ÿ â¡Ãna"> +endobj + +306 0 obj +<> +endobj + +307 0 obj +<> +endobj + +308 0 obj +<> +endobj + +309 0 obj +<> +endobj + +310 0 obj +<> +endobj + +311 0 obj +<> +stream +x­YËrÛ8ýìڞ’‚o¦Ê‹'©ÊTwǝ¨fI IèP„š;šÍüúœ ”DùÕiWª,G$..î㜃ë?™Çóè_ÿ™o_}žÇ֍ûšÕkö'î÷þ#ß².è5Aß,VÌ-,܏"Á’$á^±Åöóō¬åVµªn^_΅ïyU·]ªš]~]ü‹Œƒ/ÃjØY_..¼úÄiÿ0àAày©{ª«Ö>>^Ë£Øóûŗl>g;¹VÌm5cÿUµ™/e£ +öåÂcºaíF±•®›Ö½hVö›ÂäÝVUí—KÞï0®‡ÜðÑUÛÕk÷;õo²· ö{ÇÈY<·`7 îEKb¿¶V§†±è{ÑÄbäqOD1K‚h°ØÈ[Å&§ +¼“xû£g'ñ6]»Ò¥š¬ ûpÏØdmD¹±{º–õG¹ò&«£ð9«óRÉêlíóv.Ôª”íî¬«¼VT +²<³¤Ï± ›\ëô]}ßɪ8_ü¬K])Y?¸øËå¤yæaâ¡óÕ8^`>½”Êüîâj¦a’åf·Ÿöëª‚ÚøÔ‘úè€ä¤pØdC'vº•B]v…B#¶ÖìT®W:—­6¶nX¥rÕ4²Þ£)Ùû +HRɲܓ/Ô s?ðyw{ï=ÄA +rtu+÷lCõŸodµVgoùš3¹‚5¯érÚcՕåô<~pÏïÏ#;lZµäšœ)ó*Y¡òz¿k66nwf—ŠQx^ᇮ¬ïˆ¬õÑw/KÃ$&ßeQhŠKݪ +VÚ â‰åc&̎^%"7`ì—ËkÌV1€Î™í]­ÏrWÈV:ÇdÙrˆÜø…rwaò¼«krXQ¨–µù/¾× +–ä²DYØå6ªK…G¹Áë9I[ɚ>v¦i4^æ—?—¢ž2JYJ1;¦ˆÓ¨¦1äfôÒÿ&åsÇ(×Ä cßú|½Ãt,ÿG©¥ik„k²ÈFÇܲ°ä*øÍZcS‹OÎ~íÀ(Èu¡W+US¤WµÙºlÕz­‘­IÍã€û1¼j€BÆ­íiçôØõêQ ÕÄL­¾•eGþ0U6 +õúoúÿÛºÆ7C­ÖRƒ,¹-R6÷â!Y恚ÚËlzПî‚ÝR/{1ŠcذSnÊŇvx7®Xe*5^1 ¸Arºª#ö7Ë?ðZ3c>õMEÝ®´Ð0ìâÂGz"vŽç¦áòS¼œº¶[䊪D+¹m\°Þúh-êN9·Ã”'!L<åzO³÷ôœ'F^ ×ý6]E©ÖÈæÓ§@nQ•á“§8¢û³®IA//x’È«9"£¡\9F’ñà9ǰšã¬Ž(Ù˵Ý6@¶ÚÁ­ß”¸®Ý ~þtýþýc……”ˆ§ ‹ð$ñüÌ꧇Îj,r¾hf@e{ÙRƒGTY´ƒÌû‚š§™BŒÞ‚¦¸¢ó¹ˆl¶zV÷½,²¤¾‚zZÿ +Ì[rP‰¶†AñZUù@êl­*U£à§'5oyç\IoµIçr˜•£¿—DÆ-©tFÓ85`Ÿšíj³Ó—~z,Áó,‰ú8Uy–d)ˆ^.$8qb·QÐxԉ‚¹¿Àl8]•…<ð£1B„V$¸£ßûðڈ®L½•-} ¤Û©Ú~Mi¹ƒðs1½ÕàO2Tс.¼ÊQO3‘9ó,àišRûäæx¢!=÷ú$\£¹(à¸$§§wÐ3Í4¼54QúV _›À0*búÆÖTȋ„Å%Î~3­zÍnö¿v7oÞ9Mi‡˜ÔNÎlI„:3cõ¡xiEƈ-¥¦¥®Ñ¢J*ÛØ8.­sœÑÞRçÂ8ØÂ\O/ª~8”ØùõEŒY¨·—t¸óóIS­@íÐ0G©·„N1Zû צ÷€,㣆ÒQ…žé›Ìîß_á*HIÕàʲ²PÁÔw m81›%<ƒ$úüujQ[쵙+ï8æÂÏBXÙò®mUn·M§­Ä»ÙãöP¡¯Î]`zo¼|þ\â¶QÎX«ÛEѯ3ö™sŽú9%ê !·«]å¸ S ­ÖGb¼dÌ”hv¤x èÏ¢Œ,©ÇöîFA¸W(‹Â]¥éwFÔÃÙÍa¶ƒ,‚Œ„îÌØDæW¶ø ‘­vÐA~ìÁ[šò ›r¬Ý])sË_.È}@ò€3ӛ æda6Šáç63ŽûCEŸ—¦9ŸÏÄ—½˜E©ËþG™× ØÍŽŽŠ1‡«tcr-é‚FwÉ4n¥«¾7 +×gÌúÖL ¸Âƒ4:Ü9À¹¾¾ÛM׃s‚{]žàbm=' ¢^¬MIm©[Jyúá? ‚õã½$„è+ìÖ¥F<”¶¤89”… ²û7sÞä3ÖA}4Hüûs@ßK8);t°S»×?`çysÁ{vÀ,òӇvp-2¡Zޝ²°7(I¸Ù´²n)W¶›(Gw²ü†UµéÖwk¤AðOoU°;u«¶ÍtÊúx{½Ð”ÕÃهˆ¾À U`TÑÕÍ5õKq°Ê3 8‹xêy ¦SØ#öpZZ +ìæX*D‹>öGEýƒ…0o“4áI–½ñ´·Óă·Ix”þ0îÑÃÊ)šCŒMÊ+ý,´Éê0IV6-1“Ù©êl0) ñü\H¢Áº8·”8XyËr§Å`Ó֠ŜŽ•-HOŒ§ªÂøhc +7“rSA mÒCùá¸^O‡‰UÈ!¬{…qMCJ  ãx­Ÿ½DG'Pc˜ùŠq²ìÊM?$ùÑn<̎’àëýµłQ”UwKÈ ÉÐa@Ÿxaв„~4Œ(/Þ  PYÿ÷P +yPŸI’E¸À$Ý +˜Ê°ÒàÚ[;D¡°söÎÔÎM«Öv]½#«ip†F½qÜsâèÉ}´“†´u2 õhc«¿lÅ8 +Ã蟬ƒ¹#ˆØ{V&®ý ïHü{Rì?,*¥ŠæþɈ `­«‚&Qˆj³1w”zÜ_lïœLQ +x?«>ûqš‰£Ö½bbªxÇ¿)Ð8±2í¹•€Pk¸ä9€¹bÞ“É{þGö)¢Kc€Ûl“нB‘*Ge¥H‚B!º_ür!B\‹m#þþ hå¼ +endstream +endobj + +312 0 obj +<> +endobj + +313 0 obj +<> +stream +x­XmoÛ6þ+·|±ƒÅ ©W*( +tq;kº61 +¤ðF¢cµ’èJt\÷×ï(QŠ-Ù­7 ùàDòïž{î¹c¾% ¨ù±Ÿq~yË(…Ǫy å#|Öün?âþ˜]Þ:Ô7Of hLø”Pæ†!¡n³|œK-¡Åùì‹qì¶&́Y "Ïç0ی¯U¡EZT —¯sYèQÆ' *ða«—ª€$uª +Qn ¼Õ|•[|[J¨rº£\/"‘ïc€³d¼Pe.t/7h^^ôM9q|/ˆš·²ˆËíÊÙ3è)æ:ՙìÜZö '^ä/ð,Fc±ÆŒËÃQcåhÀXè!ø$pµ!Të‡/2îgï9GÂß³Et7ªLª¾1?Å8.¥ÐƒN;xUªdËò¿Œ› +ÝÇ><éô\%lÛÈ ¼Ê2HµÌáIdkÙð¯ÒeZ`ÿ؄wÊê. °-ë{Uô=;žõŒÏaâ´ 0E¦Q໦ȯ¿Çr¥4w‰ëG|—æ0Ûx±_EÒwÁ"$]äŸLwÓ¦È (DŽ©Çª,eµRè7-@ žRµ®`#¶ •iÜéìæ3µX¤q*²³çÆ®³e¾G8ªF' nèRf2^¤2Kª~¸'^ä6Á\^¦Ø1–„$r][Ŏ)æ{§_ÿ„b'~s˜b'ÙΉJpŠå«C’r´#÷LïËÈIÿuXF&žË uü¶3 ‘ðˆôIfÛºÌãwÆÇf–ä‘ÀM Fóó¦3£¨9©ã*õxä ÃۇÛ?òƒ ß`ýnÜYÁÁ½Ë¼<:ê2I‹µ–‡R 8ev‘«ŽÛW9Yw6öYÍ}\ÆkÓ®mià{–%Ÿk–ˆšðÃÈG£]óštP3ŸdPÊL=6“üϛÙü¼¥¾ ô±@£Ñï##(£ÉÈr¼† 1rÏ·…µŸŒÓö|„ñS'âáýÂXíAYÃ5<Á c‘¥Ï(ÏGG-.5dJ}…,ý:¸£8Èö(´xL¯¼EQùÌeu&Ô¹t Žn7p¥yÞ¿*S¤47¤µë]ОàÕªL3`þ˜3 À®Xp…£m•c¼±ÈntkCGL¼È(+7ÑÜJ½.Qʶ+yÕ_‡°z¼]?Íí͂ÐÞ"'è´¾6NpEƯ˜µòصqÿ®Øßà9‰ºÚ/ÒLOpd»ÃÂõ×`lµ`×Ü$s̼)-C>ców};mçÕg¼ßÖW™ôaž8ÜlHÿæfºüè•x”×j]èÓÐ.ÖùN\oŒaÕnMí0&pƒÃ´l¤fdu¯ª¨B5–ÿ•Ò:p´bÍ\Ž~Ìĸ3QUÒl›¸Œ{(ÃxK~7f>NË×3øÿN?‡« +endstream +endobj + +314 0 obj +<> +endobj + +315 0 obj +<> +endobj + +316 0 obj +<> +endobj + +317 0 obj +<> +stream +xµXKoã6þ+Sô°Nasù —E±@{ج{jz`,:ÖÆ–I®×ýõR¶£W¼]X Lk8ß7oR~JPÿ9~/6ïï¥ðTâ£XƜÅ‹â E¥!Š('"Å'Rç +˟@À5 ¥‘a‹ûß¾†ñÉ[ŕ4\s+\Mô¼ –Ÿ¾øeþþž«˜„ùjHJhM#Ešùfò!qY•V‡›ùgøuë\@eTø'C¨JPÎ‡ùþŒi ŸweÚTDQ+ñMŒŒi¢i<Ìø‡­Šô \á“16_­l[W,óbSB–ƒ]TižM¡ÊáÑÁ®t ìW.sÿ¸ª•ƒòUö ¦ñe—®%R3¢TÃÀ£××dÑEÃ>OáqW}ÝÙµg™I‰û•„Ã- BøØ6+}`¬ ”«|·N ²Ï¶k»p5=7;q˜ÿ>¹³;¿ýöæ]Ó.¬*ôl4„qª<~ކ…L‹ÂÆ×b;òœ6yŸ8cõ¦Ô§`‘gee³ +Ò,$éŸ$l–øŸ…C'ô¬•ÄDG”ÂÙ$Ïև)$9F¦‚ª8øØäIºìÀ¤Â¨­L«¶EŽ5T¥XhÍuL9ži…to‘"%ŽÀ!ßÁÆwgÛØ,ÝîÖ¶ràá:AaZX×lÙéãt1M¦nºü»cVdê=SÈ\êý»Ý®Þظj•'¤ƒ<ã’yè™OÏ ¥<ª3SWàÄM=Ü@ +·:Y`ôªÐ +›ºYCSaq•°NŸ]ÏÁ 7ºã¾<»âK5õ:ø~\¬löä–i0ù¾9C03‡­jë‘=… pey+Û<ÅBÁŠ€å®Q +IN¶n|Gž< ÏäËPN×w8—1Q‘mx»Å¯l<êÖi.Ií½@ù)¹“äÁÝ}^$åí+/‰…P±„ƆVH*æbŒ¡?ؔþ ӚÏAJ4þ`ºyª8ö“ÏÀ¯è)ë ZаA(‰NñúY,Xë¦Pé BmÚBލùÕ¸£*MSʤ§}C•)Þ¶½áô‚*NÖÐKR.ßV=G ãT©Ÿ’0ÄAá)ˆ'ápU㎪hÇIÔî ª²P5o +õ¡2„úgt‰3jsv²*.¨rÑVídU^°÷> +endobj + +319 0 obj +<> +endobj + +320 0 obj +<> +endobj + +321 0 obj +<> +endobj + +322 0 obj +<> +endobj + +323 0 obj +<> +endobj + +324 0 obj +<> +endobj + +325 0 obj +<> +endobj + +326 0 obj +<> +stream +xÍXmoÛ6þ+÷­ÎP³|)jÀ>t]·u؇55°((2m«%G¢ûßï(‰¶%¿ÄN¼¡03äïyŽÇ»£€Ô}Úïdþæ–Q +Ó +§¢ â,:0(§ (ªHaH9H.9 à\Ci`òxD Mi¨ÙÁíoOíñÉ¡â2Ð\qIEuà<«‘û¯d?ÞÜr `4†ƒ€ژ„¢™Ñ|ðáÖ$öfôޏàcã€[2*ÜÌ¡-e@x½%" TĔ€Ñc³=¤ÄÈ#±q>]fq ÷Å2§ù+¨Òyê&m] DRIq(¢x´ƒgðB†\…„éèÃ×`V‰YX°³ØBœeenJü*Jä[ƒìñÓܚ©)+§,+3FÚܐs4 +2£1Zƽ«…IÒÉâÜíC1Eº2Y…Fɔ8¡WMú wŸÇSãØÆ0^–ε¥ÉÇÆÈ†<«?H>ðñÆto¬‰7¦h³¡Ž¨:0ØÕ§´«¯½þž)¥ùÑóL)œm*P/3ÅõÙ¦=j*R4zw •àúÌT+ÁY-ÁBª ¶ËIÞÂå9QÖ oÆ(©­øëòÖÚ2½_Z³½3'©`ŽÒº*8 SW*¼ ”ïBý4+J ¿˜*)ӅM‹|²2 +`'Ï:·{§RQcUènÆ78ŸÈ œÊ~öSmö£Òcª/7yLÇv†h: Zã â?ãÁßNÀ]h;3tx.¥eM!äס tØ¥03étfOqø½–x Ö$}%*ê’XÑSþn“´§a‹dfbÛ4~6ì®ÊD².“õI&Ÿ¯ÈD°ë2¢w&ìÒ3¹/¬-æPÖáv!&®Ë†õnúš]z.§Øp"±Ydðþ}mabØ/s¯ÿv"ñ_”·BíÄ®/Ç!?ÑjkîV¨èµÕv+ÔNt„x¸ŸpBKˆ« ¾˜Î73¡Ø·çç¶ôm·òö=!ÔßÛIõš;»xù6Շϻ,®*xûׇ:~„Ò­ +ǖýÏAR¯Þ¼ÚE.‰TXëÈÚ4îCÖNæxpw—橽»ƒ6.=4Š_õ߬ ߺY­L6é)òVñ5ôk»XéëÕý‰>Kqý\Å{®ÅãŠ_nžJ,B+IB Rð¶þJl!ÙÉeÿ‰¡|tˆ'†Ô!ÇV Ÿ²å2±EI`„dl&ñ2³ð-ΖøºxLÝ«˜cð©`æ »öÏ­Ìøu™'®;òNññ$¸B·ˆ†yÌJWÿ  —ÔBíð°F´1UÕɬšã“ÇT¶çïa€“×Lóìém,Ú3IŠÜƸØ?ô¶ +´kùeot¤ÇnȘÓô×â`GF[€ÎÅÎ|ÃéñXoFàýÃ2Ϊ>DÎ #•0„½¶#à» ·q7ä”\2í–Gë…ù±ï!M°­iwÇGäMÜÁ=æ‡> +endobj + +328 0 obj +<> +endobj + +329 0 obj +<> +endobj + +330 0 obj +<> +endobj + +331 0 obj +<> +endobj + +332 0 obj +<> +endobj + +333 0 obj +<> +stream +xµVÛnÛ8ý•y«]Ä,ï¢ö-Û½ Û h]?-²ÄØj,ɕèuó÷;ÔͲì$"LÓÙs†sfø(a@ý_ûgïæŒRXU¸ʐ³ðÌ¢\ xDiʉAqʼn”À¹ÒÂÝ[°Ç†ÒÀ°3‹ùßÏùøêQq%Á×\‡ +BS#=‚ÀjäÝGœÁï‹ws®B`wÐ0b )¡MA(†Yd“›4¿Ÿ.¾ÃŸ øâi ¡B tè_v踩хqÚ²kb>ƒQáwÎÁP’ð†gP ¼ØOæv[ÚÊæ®‚¶Eš;[‚+ *2»_[ z;q봂¤ˆwÚ]Aáp¿ÿ^]~‡ú`nÝ픀gYý™V¶SØF+{åù3‹PD†”k#Êïæ"O³(÷Q™Ìâu”æ6ñpl¯›ÀWP¹¨ti¾‚»²È Ê1vêÒhŒ ûuЦ8Šc[UércaùàOßà-QÕ¦‡ú°ŸÙQRC¾ÜNÁ[ ’H¹·L&™uë"!ýå±úox]T7ÅĚbbº¿2bR}f18¯9>oºó'¡tW·'¦†RüÑóXm¡0Åт› ©JÝY„µSa@¤/í.ÂË¥Å(Õ4£ŠÐ:L[ғkçÊt¹sv(¯'¸pDË©L¢Ý«#’¥ùê×uQ:øÃVq™n]ZäÏv„6«:dMÞåK:¡äûŽ ›Ž CÑaòõOJ;ÄrTÿ#^`­÷›4¾¼ P¥Êv¹+£¼BÁf¨Ó¸(Ê$Í#g+r1§@¼.§@qJlõ$§)vžâ®éÞØãÇ;¹œ€R¯K@GrûóIÞÈwHoW³ès¢p2øÿ¿·ˆpúUœòœ€ã®ÔuGFm?:µވ3V ­k:Y¿p~âªÛ;€:ctpÕÅSO}{«ÑÁ³á.Öx7aµÐÝÕ½ßDUן?ÔW'´ipº7“¸þõxq…}¢›6Ý;aÆÚݙ?˜LÎ)”¢÷?.Ö­0Ý:rã`\Zˆ½fQ›Ï‰uȋc¾$öÂ3Þïܺ]™ƒ{ØÚߪ¨oвP‰=G˜b„*… 7]²æ «ã)GµéW‚uÏ©þ(Oc¹Ã=ö×yýY5o eû¬Á’ÃŒE¹õÍÂ?xj÷¼wÏqÐaìúöþú÷Û͇O¿½…Mƒÿ¢Íî4í3!Púò‘´ý؈LØ6„Øbé3CŸõ‹sÆoq®…e£î#B8·t£n2#¢%S”㣠ê°MÆ'Ì٘¨Æ¹ã_V¿´À¤”ÃFzqz²)0ãg8½¾måF%”×53Ó7M/ÿ±‚é +endstream +endobj + +334 0 obj +<> +endobj + +335 0 obj +<> +endobj + +336 0 obj +<> +endobj + +337 0 obj +<> +endobj + +338 0 obj +<> +endobj + +339 0 obj +<> +endobj + +340 0 obj +<> +endobj + +341 0 obj +<> +endobj + +342 0 obj +<> +endobj + +343 0 obj +<> +endobj + +344 0 obj +<> +endobj + +345 0 obj +<> +endobj + +346 0 obj +<> +endobj + +347 0 obj +<> +endobj + +348 0 obj +<> +endobj + +349 0 obj +<> +stream +x½Y{oÛ6ÿ*÷ߒ!QEФ¨ý3dÙÒ¢K=  +²MÛZdÉÕ£i¾ýî(ÑÖÃuÔÖ‚6*yäýîÉ»ëð=>ý´¿Û÷Ì÷a]âR$"΢#ŏHaès/ˆ@rÉ=!€s …Õ÷À#žÐ¾jväãþ·çîxK¨¸ZãW\E?åkA>³ÈݯÅ~š½¸ç2&`¶‚F"Â÷ü†MàùÈf¶½H“ìáSV—³á×üÙèàÄ­Ìh娭Rx¼¹õç4.KD¾+Li²*ÉÖPm “÷ÒîVs¼c¶¼X"†fÿÀ€3OiŸ5»"ߙ¢z‚|qy]!tx{ñäíÑ3ûƒè…ÓhÐj”5eÊo­‚úuä«#ÝóŒ÷Ïkw~ÄJEâ3¤ÓX©PMf%õ·±þdVœ+_Le%µú&VRéɬ¤ÿm¬‚Én!ÙçÝ"R~èî‡V×=X(GX +îkíaR¢´å8|yÚ`qýg<ò$žE>.ÀoªªHæueyã¤0\)íòUsLòìP¥ŒnåD×ÞºÀ~en *LL¨cÒé*±Š‹až×™õúyþé +…Éëõ¦¯hëf ¥1Ã\COk¹ÜݾþãýË»›ßÞ Þ[›²i¹¯JxЕ.„k=ÀÀã&A-·nÃ#ZâEUÇiúäÂÔ,=xã k¢ÆBÏxKêέòL|¸nÙ†NzN‚±'Sïräc}ˆ[o«6u {\t%qi˜þÞŒ)Á*íB€^å…”‘ËÛý֍³ì2<é44+êQ+š­+(sXæåͳ!DhÏ9ýtbm%Ý+ÌÁÔeÖ¦8SŽÂ۞IPÇf‡D<£dJε7 vchAJ¬Šxñ`aoóºìèi2»ŽE%$4ß/óô#)û³Šk 1+ôð2NËs%äyž§ÏJ;¤};–‘äïäh& d¤"3••«.ÿ£G†{èÉ|¦mî¼mƒ yD‰uãb}(…>`œÂ.ä}-E¾À¸Ìª+ÜAÒ/z†!ëlS9Ô†¶ª¢sØF¨ Mt~Òòà.Ïã¡›OöÿaHÇýèbT'´jxùÏ{õ£$¥ü†]2ÍX”6ô‡™dT81áûûu!„§p&Kvq*Qr_6ÅeÓã…Ÿ-¬¥±¨dÑåwÍsÿçló +endstream +endobj + +350 0 obj +<> +endobj + +351 0 obj +<> +endobj + +352 0 obj +<> +endobj + +353 0 obj +<> +endobj + +354 0 obj +<> +stream +x½TM›0ý+skrÀkã8†Þú)Uêa7¢ê%q6kEý÷µù¨È¶¢bWÀãñ›yï™yŒàðŒï¢¹;Œ¡²CL@†ïñU4ð6»;ʅHv†á(†&lœs„i +Y³©Ý6û 锋!"1då&»pº…Zœø,øÁÃØ Ã$et¡ø p,NHŒ’Øß³©ø½–ê)ô¿aÒÑôSŸÁùÆKaT¹“Z¡‘!G,A1õâD$ <ÂuFûފ×+ÐdaÆ€ïø Í¿f¹`[Œ'H•7¢\pNÚ¹`ÄYëÝ«ýi´º3…€I2]tð†7VxS꓀û÷nôXÇíS=}WdçMø½œó9FI’&ž”Ï4™ó> +endobj + +356 0 obj +<> +endobj + +357 0 obj +<> +endobj + +358 0 obj +<> +endobj + +359 0 obj +<> +endobj + +360 0 obj +<> +endobj + +361 0 obj +<> +endobj + +362 0 obj +<> +endobj + +363 0 obj +<> +endobj + +364 0 obj +<> +endobj + +365 0 obj +<> +endobj + +366 0 obj +<> +endobj + +367 0 obj +<> +endobj + +368 0 obj +<> +endobj + +369 0 obj +<> +stream +x­YmoÛ8þ+ü¶ÎÁVù"JT~ØÛÞÝö°²ip¯9²DÇÊɒW”“¸_ö¯ï EI¶ì¸Šc…Uj8ï3>üPŠî7Y½»a”’{K‘qy¨î‰ °E$ )÷DD$—Üó}¹"•&‹?Až`‡¢4TìÈÃÍß¾Çã jÅ¥¯<ð€‘„P壿f5o’ùóí».#Â|r» EŒøÔ£áQs»š|Žë*{¾º} ¹%¿68Á“Q+ÇxJßã–'Z&¥’Ü>9þ$3$&Uù4[ÅeEij «æÍÆè”Ì·$[Å÷šÔU\˜EYÁˬ, É +òysýñ¯ùgV/›=‰6d[nH°PdëM×°u©Ñ ÆÉí/„yTqÎPƒJ©®@H#¡\€&k|Þ1yŒ«L×[\~Š·æ=¹›¬ãª6°pw…<Z”5×`A ¢Ò)ùV–+ü]äÙzf©céÕàl–Ù¢nŒ3º®³âžØEÀþ8ÏQâÃÆÔĀŠÍJƒaqúäm<àÁB=Ÿ©Z_~ɊDÛ½ë2+PNJ¬³g’gÎ ú©œ¥ÙJ Í:Nô””Qæ›UAuRÃN_/㺍ƒP>¯ã¢&›"«áuqNõ¹„UÀL`™o­‡*½Š³ÂÚŒt®A:hþh¢ ñì…aõÈíR}Œ|æmãº‚¢qßՐÌmâMþOçÓdšNõtñ?|Õçd¨Ðóiã~ü aG»–åê¹µ"Ö¥ÉP LˆÂªß8à}W˜úªÉú0„û"ð„"!Ö&6 ácéh_U&R…^ؔÞ'ˆý²Üä)fVQڼˆ " R’ÁšÉ︀ÿoló§JÙÔFíç:óVº^–iãD›UÉG]d3f±)’¦œfD?êj[/1XÖ%i9µÉç¦D=âd™ÍÚ4« òm_VÖ±}(É^lˆŒ‚j]ÆJç˂}  ’ÁÛÿñœÚb™‰€c qEцUÐ1U‘êûÑA îlƒXjèK!ÈãF^‹¿æeòÿ§ÌèîbÁÓ³|ÔÃh3˜ò¢wÊ`E¥ˆî}¥µ™âj=ÛºZu’¨´I†e\ÜëlÔöÈ4úŸòœü-è0\‡¨.Ê@IœÇS 5â©m÷ˆÉÐÅÜ¡zN ¹Ç•Œ^¬'¸- usàc²âÍ|à3{™‘Í(m˜w’RùC¨q7'jà¸é2Ã,ôÌJ\6æ †¸üÔv{†E¶xíu\ŀ9C{°¸yy-ôøGÆÇ2.f(ã{ÅÕäöŠk‘— ß³]yQ[`d6Ãk„¾R@ïÜ^‚¸L‡«œÍtDÂëæ6¤h‰ç1ÞLÀÃuÀËæ8Œ‡€P‰¶Ó:FwWCoÌB˜gU„`­ìüx£ëM°]ë÷oï”L‚« Saµ„gÝÕÊÒÍŒF-ÏY8s,xö?>æãg¶/l;Ò6ð8ÞFÄêjðrûÌ·þÔdˆyÆË7b¶pñw4â¸EÍP+€ü¼¾Ú©"ó»™+}i#^Y(l&Àó! Ü A]ã79mðe€›½,ýeÂܵalñ†‰áÕó¯‚׋ÿ +endstream +endobj + +370 0 obj +<> +endobj + +371 0 obj +<> +endobj + +372 0 obj +<> +endobj + +373 0 obj +<> +endobj + +374 0 obj +<> +stream +xÍXaoÛ6ý+‡C.æDR¥¢Ø‡u-:`ÚÆÀš!_h‰²µÊ–*Ò±½/ûë;Š’c˱ti7±R¤îÞ{÷ŽÌ'…Àýtßéâ»÷4`fü043øÔÿÝ}¥ øaâ£nd’ƒ_J!¦„ AAJIžÀd1z«µÐV7æùÅä·ˆw‹âˆpãCÙè¯nªß/"÷à ³Á*ÊIâçnFíânŠ΃ ö³yY);\ïÖ^Àx ¿WÕr•Úªb  +Ú¢Zxcv®Aç9Ñ2mÒ¦¨ÝÙi ö󇂑˜!ÌM¿×vÕ,Ánk턯&ð®ªh"øYaò0© $${aþ¢0ôÍáÖgödì)P¤´ß³nôÕ\«¹‰ð€®=˜öé2›Á2D¢¸¢+ˆˆõú8±ìæb€üX")IÐcŒP&œR)`²½ÕM^5 Æ%P,g—PMÀ6ji܌ê9wòPËY© +ÆVPc…—¥.«Vº›Q3¯ÓªÈÌ́7Êtú1€ëw‚1ƒ”\D?»¸×£)\<= ±ËG-³!4!atóéQtÕ²Üɍ1ÂX ûòû?ûÔUGI_ˆC¯ºÂÂC +"$’чHA’ˆ‹ä¥$ +#q¸#âž>Û¶ûs È¦§¥Ð + D EܧKÛV÷q—w%~‚u”‰#Ï)ÊñžzÞ»ÑV@ä§H¾=ÏþWä=;ãëû’Å»6>×.÷cã0Äþ¸uÚøéÀøm£-jì”ó£ÔâŽiÿÞ¡Æ8Gëç;çÁ7­ßgð±¥¼(žçy`ú¿ß3Gsü_ÍñõÙ.ÏEtèø{ɚyòîËýÕ}å%„EôaÎF’”»Ââ «ç~  \¤WÏξ}Ÿ$‘»“#_+«=ÏQŒgJzÀ³+ K=Ã+Ê­ö2l+sø~l/q×]ô©»B›“G̹ÂÑy1BdÀ•Úü µh®ŒÉjw¦öbEB’äAÄrÂdßü‡Äºæx†×Нé}]>?敢îŒ}WI–ü[^ó𺪑սM(íY¥î™WÞÍэ§½è¢£ ·ý{(üí¼Ô8¨1ܧÂ[Žq˜wæÙ_¥‘ëBߪiéÜ Ë w¯Hµi[%äUYVkçú…áµÞªÆúg^ÙgúâiçFÜeLCJ÷–·?¾v*3ݍßÎñÌ3×e yQ¢Oþæ”Q­aíøi«ºÖKÀåƒÙ¸ƒÒT» p²,PF7ÈhµšaåZm 䫲Ä0Z®]H¦E!m4f]‚»¶»ÿ.X?ž¦.Õ÷q‡‡‘Q( S«Ô÷[íhâ¢íih‚³Ý¿ÍŠ…Ï{Œ×§$&TrÇÓËRÓÒ2‰àÂíÂ>ØÞÑÞý ëú¼q +endstream +endobj + +375 0 obj +<> +endobj + +376 0 obj +<> +stream +x­“KKÃ@…ÿÊٙnÆ;ÏL7¾V"´ÜtÛ¼¤iÚ$ÐúïÉ´V°¢d1—ÌåÜóîlAŒƒüw8Í匡책Òr¼ƒ Nˆ9g2¶ÐÖ“Éت¹Â]‹©¯Œ¯œŽÐÊZW#L¢]! Y…®tŠ<èŽEƒ›ôr&t.81âÚ@kËȍJ›è¹ª‹¡^—“ô ÷)¦Áõªœ¤ÿsNU&Ì՗C—­û¢íÔvõP!C“ ]½Ç®Ê»n¢óxRÓ SŽé2Êq ŸÖØrx¼žG]]Vzï¯ïcï¦Þç«~>ùq9ÒÁ£±Kf]ÎÒHsÊَ9ÿVªøû”ïaŽeë—ín}ÎØÕ7ÞX0kPx¿Z×*XÊíJLÁzb¼s¬ :®ˆàÿ¿"Æz<íùVõfó·‘*¨ZJ˜àÒ«Þ®²¾Ï{/[Å´t³#!'ã‡EÜ?—éE$Ä¢ +endstream +endobj + +377 0 obj +<> +endobj + +378 0 obj +<> +endobj + +379 0 obj +<> +endobj + +380 0 obj +<> +stream +x“MOƒ@†ÿÊܤ·³ßËÁ‹_'/­4é)†B ÄD½³@­Ò؆ÃNvv†÷ywvÈ8 ÿÆ5Ù̗òv؆&‡ð!—dב?&ýNô +C)… ¹6`­e(Cˆ6Á}Yl¡[g°ó Þ³¦+’¸,?`Ì¢·ùRš f:¤>QÄp—¼?pø‹4Cr5£ÜE° eJã¨Íh+ “ցA’B<nëþð‰Ú¹_ëº)>몛ÖoI¾ G€ô³5˜pPލ=€ÄÀGÜGtWB+ç(Fø2‚5èԑËd-S”Ò ¦ÊÇu7E•Ë;¤QŒÞMÛA ­ïEϐM–tE]MŒD5õB."ÓlTbÂF©P÷„¦9àÂJFqäü`£> +endobj + +382 0 obj +<> +stream +x‘½nÄ „_eºÜ5Ü.°ÚüUilñNâXùlÙ¾æÞ>`œTQQ0ôÍ0;ƒƒòÚ÷·Ë©a"ô+f›í쓰ÌÊTŽ,)¶§b ê¬8«ÄÑb½OB;í‚$ay‹¥ODÞy´›á>ž-¬?P20’±8Hå%«x94Óµ½žÇþ?ñQ—ÔP™L>ù*•r…ú|Ûa¸¡Å²ñ§¯·ôC¼wýÒuëÛ ªÂ7Ú Ø†Ô†‡Ö¹•ï2ÌVÆÿ[y +J³É‰†v]‹qå­#ˆ/-Ç»-Hr!ÎÓ©¿•Yd +endstream +endobj + +383 0 obj +<> +endobj + +384 0 obj +<> +endobj + +385 0 obj +<> +endobj + +386 0 obj +<> +endobj + +387 0 obj +<> +endobj + +388 0 obj +<> +endobj + +389 0 obj +<> +endobj + +390 0 obj +<> +endobj + +391 0 obj +<> +endobj + +392 0 obj +<> +endobj + +393 0 obj +<> +endobj + +394 0 obj +<> +stream +xÕY[oÛVþ+íCäEDŸ /E Û‹i6Mõ°ÀfhêØbB‘ +IYëýõ9’¢d[n܇ÂÄPsf¾3÷}p`ôç>óÍåGÎÜ´ø* SÁÓÍ H†GTqÌD SPB‰ Aˆ × {<‘0'üÄÃÇ>Æã7B%T˜$ø "¥ +dǐ|nû|ÿX^~*Âòì8„,`VŒ ŠYnæÿÚueQé‹ågøq ¿Z<ĔIzsŠ© +a˜ÚDpXîçµeȟÔéφ Qàñåj^´Á¶©·ºéî ¾¶´ƒœ( ¢/jˆßÖùn£«îˆŸý:€Ÿ®¡ª»)ÉQ‘ø¾6÷ŸŽýK(:h»¬Zµp]7ЭрEÓvà/RtzC 雕ƒR»Ö_¡Ð-és!ú‰ŠaÁ2'ƒÇ"T¤–n×T°Ò×đ8åë¬ÉòN7EÛyk bŒ4DYÙÖ°­‹ªƒ®†Ï4H&ÌÖuSü¿®º¬œÁªhtÞuWw½z'j!\ïȓb–ˆ”€9w*ý?T⋱Žâ„èb«(A¸ ™WI›m4”úV—/ æÙªÞWû¬Yµ³1†©mDŒ®ÎRË×˧£S§y€uâE(R ¥Aç±13J7Р¬÷º `I–Ý•%tÖ„>ÃçC’èà&@÷Ìëê ÚÌðDvUš#ø¶Ã·-äjYc¤æõ­nôŠ®{]—(ª¨n3F_ëVÃÌXQ7í,¸xacŽ›?Œ¹Ð¥(6 p›xÄ\*!JRxŸÂÃó‰?,*Œî!=S”HÎÅÙ7‰R©8W”ŠÃ{E¥Ke2~H")’3 FžB +®Ò8ÀôC¥ÂKxzªæŒO²j"Α/ÊqYuþ‹îÖõ +.áû®kŠ«]7ÊÚÞJð8ÆÚc–’‡H÷ì˜eÈ…Yv„ù7LI¼ÕmÞ[ŠŸ1d)U¨’¼~£Ø`r=ÎG«“šV§ÈV§ˈ8(y&ÇLR yûbò¾Ï-}";=vϊ^ñ)zÊ|ç¡òòÙð%^øR)_·ÂWT_€ˆŠ*{’ßpi¡'ϝ«)ô®èJýö¥#8 °J]"aτ˜Òq›Ýê%úçù§‹‡€h +*gÖ;,¾öEO¸d»K>“G©8=uÉÿòîÜ;V€Ä‹²ø¢Ïº vñ~ÆŸplܧcJ¡=‡%6ïÓ²<¹D® DîÅ‘«º‘{q@äêí@ä^Œ‰|Qí‰ü "œ›ÂáË馥'ä¥SP'ˆV^^>@4åMT“ƒ'ŝ]³üô¢Tâê‡2k[øþÃOƍd” CÂòÝ<7ßRk¿)dy¦§Š>Fµ0MݨFf«{ÌÌ͝‚)Cðƒ»p{Pf¹ÑŸ¡Ã±-«ìˆì'ô—ö%˜É«Ò֟¶Þ5¹¦ÑÆ7º§›†I~Qåån…¬Ímêή8nÀT œd+íPt¥÷€S%îªÛú‹}ý9»ÍlÏK’{ò ¬ÀNØÞȸ.à!ö媍e¿¦™£¿¼µ&ùf#‹¾é8Ùyÿ T*Rde¸riH?¨ª£ EŒnÿèÞæ(\8n|dt_¸l°_IR×Xƒê™1nÖe "×MÁD¨r`Nt œ!›ó bLHïå¸ùA¿ÅÞc²ÍiѳI%¸5ÉuÛâNV*[j^ðÙm?¨C9^u¥AJ …*Êb‰PÅ+ú&óùÊøÆû¼ÍnôßÞL=*~e·Ç²k´j·¹¢Å˜ž*þ.¸]J–:[‘n‘»Ñô§yµàŸ.SÑ­¡ýuGJ°:öv±uÀmþL* +à"sBQ‡‚/®2²•OE9þ."ŽWoQ§Üxƒ¹&¼Æ[îݰûcRо~ÔUy®–x€“ôاEü¾-VÚ­,iQêbpfj·:/® Æ2úó‚½þì‹ìX¢Y™fÛ­ÎHäJ¨Öa¡Ù«‚üa¦ÂLµÊEéæðÐFÓrì» ”ÊØÈÿ»üùÀþ\ҚÏܤS“ÄúqLGúÓ°XÀ{â¬k\5¸o0©Ò%œfx<Î8KLӁ8N¥ ŸÁ+“„óoNÙÉd R¼§¿è½É $ rHF -\àÇr”Ób¬[ƋÿP"à´ãn³ûÊ_Ýøæëæ„Pp¼žU³Ë£<¤Ô7ôëûQ|žênºJ鷀Q:ÃßZ0™Ðò›Šó¾hõjqÔëq†#PÊÏ@{ –j0÷`ɄœÁK?ì ÜBÛͱ]Qo1«àÎßÜÇþ¦ æÃà4§®Èo«¢]Ûõsf@fC"יÞBEøŒ»Ä¥»ÅpÄèXiÈÿ +‘K +ט¥AK:iF> +endobj + +396 0 obj +<> +endobj + +397 0 obj +<> +endobj + +398 0 obj +<> +endobj + +399 0 obj +<> +endobj + +400 0 obj +<> +endobj + +401 0 obj +<> +endobj + +402 0 obj +<> +endobj + +403 0 obj +<> +endobj + +404 0 obj +<> +endobj + +405 0 obj +<> +endobj + +406 0 obj +<> +endobj + +407 0 obj +<> +endobj + +408 0 obj +<> +stream +x­X[oÛ6þ+ç­Î³¼‹Ü[¶C‡h?æE±éD-¹’¼ÖOýë;¤DY’ÄF #°BŸÛwîÔw „õŸö{¾~Ë(…Ç +¬´œÙ#å#Š,JC’PN„Å'RçJËß@Àä0”&†y¸ýû5wÞ*®¤1øÀ5×VáƒÐÔHoÁw`Áòø5_ó÷·\Y`fKh1”ÐF ÕÌ֓/飻š}ƒ¿fðÕÃBY =ëP>SV¡ZHÖ1&Q§-¼Féóv0*üÉ1;”$|odyíÊe:w×0/]Z»<ìÍC0TE!Ìå%Í“Å|»vyMVEºð"î'÷W¾§}ˆI‡“…Z,£ßEëwÖøé1‰±Tyèó3>ä7‘ÿ@•¶òÒÓTéDŸ¬J™gUYM­0ý£7'zEêH!§Æ,_0QÃù Ë(% +£ ÑÊ«‰yòÙÕOÅÞÃM]—Ùödï  ¸Ö†µ7ÙpL>uq“…Ö„Ú¤oóÝSQÖðÁUó2ÛÔY‘¿Vp{½ãQFÂÏ©·^©ÄzÓm½±¤_o䡨æ‹#%Ã9Zãۜ/›Ù“Oý®Býó:ÍWîTÚ&Æã"´µ¾ì?eù¿Õk þv5¤«Ôfå°×@ +›º_‡%±‹¦ÂbØK¹Í_Cq»F#²Ø>>á¿ ÷_6?…f ŠKED‹Š|»~påK B§ïÈ£9Q8#üƒßZkp,ŽÑ”ŠØ¬p4Ž{ðž¨=èÅîÛŃQÛb÷Dí'âØ|‰Íuݝ$R%G¢ŽíEE}íÁq¢±lO5b<ªîädzF×´±ýs•VÜ|ù"+´‰‘ÅýâÓd~½z×O®ˆÒ”ïcîƒ=eíéÔ3.&¡A›-Ñ ¡¦#Õ0Å-dÊxýw…u–»PÓMUô[ÜOܲÀü®Ë4¯ðiúÞ{EFú09™´^° Eæêm™C½Û¸ß÷U6CE™Uâ¬U‡)F¨R t·êÜ¢‰CÁgUžj££D×Ì»68v¥¦Ï»RÑèÊšãy¾êËO°ä5þUVÕ#IS%£H€9±UÅDª æ6;ÿ9xyP,áíñÁ%žH\pÞôž<9>G$b\=Hà&·ÞÔ»ÆòÌ穇‹ n¶ô~/}îæEp¿÷¾ÅÞÿû$@¿£ti±ïã˜scÞòFxÏh]‰ScÄÇÚ_®aÄJ–µEÙӈ[E­yf%Á–Çd{a†$[*&ۗ´L×¾ê’Äh"P‘'ú5®œ,0ŒÇ’¯¤Ø¢‚»ÞZì6!Þ`Éu ý‡Ñt>?xB¬ȼ¿‚éÉ×P<Ôiæ3iYk(°!!¥.PÂúâx ‰{7“¡ó"¯êr;¯‹² +·´)W8#ÿëµí†ì¸ð‹všcTq‰7½ G2n&éžÂœw‡=¸š$24v‰Ã¿i¥PÌ~ìëdœtؾ™ç»XÒù‘–õ ™|Nñnôó-I‡ó„±çÀ5 8ÌU¨ H7›ÕÎ?tí >§;ìgóÕv~.Jp?Óõ¦¹@LM©Ú΀(7‘ †jžb+|„׃²¨ƒ†k¨œ;|€M´5Üà&›ÒÝ!x0v?ÆÝ«ièÓ¸|ôäá80FŒåÝz޾]ˆ •ÀŽÑ,Lö©Àõ8±¶C2¢¸@E2£KÓ­ø$«wð¦&ƒYibq.á<îN~p-\••nq8˜pà³ÄÆÍîè=€u+ǬMнqËÀ×G͒‰×\ÿzK&†ØDøŸÂþê*ÿs‚®ë¸·òäê]㲯ÿµ +endstream +endobj + +409 0 obj +<> +endobj + +410 0 obj +<> +endobj + +411 0 obj +<> +endobj + +412 0 obj +<> +endobj + +413 0 obj +<> +endobj + +414 0 obj +<> +endobj + +415 0 obj +<> +endobj + +416 0 obj +<> +endobj + +417 0 obj +<> +endobj + +418 0 obj +<> +endobj + +419 0 obj +<> +endobj + +420 0 obj +<> +endobj + +421 0 obj +<> +stream +x½YmoÛ6þ+‡}™SØ,I‰5`Úmè:lC—Û +ä cÓ±6Yr%¹Ž÷ëw|ó ã8bFl:’ÏsÇ;Þ]>% ¨ùøßÉâõ5£î:*҂³âÈC{ Å)"ƒ<§œ$.8ISà\B«aö +Xã Ii.ّ‡ëwO­ñÑ â"•xƳBàC’Q™ŸYäág²€·ã××\ÀRÏÀ1bRBÝ6 ¡¸Íx1øPÞ/Ôòjüü4†?œN¬ÉhbFŽ­)RÂíšÀåy*`¼öËwHbÙêN×=(ètÍ –å½®:˜5-Žq˜– ]weS« +¥ïðÀOj2wr0iðe×wff ·›^wp3øfÒ,–MëvßÜ\ aY­:Åqÿ + )–sƒCUkµé@Õ ªå\Ï5LU¯ ì ¬Ñ-VU_.«RO ¬çºF(õT·e}7„ÛUuSe þIS5m·T}õ-*÷C¸_tkè˜M§¸L7Q.CPÂjšÙj: ¶§™³=s¶gõçOJ. šy؛ŸIy8_†ù¶ÊÂ1{ zæV‚Ÿ½U’¾l+–»•(䋶’ž½UÆ_¶Ušž½Ï^¶=ûX¤ÅãÇ¢Èh‘Èý™%\žy‚Ó,HV‚Œ b&À†žàeQ0*$‘‰LÌ>> ~Óý¼™ÂkxÓ÷m‰>¬wÁî$+Žž0¶ÅŒ1š’´¸8æDä„K±ùã¼i{øcF[.{ "û“D)ìÝÆ^¿YÁœ~)šŽñ-Ð'¢:§"Žê™‹êY‘P.”“I¥UûWÙÏo7WˆkÿNà4!̨ÆÓÁF£Ÿ à8£õgÁ]ùÃêU­´¯<ñ¼ø…xå"âµnË^øýÝS´>ª/z•²ÏõÌÊJŸMGG‡Ó Ñ1gè€NiîŸþýµžôO1zoE¡ÇÑßÉxÇ*o¨ÛfUOñÞ‡û³é%¹¥'Ó ±KŠˆ]§ËJw§X™ ~—¸»ÞæUåižM‡¹^ʧ8‹èÌOùY—ws›1 ¹ÔȦ,Ï#AÙeIÐ80¬O‘ø«œ¢ï¿ƒÉE9ûSþMš¦EgP½¶Tšå¨Ò³Y[ëÖç´Ùô|2qY>Y6§ø|º8Ÿ4¿,Ÿ4öûúŸßW‹[{Ïï—8bOÙÙ$¸óö,»‰$ööûötäºÖ]S­zï"¦e‹QüY€`ì²Xìë›gQøô| +i‘\6‘I ñà†ìu»l*e3·c3l.›,ñNœ¨¾i·|8Xå3øÿþñ8±°Sãs÷ï*,îã +n+„|í¶òB¾@Û ù!_ší„üÀ‹€‡}¡Pym…ÂÀŒ€‡¡,„Òx8âð0p Dcàtœ§‰-BY´ØŽäü!¨0¶³ð¡ÝR^( Š×6RÑģ۝]{„æMš‡æ ¦íªëà͇÷Ö)’L§ÀNÒ¯ƒ‰}{õí¾«qADF¹s—]?iÄüøÈL¶…xw `2ê¦Þ ì ¾õÄ$XtI÷¶ÓÕ,šˆëÚWCˆ&ڍYæÞÚRã{ÍM=Þ©ñˆñ”ÈÂü©bH ÁwÒÒ²W­ÂŸïŒ +²‚8ôãD&³Ü´†,ˆCŽ$“ù"vº?PÆ­h2£{¸¹‚Ñþ4›ØÎ–M¸T}§ñuß ±( ¦ˆ¢œú›m_ٞš¹<õ®é¶.1Y¾µeÍ=LÒ ×ä2<\²ŸcͲbÏ Ë Ð?3³}ãíVƒ—6n¦Ö#,lša‡­¿¹"XîζÕLCŽoP±5 +e¶c9â¹= GóüðˆL%:*‚tu€!húp»='‡¹‹ªÝâˆÂ€¿hڍQ™2ÊéÊÿt|ÚI v œú×6Ñ}s—µ¿‚8eq•AYn¡Ú®¦kMÎʶ뷍Nc°½œÆ²1‹|[¶êaÙàé@õÞ7F¥ÖºÝj2Ñ]‡ÕÜv=SÚíõIǶ‰í“Ö .èS&s|°mYcßOF‹}PTÎê¶ÓŸW¦“^ VU³v'ÇÍ·¥~ÝÀRMMõˆP~YM×x/¾}c¥D9®_:¨î8-4ÚièÍξèØÜ+ځؑRiVõw·µ ¡¬ÚVm *ÿ}`<‰̹wBl?áEëÇüÇ:ÍúTÉv!¤!–=Žq~ãüdmüÕ@Þ? +ò‰šñkÜ< +ðӓq Ì.°S䉻1pá#—)‰0‰—áüºÑĊ +endstream +endobj + +422 0 obj +<> +endobj + +423 0 obj +<> +endobj + +424 0 obj +<> +endobj + +425 0 obj +<> +endobj + +426 0 obj +<> +stream +x½VmoÛ6þ+÷mös|%1@?¤Å–e/Eë +Še™¶5H”*Êuº/ûë;Š’cÓMŠÙ ¶É»çž{îxä „uãgQ·d”ÂÆúeè6ð˜ÿ>~5¼Ìœs+Ù¼+ƒQbÉ!IB…‚¬ž-u¿ë ôŸZ}9Ïþr^bôJ(IS•¢g¶š•¦¶9•ãöñ”Oœ…Á}ø>ƒ·ÈU !Uä⒔Ò$eŽf$‘+HDL úE<&1þf³Ð°þ$"“ƒ…ˆC‹„1‚АðhÄ G»_VDŠ`ÊdŒÓQ¤Ç<Áµl?{½«ïtÍЦn£Mo¡Å•¶¼×l[Z0Þh¥[mVTs«¡Ø6Vô«šÎ¶y¡/!P¸*0áõ}õîÏëåÕ{j¥Žò6/€_„ k#Ù dyýò ƒc¸*-¢T‘1Œã¢ÛÙÏ¢3lԚ1¥د~}ÿó9Á‰›$ãÖÔu ÔT%h¹`©3y¾V»ï´ ð¿Oföcӕ7¦Ï+ì +ÛT»¾Ä’”Æ×̗¯4Å6¤ËñpD*~~¶Ÿžbû›îú²ø×ÿ‹ji>"Ÿ›¥.zb&܃ÜÎFŒ)„ BP:†°ºZ‡d#¿uh¯ÉQS6¶nÙaÌÀSÐ1ä<È +µ‹Hª& o֐WÕ¤*ø0†é))SR|Õl`Iƒ”rš–ƒ2§È_s’ry Ià +y}_èÖeÓnsÈq°ù¢èÕÙùJËÃùú}ö&ïòZ÷º³—s”†R¯*ÌÿÈ~:f£I¼²‹‰çÆÖƒ€î×àOàf M]öÈábXÞo›j0­óö%ºÞ…4{ ‹¥zlߕ½~óú:l0ý§ ¶.+mPžÀyš_O;Ûü£ +ò⇼²!†z´O9žÔh³³wˆù¤[nÝw³Çìl1…’Ó ?®ìiˆ4&ØÊý`ÄDž¦~6»QYy"ºˆÓϊÞw¥ÙÞXÕhòžvR†qÕ7`‡t-ÜâX)ªÝ +!°¹{m,¹Û9n"¼c”ÇDÍáP0 ‰—”˜&ÜÓiÜ5Mb¼¢Žsð%â4˜¦?ïf•àÕéº9gªîÚ¦Ê{ý،wEÇî´ïgWàHèÜÀºÊ7`uï” +ZO)ÂÓqÜfÝî¬nb»¾\û“Zç}8‹«.ߨY'yU»kd"Šê_¸ì>œã#1NÇù0tûYøä4¼]Zÿ²À £üäeQtåY|m{ü,7Û»f‡DòºE–›gº‘Ç2£óÏ܈sÂ1¼bªÜÚá:†$ˆÄþÏ~™q5ÿƏô·ÿïDö€ +endstream +endobj + +427 0 obj +<> +endobj + +428 0 obj +<> +endobj + +429 0 obj +<> +stream +x­UÛj1ý•¡/±K¬è~)ô!MKiéC.~h!`[N6ػήBí¿¯¤]yÍ&q!³b4sæhæhôÀá×|g«“K‚1ÜVÞd¸¡Äô,Ê[`؇ JaŠ˜AEœ¥J ‹À௏Ð+Mz—ß߸ +¬¨àZû•TáLb̓ ‘yúÌVðe|rI…Âa¼€úD8F¸NÃöiÆ«Áy‘ån8¾‡oc¸¨ ð$fÁÒ)8¢{à1C“?“Þ}<”v]ÚÊæ®‚)¬£g–ƒ»³°^Ns{ s»Èr;‡›-dÞiÓ|[˜E9Ïò©³ÚÑ%ñçéòT?ÖԏÔõ#7=ðÕVÚ`ٳ؏'ôi¼NñÏRIÃ_põâØ0½¿Ð’Q} U.“‹T‚°÷Àm†ÿWÁ¤Ó:"0RžÏ“Zwê\™Ý<:Û*âÕÃ0lò×`G•c‚8ywªL«HqêÕ]Q:øj«Y™­]Väû”†ÃÞ …Oe ÷œzdºãù¦òEWù²Q>QO”6éSÉñý‹ú{¥ÿµj>”µ4ê]YKcž²Þ¾ÅúOkŠ„n~úÿ}CÇϳ®ÈW „¯zkXú™Ö½t­ScN ³¨°tÝV;‹âò9Tck¡zœZ¨Æ)úºØÁ«؛î`q§*•N-9[N« +NÏĦ0©SSü8ÿ5˜ÅÝáÑ~©@BbZ·k7ÔG¤1Bä|0™dyæ&“4Ñôùz7Ú1Ïóo“®w+»\tix À˜—4Cówâˆ>$nûbÜõ°#×!qá¿4ìŸyåÊǙ+Êø¤L—.ËoÁðÁkXڅû€šÜb‡S¡6¼¡­ax<p¶kû©ûÂi$ý­ªù-–ÅÔ=‡'ñÙQO÷N9ÂÙU¦J!£X,@Ј­Â¶Ò &‚rÕ÷âÑó ‰ +endstream +endobj + +430 0 obj +<> +endobj + +431 0 obj +<> +endobj + +432 0 obj +<> +endobj + +433 0 obj +<> +endobj + +434 0 obj +<> +endobj + +435 0 obj +<> +endobj + +436 0 obj +<> +endobj + +437 0 obj +<> +endobj + +438 0 obj +<> +endobj + +439 0 obj +<> +endobj + +440 0 obj +<> +endobj + +441 0 obj +<> +endobj + +442 0 obj +<> +endobj + +443 0 obj +<> +endobj + +444 0 obj +<> +endobj + +445 0 obj +<> +endobj + +446 0 obj +<> +stream +xÍYmoÚHþ+£~)œ‚»ï^W×m¯:µj¥6åÃU—Sä€ ®ŒÚKÿþfm/ø!ôtBÎzfvž™·å±Ÿú{²xqI Û—0ìxÈod‘ +|Ÿ0 ™džÀ˜†<‚ÙoÀá94!¾¦;.ÿ|HÆW«“Bk|`Š©@âWD «Á ¥æîk²€7ã—L@ŒgP!¢ ˆGªm¸Gp›ñbpMÌpüލáK…ÿDÂíÊ.‰Rx¬”ˆŠùR +ŸïKé€â­)+/ Œ§ƒ<ºË£"JM!"˜0½M"˜F³8¦p³†_™ì’hf L§åÂMfL¶€<¾—òê{’Ð@ò(hWÂÌ߇âs§Ým'?°âØ>ùÙÍwD^\@œÂ$Ëòiœ†&*^ÂÕàj°"°&Wà üoE/¬#.‘û0¢ÌqM¯†WCϾ¢Æ­¸ô‰t.¨Œzƒ’£ '*LîÃua7¼ ó0I¢îc33Ð Å2Å?#økTšüÛ(\E…¯.Š ˜XÆ)v+©ÉÓ¶Rì譄ػU HÀuóA+Îô‘¾ª¦hIA™O=ªÀ&i·Ãã“$%´¬ŠbŒjßîSëàSdæÙ´€ðژ<¾YbtnÓÁA\TKêq¶ÑS ŠÕgך3é1¡šZg¹?¢b’Çw&ÎҦʜË@@£ +Y8 û¼ÔÕg(™mô|°8ÈnqPUqP¾t:ÙläåÙ2bnv*åGÑ£ee˜ä‘M‚eF*6ÓçHõÌWž¶þØêôÞU©“Ó;FW [2'YjBÌkémUþ¶•Shë]VlWß¼"eé•qEOÝq?c¯ƒQlU¤OÝÌþ6K ƒ'&Ë=ÏË+p™ø&Kl{ïã$ªÆ¢ÅYo§o¯VKn¤ç¶²¹îúŒ+ç³¾bLmtú5-7•Ó³ñ\-7Ž•ÁK¤†L3mz-ûî¢ç·ÀšÐ։oÊu~ó:†C·á±¶7-ºâ4Ë<³¾‹^žÃ0´AEË0¸ëV(Y§`7vi·ë[59&]®Æ‡Úi7§zìöê;À + ô8P¬ù ­Ë·SYw6xü± ðù-™ýC13³úÄü=øŒ÷k‹ÈàÅÛË!Z‰:âŸñ‡¦xM=¡‚ 2Àh„÷lg€BEU»£ÑSŽўP“ÁMË/ÑñeîØœ¸ÐÞ:ô Ä®´õæ×…”mTØI7Ü{#Šn\¾„ +Ff¯R5EI +Ù¬¾XuéÞýX†I/Hì§µ«J0‚Þ "؞<Å0‡ø|OžjJð±AÖ.gIš~-¡å¯#¼o¡GÀ­hWy¾vÅñ®W{ê—à퇬ÍÙÁ4íI««¨ ì^­èx‡†‘l¢ÿ ×û5üöÿа?Ø´áMB*Ͱ½‰Ï±{À“mšme«›W_ û‡mp9u?|ù¨‘Š +endstream +endobj + +447 0 obj +<> +stream +x½rà „_e»È…?A@ÊdÒ¥q†&¥"#ÙËĈÆo΍+3Üí~³wgœN­Ãüü%8Ç´¬m¤ gˆõ]Ë0ã͓LPǏX­å'$SVÃørðsã/áuãI®ª\YÖu΋ß5ã1öù*\WA[ÐW@+ i.âŽPîVH}·CŒiw8õ9 ŽÈû€Ÿ˜sœ‘Ó>cˆé«€[b]‚vNŠ%ÊÃ)‹A¬»YDŽQ4{?öËšû´Ò🍒›'|xl±ý…XT +endstream +endobj + +448 0 obj +<> +endobj + +449 0 obj +<> +endobj + +450 0 obj +<> +endobj + +451 0 obj +<> +endobj + +452 0 obj +<> +endobj + +453 0 obj +<> +endobj + +454 0 obj +<> +stream +x½YÛrÛFý•y3µ%"\ƒTå!»Iv²w‹©òƒª¶†Äˆ„7CÉڗüzN®I]"%%[„€ž™¾œî> ~a®Ã™K?Ýç&ÿæ#w]¶mp+bÇ'.ê-ó], ‹"×sü˜…^è9AÀ*”/;*pÏ®…ûrz!…ïÉ'z5½„o%xÌ}lÀ(1úžLîò„¸ç;±Ð9=„Þk³+“–Zâqà¸Þ §ÇÒ å«ë金ããȉžW»²6ìÝlê´2iYLUöý0Ø$×Éë½OÉÛäSßÃÖÞ è£ùÎóOtùÇ£yþ9ø¬ÕÆÐß׋ë‹Y:"—aŽhsòÇVÔ&$¥á›†U™J ›O5JÄÑë%âøœQÿZ½÷Œ²ÅfÑrvSÖ¹zºuQüÊÖI~κŸ¯þóïXG˟kà­užx%ë„κO/ +ݧçG.è +˜ûZ‘ Â#Û­êÍî1»®¬é~ט:-¶dSoæ`‘ç„à œýŒÿŸ;MAæE;gaˆB2ÞÈ@æýnênL…úN7õ7„ºv6 +u7„ºF6 +u7HÈ |[£ûn•w¢àx«þÞ¨ù ¡q«þ¼À}@h¾7IÍž<îÉí¡gB˜Ð?2Õ4ìûo-$|!{H€˜½[lìӋ7S¤y¡ +xłeJϖ¼{²¤ÅÉbRÛ{žÕ+u˯öÁÈـCpMÙ>]«&Ý|çÎÖýÚ9Š—ÜE÷óñéá95R)#¨½º;ÈU›¤7u™޳ӑ}Žø‡¦sD»q+T®?ëqØGmöuь¶8) ›2¯2mÚÃö¶}˜èµÏ »UÙ^c?¤ÜL!¸TÀ$o´É…ƒ¹µÉzg®œ/¬D«\Ú°}£“ËÑlÜ©­¦:a +jee£é¢*›&]g*Y +Ò[(© Â*±Öԉ®Ùõ”Õ2Ó7†äÖ¥1e¾¬ÓíÎ\_\2U$³H-{m:í¹ã»1žBù}‘nÊäÈ·ÈëÀ“q«¾.H$qØj½ñCï²­EåÞT{ËÊçA”Ì©sÁXÎg' ·¸dÖl­³òÎa¿6ï[º¡ÈpŠJ~¤zLÖ Ž¯êò6…ö61ãë;? dòCU§¹ªÓìÞÖÛD¯÷Û-9¼Ú׈‰n€2n`<+J¦ŒÑyeC¹Ö$š«ÄÆM%;]·W6~fŒ)AG¿€7æÂ§³ýeëK”÷Æ@ò4NºÚߨ¿º%„ºùᇟ؇(ßTz“Þ¤´ÂXµU’¤D8/Û-Òæâ ¹çnA¦L@Ù÷˜†ýºúi)YwÀ §¤VU‚)<›ÞïéT›3×Õ¦qh¢1wî*P¥j•σ 8Zbf°õ;h|;‹0ÊŠ·)]?ò"²çû½Õ½îɞ»&Üjóô¨Ò!NÀ;Àp<_ÕdÐU’ΉûŠÇ¨)'· +!Hº©´M¤æXPì.ðçȦ±ژïÊ}–9@yÒ +é°Zª=t¨¹¯ô܅•é—ºpŸÍώÁ(6Н$ÚªÊH…ùNQ'iwjÔ)Ží€Û·KŠÒP +,­>ێŽ›ŠðÆ´•!ü9ý„X#é'Âî;í'3úßUÀ[]7H'4“Â`îiXS"%󒨢•3]ª/yĝˆ>-ZÚ@­¶+ï¾­&IÚdê~,³ö…»B´²{dzŠ$ÜìTenË ¦¿*jps,yà3A, ˆü³Âiçˆ?Nìy äÏ¡!†ûð‘p†Ø×ŸGs2fº-P?7ª0h'Ã*Èr4À‹|°¹G÷m[c³¼lŒ¥=[éªüÌ 4ª¸^ðHƒ]öbu‘€Bh;Ogû%p³Ñù]â ¶]´óÆhTK4\”Aí¤¼+¨ÃنTx™¾ÕÙ%ú¢!ꅥà0À&©¤ë·ä=›â÷Í_ ßOÇÅHˆóè ÅèÎØ{>z‰:>Fnc⦅æ¥h핪ìŒxáͼ®b¼Bm„ˆa}ßGÏZ0„p +ÔoqHa@ØÒÿƒ>dißÔá·µªv¸QnTK7´Ùtzæêžiô'$X +¦Ú†ÎRوݾHjëiV®ïnˆýí 2 /€‰n&ʨnnnâ½0<³2þeÈj§|vnjzpâš0i梧ʳ¥vÜë;×.5ÿÍÕWö˜Í¹z|<±ßȸÃÄô̓Mð–ÎÅ.Éãø„¾þLß#fCóa({§ rèÖÍÜç3¸ ý6ON'œž|4~¹“Ù¸sõ8SŸwõ@¬'²·\ŃPöšÁAö;ˆ¥½ì|%T¶Íœ\’¶½+Öväjc9™šj•}¯¾¦ù>gÅ>_SêÞýàÔ,;š*¯ýÄËQ÷yx¿’žœYÚÑÈÐw¸K4²¥°Ýp>ߦ—rÛf*õv¢ó¬ºÇ»}4lm‡uj/4tÝ£¶PљÎQú!Ÿäle‡ßR9¡‹aÄð þїUxI­#I/h N>Õì#Õã'vbszo&Îl~½¸Ka6¸ÚIÑ 5¼Ÿ&bßÃÃib_×%*(Á“Š$.Ò•˜èaºý]¿Ùl°—-º½Ðu"|GAë” +"öm•nèq$'\ð–Ê÷1YZ—üò;ÈN®ø +endstream +endobj + +455 0 obj +<> +stream +x…P=OÃ0ý+o#EÂõwì„XXÚzì¥n jÿž3¡ Ѝ<ÜӝßÇÝœ ðò~k{Z®ç8$jyí¥ð3`<@q¢‹ºæ’)#dZCJ‡1b …b8Îk'fÀú隯¦¤’F;G@Zi½! ,wº$8Cü$¿”ö„û°\Kã!4ÂÓFš3>Ù(ÆÉ&œª?óæ%Ƽ¯x XMGøO–«Ò™“5šÉ?² ÝrÈ ÇJØUíÐç¦ë»”1ì±£ÚõmF&2Rþ:Ƅ÷wzúöÖ"¶Õ0NqìhH¤ÒMÛ+ñï¬Eðµ*Ç&¥˜Ê všež+¥7Ӟ«ox’ +endstream +endobj + +456 0 obj +<> +endobj + +457 0 obj +<> +endobj + +458 0 obj +<> +endobj + +459 0 obj +<> +endobj + +460 0 obj +<>/Rect[396.87 476.239 554.707 488.239]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +461 0 obj +<>/Rect[40.0158 464.239 127.836 476.239]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +462 0 obj +<> +endobj + +463 0 obj +<> +stream +xµYmsÓ8þ+ûöæªÓ»åûÖ-½+… f:Óq¥5$q°]Jùõ·²åرcú2¦Ay´Ïj¥]=Ú~JP÷ãO¥páP(CÎÂ-Òk§(  !(®8‘ð¤f€€;œa( Ûòatò+œW\Icð×\‡ +?Mt|Vx^ýš,àhì¼gÀ4ŒgP®ˆ¤„4#iƋ½—É2Ë£ežA´œÂëåí¦Qãèþø ¼ÃeTzy„ÙÆ£1ºKCÉã»§mpB2ƒ··ï_C”A¼XÍíÂ.s;…«{x_|Càt÷Émê¾OÒÐT^Àà'¡×øÚ#) :5žîyü,ΨÚï;`}·—ߨ%Øhrã|ÂÿÀ,™Ï“»xy ߣ4Ž®æ¶ÃƔ!aP²!Z4\zGpᓉͲ@×é€S"¯-µ Iÿ5©÷hØ¥Šn/nª#ڏxØä*&· øƒQ†õZXñƒf¥wJAé+bšz·1‡R½åCs¾Üœ‹õó»Tlt•2ƒ©”z$ß=?Ô4¦ùÁhÁÍÀ¨JíX ‚Æ €5XŽ6Êc’„xF§]Žš»g-\„œ¾öT ‹„?½§BkHÖôô•Í&i¼rekp¥äTµ+¥.BãT!øp9:9je< án \Úcpãû•u¥i“œ(ÊB%~«ZsÃh-ëm˜'i¶Š&q )Ó†±ôáâ}Á«¢qÀÀ0`!.և!8~ˆçЁhÆ`½ºÞ¼|ûùßÞˆç‰}®Ðf Ö«ÛƒO6Í0¿Žâå´7/Pfú£ág¸€x½x•#ÝÁËûb¿ÍY™Úä<.g‹±à[lj…ºÁ?øï‹J½vù]àE€É_Ìñ¶mß\5Hn±6ˆuAÕÅ´U Õ©ą(jmuï,Ö#:]§üXíÔPmªâãA¨mÛ¡Z·Ò Gõéqäô;î#uû8HÇï°õÔ2Jbºø„Ú®ÜQÔ +Jhx‰<è%R€ táe`Q9_ߚ–ü0¼’ü¸R(§¦ß'^í~æ·6󫙇èuïƒBØÎãåW¼`sò³¶-£t†=ò¢ÀÂÁñ²X>äX/ö ')ŽçQ<Ï`‚a¹çOP%E¨QŒíZçá4¹²àÊÎÈÎlj—X9³øG~vWZá@(´¡äÆ{ƒ‘ΓïvqeSà”êG\Ɲ¼ÆÍ@ÙR:¹÷'Ln¢U޶ÑoÀ(­¢k‹E€ÁlFvKo)9Œô–ls~ft•Péøã¨x8˜ +³œ?F¥ïހJ¥ãÛµ@°s04O¯ÒyH®Š§´?LŸ\•èŠ3‚ª¢r¯xF¤~z‰ È0¼éæÓJtɂÊðÙéù¿—çïÎ_·ø±n +…@Qœ'ÍÒԑ;ï¥GaöäÝø]¯Y'þß'±kŸä D°š;ycÝs2gšLn]3dÕÇÑi/ßdZB5ávÏ?ž¿|ÓkÚ)׳èv‰í•‹½de—ûH`Ÿ%ý­%œ¾}ýª—IºEØt–¤ Ȓö[&¿³£^㪡ÎIÁ*Ç×â¥hÊ ª’Ö ÖU5t ª6@ª R[@¼ â]PU× j Ð‹R‰_UÀÅzD*Ó]ž«—·T›ªø4ïµm7ôbÇR³[6\C©ìŒÄ‡ žD3a\j-a¨Ô‚ ÖL‘-É4›G×Y[3……á`§f*&9Ñäç­«+>Ùtà)óOu•<v¤ ÆPV‚þ†16KÓøú&_$YŽÛܖ-T¬N˜)ö3Äu@ñE–—jˆ+|ômv_g±OIaʯ>µ«Ôf®Í{•$s‹ÕÈ=è&QŽTÈnÊÎìݍ-2Ðõk£,K&qäºÇu‹8îvˆ1¡½–§Å­¶­eÛ#‘8£’HÌСb‚)ú(݂‡u(UX Ճ©tÏü!ºgwT+ÝÙ(æ9΍ÂJ<Ò=š¢Yí¦Aò òLàC„*Õtóiu3jC÷Ÿž\ž]~:<;íܼLã¶Â •‹=Ìq x·“Ìí,‡e>»DÄñ´s{Í{¸``䞑5ïÊ8À(=#GÆ£$ϓEYú¾Î£¬Æ³Š6ëƒ×z|:¾|Ó¿›Ú“J$}“¤ñÏd™Gs÷ç´õ!k'ݍñ÷´Ïðú2÷MPU× j`¤Ú µÄÛ Þ…mÇÆã\©"£ëÕU#œë® ñcõò¶€jSíi ­Ò‰fóMØ2 ±‹)~ý·ßÀõY\WãlO¨ýåÿ÷?ðÆ7> +endstream +endobj + +464 0 obj +<> +stream +xQ]OÂ@ü+ûf1ñ¸ï–G1@P”P.<’¦B[Khʍ¿Þëœ0fÓÜe;³³7³ŒàªÚs•u}‚1${ u• plþ + ®p1b=DPÄ90ƒ+#ˆo Ûs½–.?ùô*¿.Ýiîí±Ê ¯º>Ţꨚ- p‰p=SR„ÍL•9“ñËS0œÜ‚áX‹ŽÚTOa-‘<.PPk‡QX:o©±ìÀ"*uº +·§ºfY¹»#íŽ×<«áãyð:>ÿ©#y«#N¿ÐºÈ L“w Ÿî!„¯Âtâ49”‘3 HÀ‡yã£ù6­7þÈÚè56f „1Ò6¶0?Ëʂ„Q!<$É)Ìv.:¡~oP¡Žãé–:Q º½uö=ãMôî!JXýC‘ïu˜kãh¾†A~È¢2Ô©éV¦ºG‚ P‡ÉÎMcòì–®Ž +endstream +endobj + +465 0 obj +<> +endobj + +466 0 obj +<> +endobj + +467 0 obj +<> +stream +x½\moÛ8þ+„C“ƒ«’)Q¹l€—»Ãa»Ín|À›ý ØŠ­­by-¥‰Qìß¡$êÅ¢˦’µâÑÌpÎÌCSfÿ@Ø"Ë?ÅëüñÃ/c´Là-y”xš‹íÙT¸ƒ\Ç–í!N9µCð‚¶zø;²Ñ3hŒ]A4¿üû5·rT”3!à‚:Ôñ8\ØLŽàD²‘«—ù#ú4“£'ˆ8hö€òˆbØÂ™—XÜÌÏ>n6Áz¾œÏ~G×3ôsŽA¯U[¾£³Ê‰%r«³U˜ ùÊߤÁÍãuê‡ëù‹E˜†ñڏà½ÇÇ`Â{ë +^üÇM$V5ˆÃ'.n E…B¼ÿŽãð¤ÜC„i#'žŠü:}yJ7O_ü€fÁKŠ®_Ò­?—Ñ£ÏAºŠ‰ Äq9™“pýoýÌKz°€w‘šÞ,މÇíAN‰ëYœ¶œÞøË ¥Ü"¸aÖ_¬‚m 4Fé +æ3Ž¢ø9\/Ñ* ¶þv¾Ú]Ô3Ŷ¹ÇP­ À=Ã,KǦN–„1™Žƒ‹Á–¿@}Ô +’{žƒ=Y¯èß¾(mOÈL +j9y —ï +ݝ=‡‹t•ÕÂ*—«ôî\H(šýx†àçò>Šç_å!äÔ6˜§þzpÛ»L®~.#˜ñ×o+oO6þºu{6ŽHŸz>uÚ(mA¹o[¶¦ðÆËíîÎ!!ãí"\ûiWý7?zʆVϊ÷zO1ü;[ ÈF]‚fÏgQ*3\&S; “¼¸2`ðòÚÆOËU´ƒ{¶þrëoVÉݹ…>æ·4”$J‰ɋ†D’I䅜,×ïaB!Û=‡0i2Cÿ1@›m¼ ¶i(-_[K Šn><@&’œÏaÏJb´ž3'Öù»!=;^“!0+ú†éÐ?¡#R]gâ.µh³IXAÞåïwgy†ŸØÿ8·ŒåtÄYåç W©”§Oi¼ ýè]‚nþù/ô &RûàfÂÂ2슑‡‡uò)m„ÛÜbEìEX2ÊdϘգÍó.YÅÏh?eÉP>?I€nv@Ik”¤ÁÝï²WÈ˳OAž­ù}ÉÓfo¡ +Ö1€»†Jƒw§è ö£$Fÿ¿¹¢/°œ€‹¬Â¯oþ÷ å$™"¸cCíßì2s2ïó÷S£‚ÔT7³ŸúÑWäßÃ|fóõÂ-Ò`Qt_³ î·Á·0ÝÉñZÖÀjb6vN©¦}ÓÕÄl{ŸrU5ýgöùGCÕÄ AUM’–U‰ (Ž¡ËF!@.+éà’à©ˆ™ö„,½ú-A.·=–Ɂ• U¦9n +‰¨KÝ=U»¡K²4! ;Õ„Cˆ´Ù¥ÔaNC©¡SF hO…°#‚B:,‚B‰ÑC"à´¡ÔtÄEÐa=Bw K`\ÜL!즐¦ôw0î¾R#<§G(hC،€â¦êAPF{”:" ®è$¬Gèážl†G`s<<Û£Ã=1Ú ›ÐC\qރwpŸ°9¡”0È®žªÒ›‹žÊ(„MGnŸªÀ}B§)$¾á‰U(¬@ý]]~Ø\åWÕ×d²Â|w2ë01ëTVGfÊ‘yÚ¯lIûö(‹^û­½•#S,T.z+Ó§²<¡Ü7iløø¿ou$Ê'¢ö©,DUs5 + ì0ì[‡…`o£É8,û!ít<‘…#€/w¹Þ$9nBL0>mEpu +ÙÛ­qù¢Øé9¥Ú4eoFôd”—ä­>^VŽÌ=¡oAô1•Õ‘' rdœè+ÓÆˆÞ(Ø%я vIô5°½QHJ¢¯¬ŽLô5HF&úz:š"z£à—D?v>–D_ß,Ñ×°6Aô•¹q¿Êml@…0ü¥fÑ ÏÔ¬ŽË35G¦y¦fÚϘ[ñÌè`+ž©ƒmŠgÌB¢x¦fu\ž©C2.Ï4ÒÑϘ_ñÌèù¨x¦¾Qž©cm€gjæLæ1Ç(®Ó÷™òØS֝ò«<|4ìü2µ‹#aGž>êÐ7}úˆÚÕ)»½ÓGÿ½ýò“¡ÓG””çÙ¤Uuˆ¯vð(‰û•ǾyylwðÁ£.§œØ#ž«Nq}¯ïDÁzraóiÞ_'Üãu¹3ä‡'‚KOLåÜüó~2¹ø5;‘û}’î6Áäb"ÏÇN¦“ûûød9¬.ŸB‘Z®mΦÈ–#\ù&Ôv~›æÇz³µEiSþ|?Ғü™dÇhëÖN4(mfñ]LÔaÇI]úgõËoååŸ{G‹åŠ*ÿҒkˍŒú} OÃp›Ë ±]µ»ôó_xÌšß +endstream +endobj + +468 0 obj +<> +endobj + +469 0 obj +<> +stream +x½]]sÛ6ý+½$ÞI|Ìt;³~ìC»“&~ؙM™¶ÔÊ¢+Rq¼;ýï HI$#À¸lgbY0î%îå¥?ÎÂæÿÃÏùý›÷ct×è· +ÆDÁ‘nÉƹ"hs‡8æIBeÆ +$¨Äˆä£M…nÿ†zÔ/¥ÄDvq&ŠBâ½ÿi܇ýåO"²}ø1¿Gß]¿yO13ï\ߢý%ÄyƍÁ<׎µÁëû—¡ß®®G„¢ëŸõo½ºz¡üoÖ>=T³·³¶úÒÎ^Í>}ª¿ÌÞþ‡ã ‘‹W(/2†1übú]Yˆü*X†¹~ó·½4[-×U£ûíEÆnœ%ó߬y(×G֞iÐØÜïíìz±lP»mëͲ\¡Çåj…šEýˆžê-jÚ6ªoÑ/Ûwßÿˆ–kôî©]ÔkÔ´Õúô´û™Í|Ëu¿üæ^ÞÜ¿±C:˲«è‡kô«¡/()z^è9Ö#ÚqA(¢ö\4ãQÌ|ò(túBsé+ý?bSÁ•Ò/¨¤4ý‚I¬øÃD±#ŽÇ°Ý,7<£{†]k„ß•wU¦nÊyûï_~þøòã•!Ý~Ü_¡19§±u"E¦öNþ¥çÊ̓6Ž>W›fY¯ßvÆ()r¾‡D¯g¦×£ô’ÅÙÛûƒ^jûfÁŮ٬\j- |ÜH¹ßªNZÙQ_²£áXv/ô`‘º—ïÏï4€Qô¨§x¢”‡{¢ê´“×ÇaÅØV‡FväNɱ֯auèD©º`Œó£^ýC dd‡Æé>´^8¼¿kò²!0uÔ«¸¡q`‡Ö°!X“a³âŠåǝ +y'žËpOw:ÆiÀ“`ÇHôN ?èLï´ØÆþi±­AÓâLM‹íE.Q&äQ§‹”„Sî‰3îI`îI“ËSèŸc&‡;ÔÈ6cØqh c‡5ƎC¯0v(ÁŽOŽ!ž;B<9vØNAìèc&‡;ò‘Õ6°#gìÈc6VÛ+Œnh!ìñäØâɱ#ēc‡íĎÞ9fr¸Ñ±CŽÄ,¶q€2"fq&ÃØqèÆ7´v„xrìñäØâɱÃv +bGï39ÜèØÁGÂAÛ8À:“aì8ô +c‡Z;B<9v„xrìñäØa;±£wŽ™ntì`cQ)JYLTÊ¢¢R•²˜¨”ÅD¥,&*e1Q)‹‰JÙXTÊÆ¢R:•ÒѨ”ÆD¥4**¥1Q)‰JiLTJc¢R•Ò˜¨”ŽE¥t,*%cQ)JILTJ¢¢R•’˜¨”ÄD¥$&*%1Q)‰‰JÉXTJÆ¢R<•⾸SᑮÖ.S#ñŒmì·ÛÛÕÙÍGvBÛx|+×Þ¼ìíêìÊ µý7Eû[¿rSÔv +[%½Wé† Fºmì_è¶5h¡;“A7E{¯Ò °Ñ6 GäHÎdØ,„¸²7EÝÐ.º)Êe¸'{'µ§¯hU/ònZèØb¦cɉm ›“œØ^A[H7´€-$ȓÝBœ'yÑ´ÈpO‚¨ãN—m!§@øsÌép£c“d2–œØÖ0vX“ì‚3qÜë2vHzÔéBv¨pOœ_Þeì 4ܓ8]§‹ØÁO8b‡ntìÀc+KNlk;¬É0v`Á;´‹$ÝÊ@ˆ'·„xr2âÉF’®Óeì8öt<ǂ7ZvÐb$f±ýì°­Aìp&ƒvÛ+hg醰³y²”ržBv– O–R®SÈÎÒ?ǜ7:v¨‘pÐ6°CE„ƒÎdvØ^AÚÑ -`g òdwç)dg òdw×)dgéŸc®†;äHTjØ!#¢Rg2Œ²ˆ`‡,Âw– Ovg òäd ēÝY\§¥Žntì#Q©m`‡ˆˆJÉ°EDÜ2³ˆˆ[æÎSÐÎ"òˆEä;Kïs:ÜèØ1v^Žž´­aìà1Q©í¦\Dì,\Dì,\Dì,!žÜÎÂEÄÎÒ;Ç\ 7:vŒ´ì`1Q©5ÆÆ"ØÁXÄÎâÉí,!žœ „xr; c;Kï >ÜèØ1vø”Ž>¥1‡OÉ0vИ¨”ÆD¥4&*¥1Q)‰JiLTJÇ¢Ò±s½tì\/=×KcÎõ:“aìÀ1Q)މJqLTŠc¢R•☨E¥}æ‰&Î +Ýö•g‰ÄÀ³D‚çƀy㛇ò®ºø¹ŽóǓòs“èØ\– VìqºØªÂgV—7íâïÞó!9Ub÷ ÑiÿîїËGº‚¦=9š‰Bd4rö XH~f7,DÑS«‹jy·h“ãbâüS\'™R…:Â%ŽˆFìOÌk¬¾6ZöÚ<žtóò›O«zþÇ3æÀ‘ˆ‰”Ô”gVÍÓlpÌìüÌìórúÍ,gæq9T¨Lª\¿¥ ‡e2Áì0ÎO]ïgÇì%vvÌc‚)tƒ2Ýè¬MÞ#Ôù™lr˜:u}X:z‡±“cžºL19„@LNgvr:?ᓈ˜›•¤ˆ¹…ØY½­×é՞åg~fÿ¬VŸ«v9/_W¯nžƒ Ï16ÜÕfùß*96úFÔ6$ÅBÖAã©áýBæJº…<_”› ™ÀBö¬‚.dÏOßBæE–$ñBN‹˜]ȞÕôpé;ºƒp=ký ‰¾•|jõ)9$:o<ƒD«‘øy,1ÇëÓC"ðßçéõŒœá>»N(gžÝ7pz¦ ²MÏ*¬žu~fN¼:=E&dêÀ$-bNÏ:«€zÖW=K +‰Ó³Î* žy$Õ³¤8=ë¬ê™É6¥žuvõ,z–‹iô¬ó3sâÕ陾 « +‘Zϒ"æô¬³ +¨g=p%ѳ¤8=ë¬ê™IR=K +‰Ó³Î* žy´)õ¬³ ¨g⮞gVÏ:?3'^žéAÊ,§Ø$ñ,‡=R$ùƒ¨1yVa×0#ckXPóVÒ5Ì jLžU¸˜¿®1ZHlÌïY…‹ù}H + ‰0Äy&nñ²´E¦3¤!‚þ3†':¤}vñ6è/hrA#E&Ï*¬ ï) +«^ é*Tú-–FѬö$…Œ‰³¡(š¤ƒx=KÑ(€„Suj@Ñø9…)šÀÄÞað¬(=cøl‘BÑøÙÅ;ESÉ C”™<«°Š†½Ï8pòåIË +œ,F³Š–2§h8Ÿ@Ñúðzž¤)L\ü„! M6“ó0I#i„`ââ' Yibçë*I土]üAÒæ©%·õ<« ’æù™9ùòM¥T´C֙1«“žU¸¬³®YgZHläçY…Ë:}H’fi1±‘Ÿg.ëô1ifž]¸¬“*ˆ[ážUXAëü̜zu‚&A²Î´Ùͳ +£õá•"ëL ‰ ü<«pY§IÒ¬3-&6ðó¬Âe>&(aÖéمË:iQð¬Â*Zçg&{²N©G)Sgi!sŠ–“ ­¯YgZL\ü”ø¬ÓÇ$i֙?å>ëô1ifž]À¬S@”<«°’Öù™9ùê-× Õw¿eÚ¬S@<âYÌ:{àJ’u +ˆçO<«€Y§(€²NQ@d¢˜ ëô0Ù¦Ì:E1AÖ òñǞUXAãGìÕË4š)“XÉ´1‡¨ xVc´¼’d<‡È:y>AÖÉs ¬“çY'Ï'È:=LڔY'Ï'È:Ha€MTð>5»“/OÒTÆÒgI!sŠÆÄŠÖ‡W’¬3)&.~bb‚¬“  ¬3)&.~bb‚¬ÓäN™u21AÖ òYót¢Ïš÷ü̜|uЦÈH,i¤4@§( ôà•$H£ ¥:Ei€B•(Hi€NQð0Ù¤ Ò襐/h }AƒçgÖɗ'i,+H²iVÑHi€LQèÃ+IF@JdŠÒ* Ò™¢4àa’ò@šgw/iÑ›íu†tŸe™‡F¡ïБ¢ç…ùþ8}§s×Yj¡Õz¸ùª\÷}k‡/Y;ñþ§¯õÿ°ƒ^XéTR¹› &±âcc"ühŒ»‡zŒ›<Ãû1¾¯šz»™Wè}õçv¹©î«uÛ ú]W_Zô×vSÎÛe½F¿Tí¢¾i.F˜œ#l½ë™£;ïzRð‚çèúñåõB¥¯µÆsÕy¾ß{6Už²^`RèÊmÈeP-ô”^…vü®¼«Ð3JMAÇÁ.n–··Õ5Ë»õòv9/×íêé-Z®õÀ7÷»a.×·õæ¾Ü¾]TO¨Ù><¬žÐǗMU¡òSý¹úxõjÿÜ®©;è‰Þïᆔ$Wš'ÚO¹¾9²º±S½ñ¦:C¿Ôša¾Gý§sý‡M¥‘/׍¾‚²E÷æ¯6õ¼jšåú-kæG¥{_C /–w =ț²-Ñçzµ½¯L—»j]mʶºÉ®^ ¢Ì_k¾3Œéócm&¿iý‡j³»¢õ¼z…+´(?ë ß®QS}Ö&Vš åýêBï¾ÿÑ\â¦ÞÞ- X»ËÞÆ\—f”þ·Ýqkµªµmãz¡Y®˜¢ÔøöÈÐõB_ëîÒõÏuÝ¢¦Õ-yžu¥§¡©æ[3n n¹~Båz~ß6-jË?ô8[TçhyS•Úí=Ÿê-jõvu£¯ý¡šë¿®¯^˜5`¼Ö7‚¨ÌŠœ™PìÕúfùŀš+ž &Ì0e?Ïà×ÿpø" +endstream +endobj + +470 0 obj +<> +endobj + +471 0 obj +<>/Rect[448.416 763.039 494.888 775.039]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +472 0 obj +<>/Rect[184.516 540.639 213.966 552.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +473 0 obj +<> +stream +xµWMsÛ6ý+ÛSä©Ì$H‚žéÁ™¤IÓ8_ÖLzð"! I(hY>ô·w¤dYNÛ¤nGÂPÁýxûö-ôâˆAL¯ñZµO?²8†¥~»„/À†Ïã¥jáٌ¶¥ôËlã xÅ,Ë¡(Š(NK˜µh·d¢ˆ3˜m&ç$4fª«AÝÈvÝ(Ø(ðÆ|¿ÒZÙõ²yâàýóŸáZY§MWžýk¹Tn +^Ýx0V«Î«z +†ú볫“f++½\ã•têdö;°fo(†8‰ãœbØH絙«ÑÉà}°sùº“4‹½ãWÊâ烇 X'OàÅ >ŒPf1+³ôû ã"⠡˳èöa_®Õgoû(«;Oßç¢ä‘â¯\´r)ou‡™y9åî/8%kH"'E؊ÀVI –±£ï/•E,÷E3 ¸¬@½Ðœ·ÊêJvS )¾_ˍö+$…'·PâÄ hÞº5.Uԑ|ëvxsó’ KJV+¬ÏX„§ó¢€ç†8d3Ž£$Oò2Ãi þ7°%Y¼}…ÔŒ°MÞ+»0”n¥SÑp^&Q>ԃE\ð¤ <|2+\VVžð]!u»¶æZÕàô²#œeç›-‚=q›Eâ8ÀöëZzÝ-‡6«MÕ·Xé‡þjõÊ,ÊÊ1†·h½«¡ÎW'S@#»¬ó ñÝí" [°ÀȌ…¥UÒS‡¬ð~Á ,c¥eƒMv>H¥U~ej‡Õ ^Ék…:Ñöñ» Ô‘ð°Vlü–ÊÌF:ùeAêá”–&ËÎ?4=ÅöÄÊÞ@°ˆ•ä­žŒ›gx½šÌ¥ÓÕO3Û««ê´{Uã|xYǦÁ£ij°ª (º•^ŠËøŸ:M“Œ|RÄõdW—âìÈ:¶Ïûƒã»h“¨q,†›WŠÎàÕìâ ^^_¾{‹—ß.Þ`ĐŒá·CsgÀY_ù:͊(I1Ä1¨O +:e3wÊ^«û ì#g„áÿ0­W–üS°ØÁ<˒žï:Aó‘#ºV |Ò>Ö¢ ¡qÈÏJ9g¬#Êrxùêv +^>ƒçDi*äB/{qdÙq.)R¥Èïs…bŸ<$H!(46l­ŒµÊ­MW»AKsÔ „ï´”EI™"h©ÁFÚb[bsΕß(Ց^Ꮤl†ô®È +²ßݤ)„-ì¼nä·ìµš‡­éq u;ö‡+6&¶]­¼Ô ÊiÈáTäiÉò4ˆj´ÆÊ-÷mŠr}J^a7H]°ËÜÐߨ¦ºëý.(|˹é=Óë'ÉBº½)خ݃þCua• +C¹Çä´×$ó*ZFŸ®Lð(ÃéšñÔNÞjo ¤ÐQ™ß3…b³odwŒ-Ó"áƒÉ¢bQzòL¨5^_FëmPx$âîPTk«HM·Ó°”Äã0À´…ÍJ£VÒñÉ ‹Øy–¯;ý·ÓZ“‰³#眕dy”ÝoRFŽ+Ç%ŠÊ8Ì@J¿ëÐÅ `÷fg=ñmö@»(kÙôˆîN›ð¸´‰‡“÷, ÌÏÓ(æ» )¶A|\¦9 k4½®Îö,ø‚ˆçCÝS†jŸËÊ> +endobj + +475 0 obj +<> +endobj + +476 0 obj +<>/Rect[60.0158 708.639 133.366 720.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +477 0 obj +<>/Rect[136.146 708.639 147.266 720.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +478 0 obj +<>/Rect[60.0158 690.639 148.376 702.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +479 0 obj +<>/Rect[151.156 690.639 162.276 702.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +480 0 obj +<>/Rect[60.0158 672.639 126.146 684.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +481 0 obj +<>/Rect[60.0158 654.639 130.026 666.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +482 0 obj +<>/Rect[60.0158 636.639 125.596 648.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +483 0 obj +<>/Rect[60.0158 618.639 123.366 630.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +484 0 obj +<> +endobj + +485 0 obj +<> +endobj + +486 0 obj +<> +endobj + +487 0 obj +<> +endobj + +488 0 obj +<> +endobj + +489 0 obj +<> +endobj + +490 0 obj +<> +endobj + +491 0 obj +<> +endobj + +492 0 obj +<> +endobj + +493 0 obj +<> +endobj + +494 0 obj +<> +endobj + +495 0 obj +<> +endobj + +496 0 obj +<> +endobj + +497 0 obj +<> +endobj + +498 0 obj +<> +endobj + +499 0 obj +<> +endobj + +500 0 obj +<> +endobj + +501 0 obj +<>/Rect[60.0158 173.039 137.266 185.039]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +502 0 obj +<>/Rect[60.0158 155.039 141.146 167.039]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +503 0 obj +<> +endobj + +504 0 obj +<> +endobj + +505 0 obj +<> +endobj + +506 0 obj +<> +endobj + +507 0 obj +<> +endobj + +508 0 obj +<> +endobj + +509 0 obj +<> +endobj + +510 0 obj +<> +endobj + +511 0 obj +<> +endobj + +512 0 obj +<> +endobj + +513 0 obj +<> +endobj + +514 0 obj +<> +endobj + +515 0 obj +<> +endobj + +516 0 obj +<> +endobj + +517 0 obj +<> +endobj + +518 0 obj +<> +endobj + +519 0 obj +<> +endobj + +520 0 obj +<>/Rect[328.91 489.839 395.04 501.839]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +521 0 obj +<> +endobj + +522 0 obj +<> +endobj + +523 0 obj +<> +endobj + +524 0 obj +<> +endobj + +525 0 obj +<> +endobj + +526 0 obj +<> +endobj + +527 0 obj +<> +endobj + +528 0 obj +<> +endobj + +529 0 obj +<> +endobj + +530 0 obj +<> +endobj + +531 0 obj +<> +endobj + +532 0 obj +<> +endobj + +533 0 obj +<> +endobj + +534 0 obj +<> +endobj + +535 0 obj +<> +endobj + +536 0 obj +<> +endobj + +537 0 obj +<> +endobj + +538 0 obj +<> +endobj + +539 0 obj +<> +endobj + +540 0 obj +<> +stream +x­XÛrâ8ý½…lU¼ºùö˜À„°a¼³;»l¹ŒAc3¶Ée¾~dŒ/ddvé´ZêÓ§­ßÔ€Ùoÿô׿O„à!C6µ1²%/ñ PLÑ `š:Ԉ °nMφAÌÀò7@À³˜`AhZHò2éq1Íö„ujYâذuñB hÑlýoíö]<ü5¸q²½#€!p–  +5˜¯bkP¬â¬;ƒpÁ^.ÿÁ|>5NJÚ&1¤Eœ­¿Öm€°4 +jQ¸U»êÙ:iå†d#2¯ØÒŒ½W—‡±8ÝQÞØ…å ‘<<ΡÆÞŠ'ݬ‡[Ô¸·#±\c‘ñäϘ7Âg$ž@Û/Ç'ž@½Èѽ²ÄãêÄ|d¯ÏQ¼H>JeÚy¸PA*ÿ­´‘*Ç“*¾ZEüCu¤âò–¿ë ¥]ޞÍáàã½{;¼î»7î—ëá ×Ø9T–·ǽ;ÑîË vÓWž¸ƒ©ûÏx<:ÉöT¯Î)–ý±3>n1i?R?ÿIU¨¤ +endstream +endobj + +541 0 obj +<> +endobj + +542 0 obj +<> +endobj + +543 0 obj +<> +endobj + +544 0 obj +<> +endobj + +545 0 obj +<> +endobj + +546 0 obj +<> +endobj + +547 0 obj +<> +endobj + +548 0 obj +<> +endobj + +549 0 obj +<> +endobj + +550 0 obj +<> +endobj + +551 0 obj +<> +endobj + +552 0 obj +<> +endobj + +553 0 obj +<> +endobj + +554 0 obj +<> +endobj + +555 0 obj +<> +endobj + +556 0 obj +<> +endobj + +557 0 obj +<> +endobj + +558 0 obj +<> +endobj + +559 0 obj +<>/Rect[60.0158 420.239 137.266 432.239]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +560 0 obj +<> +endobj + +561 0 obj +<> +endobj + +562 0 obj +<> +endobj + +563 0 obj +<> +endobj + +564 0 obj +<> +endobj + +565 0 obj +<> +endobj + +566 0 obj +<> +endobj + +567 0 obj +<> +endobj + +568 0 obj +<> +endobj + +569 0 obj +<> +endobj + +570 0 obj +<> +endobj + +571 0 obj +<> +endobj + +572 0 obj +<> +endobj + +573 0 obj +<> +endobj + +574 0 obj +<> +endobj + +575 0 obj +<> +endobj + +576 0 obj +<> +endobj + +577 0 obj +<> +endobj + +578 0 obj +<>/Rect[328.91 733.039 395.05 745.039]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +579 0 obj +<> +endobj + +580 0 obj +<> +endobj + +581 0 obj +<> +endobj + +582 0 obj +<> +endobj + +583 0 obj +<> +endobj + +584 0 obj +<> +endobj + +585 0 obj +<> +endobj + +586 0 obj +<> +endobj + +587 0 obj +<> +endobj + +588 0 obj +<> +endobj + +589 0 obj +<>/Rect[328.91 474.239 406.16 486.239]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +590 0 obj +<> +endobj + +591 0 obj +<> +endobj + +592 0 obj +<> +endobj + +593 0 obj +<> +endobj + +594 0 obj +<> +endobj + +595 0 obj +<> +endobj + +596 0 obj +<> +endobj + +597 0 obj +<>/Rect[328.91 239.039 395.04 251.039]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +598 0 obj +<> +endobj + +599 0 obj +<> +endobj + +600 0 obj +<>/Rect[328.91 154.639 397.27 166.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +601 0 obj +<> +endobj + +602 0 obj +<>/Rect[328.91 118.639 395.04 130.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +603 0 obj +<> +endobj + +604 0 obj +<>/Rect[328.91 82.6394 395.04 94.6394]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +605 0 obj +<> +endobj + +606 0 obj +<> +stream +x­XÛr›Hý•y‹¼UfçÂõ1±¼Y×ʲ"ã$ªÚB0¶ð +P`°´ùúÌÀ@$yEñ”DÑG8§ûtÃ7- øÈï8ûsŽ O•<í@8”Oà@Í©î+ÎÀ‡P ‰8>‚66´ r\àùIÂl4¹™þóïäýÃôêo°-ëtÍ.Ó¼De-×tqq>äƒpÒB§ïo¯ÇÈwr7½Ö#æ7ƒ€ušÿ7¦ÛÄ먪Úh%ލ£Å‡Ì¢' +2ÊVEÒÇÅ9qÑœ@Dêþ*ceº¬Ysqp‚OœÚÀ0 +œl¹"œC—@×âbÇ%–0´AIÁ -ÿ¡ç#ÅÁüã‰÷BrìØ¾Ï°‹ÝÀáą¾­;@X%¶KH'ö­³½Ç·–‹l˕Y#NáN¥W_:œþ(‰X´/Éó£¬HÆ£CeqŽ2Žíbµ2Z€iÞy¦Ž¡©9ÞÂÿ\›5õŸî²hsÀeOzet˜pM;a”SšT3.£&Ý~Ió¤Øê²ìD·‰Ž=ê4 Á¥ø›Éh1º«ÿ==¾‹ËŸ˜¼Î–´ìúýwFiùÚÒPL—ñìNÄ;s¥AÇòÛ¬Åò™Æ‚ù1}Icª®Žv¥Ñ}P­þ9¬cŸß¯Žu5À4ë˜O=ÉÏÌëØC–-³ŠâÔÙঠ4‹ˆ^uΆ[N¶»R±Y‘æSsSÒyÁ¸½Ê±({o(rÄ}­õ€J  Hê¸iÒ7;xàp¹Úƒ£˜.˜ÀÊ:BÚ¹¹‚ñšÝ«ÉZ.‡ª¡/™yÛǯEØõ-Ÿçñpc‘e |eµáó¨äE•Háš|{²•uÞíJiµYGÿOÒjp`ԇÛÖ¯Y/xÛ!àA¬^—´qSŠè[¸ˆÓ­B÷oT&u}·›óU”mÖ´:1—«èE·ŸŠpÈ禄t.}„øz;Ñx£Æ+¹Ž×äªîÄo÷ªÔÍqm¬ÓU7¬«ãö„Ætû›Ò=…:sg$ ^1uß5ñjE)¤l=¼Ÿ·0…ÅÕ`‰qG`e”WE™uŽpöl—òÚü™F'¿:nX~õ;ñƒ1ùI€ºm¡.SOÿ:YÄ lYê¸a²ˆÛß×gsdÙ¤[‚>Ó²J‹ü28õBABÿJÙwN„uìªã†ÙŁÓñÅ»Øóº±µ=1[¶iÂVs£èê}y>´m™2:›~ìÞ²´Wü½É¨üê¸a]PпùjLäõï@vƒºìO͊>Lû*¥;ÊrB»2‘¤Ô¬1 yŸ~“5I +endstream +endobj + +607 0 obj +<> +endobj + +608 0 obj +<>/Rect[60.0158 748.639 128.376 760.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +609 0 obj +<> +endobj + +610 0 obj +<>/Rect[60.0158 712.639 126.146 724.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +611 0 obj +<> +endobj + +612 0 obj +<>/Rect[60.0158 676.639 126.146 688.639]/Type/Annot/Border[0 0 0]/Subtype/Link>> +endobj + +613 0 obj +<> +endobj + +614 0 obj +<> +stream +x­OOÃ0 Å¿Ê;vHËìükrEBˆ+¹ í2J7©Àº Ø·'Y‡@¨Û.S~rìß³½ åwˆu7«˜ëmJyí%ûѯ¡(µ‹Òi+”‡4V I}ƒÕ>Sƒ#*ˆêö â!Ï$v. i¥õ& eÉéì¿ïçþ u‡ë0«¤ñ`‰°Â°C“ Á¥”\BW> +endobj + +616 0 obj +<> +endobj + +617 0 obj +<> +endobj + +618 0 obj +<> +endobj + +619 0 obj +<> +endobj + +620 0 obj +<> +endobj + +621 0 obj +<> +endobj + +622 0 obj +<> +endobj + +623 0 obj +<> +endobj + +624 0 obj +<> +endobj + +625 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÎ10 À Õ¿èUo €ùï–)…¤’BRH +I!)$…¤’BRH +I!)$…¤’BRH +I!)$…¤’BRH +I!)$…¤’BRH +I!)$…¤’BRH +I!)$…ä\h×}f +endstream +endobj + +626 0 obj +<>]/BitsPerComponent 8>> +stream +xœí]KKU]Þþ±GtAB¥ ˜‘E,‹hðAeà ¢¨l ]ТA™RÐ@ÊH(‚´jí +: 4è2ˆhÔQ¢Ð÷t|¿Ý¾SßÞë¢ëÄvŸŽk¹ž÷¶Özßµj~þüé9؃G˜]p„YG˜e˜;„ŽŽNMMñyÕªU[·nÕ۟œ0w¾uë֓'OðÜ××wìØ1]=сÜ466.\¸0Ã_>w&&&Ö¯_‡/^477«ï@±Xܹsçôôô¡C‡ðc?þ½víÚ®]»²jbn¦åúôéÓÚµkñðþý{jø«¯¯÷2 9EØÀÀÀÁƒ[[[?~¬¾õ-[¶À ß¹s'¨OÐ³e˖}øð!“Vª"¬¦¦&úR¯ŸˆFêîÝ»'OžzôH}ˆHåŽÕ$Ž^’|S/=]„I€´xñ▖Èþ’B¡m¤ ‘qp¶téÒR©4>>þùó禦&•;ÖSŒ’ µ6Â@ÕÞ½{ñ€©¶¶öÙ³g¾ï/X°}k+Ё¶¶6Ê8¼ýƍgffø‘ë½nݺׯ_DzBJÌ"Œ1ú$ÊDɪ¾+ÿ§N:þ|{{û‰'ÀD~ɒ%½½½¤-äZ2‡Š >ô)©««C¸ý¢ÂD´_¾|”tÉÊlÊ488ˆ4Í ûæå?3KqTé”h Œš +g½Y+¡,˜d'!ÈCCCÁ #ÝçgÛº„ ïÞ½;ªïðù‹-òT-:HôéÙù¨)¬ð£ØxÄSO? +ÍUS TN Ü\¿~ýÀÁ÷\Š™9£zÉCí¡‡° ‡€÷B'0|±‘HH²{èɊ+Ð~TT94é”õ_Nç£_ÔC˜è;Ùêéééêê’)3^~ùò%¿¹3/B˜ÌÌòV/‚ÑrÈ;på0Ézº‚¯lý¼òÞ sü +ÿ?âû͛7çgÅ-sì\>WPÉ0?þœÒ).˜ÞĪ—§ž°Ð"8-Š$ áþöíÛs5Œ´7ƝÃÚÚÚ{÷îaŒÐ™#Gލa‹€*Ãkze“ã•Ŷúƍ)ÖEÃÄr4>>þýûwÄHÒ3¼Áص´´$ WVÀ0544 ;“““^yé™orm7²èŒçj֝5¯Ö;ü)a–Áfa–Áfa–Áfa–Áfa–Áfa¿abbBžW®\©r!¸J8Â~3·øœ´…˜9J¥Ò»wï¼êDÄwË2¬I¨:}út¿lƒµ¶¶öõõ™µ½b8¸ç«`ߙ»í I6¾%Р Ló!Áyç³R•C ULv„…Á„ª¼óp$q6ä)%9ǔ$Ã! Uy÷¼b´)in±À0}ýú•Ïê‹Ã‹Åâ?Ø4\A>+ cMU<ɉj& ~ppÐ+'áø¾¯¦ÔG000pñâEÈxCCDVꇟ’e–*fþš’ª-ItŸæHq¶6¸9sæ åCÒßä³rˆbՈ”$Í+´Æ)jhhЛ;vÄÚîlA‹Ò¤hAx~°¦Üˆà\G×y $_È $#XA¬da’®¥|ߛý›£…4‰jÄÈ&“˜ѪîQ»§²´bAŸAAGJD«IvOˆT£÷£DƒÂzek?± ûŒŠ‘§j‘žÍŚ™¤šG ÂàÛL¿HXhò€0dÛ¶m&5‹ô^@ÑCò‘b± m„…¬A±Xü§Œ¼cz9ºI¦ç<ïìÙ³AéUÞÒ#‘WhbC"ÍZü•‚p(YSSÆÆÆž©©Í’ÕULÂ:::&''{{{‡††¦¦¦@$õž5Q){™@Œ³œW¡s)KÏzÂz®q_ÂÇ^½zU͊Tp[ٛ]א£øººº®\¹’¾‹˜èÉ ©===øíú¾Ÿ~À¥ž‰38¹ÿ>ž!Őt•k¾hýòåËoÞ¼AÓ°À$†›¿p!kÖ¬QYÖ÷yûöm´‹çM›6 +…tµ6bµÞ¡z8Â,ƒ#Ì28Â,ƒ#Ì28Â,ƒ#Ì28Â,ƒ#Ì28Â,ƒ#Ì28Â,ƒ#ì?o󒓥õÂö ¥RiϞ=¾ï?|ø°¹¹™éS* ã­8bް_UþpŸ^Ù-p£££<Õ«bÄa‰7C©iº³³SŒ°ç«T/e9ý9Þ·££cÿþýNêO¯U|í½fww7­nõ4ÌkÂ0j7oÞd•šؓ` aˆ¤> +endobj + +628 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÒ1À íZSìƒ ì 4V.*r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹Œ\dä"#¹ÈÈEF.2r‘‘‹ÌÅ  +endstream +endobj + +629 0 obj +<> +stream +ÿØÿîAdobedÿÛC +  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YÿÛC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYÿÀ \"ÿÄ + ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ +%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ + ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ +$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ïfy£1¨Ž6ih̄„ú{Rÿ¥ÿÏ?ïñÿâi÷?ëí?ë©ÿЬUl‰Ý•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjݯä;y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚Þe ÷u‰âˆRÀ‰ èGû>õ6Ù¿»ýö‘ÿä!ýr“ù¥X¡‚+Üÿ¯´ÿ®§ÿ@j±Uî×Ú×Sÿ 5X¡ìnŠ(¤0¢Š¢º­›Lcr7sƒ´ã¨¡"€/QLŽT•с ¡‡ÐÒî\¸`ôæ€E&á’28ëí@ Œƒ‘@ EPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPwÿ„?õÊOæ•b«¿ü„!ÿ®R4«ßA.¥{ŸõöŸõÔÿè V*½ÏúûOúêô«=-ØQE†# ©21X‘[_­ªØýš!aǞXÀçAçœÖåËejá’0¨P²K·rì®sœƒÛÑ¥ßùj2ŽJ©`9aœ~* +êè [ûøÉrdWrÁ²Ë(_7,†;ý;Öޏ ÐX,w±¸f!W°ÏäŒý8«ÔPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPwÿ„?õÊOæ•b«¿ü„!ÿ®R4«ßA.¥{ŸõöŸõÔÿè V*½ÏúûOúêô«=-ØQE†1ŸiÆ3Iæÿ²i$ûÃéL  <ßöMoû&ªGym*ÈÑψ¸rÝúÓàž+˜„H²Fx §"€,y¿ìš<ßöMDYC,€žM6i£7Ìꋐ2ÇšŸÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“J¯»väry?A]«iÖí¿t0ê²TtJlšMœ²$¶Ü®ÒÆ<œzUó#>VrÓM7ö€¹¸¸2ÊÀHÀ ì8þ•8ûj¦‘ Íä’Íw"»«…U#ñ®ôËià…“cœ•ôúTgŒ¬é2‚ªJô¨¥Ì>S˜±¾¸¼ñÖi|ÿdWi‘‡Yã`?Ý5ÔÔi§ÁŒ¤Q)ˆaOº=ª-¿¼?*Mܤ¬2Š–ßÞ•[x~T†2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ §Å÷Ò-¿¼?*r!Rrs@¢Š(¢Š(»ÿÈBúå'óJ±UßþBÿ×)?šUŠo —R½ÏúûOúêô«^çý}§ýu?úUŠÈì(¢ŠC +©©Þ®a-Û£H±Œ•^¦­ÕkëQ{jÐ3m Œœg½U}jÙ/#²¡óŒ§…QØsOmfÉ^ eùd w`€»zîôëÞ©ÿÂ>UXÇxë)-‡ÛÈSÑx àr;SWÃî"Øn“äsˆñã e§½hǪZ°beE^ªIûÏñ$÷°Û¸º¢•Ý’}ÀéøÕ\9&r J&PpàÓ¯õ6£¦›ÒÌE`›0ñîSÈ<Œûzçހ,›Ûaiö¯9|ƒüCëŒ}sÆ) õªÚ}¨Ì‚›Éã9Æ>¹â«:ì´µûcùŠÛŒ‡w#9Û÷·c·ÞϽ6 'ËÓͦ q0eLt“~1“ô  ږBFŒÜ eƒ‘éúò8íREyo4ž\S#¸,»Aç*@oȑùÖtºœÒ$“ƒlÍ,Ё0Á¤¶y3`cÓҖÇ@†ÖIZYZ10#7ۏïSøP™u>)9.:äÏn¿—J|º•¬o$~rPW8è3ŒôÎ9ǧ5Xèêw~ôŒùßÃÿ=?©_hwtÑÜÙòã+Ï1ìÆK`sÓó  SªY+:µÂ+F»Ÿ'“ϰ¢ëQH-’xâ’t|SI$àÇãYÏáö–3܁ÞÈ¢?™Y—iÉÏ#“ØV–£i-Ü"8¦HÇ!–H¼ÄpF0FGó MNÙáWW%™C±óœŒ¥%–¤—K9xž ‰1À#=‰íùUdÑLsÃ2Ý8–¼”m£…Ç?ŽyöéOµÒž(oDÓ£Ét»XÇÅc8ÉçÔÐÑjú|²㺌¹à}2?OΛý±dZ ’ ²ˆ†Õ< öã­BtaÎۆS˜Ø¼¨T<Ô6úÐÜ$ÍvŽë"Hsí 9%‰ä7ùé@¢Õm&“̍»‚I Ó­X[˜Y!u‘JÌqþ÷ñøk,ø~6µû3ÌLMvî಩cÁíœþ•nk^ÞÑVáDö®]£Ê±ÚW•vcހ$]JÍ¥H…Âq•ë×üåLµƒDd[”*^2I'¦S ãÐÖzx}Õ¢lýÚ¦Ç+×q‚0H8Ç=8íŠ-ô -ÌrÇsÚ"+±Œ$©YpÃvOz=ºäáÕátK$ò,h9%¶ƒ‘Û½Ký­a¶FûL{c 1úœ zóÇZXM¶³3ò~ÜnÚÛºvª°èE|%Èu¶…Eul7<Ÿ” ñô  9om xÒY•@YCpHOëPliþX“íI´œIÆzuéÏҋí5/¤fw*Ýà ÌAÎ +ÌþúŠæ†x|̶ù<Ÿ” …G²O>¿…k6©d¡ÏÚPìœs×§O^•u+&xÑnc&A•Áëþë?þõ[/"9ðË*ʬˑ1Èd}¦¯‡ÙZ-·Jˆ«¶M‘mf두qŽzÔ|jÖ% ‹”*/|’z`wϵ0k6!HßpX¿EÃA¿J«i ˆ&yՌN¥vÆG +É<óì=¨þÀýߗöŸ“j¹ÎFáëþÕ^­‹FÒ-ÂRÉÏLùö¥“R¶Kx'æE;¬hэÀ“Óðªš\±'µÌ’Áå…]£øC à‘Ÿ½Ó#ëÚ¤³ÓfMÆÞGTžIåÜ2HàŠ²5{EÊç +FqÛ¿¦Hõ§RÈ<ˆnc íÙ=6œÄ՝>$–vöÉy¶8aX°ÈH%y݀ÀgëŸlTÓh¢Hv —;æš0Ê|Æ'g3ð  [Z³Ƃ@Q£yC)ƒï“Ò¥þձڇí(7† çåûÙ±ß5Gû Ú^‘ÚÅI +w«¯“€PpO~´õÑIi¤–à4³¤ŠåS/³2q€ƒŒÐ¨µ{ ¤ÇrŒÜñÈíŸåϽ0ëV_ºòä2y¬Up\Ï  u¨äÒ7)pѱupʼŒ.Ú­,rok´f%IýÙç +Êy,NHn´¢š¥›­:+ìÞFxg¯NœÒkXùFO´¦Àqß=3Ó®1Îj‚ø|„ò~Ô ïÛåüÛöÎsӌã>çCiCyW +ŒBZ2ví]¹A÷ó  ‹ëkh’I¦UGåO\޹íL·Ôa¸žéá-±ºBp§#9úc½Cw¦Ë:[îBÏ2o‘7† 0IñëùÑ‘V—VÞc¸@„ã6í  ?µì^a¹M¹ÚzäqžqŽsÓó¨Ù‰ŒFá7¨ÜF}³×¦qÎ=*‹éo±Úö/9£Sö”#³Ÿ”çð¦í_%nql¬Ò*”ùÔ)÷³Èù‰Æ?µ.·b‚-“,+¢(^ûœ(?Ìû㊒VÒ_%LȲJªBç8Ü2zsÛ֪ɢ»H<»•HKÂò)$˜È#<´v4[èk²Â'9S Ý·ŸÝ€犰Ú͈]%óȑüŠOß8Sô<óӊö­‰r‡Ë88úã^xã½gA ÍfkÅwo',cbIËrĒrJl~1BѬñª2ð–†æù²O•+Ó4©>¥m¼3–/Ψ…rOJŠßZ±šØLfXÇ ؓõçŽ(m:FÓííÚ伐º¿˜ê[v8ëŸn¿D4U×÷Çý /]¬úP¡©ÙtûBnŒe³Ú–mJÎß>mÂ.©Ïb:çÒ³¤ÐZDhÍÐ.óò¥ÎN㟛©ôª×ÚEò‰ZÍs½d`€*†# -ÇN¼ý(V^ÒYf¥XÞ&`w$çèjXµI€1Χ98èxäñô"©I¡$¶æ&˜ŒÈÒ¹J´IÙÚcx‚åÁVaË´¨^w\Î +¼5KwŠñâÜâÐeøÀ?.î V]vM5¼·øœÿ,ð¤ßLb¤‡J1[^AçîK˜Âgg+òmÏ^}jÐ\y†v*2¦ß»ò°l}K“@¯5HàHü'’BT6Þ89ôùMW[yäH ³/92_0» +ƒƒßtæ?µÎ“ª—o•…*›°$õßúU¹ôçÛKc$6ÞB'"=§ÌPIbř¿Æ´co`Ù$Œ `vú(¢€ +(¢€ +(¢€ +(¢€ +(¢€+¿ü„!ÿ®R4«]ÿä!ýr“ù¥X¦ú u+Üÿ¯´ÿ®§ÿ@j±Uî×Ú×Sÿ 5X¡ìnŠ(¤0¨çš;x^YcY˜ð*J©©Â×ı,ŇÜf+»èGC@‚úÞçT™9ۂ¤zò#ñ«Î}‡R•$âdBHA4‹æ·²žyéÉ4Ùt›˜¤;mÚ{^?p²…Ëlw'׿^ã4ÒÑXZ}ìzɚ`ûw3yž`*P„듏 úÓõ[KÙµ8¥‚ËŒ««p,9n8ÏAϯjÖ·º‚èf ŒÇ¡5ehV3ÙDVu +Lq¯<…þµmc{qn$†|Þk‡ú@óTíñò†ãÇJë Š$ Ÿ˜‚À{ ˆ£ÌO0G½wX.y wýEsßÙw²"(GŠì|¯7˜ÐÍc ÿu_¡ã8¦\h÷ +˜íݓl¨ž\Š +©t`OLã§n3@=%eÛ[]ÿ`IoorÉ L»’vž§G;U+]2v»ÚÝííPÆÝ¥FÀž $¯Ù4ÐÓRE}ÛNv§ëXšÜRO©[Ɩïp<‰>U.W É*¦jCÊÞÆB’n”ùŸë—j‚¿‰þôÐK,p¦ù*ä Ÿz}rñi:‡›/˜±bYÙÆÙ~|Žäð=@­ ÖæÚ{:HØä4Ž˜äú÷ ؤwXгœ(ä×7}¤Þ>÷DyÏ+2+Œ°?pò@úñéHÚn .dÛ±h¶™^P{„{t#†€:6š5‘#,È Q뎿Ο\¬šN Ò±HÙ' 0kƒ(ýæâ6㜎±Úµô;Y­mæ#ÆMÉvŒÐ ž§¯á@1ȲƲ!ʸQN®zÏI»Ž8\›ˆÍ¸ ægTïߟ­KâK;ËÄU´‹yHÃQÎ6žXõÁ#¶(mäDÛ½•w£'>”µŸªÚµäV¿¸ßåΎÊHʎýûf±bÑõq¸5Á‘†ï0œ2)Hÿ<î>â€:ª‰îaI–&p$a¾ÙÇó¬Í:Êê11Ek›nȃ@ œ‚q“ËÞ³ Òo–XÝ`hcD‘Ì ùŒn½úÐSEsqh—1lxÁI”C†ó Áï=~ŸZežy„YÒ@‚T2†bLgspNsïŒúPNH'¥":ȁ”ä‘\ÐÓoã–Ól.æ5ÚY¥Tdû‚#ÔôŸÙz†ð69—‚'óFvSÏ_lwÍtôŕGX‡¦FEsòéW1*¤Py±0ÍRù%€mÇ€'§_ÈÕÍÒêÖöÅ"CjI`ĕ@ ÏÔP½ÉOgy ¾ÖVŽK„È2ÒüÌHÈlï=³ßMԀ„­»b6$£im™-ò`pzg€:ªZæ%Óoš‘mÜK¼´²ùÃý!|ÀØ?ÝÈçéÐÖ®“Û[Çm4ròÌߛ…ãØûã¦hFŠçî´ûÙ5 Ý÷<…–ãÌöcËÆs×=±ÎsPϤ^$A"Fhqxƒ‚d!X7RR§“Î(§¤¬©-.¿áÊÌ× ¿$gîîúqYòé×nÀÅg$HF!_<£Àî<ñÇ÷sé@RÇ2o‰ƒ.q‘O®pXj!‰#,$ۗY ‰2sÏqéMMà*¹Sçӟ4ýñ&IëýÞ?Jéh¬{:{{è§`yó¼Ó¿9ËŸÏҙ§_MyqöyvC$m"6ü›aEü;ýhnš²#– +êÅÖô=p1\ÄZ]âF –“Kóº×ÍU$íÀo½Ž¾þøÍ9´»”šVk&š'vcL>f1  ’Fvãž~lŠéé²ÊÄòÊáfcÐZÎÑìgµ3½Ûy“¹AænÎ@ǧÌÖ]æ“yp.ãòIyû¦iٕØ˜ÎF2½G}è¤3F±«³…VÆ7q’z{ÒÅ*MÉFwGW³7Ð$p‰<©£p¼ yÆ}«%ôk¸¬’;u#1§œªüÈC‚G$W#¨ô ž’²í-.C{vÞ²²¶Õg\ô?>*ŠèÓÀÆKxñ +ŒÊzlÃwîh iYC:‚Ç + ê}¨) ?0"¹k}*ô6d´ +†A¹~CåHrMY“I½[o*ؘä(˜—Í?#ó¸úœôüsڀ: $X×sœ ùœSªŒðLú\QDž\£%wt<ýªš¥Õ¬·xZ8È]­#†f99Ƀۜ{Ž(QnaiDK*³ØçîäH©kš—D–G¹"§F6]Ên±úb§²°½Z3ͼ®÷c.ñµ”•qœñÇ`8ÎyäzŠÇ½´¹“T2¬- aŠQ.Ñ ݑœœät=RµÒ®Ê"c);߃Ðäuä÷ҒÉ4ˆé"+£++ †SEsK¥Þ¥Ü¸gEb»šPQÌr1È í#ÔÆ*$Ó5þÄ#´1ýœD2޽Žñ÷¸Ï ç“Ú€:—‘#»ªî!FN2OAO®hèÓ µ"3a‰åòY•Á'“×­-^+§h^Ö6—jȬªá~òàHï@,Á³4‘Ë ˜ÝXƒƒÐú翲/ùûNq»Ìþ˜#éšÑѬÞÍnVH¶3É¿p ïàžhNŠ( Š(  +ïÿ!딟Í*ÅWùCÿ\¤þiV)¾‚]J÷?ëí?ë©ÿЬU{ŸõöŸõÔÿè V({ [°¢Š) *½õг¶3²–E#vAžMX¦KMÇ*†G0=  gñ +¬Ê«lì½I»¶‚_¥+ë-2Ål« ²ª«»õ\x«ßÙöSy."D¦Ö `tÈúÒ &ÅYœ@$6w0sÇ< öGûy¼½ßdlȪñ(qóÛF} m©%F@¸N2Æ<´P®ƒ÷‰ŒsÀÏaÅX†„8RÇ{—99äÐCVWqBc 8q¸o}£L÷⫍y¶+5£4$o1.ŸNX¥h\é¶·R‰fˆ³ñÈb3ƒ‘œpy¤{ 9Èh”…M¡rr9ü9çڀ¤ÝMuÁBpFá둚›O½{¦¹ŽX|©-ä° ¸ª¶Aú0¦jZ‘±š0îI3™‚ªò2xÉÏô4ºU„z|s*Ëæ<²osÏ]¡{’z(ïR]ØÚ]•’ålgqŽààò8èh”:Æn ¶hٞâI6Èì>˜cÖ´®îÒÒk‰3²$.Øë€*!cf?”²òâÙ‰&¬0Ic*ÁY`ƒÈ"€0$ñ "¤²BѤešU6åòن®EKý½!BÈ´«»pÞà ð{ÕäÓl ¢%d$ã{–ÎF:“é‘@²±µ‰¿w ’Y‹·<’I  +x„Æԉ›æ ¼cnÝÙÏ®;zÓå×\1ڐ¼(gl|Ì¥‡œsVZ×M»ùv«`gr9Ûòã þ«ÎЀ I‚AԁúP=^In-íçcy#VݼČü õ­_¿ºû›Ï°ÈT¨ +2IúÔ1i–0̲Ç ̌@ÀÀ8'Ç~µ,’Û\Û¾üIɱ¸þ Ãúâ€3—\pÎ&µ…ó@c(Ád OAÏZÄ + ó$€ƒ—@d3®6¨?íg¥h=…œÁ•¢Få‰çX‚Oæü*ŠÙéÒÜC¡XåûC&Kïq•$ö þ"€6HŒÇþÝ ä„P¡ÃÈ+’AL÷#iéí[‚HduMì,™]Lj»¤ó˜«;únÈ9µQ_óH-XB±Ç$®X|œ¯Om¤ŸjÓ²¹vË8RªäíÏuÉþ#ŸÆ©ÜéÖqi×PFÉlÙüØL’zŸö›éš¶‹m‹ËÂåDiƒÀP8  +Úô“Á¥\\[Na’.0 †ÀèsU¥¿¹°»’97\Ájï!‘’GAÖµ§Š+ˆ^”‡æéJÚÝÀi¹rEl¶õ'ÝýkF=2Î6fHFYƒrĀCnñÏ8K¥ÙÍ÷áþ÷F#ï6ãÐú€hö"òÑ'FìýÖ 8$uJ§y¬-­úۘé* +Œ‚sŽ;;օ½¼V°ˆ¡]¨2q’NO$’y5Úeœó™¤‹.H$ï`2:ŒÐjøˆ–„5£ è’0 Un˜õ5·+ùq<„gj–ÇÒ«6›hïFbT `tgõl€Ad¢€2.5¿*5d·g&4üÜ(näöôõÖ£8ýßDL‡|È[9éÚ¥þưØT@FH90aŽ˜9Èü)ßÙվ̹P8àc¦qœ3@:ü¯m!V9“C©[°çꢴí5A=еòϜ»¼Î~èÁÿn~4õÑìwî3¸`îvn0GsèH§ÛX¥½åÅÎA’m«Âã +£ïߚv¡vÖp+¤FWwXÕAÇ$àsYrxciÿÑ]’&tXd²6Ò=¹Î·jے$”(‘C`Ã=ˆèj…ލir“›Znä‘Éàg8çYõ֊ibšÔ«À²<ØpBªª1#ׇ4kìm˛F A;¾E’߆>¤væ´¡Óm ÎÈFH`KŃc9'®v¯_AQÿcØù>_”ÛCnÍ}Àã{9èHÆ{Ð/øHPÜ2-»2.e`~bûpFÍ4ë³`ÙcSµäËdìdf÷ùMh*ÉdWX*¡@…À.qœqž´öÓíY•Œ#+·'øró?eÛø®<´ŽÑ¼épcþR +–äú€:VŽ‘s%ޙ ó¬|’=94ѣ؈Œb· çÌlŒtÁÎ@öU¸ ŽÚ†  *ŽÔŒu™àšäKnd‰n HÊÃ9ې1ùóZ6É~’•ÒÑ@ÉÒ.Ô ¯–ŽY¢”y›±ø_JTÑnŒLÎ}£Ê]ùòþ|à§zéh Q4kõ2S! —Ì€ ¸œ.¾•~×N¸H–‚ëÎT«æÆ~•¹EcéÚl–—Ê@c‰Nìî%Ö©.‡)ûIòQl‚øŒ…ƒqӂ+¥¢€0t½6îßU’iòW2&ñûÍÌã¯×§jdúÍ,’:+3´ç%Ï;±³òý+¡¦««#¨¥r÷:F¡<Ϻ$;Õј8ÃQžr~lUÈ´¹‘ŒÞH Ó)¿øAvú}ìÖõ‘¤ØÏgqܨ™Ä›¼ÝùÀہ×Óîý9õ­z( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š(  +ïÿ!딟Í*ÅWùCÿ\¤þiV)¾‚]J÷?ëí?ë©ÿЬU{ŸõöŸõÔÿè V({ [°¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š*©L“L 3{àf¦ªÚ—üƒnÿë‹ÿ#@ÞÔîµ=*Bè[Å ˆ[jg+sK'ˆ­cŽLW ¤E(N‰ÀÇ·½ai‘O?ÊÕYæhH +½OÍÈü³S\É=äú![ˆì`rÅJa‹*ñÇa“@…Õ%”$t=©ÔWe«Ùéڎ·*JÈ×"3å)`Š ÇӒ*ë¦s2H±U-ÔàWckq'…KKYRþù›í 逛›,Äý:Pj޲"º«AõêŽÄ0GýÔP£èI@N>•ÇÆój^ÔuS<©p²HÐb*?k°# Zãí ¹Óô=GHû4¯;É €…ʲ¿CŸló@ÅâxauxÝ‚Û Y“ŽqŠÙ±œÜØÛÎÀ+K¹¶Fk[´û€®­r †Óa#Ô +×Ñÿä cÿ\ÿAvŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š(  +ïÿ!딟Í*ÅWùCÿ\¤þiV)¾‚]J÷?ëí?ë©ÿЬU{ŸõöŸõÔÿè V({ [°¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š*¶¥ÿ Û¿úâÿÈ՚­©È6ïþ¸¿ò4àoù¬?Ü?Ì×C\÷¿äR°ÿpÿ3W¤ÖìÑð¾l‹æŒ‘¡e N1šÓ¢Š(¤éK\׈õ‹Y4å‰^`TĒ; Ï×½t”µ[OƒìÚ}´'ʉS'¾Y ŠBp ô®En.µQՒêh¦ŽI ¸W!S ÇCœæ€5¼]ÿ"®§ÿ\®hÿò±ÿ® ÿ ŠÈÖ®þÝ૬`Íg¼ãԎk_Gÿ5ýpOýPÚ(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+¿ü„!ÿ®R4«]ÿä!ýr“ù¥X¦ú u+Üÿ¯´ÿ®§ÿ@j±Uî×Ú×Sÿ 5X¡ìnŠ(¤0¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(ªÚ—üƒnÿë‹ÿ#Vj¶¥ÿ Û¿úâÿÈÐ?áa)ð "ßýqöcלTÞ¾û>ÎÂK‘$I‰‹ÆTFÝòOSŸJw¿äR°ÿpÿ3]-Q@u‘9ѯE¨&s ì ×8íï\¤“}§FÑ,൞;8æˆ\3ÄÕ ü޵ÜÒPc25}¬›†v·Qõ§ÑE!R=kµYtÿêZCÃ+]$X!"EsÁ¦9çÒ»:Jæõ›Ccà ›S‚a³ÚqëŽkcGÿ5ýpOýU?È«©ÿ׫š?ü¬ë‚è"€.ÑEQEQEQEQEQEQEQEQEQEQEQEQEQEQE]ÿä!ýr“ù¥Xªïÿ!딟Í*Å7ÐK©^çý}§ýu?úUНsþ¾ÓþºŸýªÅd vQE!…Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@VÔ¿äwÿ\_ù³Uµ/ùÝÿ×þF€1ü ÿ"•‡û‡ùšÑmfÁfXŒù,þX`¤¦ÿîîÆ3íšÁðùœ|9Ch Ÿìï°¹æ£ÐEì:}ªÏ=ȶؖ-‹Žî{œûÐeEP\ÕÞ£öß.šEÒÚÁi|¸Ünv8\8dç¥tµÏø{ý'UÖﺇ¹!öEù“@V–âÖÖ(äF¡CHۘýOzšŠ(®gS¾Ô ñ&“–8ín%e1 É`sýtÕÇøP´_h®#D¯æs÷2;úP¿‹¿äUÔÿëƒUÍþ@Ö?õÁ?ôT|Vë'„u'F ­nÄr ^Ñÿä cÿ\ÿAvŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š(  +ïÿ!딟Í*ÅWùCÿ\¤þiV)¾‚]J÷?ëí?ë©ÿЬU{ŸõöŸõÔÿè V({ [°¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š*9¢Yà’'ÎÙ©Ç\Š’ŠÍÒ4„Ò-£·‚æw‚1…I +? iQEQEEqçy-ö}žn>]ùÀüª¶‘§&—§¥²9‚Y܌brOæjõQERRÑ@ú¦˜º¥´–òÜÏ¡GHöŒÄ·m +Û[Ed¤Hs×b¥¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+¿ü„!ÿ®R4«]ÿä!ýr“ù¥X¦ú u+Üÿ¯´ÿ®§ÿ@j±Uî×Ú×Sÿ 5X¡ìnŠ(¤0¬ýri-ô{™ar’*‚väV…G7•å7Ÿ³ËÇÍ¿§ã@-o.Ÿw<¢7’)$ +à`ÍffòB’F°îŽ9X‰ | L涑í¼j#)/ÌO\ŸçN†ÚÒ8qQðzŒ´‘&¿*”Cˆ@»ÎýÁ7dîñM»×$Žà˜Â•‡‚¡¸bQÏ ¿*ÕOìç¸Ü†Õ¦a³‚¤‘ŽŸ•FK…WìÁ]ü¼.XއðZÊòæXõs4‘—°žQʯî”ñøš§¿u¼I$QK4‰#FŇϻï{ü‡§¨­«v²t6ëF䔯ÒzcëÅ9,ìÌ.±Á ŠC’ P_í9¿±ï٠品ÛzúwªSë—P´ñ˜müËu‘ä%ÈV +á}þjÔ3ÙG +D†&ÌX‚G‚‰à:sD£N˜Û8Gå‰S†÷ü¿JÇ}vò&t¤¬^V^¼"màûüÂ¥½Äúª6ípbe-ûÈËr=+EÓL™^Gû+¨`îÄ©ž2~´çOi„ö6P0Ù`x õ{™à‚Ý­™ù‘·÷IíY°kqZD³¬-4‘£FÅ‰Ý–Ç ¿JՊm>æÚ(ÖH^=ÛKªqÇЊ’æ`ˆ%I>QÇ4Ÿ¥jw÷qXÒ-Ëìä0r¿—Qõ=A.¦ ÂéÏò’@ڀÛ­mÛGgÖ¢ Ûû²>PyÇ¥lÒr‡ÉY¥É*HÜÙëÅfYks]ê!mˆ ÝÎAúcšmΫqi©ÝÝÉoÄ +ùÆàÊ;òsùÖ«Cio!¹d†&Æ<ÁÇ֚E„—HçìíqÁS‘¸ñÇé@k¯\ΑHУܲ YHo”ç¿Ê:TâáMÒÁ ‰&cr@Þh>ûø­h—N‹ÍxþÌ»[.À”ûúuªòO¦Ú(A +m–A´"‚¼ôÇò èú“êPI+D#„g?>ÐX~‘ø϶ÖïdÉ-½ºÆém!Úä&b tê­¸d· bˆÆ®ræ0Fyä’?-£e‰6…8ü¿‘é@üzõíÕ¬rGCæ< ¬ÙÆ×)ßùÓáÖ¯ ‚%™"‘æ-å9cÇï•>n:|àñéZþNœ…¢Ûl¦fårbô?­Jaµ•B‰Ã)`ŒŒþ¸ zû^»k+±‚)-ы¾þ§v>O_Ç¿«c©Iq©Om*F7Á$°ô§ßGcil’Mh²$d"*FåŽ0֍>kdžXaL‰C¨WƒÏæ(Ö½$’ıG".ð¤÷•AÁ=;Ó¢ÖnŽ¢–Ïj»w¬nÊOŒä{V›ÚY–3¼0äüÅÈžj¼wt×m*¬lñpfãZmÞ ÖÚÄ0c)"¨$¶ÉnÞ¼UHõÙ¥6¤E +Ç:¼±#qÎJ¾ÐØÜ^I$’Ç3´`l,ÕçZŠiô˜)·.¨Æ=€…ê=3@ ‹Q4 k©o<»9Âåˆ>Õ kw .֎لE»d'vç)òzãþ•ª‘Ú½ºÀ«…Ç ÆúSÞÂ) VŽuâ páøÐbk—!"–h"Xd_3*ç*Â±ÄÐ-õ—Hžì$m,Aò€àn\ðsÓµPþޜ¦ÔŽÜJ‹#ɽŠ€!ÛÓï|â´¡šÃl–±ü´Ü²(ÆÕàŸÀÒyZX¾NK.Jàž‡ëڀ3ε{#æxlΫæ9 ò nF=ñOµ×&¹¿H–×3*ç ” ŸLr+X}™¦1(Ê2ÅxÜ298úS+('I]`Ž\mFlôF÷V–ÞúHR8Ìqy{÷1 ÛÉhöÅCöËÈü.÷^b5È$î<¿åZoö. Ž`i¡êȧ×Ò¤E·žÜ¢¤„ä¸+@m¬ÝY½ÓMË–UL˜méÍié7³^Å/Ÿãldg ÆxÏ4è&°œÉù[£fVVÆGbqéïSY­²Ãþ‡åyDÿË2Ïá@(¢Š(¢Š®ÿò‡þ¹IüÒ¬Uwÿ„?õÊOæ•b›è%Ô¯sþ¾ÓþºŸýªÅW¹ÿ_iÿ]Oþ€Õb‡²» +(¢Â©êvÍyc$*–Ç H˜«•OT¹’ÒÅå…Q¤ӓŽh$è72Ú´RÍ—@…•y@x÷÷­„†ös±_ËÙòŽ:b±d×®âšåL1‡zç§*ÏÞÎ9ôã֘u¹þÔ¾l°â$f-|¶Êúÿê  K ùxhü”yx` +‚ Ao¡ÝGp’¹¶´mu-’=>÷éDZȭÆ~ˈ_sː¤(ƒÏ©¦¿ˆ®Uæ"Þ?,3"äãi «¸ó’>lž1@ Ŕvë$h>ΐ³ªòl–þžõ¯gnÐéñÛ¹UdM™Œ`}ERÑ®äžkÕ¸ž)gÚ¢3òà"çŸ~~µ­@ݗ‡æ‚5V‘U“Ê]ÊÄîIϱ拼>OœcaŒKœ÷«¤¢€9·ðëˆ!Xž51ª«ò‡*ÄóùÔ°h8˜ò±…''i;œþ÷é[ôP:º%ÌsBÑù åÈí¿ž†Bý;ðqøV®§h×°$hÁJȯ“íWh  Ý3Mû ‚6ä$d(ÆJ޵ZÿK¹¸Õå=ˆèêAÎG¿ZÛ¢€(jöo{j±Æ²¸q¸‘Ó=­c[h÷mw*Ëä€g †ùPgo¶xü먢€9›OFݼ%0¬Ä¬¡w}áÛïgê*fÐ¥‘ãn¬Np‰ò¯îŠ`~$º +("ÃMšÖós=ÌþnßÞrÛôËšÞŽuG‹ˆÓc$ƒ°à¯âAüëbŠæåЮȃB́$cÈ̂Bî~„ž=*֗g4W7“…|ݐ,ƒcݹÿ6-÷EmQ@5‹&¿°0&ÌïVÃçqô¬i4YãòâM©çOó,@íHÈÃòyÉþ`WQESÔ­ ޛ-´eP²€28ã·Ò±Ï‡æ”ÎÒxüæÏ–Šp9~•ÒQ@ìž‘îîXH$22°c•Ü1·”¯¢ÜJ”µ‰‚º‹Ó* ¯Ëú×CEci6O£y3‚!VÙ#ÏÌä}Xþ@T·v˪-Ê,Œ±©Œ”Úå²¾ç? ­J(Ÿ°ÙÅ-u‚8ŸåÆò­¸ŸÇžiø~Qm:JÑhÝ#l´´Œàþ€ü+£¢€9Ht»‰µžP!ƒ3& “28ϯ +É«:+ÁlVhÕ·*¦|ÂeWúŒ)ë]— C$6R´±Œ³<ÆÏ jï"îË÷?¥_¢€3ï,äšR˴呁b~\HüqR2\ÜÛÜÇ&Øw9•ë´zýyü \¢€3â†æÚHB±y†íÇ!ÿ/Ö¥–ܾ¡ û„B¹'žHÿÖ­Ñ@?|‹vñF …òº7Ê£ú5(%¹±’J‡l}áFy•[¢€9è4 68œÄß$ʀüØÞTƒÏ¦ O¥Xɧs+ƒä  #1yß(üÍmQ@¶™5åÂÉ”A‰¡"A¹ îüU­6ËìQ̙ ¾Rù‡_~*í‚úI-Ë7“‡3;{¸ü±PL%³¤·0ffä1éÅt´P/'‡®¥…UäŒü®›7´7|ާÿ­[·¶¿iÓ&´fHŒ`°ÈÎ1š·Es÷š-ÍÀšxVgu89ËBÑãð-š|ú,‹vg·[v_ùã"ü‡äۓïÇåšÝ¢€9Ùt ¤3€ð |‘…à’±Œé”?§®„òK4³š²\¬e„`cþø$ýk~ŠÅ²Òî-õ_´nˆFG͌’ÿ(ú*]OO–æî9¢X$6ˆ¤Ã dƒ¸~U«EsRh7RÉpd’"&ã'žrہÇÐb¶¬m>Èn@Û¶YŒŠcþnŠæçðüó¼ëæE¼’H$@CØùO·֖`Ö1K¿¤mÄ+ïZTPEPEPwÿ„?õÊOæ•b«¿ü„!ÿ®R4«ßA.¥{ŸõöŸõÔÿè V*½ÏúûOúêô«=-ØQE†Œ¡†= -^{('YÆ¡¤]¬ÀrG֝¬Æ#H(í´TÔPc¶Û·È‹GQNû<;Ýü¤Üã vŒ‘ïRÑ@ÇQcˍ01óŠ’Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(»ÿÈBúå'óJ±UßþBÿ×)?šUŠo —SÿÙ +endstream +endobj + +630 0 obj +<>>>>> +stream +x…̱€ EÑU~mIÈG2=ΠôX¸¾À9v·yomT¢ß¦1ÁèÈ"IY@²¤ Ñ’;Lãĵ ãùŠ*²U íã舲 +endstream +endobj + +631 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÒA0¡Ù?ôLá½ ó\͸§ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌ€f4# ÍhF@3šЌÀ'à +endstream +endobj + +632 0 obj +<> +stream +ÿØÿîAdobedÿÛC +  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YÿÛC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYÿÀàf"ÿÄ + ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ +%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ + ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ +$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ô0vOûá¿Â£7Q†eĤ©ÁÄNqß°÷«µ^Ûý}ßýuúÓBd_jOîOÿ~ü(ûRrûðÿáWh¢ë°jRûRrûðÿáGړû“ÿ߇ÿ +»E]ƒR—Ú“û“ÿ߇ÿ +>ԟܟþü?øUÚ(ºì”¾ÔŸÜŸþü?øQö¤þäÿ÷áÿ®ÑE×`Ô¥ö¤þäÿ÷áÿµ'÷'ÿ¿þvŠ.»¥/µ'÷'ÿ¿þ}©?¹?ýøð«´QuØ5)}©?¹?ýøð£íIýÉÿïÃÿ…]¢‹®Á©KíIýÉÿïÃÿ…jOîOÿ~ü*í]v J_jOîOÿ~ü(ûRrûðÿáWh¢ë°jRûRrûðÿáGړû“ÿ߇ÿ +»E]ƒR—Ú“û“ÿ߇ÿ +>ԟܟþü?øUÚ(ºì”¾ÔŸÜŸþü?øQö¤þäÿ÷áÿ®ÑE×`Ô¥ö¤þäÿ÷áÿµ'÷'ÿ¿þvŠ.»¥/µ'÷'ÿ¿þ}©?¹?ýøð«´QuØ5)}©?¹?ýøð£íIýÉÿïÃÿ…]¢‹®Á©KíIýÉÿïÃÿ…jOîOÿ~ü*í]v J_jOîOÿ~ü(ûRrûðÿáWh¢ë°jRûRrûðÿáGړû“ÿ߇ÿ +»E]ƒR—Ú“û“ÿ߇ÿ +>ԟܟþü?øUÚ(ºì”¾ÔŸÜŸþü?øQö¤þäÿ÷áÿ®ÑE×`Ô¥ö¤þäÿ÷áÿµ'÷'ÿ¿þvŠ.»¥/µ'÷'ÿ¿þ}©?¹?ýøð«´QuØ5)}©?¹?ýøð£íIýÉÿïÃÿ…]¢‹®Á©KíIýÉÿïÃÿ…jOîOÿ~ü*í]v J_jOîOÿ~ü(ûRrûðÿáWh¢ë°jRûRrûðÿáGړû“ÿ߇ÿ +»E]ƒR—Ú“û“ÿ߇ÿ +>ԟܟþü?øUÚ(ºì”¾ÔŸÜŸþü?øQö¤þäÿ÷áÿ®ÑE×`Ô¥ö¤þäÿ÷áÿµ'÷'ÿ¿þvŠ.»¥/µ'÷'ÿ¿þ}©?¹?ýøð«´QuØ5)}©?¹?ýøð£íIýÉÿïÃÿ…]¢‹®Á©KíIýÉÿïÃÿ…jOîOÿ~ü*í]v J_jOîOÿ~ü(ûRrûðÿáWh¢ë°jRûRrûðÿáGړû“ÿ߇ÿ +»E]ƒR—Ú“û“ÿ߇ÿ +>ԟܟþü?øUÚ(ºì”¾ÔŸÜŸþü?øQö¤þäÿ÷áÿ®ÑE×`Ô¥ö¤þäÿ÷áÿœ“¤‹¹‘’?Õ·nj·Uì¿Ô7ýu“ÿCjzX5¸ž`þìŸ÷Ã…fŠ‘…W¶ÿ_wÿ]Gþ€µb«Û¯»ÿ®£ÿ@Zkf'º,QE†QEQEQEQEQEQEQESw®â7 Ž£4½hh¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š*½—ú†ÿ®²èmV*½—ú†ÿ®²èmMlÅÔ±ERU{oõ÷õÔè V*½·úû¿úê?ô¦¶b{¢ÅQHaEPEPEPEPEPEPM}ÛoÞÇZuÃ[ E¶Uh®Z>pœ¡;Ãd¾z¯L~¨Æµ¨[ÜE S² ‡ËÙ$yD`ƒóuî*ïh kºˆ¶¶cw)Bˆ”™y)èÃۃOMoXi¯Œ’ˆJ£þí£Ï“†P§‘O\úö®îŠËðõÔ·šLrÌï#îe,À Ø$gŽ÷ïZ”Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@W²ÿPßõÖOý ªÅW²ÿPßõÖOý ©­˜º–(¢ŠC +¯mþ¾ïþºýjÅW¶ÿ_wÿ]Gþ€´ÖÌOtX¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š)¬ê§“ŠuC'úÏ€æ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…jxTTP¾jxQæ§÷…EEKæ§÷…CbsnÄÏY?ô6¥¤°ÿcÿ]dÿÐښً©fŠ(¤0ªößëîÿë¨ÿЬU{oõ÷õÔè MlÄ÷EŠ(¢ÂŠ( Š( Š( Š( Š( Š( ¡“ýgáST2ߐ¤Œv ÑFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”QFûúQ†þã~”RXDZÿ®²èmK†þã~”–ñì묟úS[1u,ÑE†^Ûý}ßýuúՊ¯mþ¾ïþºýi­˜žè±ERQEQEQEQEQEQEQEÅÄV°4ÓȱĽYº +Cs¹Ks*ùÎ¥Õ3ÉQÞ«k6{¦Mn‹¸É€FqÆy¬VÒõÍ9…^UF…T>>AÂ÷GlŽ”ӗPꅀfè äЮ®X)ÉSƒí\°Òïš$ͼÕ¦Tmê +#+Œ7út«7mè;-ƒ*,›¿ÖãtxOÇ×üh .¢@™ùˆÈÔêÇÖ-ngÁ%)å`¬nÎàqÏ€x=Gzk[Þ!6À˸‹;w¿×·c·"€6¨¬8l®ßA[YVE”N¹À>X” ÿs·áU¥Ó/^i–$x¤Ý)`ÚÊAòÔ çå;{cå>´ÒÑXe†¦—És1rù‰óoÚò.¸ôR¸ïTshÓË$ŽÁÙÛÎçÍ#9ÆÎýºJè]•³(&”€GC\¦¡ ü3Éu$m*°yƒ ¦,­Ÿ½Ø;òiòéڌ‚P‘ʳ0}Óy  ®@ÎA‡Ozèîn ´@÷$JNsŒšH†?0:”ÆíÙ㹬ÝNݖÍ"¶µy@  ÆÊ$ Œ©n>¼÷ªQi÷ÑËnZhc„#Ãæ|®ûxlz‡ëšÚ¶»·»RÖÓG*©Á(ÙÅOXvV·B-MÌN€FŽÊX¸À*p¥WM"ê ±#yŠÑ•ÌÇêÈnýÛ ‰P¨fU,p¹8ÉôÔÈcÏÎb=ÿª¹‹]6ô\ÂÓZ?–—Ë·rà|¬¸÷ õÉý*Hô­B;ALÑÎÐD¦Lì”3cëÁß8 šŠÊ’+˜í4æŽ A ibYb62õ$ɓڳ"±Õ{vòœJ# +diC$g ÓäŽ0ÀñҀ:Š„Ý@ y‹å’7©'~|W5m¦ß¤iç[ÜIdómüÅ̄+‚ÃæÁùŠžHÎ3Ž9½™1Ь-¥Œaš7`[8Ãäóߊݢ¹xtËçXÑã–0<±pLÃ÷ì$œ`ôÀn¸<ãVŽ©cqq8kf(ÚEŒîÀY ]§@h^ŠãÔR[E%µËÇ#>-üÕ!?{pzöÎ*Óiz€±c&ùg2&ð%ãeFH}Æh§¢¹tÓõ$–جs3„Æù%cëèA§`h³Ò¯O–³¤«š†Ui· È9qŸJéhÕö—²;óÒ¤®Y4«äS¶&óŒh ž`ç† Î{Š[«I,'—qµ@ó× ’HvžHÓ3ªc{ÉÀÉÆO¥:¹ë[in|?¥—‰åt’9J³e±ž¹>ƪͦjÙZ$P;Í*ÌâPXK‘»’À2ôàPWQ´Ñ¬Éaæ8%W¹?¨üë}2ïd’"Ìe‘®7ì˜(ϔ‘d’Ûnš)QÚ0ùÀ êO§lŸÆ€6#‘%dƒ# ‚;Ó뚏F¹Ž;h‘Yc"38ýâßÓ?ʚúv <ß*)Q¿|¾hÄÀŸxÀÇ\t ž™©.ï-ƒlb­ŽÄuÉj†æ?5|ã!‚/4F;pzã=x5m´›Ð’K)q$ŽKy˜ùJŽ?O΀:Z¦FgP"~~ïçð®v;MBÒCk2ÄCª[™Wr’Š2~lcp'®yéV-´ûˆìõHÚKqÊہÞ^=}}hT_Z•¶a:sÄ'?ë8Ï…:êêHē¾Å$(8''ӊç“F¼ÊQD6Ž|“¸p¬¯»ò%GáOû£tðy‰-¸Œ@ œ¯™¹‡QüK@çW°ÆÿhR²nۀIùqžãiút‰-§™G’FVSAs‚+=¬dÓïí§·‚k±²o5·®âîc œ1òž•{I 4ô"ÆáŸr)ÈS¼ð=©­˜º—h¢ŠC +¯mþ¾ïþºýjÅW¶ÿ_wÿ]Gþ€´ÖÌOtX¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š)*Ž´3¥\#Ä +àº)%}ð8õ¬(.Þ$±¤$#¯ „ŸÃéî9 ²ŠæeÔ¯`Åqv°ÅƈpJ Žsîzu©¬u;Éõ“Œ›701mÁØÃã¨Ï¿ÐQXZ®©-®§QÊAŒ22€3`œç'AëG†Œ†ó ŸÝE÷¿Ü»Eri­Þ2nKˆä'wœñ쪡ü±çû¹éš˜êŽbh]$Ë4qÜlÈuó¢]Øtfé× šŠå®/¯`¹b÷Lqʊd\+bDùŽ;àžz½3Z–×ÓK Iw3Ω!@T 䏺H=#¯·JÕ¢¹Û]Bîâîa»YíÚP ÂÆ>o‘™—Ó‚çSëz„–—pÄ·Il¾æPw0Æ>¹  º+™R½R‡`ˆË/ïÆÃû¥Ú¤·àN*í+릚9xš%4X|œò=}h¬¢±´MB[Éîcše”¡ÎcQµy̪7ڞ¡ö3$qå@ì  ÷FO\ÿú¨§¢¹iu[¦šXetf1gÉDÎÞÉ >¼ƒíQK©Þ}¤J’ž(çͰOõX*=údó×P]EeèWs]ÛJÒȓ*ɵ$\|ÃõIzzÖ~Ÿy|míŸpXÔ[!‹Ëë½Ww=xÏÒQX^$ÔçÓÑ~Ï&É<©$¨Ã•ÆIÏ~€dúŽöµyEfð\•®R0Àö>NJӢ¹HµY÷ ØÎ£Ë<˜Ô‡Œ˜Ú´tëû™„ÞS-ôI·l념IuÁÇç@4µÇÇs5ÔSyÂwòºÀ‰ËŒ¯Ç¿5j+íQvÈÒù£Ÿ/ÊqpIöÀ šŠå¬õMFä"‰£ËÊŠìª ˆœî^8ãù§k݉-Ri“2& jƒsžFpH8à}Ü㸠ž’¹tÕo#ڊê²Ûlå—a%ýzþºÔ²ê7–ê©=Æ ¢6ÕB– ‘’p®Onh¤¢±<;u-âIq7ߑ"f mƒ ~5žÚÝÂÉ"5ÜkûåVp€¬JXƒß àvÏ"€:º+”}ré<’g^|ÅVhüÒ»ºäü£< ¹Á$º½ÒÃ3-ÂyÁöÉ—ÿËæÉ=ËÏÍׯ@h§¢³´‹Ö¹´Ï‘fÜÃoñ lþ]8ô¬û­Rî=Bx’Eޒ[}œ˜ögÌõëŸn1ր:+˜ŸSÔ-â $Ê™§*G¹X‘Ï@ú֌—·KáñtB‰ÊX)ÀûØús@ÔW3.­pŒ¢+Øå@3ùñòw´zñýß­$œö°˜Y‚ÈÅLJÃ%³&¸ÓÑ\Â_j„,¦q-$)äŽòmÆ~•zÂîòKø„Î)|ï—f6l`ŸphfŠÃ¿¿¿‚òâ#ó +F×ü™ ¡ õ/úUµ‹¦Œy·±Gn\ƒwµX)۝¼|½1ךꨮWûFâ)îKÿ&6‘›Ì‘>áòЪz–8ëòžõ­£Ü^]‰.ÿw´¢¬;q´˜ÑŽO~XÂ€5(®SQÔ/%†úŸmZ\4* ì|õù°?ï®:V¶±¾ÞÊÔG;B«€çŸJÕ´¼¸“C{–dyB±GQØèýT©K\âÞê01ygóQJ‚¢üI’xô?ýz©«u<ŠMßË€©~|ÆN8ëڀ:ê+š{ÝBšUÄ×M0O/ÉÎWŽ˜þïZ—S1Òb‘[Í,Ñs޹uÉÅhÑXº¡5ä· 4Ë+F“ ’x¨é÷X?ΒkÕ¸žH®Ü¼ñ°Ë»d*¸ôÇèx ®«Ù¨oúë'þ†Õ‘e©ÞO­$(©½ÔÁ¹P•½yã¯þzö_êþºÉÿ¡µ5³RÅQHaUí¿×Ýÿ×Qÿ -Xªößëîÿë¨ÿКىî‹QE!…Q@Q@Q@Q@Q@Q@Q@Í4p(iX*– + õ=*«ê¶I"Æ× +¿Nqϧö_ÿeÅ^‚ê “ˆdò,œu³´þ85KTÔ-´é–Y"w˜BåHé´`Oà)Ú%‹YZÉæ²K#>ÒAعùW#°íON†ÜÍ卬‡åÎC P¶¯b±y†pqL`ç g§^œý*ê:ɺ0d`#¡•{¢-ÔÍ2ͲBå²Ë¸`®Ò1‘ZpD ‚8—¢(QøPI5‹÷nœ|¬PàÈëùwô¤—Y°†O-®ïEçŒãŽøçZçCIÑvOµÕÜå“pÃõÈ©ãÒcÔ¬‡åpøÇ¢í ¾­f§h™Kܾ‡åÜzg4É5›DGýêùˆ…ŠçŒ…ÝŒôÎ9úU7ðéfý1ŠG¡“8ù +œs€0IéÖ§›BŽ[g„ÌÀ33g±˜ÿ‘ÍY:µ‘Ñ®Yn' Ú2FzdjkKÈ/Ì»Ëm® ©À8 ûY÷š2Mi,j嘼’Ó,Ê@­K£[\Ãö¹o1æÜL"¯@HþS@®o­­dŽ9åÒr£¸öŽ}ê8µ+w!EY  +sóãñéQêzkßK ¥ÇcÈÈ_›’>ë<{j4eK¨'¶øÚ?”q½Ë8üAǶ(MÝcFw`ª£$ž€VöÕ¡1ذv÷W°ü(ŸPµ¶¸H&™VGÆ퓁ŸLžAo¬ZJ â9&„<ã''¶Hã֙{£­ÕúÜù¡r:•ݸ+dcž?#PGáôŽâ)o•vVL–(r0sÇnǧjµ¹§Êè‰p }»x8!ŽúÆ}x§lXbCö…Â^rÛF=yãŽõ z,iF&b("Î;DŁüsPÛxz86;)'–6rª®矺j²u»!"©vÚÑ´›öœ 0R¿\œb®ÁàŠÒ…«c/,ñ##ôª—í´væXs6ÒA_ºF›<ö;j/ì7óýºB «’W.X!Pwgß=*1á̉·e·ŒœìeÎI9á¿JՆö Š*È7¹`¾Wï~Y§Ý]Ci›;„LŸsÐU =¡Õn®ÜV +‘© ô3 tÎåW®­…ÊƬÅvH²qß4]õ‹ÞE{”S%‰à c<ûdgҥӜIk½s†’B20~ù¬‹Ï3%ɂPLŒî‹·•gmÌrO×ƵôàÂÓ0gI¹€À'yç©­˜º–¨¢ŠC +¯mþ¾ïþºýjÅW¶ÿ_wÿ]Gþ€´ÖÌOtX¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Škp¤JΒí­ìad%™9<ç«J‘UQvª…€b€ Žî)$ä‰<¡#Àz§g~RÔÉpà¢F]ÎS­i…Pŀž§iYU†=  +—W,‰–0ê[{©Çl +‘n‘¥xW-4q‡eŽsŸÂ¬SB¨$€=N:ÐdWâ9.&¸”£\íSÁËd{žŸ¥Mw< …k)l±ÆO§µ^e 0ÀèE-B—½Ä)>dj¬Ã çþJÝËÙÝH÷Û.2Ý Ó¤HÉ u¤eWR¬)êë@Å*Ÿ.2xÉ¿ðãükœ)~-c?)ˆj…Áߏ7×ÓúWQҖ€9[KýNà*ù¸-$bL&LD·Ì¿•[Òïo¦Õ^)ØmÃÆGÜü§ñ­ú(™kû¨/bMåQ¥aåª|Íóc<õôæ¬ß›¨µ9§Ü*$C`\†Ëàþ†·h ^=SPX¤”™âA,ðˆñ°†ÁAÿÏåïWôIõ ‡/¸0BãsdÃþWñÍlÑ@––ש3¬ó9Ú£«Œî%ö¾~€þ”¶º¶ ¶ÒÝ\6åR[ˆÂòœüê>ƒ'~_zë*)àŽáLÔØ=29™ysuo£Û¼ÎÉq#(‘žHÉèLÖrꜶ/:¹ÌpFHÿwWoÀ.q]MϝBí<;=Ϛ £â9vç+‘Î;÷éU_S¼À¹o³©"/&fv©õêß\WO,I2l‘C.AÁö§Ð/>¡xóÝÂÌ]¶dD©œƒß®yèi‰suId*–Y™rѯ›Ïä+«¢€9”½Ô¤ˆÊ$m±ª”ýßúߟ?…Bš¦¤L¾d›ìM„É€n##ðÇ_­u”P;fów¦f½;Y—ï)˜s¥ZÓf½k˜MČé4nÅJ`! ý lQ@œ_mI.eŽâfxS†\î"VÂý1üÅ^Òï¯gÕ¤ŠvA“tXû€0 ùÎ·¨ vâ]I¦”¥Ã¢æpFcoçúŠ©{©^JóD’Jždn…à©ò‹½þðêk­¢€9Øç¹g<Ìð¬êˆÛ:'’ïs’jޕusm¹`=zŠÞ¯=ŽÞÚçÁ—7R*Ï}©Hæ<•fl n >Õß[FÑZÝ̈ŸRKIҖšÃ*G¨ qõ›éôëíJÓËû=¬Œ«d ÷Ž{wÅhO®Ao¦%ûÃ;[˜„ÅÑrc¿5Ïé²­¯ƒ5KYˆY ’hʤ“Çù«š»Ú|8š –X`{PKo2\[Ç4yÙ"‡\úš’©hÿò±ÿ® ÿ Š»@Q@Q@Q@Q@Q@Q@Q@Q@W²ÿPßõÖOý ªÅW²ÿPßõÖOý ©­˜º–(¢ŠC +¯mþ¾ïþºýjÅW¶ÿ_wÿ]Gþ€´ÖÌOtX¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š«©È2ïþ¸¿þ‚kÁQ¤¾±I22TŒ‚2kgRÿeßýqýÖG¿äR°ÿpÿ3@˧Y,q¢ÚÄ3”GÊ}ª×N”Æš$p*+ŠXjJ)®ªèÈà2°Á¸§Q@¢°´‚@ñ[Č:½*Ífk×ÂÏH¼–;…Šx¢.¸Á98>µnÀÌl-ÉÝ9L‡ËcŸÖ€,QE%@öV¯?œðFÒÿx¯5âïù5?úàÕVMròK+ÝFÒ8ZÒÕÙv0;¤ ÷ˆ9ãۊ“Ä— wà«ëˆþä¶¥ÇЊÒÑÿäcÿ\ÿAv©hÿò±ÿ® ÿ Š»@Q@Q@Q@Q@Q@Q@Q@Q@W²ÿPßõÖOý ªÅW²ÿPßõÖOý ©­˜º–(¢ŠC +¯mþ¾ïþºýjÅW¶ÿ_wÿ]Gþ€´ÖÌOtX¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š«©È2ïþ¸¿þ‚k³›_A:Í Àz‘šßÔ¿äwÿ\_ÿA5‘àp€AB?S@ Ñ­ôû.Åï^)îîHŸs6YŸ¯ºJ¥e¤ØXHòZZC ¿VEÁ«´U=Zé¬t›»¤xbgQîršè²##¨ea‚È"€8kØ­dÑt‹ V{Ëù¡y¥êÜüÌIü®åY£!Ž9UôM2(ì¡HÕüÀãæõ«ê¨UÀµ-5†TÜS¨ /J‘müªE) +ð<èã¾IãóÍZ¿í¾Ë €‡K,{q[Òi–RÜÞÚ6”I#© õª^.ÿ‘SSÿ® @4ùØÿ×ÿÐE]ªZ?üìë‚è"®ÐEPEPEPEPEPEPEPEPUì¿Ô7ýu“ÿCj±Uì¿Ô7ýu“ÿCjkf.¥Š(¢Â«Û¯»ÿ®£ÿ@Z±Uí¿×Ýÿ×Qÿ -5³Ý(¢ŠC +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€*ê_ò »ÿ®/ÿ šÈð7üŠVîæk_Rÿeßýqý×;á˳aðö+¥]Í à{ŒÐW‘K\}–5ÜZl· wEîPÒÈzí\tÕØPEPMæ£+kivmÌb3I# Á@1ž¤ÿ*дÖ!vck£Ì1‚Ÿl×=¤YZ^x—Y¼khE"B„Æ8ecõÉ×O@Q@wzóC­YX%¤›.\§œÿ(àsÔýiþ.ÿ‘SSÿ® T|Cÿ#G‡¿ë«ÿè5{Åßò*jõÁ¨æÿ {úàŸú«µKGÿ=ýpOýUÚ(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š*½—ú†ÿ®²èmV*½—ú†ÿ®²èmMlÅÔ±ERU{oõ÷õÔè V*½·úû¿úê?ô¦¶b{¢ÅQHaEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP7qí'‰H $l ž™#“áí2ëNÑáÓoRÚH‘ +–G'v}ˆ¹EgXhšv!’ÒÕcsܱl}2N? +Ñ¢Š*+‰|˜ZAÈ@áPdš–ŠÉðí„Öi ™åyåç Ç8üáZÔQ@Q@—Zyv·7Èó¡Ê¿Ÿ Ú}°Ü~»cw{¤Üiö‹É¢1ï–VÊþ‡?lQ@ì`kk x‚ÑF¨HèH«Q@Q@Q@Q@Q@Q@Q@Q@Q@W²ÿPßõÖOý ªÅW²ÿPßõÖOý ©­˜º–(¢ŠC?ÿÙ +endstream +endobj + +633 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÒ1À íZSìƒ ì TV/:zҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/Bzҋ^„ô"¤!½éEH/BzҋÐgüD +endstream +endobj + +634 0 obj +<> +stream +ÿØÿîAdobedÿÛC +  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YÿÛC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYÿÀD^"ÿÄ + ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ +%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ + ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ +$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ïÌñ“*?Ú϶ÚÿÏÌ?÷ðTº‡üƒî¿ë“#V)ék±kr—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R—Ûmçæûø(ûm¯üüÃÿ]¢R¼µ$s '°qRyÑÏDÿ¾…-ïú…ÿ®±ÿèkV(vè +å}CþA÷_õÉ¿‘«_Pÿ}×ýroäjŨQE†QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW½ÿP¿õÖ?ý jÅW½ÿP¿õÖ?ý jÅ7²R¾¡ÿ û¯úäßÈՊ¯¨È>ëþ¹7ò5bށÔ(¢ŠC +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+Þÿ¨_úëþ†µb«Þÿ¨_úëþ†µb›Ù ©_Pÿ}×ýroäjÅWÔ?äuÿ\›ù±G@êQE!…Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ïÔ/ýuÿCZ±UïÔ/ýuÿCZ±Mì…Ô¯¨È>ëþ¹7ò5b«êòºÿ®MüX£ u +(¢ÂŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š(  +÷¿êþºÇÿ¡­Xª÷¿êþºÇÿ¡­X¦öBêWÔ?äuÿ\›ù±UõùÝ×&þF¬QÐ:…QHaEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP{ßõ ÿ]cÿÐÖ¬U{ßõ ÿ]cÿÐÖ¬S{!u+êòºÿ®MüXªú‡üƒî¿ë“#V(èBŠ(¤0¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(½ïú…ÿ®±ÿèkV*½ïú…ÿ®±ÿèkV)½º•õùÝ×&þF¬U}CþA÷_õÉ¿‘«t¡ERQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE^÷ýBÿ×Xÿô5«^÷ýBÿ×Xÿô5«ÞÈ]Jú‡üƒî¿ë“#V*¾¡ÿ û¯úäßÈՊ:P¢Š) +:{ùþÓ,V¶¾x€0îÁ$ö_SZ5›s¦¼³Ë$on³€%UPwc¸=Ûéæyû£Û)8'¦9>içW²_73qÁ88'8Àõ9ªw^Y÷…¹‘É%HÜ9Ç=zñ֔èl^f7‡2 cËÿ‡ô  gW² ™°eŒƒÆN}9⥴¿·½-öwß·¾8#ÔzÖrxyâcrìT(}˒Ø9Éãõ«Zv˜lfšCpÒy€»vÉçŒó׊ѢŠ(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(½ïú…ÿ®±ÿèkV*½ïú…ÿ®±ÿèkV)½º•õùÝ×&þF¬U}CþA÷_õÉ¿‘«t¡ERQL•^)›Û×ô  ¨¨w·¯éFöõý(j*íëúQ½½JšŠ‡{zþ”oo_Ҁ&¢¡ÞÞ¿¥Û×ô  ¨¨w·¯éFöõý(j*íëúQ½½JšŠ‡{zþ”oo_Ҁ&¢¡ÞÞ¿¥Û×ô  ¨¨w·¯éFöõý(j*íëúQ½½JšŠ‡{zþ”oo_Ҁ&¢¡ÞÞ¿¥Û×ô  ¨¨w·¯éFöõý(j*íëúQ½½JšŠ‡{zþ”oo_Ҁ&¢¡ÞÞ¿¥Û×ô  ¨¨w·¯éFöõý(j*íëúQ½½JšŠ‡{zþ”oo_Ҁ&¢¡ÞÞ¿¥Û×ô  ¨¨w·¯éFöõý(j*íëúQ½½JšŠ‡{zþ”oo_Ҁ&¢¡ÞÞ¿¥Û×ô  ¨¨w·¯éFöõý(j*íëúQ½½JšŠ‡{zþ”oo_Ҁ&¢¡ÞÞ¿¥Û×ô  ¨¨w·¯éFöõý(j*íëúQ½½JšŠ‡{zþ”oo_Ҁ&¢¡ÞÞ¿¥*»nš–Š( Š(  +÷¿êþºÇÿ¡­Xª÷¿êþºÇÿ¡­X¦öBêWÔ?äuÿ\›ù±UõùÝ×&þF¬QÐ:…QHdrýÑõ¨êI~èúÔtý¯ö×ö`Góvî/ü#Œâ’ÃZ¶¿žé# ‰o÷¤ršç¦µ¿{纆 ·7OJ‘±6íÝô¦-¤°[˜Í”¦+‹£“å*¨0¿/¿¿|¨Ï™]Ö¥ikj.$™ E‚‚„6OµL×0…cæ¡ ·pÏL×oa)²±K‹IvÍ~Ï"ù_qGLÐʬ› •©Þ}‘þÝs+,@©Ê);x†(åCægKmyð$„¬e“~Æq¾¿Oz]C#SÈ*r r­œ²]Zˆlæ6¶Š–óTž@ÀÅu誈ªŠ@À°©h¤î-QHaEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPJ¿|}i)WOEPEP{ßõ ÿ]cÿÐÖ¬U{ßõ ÿ]cÿÐÖ¬S{!u+êòºÿ®MüXªú‡üƒî¿ë“#V(èBŠ(¤1¬¡† '”¾ÿ™§Ò1“è(¾Zûþf-}ÿ3\Âk·­§Â?v.ÍÆ×;xòóײ8ü*ìºû:[À†Hö‘™)RûNHïíÍmykïùš<µ÷üÍdeŽeh"UvÆA%Tà·áÍY:†ý> ¤VE™Ô(à’ â€.ùkïùš<¥÷üÍRÓu1~²Ÿ(Äcê¬ÃpöaÕO֛¦j«¨<¨#ØÑª·sŽGàôÏր/ùKïùš<¥÷üÍc®¿ºÝgû<…¹“.2‘¶p}Ïãڕ|C \­¸†O0áq‘÷¼ÍŒ¿‡_¥kùKïùš<¥÷üÍeÝkk~âÝî*³2¸¶ð|ÔwúÛ[‰%·Ã)q"yƒ#aÁÚ:·\ô×±å/¿æhò—ßó5ÚØˆÌ /*ÀY¥l±CcÜÿ…[Šòk»)å‚$‰•™#2·ÊÄ2qÛ"€.yKïùš<¥÷üÍbíIoÈVàÀ +¦ÂOr¤ñ“Þ¦KËÄÖb·˜ÅäÌ€¨~\îç®F(SÊ_ÌÑå/¿æk"}q£šDK'‘Q¤]Þ`Ø2ÔË¿¼ÕŠg³¡.!wa€è?ZÚò—ßó4yKïùšÊ´¯q"­æ,lw 3‹à{t楇XŽ}.æý"&sƒ€[hù‡æü(CÊ_ÌÑå/¿æk2÷Z[F‘|†}Œ1`ª2»¹'éÏî*)|GSL¦8·ÊA9UÜxôãÏ_΀6<¥÷üÍRûþf³­/®e–ýe€FöávǸI\õªÖZ㵬< ÀˆÒYTŒ C§Ì¼ûÐה¾ÿ™£Ê_ÌÕªì¨/M³æsH· üä“Ó½SoF¾iû4Œ#ÜÖ,¼íÎp{â€6ü¥÷üÍRûþf²†·‰Ú-]dˆ1—æ"œƒß9¨—ÄjÖÆ_²8Á–# –è=>½èkÊ_ÌÑå/¿æk¼C +Í"’0rÊA;‚î#·lÓdÖæ,ª¶¾_Rû˜ÍàjÚò—ßó4yKïùšÈ]yV3$¶òWr™22YWqãèãSéÉ,W™·òæ¶MÛY²+¸r(CÊ_ÌÑå/¿æk"-nCnÎöÊ­YŒ¡T»*¶yèßþº^Y-d)夒Àl å•?…kùKïùš<¥÷üÍbÍ­M"º[ÁåȳÇãpV}¤•ê§nzö§|ão²IûðÜ?x¬ê ŸO¼§èh_Ê_ÌÑå/¿ækMvEvÅÙWÍæàûßýoéM»ñ”fH!WuFhɐH]ß08ü}¨kÊ_ÌÑå/¿ækûm‘åO!¥xË3€@Ø£nqëËSƺ’¶²´gxŒ©¹Q“ÇÐЯ”¾ÿ™£Ê_ÌÕՑ´§¾1ð·j8lœ€9úžø#Ò¡‹Sž}NÞÛÉæ lòÆ:s@¾Rûþf)}ÿ3Y²kÒ YÜÄ®|Î.1ØzûŠM{˃hæKug¸PãäUêAqøP¿”¾ÿ™£Ê_ÌÖgöÃ<ælÞF.ȟ8¶ýãíÔ}Iüj¦Ÿ®Í5¤,aiç‘°@?sŸýó  ï)}ÿ3G”¾ÿ™¬Ÿíä{ˆâŠÞjæ2\g>Yn ÀÇ<ç¶9¨­ Î©Ùës°ßsoû³åÈÃå,±Žüÿ:Üò—ßó4yKïùšÈ]wtK)´uŒF’ÊwŒÆ¯÷~¦¤mn0" 4’‘rNý g·4§å/¿æhò—ßó5ˆ5ɍÆVÕ¼¥Œ™°ÊûIÏz”ëè0ÆÝü© >áó႞;}á@ÞRûþf)}ÿ3Y驼º²ZÇù_¼äó•ÇOβ®uËÈmu`<³q°µùx*7}HØÇò —Ê_ÌÑå/¿æk.}_e›²¡óC4`öÜ#/Ÿ§ËYé¨êFظy n!imög‘T€Þ'µtžRûþf)}ÿ3X7º…í¬ÆÐÌdq$X–8b®½2 +~DV͑‘­PÊò;ó"oÄ +—Ê_ÌÑå/¿æiôP<¥÷üÍRûþfŸE3Ê_ÌÐ#Pr3ùÓè Š( Š(  +÷¿êþºÇÿ¡­Xª÷¿êþºÇÿ¡­X¦öBêWÔ?äuÿ\›ù±UõùÝ×&þF¬QÐ:…QHaHFF)i(˜Ò¬A$[®J„'žTùӍb»±üË´þõø݁ÏóÅW¶Ö¼ô’A.á»Ì©qü?­ZþÖ±Üênœôã¨Ïr2(çN´*ªaW ÉõÏó§‹+q€D<¥mÁ;œÿ‘PbÀ*p£y #œr;sÇ5bêîHē¾Õ'’OÐPvÚm­ªÈ"ŒâAµ·»?ËéóÉàqͺuµ›³ÀŽ”!-#78“Ó&¡‡UŠ{߳ƻ”œ,€ðFÅlÿãÕ4º¤WBÞI•e%FÞxÏLžÙíë@ }&ÊE…Z–¨ÀàüÃØæžºmšÜ ź D¦`ßí•ÚOåQ®«i(>L«! ‹× ÛA£>•k–;£Ê‘FÅ'ïœôÏj¹-¤;4‘†fIÉèGëUî4‹Þd$îÜk²î É‘ÇCSZß[^ò‰6`œzàQÁçÚ£}VÆ7™^áT rz uç¾;Ð1h¶ÁíÍ f%·2†³‚3ósV͝¹µ{c0>w!úš%½·†Ô\Ë HN0Ì1ׁP.«j̸•dXœrí×½*é6Kà pI$Èìä’0I$’M:-2Ò¯´$dKÉv ԀN=Î3MmVÉ]Ï]Î2£ù}3Ž=i-u[k–Ž=áfq‡·¶}hSajēË'“ü_{ó¨[F°f$ÂÜçåóo#Œãy©®5 [Y|¹æT}žfeÎ3ùÔCW±*‡í ó `äã‘ÛŸZiÖ¡q垡³½³»3œç;x§Gck2ĐªÅ(àèF1ÓéU'Öí`ˆ`d,E9êûA'·>¾•j BÖâáàŠeiS9_¡ÁÇ®€ :-‰R¦92NKyï¸ñŽ[9#c4ó¤X—f0J”Üvr»O˜ggÅê–ÐI*O B´É8UcÀô ùsSËu Vßhy’@!‡9ÏLzç"€maoj$#f\ogvvn1É$ž•é6K*H!Á@Ûo•Î ¹æ¥ûtÁ¢³£Ÿ—9õÈ# z½‹ÇÕ ©`­ÁPÇ#ØhÇÙ ò"ƒËT;J.OË·, +§y¢Z\Å:…(Ó“¹ŠŒõ!s€O¨ÅX‡Qµžs SgŒuÇ\øÏ5 æ±mk2Á¸<¥Õ +Ž€ŸSÓ§8  aÓm!ÎȲXÌìX¶zä’Iüj/ì[+Ëòä9ȝ÷tÆ7g8ÇÎ)?·,(yÔ9¨óŒãÏ5/ö­›åý¡7mÝíŒg¯Nœâ€û&Çya2»J‡m½1¹Æqß§>›i!ËEφ#øvö>œTcY°1ÀÚyuéÍ+ê֑î2ʊ às‘€Iã  ¾›jÖíĪâ;àA8=x5™¥-Š\“ÎiÈÜNîBÉ' õ©µl¼Öí ¹sŸA“ÏNœÓbÕ žâ( %÷–ãHàƒÏB(F•f!Ø(epDŒ0] îÎz u¥‹K³†&!Â0 +Abx XO«Ο6¡kǑ$ʲc$zpO>œùTK¬X¼eÒpÀà)$ç8 u àò= ,zM”NYb$’n‘›npÆO<àqDZM”LJCÜ—b ¸Éàgœ)ÃT²/ +‹„&eVB97Ýç¶pqëO´¿¶½ÝöiVM¸'‡¡ú …±ÎbîÏ'ø¾÷çPÁ˜“ wãÍ| Œ àdT÷wÖÖeEÄ¡ dŽ3Àê}€õ¦ RÈÌÑ}¡7.sžœ ‘ž™šcé2ct-îDŒ tÈcžGƒéR>›i"mhFܱÀ$cwZ}¥å½ê·8SƒÆü Aý¯c‡"pvœ`Kã^xâ€$M:Õ-d¶òËE!ËïvbÇÔ±9ÏãKo§Û[h£;”± ÎY‰=rIÉè:Ôpj–óÜyhà© +QáóéùS[ZÓШ7+–Æ0 Îz©í@}"ÆI$‘àɓ9ے g䑃Å5´kEF…ˆÉó_-“’ç,¡Í;ûZĘÀ¸Sæ+ŒôcŸLž9©çº‚Ü‘4Š˜F“Ÿî®2 ŠŠ]6ÒUÚч2ŽÊA=y}(ƒL³· +!P(Ú0Oh_ýT~‡V±H†åŒlð:óížj/í»/4!‘€òÚBÅH ´€Aô9#Šzhö)*H²”!” h;vçÆvñïDz=ŒmX[äP ©ÀÀÈ'Ðžiñêv’¼H“ò– +˜;²¸ÎGlduõ§\_ÛZʑM(Gn@ôÆO ÏÐZéö֑ºB‡X¼ŒäÐe‰8”‘é¶‘Â"X¾@T€Xž@Àäœô¦Úê)qe5ÖÇXây9; ãê Cg­Ú]I³&2ÛJ‡ÎåÜ3è€'m.ÍÚ&0ÿªPª°€Œá±ïš‚mÕ£“È_*Vè噶ó»Žx矔Š·imz\[Ê$ÛםÓÞ “ZÓ㕣{€I`žG_Ë¿¥%–‘ommå8óX©Vs‘NO_zwö=‰i ƒï‚ÞØäíÂôŒt¤¸Öl æBí–*ŠI8ëSÈ êö¥Â¬€‘÷󑳌óïŽÔ$m¥¼‰$Q°t,C’[É'œàu¢M.ÊFÜöêÇçä“ü{ó©à¸ŠáY¡páN 3U­µKYÚ(Ä«æÈ í#‘œg×ý(m&Éæ2´$±ÏÛ®Òqœgf§šÒ í~Í"ˀ‚0A#‘‚T¿Ö ±™¢‘\²¬lN>\3„ëë“O:½ˆXɸP$$ ƒÆӟNxç½Ik"äIænf,űŒ’NOsV<µóLœî#oSŒ}*;«¸-^wÚíQ‚I8Ï{ +…uK&—Ë[„-Œ‚:—v3Ó;yǧ4vŠḮx¸œ‚A¸ ŸcHºÕ‘órìn;I•Ý•ÇQŽhFŠ úŊ’áÈ]ØPNxÈçԎ•4w°Éaá%!tWˌLþtfŠ¥6©e’u `œc¯åšH58g½6ª “ Á—•À8ëë@¨¢Š(¢Š¯{þ¡ë¬úՊ¯{þ¡ë¬úՊod.¥}CþA÷_õÉ¿‘«_Pÿ}×ýroäjŨQE†„dKHÀ• 8>”ŒúK7™=ÐrªÊ¬"B÷›?6>‚6…Ñ*yÌ6³¸;{¶銡6¥¨ýŸ{þáAŽT œ·?‡çHº…ìÊR{††VˆyiG÷¹S“ê9Ç=¨ôú—2¬ÓÝ«ÊWd‡ÊÀ+»vgÇ5wP±kÈ¢DœÄäŒc þՇm¨Ý‡‚5¤l"¬erdM™g'ëþµ¥Îïm\˾é“Ía·úPvжO™ÌÇÝÀ?"¯¯û4Éô¹®u+‡yBZÈbb»r_aÏL6à:¨tÛ[µI—gHˆ=GÝ;²:c’GµTº”¶»¢¸áVVY0Ûöãn{~]i—ú•ý¤§œæDÜÈæ0‚ŸÄã“@ÛAûÂ+²©#)”Á-µ÷ ñéÞ¦Óôu²»3,¡ÐT]œ¨b ÉÉÏAÐƳ^îý£¸{vòÕcša¶<ïeÆÑô<Ó¾Ó¨¸11ˆD%”(;ȕ€Bjÿf'Û¤ºó]™¶ã¦QSÿdýieÓÙ´¨¬¢£1ª(qžBヂ=k?NÔn&ÖR frNÒC·YGP¼ýãZÌò[iÏ$R,LAv¸géÇsÅ%žöm2K#(pÆC¸.Üobnjú“TeðÔS6én±TSµp8?ýô ôª¿Ú7ÞIžÙIÚ8¶+G7™tôvG^=k£·möÑ0s (ò1»Ž¸  ôÑÔ\fY¼Ëty8¶à‚ùݖÏ#“Øu¦M¢o™¶\l·iDÆ=™;€ïg¦ã—w{;_ÀÍÑe 9EtŒsÏ}iÿÚZ›Ítªà:—6dÆáOâ9çր4áÑR$E1ÚXôõŒ'ôÍ@¾Es‰ÿvWS-»nÜç8Ƕ?«=Þ«k̲´ä4ˆ»£pCqߓKî£4Çq”VY/´eyé×Ó­^¸Ñ<ÈÊÇpˆA–v6®20AÜM“Bc¹£¼a+«#»¦íÊÀÜsÇ_çY¨ÝÛ£;]8f˜³!\žTà`U»ËÈu`UcO´TÎ)«ný€×C{THã¼ýÖÄIG”2ásŒñÁÁý1OÓ´‰4åTèÇäL˜ñûµìy<œõéíYö:…õÔñÛ­Á*f\É´°Æäôãï.+CG¿žà;]»[·¤\ïaìOO¥M¨éÍw*ËÀ¼¦…‰MÙF œr0xþ•]ô;y`òÇË ç®6ãò­icI£häPÈÆ¹«K&ŽÒÏìá­äk¹Cº¯;C>?Lb€6tÝ;ìQÊE‘寿TÚ0rZªº4Ëj·£0h„ayÏÍÏÍ×Eg¦©©¸cfT8P§úÁ– q×°éÒ£Åé¶?¿È$á@TàœõãdP¤z€#CrVe`ÛÙ2 ç~ÙäA³æGfݜçg¶I>5ÉWž÷t ˜Dr2àg±A×9ç¥d›ûøJíåyÅ<Ùq€?vé“þjõK™!ß9‡|©…‹€ ،ÿµÇµiÙéŸg¼7rMæNÁ÷›A-³ ÉÆ`Soô£y<޳ùiÅs¥k^i¿j»v®#vç;$9üÅTŸÃÉ,Í"Ϗ0°2nʗ-Ïæ#œý+^fe‚F_¼‘õÅsrßêpÉf(Ý$QÈ7&<Çf;—Ã=s@—ÖopÐI  bU™7ŽA‘ëëTƈ|Ì5Ñxwù»J ÅölÎGnøÇ_n+. VþuŒCsæ<‡l£Êâß"ÍK~Y©´uººÎPÑ,£k¦í¡GÈøóÁ÷ÏzãCó|«€Œ±Ç-x@Ã#~~ Ž”’hr2ÎUˆ-½IÞvl%°Fzõõ¬É5‹ÓF9ˆLÈ<æPC°ÛµA09n˜Õ%ž•ö;¡2M¸aƒ)^¹9뚣­Ï-¶¤’G;CþŒÛ~]Áßëþ¹7ò5b«êòºÿ®MüX£ u +(¢ÂŠ( „ÊZ@A>¤ÓÑÖE „2žâ«½³¿Ú¸U‚ +õYˍcŸ +%2?°þïҀ'[¨Œí `$SŒüf¤b‰™–>•XÙfåå2|®ÊÅvŽÃšT¶•­^9æß#9pÀp¼äì(Är$¨6 §¸¨Þæ$ž8K#œ`vàŸéL6óïƒlà*¹y0¸/ÇõÏáHÖy¸I¤*ÉæmÚ:àƒÏК²î±¡g`ª:“B:º†B +ž„UO±¼¶~UÔ¾d›üÍÀprö)RÖHºÅ(H£fi|œà}2søP†î?µý˜dɍǎƒœ*-–#$Ó$edvÚäõ;x‡øÒ¬,.Þo3åe ³o¦{þ4Øá•P0+7’£9éϵW¸Õa¶¼šÞfT1Ä$›³ž?Jm¾·i*Aæ?—$‘,…OE%wc>¸©äÓá–êiÜibà€pz~uB/E®Ù›Ë + ©Q’ÁBƒŸ P¡­XŸ+÷Ø€ÊH `ô'Ó5%Î¥¤Ì“°\€9$žØª ÄÓBë)#pTáz}*ký(^;¸˜ÆìåC=¨K½J+d¶“ é;mAÏLôü)£X²,fݸŒ€O©ªD°UŠÑWslrŽKc𧂐Ô*ë6L„Œ qR§vâ7p3RZêV·r:A(fAžFq‘ê3U,ôE¶¸YZrå\>6àd#'òoҟ¦èé§³¸ åÆ +µ}3ß·å@]nÁ•˜LpGÊ~~vü¾¼œ}iÃW³&%’Òç)ÈÁÁϧõ©©[,Ïcq’!1‡àà†Î~ïOz’ (`ò„ŽGîùÿsþU,újͨ-ѕ†6|˜êWv9ÿA¬X‰€óÇåÉÚ3éÈÅ>óT´±%ąN”œq“íž*™Ð"ûH•fa–ÁPwa˧'rëNK©]ÙÙK¢§ì¶êckK;HʬXTü¸8;½9ªÖú­•½±ŠhŠBÆÀó±¾fϦO_zKÝnŒ¸¹tYwîÈŽr=êa£&f/37˜²§@0$!ê?Z°º•³Pù‘³¨â?:·Yñib™®"ÖiSc6wn3Z´QEQEQEQEQEQEQEQEA\,칕ª¶zÖ¥¢€ +(¢€ IKJ ©ôQ@Q@Q@EBÒ4i†‘·9êI©h Š( Š(  +÷¿êþºÇÿ¡­Xª÷¿êþºÇÿ¡­X¦öBêWÔ?äuÿ\›ù±UõùÝ×&þF¬QÐ:…QHaEPEPEPEPEPEPEPEPEPEPEPEPEPEPErÚ#Ï{â-m'º¸1ÛNH$!Tc¦*̺ÕĶ·÷¶‹ +ÙÙî¤É2²õÇ í@SK¸šëL¶¸¸EIeŒ;*ôŽ•n€ +§>£o¡oe!a=À&1·ƒŽ¼ÕÊåïæ¸ŸÆI¤k$¶¶„ÿqÏSøր:Š+/Ãú”º¦œf¸cš9^r¹S‚GµjPUîo-í6ùòªèSøUŠç,›í6Ԅ˜" tD°'&€7áš9âYauxØd2œƒRW-ሼصKBòǯ³c• qÇj±àë™îtë£q4“4wrÆ­#d…@ Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ïÔ/ýuÿCZ±UïÔ/ýuÿCZ±Mì…Ô¯¨È>ëþ¹7ò5b«êòºÿ®MüX£ u +(¢ÂŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( WÃò2x“þ¾ùT_ðŽjgC¹ÒÄg.Z2 ùlá¿Z—Ãò2x“þ¾ùWU@Y¤±ÛªM³p:éSÑE©[jzÜMnÍz˱›?» +08ïë[ôP-'OKÓⵍ‹ìÉg=Y‰É'êjíU±¿¶ÔGµrâ71¾T© :Ž~µj€ +ǟM¸‡[mNÈÆZX¼©cA¶*‹«{U qëþ¹7ò5bށÔ(¢ŠC +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€9_ ÈÉâOúø_åZ¯©Oq{sk§El?y$„…ÜFBŒw¬¯ ÈÉâOúø_åSÚØêöשj¶­Ôæa;±Ü™ê6ã“øÐí«Lö±5ÌkÅAtS§Ó55 È''֖€ +Ç}Vi®o#±Ž&ŽÏ‰e” c%F=;šØ®Lhº²ZjÖ4 ԒJ“;Ûwð‘Ž>´ +ÇAŽæEÛ%܏pÃýæ$~˜­Ú¥¤Á-¶ ¢'”ŠŠªs€žæ®Ð\å£ý«Æº‚ˆ[kdDSÛqÉ5ÑÖ,Ú}Ͷ¼úš$ÂxDRÄʹ䆀+x=Êǩڌùv÷ޱ@yÀ¦xþA·ßõÿ7ó­=Mm:ÖQ3+Ï<­4¥zdöJÌð?üƒo¿ëþoç@5Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ïÔ/ýuÿCZ±UïÔ/ýuÿCZ±Mì…Ô¯¨È>ëþ¹7ò5b«êòºÿ®MüX£ u +(¢ÂŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( WÃò2x“þ¾ùWU\¯†?ädñ'ý|/ò­ ½sËkÅ´…&k™äØ ã;AÁÉ  ª*½ÏÛ, ¹Ñù¨kuíV(¦I"E<Œd³O®kÅmu=ޗ§@°²\΃9‚|ÄÇJݵ¼¶¼öi’_,ím§;O¡«íRíqÊø2läŒuÀÍK@5™QK1 +£©'¥:¹ÛoðýÔí,¿» µ°¹ÏSŽ¿tÀ‚ ×5àùß×üßηì¿ãÊßþ¹¯ò¬ÿÈ6ûþ¿æþtÓQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE^÷ýBÿ×Xÿô5«^÷ýBÿ×Xÿô5«ÞÈ]Jú‡üƒî¿ë“#V*¾¡ÿ û¯úäßÈՊ:P¢Š) (¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Šå<2ÊÔËM"úÒîæ/ìë{…–v•n¥“*9å:’+¥6fc7Ù óIÜ_ˉõÍY Q…ã8íKEW>Ò¼n;¥§äîÀß=*µ¥Œ6fVŒ3I3n’F9f>ôjŠ( °|[m{£Íeehfy@ù·ª…Á÷5½ESÓäYD.a6Ί«:žƒÔÅð7:mñóý7ó®Šh"¸M“D’.s‡PE$ðÛG²ÞáLçj(QŸ^(Z(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+Þÿ¨_úëþ†µb«Þÿ¨_úëþ†µb›Ù ©_Pÿ}×ýroäjÅQÐ:…QHaEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP{ßõ ÿ]cÿÐÖ¬QE7²SÿÙ +endstream +endobj + +635 0 obj +<>>>>> +stream +x…̱€0EÑU~m@ÃöqM ××rŽÝmÞ[›‹£ß`JK•œ`tfD(•t¸º’” ãĵ àùŠÊ¼U™ íã禮 +endstream +endobj + +636 0 obj +<> +endobj + +637 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÒA0!íڕØý {ÿ­XÄ"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"! ±HˆEB,b‘‹„X$Ä"!‰À+Ü +endstream +endobj + +638 0 obj +<> +stream +ÿØÿîAdobedÿÛC +  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YÿÛC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYÿÀÜX"ÿÄ + ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ +%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ + ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ +$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ïdyÖd‰bŒ³+7201þϽ/ú_üñƒþÿþ&žÿò‡þ¹IüÒ¬UlNåOô¿ùãýþ?üMéóÆûüøš·E+ùÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™Sý/þxÁÿÿGú_üñƒþÿþ&­ÑEü‚ÞeOô¿ùãýþ?üMéóÆûüøš·Eò y•?ÒÿçŒ÷øÿñ4¥ÿÏ?ïñÿâjÝ_È-æTÿKÿž0ßãÿÄÑþ—ÿ<`ÿ¿Çÿ‰«tQ ·™N7¦xš(Ã*«q!#?ìûQR'ü„&ÿ®Qÿ7¢†þBÿ×)?šUŠ®ÿò‡þ¹IüÒ¬PúêQE!…Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@RPÑMܸpÁéÏZ]ÞGyé@ E7rñÈç§=j“êöH™²wä)8\ã'Ðgր/ÑH`9E-QEWOùMÿ\£þoE ÿ! ¿ë”Íè¦Äÿä!ýr“ù¥Xªïÿ!딟Í*Å .¡ERQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEUmB9e°ž8ZVB1 Errhׯn"‘X—™W÷d•Ã`‡¡8üjWÓ5-ÛÝ×·ádvÎIúàWOEr©¥ê =»­¹Û¨Q$¡¶ù'‚1Ç¥\ŽßPµ²6ÉkË$E7a9ûÙê9í[ÔP6›{X¡-¸Æs늚Š(¢Š(ºÈBoúåóz(OùMÿ\£þoE6$ÿ!딟Í*ÅWùCÿ\¤þiV(}u +(¢ÂšÍ´tÍ:£—  ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þÉ¨è  <ßöMoû&£¢€$óÙ4y¿ìšŽŠ“ÍÿdÑæÿ²j:(O7ý“G›þɨé‘ͬâ9Ìgk~éô4?›þÉ£ÍÿdÔ[—vÝÃv3ŒóI,‰ m$Œd±èMæÿ²hóÙ5M¯­RÙnâ1t·§2‚A€&Y761Š}BŸ|TÔQE]?ä!7ýrù½'ü„&ÿ®Qÿ7¢›ÿ„?õÊOæ•b«¿ü„!ÿ®R4«>€º…QHaQËÐT”ÇRÀ`â€"¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•2Š–ßÞ•[x~TÊ)þ[x~TymýáùP(§ùmýáùQå·÷‡å@ ¢Ÿå·÷‡åG–ßÞ•r¾#¼º°½‰!½‘bºÁpL!HËlu©.±{mmxñ "K˜]0Aí|õ®ì¢y|ǎ6“nÍÅyǧҒ+adh¢‰ +.Õ*¸Àôª¹6Ôä,æšKÍ"î[—3ɾÖAž®3éQÙ_Ï:4W×RºÎd·ef »ùÇWõ®Ài–ÂDAtbêÛ9V=H÷¤MšÌ²‹hŠÅ•¼¾A=éó!r³„‡2ivºdƒ)<›“Ð00üñ]‡‡ç7%«1˪lo¨àÕåÓ­Ô¡Xaºá>ë¤{ÓâµHd*‘®I®OZNW‡§ß5F¨Cd‘ùT•%Q@ÓþB×(ÿ›ÑBÈBoúåóz)± ùCÿ\¤þiV*»ÿÈBúå'óJ±Cè ¨QE†QEQEQEQL;FÂ6磐? +̗Y÷^UÌIxs½e @PX’1‘À¦K®5°-ue$Ja3 . F`rã¿­X½Ò-¯%WtUáÖMªpÊTäþ4Ø4é…ÚKwr—+- +¯•´°b¤–9 Ÿ”t­:ÏSÏ$Æ •J€7† [ƒôSM]fݯ¦¶Äp 3?+.H?Ê¢} c¹óìž;f,DY\„eä:îý*#áÔa7-ÂÆ¯…ûêC¸ý8  Pë0>—o$rD“°@„e”–Ç8«/¨ZF›žt®áî3Ž=y⫝0ý†+a0ÂMæ–)ÉùËc¯zÕQ Ê6x¬ðcÉ>W»æçžxãnçZ²‚J$,U^§œ~úÔ«©Ù3º‹„%8onqüê’h@C2½Æç˜ì;·d ñT“F¼žI!¼¨Ñ¡*ãæ`}rzzƀ6åÔ¬á IpŠKú×>ŸZdšµ”hìnìm¬r3Œ}9ª-¡ÌÂVûZ .¤ÇÉà«áFî'œš²4„[ƜJpÀ¥Ø ÿ²Ð–¥½òÈSryJ¬û†s×éJu{šnSfv÷ÎqžzsŸNj;-1­!¸Œ\ʪªê¸)„ +S“Æjðü¾N Ì&mÄï0±È*É߸ž%¿ +Ðþ׳ß2™ò]P¤î%w ¸ëǧ¥i˪Ø7K¨ñ°Éœÿ8'óª°è̊<Û¦•ÃFŊÿpç¹'ó&’m eŠ4óʘěNÞìêãðGèc­Y‰ãO0yo¿˜rì#!½5#jÖŠírŠ­“ÎxÇ\úvëëU‹+ek·2‰7´}õ;(å’7¸Pñýî¼tÏ>Ùô¢ßR¶¹iÄNJ™ÈÂá†A¿RßH–ù. Ð)#»´j„nÝÀæ ãŽÙ÷Çë=)­,§ƒÎ‰bS·n QžOր,.«bЙVå +d Œäç¦SšŒë‚DÃ$Œ9 œŸAÅgéUÈh¦½\°¬h Y{ýãÏAS&ƒ°[“vHÙÏFÆ9ÿkô  £W°14‚å +©õÏ=8ëÍ?ûJËÌý¦=ÅCxÆ29éÒ¨K î£Dб¨/GÊ¥{yÏ­Fž(ѯڃÀ‰´+Ǹ¯;rp3ŸLûЊj¶.ªVå0Ͱg#žÃñíëH5{‹¨ÎÎO¿8õçÒ©A ·š9g Ó2Q +€ “ÏãUÿ°nMÊ«ÏG*±9‹!‰Ã Ù'¨ÅlÁ¨Z\H±Ã:;²ïUHõýiªØ¢»5€±‡9 Œ‘޽9¨lt•³kfîhD™ùp¹÷àqïMŸIv½’òÞácØ‘¾=êER1‘ÏÈhhµ8¦»?d뎌6†¡¤‡Y±–ØL./ˑÎyNpzzKM)--'·I¤¨¨ €'ãÓ5KûFÏuÍƑ“ +@läï<‚>r¥ý§e½í1’êpr<Ž}ðqô©Òâ bHqýӜÇò¬hü<Ñ´*.÷Em +ñ– àä¯8Ï¡>õwNÓå³.Ò\,²£…Hh +™ÆFNOÌsÓé@“U´@Œ&F,m-ü…7ûfË̑L¤ÕvÒCÎݾ½*½¶†¶þX–U•¦9^®ÈÊ߁'8íÒ¡o¹XÂÝ&#X•sé:0îÙãÿ¯@jö• t™` +ã'9éùúQs©Ã œw1æt•Â&Â0Ĝu<U¡ÑŠl2]4®7,ËÉٟÿ]:]#v™ ¢J¹†O0#ܬrN +ädsë@aÔ`‘yhdl~êA‡ôã¿NÕ†¦·ªÎ š8ÂïW`a’;Ï:Ô?Ù=Ì3]³Ï +” Þþ˜ôÅK¥éòØ)F 0ˆ‘lïÍÉË^>”'öµ†#"ê2$8R¾9ôçŽ{Ð5;PȒJˆîØ9þ-£'¶HÇ=øª2hLd˜Çrn ƒIRåð¼ð~b3ÏÒ ·ÐîMǜgû;#l.IA#6A ßv9…i¶³§¤†6º@ÀG<`àþã>¼T÷7–Ö ›‰’ ¹,qò‚?¨üêƒhŠa1ùǘž<íþónÍ>ûD·¼òA%$Þ㯘 Sè2ªà4fMNÎ)op÷mÇ¡àséÉŸí´±K"L…!%d9ÆÂH>œVJøy’ÛÈ[̉bò®ãËH 39ùNY½{zUéôß2ÛR‰% +oI%ŠçfQS¦yû´bÛiáy£•LiÓÆÞ3ÎzqLƒP·¹‘ ÃgœàŒc·^â µÓf‚Òævٗ„dy\cŒ“þ™§i-i*Jó#ºîÎÈʃ9$’Iã©&€,Ë¨Û ;eF`pW<ŽpF5›—bÌ +ùfBøù@'מ•ö.ngŸítî¯ ÙÁÚ~\zqǾj+¦[•*«:²àŜeƒüô  CY¶këkhòâpؓ R?„ƒÈ5.™¨Ç©C,±#*$­ÝüX=k=ô–6ó/[ÍÁê§÷y9ùrIãžýëKO±[äDl«¹n˜Å[¢Š(¢Š(ºÈBoúåóz(OùMÿ\£þoE6$ÿ!딟Í*ÅWùCÿ\¤þiV(}u +(¢Â›$‰‘Õu,p)ÕCY¶k» +Eæ–uà1‚y  ‘Ȳ d9\òé7L¬ÒdF_,ùòHë銳¤[ÞÅp Ôl‹&<— ·“‘ƒéޏ  e‘ÝÔ²`0•ïÍ8:šÁ“J’mRêV‡l¾ï¼â#®j¤:v£Øy-šeW- ?(iî–ìA98 ¡$EteeaÊr¥®Li:„j‘ÅFÉm²7ŽPo(® Î~ñèsW&Ò¦ŠSå۴֞j¹·YÜ<²8Éïs‚}è ¢¹ë=>ö9ì¼Ø\KS%ϟ¸ ‚˜'=}°zç5&¹ayuwÚïÎÀ¨á€6ìîÁ útÏLcÔb[˜a}²Ê¨v—ùŽ>Q€Oê?:–¹‹½yFò<É%IÑòã’Ò«!9=”~˜­=vÞ{‹X’Þ4ùÕH €°œué×Ð’:¹`§%ÓìÉ¡¤T*à±ÀªZU¼ööó,á‰fC>óŠ9?Pk6ßK¼û— º4…ï3µ0pŸ‡¯¿µo‰ÈP:ï%sÈêådÒ/B®ÛvfýÒÈÛԗU$ryí×­M‘s:Dn¡©Œ¸u>ÜdÐKHX*–'rMfEgq‡%¨fY¶°Rž¼sY¶zEÓ)K„e¶,̰³Œ§È<>öO^ôÐÃkÙ²€”[w ‡Æ$%pG¾3ÍS“EšiŒ’),^BO˜GX ß³~TÐI"Ç;ª£$ú +Ueu § Œƒí\âØjF³¼MŒ—ó €ƒû°1×=Aö¦®™|²ÀîÃvù<ý¢T ƒ#=±ïÍtÔVfmOš¥l#+ómç×qžý¼ÒZ[Ô´‘:9¹ã¯'  ôÉ®cMÒoDPÃw"Y—só#l“† üØç‚{КV ”vlÙ?š?t6àyÉÈ#×=è¦GY2œ©µÍ&¨}©KDà¬[D©(;Hù‰9ëØ=é³h×I,qÈñŸ,˜’Aó˜'×ž¾üÐOQ˜DâàHs…õàê+åÌ-y™]dsy‡” +A}qüê¤^)·k‹fºŽ!†ŒL2~@;‘ÜzûÐUEbIa}ý—m²5ɏg #qÆyÁçÒ¨¾•ª…‰ %vþøDeèÈĤd÷[iíòÐP$FvEe,¸Üäg֖²tm=ìf¸2',‘§˜H&B«Éüýj &ÒöRi§€ªHÓìÀœÊœ/¨ã±  Úln²F²!ʰ ±¬«:ioÞä¸\FQ·ã÷` ÃéœñD¶wMá»{S½®8„Š®2Ûq¸dðsƒßŸZÔócó„;‡˜Wvßn™ýiRE§ ?åN‘µ Û¼§Ê‘#P¦Ì +çžØ=3Ž‚§“NÔ Äãi¹o5$ƒ»99<¢“ÛŠéh®U4kæ[ VEÊawH2òoÈpAôãœzbµµû{‹›TKxL£wÌ€`1Ôdù熀4cš9 „`J­ìid‘cBÎp¢¹‹½"ý¢;F,NÐ%k1É ÷ëíVMÔ>Ñ+’Y Qæ}Ö ¥›þƒù{ÐCEsšeµÅ®±n—:¹†O2S&á3n_›þx©õ:w¿’xci q’&<ÜÈÁ8î§¶q@´Ã"T.¡Ø«žH˜¬Ø-®£Ðeƒfgd“dm!ùA'j–° dÝk%t‹·UÿFxØ$ 3È£Š ¤í À'ë“@A‘DŠ„üÌ Øc?ÌS«ŸI¼Y.Ö×+ <µc‚°ü£Óî¸öÍUºÓï)åû$Ÿg+)†ÜL€•L7UÏ‘»Ž§L³GS#… BŒ÷&–)Rh’XØ2:†R;ƒÐÖtÖòÜé¶!YLnÙ=€æ³t½;P¶¾·ymø@ˆ]œª# +q†Ï\ñ‚>”ÒÑXZ݅íÕäok¿;@G ‰³œã ôî3Q2ê[¼‹f‚ܸó‘¦Ü&ùó»¯LzàöÅnOw°c4ò~˜õ>Ô¶×0ÝÅæ@á×%Obêìkû;Q‚,E¼…#…u/Äü¥¸rk[Hííœ<-ç-‡“{œ÷c“ÏÐâ€/ÑEQE]?ä!7ýrù½'ü„&ÿ®Qÿ7¢›ÿ„?õÊOæ•b«¿ü„!ÿ®R4«>€º…QHaU¯®…¿žë˜Õ†óŸº3Éü*ÍGq +\[É £)"•aìhã]™áÚەSs³6~­«É+9äC†Hف÷©Ç£ÚùP- žT ʂî䃊ÐtY‘ÆU†õ„šô©I5¡ûDˆŽ¸a´†Ï$öä!×¥¸š?*ÝLs¬~P-‚·Ÿa°Ö‚èö F°ƒìÇLä~$Zu¤N¯*¬»vòxÀ cð'ó  Åñ–ƒu«¢HQX“Ê—m£¼ã?Zrë墑ÖÔ¶U\2sՇ¦Þqž¢´?²ìüÈäò@hñ´ pr23ƒƒëL]Å#tXHVÇI+ށN~P2xë@Gˆw$$Z7ïÁüã—,eO¦ºúS&ñ'ÙÒC-£$¿›óSƒSÔâµM³xR&J"”Q“ ¯\åAÏ\Н‡e•d.¾c8›¡ÆAçæwÍY³»k‰®bx¼¶öýì†døŠÿR[)’6Œ³H¿»ÁÆöÈ~¼þ†®¤HŽîªHAc끊¯}d·‚ÜH$ÆÅw`o¡4¹4^i•D§xDtÞH2O¦UÔÖ£aþ¯<Ä8pGϞý8ÅX}&ÊL;Ve9äçƒþÑüè]"ÁdGàÆ>cƒŒã#8=O_Z«i­ý¢UCPÎU\8en à÷éÚ´lîÕ¬s…*gµV-€B¾AÁ9ϘÙ¸9Èú +»i Kj`ØPTºÙ]@Ú­³\Ƅ¶70üºóLµ×Öæâār]À °ÈÖ´……°»ûP÷Þ»Ž3뎙÷ÅBšEŒr¬‰H#çlp08Î8  Wˆ-¥˜€Dh[€è=O±N½0Ù)¶Îÿ7-°)O¡Î+rX’hž)T28*Ê{ЍšEŠˆIܬ¤³³ÎO°ü¨ŒÚ쐙#{3çFåùxElçþüh]uçxþËfdŠWŒ\.O”$éô8úúVŒúm¥Æã,\³n%X©'zƒèǵ=líсXUJ¶ñŽÇnÜÿßd†%µa+‘å+63’G>=è_?—¹ìw.èÀpsómçÓ¦jàµÓmÌÚ«“¿s1!vóÆNç§Jx´°¸‹hJr€‚G|ð~¼äPjxÙÖWhO/Ñ÷º’O§ËZÚmêß٥©L’¥Obê)ŸÙ¶>YÊ!Îá‚Nsœç$œõæ¬ÛÅ,p±¨ã?­fê¿Ù§¸¶7›»ÎO?]Ücñ©mu3q¨½¨‡*€æE`ÀŽ:ž^ O%•Ã3I;IÔ瓕Ûùc4CciË\ĀHs’3×8÷ōٲ¶¬fVi5Pq’ÌsøÖyכl„Z`Ϝ7”ï+ǯB~žüUýNÒ;û&‚GØ7+†ô*Á‡ê*¼VvVVéå¤ù ¾Õn+{eI o1ùZAÆã€§Œú(ü©cÓ,ã¸IÒ² pNÜã¦qÆz‬\Ég¦M<#.˜ zò*£kR«ÉÙ ¸ˆ·˜Æ0ª­{ä0ýkRâ®`xg@ñ¸Ã)èj¬,íð«· ³͐3’NO +?*©g­­Õð·S…Ë8“~q‚­TáÒ¬ œM;\Ãçb3ŒgÆqÅX¸…n!xœ°V;N f>³"Êð BgYv—!Wvsô¨âÖne3ɪ¼(ªëóà…+¸çß·£q§Z\ó¢Î[y!ŠœãAôâ¢m'O™3ä¤ç*ÄvÆ8=1Æ:PøHÔ˅¶r„„œ!wtôÇò­;Õ¾Y%>Z¶ÐÙûÜsBØZC9¹p:î;GÎ:;⥵‚h;dTˆrôæ€&¢Š(¢Š(ºÈBoúåóz(OùMÿ\£þoE6$ÿ!딟Í*ÅWùCÿ\¤þiV(}u +(¢ÂŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š(  íJÖ[˜æŽ #ªŸw*Cœ{u÷Î*€Ò. ª>Ï·O(ùcŸq]Ê&‘}ѼvȪ¿2©qµ •œ÷ô#‘úÔÏ ÊÈÌU|ãÛÏß2nòïøWKMgTf +=Î(ÄuÍóFmÁc庿ã úuÕkÝy`v¬×f Kcp`BçéÇÒºJZÊÔ4ÖºhÔcŽEÆà?Z¯&™tú¡jpÒÏ÷n>⎽¹·h VçG¼•]£·1£±Ù”ۍޝjfÑo yóÚ76eÞrÊPúƒù×IErçH¼x¦Åº¤nŒ¢ßÌP.}9#4ét›Ýñ(ˆIÊ[+&Ò¿0#“Ð`ví]5Ëˤ_»>Ô©c#ùŸñò «íÀ#Ÿ_JžÒÒytF‰ vŠÆ àíéÇ¥t4P5‘wºÈ«¶e–GDÞD12…㏼AöÍ\Ñ,'¶·ž ¨•ceUà䱯œ§<Ù¢€9$Óîî a&ë‰á˜Æé»*¦Ônx9ûßð/j³‹p‘F÷nn#t%‹rê!G>­¸þ9®’ŠÁ¸Òn_A°µ„wF±± €]Ž"H÷[û"õ!_21rC:ˆüÌm]»Pä÷gñ®žŠÀ³ÑçŠ{y§"I£—&MܕòÂÿ<Ó.ôÛÉõ9äX€VRE“{“ù +訠 -oMº»0˜b#Ù÷ÀßÞçúsUï4kÒ´heFumðìœ:ÿ:éh dh-gz³Lí4±¢©Y0ˆÂœþ ×B»Ôě2»~fÏB1ëRÑ@²[™´©!òœîIÛÏ<ՙ$–%·ÂYY‚È3Ê u÷çfŠ¡oÊ_4#Î샎yã¿jŽQst"/nAŠgÜ¡ñ¹pB}òçZtPM÷¥ª˜üÝͶV•8>üàΒÚ)Rá‰WT çsîÜsÁö«”Pl‚êêžFÔ¢¾>žã4m<ÙÆaÞ¤•åqš¹Eg[Gt‚c:’Ië’O·>•nÔ¶ŒA +8=ªj(¢Š(¢Š(ºÈBoúåóz(OùMÿ\£þoE6$ÿ!딟Í*ÅWùCÿ\¤þiV(}u +(¢ÂŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( “µ-!èh AÕï5;¾™-ã^E*¹Ýò’:þ‘ ·xNã\3Èow4èC|¡C}Üzb¯xžd=yÿ¾Rû5ÜŸCò5Ó3B„/ÈT·ÞϦ(­³œ]YÃ8é"üÅOPYÀ-là€ùHò=Æx‹X³Ô,ƒÄ²‘ev½”„“ka€=ø'ò®Î¹ ,M®ªh÷òI$–΋¸6î@ö!uÊAPGB8¥ªÍ&‹d×(ÑÏå(‘Xr åW謖×`7p$5¡b©¹Ͻj×.$¹µñEåÜvs5¬–ëç(^w^ k^ßKý˜×Ú{Á$K“/Ÿ˜Ûý +ýõMÒöDTyÓqUè+LŠhü!©ùÑ<*í;ÅŒCӎÝêÿƒäRÓ?ëˆþf€7(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š®Ÿò›þ¹GüފþB×(ÿ›ÑM‰ÿÈBúå'óJ±UßþBÿ×)?šUŠ@]BŠ(¤0¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¤= -!èh›ðü‹¿öñ/þ†k¥®cÀî#ð˹è³ÌOýôjœ7W“xb}wíS}¤3JˆìîíéŒPgECi8¹´†ué"ˆ©¨¤=(# ƒÞ¹_ êún›qk2Ê ­Ä‰4¡ æà“ôÅutSQ•Ñ]2°È ðE:€ +(¬Ùu«Xgš9ª°0Y$1ŠO=hMoþ@·ßõÅÿ•QðoüŠZgýqÌÕÝd†ÐïJAˆ#¿KÁ¿ò)iŸõÄ3@”QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEWOùMÿ\£þoE ÿ! ¿ë”Íè¦Äÿä!ýr“ù¥Xªïÿ!딟Í*Å .¡ERQEQEQEQEQEQEQEQEQEQEQEQEQER†–ô4Ìøžt=yÿ¾P =·„n4O&C|Y DpÀ·ÞÏLc½høþEßûx—ÿC5ÒP0}–Æ3Ÿ*5_ÈUŠ( =+’¶º6zƳ$–³¶Ÿ8G|DK#•ÁwéÎ3ÚºêJÅð‚Nžµ[„ta»j¸ÃÜvçðÅmÑE“¬jÚG$7vÓ´2Æ@xã. þéÇ Öµ%s–I<^)t®’-³¯÷€ÁÆ UÿÈ¥¦×üÍ^Öÿä }ÿ\_ùUÿÈ¥¦×üÍnQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE]?ä!7ýrù½'ü„&ÿ®Qÿ7¢›ÿ„?õÊOæ•b«¿ü„!ÿ®R4«>€º…QHaEPEPEPEPEPEPEPEPEPEPEPEPEPHzZCÐÐ7à?ùíâ_ý Ñ-î¤|e-´±˜Ùd1H8'8àŽ†“À„xâoýÕHu+/øXÉö˜¶5ªÆw³Ó>´Øœõ¥¤¥ ºïFRHÜ1‘Ö¹y.Ƈ­éö±µì¶³)†O1òàeXrO9ÅuUâðcÒá½_½es߆ìÐÐõ-"Êt#"–€ +¡.±c ĐI>׋aØÛS=760?Wë›Ö~Ãawq,ÓMh.“±|Rvü(WY!´KÒ ÀÄôª^ ÿ‘KLÿ®#ùš¥¥‰Ç€åY÷åa&ñ‚S¿¥]ðoüŠZgýqÌÐåQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ÓþB×(ÿ›ÑBÈBoúåóz)± ùCÿ\¤þiV*»ÿÈBúå'óJ±Cè ¨QE†QEQEQEQEQEQEQEQEQEQEQEQEQE”´P^•£G¤§—mspaÜÏå¹R2NOl֝-QEU{ëHﬧµ˜f9£} X¢€*éÐÏog,ˆ„ZµEQESP²ût \Mn¥XG·æê .›c™§Áes+µKœœ{Õª(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(ºÈBoúåóz(OùMÿ\£þoE6$ÿÙ +endstream +endobj + +639 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÒ1À íZSìƒ ì tV0J‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0R‚‘Œ”`¤#%)ÁH FJ0RƒØâ +endstream +endobj + +640 0 obj +<> +stream +ÿØÿîAdobedÿÛC +  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YÿÛC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYÿÀâ`"ÿÄ + ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ +%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ + ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ +$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ôCþA÷_õÉ¿‘«_Pÿ}×ýroäjÅ>‚êQE!…Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@¦ê—Ú†µ©Úƒo6S!,ùþ•qµí1oM›] NÒ0p¡lc?iÑIK@‡‘XÚ}ÔÐëך\Ò4ˆ±¬ð3œ¶ÓÁ÷ÁþtµEPEGRÕ¬ô´{#DŸÞòـú(õM.õ¨–Â@ žhX>‡©øoTŸU²ž[•dŠâHv)Æy&€6(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š¯{þ¡ë¬úՊ¯{þ¡ë¬úՊod.¥}CþA÷_õÉ¿‘«_Pÿ}×ýroäjŨQE†QEQEQEQEQEQEQEQEQEQEQEQEQEQEr¾ÿ‘“ÄŸõð¿Êª¨›FþԆy!XüƸ…gŒœq»ûÙãkÃò2x“þ¾ùWSŒõ ,'7V0Nc16ÃÕr:UŠ( ¹­fæßOñ^“s,ñF$ŽH$ÜàaO ŸlŠék7[Ó?´­¢òʥĬÑ;Ðû‘@ã‘%^7WF § ­>ªÙ5ã¾Ù1.ïÝرێþùÍZ ¹ÿÈ©{ô_æ+ ¬è‡Y²’º’Ýœqʰ<ê(FÏþ<­ÿëšÿ*Àð?üƒo¿ëúoç]˜ Ž2rQBçè+ð?üƒo¿ëúoç@5Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ïÔ/ýuÿCZ±UïÔ/ýuÿCZ±Mì…Ô¯¨È>ëþ¹7ò5b«êòºÿ®MüX£ u +(¢ÂŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( WÃò2x“þ¾ùVãêúz_}ï![žžYnséX~ÿ‘“ÄŸõð¿ÊŸeµ•þ­k©ÇI¥7ò$\zú‚(§¢¢¶š+‹h恃Âê +0î*Z*‰Õôñ<ð›È|Øiw*\Õêã…º[kzeöȦ’Y'†Vãr°àƒíÐÐ]oqÌBX$‡¡-døbán¼=e8;Ä¡Î9,Þ*Ö «]_[Y”,lÿuOSëÅY®nę¼q©4œùѢؓ@ RÇ4K$N¯ †Skœð?üƒo¿ëúoçOð‹5KqŸ. Ç=ç™àùß×ôß΀:j(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+Þÿ¨_úëþ†µb«Þÿ¨_úëþ†µb›Ù ©_Pÿ}×ýroäjÅWÔ?äuÿ\›ù±G@êQE!…Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@¯†?ädñ'ý|/ò®žH£•q"+F®cÃò2x“þ¾ùVÿö—ï¿Ò¡ýÏúϜ|¿Z´ªB¨€v¥¦G"Jã`ÊziôS$Š9qæF¯Ž›€8§ÕVÔlÒC\ÄNÒ t>ôGÃv·6vw\Ã䩹‘â]Àáät÷&¶)ÈÈ¥ ²n4ûˆµvÔl|£$‘yRÇ! ‚šÖ¨n. ¶Ûç̑îé¸ã4KCÓ—fé#‰'šF–W‚ÇÓÚ³<ÿ Ûïúþ›ù×H޲ t`Êyƒ\߁ÿä}ÿ_Ó:騢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š¯{þ¡ë¬úՊ¯{þ¡ë¬úՊod.¥}CþA÷_õÉ¿‘«_Pÿ}×ýroäjŨQE†QEQEQEQEQEQEQEQEQEQEQEQEQEQEr¾ÿ‘“ÄŸõð¿Ê²ÿ³ _ êvr„‡Qµy±Àg·î uPh–ö×÷7Mq—.Uò±úb¯Mko;šäaвƒ@ôyb¸Ò­®!PhÕøIz›$H5‹ÀU@rjm,|W«[ÞF‚Þú(æÞãåH?ç½u•…¨YÍ/‰¬g[o2ßɒ†lmÚp@Ç~E'ƒ®} C9‘"–H£ÿ+§ò­êdQG kH¨‹ÀUŸ@sVƒí^7Ô|àm­Ñ# Î7rH®–±ç°¸·ÖŸR³TÍ•,lqÈ9«áÍJÛ'Ë·»u@{ÎGàùß×ôßήé:4––$×·ÌÓÊñr{j·¥iiVï ±«ÈÒ±vÉ,zÐê(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+Þÿ¨_úëþ†µb«Þÿ¨_úëþ†µb›Ù ©_Pÿ}×ýroäjÅWÔ?äuÿ\›ù±G@êQE!…Q@ ¥Š†‡Qž”P@$z õªm —íqà‚äzgZK}=DQ‰¹t—Í\º}(ý5Y]C) pk<[Hú„Òw©IÈÀäS›%ŽÖH­ÆÐî\‚xää`hÉe ôëNªqY«,-,j …Ð#—9¡šÚO·Ã&ÐÊfÜ_' m<éœP‚²º†ROBA¥,$n<žMR–Çe‘‚ÔìËàò[${iÐY –í> °32ãÈÇ¿€.SU•ÆUƒ™5L+&¬Î`ù] î3ž?*}¼*ñ°t yÅÔtèx4nŠÃ¼{ÑªÞ eFO²©;É9n•›ec¨Éöi–ßeefÁUòÀ+ëœç󠺊华§$֒4_2GÏþ©—ïûÕ­fÖâæâE‰«µ2…ºrrqП­oÑX·{;}-%•‘Àuݖ#iïÐÕ};PŠX^H̓!–_3„P¿2þ'ùÐQEszvŸª[êÉ4…âN_9Þ 9dzmÛ4K%ÄZì²%´Î Èœn_$tüsùÐFÄ*–=ɦC4s’ÄÁãqGzæmtíRÞêÞILñ¤x}ýÆ.}ØÏ5¥¦Ø\Ûi·6s»1pYdVÁÜã/ƒÛç,GÔPÅ'A\͍…õ´‰:ÀÀ —1î츿­tôW&º^«çDÇfÕÙ!lù<þ¤U½ mõYcxöTݗÜ÷6Z€:+š³ƒPò!M¬ë „ù‚Nß½ŸÊŸ©¼é® #‚W +aŽ ¹`?Oʀ:*+–:v¦×pÊÉ»%YrÿêxY³ÿ#§\b¯êñ_5ÃXšEx‘FnÖ“ŸÂ€6¨®VÿM¾»œE ’c UgŒÏ¡Æz})Ëà[‰ #…Ò1fÁ²ìlzaq@E… Èº¸’hÚ[gŒˆâtl ÇþÛÓÖ·JZ(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+Þÿ¨_úëþ†µb«Þÿ¨_úëþ†µb›Ù ©_Pÿ}×ýroäjÅWÔ?äuÿ\›ù±G@êQE!…R2†R§¡4ŠÞ ŒÄÆ8I“Îò‘]°s†ÈƒÚ¸gËÃj^Ô4¬$RA<ã_®I¥YHZ¨Q‚AéÍ2MÅטMƒaÆ8 ¨Éæ€*¦¸Yµ®Ø²¨ì$ÉVeÜ1È÷ý+BÒæK‘¢0°I`sÎsÓòÅC‘g ‰„eš5 +‰9ÀÀ'Ôã½Zµ¶†Ò ºlŒ…ô udzŽ6HL»ßi䀼’@8zU;Mu.-àÂP1‹ÙÀ“?­hÜÚÅwI”§p Aõ}MU‹OÓ¾Ò(TInå`|¼t8=(ºvª×³"5±‰d„Ooݔ'ŒñÁöçëSß^=³A0‰¦Š¢—Ú8’N¥2×J·³½7ë°ü½ƒ ÈÇ ö«VÝ¢¬é»aܤ +œc‚:pHüh„ÜSۉ’&¼K‚yùÀ?¦išv¬nµ# ÝåMoñ䐑’™îpAüêßöE’ºº@¨Ê\pŽœ”¶ze½¤è3¹`3ÿ|ŒP—×?dµi„~c ¹Ærq֨ͮE¨™¡lbB=Õ¶â­êpÁqj"¹vHÚD©ÁݑùÔ6Ú-¤1H¯”ɼ1nᘱü¿*›N½ûl.̊ŽŽQ•[pÏ×ù +ŠûTŽÈϺ2Æ(Õø=rpZµµ†Ò# µIÜyÉ'ԞõƝis)’hUܦÂOuô  +qëjÑxpè®Ì÷·Ðà!L½×¾Ê%+jαÎŰ +®<÷ÅZ}O’%íÁUÏs“ž¹=óß46`ÐùF ¦I?1ÉÏÜ`Š€kˆ#™š(¤€ÙÎ΢œ5¨ÊNÞK~áúõÚå¥JúE¤»¼è•ò[§£Ž ÷ é“4Jí¹ŽG-ƒê2h–¨ÝO«,oͬé3FN26:¯@2>÷r•§¨];F˜"¹ 3íœrúƒ:Ö£s!e!†sÓq°;dŒš–æÚ+¨LS¦ä$zr÷Íe·ˆ+s5Å´‘"*4™ê»‹(ààžTz}áZñ³4HΛ€YsœLÕEÒ­Có +¨T#Ów__¼jäh±F± Â¨ +°  fñ +,“þã1Ǽ)ßó1N¼cއ¹éBø…IsûµfT;ùb­´äcŽsÜ՛Òu¸(‚9gR Žqž¤SOm"ÁšVku&\îôääãÓ'ž(½Þ· ¬lÍ ®è=×Ï4E­«¡ßWU°WÜ>Qž¸ÈTé¢éè pC‚q'9êyî}h:=‹F¨`RNwœõÉês@äםP­@ O£Ü*,&@v/‚ $ `ôõÁâÙ͞$gڟ¼%ŒŸ›oQì÷­ì›27û:f0ôã¦G|dÓ±ì<“€$7S‘Ž˜=¨Õ¥ÂÝZÇ:Œ\ã9ÅfÍ­o%…­KGWWÎ9‘Œ¼sŸjՊ$†%Ž%Š0ªUIt»W’IV%YdêǑÔdãׁ@ a¨‹ÙçˆDQ­ÎÉ2s‡ô÷ã>ôºž ,#‹‰$™ö"–Ú3‚y8>ž•5½½³—†0ŒF ùÏ?™¥ºµ†ò0“¦à­¹yÁSên¦€2›ÄHR1¤“UŸîî<€zg*Ͷ±ɋdgJ±zÅægòâ”h¶Ÿ:´cË%J"ü¾^ÕÛÁÔ¯¢éîá¾Î®ÚJà…ÚÇp8RÇ<Ñ*Á„m™ŸYÀ Œ£¿z˜kJðÂñÀXË ¶?֜}±VI±Ib‘mЕUqÐ{•öˆ(Ú  Œz!ʯӓǽ;KԞý¦ o儯6å9ÏÀçôº¦¢Úz¡U²Kd€¸ëƒúâ§´±·²ßöxÂoÆp{ àAÉãޖîÊ ÅU¸MÁNG${”˜uñöT‘mÿzòÕ àg`~ ÇÒµ,îîÖ9Ô`8Î3œUwÑì_~mÀÞA;I€F:p1W!Š8"X¢@‘ Â¨ +̛Xh®±h•Ì~g˜Ìy ÷Í@5ù ¾ñbKaÛO”ª¨$©+Ï\tǽj=´ƒÂ¬‹{’1üª8ô»8ÓbÂ1†^I<0Á•gËâ„5›Ã0,ø$ݜc}åNmdýºÓ„2É$.¤ .ӀÙ÷8[¸Ñí&YHM’IF9ەÆ@éœQo¤ÛĒ,Š%21bHí¼¸4vGòâw#;A8¬™µäŽXaÈx£™Ë>6«’8äð}+]”2•aFª#F° -×p£'>€òj¡i®Ê"·ps9Än®>oÞ¬g#}ð{þ7öòý¢tXHüÅV2,ɜŽFàóžÕ`hºxV_³)VìIÀùƒqéÈê)ßÙ;ݍº±‘J¶îA`þ$u=è<ø‰•LHÛÛÈR«·,¤¯<°CÍY—\‰ 2¬LÃ{ ù‡;P¿ëŠ´{Qà '$œœã9=ÁÀãڙq¢Xγ(£JCŒ0Hì8ÍT¤O/™,©!òw…Œc§8Î +а½’æYâš"X +†÷‘ž'öE˜]­Õ™—iÝÎxÁ?\qš[:+ gxIÛ6ÒA9Æ:ž´¡©½œþTvþv"39ß· +:öäÔzf¤n¯î ,],‘9dÈü Ow¥[Þ^,÷¸XÊéÔçüŠž++x]8•Y7`ŽÙë@(¢Š(¢Š¯{þ¡ë¬úՊ¯{þ¡ë¬úՊod.¥}CþA÷_õÉ¿‘«_Pÿ}×ýroäjŨQE†‡¥-ÆÃÍ$ +¥¼¡d ª¬dnzÊçP1øÕÉ´ÝJd1 +ªdmÞg_^€×KK@µÆtòDÖö‘C$%Á¶üîœvµu«9.­¡Ù™ã|íÈÁàó†àþ‡Þµ( cH¶šÓP†ÝÁýÙÚÛs·>L×5=ö™q6¯$ñÀ„»Bc¸ÝÌAN[qÇ{ñ]Ì[i—VÉtHJ˜VŸ=Ä —üFzòsíM6z‚ÛY¤ö¥ÄBÞ +á·mpK}0_þ½u4P.ƒ§Ëe%ÃÍ +ÄÎamb çOQó1$ûw¥s¤_9ºòbE¼âg߃0`v)î1ÇÓok§¢€35{i¯ô±L…Ñš&| ŒÖOöeæeŽ ¶ìêÍÿ|²àñÇ'Öºš(—}&ø]C T@°&"3¸ƒ×æÎxüiÚn;ßZݼ "¨"@À7~ œ{ Z騠 MbÎúæy…¨%·îߌ0|‘B8¬ÿì[¶H­bXÖBʪÊ!xê«ßîŒ×WErV"&˜Â.>tÉ¿˜q/͎üONJÐÒ4Û»]NIfTÚCҒÀ©ãÐg¯Lñ[ôPÐ^ JæxL¢VÊoÚFbŒ¾àՋ« $Ðbµe2ËǸ+¸©ê=¯·ZÕ¢€1¬í'²ðìñó98HúÌÅGײΑ©L±¼_èèѨò÷ãfÅ>?ßݟjëh  ]:ñoüí‹›¤i.72ûªGûâêðÉk&è•!¶ »†$¸úŒtþínQ@sÊÚ5Ð1H%Yg ùÎÄ~„Vn›ozñ-º@Ð*4LY›1ÈÇ×ú×SEaø{Oº±ó>Ј™ERW‘ÇV8õõ<Ô2Úêbi-Ã&eÃyÀd8qí]ÈÝh÷QK,©ne 䂤%@àǃÓÖ® [•"7€šBè¾f7˜ÛíÍtTPvkqihÉvÂI‹’Òg;ý>˜~•¦i“‹Û;™"R±Æ¿¾G?&$Œ‘žÃ¹æºj(žÖ–ãûDÉ»¾Ô€#ôóÆåϸÇåU®4Fi_- frŠìÆBÁ³ô#îóòãÞºª(3X´{¡l|…ºŠ7&H€*@<ñÁ#¯×µf¦“v·!„J²Ÿ?Í,vy[|¼žOÍëÇ~µÒÑ@´ú]ýÌ+ç[FÛRÊ3†åUòÊËՇ_BqœSK¸•f_²HÏŠª]Õ·P~aó`çž$Jë( e4ýBÚÐ@! ©±÷$v c®I«Ëi,Þ†Õbò¥hJ7¹ËŸjéh  ýRêÚVWâ,ˆQwäïÁäNÞÕSPÔµ³™­§`ͪÆ3!ê¤üdךêh PÏyd$†’0!`‰ßÝÝ+«¦º«£#¨ea‚È"€0R¸i–9ö™÷žXæ3 bÙÆ>ðþ•5­õóh·×3)[¸ÑŠDcÆ0¹_®x>ÙÇjØhѶe~áÊûcúÓ蘿ºÔm|Øþ×9ÀI|¤S?1ÚxÏ¢äð)“j:´w7‚G•öı¸å[ÝËc¿SŒwU‹¦_3ÿhféî!€)I]œmÉè<ûUmRöso ™ÙÜE¶Û"ß8ÎA-ß9®”DŠîá@i1¸úâ’’’(”,h0ª;JÊŒÇC²»üÆhDß»9#ˏ­cß__ÜEy3Cü‹`ŠÊÌò1Üç5ØÑ@äâÏ"’â"$Jñæ0à×#¶j Ôõ²,g¸pý疘 W%IÛÀ¶Ò{fºê(–mGTK©P™ ª6ÔXÆÒ6d>Ï_lSßW¸y$̊Ü/îÀÜÂ,‘ÈþõtÔÉ"I>úÆq‘Ò€9ÇÔ¯¥µ’G{œÉòydnP‡=ð8§è¬-`Õ) @$FhŒùc8Ôv­tJ¡T*ŒÀ €À‚‚zæcÔîžÍÿҘh˜JP ј×vÒÌO8ãÛ­,ú[;Hf–(Ú-ÑY¤pGû»šéUB¨U(v¥ ^-Rî9$in_gŸ{eŒ3&áp;OCÅj—ȱ»É,ŸêÍʘGîÈ¡`gKuÉàõÒM sDzU ¹ÔƒùŠ’€9¹5©‘w>áçc÷G±_ný½j½Ö¥¨ÛË6û¦4v| Â|Ÿ.r¹ŸRé]e5ÑdB®¡”ðA€9©õ àì>Ñ*F»¼§Xó˜mÚ§Žù'ŒTª^°)ûŕ ¦EH· ¸Ïž•Ð$kí£ŽO֟@PÝ_`O8•¦˜7ÈÛ2Ár3Ñ@$ ôúÒiúŒÍ¨Cl÷XœËåÈêH£<=yšÞ¨ÞÞXäeãÎÖî3ր9ùﯕî¶ÜJ%Weò„xÁÇ]¤žsŸN*uAb“eÌÆ5Y ´¾R“ràüª~\`ûžµÕQ@캕ɗʒim•fpò$Y `ó϶*?Y¼kky®Ù¼·ŒÂ>2`»ï3*ê*(-ⶏd(88Ð +çÿR[Ûužw€ò ŒŠ!,r +çïàd;pj;=OQ’[\Ë+‰" F¤H¬ ³Ðç¡Ààæº²20yˆªˆ¨ŠT`0  M"öåô뫋‰ÝÂ.åóPC·'8P?œw&ªÚk2Û¤k3K91œùcæR€±àc®k¦ A±¦EPîò‘Sv3Žƒô sûRöEÒIC E"ò†&s÷ÃddcÛˉo¥ˆ-̎öòÒn‰t˜È㑷±ÍuTP'c¨MgoöuwǖæÇÉ>fôü*C¨j[˜,’Žá$~PÄ2…#Žr 랕ÒùIçy»G™·nîøëŠ}aZÝÝ.¯œ÷,Ã2‘¹cnp=ÏLU+Íjö%½–9EfÒ¬‡hêKüˆ_®êê +)påAeÇ"«>Ñ¡¸ˆÛÆc¹%¦\päõ&€1î/5p³8@îž_–6…³Î3÷€ïŠ|ÚĒX$`MkvÞAÜʧ*ò*–^£¿ZÜhÑ¢12ƒ]¥{cÒ©®`±ëþ¹7ò5b«êòºÿ®MüX£ u +(¢ÆHH^9¨òßÞ4ù~èúÔt|Gl¦0þjoœÛòÊùö¤oÅûµHndy%h¶(zšÊ—M’_Ïnñ?Ùf-.ü|ªJã¯Ö™¦i÷wP´™r,«"´©”b88úûUÙݛQø†)c´x㝾Ó3B£#®jOíȳ$¾"QNQ×*AÆkmúÓMAlñË:]  8_ažjÓè·B=A>ÑÙ®÷;E°’¤ŽßC¼‹w:ü6³˜%YÄەQ03&î…}«[sx×6úMƦ–7&çÊkh1(C =H=¸é]^`‰<â¦L|ÅzgڥإrMÍýãFæþñ¤¢ÅÜßÞ4noïJ(w7÷›ûƒŠ]ÍýãFæþñ¤¢€sxѹ¿¼i( ÜßÞ4noïJ(w7÷›ûƒŠ]ÍýãFæþñ¤¢€sxѹ¿¼i( ÜßÞ4noïJ(w7÷›ûƒŠ]ÍýãFæþñ¤¢€sxѹ¿¼i( ÜßÞ4noïJ(w7÷›ûƒŠ]ÍýãFæþñ¤¢€sxѹ¿¼i( ÜßÞ4noïJ(w7÷›ûƒŠ]ÍýãFæþñ¤¢€sxѹ¿¼i( ÜßÞ4noïJ(w7÷›ûƒŠ]ÍýãFæþñ¤¢€sxѹ¿¼i( ÜßÞ4noïJ(w7÷›ûƒŠ]ÍýãFæþñ¤¢€sxÒ«6á–&›J¿|}hz(¢€ +(¢€+Þÿ¨_úëþ†µb«Þÿ¨_úëþ†µb›Ù ©_Pÿ}×ýroäjÅWÔ?äuÿ\›ù±G@êQE!‘Ë÷GÖ£©È`ŒÒl_Š›bÿtQ±º(*m‹ýÑFÅþè h©¶/÷Eû¢€!¢¦Ø¿Ýl_Š›bÿtQ±º(*m‹ýÑFÅþè h©¶/÷Eû¢€!¢¦Ø¿Ýl_Š›bÿtQ±º(*m‹ýÑFÅþè h©¶/÷Eû¢€!¢¦Ø¿Ýl_Š›bÿtQ±º(*m‹ýÑFÅþè h©¶/÷Eû¢€!¢¦Ø¿Ýl_Š›bÿtQ±º(*m‹ýÑFÅþè h©¶/÷Eû¢€!¢¦Ø¿Ýl_Š›bÿtQ±º(*m‹ýÑFÅþè h©¶/÷Eû¢€!¢¦Ø¿Ýl_Š›bÿtQ±º(*m‹ýÑFÅþè h©¶/÷Eû¢€!¢¦Ø¿Ýl_Š›bÿtQ±º(*m‹ýÑFÅþè h©¶/÷Eû¢€!¥_¾>µ.Åþè "ƒ(ÔQEQEW½ÿP¿õÖ?ý jÅW½ÿP¿õÖ?ý jÅ7²R¾¡ÿ û¯úäßÈՊ¯¨È>ëþ¹7ò5bށÔ(¢ŠC +(®vüZ¶¡wý¢Ò+*¯ÙŠç ±þÖzþÑQ\Õ…Õá˜Æ²HâgŒ`ýӜvïȧ¶“u¾ð‹aópÌùdïܜz(©¢¹UÒoÒkR¨YT&Òî3-Ó§ŸJÐЬgµžâIâ1`>Tœž}Iç©  ª(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+Þÿ¨_úëþ†µb«Þÿ¨_úëþ†µb›Ù ©_Pÿ}×ýroäjÅWÔ?äuÿ\›ù±G@êQE!…!PH$GJZ()h¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+Þÿ¨_úëþ†µb«Þÿ¨_úëþ†µb›Ù ©_Pÿ}×ýroäjÅgºFGg*Ã<Î›äùé?ýþñ£KX5¹¥EfùþzOÿŸühòüôŸþÿ?øÑeÜ54¨¬ß ÏIÿïóÿ@ÿž“ÿßçÿ,»†¦•›äùé?ýþñ£ÈóÒûüÿãE—pÔÒ¢³|ÿ='ÿ¿Ïþ4yþzOÿŸüh²îšTVo?ç¤ÿ÷ùÿƏ ÏIÿïóÿ]ÃSJŠÍòüôŸþÿ?øÑäùé?ýþñ¢Ë¸jiQY¾@ÿž“ÿßçÿ<ÿ='ÿ¿Ïþ4Yw M*+7ÈóÒûüÿãG?ç¤ÿ÷ùÿƋ.á©¥EfùþzOÿŸühòüôŸþÿ?øÑeÜ54¨¬ß ÏIÿïóÿ@ÿž“ÿßçÿ,»†¦•›äùé?ýþñ£ÈóÒûüÿãE—pÔÒ¢³|ÿ='ÿ¿Ïþ4yþzOÿŸüh²îšTVo?ç¤ÿ÷ùÿƏ ÏIÿïóÿ]ÃSJŠÍòüôŸþÿ?øÑäùé?ýþñ¢Ë¸jiQY¾@ÿž“ÿßçÿ<ÿ='ÿ¿Ïþ4Yw M*+7ÈóÒûüÿãG?ç¤ÿ÷ùÿƋ.á©¥EfùþzOÿŸühòüôŸþÿ?øÑeÜ54¨¬ß ÏIÿïóÿ@ÿž“ÿßçÿ,»†¦•›äùé?ýþñ£ÈóÒûüÿãE—pÔÒ¢³|ÿ='ÿ¿Ïþ4yþzOÿŸüh²îšTVo?ç¤ÿ÷ùÿƏ ÏIÿïóÿ]ÃSJŠÍòüôŸþÿ?øÑäùé?ýþñ¢Ë¸jiQY¾@ÿž“ÿßçÿ<ÿ='ÿ¿Ïþ4Yw M*+7ÈóÒûüÿãG?ç¤ÿ÷ùÿƋ.á©¥EfùþzOÿŸühòüôŸþÿ?øÑeÜ54¨¬ß ÏIÿïóÿ@ÿž“ÿßçÿ,»†¦•›äùé?ýþñ£ÈóÒûüÿãE—pÔÒ¢³|ÿ='ÿ¿Ïþ4yþzOÿŸüh²îšTVo?ç¤ÿ÷ùÿƏ ÏIÿïóÿ]ÃSJŠÍòüôŸþÿ?øÑäùé?ýþñ¢Ë¸jiQY¾@ÿž“ÿßçÿ<ÿ='ÿ¿Ïþ4Yw M*+7ÈóÒûüÿãG?ç¤ÿ÷ùÿƋ.á©¥EfùþzOÿŸühòüôŸþÿ?øÑeÜ54¨¬ß ÏIÿïóÿ@ÿž“ÿßçÿ,»†¦•›äùé?ýþñ£ÈóÒûüÿãE—pÔÒ¢³|ÿ='ÿ¿Ïþ4yþzOÿŸüh²î–¯Ô/ýuÿCZ±Y†Ý2ó682¹Žõ.÷äÿ¾ÏøÐí`AERQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEÿÙ +endstream +endobj + +641 0 obj +<>>>>> +stream +x…Ì«€0ÑVžF,û…løPÄAûP@Ή»ff­!ö€)=Ur€Þ`Ì$±Á + Y"4”Ü¡ìèî†÷/ +ó^d€zÌçä   +endstream +endobj + +642 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÑ0!íÚåØT`oøßj,И 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1Ac‚ƍ 4&hLИ 1á>T +endstream +endobj + +643 0 obj +<> +stream +ÿØÿîAdobedÿÛC +  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YÿÛC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYÿÀTÆ"ÿÄ + ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ +%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ + ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ +$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ôš(¢€ +*¤„2!a‚pYH ƒ‚AïÍ[ Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š)¥‚õ  »½>àÃ$Pl¤…È^“’p8æˆD/™¢ +|Ð p9oLÐd¶:ªàb7Ȳ+Åþ“n_Ì p>s•T>˜ùäԗz$€ù–s˼1`¥€ +OqÇó¨â¿¹±º1Ý;4Ys†_›‚çQ€(햩Ôè¢ #wRw0;zóõ4º}‹[]Í:ÍæC2ŒAž?n”ûxmn%7pîC)‘‚qŸ—×¥PòïôÅÞ%2Ā–R2 +…=s@#Õ&•náÚNåÚ7í^$ôüÅiG$sÆ6WR25œ·º}ô¥.jð¸Ûæ09 cñ^žÔÛ=¬.ZKkƒåHGù‰'씤cþí3¡Áâ³íµIÒCô[pހñØzçåíZQO‚¸ž„ú¨´SÌÝü© ‘Ðâž$þõGENzRԑÐÓÖOïq@QHzRÐEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEPEP{ÛsslÑ$FAÁèk&4Ô¬™~X¶à– Ë·^zsZZœsËfVØâMÀûã=¹γíõ[ˆ#Ρ U\…;‰Ø¤ñߒGá@lõK{¨Ãó<'œãƒõâ­Ë s&ÙQ\r9ج–²¶Õ y`2DÂV¾v‡+Ÿ­;P¹ŸI°€¨2ˆ”ù‡nAÀéœñúÐ:Ž›"_C=¤EÙåMížc“8öÚ§5b}[캌N¡Û媏‰Àüy5>Ÿ~n„¡ÕWÊä°<“þnX’hÊH¡”ÐMݜJn,B}¤ŽU»ŽG+Ÿséލ&ßP´¸XnÉŒ ö?Z|š^jÉ0`W,XîÀ.O=y-úSt»ûƺ—Ѩ&r:ðI眒}(7֗è–÷0:´äS×6GNP6™si?ú<’˜FYŠ`1ÉʯÚÅau »µÆíÁ‹Fx8àöù‰üi¶‹{­2͓o fSØŒw=‰ì:PšUÍÕÄMö¸V&M£¯$à‘Ûó©í®c»Sµ]JõW\0ÏN*­–¯Ô<£ìí*«*»w ŒøÕgÓ.­œËk8%K¸b `ö‰õ8é@Í÷*gCƒT´Ë¶’öêݤg€Ør7©$ä:t­åŠpÞ[¤›«m9ÁA QO1ÿwò¦t84 ‘Ð➲z£¢€'ëҖ އñ'÷¸  (¤ëҖ€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€#™ŠBì½@$TOäN¼Û]Ù9ÜU‚j¶C"H£ÝÇN„Z®öO46²Iæ1.·ÌI9<Õ8õ;ˆ%ͳÔpÎpÇ%Âäc~uvW¹„@ﵘ)ó +ƒ´žÃÿ¯VdH§ˆ¬ª¬½Áí@%‚+Í"ìY¢,—6ìG;HÇæj¢½ö›*«ªÀH¢RÒçc“éÖ´,tÿ²Ý\ÌX0Žˆ€tú“’ +¡m}qe4‚ô³[ªŒ»žTîúr0£ô  všÍ´°FÒ³FHQ—œt?ˆ­*Ì{K]JÙ؅ƒ—Œ‚#ðËSu§Y^´èò²ñæLîÏ’րºW”Ì«íã¦s÷FNi±s«2€wmÀþC­TmV[;Ç·»¤A€¬ nnœã<ò}*ðimyne͸·Õ$ï M2Ǽ¸Ü>\Žþ¸ tñ`ìòÙ"!Æ×T]¸äõ¹ÍfB×önÌñÈ"ó‹I&Wï1ç c¯n•b÷LÆÒiòÄt ƒXðr;·JK}Jâ"ëwnB€¬ÌĂ7dtôÈ  vÚÅ­Â+h‹.ý² sùt?•XžÚ ō¥]ê¿2ò@äc?‘¬éllµOÞBñ¬»~m˜8ÈÈÏq×Ú¥Ó£»·ºž)¼Æ‡h(Äå}0;þ^óE~^ÖL±WÎòwç¡æ¬M~l¾Í ˆ1¹°ÀôéŠv•©­å²™X,ÛP°ÆHÄÕÙ ŠáBÍH#pεS’ÞÛQXd 7ÄÐ:GñÁž‘êZ|R)#Ɍ3œ‚0¸Ž9ÜzV”zrÛOq<ï•6á»`pôªÑ_Íhæà\,^a| œ‘èzÐF£§j‘ÍG+1 ¨Ü2GN¨¥Ò¯-&I¬§2B>sŒrÇÇ#?V »¥µ`Ò+*§©Ã)àœ¼#kÛ*3+NÏû֐åc>‡õ  4«©¥†ñç3ʗj»xا§Ôš»ñ]D6WÀ9Áªv7âý'ß—Ú¡‰û۔ê@t†µx¤³fÌdq»ƒ -žÇåÝր5?îþTΝk*Û^Ûæ%äR,Ì|«Œ’F{sÒ¶RH¦c«í889Á 觘ÿ»Léր:SÖOïqQÑ@õ¥¨#¡Åèþg¯µ^·Ôà‘ÄRŸ*s‘åžzzþ"¬¼q\"3¢¸e$tô4Žt­?PÓØiíäåvn³Ðp{çñ¤nôéÄêí a!e,Å2Y1ôãw¥iYÙ "â)£vÜUÿ‡Œ`õêفPhƒÏgsonf‘Q®hðø9‡¯ýj‰Ò¼˜ó¦JApœá@×=kFûOK¨•ü’ªPPp§üåUìàk+¹#Q•(ò92_+øãu15"_"íIe$äòqÆ;Ú´m§Šò–<” ‘Ž ëYÑÝXêPÃ%ÜiŒ¦[ûÃ#SKžÖU’Ņ̃€76ݘÀÏ¡ÈгF†™Ó¯ŸmªÍ†+菚ꅊ.6’3‚3ëÅk1RÁ ˆÈ ó÷i:ñ@ + )âOïqQÑ@õ¥¨#¡Å³ +ZêÆ;y"vŽO0©ù€lç ?TË_é+¹k¥M¹Â€sž£‘úÐý!äsUínÒ{U™Ç•’T‡ãw«4Vk(¤ŽP€#I pÉéõ5N£¥ì +ŠmÃ`"å»(ôàO×ò®†Š¥uqk)žÊáö€9o”a²Ï¯N}¦¹IZå¤9亩Ààcõ>õvòÅ.>uÚ³3 ‚@#‘é†?c“w¢  ,láÙ±•`G¯ËÈ?¥n4¢I0$0©ùW¹¥6ÞöÚð8ŠEmŒçŽHÏòªöZœW`C.v(9ÉŸp ª3hžH͓îÂêŽY¤Á*ñãùŠÛhÏjgCƒYº~£qö³ÏΤ( X´ƒ}VÉðh +)æ?JgCƒ@ +  7la†Ç®(7P·ºµ´µ[Y$&2ÀìsŒJ™u [¹ Ò;8eþçÞµj¬ÖóO̸t`ÜwÇLÐpÒ®-¦Å>äG,"UÁ*YIÏ­ZÑO È>S¬€u+Èüÿ +͊êÇSŽ#s†áÀ*Ã…a‚?ÞZeƝ5¡ÚI.À Œ ÇÈ>ü°ãڀ5Œgµ3¡Á¬Ø5ͲÍä&2°gq>ükc‡¨ h§´g·åLèy ކž$õ⣢€'ëKPGCOY=x  (¤ëK@Q@Q@Q@Q@Q@Q@¶_¶}¤<ü¿/nç8õ÷¬‹Nke“ûA‰‰N<Æà©Á'#Ýhq•]Jº†R0A²ïtHgˆ‹}°á|£‚½;ҊhæŒI§½I@¶bêßST¹šgˆG ,ÿp’É·×ý}jàÔàûsZ>R@v‚ØŽã󫬡†:ò*…Þ™ÃK"±Y]Hò þ‚€4(¬.£k~RCû—•pÌ20XŒŸBµ½@Q@Q@Q@Q@!èiiC@މbڏ…$#ý¥ÝI‘Ÿj½g Ññ<—WñªbÝc‡fJòInùÒø3þ@‡þ»Ëÿ¡è(¢Š(æ[uqæÄeîèjµÞ“ms½‚ùR?Þex#ŸûèÖ ¶¥$ú¦¦`Z‰Ä÷rxÈù5փèh$ÛÝÙÜÆ-ð-K€Øä*à×#é‚jóÅí Y‚º¸ÎPñõ¬ÒPQÀ© …¾xñ´+ ñŒcÞ±íf—MX’Q+DDŒáŽ|°ŒöÁé[Õ±G4l’(ea‚¥fÝXÙê©4°º´¬ g#å`ÀõQŸPÓ ‘ZMÅ#Ê¥“só`sõö©'Ѥ·F{rC±«•àgޏ={Ս:öâ{©­o! +Ê Ç àë‘@Ûêvó\ù$ýß0»’2?*½Y‹i§Ír²ÄÊ^9>èl€Fr1Û®xô¨u;›ëIæ’-ΞY(6å +Ē}rç@4U&¿ŠÜÁË$ˆ@ã9ù°«´}¤Avë ýԀýåGã£òªú\Y^L. …]#ßvöÉ#¸ê+f’€1¤žë‰í. Ùó•YŒ0:þ5gT3ÁgÙ74Êê¨Nr1ÏçŸÂ›ªéqÝZÎbR³˜Ûn2ÄwüqYËq{¦Ýˆ¤ +°•"@\@Æ{w Pë}T‹£mtHXªìëÃù‡läÇڴђhÕІF©ÅdÇ©é×öÞd§ìäía¿ +ãÀþ•bh^*Þ+9$ÙA¹0\ ô÷  †3ۚgNµ¶©pÐ]+)T;?ÂGÕÇJÕGIãWBdŽŠyŒö¦t<ЂGCOY=x¨è  úÒԑÐÓмP”Ru¥ Š( Š( Š( Š( 'šÊŽúÓRF‚êeÎ$ ò§LûÖµQ¼Ób¹ÂþéØcpì@àñüF€ ›O0èòÁhK30À?x;vªñë ïÿZˆaÔlËï‰[î¨Î?,*’ÞòËT‚!u®äQÉ*3´úó@°Í˘Ý[@9Áô5%aO¦Ý[³I§J¡©Ð’Ę÷ŸÌÕË;ÙFœ÷Èb1’­ÁÉÇãÜæ€4­-EÄ7)ºÀààò¡ô©h¢Š(¢Š(¢Š(¢Š(¤= -!èh’Ñ'šÛÁ×s[ŒÊ’LWöni‹8H_í‚æ&/»“»ŒíÖ¨x0¡°<ƒ<¿ú­›k+kFso +D_ïmÍX¢ŠJʟB·’3 lÑ[´žcÄ¿uŽsZ `;VEö»m œòÛH“áÒF܂7Çþ;Y÷Sê:R¸P%ÊJŒ#·‡ZžóF–0$Óå:ÿìdààûò{ԑê7Lc¿<£É»´ dwP“ÝC‘E4ˆ’H UÏ\c<þ"§¬é-í5XHØ2€qÆ7+cðY·W†”Œ‰QÊd£ˆíÇaÉ Ž“Õ;}F)æò°QÆàwp QÍ]  ÛÍ"âCˆ¤}ܑ¸r<~=»Ñuq5Ž›¹QwDU>lWŽ}«J€FÈ=dÛÝÙ깞4 ܎F\+d7ԏҭKo$ZsÅfäHX²·lŸæi·Ú\q°Ëb +ð2võÿ¾Ef¿ÒZÛ¦€¯ñÎ8ÇÊ(Ì:³Û š€äá”cŒ°ÉÙsÇ­j¬‘J¹WV Öa¹±ÔÃÛ\.hÆ[æÆÓŽpßFýj tö¶¶3XH¯µ·1ã-‡R~¸ +Ô²c=¹¦tëYÚ~²$Œ “’±ïiHÚ Èõ­8&Žæ éʟ^½q@ ¢žc=¹¦të@‹ï–¢ï¥K@Q@2BF1O¨åè(üS껺ƌîÁUFI=¢9 xØ2°È ä³EF²׊}BmÇÛ>Ò$—>^Ï/wÈyÎqëïXñk²‰LrÃó+s€psŽ„úVñ8úVZ]XßmûLk­‚¢Bs‚úÈЦA$g¥fÍ£[»´ˆYeßæ/Ìv†ã·Nئ_iÓ5ėVÒ2Êyʜ€=ùÇȵ°—- Ôl‡¶Ô$õð9êÔVf¾ÑÒIØ,ŠÌ]Èè@ÉÇ·Z»£ ÚÉ àHm ûß3Ïü´bš+„&6Þ¾¸àäUiô»iIqG9ät9 9ð#@¯4ëˆ Ma+)'s…s|Äþ=i–º¥Üw+ê» >ÕÁB~ý2Ɲ§µÌ:ËZ»Êéå»1s•8([ëZl‘\y«,k€vg<ž‡ü?*’â¸RÐȲ(8ʜԵ…s¤Ï +fÖYYX¢m´+3žpHëØTú=ä²3Z\*¬¢÷ùþ4­EGÑÌ ‰Õœ§85%QEQE‡¡¥¤ cÃ7qXxbk™Î#ŽiIÿ¾mYjQ]ºÇµ¢•“ÌýJúÕ4Б4ۍ8>m¦,ÀŸ¼¤œþ<Ôºfžöó¼×ۉ‚„Y"dwã·jÕ¤a¹Hõ¥¢€8é¬.ml,´ù­Ð[År Îw ’;W`1´c¥C=ͼ,«4¨…º=jaӎ”µ¾S.]„7ð·z’°'ˆ\ø¥â˜e¦SØç’=èo¢Çá{ôE +¢#€­+/øòƒþ¹¯ò®rêñï| y$§tˆR­Œ×GeÿP×5þT=Q@Q@G,1΅%Eu=ˆÍIEbÛ2ÒFÜ¡ŠŒíäìÇN¼/Z[ ٖålîðû‹aŸ†?3qïÀµMhљY•K/BG"€3îôÛ{¥ha“Ȑ)»ÆTá÷G4ÛÙ§±ŠÝ£‰X$d0ÉÚÊõ¨µ :íï>Ói9V=FpGã·½Icy4·³Y]E€áŽ>`ýsž€u  ±]ÄöÐÌΨ&nXu#¦jÅf_ØÅ¨Xˆ-å,a ‚¸À#¦h¼¹¸³’H„ù¾BۛŽ2:qҀ4é=j•®§ áB!•€`ŒÀäÁïÖ¯P}æ“˼˜Û+rGàÒª1ŸFÒ|´sçHÌ|¡Y™±ÉêmÒÁ€2Eņ§IÐÆ¸<±Ø;‚> UK­6ás>9dØË”ÀaÃt8çæ#½iÝéO…EFFÜ£.r#¿ATݧÑôÛu^Õv2aI$ž=(}7Q’ââKyâ(è ÔŽ~½jìW6÷ V9ÙK;‚ëTmµH +™.C'9s€ +ŒóœôãëíQ=„‚Qwi&öÜYv¶2Ô¶{  uM§ Óë7G¾ží%[˜Õ%¶ž9í×¶+J€ +(¢€ +k.áN¢€3u{i®4頇ärOÈÏ隿 ¼»Ó'·‰a´·F_(¾3€21ô®Ú¡¹´†æ&ŽTX`ÓL–®P·Õm' G‘Š„n¹$qWc2‡ƒ)är c^h†´Ã2G&Ðç$»>بm.n4É>ÃåI:‘û ÎÌ·>”ìºßS¤׊§6“k!wD #©ŽA?ùÔqjv’@²™Ò<©r¬À^*â·©àóRQFÂ+ëkÙVwy`*X19ãs‘Æ{b‹{Û æ‰ç!¸ùJ ÝÈ 0{ñŠÒÁⲟ@„G8·™£yr7h#>¹õ  +÷:u՝Ú=ˆšPVFÀ}¡[1ã<€G ú՘õœH‘Ê 2ãÍ#?/ßãƒõ­z†âÒ ”e–0ۇ^ÿŸâhb¹‚`<¹U²3ŒóùS.-<èVFBÍ»>øÅd¾qfÏ5‹‰$ÆWxç9ÇTöº¹y3¤·¬»6Œp9ݱÅ\–âHa—(G”ªwžŒ{⦅£™ÿuŽ:t3EqxdIPÿA¦ˆʌŒQ9EèÄã“ô  ¯ì‰àºó`¸b›·,Àœ+`žy#Ó¥O¦ßÏ-¤ívª¼G¦HÇQв’ef¹‘¶d ǐI8ïÎG]‚´(ªwvfY’âÜ"• FA±«•™­êRiV†è[ù±)¾l’õ  ÝNÀi¾ ½·ß½¼¶foRNMoYǔõÍ•gx îðÅùõ„šÑ²ÿ(?ëšÿ*žŠ( Š( Š( Š( “hÝ»wLÒÑ@·ÚeÀ/-¬®z•NÖÉ`ǜóÓ¥>-Y¢cÜee +X¨#p'¾:±ü«^¢žÞ+„)*`úÐRÛé·nße*“:7Aޏç£oáPy—út€Ì_ȱ,í¼0ÚÇ©ÚŸ>•ýžqjò,3*`–ÎrÇ^kCí[éÑIx†I\ªº þ& cðÍ6ÓV¶¸¬â)X‘±²9êúÊ +AèEf%•…Ü25› @&3 ôü)¾•§Ú#…Æ +ÈsÀ’9í’?ZÕ¤ëÖ¨Zê¶÷†;£9ÇÌ0888>™«êÁ”2Aw  +7ÚTˆF6>ÆPGNA?Î&÷I•8Á¶yX9ÎXcœøŠè(  +n¡öא $"?$†Î?•_¨ ´‚ÞI$†0&7c¾3æjz(¢Š(¢Š‚ä1ƒê•5^Dÿm}úÑvû d‚FOO¥":¸Êk96˜É’T~ÁþéëQÝYÃuÉS#9ƚʨ¥W‘?Û_~´ÔÅcÿùÊÚ¬K žZ¸`r¡O;O¸4ý2öéõIí§$¨•Y0TgVòJ¯À8>‡­+ =85¥î+R‚GCAR½E%!’,€õâŸPR‚GJž¢šçR²( Œ{ã9þ”á =x§PbY\XÙ]-´»™°c ËùðOéíQŬ¬R¤7;À þõâd?ÄFApµ±UîÀó£ GFî?’)b¸ŒI¬ˆzr*9mÃH&L‰–<‚9üêµÅ¬°ÛOöi4’«€n +6‚{áOçPC¬˜­âÕpw1ÚJöê}z·,÷0˜w¢0#÷…½íšžáa{yVb¢&9'ZX®!™£‘X7LŽK46òE ½‹œŒä““@³i×1J.-&3H@ùä#v぀Îì~¥ƒTh_ɾ8`Õq¹ˆCŒÀÿCW”¹U€#çÇZ|°E1S"+$©#§â€'Šæ1$¤¨z2Š–°Ž5œr=œÌJ+Ð2B€ óƒÓڝ°ÐÍ,WpIG¸ù˜'ŽNqé@tTpÍñ/OLT”QEQEVŒ™‡çb‘Ëå8ÃÍoÒ‘ƒ@¾#e Þ2Ê`àŠÔ²ÿ(?ëšÿ*Ïñ¬óh·V¶p«ù±֍ª”´…Xa•#ð  ¨¢Š(¢Š(¢Š(¢Š(¢Š(¢Š)®‹ Ô0ŸPr)ÔP;q¤Ëi4oeæ:sò#*6œsœMNšµÅ²F51½œª~UVÆO×­mÔSA…•€r3ڀ3ä³³Ôb m"aýÌ’rr>´²É>kf‘ÄeˆŠ(þ.9ì:æªÝi70“%Ãœ6Bp1ïןóøÈº¥Í¼ÇyŸ1Ì ®ÅŒœõè Z³Õí.Áċg]€'9ÇòÔµEG «4aס©(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¨§‚+… *皖Šç¯t–“-™iÑÔ)`»IP09Æ;օýù³¼‰Yw@bwŒ| ÏäMhÔ7ÐÝ&ÉãYÐÐ[˜Û/åX .xP8àvéúTõKN±ûÚ˜4“H\‘ÐÊ=‡õ5v€ +(¢€ +(¢€#õ‘ýOò¬¹uËDÕ_M¹]®]?âó þÿ +¿v̾YCƒ»úU ›[[ÙãòfÈo:1É#¦úôrk ÊB`©êÈ5u¥…Žéa\×ÿ¬Éõ¯aþZÙjRÀïζIXü¨[ä `úƒÈ=ëM5[Yšê9*Û±WfCÐðzg‘ÇZÆ…ͽýµ¬ÖÇˑUL™+sÆ{çӊÖ+ÎFCzвö¹¢n:íj¡(–Û8· ’Q›“ô=? +´³2ðãpõ*™\eH"²¦Ô"ŽÚYF¥¼·ùOëSE4R•ÚàJP6äý*{}”7Qҕ^Dÿm}ZfeáÆáê:þU2:¸ÊEN±:J¯À<ú´§(n£§OjHç<ìq(S‚3È5J]ÄHT¯ZJ•%Wà}ZéÅXR‚GJ +•ëI@‰ëÅ: ¥ކ€'¤=)« =x§PöeݜbH¤œ³1U| č¹ÏPÇãW¬õ%—ÎIŠ£AyN~îK ۅÏãZ5°¤ÑIŠ +È¥Üß­Íñ‰"`È{Š’±'±½†æ¡™¥ +À7N3ÎG~?•X±¾Y»jVH•7•R9eõÉÅiԐ\ó,JÌãæCRE"MxÛrž†Ÿ@Ré7²4öPÄä¹??B6ôÁ óÓb FD»“ÆXî+æççŽ+V¡šÚÊ4±«2«c•>Ô#:¡P̱‚zŸAN¬½bÍî"µØ¦A »˜ HØÃ¡ëÉÄÕ¢µo"à0؊Á€<)ÀÏ9,H  z)‘J“F6 ¤dbŸ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ozGþ÷ô¬›[™%Ô¯ ll„¦Î9årh¢€.ohhÉ_nƝumÚuāDRMÎñ€ }h¢€0£Öo"Ö4‹ee1]FŠG¡##Ð×ZÊ®¸`=Puå¼A• +Và‡¬[ëH­.íf„mc$q㨠+ àz}ãE%•̈Öðçr¼Ò©-ÉÀÎ+TŽr2ÔQEKm#H­»ɛ™­õ9—Ùl¼ãJ(©†ì™‹(n£ÿ­K çÉÈ9=h¢¢;–Y¨Ý@Ž(¢µQ@(b½ PÀäKETS“Âñ¸ù[Ó¯±¢Šæn¥’É.®¢rgP¤;c¸n8ãfº 6w¸µß!ƒÈqEnŠ( ¡¸·ŽâŽEÈaÏ­P?,K¦y³[pñ„9>AÇlœÕ½Q¸½’T•‚ÁQ@tQEQEQEQEQEQEQEQEQEQEQEQEÿÙ +endstream +endobj + +644 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÒ À íZs|ƒ ì 4¬¬TÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬dÈJ†¬d<Iô +endstream +endobj + +645 0 obj +<> +stream +ÿØÿîAdobedÿÛC +  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YÿÛC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYÿÀô¬"ÿÄ + ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ +%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ + ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ +$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ôš(¢€#’xbdYeDg8PÌcíëO 8 àààô5›¨Z/Ûí­ÅÌ~Y†Xö†;I;àç#¾}°sXÜ0ì­o#€°U\´m#"Œ1ÈÜ_S±}h¥¢¹D»Ôco>A}1ˆ(°:‰ÎO'8äW¨eXꨩhŸlÄÞcÊw.õn3ާ!ðçi(ª¢¹”ºÔ òï$—…—v܌…Á<äBò:€q]5QEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQM. ã?¥:Šg˜¾¿¥búþ”ú)žbúþ”y‹ëúPè¦y‹ëúQæ/¯é@¢™æ/¯éG˜¾¿¥>Šg˜¾¿¥búþ”ú)žbúþ”y‹ëúPè¦y‹ëúQæ/¯é@¢™æ/¯éG˜¾¿¥>Šg˜¾¿¥búþ”ú)žbúþ”y‹ëúPè¦y‹ëúQæ/¯é@¢™æ/¯éG˜¾¿¥>Šg˜¾¿¥búþ”ú)žbúþ”y‹ëúPè¦y‹ëúQæ/¯é@¢™æ/¯éG˜¾¿¥>Šg˜¾¿¥búþ”ú)žbúþ”y‹ëúPè¦y‹ëúQæ/¯é@¢™æ/¯éG˜¾¿¥>Šg˜¾¿¥búþ”ú)žbúþ”y‹ëúPè¦y‹ëúQæ/¯é@¢™æ/¯éG˜¾¿¥>Šg˜¾¿¥búþ”ú)žbúþ”y‹ëúPè¦y‹ëúQæ/¯é@¢™æ/¯éG˜¾¿¥>Šg˜¾¿¥búþ”ú)žbúþ”y‹ëúPè¦y‹ëúQæ/¯é@¢™æ/¯éG˜¾¿¥>Šfõõý)ôQEQEQEQEQEQEQEQEQETR 6}jZF”Š‚Š+ñRN§Ý[ÊѲM°ààdýÜûd~´Ò¸›²:*+þÑwñÊrDI– §#ٖ®Å¯]ŵ¹[`fv„y%”ÿºûsO•‹™-Ê[ë•®š‚A óGtmœœç±'Ó?¥\†{È/o­"޵Ö噊¹<8ƒŸqk¨Ág0¸ 0wÊáFâvýÖø#ÕÑ\ê&/²y4ÿjÎÞBìs܌ñÇCÒ¢Ò5 /#¹ó^7,€pY ;xìF +œ÷S@m¬o¥ ¦Ù₸ŒÌ +åƒ+ìçŒüX֎¯iö›fT…œ¾ÕÄʲ#ðpOBGSM°Õ–÷͌Fâ5Ý嬁í€ÃŒƒÁô¨^Ýj³5©ˆºyˆ%•Uv‚e»Ï#òÍE¦ n ¹ž#Éýä à…°%”d€FÕ8Î989ÎlèÑ][Ëq° :ï2I,,®Ò«°óx`FTç€Þ½AÅ_Ñá¼··hoò]äÌNÜ:qô§_ê‘Ù@gd/ ‰¤GSÃ3·êGO_çVç^[i§ŠKfi# U#30<Ð•Ëz@®ôëÇd¶I$( +ùžh”уœ’AOIëÅW{UŸí)Q¢Aj®¾fÃ.@ÉbO ;Uù5ß²ùQK yI;ˆ`C ‡ œtû­éI¾VÖinmÈòÃ2•8 1¹8<‚ÃŽˆ×…Ý–£‘Ú³vK悭܂s¹[ r3늳ªi²Ïv’ƦX‰ñ—À$p5?E5nòý­ÞÕa€În † Ñwwî@8Õ[MÔå¹·»fÌ’',ˆ¼Œ“·ƒÐŒ9þ%4Iô‹¸­„Q ˜)µj-XÉu$Ü£äùA¤ÊqŸQÏéÎ I}y%¤éˆÚD1HÁï;®QîFïʀ1®4«á4ðA i²GGus–Ü[<™Rzð*YôÛ¥•'†À qù¼¼òɂÃ8 áˆä*Ñ×qnŽÖêŒÁ_ç™BlnŒ¿8\c9#±ÍGý¼òʍ·ú2ì2¼† +Ä®@ÿe>†€*E¤ÝEoögQd$PÊC» ò@ʶå炸ëо ááˆÜ£ <ÍîcÆõ°sÁFèº0+WOԅã¼RÂmçN|§a»ÇN¿NF ¬ërs 2$&ÝZV ÌAù*ßFS†#Ð 1´«Å´H„Já@m¥ÎÁÚÅFá÷”îœ9äÔ隀Ç“4o)!ÜH›‚¥ŽVRsÐæ·Phì®ehçµ̉:¶r¾¹Ë­T“]ò‘wÛ| Ì #å(‹8 QÎ8È%ÒîÌð£[0‚ ¥“Í\‡8m£9v0Ê1Ԛ•,µ +ß>(žå˅fiÌ£’1òãûß0'96W_™™-¤kpʂ@FrÈr½FI +?Ú㊎/ ­–X­ÉÚ[keUH'vì{c§ŽÜÐúæŸ5â#[“¼|¬¥°ê3ø€û,ÕfÆ "Ya‘ÙÎ0Í»h#”?CŸl;S'ԂAi%¼b´¾Åăí'“Èþ}j µ¡ב%³+0ZùŠ\’22ݲ žãñ  ÖÐïìðA·É·™Ú'fæ1´•>ãvÐG±=‡Ía¨Ë0¬O*³yŠDxù£ï’Êß)ãÏ8­(5o­Á“rÑ,Ñ+C©éÏè}3ôª§Äj–±K-£ÆgÁŒ3ŒH'±€G©ïÎ$M6Khê+Þù¢wYImÊÓ# ÙPjºi·r\›„IlÓÏâ%‘Cylî™æÆ½ëZ«/öQ¾Š ÚÈHR¼à’z`uϧ5Vï\š$•£³?»˜Æ77ÞÛÉr \•ëž=@  …ÜŒ‘íf]¼ÈHê1×ÜÿJźѥ:´2ې°bBx$lÜ9íýîï]xÝ¢‚’^5ó¯oºYs·=M9üC +ÜIäU ±£;‹î@ÀèsŒúçë@!Ñ.–ÖÛηGxP¡x#íÀ'³ÇìU}ëOH³¹²©VØP¤¡œ‘÷_¯R>ö;ãëOÒunÞD¸ŒFÅVhpÀ‡‡b:àä~^µ-Åô–÷ÐÀm‹¬Å‚²>O +Xœzq޽ÅQ½ÑÚKégD2+2JŒã†N½ù‡£dÕ;½.ÿÌ8ƒí#î1Þ ™GrÍ”Àà}àM^]|´6§ìl%ºÚÑ)m*{îö8c«\Ðúù듐›Cop¥Im¼û9<à`ò WµÓoc2Ž’”Fï Æä?.@'ï.{ƒ×9¨›J¼±ÚEnùF4¯0(øÎ.s¼‚rzd“W_ÄQGr"’ jƒ'#rÅH¾1“ŽÄœÔ–:¬·û%€ÅۖH?:1 §Ð3cÍgÇg©[²ÝÅm<× ” 4¨K÷s†À‘Õº‚kGÄ0Jt»©í¥ò¦H¶—¯¡Aþ„Ôz¥íå¥öUµh÷À$c;ÿa밊^ýã©´“dI¾V >P ßûFs@$ÓîÙ÷Z¬ ]Kæ™Ä€mVç ¤õSÊà€p3=7í¢êãíæ(œ+ƒ¼0ó:6Üí8g9ÈSþ5[#s-£Æp.%N:‚~€žqM›^–@ÎÒG•y#•R»‚ÿxÜŽEG6›sMm§žewq4“ä²ï\䐨^8#’{S-´ËÙg_´$è¨4•¤]ÊÌ­€Ç•`Gº°Îy½-Ð un†äy~b*uc#ZˏZ™®B¥¿œ®§äG]ÈË÷—êF†Î(Íõ¬×ÚTbXÇÚP¬†0ß+0ê¹ô<Ž}k=ô{˜¡–(J¥Ûi Wþ4fç$ƒòŸQÉéV#ñ SM$pÀòD[X|çnáœýÐAàž=qO¶×£¹žc‹q‘ö3¬€ ùKñ`ŽêEQ¶Ó/e~Г¢ hÒV‘w(?2¶UêÃ9äTÙÞ[IóC+3þé S Äç•+’ÞÊóÔ¢®Çâ1qnžU³¬Ò• + .㏘ö àêGZ¹§'öGÚ¤…Dѝ“£ÈFÀá‰' }Æ(9¬µFŠÒ3%Ÿ0H +§€ë’UÀëžrqPÉ¥_ÆËM¨[zà¬mÕX’Ù,Cc‚¤õàV’ëË!C¬Ž›Üäev܌ƒîEZ¼—7ê-–“ŽÜ] È?ï‘ì:ó@ôÛMB×Q4Rˆ¦wg× +­“ó|ˁÓ¸è$Ö4“5Á¸¶€Hòm.˯Ý'=ˆÜßzQw­Ë˛}†bcwʃ#pÏàÙ¿ÛÍbâùcÚQó*1UQ–÷à†Á»Š‚ïK¿[“,fŽ5ÛWD‘Ì äà½pk£…¡¥@’”í8äf±?á h$»¶Äÿ8”FܘÎÜõÈ`Àz–×W’}AA„¥¤Œð£’3æ)ÈÈê7.HÏ õ  š(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +ŽH£‘ãw\´GrC‚?‘5%Ÿo§iî«qnG +ÈC±PÜ»Fp£8hòI#•<`ç9éWt›{»k§–KB‹6#˜n;@Û!ù©SÉ?*žçÆÈYÿf˜·d Dđ·ÐÓcӌQ6—g<ŠïnRË#/#¡8<‘ëÖ±×M¼¶“ìð@ïmܬҏ˜6çw$©?Â0@$œUë ;¥†îÚï{G;ÈVQ'۝ª õÚ~<Œ gM´(©ä€.Ö ¨0AÈÇ8ôÍFúE¡¶xRsåŽâß1Î[’HÏNÕ¥EWŠÎÞ†8ðcÝ´ä’7·æy¨‘bgiL³n$b¹o¼vç>¸«ôPXl-à†h•’bLžc³–È’Äž€ +k閒M­ßRv•û¥†pÄ{æ®Q@ÇE° )Ô 3¨éÐöíéÛæÒ,˜b`ÎWg8àýÜÿOj¿EPþȲ¼«+²²ed`7]£8\žxÇ –â –‰æóL‘)U‘%dlg•#®åV¨  +Ù&VÁ¸²²ÎÅy œ.p G9¥J²D%ƒ‡ ]ÙË0$’Np?!W¨  +Siv“îßn¼Ë#+ê<€õÀô¤:M‘ž9¼’6 ¸v0àœqž¸â¯Q@ÖÎ! Q02,M¹ œ•ëßèqôªw:œÂŠÑÏ;eÛ´®A¿ð޵©EeÅ¡ÙÇzӄÊpV,ªÛJŒàåp9¾µ(Ñì€lFùbï9÷.2Ӝ¨äð1ÔÕú(­­ŒaÜ4q¢ya7¸ÎG_LŸÏéQÅ¥YÃ7š‘ÿ. ‘ŽÝ¹ÀPOæ<9«ÔP&Ò¬‹£y%J°pÙA!· €ppyô¦¦b’¬‹ \mca@9 àz‚¯Ñ@ŸJ²’f‘áÜX6T³lç©Ûœdúã4°i¶Cféä1Ž7ò1÷Hîp}:*+š¶Ô/¦x݂FÑî‰?¿‘º0ǰ+¸ýä>¸¦6±wöW"p.¥ra·1~ð¦íß'©òÁëц=¨¨¢²4ûùµ >åRh…ÌD¨xÀp8È$g½F.•ŠšœÓÝ««æuÚU7 Í×h`8 Ë#ÛÐÐcEbêWÓFlîaºŽ )×ä 9Mފy§8¬ÕÖn§‰&gˆI<ª-áa×8 +ÈßÄÛ'Œ€u”W2×z…Ô•¸h‹H»>M¦0pc'Ôn}zRÚëL·Ê·S©ŽvTHÀ£c´ŽAÈ8ÝØð(¡¢°5BîÊîpóƈ΋2}å`T÷`Ù%}1뚞K›ÖðúÜFøºŒfaåäü§=G8õ zÐÅÌA¬KæÜ¦‚xÐnóP|²0MØ=v’£ð)œ|بnõ[Ï!EÓ,pµº‰·â ¢Y#kiå+?.å?p§vô?‰àuÐÔn¥òm洝RßÌ"iBoÚ0Fqè}9éŠÓ¢¹»M^òV’åž7푮UÕX¬Œ­Ôà€ÃÔ:óShڝԗBÏPhüò„•à0a×ê ýs@ÔW7>¥yh$ºQvÓ!XráYqåŒò:ñ׊e®«2]LÈéq‡x†UˆP[o\¤6=cQ@=Ëÿj_ÎÒýšéc Ž0ÂB§æÁõ(A·8©Åö¢o<µ‘j€&çï/=·.~…[·ÐÑXú¦¡qo¦¶¡j<È ;ÀÙó!ÆAÇ¡è}8>µFãTÔ ¹¸¶&/9ÚE[ž¦2ƒø†0zóŠé¨®ZmNt’gµÔ²”Ûå9$¦AáX€ÊGíàf¬Új³ÍªG¸I­Ð…‘Õ0I`vþéʰ#×oLâ€: ++×®ç·ûbÏp"´xpŠÁ8Vÿžƒ¨ÿ2'”]\hËó¿ÚmX¬Èƒp†\z2œªÐÝÌÁz¶,EÙ¸PÆ1$qn9B;ËÇ=þiúýø’{¥òñåHÔ¨îÇòU±ßmtÔVìú„7Ò¨¹ daˆÄg‚sê­×ý’;óT¯µ[´-ó´¿w&Õ åŸºJr͜>1÷H ªŠæ`Ô.åwv•$’•ÂF “ƒ‰q둆_]¯XÞ=Ü÷v·eJî ¢² ‰מžÜg€6(®sû^æÎä6¡*,@ùn@à #9$0Á'ïV5FãM»I7â…@\óÙÉrÊ sŠÛ¢²/ož+ˆ.î(´÷wšT2Èà·lއ×ׁYgY¾Ƌ:=ÄۘGåaÕF[*½Ô 8'£`ä€ÕÑ\íÖ£wƒ%Ä~B`3ÌVå_$€8 :ŽG|UF±soi[¯5‘xó•PH¤nGv$•ã÷ô ²ŠåõýÕ±– ¢q6jëÊgØýÆ£zbµ¦¿ÝoÔ¨µ‘ÈÿTHᏦÁ¦yé@tW1¥ww ³<ˆb_j¦@\á˜ûXnô+\Õ½"æáãžÖâr·D³DdÇPTx+ÿ+ë@”W7s©ê^dÊ©åy'šƒµJ°È'‚»€9ô=°qê³\[¼“êÙÂèIîRàÛ[»nŽr9䀦Šå›Zº„JóÜG@©å2½€ÝÃdÝHÇ\Ö֝tÏeoö·QtÌcqŒ0g {pqêhýQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@F­bay~Ҟ\xG|¯Çn}TçÑäŒ[<.Ò̗1»›|óÓ;Øw°8&´u¾‰T•V\Œ²îHÃ)^¸=¨U±UFk…P䁐AÆr;c#¯¨©în µ·3Ï"¤CsӞ•™>„&häk†3".Wï°ËFr8>¸1VâÓÀÒE„î$]… EØí´dãcžÔ­ªØ©”5Ê/•’ùà uçÛ<úw¦¦±`åÀ¹A°ہp2AÏp9Ç\sU&Є— 7œ„³+L-ÁÈb£8]à ðzÔ£GÚ£ ¼*aÊäîCò±÷ÇÊ}G¥Io}c +±:ÇlŠpI#nH ò0¼ý=©Ë«ÙÎgEˆ.íìqŒrrLÐæ¨Çá÷I™¾ÖNÓóÆY؂pY‹rpJžA#é3hhË3å(æ\îP~Pà%”ú†4eµ{ˆH÷*ªIƒ‘“‘ÔpAçÔTóÝAooçË q†ç$ŒuÉ"³¤ÑÝSC970E±Y‡ß#!w~ƒëxÀ§iVs ¬¯S +¥‘7*ýÎÜe€Ç@2x€-G©[19–5Nî9ÁöèÃê L×P%¡ºi@ÌÞzmÆsY?Ø2´3¤·»üØÞ!û²B«.1ó1Î9Àä3Z)b‚ÊK9˜ÍnÊPêŒ`žÿ^¿Î€"½¦ù7H4~óß 1Ô•ãý¡K&±§ÄªÒ]F ‚rsÀIôÇÏLZ ¾Re¸ÂÄ`ƒ¹³É*q‘Œ8©-ô/*RòN’ Ž<¬n*ÁÉ'vá·Ð|£Š¸ukˆÍp 9 dF1œŽØÈëê)·͕¸—2ïh¾ò $ôÉúœsޏæªË ,†7ûCy± PÌ͎lœ¯žp§Ã¢ùVæ?˜ÈÉíVžXã]ÀB©ìIéY#AÌe¹ó%WóM„ ‘†ÈÜ29ÉãµÈ´ð4¥±š@À.Ðñ®Í¸9]£œcŒuè(çR³¤FuÞä€=Ãm9ôçŽ{Ó­`F7)ˆÎ¯]Ûxõù¸ã½Smg2È÷qùÎûՖ¡NÐ0ۆp ÃED¾uÖá÷¤cÎXŽJœárØn‡šÐ‡SIô™/¡¤òÕɉyb˜¨÷ÈÅ,Zµ”­­Âî‘ÔsÈnœúœ¦“O²žÖâæY®V_<«mX¶@Æzœ’ÏÐqU.4"ðÜ%µÈ‡ÏR‡t{€RIär¤±SÛ8ÁÀ  ðMg:6¡ «¡B¦UÎ +©?ž3LƒW°¸•cŠåÛ󞟟o^Õ-¨´‰¢W-rÊÆÐy#óÏçŽÕKûf5™•v× Ê¨mÑãÝJu{‹Í*S NsŒ d罩£U±‰ 4±¢–vó¿vqÀç$†õSTçÐ$–ååûf ä’Ñ–a’`–À +ÀãûÉy¡ùÏ,°ÜyR·Î¸^ø ÿßJ­ÿ}xÐã¨[´ $2¤¸ ´á‹p£>班Fº½›ªí—hĂ=â[yÞK…†âì5D_*Qo¼ ž¬ÇÓî4™¬¹%°PíV8%Ô6àXœ=hÜZ͜–þ{I±Ý ƒ¸†Î8Æz†©F©dÒ¤ksg®A§=9<STN†Í vº†Þ<¸öíbw2OÆàz°ät:&w¸\ϼH#bí`8'0-œõfõ  KÛkÕ-m*ȃ©ö#šÇÈêî׊ӷÊ£fUQʌÉ ä‚Iûnjc·Ö_l·T.D!–B¹Áü#ð К±Uß*$„…(NpÛ¶cßæãò©.®à´Tk‰m«Á98'{ +Ë hãYïÙeY‹„ÁÞ Œ‚IÇzçî󜚽y`/lÒ)äýìd:Ê£aßî z@ M^ÓË-$ªŸ¼1Žwnãp#Šá³éK&±§Ç!F¹MÜ NIc©Ç8ôç¥Q_®Åß8„•1ÆUAÉ+Æâx àóÎãÓA¡ùP•ûB‡ÀÁH¶ª²¶ä dôÉžE^]JͦX–áÙC.Ad`ôɪ­X³Â#›Ì¸@ʤ€NpO± €zf©Üxy^Id·œDîÙãÞ©Îà@Èä1$œƒV%Ñcxö,¬ŠÊmª·8Gí€(í¥íµê–¶•dž8àô?CƒÍ1µ+5–hÚáU¡Îü䁓ÏNÉôªÖšdö×¢µ'–rdŠ8І'Ûqžzgޛ©iyt³¥ÈFp$BáHÏA¸HoQÇj=NÊYR4¸Rr3žGâGnµ<·DÛdp§c>?ÙÉýEdMáýñGå\…™G½ã,0§(q¸|Ëêsî+NîØÜ,låÍoñœ`äw(ƒT²/" ”Ì{Ó =zúf–}FÒݶË:«*F ÁŸN9÷¬Äðü‰h·jÂɈp3÷p¦ß—æÝ€~jFÑ&v}×ìT¢(Ý´œo!¾n ¦xô  0jðI3E(òXp7 Ý´ÕO¸uõ«‚â&…æ hX3ÅI ùk"ÛÃþR¨’ád ´E…+‚6‘“ÆÒWèºóoFµ–Î ˆeÞWíї ³)ç'ù÷õ  U±wVå ‘C/\Iç܃\qL]kNh^awKsÓ8ÈõãŽüUKÇ$*±JªÈ¦5ޅ—ËÉ*¥A۞9üê/ì)¤¹0Ë8ûtÂçzê< ß?N»qҀ4ÛU±C0k”Kç<`dýqíCj¶(#-pŠ$݌äcÎ}1‘œúÕ9t!%ÊMç!feyƒÅ¸9 Tg ¸dZˆøy™²÷{‚ D¥ +9Ünä—!vŒ¨â€.Ï­X“1—’pá$`€ß\g'Ký§d$xÍÂ+ Ënã.î¾»Nq×R ʀ!œ"<ê6’z‘éߚ®Þù.^"ß*ð¿ÅØwðr¥a©C¨,ÿgå¡m¤3‘}ùÔPjöH»¥UpFc`«d }A88èi-4Émo–au¾$‹ÉcùŠç+¹³Éàã¹ÎsšŽçF3\–YÕmÚe™âh·sŒ6x 8<§Ö€5"•% c`ÁX©Çb§Öni-˜¼÷ùfà´EØSjú{ç¯>µ¥@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@FA*“O$6YÎé"`ŽHÎ}úŽ ƒ×½^¦ìA!"ï#±É™  +±ßÁ-¼LÎÌÞPçÏCÍEë¬þDï8,ÃtÈo|Œ:gÚ¯²+íÜ¡¶œŒŽ‡Ö@$»{wR©!ŽGe$&:äqßõ¥KØ|¸‹Ë2?– gr—ÆpáV3±Bî98Éõ¡‘_nå ´ädt>´ž/+ð³¼j®Å6gy;[èGRIq}ûƒ%¨”Ç!ÇÜÆsԎø{æ¯SU3±Bî98Éõ  +ñßDÐÂòC+ùjù¹ã#ŽÕlÂòâ î/†GÊÁOLsÛô«¬Šûw(m§##¡õ§Pxç 3¸VŒìv< ö?ŽAükî@]Mejñ*±ûU»È[!ƒËÇmØü«ycEfeE ÿxÉúÓ¨•MkPyY3/;…&`Ÿº¶ÊޜœŒs!Ö.í¦†YÝ~Êé̀&xÏ_0ü£Øc¸é¨  -JâòÆvHdHáϜ \ädñÃNHẌRËr÷BÓP°¸1C3y¡‡Rã8ûüdCuèkrŠæ.5‹Ø|éf–;|mD‰“ ŸºáORÁ²@î¤c®D¶Ú½ñÕÞê‘›Ÿ$¿w÷{±ŸMÝqœãôTP5/ÚìõQۙ|ÇÀgPÄÆÙÛÇ}®JžûXsQɬêܸ™¢·VeEÐxVÛ݈|œwR½3šêh  Í>öâîKøeQ аUd.PßCvâ§¥dÅ­Ý ˜à’u]².ýê¾a\áPp§8?îî=³]"[ÅòΪ|Ép’NqÓéÔôõ©hKÔîø²Ô?9· ¼ ;€?€ŽA?©`È?ˆƒ]UF!ŒNÓČ¡Xç¨Çó?sRjó3¸70Mi#•”gœà0 ‘ƒƒß°jSÅ/ÚÞf’6ÁPëŽ0¹ ßæM®÷•…utP+ý­pÃ%ʍ¾dácã +@|냹N9Ç|ԗz¾¡gtÞi‰b…@fl,nA$’OL©\c¸aÎ+¦¢€9›KçMMöá_텡hJm+É1¸Ô¯ú´æëQŽôÛ«/!N%aó(ÏmÉß±VëÒ·è sU’à4WÖ·.#™B¬8Y1ÂàÿyK 7ïIw©ßÂшäF‰Ac1PՀ1³g áÁÆ9ÇLâºJ(âòêâÖÆæÊhãŽ~ìÞ¡ÈùyãåÝÇr1ФúôvÑ”† 6ãŽ~ë+’@[©ˆ÷5ÓQ@•¾§4Ѕk ÑI0g Bóªžû[?Ü`E]äw0C;(\H'u˜Ê€3þé,¬`yã$tPpȲ!A#!ÚÄzԔQ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@/u+K‰.¦´ÄˆÆÒwØ`Uˆ¥Ic!ʞ„Œ:Ëñ6˜Ú¦‘$p·P‘5»vEä~}?̹Õ$Õ|öë}Ë*ç"ðË´üãùÐR=4µÉË%½†»£ÜZH~Ív†6Pä¯# ãë]X Ž9 ¬­zòëNµ[¸64Qºù¨ËÎÒpH5«Uµe¼Óî-ØdK/æ(tuu ¤FiÕÁZ$°øv=f)äûu£í•w®ªpT¥wPÈ%…$C~túÎÖ5¦ÛG"Ƥ•b]Çq’kF¨ë:zêš\öŒv—_•¿ºÃ¡üè(5R/šÆö!ÆÒñ•;’UÝ8ëíSEªÙMt¶É6'e,”© zdW;pójÞ27îu}-·0î¡êβQÐìõ›5Åݨ(;‘šé¨ªÖ‘_ØÃu Ìr aVh¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(®V 4?Énþd@z,ÃïÄs]Udx›LmSH’8NÛ¨HšÝ¿»"ò?>Ÿ*xwKdT¶ +®AÀc… çåôçž+JRÞ!`…§5CÃúšêúD@mr6ȽÕÇ~u¥@ EPchZy2â&Q3ï‘C®}ÇJÒU +¡T`€)jޝ=ͶŸ-Å ¤‰K”~ŒQ@¨ªö7+yeÊ}ÙP0üjÅSºÓ-n¥ódŒ‰Jí.ŒT‘èqÔTðA¼ H$U}KXŸÛR³Þ4V†X­$Ù Vùúd;Ð]ÿdkwZ3ño.nm=6“ó/àk¥®s_Q¨i6úƘÂIìÏÚ"#ø—ø—ñµayýŒ7PœÇ*fŠ( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Š( Zø‘x½áû¶:¶dOD˜}áøŽj{Ôø¥-o imϔ»ˆç§|U¿iªiG Ûu [·÷d^GçÓñ¨t÷³ñF…o5ÔYa÷†J´rPs@ֈÀ°¤­(ˆmË6æüMOU4ý:ßM„Ål¬ÌY‹3ROZ·@s—¬þ)’Öõ˜Ã%¶èâAùºwé]SÔtËMN4K¨÷l;‘•вŸPG"€2¼0tÖ[‹9ž OuÎGèk¡¬è4[+y’X£*ÈᎠÈõ­+œ»Îâ%½#ý +ü§=’A÷Xû•ÑÔsEñ4S"ÉŒ2°È"€04µ:_ˆ®´áÍ¥Òý¢ÙN~aýiº!þÈÖî´gâÞ\ÜÚzm'æ_ÀÖµ¶“kmp“Fº.ÄÞå¶/ ÍQñUœ²YG¨Z ÞiïçGâ_â_ÄPíZÂò+ûn¡9ŽT *ÍQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEËCÿ/¼?vÇV̉蓼?Íu5‘âm1µM"Há;n¡"kvþì‹Èüú~4¯Es©«Mªx:[ËL­×’C(êŽ:çPh¢y&°¼¶ ¤‘~û̐!#‚zæ€:š(¢€3ùP»ã;TœVf¨]j1GrÑB-¥RT¡;”ç ֜±¤Ñ´r(da‚zç|/o æ§b¨à›+Žè܊éh¢Š‚òåm-ÚgV`:*Œ’}*¦‹«E¬Ù4ÉDèæ9"~ªG­h‘žµÍ\ēÅ1܏–ÏSÄrú,£îŸÇ¥.ˆ²5»­ø·—76ž›Iù—ð5ÒÖЬå’Ê=BÐfóO:<ÿþ"µ,/"¿±†ê˜å@€,ÑEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEÏ[iךV¹}5¤+5…æ%h÷…)/ñcÖ­Gáû:Ü,2DÙÞbºçnqZôPRÑE‡äJž/ûDQ¸‰í¶JÛxÈ?/?nQ@Q@gjº=®¯ŽïÍ(v¬…F}x­(´6æÒْ6–|²dŸlšËðåæš÷KŽÉ¤2[®ðJg’¼vÍnÑ@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ą$ă½S±Õ-o›d2~ð"Ècníìx>†®Öbé¯ù¹·‘@.[c0ß|~,|úвêÖ16 ÌEUü¹:‘çïsÇ#^*C¨Ù+7vÿ¾ÿWûÁóóŽ=yâ³ïtW¸¹c˼¤ù‰‚O<œ àØl€GzY4yw¸Y!2IÙ#xòñžڧ؃×&€/Ío”LŠÂGØ +°8çn~›ˆБM‡Sµ¸¾–Ò’Ic]Ä+܂8=AÆsê+,h7g·î’DRRÊw1+µñÏFëìy紐è÷PKhÉsÈdf1üÇvw¨ç£»Øúñ€ Öú­•Â1[˜‘Ñ É¸ w ñŽõa.íä·7ÏB3™‚£y¬«ý ®¤•H–ÚyVGFC¸’m ÿð~¤Ôº~”ö²\,Ò,ñL¹vi1Æ[sӎ:þB€-NÀ@'7¶ÂÛžjí'Æs×ÓF«a‰K]‚¾÷ò?1Ò©ÿbº›wK€%@W)œ•ÈBFyù”úç¶W˜¢æá#må¿w¹Ba‰M¸9ãs/n`P¤º¥œWP[ã2Î@U ä Î0 I{{ Š#Lqæ6Äs`9ã'ø¬aáùÒÖxášÚn1µ˜&UÁ';ƒ!Úµd·šêÞk{–L2®É#*ØëÉ<†‡¥j¶Û´éy–ªɐ €ôϧ§4ø5 [‹“3Æò¬å$Œ^GùÍP]_'dŽA#2´i‡ûÀgý¯œzzTvzEÝ¥À™gˆá‹wœäà’Iä€ÞÄwÉ  Ú(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€?ÿÙ +endstream +endobj + +646 0 obj +<>>>>> +stream +x…Ì«€0ÑVžF,û%Ù +𡈂ö¡€œƒ»ffm!~ƒ)=Ur‚ÑaÌ$±!Šmd‰ÐPr‡²cœ¸ž¯¨Ì¥Êmÿ{/茲 +endstream +endobj + +647 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÑ1À íZkpŒ +ìM[†£ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@ʀ”)R¤ H2 e@½ì¶ +endstream +endobj + +648 0 obj +<> +stream +ÿØÿîAdobedÿÛC +  $, !$4.763.22:ASF:=N>22HbINVX]^]8EfmeZlS[]YÿÛC**Y;2;YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYÿÀ¶Œ"ÿÄ + ÿĵ}!1AQa"q2‘¡#B±ÁRÑð$3br‚ +%&'()*456789:CDEFGHIJSTUVWXYZcdefghijstuvwxyzƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚáâãäåæçèéêñòóôõö÷øùúÿÄ + ÿĵw!1AQaq"2B‘¡±Á #3RðbrÑ +$4á%ñ&'()*56789:CDEFGHIJSTUVWXYZcdefghijstuvwxyz‚ƒ„…†‡ˆ‰Š’“”•–—˜™š¢£¤¥¦§¨©ª²³´µ¶·¸¹ºÂÃÄÅÆÇÈÉÊÒÓÔÕÖרÙÚâãäåæçèéêòóôõö÷øùúÿÚ ?ï>Ûkÿ?0ÿßÁGÛmçæûø*íô¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥/¶ÚÿÏÌ?÷ðQöÛ_ùù‡þþ +»E¥U¸…†VXÈ=à +_:/ùèŸ÷Ð¥²ÿPßõÖOý ªÅ Y‚wAERQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEQE^ËýC×Y?ô6«^ËýC×Y?ô6«帣°QE†QEQEQEQEQEQEUxo-g™¢†â)$O¼ªà‘øUŠ(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š¯eþ¡¿ë¬ŸúUНeþ¡¿ë¬ŸúUŠrÜQØ(¢ŠC +(¢€ +(¢€ +(¢€ +(¤$“@ Mg î}*6rzp)´89Ô&âÒhUÌfDd:®F2)clv5-sþsG5¬o VíhÛr؀ 22:s]`xŠÄöÅIå)" 9a´ã8ç8 uϵhiw†â6Ši#kˆŽ/v8íÅ_¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(½—ú†ÿ®²èmV*½—ú†ÿ®²èmV)ËqG`¢Š) (¢Š(¢Š(¢Š)ÈÁ¦Ë Š&r‚ŒáFIú +ͰԦº¹•¥Š;{PvF]þw|ò=8éÆy  ï.VÊ–D‘1cøV,zýÝÆ£µ¾šÛw3{È¾¤Ðó]ñ‰#`s‚888®æÄY]=’]›HÎYTJï$ƒ×ïU™2mµL¹sÞ²´O,iq,SË:®Wt£yv"´Q¶·±©)H‹,mŒ«¤zƒ\ÊtH€¿èêÁ^I¤wpà œzê*•þž—¯ ³²˜—¸È8üÀ  hë"+£V‚)Õ1$¤Q.ØÐQèI@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ì¿Ô7ýu“ÿCj±Uì¿Ô7ýu“ÿCj±N[Š;QHaEPEPYºÏß¹‹z¡“ycŒ„¾§ž>•¦¥4‹Á$„±‹,y8Uéüò~•&­b_¶@Ë sæ9“bŽÉ=@O­lõÉ^ZA§êÉåÙE#.ׄyc +_aï1ã#º &à\Y)ÚªÊH`£œõÁäך~¡f·p`ýä;—+¸gèx?z–ÖêËužÝüțî°ùÖ7ˆ´ø¤·{–i“bbAËJŸÝüù§h÷¨.Úѯݲâ6•]Ǩ$t=ð=ëm×rûÓNÂjç'¦Éuas ¹ŽÞÎÒVÉYæß3±üzž+¦®rûN†ü‹-.&–_ŸíÍ´ƒžÇïgéZzMÃܤÌ÷ÌQ¼²#\*°à÷¦õÔKM XÛ#¨§Ôí9©ºÔ”-Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ì¿Ô7ýu“ÿCj±Uì¿Ô7ýu“ÿCj±N[Š;QHaEP%ёcßÅS·š½hU̓¹Øåˆ¾Õ~ªÎ “*¤*¿¼QKzŽxúP bͱ܅*7†–Aƒåà}ìî}ªm2éï­¤ŽU|§Ëæ•Ú= +þñÖ­E »IH°™ÚQÇ${ŠÈ²²¹±ÔT¥²Èª6I2…Á?y‰å›ôë@Y´š~¯´vñ¶Ð!b€†q×yQÓýây ×OUu‰-,ež%V(2wg{“A“T´ÝTÌ +]þîVo‘Jà…=7Üð3‚}(úŒÃN4Q®ùŽZG< )<§€p>µjÂån­D€¶w0!€zqR]ZÃy Šá7¡ç ¡ŠÌ±Ô¦›Q6«kåÅå|òc¦OLŸOph]oLMJÍ¡!Ÿ¸ì»¶ä~Í;I·Óqöf™Wn҆BTûà÷­f†*œ´îö–áRFßÃùTttäu†X¢‘Ná‘K@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@Q@ì¿Ô7ýu“ÿCj±Uì¿Ô7ýu“ÿCj±N[Š;QHaEPMe¥XeX`QN¢€*[°·Gû@Ž[å&Mۇ¹5?”žwŸnÜäôúT7£%É)@FÈÀ%ëԊž $P@õ]ÛÉ7’’£>ç§_ËÒ³%NÓ.eiîX—Š˜z•_ý˜þuJòÕ´‹–º†EÀČ8Çuì¶Ùâµ/­¡¾±{‹2­1Rc–ÆãŒ}àyüèü3$ð¤±6äqj–¯¾;FxåxTÊ;’B€ àrG5OEº13Ú!D*‘¤›w'ƒ·Û9­·E‘$Uta‚¬2  +zUÓ\@É'úØËs¸6Hœ­Z‘‹ó¬ ;[½;ZnÙdmŠ~T<€…Àã<×EÔPR‘´â’€a±ØÔÕ^¦FܾýèÔQEQEQEQEQEQEQEQEQEQEQEQEQEQEQEW²ÿPßõÖOý ªÅW²ÿPßõÖOý ªÅ9n(ìQE!…Q@Q@fœÕ= +výšWR˜O º{ùˆ žµ¯\«A5ž±¾(&*Ó3"nìÝyêpIc­tò.FGQQS­%Yí!•] Ãý}èuÚÞÆ€J§kg·zJ(Åm‘ƒÔT”QEQEQEQEQEQEQEQEQEQEQEQEQEQE^ËýC×Y?ô6«^ËýC×Y?ô6«帣°QE†QEQEQESx¸òì`Š#&Ip¸½Hiŋ[IęÂ"CcÛÞ§‘<Ä*“ÝzÔ*ÐÚÛ¼’‚5$³;u÷'ühHŽHïÿãÚçÉPU£¹<’<r >µ£y¥Gsp³#˜9gO½Óà|f¬½ä)gö¢[Ê vœœœ:Öu½òjÒÜYʏÓÊÇ'!Ú#¡>€ö  I¢pÐ%ÂÏ$Y }ý隅ŒwÐl~}×çåõèGåXH?±ï)§#TX#ڛK.sŸO\“ùs]8 €AÈ= eÇ$E³>[ˆÑ°Ã*LXìÀü§€qÀëïõ­šÄמ{{SäÊ`ŒŽI‚[Ý¿„téÉÏWR³k-KûAv_~ù;`.y'¾¬îږ“æZ±ŠIžOu=Áê=©AKÝ;6Îw*áO!•€éó ƒõæ²ô[éf¶xЍS„æ/Ÿ˜òsŒç–<‘ÆhtŸ³ÊöŒ]ã9a!bBc ‚O9ùNOLÖÿ½Š‘ùÖµl-®æ1$Q9-)†0~aБÀõå¸ÝkCE¾{û–C™¸Dف@3ˆôkµ?iò€zGU:»g$×JŒ¯²°uaG Š‚ñcyòFқpdT^I ƒ¹¨4Ëñxnˆ‘ÏîIdQ黡>¸  ,6œRT².GEE@J¹yê**U;[=»ÐôQEQEQEQEQEQEQEQEQEQEQEQEQEW²ÿPßõÖOý ªÅW²ÿPßõÖOý ªÅ9n(ì&sҖ«Bmm^;(Œq¾ÒËêGsÿ׫4†QEQEQEQEݲÝD™† ®‡•#¸ÍOEszä‘jW6s‚X¾XïóoB@ëŒdœ€Tú횄k„R 9.YÇ߁Ÿv-¼±êö£°ÚܹÊþcŸ®0j8͞’ÏÈd¸— ÀŸË=”g8úñT4Yî$Ô£ó&I‰Ó@û¿7sìçšÕÔ´å½*BÅ¿¡ipQê¦}èøé\Æ©iú¬WdŠ2ñn;|±ÆG¸öSÒºw…@¶I„ ‚ÙltþTÛûcwi$JÛX•½ôôüh[y|ûx¥ØÉ½Cmn«‘ÐÓ]v·±¬[UH˜yò*K66ÙY§È¼uÇSõ8­æ— Q@FÜm=ªJ®GjœŒŠZ(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+Ù¨oúë'þ†Õb«Ù¨oúë'þ†Õbœ·v9‹‹HìµHåf;üÂ\œzzFô¥t‘È’Ä’FÁ‘Àea܆³oíU°†h rT2Å|Å<à‘ÈñÆ)֞^›$v³Þo–õQÀ@£¢Ð{šC4袊(¢Š(¢Š(¢Š(¢Šk¢ÈŒŽ2¬0G¨®^ÊEÒµ8ãšò¸óþP3ÔŠ;=kªªW›,m§¹‚Ú# ù˜…Æyä’9àdþ=ÍÂZÛ<ògb œÕRÇP«(–/)w”B[‡äޝÊx¦i:‰¿¤ÁUþò¡à”=öžqîqš‚ >êßWÝÄ-ÔrOÝ€ƒ€ÒÆÛCî&œàˆ `ìެp3êkZÞe¸‚9²(a¸`àÕ-bÍ®­Y¡,³¢¥@ÜGuž3Ó5W@¼a²¹8™2å㪂/Ldšn¹k(c4Ä:í"&eo3 c·–Àìx­[)dšÙZX¤ŠAÁY1Ÿ¯UŠçÍü¶šÀI¤š`ÄFÝžW©<ò}3é@r.}zÓ*r20j0p{PO°vúô¦Q@(¦«n\Ó¨¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(½—ú†ÿ®²èmV*½—ú†ÿ®²èmV)ËqGbŒ¬×+oN@;p¹¿Ï‘®ÚGl¯1/²VÜÙås׿îüt^€fµ…¥„òÞ0Tw'çs÷GRÐÔæœ ®­j¬  ‘ÕYXõäi žÖâ;˜H³·‘‚0ASV%½è‚â ;;Q ˜•¡ÞǒÃ$€½}~c[tQEQEQEQER‘ƒÒ–ŠÀ{KÈuu6¬p+«‚Þ%ÌOQŽ;u­ÉdNáKíáy'…SÕ-..àÙÃEŒŠvïäpXr3Ò¨h -Ús’• +0¨ùõ4ý7[7t±FYŒ– Iû£ŽO®:`æqX^Á$+,òHï² ð¹åŠôŸ\õâªjð\[jBêh÷í +Tä±Ï#“Û +8ëÓ­jK¶³§‚S#œ^Q‡ìzŠšÂþ B2Ý·׎‡Ó=2=«;ZÓÞi>Ӂ,j¸xÚ\{°ˆÏn•—+ÙÞ¬ (ÿ»`á®ÍÜÀè Šèh3EžCmök„dž2$‡$zsWä^7zW14Ù:¸h€’S¹ðØ]á²IÏr1ŒôéšêQÖDŒXdr CE+ ­ŽÝ©(ÈÛ[ØÔÕ^¥²9ê(ôQEQEQEQEQEQEQEQEQEQEQEW²ÿPßõÖOý ªÅW²ÿPßõÖOý ªÅ9n(ìSÔíMÝ¡È*KÚ}sùÖ.xl¯¥Û¼nÑ®øÙÕ|€6íÎl±çŠé«Y±V’;”Þ3"™r—··†C¯i٘^B«m!ž<«풿3lúÐÒ/…ôîFمáÁcÇSŽ‚Òöëv²¹ž7’Utʶß1r@+ëòó‘ÇZ ·¦ë‚+–bŽ¡<ÉYÀÏtõ&€:j(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +ç5[/³^›˜ÔE äÂ0aݏì}z×GPÜÛ¥Ì&)3ƒÈ#ª‘Џq@-®—Tµ‘Hx˜ä’„Ž9®=Æ+3F¹ڄ–žJªÊì–p ®A!zõêO'ƒFvloV —ò¼Òw¬«Îìàa³–úôéÓ¥Z×mÇæ¬d³¼Ãvl e‡lœ +_·•eŽî&)°Îzœã?쌓޵¥¦]}ªÕw–óА:9Ç\Ǩ¨´»§»·–9™h˜£}Å{N1ž¾Ù¬›Y¥Óµ)wòÁàÆÌ˜I8rǑے}@³«Y û?-¹ +Ûöã;°ü S´çó©© Zæ. —MԌ–ÐE%Í-ž©êÌNïaÁìk§ªWzm­Óù²ZÛË0À *ë¨òë’Êҏ´ŸUÜ:g¡Åfhy8¶1ÒrÒõ²¨òzæµå–ÚÆÛ|¼:œ*Šçµ›e´¸{ËdŽ1*ïYcb»YAbp¿|;ÿZê(ªºlÍ=’;—/’¬\wAéÇQV¨¢Š(¢ªßêšm³\^ΐÄ;±ëì=MsçPÖ<@véq6bx7s/ï²½¾¦€5u}zÇIÂLæK‡û–ñ Ò?áY_bÖ|CΣ#išyÿ—h[÷Ž?ÚnßARÚÛè~ŸKæ_ËËË&d•ýÏzè-î"ºˆK‡CÜP Ø]-,4{X óÉlœ"c×Ԛ±i¨Éo}íí¬òMžWÛ£¨Z-õ”¶ÌчQPiº=Žœ‹äZ’®$þ'šÑª—Ö‚åP-dCò»®íž¤™÷«t”“c¨ZE$yLG3r$ǧ¯ÔqT-í¤³Õ¶´»tBFæU +«Ð*ŒÿãܜK©$z]ü3C` ¾AÚpæ?Â2>QÀöºy|ûx¥ +Ê$PÛX`ŒŒàД²AdòÄ茄1.pÈÈÏlŽ+3Ã׏äÅÆwóW|rÊr€7žqœžqÖ®ÙêqÞ]Í +)؄ª­ó×¶ÏOZÊÕ-f²Ô¶‹å¤Å@eÎCd’êIôuÏ­tr"ȅeMspÌ4íbDŽ(Ô.DŠ¿.ƒæycÏ,} t6× s’<ã$ÃpAüESÖ­LöÁ’3##e v>•nÞÑ`¹¸™]ϜA*z.8«4PEPEdêävwFÖ+k›«»¼¸“·×¥Q·½Õu[¡òtáx›-!_åŠè&Œ:çËGtÉMã¡ÇéYV7÷'Qò.²Ë0&6òö Ž Éæ¶k×mfŽëςCœfÜtÀ1>€qÏz~±et÷ñË™$lT…S’¬§°?*ÿ½×·5|Õtàû +È +ó¶A‘ã9äTP·ö͓’kR®ÑºÅ*îôÃc8?¨«Z}¢Z@Eíµ¤. vzP +Ȟo´Kº<ò¬Tää𼿎A­Š£©NÖVÊmÑP3áŸa!3܁ɧiw¿lµÜ̆T%]W‚98Èê 8=3@šœsÛ_E4 Ìêsn?0îGz±­Ø¤I£Y#utaÊrãTõ‹/¶Ù\oŒ‡\®á‘íëŒã§8¬ÍêH®~Èò¦ÖÉäo-’XçÇ?Ý´¶Ëµ±Ûµ%Lë¹}ûT4T±¶FQQRƒ´æ€'¢r)h¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(½—ú†ÿ®²èmV*½—ú†ÿ®²èmV)ËqGb½é žÆ›Hcãlv5-W©‘·.{÷ V>³§$„^F#ŽT<Œq…óÛ£Ÿq[ÉcI¢x¤PÈêUîZÌÒnÍ̓¤-¸BzÆÈ­’yÁ瓜÷ÍT[©!Õ_Ë»Ê íÛòóŽÊ£$dœðj/ÞiWó2 ™ÚL˜T3F®zääWï㝔wÎY£9Ù? ‘ž@æ€5•ƒ(e!”Œ‚:KTt»k‹X s¼{<¸ÐF=2zÕêJZ( Š)®»‘—$dc"€(É­i±\ýï"Ž«»8úÕàÀ€A==딷·½°–M/Oµµ•FK\OÁlúâ«özҘûR–_ îXã?ÌÐ…팓¿›mrÖÓãip¡àÔ:nŽ,®¥»žæ[«¹WkK'_@AZ”PQÍOŽE §¨©( ~͚ÛShžj<ÇDTŠ1ÎÜ`ôÆzç𭻉ZKÇÌݑ1“ùҘ”È\äåv‘žáO 2zŠÇ°–W¿»µº–9<ÕÞÑFżŽ1´žÙö²ÙkƆ2œD5Ûµzä(íÏÞ=pE#Oý™­ˆã†?,Pm±ë×Ô±ãƒ[·óI ”“@žcŒc·ŒœN8”j°µ[I ™®c–%äYÍÀ +À`eº‘þÈïõ«z]÷žÒÁ-Ä2ÍpPm%p:¯b #rê´[Isu :õSê(¶7êÒ9YJ92ž?NÞ¼úÓ¤\6{˲û6•3Ç,…®fmҔC±K3“Û%±Ïµl°Ü0h +(éÁê(  #oáüªJ¯Ó‘Ö§S¸f€Š( Š( Š( Š( Š( Š( Š( Š(  +ö_êþºÉÿ¡µXªö_êþºÉÿ¡µX§-Å„a¸Ptàõ©!ÈŸ7ß0÷¢Eþ/ΐÈéÈÛ[ØÓh  S#lŒ¢Ÿ@zŬ×1om®F3#áGûÃø‡|t«ZtSÁj±O´•àbIä÷«U‹ªMu æVIÕp¦‰C ~`îqÓ   +·àXêmpëÊ ewØÁÁBOnz +èb6$»€8#‘õª .­¦¬VÉ*¡†å ‘ØàñƒŽ•KA¹&¸´WjÈB¤d¬>ðížç9  êȺ֤†S ZmäÓŒÂýwt­z(›¸‹ÄWÁ$C`ªsåÆw¹äñZ¶ÞA3Ã<àÐ~©­-ÂÙÚÀ÷wÎ2!Oá¬{ +KkÛø® ‹S[dk‚B$LI\ 󞴗6wö÷Wi«l^ä‚ï)9\ ~5EíãÐÕµ]Võn/z“€£¸A@=ÈÝe]~ëE>€2õëfžÅäW#ÊVf@3½qœc¹ãŠÃ×F[wƒÊTHOÊQ·. 'ãðãVÅVº»‚Ê-ҜpJªõo   K‹iì \¢£ÏÊ<µ'æ9#êsôï] .’B‰€!ÎáëTna·Ö4ô.ì|ÈJ‚ÊèÁã=¹ªš-ÃÅs-´Ž lr™¶Öþ%Ü~ñîqÀé@50\H%I|œd%Žß—¡ÇBG¿ô©4™­ÚRܔåž\î9ïÈۊ¹4I<2C ÝŠU‡¨#° Óéú’™ä—fÿ/|Ф<`dr9-“Я­oÈ¿ÅùÔu22ɺ0daAÈ ÔDm8 §FØ8ìi´PŠ)¨Û—ß½:€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€+Ù¨oúë'þ†Õb«Ù¨oúë'þ†Õbœ·v*îòä á¿¡«=EW Anç69eèOqYÁô´â’¥‘r2:ŠŠ¬iÍL9¨*HÛø*’«ÞÂÓÙËlUÙNÓ¸Ž{dŽqô«P;¢Þ<7&Úr#O¸‘„ÆÃî ŸBrzԚ݌Ò]Gp#´"Æ e9ç¦O©éz\•–þÄêŠ|ÏݦàÄwÇ#XàqZ63KLQ2üÏ$e?)nø=ÇӏzžÆñnáݍŽ£! •aÔdu«UËé³¾Ÿ¨ù7c£V‘ví á@<x8QŒjê(¢Š¯s{mhGÚeX‚ÇбQ\™´¦õ¡Nß®8®fãU½Ö®tÝR6ù®äȏÀÅ]Da„j$ ¸qh‘³¸±ÒÁ0ÛϨëó!ØKnôÉû¢¶ô˜®-`¸¼Õ%&™·ºƒ…‰GAšƒPñ \M>¿¾?ò΋þótz Þ© ŸÄjƒ•³ˆâ%úÿz€ uûJF·ðý¿Ÿƒ†»”b$úz§°ðäQÜ ÍJVÔ/ºù’ýÔÿuz +ڊ(àc‰F¨ÀúN”´Q@TÔl£½¶(˗^PäŒÀŽ=ªÝ¢Œ“`p€”Y]ƒAùºtèÇ5Ζ“Þ,ß" Áـ˒ ~èã ëÍX¿¼Vþg–ò±;R4êÍéúT6óÁ¬i¬IwFʯ××un 㹄K n³†ìpqǵVÔ­tY`•¢¸Œ¬¹È5‘¦ÝKe©˜. +Áo!eU•ÛŒ(›œñÐcšé(F¿Ž8£µš@Qöv)°H\’OãÏzؑr2:ŠæïìŸr%ƒg–F—-ÿLÔrqŽ€`zé!q$Hàc#¦sjŠŠs®Öö4ÚU;[=»Ôõ^¤²1é@QEQEQEQEQEQEQE^ËýC×Y?ô6«^ËýC×Y?ô6«帣±XFG Ó%o/ç•íê=*½¼¤“c r¬zcÐzՕŒ¸’Íê§¥a³(´¬C)È# ÔN»[Øô¦ÀÛÆz™¨©w.;Ö©ÜD4täu¢Š`N§pÍ-Ca±ØÔÔÑ$ð´nVäü띳¹K-JWUÿGwXKù»‹¹<žXý: +骤:mœMs +'oã9$¸è=…R×àͳJUœyR>̐‡¯=@õÇÿ^¬é—‚á$‰ä…¦…Š0ÁÈìqœÇÒ­$ñÊìˆK`rÀqùÖD6¶šó^W‘ölŽ$\³裒x?ʀ7+?Y°¶ÔlÄWav‡VRÝÏnÞ_>—cǸgkã#ò¥ž¸âqò°Á  ýGW°Ñ¢Hä`ŒGo囨(¬Ï³ëÿ7lÚ]ÿ–1ŸÞ¸ÿhöú +ÒÒô-5Úd =Óý뉎ç?jÐM?M´Ó ÙÀ±'|u>ä÷«tQ@Q@Q@Q@O w´R¢:·g\Ê¹»)ŸOՉ¼(±»4bISsÀ߃…瞕ÔÖ^·lk,ÅxØV]¤)dî u ×§4µE’åe’?*Ua‘·¶;õ8À뚟GšV…à¹iæ7— œGÝã§jGºKëo0wt]² P ÑÓӂ9¤ŽÒÓHo>K³ +3#ÈHÎ{mî}úнJÕ¯,ž$m¯ÕrHŽÇâ±´K£mwöI&Ió•t"Bä g ÀèN:蕕Ô2Êà ƒEaê¶ ØæµŽFi$ ¹NDmÀ-÷}r8éšÜuܸïÚ¡¥¶‘ŒqÇ;ÆnB"¡ïŽp=3N‘psë@ ¥IENFE-Es·ò©h¢Š(¢Š(¢Š(¢Š(¢Š(¢Š(½—ú†ÿ®²èmV*½—ú†ÿ®²èmV)ËqGc˜®AkwËFz2•#êÏ5 Ž$@ËÐÖŸv'½Þñm’x¸`ù)ë·ªäžõ­ l—iû¯ÓØÖr]FXu,88`r§Þ§‰Ä‘†zCQSQ¼¹¿Ù~±¥Ðd².{eNFA ÆQZ*dmËïÞ¡¥VÚÙíހ'¦IʅeOQO¢€)£\ý´Ç°¡Æ1Àõ¯j×6ٌ)t9*Ùïu8çñß=ÔjÚB<’Ĥª‰ðè*m”²Jd2:?#?º>‡¹õ  +ºM܍#ZOŽE@è0äé÷GÝöÍj×9±½–hԇvÆË!EûÛû`úœžxVՍÃ\Ú¤Ž›Y€'Ó<â€,ÑEQEQEQEQESdE’6ŽE Œ +²ž„Ôê(›Ÿ:F¢¯”†;š4\ù‹ŽÙØqÉ8§yn÷pÃsnÏꄨz0ädä­.­§›ûr±H"”©BØê¤Aöç?P*Í¥´vbÎÀKé’OÜÐM: lQRæê=›DqB1€O,jüªÏª9™HJŸZçµØd‚FºiþaóDÄd§tò õn¼ý+~ÚX§‘‘€Àçõ v5]7XGž}ŠÇJ‡?Ö<¹ç§Ý…tÄ1YšÌÅçÆƒbfVŕ2Ã5 9ØÐÔQEQEQEQEQEQEW²ÿPßõÖOý ªÅW²ÿPßõÖOý ªÅ9n(ìr¼1ÛsØÏ—ܧh  œà}ã€zñZ¶ó-ݸp $z©ÌSµ $–9-ÝP£‚cܹ +~žßʲ´õk}N\ª1a‰œ’̧øw1ãœýÐ8¤3¡†O12~ðᇽ9”2•= VVòä ü-ÃCVë¬Æ> ¦ï¯üid_âüêÞ[‰;to§¯áV«X»¡QJFӏʒ˜FÙî*J€ELFE-Tù­X2[†•‹*¹Q´Ã~µn¡¸Gx¿t#ó*\dë@^ºŽœDErê6#¡ìG§×µgi3½­ÓA42EͶ,±+¸q»æ9õéÅk-Ê)Ù#€ê0͌.q“ÍfjÚ|³^G$-嬸Gubzãs`@õ  º)¨ +¢«6âzuQEQEQEQEQEƒ¬\_A|‚96ÀÅJ=#%‰ôôôÅoT±I=¤±Ã'—#.ºcü(¼ðÇ«X!!ãîD䑆SøŠe¤¶V-Œs;ÈXîfË|dz7@}Z4}>M>)äÈr + $…üOSïßW^·º|Ɇp«°“±¹;•F2ÞäÐԈ²ÆÑÈ¡‘ÁVSЃڹ{™l¯¼ÓÒ1`dË&9PH,ÄýߔWK§_%ý¨™09+Ãn‡½7UŠY,e6åÄÀq°áˆôÏ_Ëހ'¶˜\ÛG0]¢EÜ ðzt¤aµ±ùVv‡pyh[+Q1ì`1Üv™äÖ¬‹‘ÇQ@QE*6áî)õ­žÝêz(¢Š(¢Š(¢Š(¢Š(¢Š¯eþ¡¿ë¬ŸúUНeþ¡¿ë¬ŸúUŠrÜQتJ^Ú,‘änSè}ò5Ïꖱ™bº!ãMß½u#äÇFÃ|¹õ8'Ò´´­NÚYRÎڍeÔÈêG¾z“œç<ՋÈT>YAŽN ŒŒÿŸåHf~™q%͹Y£l  +]†<Ïp+JÝÉRŒrËßÔv5ÏÚYÝY߂–ñº¨Øò”È åvjÚ$£ ’½G¨©’ºÝ:ݱ˜ðýߥ4Àr Ó\‡^Yy×ÔTEٌ±"äqÔTU20u § ŒŠ×k{ÔCiñ·;*eJ±E5NášuQ–Ü}¹ÁæF~ff“åB;íî}êÜRÇ2oŠD‘}TäQ" bxÛ;\8÷ªÖ +¨% +’© ‚]pû¾Ô »{‡žîïÊhY¶‚vG`ãø•”à“Ó=«£+Ä 4bìnÌk±‚õ*N:Ž@9ÇlÔún¢¯3YHÑ´ñôò¹0'Ó®9ô  J(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€3u‰î Ž3ñ$xÔ_Lgõ=)a–ßX±–[ød²psødqô5r⹁á”e`â°¬gmܱÎ-¹˜6î$9n¬yÏ&€j’iú‰fXÞM‚7~rðå¾ê‚z(룪7út’Å4Îʑ|ͶFP} ŽjKK¸®>[|¼j1¼ž¿LòG½gͦݥÓ-ÃC3K¸ãj19<Xœž¼ +г¾¶»ic·œL`»™úô?…Iyn·V²BÁNáÆá‘žÙǵahâò;ál% m >hŒ»´ÙÀ'ƒÏN‚€7v·±¦ÔÌ»— $mÆ=*:ÁÈ  R‘‘K@Q@Q@Q@Q@ì¿Ô7ýu“ÿCj±Uì¿Ô7ýu“ÿCj±N[Š;7嬣-d"‡Ï}Ìá ´Žz£©8ëž*ä¥ý¡$2ŸºêF +°ªë\ÂÖҌíù£!Š‘ô#‘nՓ¥ÜÇe¨di7á’@τüÛ{{±ÉÈ=é ÓØ<̺5>RiÕ5Ò© q ¤ rìj}»mcéÕ¨«M áÈ>õf7 aß·¥e%aŽ…¼¹ +ºÜ¯±î*v]ˊ®ë¹x8#} Mžda±ƒÐCWqÑO‘pwzõ¦Uäl6;š«Ô¨Û‡¸ Õ{¨Œ )”¤G!À8Ü¿j±MtWR®¡”õdP{2¡|¸ËÉŽ$cO·­Shm´ëˆåv8–!¦~ñÀäŸRkJ! S沓“£J«yn—öë$2eÔ¤…G=y¨Ô2¤ð¤±6äpO¨©+&Í#Ó&ŠÞ{âòL6ÅUërkZ€ +(¢€ +(¢€ +(¢€ +(¢€ +(¢€ +Ç×4ïµ*Λ· Ã*ç.¾™þX'=kb£žž#— +{£•?˜æ€3tgžQ:Nw"a0ÀpqӁÇlŸsL†Â-6ioï.AØ0®x;yàžçžžÕvÏM³±æÞ-¤73>§’M6úÞ;Ø#š6,Ñå£xÈ$äv'ŸZ³mqÔ 4D”q‘‘ƒŽÜVf§¤Iq+Mi"Å!ù¾l°Þ€ÿµ‚EAáë¨TÉj•ÙԑGÄyo]ǯn•·4K<DùÛ"•888#h#D¼䳕ØH˜ÑÜÈT`dèNrqœâµ¤\ߝsÓ}®Òk[eŽ&Kv Ìqn €XÿEÉë]´¿i¶I6Œ°ù‘† +žãó ÑA$ÔPãlzÔµ^¦Vܹ QEQEQEQE^ËýC×Y?ô6«^ËýC×Y?ô6«帣±‘Á÷ICDäs܎¿…W×­RKµ68Ut#!‰ GBF{ç”QHfŽ™!šÞXä@) oó pyúÔEv;ǜì8ÛµPK l›ogçèER–ÀZ¦¡òçé'{úÑEeÆY## èHô¢ŠØAJ§køQEOEP bÔÝX²«d;ÆsŽ=qþsŠ£ Lñª@ãr̦T|üǞw 3íE[]¶ŠÞF!œ´äÈWŒuÝ݇¢ô®‚Öd¹µŠhÁê`E5Q@Q@Q@Q@Q@Q@u€ŸÙs¼‘‰h_cµˆG±ª^šB.­ŸËÛnáFÄØ2rNíÓõ¢Š–ðÚhÊ.#µ ,°ôÏ8Éè=…hZ\ «H§U*$PØ=¨¢€êÌ˂6ƒó3š}PrŽ7zTtQ@9ëE5Q@Q@Q@Q@ì¿Ô7ýu“ÿCj±E帣±ÿÙ +endstream +endobj + +649 0 obj +<>>>>> +stream +x…̱€ EÑU~m“™ÀgPz,\_àºÛ¼·7GÀ*1ÁèHÌ$žQ¸fJWW2ƒ²a\¸7$¼Q™K• Ú±zœè†² +endstream +endobj + +650 0 obj +<>>>>> +stream +x…̱€0 ÁV>&/Y«r¨œ›€ö¡Ïmr7vƒ’ž¦9@o(¤h,ðJ“’ wýÄ5¡àùŠJ®UØ·¿Çñ傤 +endstream +endobj + +651 0 obj +<>]/BitsPerComponent 8>> +stream +xœíÐ1À íZ[pìM¤-Ü*++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++{𣥠+endstream +endobj + +652 0 obj +<>]/BitsPerComponent 8>> +stream +xœí¿’Ôº—ÇÅ«lÑOÐ<Àü"R²¹á`ÉÉØ­š2)ÑMàÖnÎ<Á=µû(ìø¯Ž¤#˶dûHú~¢·Û>þ¶ôµl·¾ýä÷ïß +Rð†H   + 0@2`(€dð†òäɓýK`;pâÜ +¨Ê>L +>PhÌ{C…ƒÆ¼'0P8hÌ{C…ƒÆ¼'0P8hÌ{"ßPþùãÉëÛóÍåǟOWnáᯗ§ë»«ï¿?¿JZ™âڏ#> +I¹|DJ×Üÿ -nCYÔÒû:µ»eÞ:7ëIÜr ûÛ eO$JÛ7Ôù|wG;ˆ@Ca%à'›õûÜüäG¡ì‰ CiûƋï—ç.Ò7À›××·Íÿã+í ÊXD–)56ZÒéŒþ7šÀõ®Ñ÷Ã%º©‘nGãŽÍ~MªÞ¬–jï”;RZǸfóßýÍ÷·__wq)JõÒÚ£B~;Ì¡™ªÜÝa({"ÇPÚVÙ´³“ÙâúÖJúbÛ:/º'=üõÇ·7Ÿÿ|J;9í¨s:³Õ̍ÇÊ^Ùå*cKãžß©/ºGÑzÔ wú{¤|£VíÊ*¼å¡Ëó6ånGq‡Ö»Ò¸¦s¼¶€V͛CÙ1†btzÖ6ÎÁºª¶YÒ¡ƒy²~0úÁBCy`˜Óøãâ÷¿Ìb¾{¶¡\¼;µŽ”í×>Ciþ{ö‰ßòÉ×µçh®èF™=†R2B …k€Š´aÿÙÒê6'K %xã°w”››ûë¾™û]8B¹vÊÝ÷7BQž-{»6¿jœ¶]Ù´€0”’‘a(NûsOÜúvÂprí/çÍÁ÷°Ïý z¡BoX¡™×ùÚuíöiÚ5,_ üN˜#5 …¿‡r!/ygÚPœíœ¸C{óm\èn÷PJF‚¡p§^ý`ö_óOÈCë^j¿‰Ð#˜s·Qeœˆ•é0ºÈ‰¾a="Ïd®®noï§.©¬ørGjT`úàùêJÝÞÚOy¸-F(îvØCó åYާ»«¡p#¯ICÁ7eˆ’=¤‹0B±/mR~!9 hÌ{C…ƒÆ¼'ȔUCهüF(µÉ,LÇErc.JQ@ɍ¹<`(Eq\$7æò€¡Äq‘ܘˆRÇErc.z eé’M¦œ¤ù¢ˆÿÛq†bEp²‰œ Øà 2“xd‘ܘËcŽ¡8S½ ,É÷–ûŒ1q'P˜2 ™ûk¯üØö?ü<¥6”t_ëòuPb(sæ"MU[ìú¯ð®ß3»ʞÌ6”_Ä^É\C¡,N0»…Â& å×D=Ÿ‰m(\bÕÌò"«˜d±gn?0”=Yi(\’žÜ0Dµ‘¥l"©1™¶K”%qš c(4'ÀÚ"“,ÛÅzØ»p:›rhÌxn&A›·¾DX³Ì館ÉVº®xÏ}j¾t9OIð%…,J¥e§A[ÁÀ–äöçÀ®Y9Y`({²z„2¶Ñ¶g1)$ºèÖ˄­šíþå¯÷]ÂÆ‚ŠÕ.}i¦¾„~+LBXe2G&RT-Åìsöëۂ+‘é¼8|º“òŠóᱟ¥S›õûÖ Ü}ÒN š(DïšÐݯۘÁ¶¬¸‡b¤ru‹ø¬ ÿH›EêYvÉcïěfm(\¿óöÆQ¸'7TÊÝŸ·Æ«ä¿‡ÂmfН¶y©´þ¶!LXY0j—‘†²'1÷PÜx¡É¦À…­r£‘%†¢s˜åaÆÂÆC R®;*qvm(nE¬ÕY7­=[ðIäôäð=úŠ™h;i(žÚü£IWÏOçÞNšõšÇ†§§[ÿ2PImÌåC) +ˆã"¹1—2eAÀPö#”¢€8.’syÀPŠâ¸HnÌåC) +ˆã"¹1— ¥( Ž‹äÆ\0”¢€8.’syÀPŠâ¸HnÌå2:§X³ô«Y)¿tžGŸagêWÌïáC®èÒ<Ä1 JÕ1ËçÎ ²4Q0”½˜;Bñ‡%Í¡6CéæÂóÚîÙØ¸…òöf¡2&ë}TWêöþmֆ2Ο¶ÏZË<Ø ›)ˆ_ßÂPŽ!ÆP˜Dkªý6k6î{çô£g4’üüàÎ;vVPÌÂ&]IwަW='“•~h"x¿æm(šæèÁ˜¯;¥°½ʞ¬6OÒ眬ÙȊÅ÷³E3݄]¡ {pßդ͌=Gw£¡ÜÎ#\?üXN)æ`'†¡ÉZCñ%}šqÞ0ȘŠÅ÷«EÓqÅÄ +¦¡èw9›\³}ÏêNÉLç.Ãx„ly`vRaʑ¬5Òg8k6²bñ}&åÅ8]w#”fä?Œð‹0”¶•¼0 ½œñ +F(‚ˆ2ö~H8k6²bù}&Ý=î]ã/¡3gÆE¹×¹ûYGÜC‘Èê{(“!£ÓY³‘gÐgˆa,ø“nӹŐÿåñ>=û¡/Œ_èxGk°bì„V0”#‰yÊÃ$}*b3Þ¬ÙâŸò(.–—Ø+LÅëòÊåo(ìˆ6l(¬€öp¦{†²'ø¦ì^p1ÉÉUœ-‘ܘˆ²úg¶$Sq6Erc.JQ@ɍ¹<`(Eq\$7æò@¦,¨Ê>`„RÇErc.JQ@ɍ¹<`(Eq\$7æò€¡Äq‘ܘˆRÇErc.JQ@ɍ¹<æŠ5³o˜:ÜL£gãf›© Ý” +c|A¢˜–OíÉ£Ïl›)k&ý’ÈǝΔ¥‘0“™²ì¦`({a(f/p&"7i@wʚø¦š…wÅÊ֙²ÞyA²Åa3_ƒ™²$>IOôÈm +†²'ÛÊÕÍÍýõµ2f‚vˊ7”Í3e35”ot,ÿ‚q¬ƒ—€ÂtS0”=ÙÐPºk"E«‡Ë¤Â eûLYvʃ|qü†â͔òEµ»†66Cٓنb¯2ÃPÚÐ9ʅ´‰Ê eƒLYëTL‚Cä‹ÃôÿéLYº +M‰aæ6Cٓ-G(]šò££t?¤1.¨ËP’gÊZÎX(_œÀ%“ªdß:jOPo¾ÍPxØ eO¶526ï^¨ÁP6Δu37uoÊ@œÀÏïØ¯ðîl\ rZ‘·ÁPödcCä1?ÐS®¡lœ)«f87rDZˆéLY*…^ÌiÅo +†²+[Šõªc(zŸs­%‡>³q¦,UΔM¶8læ« +fÊò‡ËÈß«†¡ì ¾)»Ȕ=ɍ¹<`(;LÙ£Ü˜Ë†RÇErc.JQ@ɍ¹<) ª†²¡Äq‘ܘˆRÇErc.JQ@ɍ¹<`(Eq\$7æò€¡Äq‘ܘˆRÇErc.™†bLÏá'˜˜Ë˜¬T+ýÓ€1Qq}fU¦,ՏÑÚÜû!ÈÇ͔åBs LÂÿµgeßêmÁPöd¹¡-BOðìÛ·ÕŒIÆÍ†¸Y‚ +ßgVfÊ*ýå|kÞ­;È [.S–ê㠕µ7Ѭ¢¼I´MڒºR·÷oa(°ÜPÌÁ +³B¿„ ¨6b +5”uy(|œk(n\Oâ˜fh:£÷¸¸·²Ë:¹š¤Ñ¯0”#X;BQæø”s2òT4¹LZ/ÓPÖ%¶½ÄJ)³¦ä·ïP­=7dõ3Œ‡0¡¹ü;¹—àà¾'º ʞÄÞCé9g(dˆ¢†ì¾&iKÇÍVa(s3eÉ +þøw.tO¯ÇÜDùâ¸G7šK0ýčÕƒ¹ÊQ¬2”Æ6…3BÑ£¥­çLbfK7”Å#oú;¹`]1ÑÎ#\œ©ÈÉŠïmƒ£6§¨áÚ†r†BwŽÀ=#òêêöö¶LC‰¹‡âwÒM”÷.DâLʔŸø_ë·§¯RÓ¶¾ eG–ÊÙza،õ¬Î|èC …^1j(+3eM•ú ±ñ±¤Oš”ƒ8>Cñ_ȱÿsñ±ì.0Bٓ ¡Œ—ªüëºc’q#öw&HW6Zª¡¬Ê”u¿pñ(Ï¿þæãc=ßs‘-—)Ûü"†û&Ë%ÜqK_…¡¾)»Ȕ=ɍ¹<`(;LÙ£Ü˜Ë†RÇErc.JQ@ɍ¹<) ª†²¡Äq‘ܘˆRÇErc.JQ@ɍ¹<`(Eq\$7æò€¡Äq‘ܘˆRÇErc.°¡XَdF §fsrì¹zba’o‰æÑg‚™²-֌J#d\87}¶AŽ8|…þøX[Šà¦¸xZVuʞÌ¡ÐϚF7ö˛dGeÍëêâ«5”`¦ì°–‘~J‚–ôì¶ùé³ RÄyà*œˆu‚`›bãiÉÜlºyʞ̻ä-¥ 0§Ç^ÝÜÜ_ë©ÄíÂnYµ†ÌCÑËIú©‹ÒšËe~úlóŸDq† +›yÅ||,;½):"aßB_€¡ìÉÌ{(ìo6 ?MK¥ó“nQ­†ŽŒõ¤ŸöâµqU:¢znúlƒDqÆ +=ñ±¬MõÿωV¡ìÊ웲ÃÕïÙN5òÚZG¹.CñeÊúÒOÕ(³G½ }Vž8ŽcvˇøØ“WŠéMñKÜ Yʾ,Š€ì`nϞhª½þ&Šóß°„K?e²ÛÌ˄pú¬4qBù¸ÍH– ‚·©ÉKžáÖ eOfоn„Qs¿­á»w[“¡„î¡0¿…§ÎÖ Æ¡«4‚/HŸ%ÎT>n/ ¤ðý| ³)¿‘W`({2ÃPŒçÆôQ”¥ø:‰ºRC ɎXé°ÃËæ£‘9é³bÄñVØÂúu:˜ؔ3ä‚fa({6ú´¢Qøæ—ܧË#±Ö"¦ÏL̔%ëq·Øï¡L¦ÏHEˆÃVØÿ4B×<†Â¬²–’_# †'_ +†²ø¦ì^ Sö $7æò€¡ì2eBrc.JQ@ɍ¹<`(Eq\$7æò@¦,¨Ê>ÀP@ÀPö#d@2`(€dÀPɀ¡’C$†H   + 0@2`(€dÀPɀ¡’C$†H   + 0@2`(€dÀPÉ@¦,` ¼uÀP+Xl(¸¸Lø °  + 0@2`(€dÀPJâᯗ§ë»«ï¿?¿šµþ?O´å©Ù÷F á ¹R#Nœ+ ¥;ƒÐ¨§á¤²Õ%úCwF(Ê<p.CÆ ŠFo{"xÓÌ^•EéŸöŠéq-¦…„su&·v¼äa3V/4«hþ¸~D#Nƒr¥ Jœ(5¸ IçÎc(]¿w5,Ùî’'öCý0g(v‚·,AYÇ»EÁúÃI¥ÿF÷PÜ29C!B¹¨ÜhdÏ{(\Æ*Û÷'ßoû¾É°‰\)ˆ'Fµ#”1w®;eO$[®=*eËù¡û £ûlG£à8àÁŠÙ;“#÷ÜCIR°µf”þ› +s«`ÒPØëœÎP¡°«æKӍ™5”‰¬Ãîq‘DŠ¡ÆzCѾ{‡©oÊÆ}螜:󔸇b\ó6a{·†U°^Ápº(ý70”©2'ìÅ @µ‚ÞuT©W¢Ä2œ›îúV­î Fäû ¿ž_®Ċ³^C±S)77”¸]¯¡ÆwùRDïMßJ¸¸ºÄá©ÂµÞ‘Flʲ“ExÁ[Êaá©ÂµÞÊ\²“ExÁÊqá©ÂµÞÊ\²“ExÁÊqá©ÂµÞÊ\²“ExÁ[Ê!á©ÂµÞÊ\²“ExÁ[ÜC9,nöÃÏS?ÓĘ;¨§!“(¦åS{„kŠ@æ&™ÓME®Dœ ʈÚ^ð6†24cÏDäóù|w§¬ˆ7Õ,¼ƒ¡L㍌5Ws6ïG+g‚r¢v…|ˆ¡\ÝÜÜ__ŽÒeºµË`(³`Ò zåÔ ³ÚÏý?Ո3A © >ÆPºk"E«‡Ë$ʼ†b¾`üW8ÀP6'…¡Ø«Ì0”ßï½ìåÒgζK`(³˜i(ŽŸÃPF`(›sÐå÷çS{ߤÿ!q e¡¬†²9‡Ê+ý0¢{ý†2ÜCY esŽ3”ññ&ó=0”iìŽAl„ÿSU$Î0”Í9ÐP¬WCÑûœk-µNëŒKèЯ_³q&('jWxÁø¦lþW6^*'Dv²/†’=~òN} áê'Hv²/†R ‡%;Y„ C©ˆÃ’, F¦, 1¡”ÄaÉNá㒧 Kv²/†R ‡%;Y„ C©ˆÃ’, †¡ÔÄaÉNáÃPjâ°d'‹ð‚ã ؞CM™$dyƒ=݇.²'-:ÉZ' +ÉhÄÔôk•-N‘²ä‚›ÔP ÿùô쇞h{Ž1ɸÙ7KpñÁHÖ:äkö­L/¦U¢“K§@Yr ÁMj(æ`…Y¡_B‡T±0”þPv…ZÄ)J–üò6¡(ój‡s™>ü¾YKÑä6½†Č; ¾^‹8EÉR¹¡(óZVgu؆B†(½¡\~<ûDãfa(‚'b»cÕ!Na²ÀP:hÛhÎEj”¶ž3‰™…¡ø0r‡Ám‰5ˆSœ,0å,î{(F +äÕÕíí- ÅK°Û°ãþâÅ)Q–j e¸z“~¨Í;&ÍKæCj(îo’.>ÉZ§À”kÀê(쓎¢Å)U–º e|¸o{E‡;&7re}ÊÊN…¡ð¸ß§¯éžÃŸªK§@Yr ÁÅ7eó™²,…Ê"¼`Jö S–¥TY„ C©ˆÃ’, †¡ÔÄaÉNá#SŒPÊâ°d'‹ð‚qÉS ‡%;Y„ C©ˆÃ’, †¡ÔÄaÉNáÃPjâ°d'‹ð‚a(µqX²“ExÁQ†be;’jœ‰`Ìɱçþ鉅ï3Î>ÉZ/ĜÿdO²£F¹·ڗ"ŽGßaO¿k/Y˜ŒLÖá?JÏ +¢>G÷¸"G(t"1nì—7Ɏdâ‰{„¡aç¢x£Fõ +/½o_È*šl>²´‚ðs½{¶—…ýÈ~|®3”ÝÓ»‚˜Ï‘oŠÑ—<£¥|øyÒæÐUnnî¯õTâva· †fbr[0—ŒY©q8Y ïdu;ÐPtúÓ0Ê!ÞÁ×k¬ ìs´›bü=ö‡3†‹ŸËóc8Aç'Ý"J7MÓx)d(ö™ºqYÜÎêˆãóCή]þûQúVö9¦7}±w¶SM†¼¶ÖQ.D Ê"œd IC¯½Í{ å‰3Êr2åàÎøÌ»:iŽ25~PþöÏ® ìsLo(4 †¹={¢©öãÊ2˜ì±Y—<ôœ=ée1OâËrª2]6ç_AØç˜ÚPôm^#Œšûm ß½[JûS›e(Roæ¥c<ÀKð +÷®]e1ö¼F›XAØç˜ÖPŒçÆô”¥ø:‰†2~ZÄräãé¬ÿ½Æ2G(YÈ ƒûsBÌcF(tD>'½Ø³ÂŽÏ$©¡øÓ§Â7V»O—Gb­E˜ÖqPmì»ÝíߊŠm¿M#/ =nS,å4´½oIø?2§"j#¾’Å|ŽüqᛲùShxj,Ùɒ]Á0”ì)5<5’ìdÉ®`J-@–ìd^0 ¥ Kv²/™²€ÄÀPÉX`(° + 0@2`(€dÀPɀ¡’C$†H   + 0@2`(€dÀPɀ¡’C$†H   + 0@2) XBªÉ(äwyÇRÔ}ކH   + 0”Üyøëåéúîêûïϯf­ÿÏO^ߪùë'¡+òñn¿Ö¿ñrP€†"®ohB½$h(Ö +ñ}Ϫ0¼)«‚¥Èo®ýó|sùñçSŠ`(ÒhûFßQº®3ôž¥†’´Â9¦5cÿ|zö&"Š4¨¡Xg^rjÏÍV÷$£‡v EÞÒ-zó­_ÿý/jvGuwÄVH +4 +ћ{ö‰ŒgÎ77/®¯oi=žÝµËT¿:1~,BömŽŸ&¥遡HÃÓ]Mãh_p–?þùN}Ñ]”y#í{'ê!z·Ê³#®Bï~òŒP<Ç58oʲwÐ3G:Š4Ì3¬g âô<»‡x:½¹!Òë/¼Ÿ°ýÏs—g¡øŽËp;f´a‹0kp” €¡HÃ*ƒ×P87šèÎãßÿú{ô +Ç«mK Å·»듄ÁV¼.8šQøˆ@2`(ÒðÜò4ûÁ¾„éºÊ¬ÊøÏÍÍýõµ²¯}|·Œ5tã͙å†ÂìnΝ[}Yؙ~køˆ@2`(Òðݔõ=µ E‘a†s{ĽvҗÖ }¢+»Ò± ½3:çŠgwCÑ·‰Œwò×qOiÁ”ì EÜp¿º*¥@Cù¿ÿúŸ£K jŠÌQ€äÚçñԁ U—ìd^ðÊŠÌN+¹¶Ãޏ";Y„ C©á ñ(²“ExÁ0”Zޏ";Y„ C©á ñ(²“ExÁ0”Zޏ";Y„i(ÎÌpï$‰$A„ˆ0”s2IÏÒÉ®ÉslˆÖlßáÐ.Ï?òB}øyf$Ñã×Â:ª#ŠK*‹7`Iƒßd§¸ÐEL3HU0 ž°²†•ž¶\ÿ†2ëC­ÆPæÅ­N¿»hC1»ªŸ*}>ßÝ)½´Ë„lº Åme(žì[«÷ÿ:Q˜f8Ä&†2êìd\DéŸÞP %û×Û©§4Ùô‹zg%†Úa¨¤íµ í3גÒŠÙÈíd³¾ 9iôœ6Æ̒+E’GDCŒTc¡!-CÎZ»l+C‰ÿЃiîRºSÒg¹ðÊô–Ò +­ÌŒuúo1BU#9`ž‰Ã„¡šéª/½·§ÞO××ÏûÀåáÃÿãۛÏmÙé'¾dÓr¥ æÌ§ÆJCéÎ 4öi8©l5B‰þНŠ2Ϝːq‚êmºó —D—¼`öª,Jÿ´÷P˜ô 6‹xâz@—yq Þñ’‡Í[½ÐÜ¢ùàúM6 ʕ‚¨¡r”\£sç€1”.—_»–lwÉû¡‡~¤€3²O5$du¿`ïë'•þÝCqËä ś±Ã)w#Ú"áòVÙ¾?ù~Û÷MÌÔ2¿\)ˆ»öŽQcíeÌ ëNÙ)—kJÙ²D~è~Ãè>ÛÑ(œŠՐȽ39rÏ=”$[kF鿉¡0· +& …½Îé å° +›·j¾4ݘYCaß–+‘7ó"ÔXo(úÂÁwï0õMÙ¸ݓ5ižòï÷PŒkÞ&xïvÂP¢ +Ö+N¥ÿ†2U愽8a¨Vè{s…» ò֍sÓ]ßê±.̸óÀT?á×ó˕‚اëՈ0;¡rsC‰ûÐõŠÏ¾5ž¬è½é[‰'í +öcÚÔû¦B”þ鿇ò]½¶Ë1kŸòØÁäÝØ0TëÌ­¶~ÊÓa©©ŒÁ„kçܛÍ2™dSå~z®\?åéX­FŒ¡°O2hßY)“#˺Ã$iºŽ(ŌIÆý‘—LCѝ~l¼òsñ[›ŠÑߔ­áß°<Šìd^pQ†&ޏ";Y„\ˆ¡X9rÒʓ€ð†xÙÉ"¼`J-oˆG‘, .!SöÑ>¬ª+ À”f(¡¸?³Ev²/¸ß偡ޏ";Y„\ˆ¡t×úX Kv²/†R ‡%;Y„ C©ˆÃ’, †¡ÔÄaÉNáGŠ5'¡g锊”Ój…k½ì4SˆÃ’, N0B™3—ß %-͔­û6Áâë[J˜ìd^ðF†ÂD|pqu‰ÃS…k½#Ø0”9d'‹ð‚·0”ÃÂS…k½#0”¹d'‹ð‚70”ãÂS…k½#0”¹d'‹ð‚70”ãÂS…k½#0”¹d'‹ð‚·2”CÂS…k½#0”¹d'‹ð‚·¸‡rXxªp­w†2—ìd^ðFOyŽ O®õ.Øœ£¨‡%;Y„ŒoÊÖÄaÉNáÃPjâ°d'‹ð‚a(µqX²“ExÁ0”Z€8,ÙÉ"¼à•†>0B)ˆÃ’, Æ%O-@–ìd^0 ¥ Kv²/†R ‡%;Y„ C©ˆÃ’, †¡ÔÄaÉNáGŠ5³o˜:|yþ‘›ýðóÔÏ41æêiÈ$ŠiùÔáZ'¤›±ãS™²,eÈ"¼àm eð +ÏDäóù|w§¬ˆ7Õ,¼ƒ¡ÌàQžêJÝÞ¿uB¦,K9²/øC¹º¹¹¿¾¥Ëtk—ÁPÂ4¢þüÐ ¿2†¢×ɽçl@ ²/øC鮉 ¬.“`(:;ùýùäö k­Ü{Δ ‹ð‚SŠ½Ê Cùýþ×ËÞQ.}æl»†2Í`'¯¸¾a­—{Ïـd^ðA#”î ûè(ÝiŒ `(Sh;aû†µfî=gJExÁ‡Ê+ýd§{ý†‚ò*•Ðs6 Y„|œ¡ŒÏŠ™聡1úF£—²ŸÃçÞs6 Y„| ¡X¯:†¢÷9×Z„kŸ¡ S–¥Y„ŒoÊæ½µâ§RqBd'‹ð‚a(Ùóè'ïԗà®Nq‚d'‹ð‚a(µqX²“ExÁ0”Z€8,ÙÉ"¼`dʃJù@–ìd^0.yjâ°d'‹ð‚a(µqX²“ExÁ0”Z€8,ÙÉ"¼`J-@–ìd^0 ¥ Kv²/8ÞPŒé9$֜C–7ØÓ}è"{BТƒ‘¬u +LY‘È´ú/káfñJ–%Çܤ†bðøÏ§g?ô$@ÛsŒIÆÍ†¸Y‚‹F²Ö)ÎÛiòfŸk!õÜÚ¢ÅñFÆÚk9Y¼ReÉ57©¡˜ƒf…~ 5PmÄÀPXB†b¼NÌ¥q¦C§Ü,^ٲ䗷°ÁE™CmÎeÈÈSÑä6½†ÂC/yøÑý?$R3Ÿž“„)Cñeñʖ¥rCQ\ƒç … QzC¹üxö‰ÆÍÂPB´¾`Uê†S8~Cñgñʖ†ÒAÃØF£pF(zT£´õœIÌ, e'§Í\H”m^¨@Ÿ¡Leñʖ†¢œÅ݀#pÅH¼ºº½½…¡„ຏ¹Ìø¯q|†2•Å+[–j e¸z“~¨ÍXÏê̇>æyÔþMÒÅ#Yëxþùãå¯÷}ëêtºéa µ˜Eŋãv?n‡ʶÄÊhü¶Wt¸c’q#ä%k`>n†ÂAϵVTïÐs<«”-›³¡ä‚‹oÊæ2eY +•ExÁ0”ìA¦,K©²/†R ‡%;Y„ C©ˆÃ’, F¦, 1¡”ÄaÉNá㒧 Kv²/†R ‡%;Y„ C©ˆÃ’, †¡ÔÄaÉNáÃPjâ°d'‹ð‚£ ÅÊv$3Õ8Á˜“cÏýÓ ßgœ}0’µ^Œ'֗Ûk¿¨ õ‹‡¡oû仌&¹,¾Ú܌[ûµãuQŸ£{\‘#*nì—7Ɏdâ‰{„¡áÓa§c'f°"ùF=™M̔Õó´­ƒÛË⯍˸5ÞdÏ%ß§à™ðÇ}É3ZʇŸ'mýXåææþZO%î Ûe0” |:¬|¥(q:ls ™-·Ò^²¸µñ·Üڒ³™ÙÛ*î +;.~¹GéÃNÛE0”0é°íǧÚDï&ãÁ:[˜8 v<ÁC¡§þ†£ ŗqKÊTôV‚šk“ÞPôÅÞÙN5òÚZG¹îC™‰K»ÃD¨l‹õzqâðç|¯¡Œ·$L>ÆPü·n½ª½uðâC-†B“`˜Û³'šj?.€¡„áÒa/ô\³Åb4f_òà³´¶©Œ[ɟcjCÑ·y0jî·5|÷na(|:ì›odéœû³eþÐç&j¦¡$ ÝëTÆ-ƒÐ›²i ÅxnL!ÑHY¯“¨a(øtXòù‘^5®ë Uåˆcg†Z/ñ™²úg,%ŒP¼Ë¹ÌÊ£núÌ$©¡øÓ§Â7V»O—Gb­E˜Ö±ðé°Ü*̐j.Q¶qܳ»ù½§'SöÀ{ÕÞt؏¡ø¿k$æsä ß”ÍŸBÃScÉN–ì +怡dO©á©‘d'Kv³ÀPjâ°d'‹ð‚a(µqX²“ExÁȔ$†HÆC€ÀPɀ¡’C$†H   + 0@2þ +endstream +endobj + +653 0 obj +<> +endobj + +654 0 obj +<> +endobj + +655 0 obj +<> +endobj + +656 0 obj +<> +endobj + +657 0 obj +<> +endobj + +658 0 obj +<> +endobj + +659 0 obj +<> +endobj + +660 0 obj +<> +endobj + +661 0 obj +<> +endobj + +662 0 obj +<> +endobj + +663 0 obj +<> +endobj + +664 0 obj +<> +endobj + +665 0 obj +<> +endobj + +666 0 obj +<> +endobj + +667 0 obj +<> +endobj + +668 0 obj +<> +endobj + +669 0 obj +<> +endobj + +670 0 obj +<> +endobj + +671 0 obj +<> +endobj + +672 0 obj +<> +endobj + +673 0 obj +<> +endobj + +674 0 obj +<> +endobj + +675 0 obj +<> +stream +xœ]Wy\TW²> ÜEhÚ`{qA»[£€¨í†‚Û‹ ›  âŽ ›lih#ˆâh‚dQ9¸ Q!‚6(.Ñ(Ôਨˆ€‰ &ꌙÄGãnÝNµó{ç6Æä½?øõ¹çÞSUçûª¾*TÄɁ¨TªŽa MÙ ³RâšM‰Ê–§ì¡’{8È=— ùwkGՎTít¤GǂNð³dw„±ïG•* :v¼yQÎâÔä”,ƒ÷ô©3ûõï?àϝ¡ƒ‡øâsþxcZ˜™šœaðd‹ì…&ó¢ô…Y‘©éñK2 Ñq™†‰%„ÿ³Cñ È0_¼8$3kɄìð¥qËâ'æ$LZž¸0*irrÊÔÔè´iï›f¤ÏôóåÝoÀ@ã‚AÌ1!½I™Lú)¤/ñ$ÑċL#ÓÉ âCf’þd $Èl2ž $sH1’LBÈ`J†02 #áėDád"™DüH$ñ']I7âA:gâJ4¤#GÞ!ˆDTğáIœÈX’K.ª ªTͪ×£~v48Æ;þͱÌñŸNîNåN78WÎÄås—y=ÃÊ?"„á±8T<ÜÁ±Ãª{œ»:ÏpÎq®t¾àüÈeºK¡Ë9õ4õruú±«»«k€k´kškëY×ó°OcM¢Ђ›<Á¢’ׂ—T‡^yÜhMçpE¯Ó9ȟ¶½Ü$œÊ?€CœFj‘à’aéÔX†.Ú£r•d«bm‚¶íґíç¿L‹Ðá]¶ó_ÔáÍ(Îù 2A§õ¿)0YÀÁoÀ©² +Áà§åcŽôDcÈ$0 +F>áý +Çê “ÚN†ôòœ<î½Y­ÏŸŸj½¡g—È‚Nr!tVGëÿÈÙÒgV›j¢¯ ©A1;£æâG EOl8¨_ÀèMzôäW§Ì0²OÄÐ_a ÿö?àü㉸¡=‹/:À/ЁÝñ‹0×~G˜Ã.‰†×ùlç  ªÎÜBEpÿhÀÿƒÉ'õÚY§’Ä{„Ò˜ŒÔ(QÛÎféÛã¡ïzDŽÍmyòôxË·JԔហnª6ð³,ò6w8ă#½²ëdÃٖ}(ð8Ó£©­³B*ЉEÉÞû؞H옇ܙ18}£Ç ÇÌ bæ` +ûÉL~ÏÌA4KðÜð)Œ”]ÁͶóuº|˜ýêyÜñ×ôíüuÑV½å®ZÐN¾]Ss{ëæü5ÛuÐ[ø¸` ý”FÐiq¦@Q;ø¥`'Ž¿quMq•¥¸ú޹úŽíý—¹Øô:|˜›ú,p€à ªc¦§1Žåu²ƒ´øa­Ø—]¨çˆtLÞbڞ­ß¾¬4ï›å0zf׃¹E¹t‰˜–¸4<Éü÷â݊-Ÿnùt/ûšß€.?F‚?½NϖTï¯Þ¿ó$­§WRŽ•a𩮉E«>§åâ—û-'š¿ÊœS ÓÀݬ—ðò¾êN{zí’ÆB_4`ßc+þ‹q4î—ý;Ö¾K÷HX¹ö“ü\*&åm©ÓÃñ7ùéÒß"¯i§ßžâËäCîr3pOЍX•¾¿IÑ`S ¨n +ñê‰]ªuÚY‚öQ‹5Ø¢jT Jµ¦Kèó,T±«‚gî)WqèͣʦDz^YÃ[gÇ®³üÄ¢ÚÉòc„bâ'fÂV)LËHŸ95sO³î +úŒè{ÛæÖ&^xfi+KÄ~ÿjƒz¹RÐX··û­î0Pñїiӌ’5”Õpe՗‡ÌôáïÊn·lní^¹!Ù¸N{ص£ 'Ëz¹¬TÊÜñq Ý-B½ãΠ'z¾7 ÝòtàÁžÙ±y?½;'9'oŊ<ý²U”†äF¯êjș9úˆ>M῝?]ñÍI 5¯_Uü7ðÚb éÒ¨„yAs3*ª—[Îéê6Uþ}]ÉçÝìâ¤À÷&ÃÖH×oÂÇxƒP¯ËÃà4sÌã!¼ÊÁÕvl…?±¨ Ê8Ãû¢ß0ðSV„e†Œá1÷pŒ²²“Ýa EÌJÞ×Ò©ázŸÛÀ0g† ÿ´'f8¾¸Å¤GÐü®.¹–ü>©~ 4”Î]œ:]„"œñ¤Ôz"ØË;&„{óóßêZ®(Z¢û-‹êå.Ëߤ‚¼Z)N%ÐüÌúãJ§ˆ‹“­'ºÈ=94ò0ÏÖÀE§,œ™›·nÃr]o¡lMQ~)­¤Õ…›¿.)-ßÑ‹­Ú®í´ÙÅô€RdÛ឴ +ºú~=X‘0ö‰rÿ]ð….÷nT¬Çîüò`ó¢h*Å5¾üfóñÒýú’/÷•TÓ³t÷¢’ Qc½ÖŽþeðaBÏ¢6ÉMt±5q,ƒA’›8[øXÓ1[Àζ6ì*·q˜õ&¿×0á‚f¼@Ñä`d„qr4p¸[€mŒ¾ÿûÞ?p_!÷s³nÀ">åô¼ŠPv©¾ƒ~å•3õڂU—Ó.{´Ðc;žµ&t–]í2«×~Տ)mpLëo:­éI]ëÏ+è}:¾RÉe-э´žþøä‡‡’ù6ػĠè„0à×^0\µ¼Ú§G¿<6qñ,š@ß/]ú凖Ïv®=%®»/mºs°ú"½DÅŽfXmWJu#ë@Õ -O†–Üݚ#õ¥~©“ƒ“úRæUU}šBCo§=§Ïéí¯[›¿~IŸ1™OzÞÑ:z/*`g :Þô¡è3Ê Õ¨óŒ`üþ1hôLÎXrûÞðBõ³"h—™ ù×ó{оØU\œ¿ö Ý !}]FA6ó և¿g¼a ƒvGP„>a¢mf¢}ƒIÊf…Ôl(– Їîw€7xúÿ€ôxÝ΢,9±o ÂU)ã’RF+YŒ?¿`Lݳ7ÅF–ºuÑöû0–ýíÃÆÄØ½S=bèŒÌ¤ÄéÑæ±t€ˆ£þ,_ÎÜÞpB(„Ï›?a¶©¼&W‡}ø‚¸­Uéµ) Ëo0íüëupÓ3k¬ÖL˜Dº|"ЫßܰàÈØæGOk¿mÔ¿;X²¨š™ÿ †æ7%ÊÆŽ‹G4îۑ»¤DWœ³~MñŽ}ø Í9uIûL»æoOCèìE‰Q"X„öñ§œa2•aÂJªíå ‡%Ç7íù JŸ.ä/£94—†lÈޖ"ÂðÝ|VqÞVº5¼¿z³±úöþšú=·´>¥2íÀ¼’IT´y´Ã7ԉ™ ÔÒÒÕÛWl[±i1M¥)yæœËW.û,–ŠööÀ”êóݬ°™`Ÿ,”&Ì[ӝXæ»±ã- Ô,iÕª£ð.œ†>Žòsyƒ”ý=ïàD–¶ã=GboäÎNo³þéÒÇ˪Ìtr·Èè¤Á}§ÖÞüLdžœM8øÕˆdOÇ¡SΣÇ.=òå†2S­ïöºƒ7Ô'ld-sfX¬ñö¸®(q%µÇõ˜7忦ç}R°>O‡NÂî5֖SñÇÊÊ뭗ކk'è; +)e ÉO$HeÁ-öúۓ¿t?kQ5(öcàŽ$¿Ëf*…Ã`á̐0gÃO0b9Æ¿m¦íP) +j?ÀÛ[y!÷r‡õÊØÚŸÇa¶c8B>¦¬!Ÿ°šß]ßΫçAzæi\û_íÁó™[››+¿>|ï¢XÚì;—+«ÿ|–; /"." ™›§ûàÔô½a”ÍD“hÂÒyÑb³ ù=¨=k†;ÌW‚¦tö#LœŽØ»æ ŸÆ¦að¦àu¸—+¾9ɦ!vTƒs% °zP0R==‹^ü+Œÿ +)zSôJE®ÆÚ«…u‰EuI Êó®°.Ԝ•+F ¯JŽŸ] "ôd3Û]Hœ‚V Tð9 +i^HiÛSZUòuTK¶‡!òCåÕ,ºDë| ã_›¸fƱÕÔΫ +ò”Ä͓WJ¶•¬³Í6 +KücQôº}hž ªàB{ûÞ%Èa8’ýf áЪpËÆ!×LQÂI> +stream +xœ]X XÇžu™CÖÝ%àÈ% ¢‚œ"—\9U@E!’x%QÛ#ÞGŒ‹'j¢ ^AP/ˆˆ€. G²*/5¯fÓë{¯|IÞûv¿™žÞéªþªZ ¥3€’H$ƒü’ÒV%e-JˆŸÆ +¦aäa”t%Îøc’&‡FºR¤«S<ÒØÑP˜f;‡Àô¡”T"ñˆLð\º,'sQJj–™eDèÜñ&Xÿ5ã`?ÑÉlaÎ1óJZ±(%Ãl,¬JJ[º,=)#+hQú•+ÌÂâ3V˜˜…&¥¬L‹ÏüŸIŠ¢Â=2>¹4ÊsY´×rïLŸ¾Y~+g­òώŸ½za@NB`nbPRprHʜÔÐEa‹Ã—D¤E¦Ïò‰³ùÔic\Ǝsu³œ1~ÞëXÛ8;û‰_8:MšlFQæT05•² B¨iÔhj5C…Rc©0jNYRÔx*’²¢æR¨O©™”5EyR6T4åEÙRó(oʎò¡ì)_j"åG9P³(Gʟr¢fS“¨j2HM¡‚(gj85€2¡¤Ôʔb¨QK¥RrJA ¤Ì¨A” ¥KéQn”>5„r§†R”!%£²¨aOQƔ„Š F¢tˆ¹Ô%ÉDÉ*ÉɃ¤Ø6à¾ÔLê']/=¥c “«sQGE¤èz}‹™Ëlen²&¬#›ËÖ°˜3ãÖpÜ¿F Ü=°yPÌ [ƒéÁiƒwêšèÎÑ]ª{\·KW£7R/Wï¢^»>£ÿ•þ¥!Cf ©Ò=ôó¡†^Úd g0ß`¯ÁEƒ¡±¡¡›aˆábà +Ù§,IÖ>lè°%Þ k‡³ú;¤„y-‚½R"dÀR¾ /¥ß1x&Æº >ð!þ•i†¥4ìnáñt¡ƒÖ*‘RmLU64»µÃö6cÙ B>¯Ío‡Ë¬¬öŸU Ï\Nã· fl§ÕX¹ÌÞE¥ÇÎäºØ¾S5ÞäHSa[h,ep.ÆÁPLcš¯0à}ÚA4q TÃ<\Mˈ+U°CkT†­jhU©eï[@ÅÔ£ëGŠ/^PV jԕPi[ÈɄ‡gÏU6@7²L:—x>ú[oÄá2Fò°üØ +tzã‰Õ'³¦£”—¾<;suÆÆ¹ä%?üœ‡ÍÊÞ«Êb\\£c¦*ˆÚY͂u³¤°*;¥pB(ä±¥£9öÂÞ]0,»ßƒ;øOz‹­[ÝøUn؝ãfgÚ 2U7«ê¥j©` Å<œ‚ûôO$:/±`8§%sÅjUr–JèUI®©¥+ñàdׂÐxä’<Ë3ÅáA덮óªžóhÙK>èõ«£ÁM`6D­[´&#=8p‘+ÑiŒpàÁÀ‚yÍÍ܌3ŠS™‡Ò÷…s¢f-‚U 1è Ñ ×;eµ7pß'™«e+ê#»@ÓÁk À +…ÌÞ ….I äZY™µ#«…Üþ—ÛÓ°4ÍÞ!ô) !ÕO» l€TÂ]•DhѤóÚ£S[F믽KO*TZVðnÓDÏþ—ºÕÂHµ´ÛÎ14È!Vaã@>Ǩ?˜òÂ]pgaô+„gXáъ¿É²*ˆ;: ˆøš\oVÖ|31䌏)–OÄ4ž‰Ý:ÌAÑX~²ºXÏ²x¬f:ß]í‚eXwÎtG» g úwžý¢è÷µí*ÈSIzÔR˜ ¼ð‰Jû ä ö*í¶é‚R¥ÉèÃs «•_) ë xƲ;õCá» +ª€ÝÄu°2ûmy¶®3@ —Ìsåd!/Y%ÔþÏ ðF…ߐÌTÚ¼éjF_ØÔït¥°¾S*ø úü–·¾·°±¨¾K€ÕÔˉ04]ѲúVÞ±U(Ù$*zñÌùIŽ®’~èËC_qŽÌ¬W7Fo¯}ÓwÅâ„búßo3Ž¢"“²+gêf„o“ëkŽd©`“ +N©$W‰»E <«§Bf6ƒ&Ø`¿S¥ äó*2:Q¾¿Á °éÓP¥ü%³,±NžŽgVhÌdïL#lÌá,…í!êז¢ÖGòcVt AìlV£`U'éh“Â.Ö$íŒG{ùþ¤5c" +3j~·í›yûŦµ[r—²no‘ðs1ú»¦”´©…Á$‚®Ìp‡Z“n‹ë´ƒ¡Î–€¬èQ¢Y kÛ`G›{“±lµ°ÊHÈwÆå¬¬Ä,ÌÛiVüÅ{ruÖZ²Nµa¿Ée^ͨìTY=7“%öGéëRˆ$nkó!ý…h‹>KKà‚Ä +ù´ ƒ-µ®ØLp%CÐæÓ¢¹Z4æJÉᗰœ˜ô¹èò{Y4çËŹ™9Kå&#Î;±´CQj=›ÅƒZÜÁd­­`(®ö«'î/\7Û¾ý-´Ö£kz 3NkmFv X“c^0o»ßµvtŸž‚}³är'h“ +ߗñë7¾B\Æg‡N*à«ö-Ǽ[ÐÊÄùŠeëÒ7Ïåڙ]÷/œnAܓKK£+Ù-)9Ÿû®ÇƒÖä|½äó åi±È—³~üûƒÊc7jä;#NfÝ@ß¡}Û +vqx,øòhé†Ü̬Ei ?‹FœÒÙʛ…=Ýûï(8ÀýIp­D}wØÍÃ`òÙ+rùW +^På}ÉåßZšhÑ`ì_ȖÀq¼`§ˆ­!ņJ[3à‰÷â@¬¤_2`±`Œcé^¦/ìÕB` ¸æjƒ +Â[17äқ²²² ‹•¦ ï!D2Ü&¿Ç +ÏO—…$*à4 cq>¯î'¬Àÿ#¬J¤Ò°JÉS¢ٍˆ(D©°Vtœ¡Ò #N܏ÑZãÄ* xk-qÎKÎÉKE&yè‹m9;8wöðúÃ_E§Ðé=G¿=yäÐÑo/CžFøŸ1}–ôxÃ}}íÙ§SÊNÏ$QmnuO¹ö˜9´6Á°½Š)Lޔñ^ˆ³ o‚¡`|£å熫 gî%,õ¸î¦.aW—TH5‚xh-tiÒ1ÏâÔCpšf &]0 ÑKQ£ ß(‚Eª2 % ~ Õ¥Ý·FÜÂÂ}Aþ_t?òj™°âŒ3©•ab:é€%x:ö~Ž%`þ¨üTm‰B¶Æç1‹s…x¾‡«¡BöÖû;·þ,ïótQê']‚¼KªA6uÔª­ ¦ÇŠ’v1‚ž¶‹~ßgäfð«§f‰°>çÑÓ/¯­)^ô|zÅxrüX,ÅØ£Ç,A·­˜|V–OLŠŠDó/½š}fã™-ܶ:~WoͽvÄuÔ̲Qô‘˜X/¨…C„GL49¹y§îîù$?9þ Nˆw +g‰Â(öEDÅxïøU!Ñò´ªøã¾ˆ“Ù{£˜åqÜcVÿ¯~4kŒ `ÁàÚnÖþ™9ŠWÐÅÌQXAã‹:S,Úûs(|ÿV´õø÷Àr1–õ’Öl ßXrü¼È7:NÍ$8ºbé¬c)óå²·ÓB\G`£Þ‰ð ˜ý£x¹¬µ&ÔL-æ0ûø¦kq>³¢ã<Üç]}Xwíj“Bö—ëtÝsžn?)¸º­­¦Š$[€ +õCµ´‚(„¥ì)<‚Èw +FÐ eýÿL¿´¡-p·Yr¡.¼’VÂdþ0æžø…ZQCÑåûW땝xÂØ9Oâï%Vž›S«qX‡÷ØÂ°‡eÇoþ¨Ø‚=Â&ŒEhAq^ÄżꮯcpÓäȚ—=5µª>&õï$0à¿ã-,Þ1B¾ŽxÓÿC/[¹ºbò`~ƒáézˆ«Kñj,«X ó4¼Í 7›'n/ÕÍÍ¿ôº?™ ðÖ©.™;?<,¶¤ªª¤¤J!ؾൠ+b猠 ¾p@kLÖͰQÈRÖZ?™AÖ>Q¿"kåýk#ÈÚÐù%U7ŵú³õJI DóBN [Y­œ9L‡âŒH)bÿwƒaE;©ì=ZI§.¬ƒN~kÊó²S± Lþ®æÜôˆûÅ·Öz‚»cÌwÉ÷\ä²"là™m7ëª\`HùûÊó>X|“¥Ýu¥AÂ`}˜PTÿÝ6Á—zù †õߢB®âܹŠò¢”è¸eKfÄÈe߀ .åÃBd‘¾Ú3¤ú§ª³¾E  »öBzí/«_üËÆÏ@Úð[ƒD8 ¿óÚÕÞÂjfŒNÿ½_a¡”¼6hVcg­O‡â éÆ4QJIã3)¬%>?`„'"ìpû¿& ÉMÆ,¢ÁÇ2pís±&ÑRÕ"=Ý=Rhƒy|)»µäàÑûÕMu'®ŠQ0й‰ôóc<\°¼ƒªøZOóõXߨ•‰‘1òÉî«3PÎöXó-z˜r€ç3©§ò¯Áøø#˜ˆÀ!üǁ1Îta‹2ì‚H7£±«#6£§>Ò\x$¹O¢v;ÑýË‚Ö\x+ª¿N Á ØÛ %ð(yœ Þ8'x¹4ydú¼VÒI¼õv‹|>ώÙÌ%±6_C貝†mÒcD:úÃy¾ýpa´(8l|$ùÖK¡jy¼ÞÖÿ¹§Âú£@¬2¼Ê%B6$Ù!Jè_ÉVöéœ#нîÃ:ú&ë4ë>6)--’+=PDÀ- VŸ…‚3’£c³¬ûšÄOI¥o V*AÈWþs_ž\Rà‡8W—¥Ö8 ÇÆÚŒ·ùDBdé˗ý™_û»¤°_ØÆk·ui¢æ°iX’½ÌåD®@ÂeR5Ní‘ +Û;yí@fÏ­ï‹:¯¿¾1ü××IB¬Yq3¾8¾èÓï|ÑD䗖”™´6n³'×Ål¿¶óôþcÇ®üx²qOo‡Î[¦h+Ý[7Ù!<ÒnB̵·_*H—º7ùð’B߆Ô^Ò©}Ñ #zRË'ŸVÈ:œú¡ì§ ?­›b‹®xÂ&y³õʎãûŽ*‹¯¿‰¸–Òù3¢rÒ?S¬X—öuà–>V‚U«žˆž‚ÝºÀí}«¹°ÀŸÅMÚPږ…ŠVg2 ÷úë}èl;’_¦Ȗ‰9f©5¢Ÿ1—ÊήBÜËó1!ÜbŠŸ®"Bwiþò‹Òž¡-^¿+0³}…GF%|ž¾PQÑ4\û˜dî´ÜSþ“8ÁK¸@vÿФÒ\àfÔc +¹"ïäøÙq>Ù$x„u¿s¿p!ônéQÁ´÷5(`øD5–y‡/››¤Ø +U¯Þ »èJâA_Ž´o“øŽÊk‡`ÿiSë~îº}§S$ný¬cBþ!Ý~Œi¤|yǮݻ¶oßñÍn]ÝK;vï$ƒ]ß|£«GQÿ(BĊ +endstream +endobj + +677 0 obj +<> +stream +xœYXçÖ„ѰE̎±!Ø{C " +¢‚]¬¨(ˆ¢«ô²°,ì,,K“f/ ˆ:*X£Ä{%Ñh4jŒ1Þ5åLþÜÿÿfWÙñþÉ}®î³;_;å=ï9ç+ʦeeeŌTE……F‰ßÝW+¡S+áSë$&môUMÞä·yɔè¥Sc–ùÇ.¶"påªa3W­ Ÿ1{Øð.#ºŽÕÝÓmô˜žîó{õ^اï¢~ýã L”8xÈÐÎՅšF ºRÔHª5Eu§zP3)7*ˆêISîÔ,ʃšMõ¢æPã¨ÞÔ\j<ՇšGM úRó©‰T?ʇêOM¢P¾ÔdjåG ¦¦PC¨©ÔPʟFPÃ)gʅ²¦:R6”+Չ¢©O©U”’â¨ÖTʖò¤ì¨¶ÔʞjGyQí©O¨”¥ B¨Ô"Š¥)'ʊ +&"l²jeelÕ¶U@«Ï­ÇY7Ú´¶YeóL6]ÖH¯SòEòÌÆÈ¼l=ºuY›nmöێ±åmÿe·ÎîIÛùmËíÛØ‡Û_nǵ3¶û¥}LûÿùdÓ'ëÎ(EŠ?: í e{²™ì5ljŽ›h§ÃN¯œ»:Ÿpa\º¹,q9à‚;útlèø£k?×@×$×­®g\oºþ«ÓšN·>e?Mùô…rò׍+èìÒ¹“ðÚ^xxp㡁·¼ mÄn²±tŒ:-1Á˜VÎAºÜh()Vb¸4ž+TÈðb2¬I0ÛÑxR³J.YÑ˲“> n2Ðó,N¢‚ÙÚ|â3~ïpõíRvòNŠ›0JÂzÑÑ©ï÷U\­ÁÏJ6u¶lyô~ÞX¸¥Qìß¾gOÁ!×C¨\³sóÎÍÅëQƒQ˜Ü²Ðʲ0 ôד. T*ýÑÊèéS“,‚Š·‚ûÀ³0 ‰€âüu^ ‡­-ŠÖÓ¸^ˆûÁBÙQɌV·Ð0 'â@Œd±Á,çO¡a8¬†xµÌÿ¯U‹}o8B‰PíE·dñÖä ,Yqþáîï®->1lw¡zW:Í<ò¾ì¡Ä…–Igi‡² +ÃåwC¦ûO[4„Ãnx' qà.¿„jÒª£ëTÛÂÐ +ÆæÜ Jrp/XñVwx¡ 9²QÏbÇîØ·Ó xóOøÚõø·çҗ²¿?òìÚcøÈ®]‡?|ûó·Þr[֐åM``al•í Ë +ò‹‹RòcEkE›íYÁ¦±;ތ{ÃfÙA‹þR‹ïí¹U†ÃºH¼©·ÌíB›Ef*¡ ¿ºÒá2ÁÑK!Jг ê§í˜Œ|Qàúù‹FNDÌp9îí°=tºrigÃI宝E¥¨€)LÍOJÓf¦¦)ƒ§Ì]?1¸Û[hËݖƒÝ÷?õóofè•Yq†Ô"ĔŒD¹Â¹B“Ÿ¦Ž†xèÇÃ}ÏÛÀžZnн%€~~¶¹<˜V¼´èlÁ‘â…;Q­arcÄ׈v¯~[NÑìú|Û™·Îg–R1&œcßÞçÑg´—[OϦW/|õZbô¶<筍#øÉËӌ šÔh%þ _×Ëá¡°Xv¿ÅƒäÑuI±ÁX¡„6°-CŽ6/–Ê[vù…‡§¼õ/<&AÛ:: 7Ð* º1rtíÖ¸ƒŽÁë,Ϊ¡ùf+¨ «üŸ†§}rqkfœ<[ öè“Í|ÀôãÊ_yÔÝùßùÍÄJÿ‚@Ù=„Ž1ËU®'y‹uCpw‚ßw9-.1rÍÞò–CÏ6«$Q¡øC˜ƒi¢{¢i5¶Å×åÈ&œ¼ÜD4ÙÄ/˜¡cSȲ‚´2® ?ëEǘeo¸€¥\'ÁzZџh^»ý˜ë6´5yûFCZ¶Î€˜"£±È¨É-áÖoS×¢9h~dßa)jeV:`ø e´!õ½N™µ¼þ±–³þNKH +ÌÍa¥9¢*š«$¦­‡õNŠ©Ð älqª1Édˆá– åŠý+ŽœŒ½æ +ô£s_ß 8†m‹¹¡FuöƒÍEÉe¨”9z®ºé«Ï#é•ÙqyiňÝÂݔ+:U˜,›¼I©Ø_µ*dË W÷qóýÂË"öÆp{b÷¤ÝK=§Ù¶+aw\Ù´‘™å·|àPŸº3±ÀTzVBks  +¶Ä¿ eʖHԙ%É+ö g¬KMb²Û o¡wWŽ.ì¹rŒ?XW_ֈž£;Kõs²b,"¾+ìEèċFM+š„•OvŒ¦o Y2w~¸7šÀxËñÀߺÃðËWÊk.¾—Œ`ޡҊ¸Ê6ÀÖ@lë-oa¬{xíuiþßìàÞBµ1%=C—žª\¡„˜‰‹«ÏpP ëä-Aý•;𰝄çd‚,a´Ï,Œ¶PPí4sBj1‡Ågq(Þ¡g%™G2º³YE°eÞ±‚ƒÎ–ƒ!î=DÄ 0ÅÞn~é['ÅraŠc“ež":L°ÃrZ¢l ­¨Ÿáë:gÙáSœëmú,õü|öC¥bÂUti÷¹s’DZMìxW¨`3y–KT¹c!Bnæ$€¾#AgÊ•„B­à¹#Œìd¼$;HøôM’Ù3Y4=à™ìªä¬^Ãð4x6ÛÉÌyBÃ[=6säb"ÞOCŸ”H3ÿAöó9gáÖãs”à(·ð‰»üŽß™¨+ˆy÷äÑ?¹AÇo‰œ]7œ¢}ð݁/›CÁùãá®ìó¿Nwôc"êiú9½ÀE²^›Àƒ3ïp‡‡ üR°wRD Ã_I •ôíIgú†NMŒž©LÒ¦§¡&9_]˜­ÏÊÒ+ìØw1wO­žÇMÏ(]»D”ÃÐÀÁœB=æòœ—·Ïï<ÿ% éz›ISgoŸš3—ïý­¾A£®“$˜ö&"[Ï>¼û*üØ´»x‘ËS‹3i/ZqÚã0DÁ<çR‰;ÚЛS߃ŠÔPûq¶ ²›,A‚ÇHJ–™nŒe{¯–ªÑ +A$ Ã`€ÌðQւŒ4þ;âŽà(k8ÙÚ²³†axžŠ}djÉy.–ó&Ñ0Ü¡v—ùýu¥¦n‰«·&YRIýoHêW̱¤¦ ³Í$ùÏkyòê­8>ñÄù°{®`÷ú )¬Ú÷~í9ÅQo4sÍÜ Lø’}ó`T·ž#{rŠúžž¤ÊzðHšðˆ?6· ٌ§‹~jVõ²ˆ.̆…’Ô8k—¥„ôìx…Ô:*¶NÀÝeXñQ¶iÙ܏£P.+‘×JZnâ!ø1eg$3ä–¥ô‡“¤ªÇb.V vìÒ³*ü $;öꍝq‡ûÓõ«•µ§¹=•[JQ±X¢%§ed¦¦*gÏØàMfZiúZ55õ“[Kfé¹ì؏’@sžRG+ͶÒUŠ%­=±ÖÍ¿þ&ËoVyK”t—´-,ÂʍÆ}ñ<ÞÈzZæº ûxuÁ{þòTù4¡ßd¸Ê=@8g­Xv¼ëe)t΁^ŽÏ◲•B›×òrÑœrfâ}rh„—²áu™‡½bøŸt&pI¨8™ž{gfõXb[7Ü·í¶MjnŸ ì0ð•Ü’ hoÙw÷=»rŠý"°ÜªÞö<åADg{öÅm±É͟ž;_²ó$·¿tK‰è}’g5:MšrYø¸þShÔ̈́_Þ¦ü‡;ç_£ »¥ý3² ¼Õi^lj^+¶Â,ö$˜l…€$h$§ÀpÜ +œq'N³”…6/±ŒLäú`9¦±¼/È¡3p/îƒ¡qc,Ùu+L—pÊôKôéúškµ5©Ê|]6ÊKÃü¢!9‡ 3øæ-CÌÀ)³Æqƒ,Ùó~æ.±Ö-I¸ø¼ç¹<ä|èÿD֍cI ôó{/_ޏәÃiôWBœ ävïÁՙ“|ÆÏÌý[ÃB`õܜWSá)r}Ö¾qÄt¿>˜ÁÌËÞ ãwï¢Ï«ê¯'ÉõSüý²Éˆ¤R£éÇÚÇßdnÿ[Ô +|ú𠄊 +öí"½?:Œ¶i¶[zÿImå$]•C?š|¾oÀ’sÌx‡¤<1]<}-®Ý¿‘Û§:t+évÒV͎¸=±ekÐ*ÆÛ'¸OïÓ×2”º2!1"q˜ô¸S[z†½¤gXÀ_ˆºI2Õw·^ŸÞx4t^™?¶Ø¿`}þú²¨²„ݨš¹vñä·÷¿ ™–cê—?ò;Ò8´›}B9ÑÝ +àBaš!$¦š{Êu> æûàuèÌÒÏu×oè^0Ât\+ñª²>èN ùgåÏ❌âa$ñâ=^ìBGŽ“Ç“‹Jò •J ›`ÚÒ݅Qͪ¿¹S„¼Ãe1£‹Îñ_¶áø’r!òÿ]ì˜òD—Áb’ZœdÇ%Ñìh™½ø {c7™`+©£/ÍuÁɓWtK¹úÆLÆZ’äk$¬ãnIò÷ð÷ôŸ‰úÜú^ ßߓÿå¬2 Óß Ø<} +ô{º¯h©Öß°x D³ÖÍÿ|¦ÂWБŕŽ_Ð*TȪ$ÒyXνOãàf•,ƒÆ‚JöµdR/ˤ*é…`á›e‡aŒø'¶ê6$*¼-L£¸TƒŸ…ÑŠ›ï%+ûèrOq=ŒTŠÚ/Vï¬u­©ZµœÃ7ïYr˜8Zw¾j)†k«V‡’žä£ÁZú‚“Ó—„ož³L©:±fûbÄ(— µ1aó‹ÆöŽ—i ÝÀ¦¹›ì +ý%Ñ]z_(Ñý'zFó:l%t’ùÓØ¦¹ÓLaì¥ÄŽî½LW¢ºÊèJá0ïð+\®0Y8ÂâÁÍ:N‹2\’êB¥b]!ÊÍÍ-…¡B©óÕæÖc-—9µpL,çây¨< ¤Ï“ñá"«†“Í^ ¶p½} ê(ºÄ|?ªÉÃc”×€€ªÅ·W*ïÆ…-Ÿ=¹£ÇcÏwï?x­T¼FזŸẂ2öþÅ  “§Í;vڙk7/~qS¼Ãu6O.Ïëå7mäH¿‹M÷®œÿF èATƒór¢ŒÉåÈ¥ svÖ@žóÇ¿zj’t}%DòpŠÈډ$9âî,! þd³¢Q"JA“ãÖ.šçå¹`,ŠF±9)¹©¹éYéÄ%SSRÕɜâdtmܱ«Ww6|®¼w¥ö †ñÃÁÛö;Sݛ†ý TdÝFgóÍYvÃ¢äØøØ5+—mZAZõ׌¹ú\dpAz]NfŽ.ÇY›—®OCH«K׊ñ ÉaqlÿŽ€£‚ÔGÄ/Å%¢_šEç&$šz«@܍mñ˜yÄìv²XØIÄlñé9éÙÈEƒ4Ú Mf†sf†.ƒ¦O7jð\gL>ÔéÚt”æ‚2²´ÙYÎzM®Ö@”ÉÉÊӛÄ<~ÉZð#¢¦ 4MläÜj9nº!·šÏ®cëí¡^ˆ)4 õºìL=çöá¿¢ßÑïU¿Þ{b˜l¤KêdÑ~sÉ<É^Iju²6+3[ËÝÄöU]PgÔ9¼‹¶ÏÔê2‘–1[`0ì.SÞëƒÀ wèç°_}Áfcº ™ÄªüXìrD“­ÍÒ"—²aŠAmÔqz¸,‰mÒÔDÙt—$²U~nNa¾ò!Ø<Â6ùÉúŒ|ä" +œ›‘§5p#À:)ÍÍÍC….dûŒ4]fº»,–gº2MüPG´çÿèHy¿˜ó…‹ìkIÞUþJËo‘_¡äC¶ƒ|%LìK:㖖/À¾òØu’9óùbÉÕ´¸Ë²ÿD±Ä“ª‡³”€òpR‘·ü~m’ª+ +%ðéñ|þS°‹Ç}rÊJ¼Ø?mæþa#n(lú£-«ÏÏÒG¤S’µÚ„4%þæÏ©ÚT"²Ö%…Xµ W_bPšhX8A„•™ +`!~aaPØ[Å¤t“ã_a +¬Á¦ (¨Äß½bѕòû¶Õ֔AGPM̶Uû–OBS˜r4%fRÄBUXXô´…•«j#bn +&˜^ ZÕnÝQ“[™—‹J˜"u~|Ð⅁Jì-G+6¬_™«KK#I Á )ÓÈܱ­`ÈXàх綐֪$-7:[¦ß´ Õ0@ÖðIÛ×l«Þ^´ ñæ[ȘXì?=.G² ±DAv~6!p>"k.ƜÜr_zGó$/ì焉ü +’?Ÿ@;žÍ‘7ž¸ráéwÐÚVîE˜†|ã03rþ +eô†ÄäS +óòJôÊҋ_Öß@Ì××|B¢#ûôãÆz7›™&WÜ”_[naVŠ9ªˆ°UÑ,ZµiCXúæLM:J`’Œ©[Îm¸ ¬Û±ë@vY–Á€ŠQIZ^lÖÊì »P¨ü…Å AñêÔ¤CzE¦²&}ëRӋ†\Z´±Zµfc’ +-e>øÙAðå£M¯@ªà76ö=ÈþñpòèÑc§ôŸTq`—–«ÍÖ &9U’Xœ´?†;ِx +1 ÿñћ .Ž®æO®í©;y¡ãÓ±×Ýf͋‹X®Ü›+Þ9S ós²‹ +”{kNV6"æþùy~aÑ«#6qQq*Í23S¬âÏÈÇ÷„´ñªõ«6Æn@q(ÉR|,Þ°­FQñaË7oHMD©Ä¬êÂFƒòØ…ù»ÐVTœZzd՞„=¨ +í-¯áË+ E(Ÿ)xÏjH|ãb%Ø<ÛÁhw+©ô%o¦Ã ƒ©¦6Óy˜ FãèfgÙ9ìàYOÃrøM¶ƒ6—Uyð$Àrça+q× Â7 ‘PH-ÞF'æ« óôÙyEJ~k}ÑaÄ<>ï?°‹ïx÷éÁۏÍçÒ fËjL*‰¯ÞĝZ}2î,±¬Ýã砙÷î‹W¦ªfsæOñ0©R¤¬N‡MW!ÕU k¥—ö;ÙôàøW¨•f¦Óstyˆ)È7V¬*]äçµpç=eÕP„ÿۃíO_=ê­×ã~‚—XªÌ fw+,ßRZ}àðÖ:ÄÜúb²çà™~^£çëHQû¸RaH 8ïÞBómŽÙnÏӋÿòûíìªrô99%9¹µÛìÚRÔÿ˜õò¥ +endstream +endobj + +678 0 obj +<> +stream +xœ¥Y XS×¶>rrªÖZã±à8T­3jmmë­³­TQqԊŒa +3„$@&7c’æYà,¢¨uj:hkG;ÜkëPÛ½övŸÞÍ}ßÛIо÷¾÷ù)ê>gŸµ×ú׿þµ6‡ðp#8γ+ÄÉ ¢Ð„ÙËÅÑ!¶ÿ˜ÎŽå°ãÜØñî)(èO÷?ßáæ ã€aî`˜ÇÑqSVdݟ‡ÏÁ×Gîβ;Vˆã$ ¢ðˆ¤ /mñÛ6}æÌY}ÿ3ß{ÞËöHœ+V†&ŠÂc'LÅI ÇńÆ&ùŠbö$'NX'ŽO°Ùàòo‚ –/‹Ý¾\ì¿".`媄Չo%½¼&Å'5蝴=k%ÁëÒC|CׇmðmŠÜµ%zk̶צ,~ó¥ésޝë=oþ‚—N ˆIÄzâ5b2±xx‘ØH¼AL!¦›ˆiÄfâ%b 1ØJÌ ¶3‰íÄrbáO¬ fÄJbH¬"æ« oâ-bñ6±†X@ø/ï ‰µÄ+Ä:âU—XDŒ!HbeŽD“‘ +RATuú¡}Rhx @•«Ì~’ýØþ2«Ã~¶J}Hþ'§;[ÊNŒ½Ü˜ì/D«y!ɒ¨Èêôáöç[’ËC„Ї.¥ ÞY»Ç´ lQ²€”Ý0öp%‡%` çAßö¼ªrÐìe;ŽÜ pg¹TäÇgköö8(ِÑ(Ÿƒ8ž³ §É¬7‚—Āò&³òl;ÎS%9 ç£užèuÀ KN‹¶¹X)Ô÷œËÝ—ý©å¢|á#8ì!‘›*1^O»;=­×—aO7§”‡æÙMgà$NdFÞg`ã‡=ûû}†–‘‰öhõ|\¦“š”:µ9§è$ ‡|}õÁщíB>ÛÑZn=1æçE×W€Šû¿g^]’[¨R’Ai8zñ®L Þ½Bˆ¸(›†“àXÞQТ®“Qüßë¥eñ‰cÂE’·Ø:)+ÎQ~¸ÃóìV½„¸S‘'zr§@Û/î#ø¤÷1š$Ü7þÇ'–-]½qþüÕ?ÿⓠw„ö#²Á•X€·ø'Ì¥á"¨¬Åyº¯r­©T:`׃…¹d‚ë ’LÒÅl4Gi\}‰±´'J½ÏV*ªFrD_CJ.šâːF&¥^m²‡+Ç£´@[؇üÞÈûØh9ÃÎeD•#­8"ŸXa>fÃÈ .ùô£ò–ã‚¶ÚòzPC5$ÕÄedfge âÄdidÁª]€BÃ޼Ưòþuó‹G­íJYƒ IjÈ(J,1˛€”ê &­>ßê©ïy nŸN£§øÞ¥J©^26dj2$C˪øâMcÑ\ô z-´91Ç@µ-o2° › —HŸ >.½„Ï4áÑø=ùš°.H ä—u êI¹¨²ó‚¥ÛWŠÝ­rq‘»È ².';ªœL£Ðb/j#‰¿8 ¤«²>µ)¥" DR«Öù/\àÛu£¥â@S• ¾²E 8°ÍòÎJqèw24FÐhî/ݳЛ<¯,Rde™ÃF­!_¨ëð-:×ÍçNcàR¨ß§Õ”æt=6ÈԊ µ"7 $P+x½`·2î7ÚÆöD:c¼›„ÞِóGô¦:yz#ÇðÅ\ +E®È ¢ª%‡L7ŸfùЍw¿ðú²ü‰ÔN^ÞŸµS {²tòo8K9ö,u‡[á^ú5 ó”ÅJ½óZUÇ) —ªöçè5婦T 2•Ýr%ˆ£Ô¯à$ÌðçÄ|E‘ÒŶ—u ‘‚3º·ðâû‘¹VfîÍãn¾Ì´¾U';°Qp jsK²uŠRiŸÓ²rU@LmCz‡Û ‚YÀŒüîÄà†Í˜ÈÑX2¤åhl0‘BRl|BAÒ÷ðQ×Ó3 á4’ïýå½#uÍcÍÀ]šÁ”mTCMMc¹Ú˜R&Œ®2F ’.òèj}?W+\\­ëçê:;Hðy%¶óª”j¥$vWë·ó@R~– Úlq*Á®Æ/ÏìŽâ <š©Ÿ«j/G¹T²ž¢9ì%ͻРFA7w¶€EƒÊ}ŹM™ƒA +Ôejpy°.ÀÆ ýÞX|)N ÏjjöZÔU*“dP‰[ÓÖn2ÕÇ +”û¥æ8ü–\­«Šäí´¤¶™´z£àpÚiÅl }ÿÒ·'⏾['Œ) )YbxG¯É“I‹z §Ê;ËÎkQ¦6 +Ê£ ªÚ¾”³Qí°·cŽ•×w¶¾M7êÌe ÎN;Ù¢ƒØÂ¬Ô;@¬&C$¡ê@°ø—Šš¢ëå‡ÀEê*tUvY­ÍÍ–óà3ЕlˆÐEP¬Ö)ÍSO$u†<-ÆýWi-¨(MýjáÔþѱs¡A¥·1})š„"á¤R²èóõÅxÏ^¹÷ä—c+$Öª›oSrÊ\•kõÀšz,t*Q½0KgÇ¿ N´e@ór%Š0 +©V렘¹äâTrû–x¹? Ö‹kN a³£n zÚQ·ðÓ`CEÐᘖģà°šOuRü´ÀŸy®z–²©FAop(6•~Hր’~ª·W&¢sƒ™Ëh»‘»ùSû­RX biwÎÉr[T9¼éP¥1ÏÁá4âÀ(nрÏ:Ðh¯«È“ˆ„ÞӚIøú‰ûÿ€?q-ƒlÏèÝdìÓ71ôhÌ«ï10 #0;â‚s³>ŒõæGÓ Gôò"Š’`gŽ2%%)9\D Rï_M=°1±Þ5£zÊu<…^áÝÝõaêp œ«<ÖÞy¤ì øêsÒ{˜qn ·¹wÉïák¾ Û=r\é8Ü2յӐ9âb?›– F³}á|îr=šgcç銽ÌúA.šÛŒò¶ÜÐÏûÞ~µ®¼{™yô_Pƒq+c=GÃg\ú­ßHá.òÞ²sè¹í+2Ò©˜˜A +[•ܨ7ç– NۊZqÛÕ$ “x{´ÑQ`>X¿Ë?1N¢)~ÞÒoÖ3—Þ7×u +øÖ<èAomNß ¨Ý‘eÕ/·}Ü֜£êö"÷¾½†ÐïŸþ íêŒN4Ù º n£¢È·î;߸ÎöÏ)\3ڍDcQÀ..âô‹8 ¿}ã˜pزÙSz0å8²ØÖNp`nðJ¡ž{«{6⠐Üfm‰ÙÞÝ4Pz8‡ÌHË]Ä®¾ÝÛßÙ¤{ ¤åF¸&–£§íú¡Ñ‘`®ø2m߄˂F»[wXփ%`SêŽ]»ÞMÜ֟Z¿Ž]ïžOù¼šÎ¶c)Þ ªçTVRÀBºË™b‘ DÊ#(~‡X,Ê DFqcB¼à_¶lkl´Oº$Ûg ,Á1[Æ +hhìpcSÉÃUå-ý4ú‰:Ã~˜6ÔA¢vñþ’yÉà”ÿmˆ¤ÌíÛ?áN¹.¥ñ ¹=DÓÑTä1y¢ñ3 Îýôaye‡ ÖPR +*©¦Ô*±237'Sà¿w'X¶6&½Oܦ-ï]lÿ +ù‹–Í”sN©bà¿ñ™Ï²÷i4=Æ£Åèoÿ„sà,ȇãá¢Ñ_®}_¸µË§ám\‹<~ûf.Â@|mΌé¯ßƒ\èqëÞ¯B'˜þŽáˆ÷;cñ3Ÿ@ N/Î<@Z,Z:öªÛÆ\þbÜZVV7ÕÊ,qZa”~­6?5t™ß\aØÎ¾i¾ìc¹ÎA­-VÍ®­íSë: Ï0ð{¯*ù—­Fï %²ã§ZÚÊ;Á +ºÏû +ñHþ)¢ýæ}‘ƒÛ̗H–ŸðÛ±Zˆ†ž½ÃÀrÇØ +îǙó ;‡B‡X˶`“ÇL™†¼ç£ip̙£¥­Âͼ¼í!¢Mx‰3˜ù{9ك„ðÝ­})ø§Åž;úbWmEñÇiØ)cq†ò£‘p!ýÕ–®X·áÍ%ë.Ýüìò¥/„ÎáqOû·9òù©rL Ù3·êg»Îm½Ñà&¶Í’ß!/œ¯*9 šAEv™¼L^˜ DZâèd\÷3õ`‡Æ³QdßLz-ɬüpâ¶]©{‚=ÎÀ…öX'Øc@? Êî"·®Ô×N¤8õ“‹‡EdÐGa'v¹y¸õ$¸Bý>ï34Z€ŸÞgÆÂ! 1(îá&·c«ÉùÐ!ø +)Ðð¿˜õü2(!úÄÃp’ÿPÎVÒhÈ$8¤w„Êùö1Ì{ìΚÙF“½N£WÙÉ>(UŠ„ˆêwK¶ãwŸŸ»tŠ_óÖÓ±ÂI‹âZÖǙ–½UYՙf¬ù¨Ý~‘Kß^×Ô®¨Pk•úŒ"9 DҔ]Â5¼]–”6‹¶°²LÐ:ã6rüÝëÏ¦nFׅš_¶,5gç§’Kd5¸?vÊzõòɸ`½@€V¥S™5fPj +™{æo=ŠÅ×phÍg¬'æ"ô'o{˜ÈO€Üi=“sÉÞtaw¢sƒ§KÎùa×i±\8ÖpÜ苝sã­î»ÜèK¹îÅÿ”ë½ü‚çá‹4zÞåVu@—¯ÐÊ9&^ý?”jJ-×èsŠ÷U¸«;Õó7±©Õ 8O_àU^lª´]ÎôS nJÔºÜ@Ւ.7„°ˆî6å'bª™Óÿ–0ºJb¿%ä…:ïøn»Üññø_¶î¯(i[§Ï ÑÕû¼žÅPÛbk»E#V¯W嬕…æ ¡˜÷ŦsK÷DÉDтk¤%+ÊTUDlï˜áŽÅY¸Ÿ¾‡ösqý>›Ú\d) ý¯”Ð O§æ³dª[S¸“É%(%ÖqO’µÀÐÏÛ½ÓoçFR…Jfë3,Òý{/c7ãî›$û'œÈÀPl“;ƬXqú¤@ytœ¿Än@mÙ}âⵃ‡¯\k]Xj« +²kpúµ›¶?8?^ûüšÿv‡iìè7×­]ò敵_~qåò_û^^,ôñ8j Ú#ŠÜ,j=rØÚzD-(ž®ÖêtÀB•e”J5¹`o¦ý’¤V«€Ô+Ý$³ió +t¨:ý[ïÁ[±ZØL‚…°…†fèUcÔm½}q®ƒ3ßSc––ȁ +(r25ÝËQ„'»ŔhJrtÀË ,%Åæ¼|Ïm¾ +¥1Éh 9|iÜix%e*3Ä ¨ó5ê¼\Ïb¹%Ë ´ ¤ÀXÌ.‡žÝËaLVqV +xÉAzV¶|_®gŽ:W 4@¢Ï¬É²ïVÝÝÙ5£“A¦2)b9rۍF ¹µ#Þ ÄmÚaÊÂi¬®j4å),ÂUÐ#úx6?¸= –üb`¤“«bmj¦ãíàx;qrŠ8#?Û ^GÍ3Á05zæ*䡐æj@&%®Niª6êÛjŸA·cpø@·pÈ[ ¹±% öÆC†óÿ²Œ>ÐÔtà@lS˜PB†Çƅím·_>ÿ%‡ý%î~ð&ûÀ†<6Ÿýƒ6Tbw¨ú¤êø„L…H&@7ÿóºR’£ +¯„Ú¤úºC«ÅfªŒaýÎwö‹/Ö  îˆoùŸµVø?˜& +÷ß&~xS³xÀ»ñõ®m­á'Ó.€ àdEk{יÆ[à…þñˆ~|âã7N¼5Y€ÛÃÉ;ßZ¹rçǏÊÊI‘#i±9X‘‘ b½b+“ê yE‚Ôp»gOý‹JöÔ,Ãúà¦gÞ;˜¡ z ­øº&ðw/èæý¾Á/5jCÑp¹™]c‚^æúR’òhèBc‘¡@§/Òv Ð¤/((,ª-ölÞ°áñߤ>ÇÜ +endstream +endobj + +679 0 obj +<> +stream +xœmXg@×¶ž#Ì0¶C9 çQ`½6,"ˆ‘"(Ѝ4¥IGAŠ ØQ E¤ˆ€± JĨh@b#örã噆ïz³æd“·ˆš÷Þ/v™3«}ë[ß ¡Ô‡P‰D×Ö'0Ê'"`“÷”%!›6l‹ôoŒ‰0fˆ0Vm9ùý®2–F#ÔÐõª1\¹ü© )š`¯E©I$–kü¬BBcÃüü# M\ÝL'OþòÓÉô©ÓfnŒýëÆÐÚ'<À/ØÐ˜,¢|CBƒ|‚#VmŒ 7\ån¸ÂPôÄÐ.Â;0`Óß.(Ššoì¾$ÄÃ*ÔzÛÒ0›ðÛH»¨åÑÞö1WÄnújûæ•>¾Ž~þΫ\¶º®r›3ÏÄÔÌË|Úô†5Žr æRF”#5O9Q(gʘZEM¤\(WʔZMM¢Ü¨É”;µ„ú’ò ¬¨)Ôʚ2£ÖRK)sʆšJ-£¦Q¶”5ƒZNͤfQ+¨P_Q³©•ÔJFPC©a”&µ˜Ò¢t(ŽEI( ’iJZ@¤nI$5’GC,‡ò£šƒZšÚ·ê”z¦úKz B§ÐU´ÀLe~Ә¢á£Ñ®ñkư=C‡ Z2LoؾaÃM‡=BkDӈF&<5ò•tšt¹4Hú£æxÍfÍ7Z_hykEk!­Z×´µ/k·@¥T鋊€mÁ®H"TÀD¾O¤a +3IDcKF5ùC¸}äôhó؉3¨¥¥ "a¨éá0i4W6B5Tu42·¡æî<Ëoiî2¸T¶mM|j¾…-þV¼ì?² +3 +Pì±SËtK×P{W97µà)-Uš£"åRâÊ>åá*(ilÅ`{œh‰4¶a`þw¨Ôh°bÀ 2~À4,cÄ0@›hÃ\ÐÖnƒÉx0Íõ=Յ'̯èNÉÕÖ˗ªî ë¨-ºÞ½åz{š:ºõљ¸3aå~µù–ˆÅß3¯ñy¾æÒ0‡¹Y³!,)46F¹Û ±\ގ;ùp¢Á†¹Yé–¥ˆIpF,ÉH¨C¨Kž‚‚‘š0M¸È¡é^ŽkíMÐ,KŸ/P¯Î=¹!¿ÔQù złÖì‡x¬<«ó7ë×[­‰_î­\µ.ØtØÜøpÞ¿Q|JÍ68ÁÃÁ‹Æó¼ wͅ.q T^Nø “ÄtDÀ(a?Œ’ü¦jÊSBŸYž~å± =î96ǚ†«çʗÎðÂó,žùoSXËA¿]c#&ØÜÚm2b'y´¾Vöª±[ÑtÿFÍԊbûŽÎŠA›?‹XA2‰ ¸-‚ÀÁ³>¤‰¥¾5Ç΅v#Ì=ROçœícC[—`‡urî¨Eð­§½–bKLÍtóoìû y©(*—¶D©AÂèхz%z~æ^ßݟ@Á0¬<Àvr\Ï«úø>!œæ_^S-¶pTlX¼&d&ú»§wEG§ +Lj£Ûç^öª°1˜ƒæ{¹.ö°2ˤÕ=ÿ·Î«×.Êp7f)ÓÈÓ¸f<ßZãeƒ9âî WÑ]í‡0äe>aP—¡“‡wLhÓø#‚J2åd§*e°9i+áÜÀVÁHi&}Pï¸ãë£Ð@s‹Ÿ4Ÿ}fp夤œ‚qz»ÒSд­ðö\Âr†'þ3ЏÐô—ñ ¢ñ»ýïÇ÷úÃÖrŒ‹(® &E‡0€)€‰!ŒÍõ +O„!|VAúA‚—7‹Z±Ž…m”ÏùŸg䁜Ïú¼~¦ÇõuDNŽÐߺ!b¥­ß‰Æ9ˤħíFÉh}…ç…°3MI7 z +£®o>3§T1ù¤ûÑø\T$«ª9~EÎÝh«ŽÙ²'m_j²B +/#Šû^ILT¦ eü¦&ÐKxmÃD›Ñw˜¥øEJ|j<Ú)[Õt¯º$óàIyÑÏñ©»Ò‘,<>çXVÆá¬|ÅMh¢¿gÈíÉI6i›ûðœ_ŠŸ‹‘ƒ¹Èn“'\Fã³Ì5 &}ÌK7<§3½HH)Òy-²[¨P¯ BèÏ2üÍÕÍÛîæ¶ÜÀ=¬´ùhzîþE7</ç«Fì‹IE±²ù×Ü^ßo-¿Õ*çã–ÑR¨l`ueÿ™=R$’ÀëB«h„Áî*E —÷ úK5Jè+’|_ª â 4Tµ‰.þA«‘lshqUnz^fžâXHœÕÞÈÔH%›ô# í¼Spò¼ürU͵c=$‚Z=©²pÀžðAòE{<[%%øÄã5ð •t¯ ëDN``ž •¥ÒpBÌ a}fBq Áh)ŒÍU%ù|\þžÃè(ÊN?q°®èq¥YGóôQÅöšð­ek󬞆æ9‡`]F3\E[þ©¢ÛÜÙ¯‹wEÅGF')¢²JrJÖ[»Ë7>0rµÏ–•ÈùøT„—FNº€ÚÑåâëm,瞇öîÖGI{ö'²d>q‡±&ñ±s×®[†ØµaùÕMuÕ·)šËÌÍ8ž•.;”‘™žw€í…X(‡ü›Ç`‰;Å‚3øwÅ)ƒÃ÷ºàýÿI0‡r’ Æ ÖijÝa¶¸5\ˆãq9 d­ ß` qùH$i0“Ðä‘}Üa‹XÂþw2ym4×èzâlx—˜Ã8°gжyúÅ2·0×uŠ6È£Ûp#ßV½~ –.ÆÔ¬Õýù Ÿ#IÛŽ|Büâ?C•°o õ߉~ãPjÀùl‘ZÜTcý„±46cªêÒãhg/?›„½(#Q%‡S¥!Y#ªË®¿¸¸ö6ü®ÔÑûÄ’q´ýoøÌ’ô”‹Îmo¬ô­÷Ì·ESe ›½ãRo<}‰< +ÖTú×n¼}.úùá9Ðg±>h½%ԕ̶àêîÖÃ×J.)N~SWֆªQiÂᨃq éd¶(ïvË](þÄzª?i7غ! > jÚi4X"r¥'üy€<%RÐÐ ´D¸KâUÍeÂ1MCƒ_áNq)¸0%@Ó¸œÐIÿï=K#˜ê’Ú Ø3d\p^D«wé2$›‡æ®s[¼Æ:xšËr)˜ëœ÷¾«¥¤õ¢œó÷túg™‚«À:KH©\·4Þ%ç0ò y&ÿˆ=e»X¬q –¨ò°‹÷ i$ 5¡\TE4˜üEÑc{aÉû-1ïÝÅç5fŸCõèâö¦Z¿F§’%hšè¹ÆÞvÉ»1‹5_ÌÙ×êmr,g¢}¼ÂÖ"ÖÙ§öjnznF©¢ —Ï©:XxøXIՙÂԄªCöoÈHBù’Ôþ¥þ¦‚½B2–ÇZÈxí\“æžXa [ÌKÀø·ï^Ê¿ÓÒþ‘lgøÛËcð(þ×ëvxˆEèŒ +KW,D¬ñŠ.|òõ…Šæ®Ûµ=d6ÃKÒY3{ü·DX@h“w%»âȑ2$ËEÒJSŠßëyf&¡hdŠV{G›Å/×[$ƒÆLe+¶O¯`K?eúõì&dB ð–x §á0Ì#t­¤µ½µýô ԃZ"š6VúÜxȖ¾ûû©Àó!„3¦µv«gP¢Qvaþ;V÷«üÃû¢ ŒÍDˆŒŠ¼bçÐÜ?;B7V8,B‹|Ü윬|±Mbñ{óßÝl(üæ–|ǒx;G¯%HÆõzø?¥H'Ϥ&©µ m.ómˆ®‹½°ë&‚y$wËaˍ‚áá…ÍøÔxþjíZK,]d4Ýmë¹nÐ|ôSÅGN+¸„}Æ)Dç?¹ÒrɚJC×ìLݹ/òÿªïGNWfZ¹†¬p•_ƒz`\“ :‹ú˜ã¡`â êè^"(Îñ°˜i?뛐šä«àúb–íöÙ¦vf$eDz0ƒÉ.ߙ»7‘¡é9¹DcŸÍý5¥ú¨~[¹[þ•²²ü’R–û癆Ҏ«äpǙÐò­U›Ž8!V5òc!^ ӉÄv¹ó":±§0îÈ΃ÛQ +I Œ‰Þ˜à!B¦²ŸŒo‰RçÁ àLbÊûP¬-n ,Á¬óL–™“ ¦˜ãÑÜ=¥·ÅgŸÌ8‚ŠX Ìná“…{8Ë×ø/FÖhN³ ÿ9Zë}[í©¿ÌvƒÑT·êöýé9é9rl̤D§%¡x´ºh]í–K^]Q?!Ò½5×:Ï5•ßCߢ—– EXR¬ÇݛRz¬^ÿÖՆ7/š‚]÷¦%§%ËJøD„ë!¡ÿ…ödÀ÷0óª¾Ïg, S뷁ðþªs=,íºùï¿X0¯ 6ÒxƒÃ±/=ø@d 'ºA9‘üvœ*9fšÇ&s$[¸¾²õhƑÌcŠ÷B²È@ƪàÝûQ„ ³?ZÂä×OË.ݐ·7V]-xFÑû¨`à¿;ñéÛd¼®`< ­p…êâzᢸü/ò”ô÷‘¿•Y`"~*X}¦sï@×ÝGÏ_38S័–”š¨°Ä¿ ^½Ûx¶ÉꋢýRãRâV¸wÌÙ§2òP¡ì±ÛÅiË\Ü]äñÍk«¾Bvh]¸£ËMÍB¬[o°.(EI†"U™B#§38Bu>S8/ž~-FdÀ@’Ð’¥jÐ(é~àKà­Ï©âû»{²Òƒ6c6͞»KQ´™(KD´ƒŒG!Vœ5â?)®0¥u§Ša؃g-uUõçšOÜ@¤ÇÞºxªñl±MúÌ៯t#öJU‡gôºEà–­‘Xà ++æ¬ùÊë±BÔÀ– +¹¤üÂ<0š(B÷ÔA7ߊ2QÉîÚÄã!hÚ¶+4>2ÌgKŒ7rF[O¦]f‹5Òri<†Ù™ºŐiðÀt~T}ókyCséwèj<ãqŠåî<¾Üpû‘þKÚ07Ëf›û:¹ï¦Ø q Xà™œ3م¹ù…ÕÕ ˆ½Xâ=ßÑ}ª"QL˜4â„P—¶y›N0@£á-92sŽfåf匑>BJQÿNÊ +endstream +endobj + +680 0 obj +<> +stream +xœ}X{\TÕÚÞø—;%5ÆÍ%<{ãÍJS3­ï +" ä• o\†ëÀ²†a€á:x!/©¨5d&jQêQQ©,++O¿²RËÞÝ·8çwÖÞÌØ×wþ›ßÚk¿³Öó>ïó>ïV1ý\•J5dvlRBLTÂø%ë¶ÄÄ'EÉkþ’·Jæ"ýMI–þò‡7kpUaW5ví·o˜çÿºÁÏO€v0̨Uª€e¯ÍŽKMˆÙ°1ÑÇ/;a’ϺÔÞ'>s¢¶ÇlØæ3šþHŽÚ·5j[bpÌÖuIÛ}Çn‹õé9†ÓÃ0/l›;;nN¼íó“&ïˆLY”º~qÚëQÑ!6†Å,Ý´ló–­Ë‡9mÌK~þ«ŸŠÿô„‰ÏMòa˜Ìf$ŒbB_f)3†YÆø1áÌ+ÌXf9³‚™Å¬df3ã™UÌæif53—™ÇÌg&0 ˜…ÌsÌ"fÈLf‚˜ÅL03•qaÔL?ƛÙÈÌcÌæEæqæef3˜qc4ÌP†gTÌ4 +,ݔ¤ê§2¨>uIty ^¯¾Öo9;Œ½Œ–£ký½ûoíßÉmà¤Ç6>öހñÞ8q`öÀ‡®‘®7ùñ÷-Ô9øÉÁß™0¤ü‰AO>!¹­tÛívMcМÕÜ:lhüЇ>ä=øyü>ŸÿØ}šû%_?éî é.¶Á´ÙTÒFáÉvڑ¥Óf—å׊ð,j°TÔXôÆ41M’¬,yÍé©+"Gº“Ø-òRz–²4Y«z^x} cX°Øx’…àehckýoÙâlnRQ‹‡¦Ê¥ÉüLGÌ#h¹ÅFô.ԉàá8«è4˜Xͱ_à}×évÔ¦›wàdœƒ vê—ìãt>•ãÕúj™Ál0á +¯Ë+ޙ¾øõ”Àقý8ZDÑójŽmöÐÜ/ît£ÐýŸnÝißôւFQsçú±c'Î=y%ôāTÚw™è)O£BÅ¿Q,LEGqcnCj¶2ïàæÍ\ö” ¹G"I#ÿhYðGãCyûSÓêbq<7gvÐ4ùZ›¤²©@Ý,¹5«%oi6O|‰7q'âÈÏ@£ÁxŸy¨EÝ:þö¹Iõ2pbÐGß ê‡0@´Gâ`€mSƒ¸Ðݑ¾–Œ|të²½ ñ¼(>|uxÄÚ¹™ÁiS= Ów¦c-G\o?:»¶Ÿöí-+ǕœYoÊÉ-ؙ›+Ì \?s£>{PU\[R#Ö\m„ ¾ jìõ.nÓ¾‘P‘³kÎßåYP‘gª.7[K„½Ð¯¸ ã/gyž)'#W—Z ߓÛà|®Pà„|<È xʑ¶Óˆ¸t׳áN‰ §‰,¯¥‰LÇ¢v0Zxrëuì> WxRÔxޛ{‘03èIç šz†ÿ®cÙtŸ‘c š²ä썇¿ÞVT²-%QªŸ°©µñÎ\‰C0´†€úŒ²œõ4Z‹­¸nÖD%8g†w'ñ÷¥$F¢Ó¸)ý{7W­ÇkqöΌÚYž DE\üŸ.ázÙ¾Åæ¦i‡ïé5 ‡R3s2f}¢P}Jé]€N¹xª-y¥Z‘ÌGïB<«ñ¹oïiõލ볭‰¥yÅE&ÌUWYªÌ:Kzµ¸£>͏—ãWâH¿)\/o6üF©´yðE9¯e±¹eyåÕæòz£°<ŠM¸„&FNK¦NN‹îU–ø"…½—P²ïþòòúá ˆl÷+ÎÙñ¥%L¥ä+óJÓD" …€ ²ÅPþ-m…‚6M¸ìã‹ò‹ +p!ç8JI%«9}¤-íïÞ0ôçËßu¬hig˜1N.O*O¯Çõ\ë¥ã_;”—Ñ Xòꓱ~çςò¼2ùhF¡ºS¡˜ÑK3̜W¦ËÌÉN.4‡ßÜYæMT“B’,‰Ö qoÚ¾¼k¹§õy{2¬YUÉ8 }yÍØé¡o¶”L†2Á@yŠ‹99H†®‡§T‚là׏)…ÄP¦þ&ƒ(¾5ô]× Ñf26(~cî+؋ œýÑ}kq½±A¬í²tØÎ·7{µ{¯î nÆ-Ù{µÕº½qô2Ñö˘ÍV£`½]lƹ^\õR˜ÁjºŽE­¬[èM†câEúm!£µžùI;“p2G¿ÂùËõG. =¸ƒd7zÞèy[a?¥-֞Èö¸¹)BÈÎthì'(ŒìޙS”‹s¼BOE!h´ã®š»VW˜qW®/Óååç +šI‘/§.õzãm£¡ÌP&~ ,-ž>¡µŸCf½øR…‘Ý?EšRyƒ¨‰“÷1è†äʒþ(Ùq‚e¤š*{ð¼…Ûæ{¯Ýz ÅŒÅ&ñœa‘º­0»(g{ám–¸])uÚ½yMø"¾¸ïÒÎQà³%+>Õ ](E"k5͔Laâc_R’×ÕÛ¶èVQéR2’*ø‡;Ñ£Úz#ќ;§‘XZÂ)èE¸Å^tú·qŽ=6t«Û•í‹(»ÃÑêQíØ}‘¿‘®0³Ëñ!æèbÛà:oµ"# ü>_°4 Û üxÚÜî4ï”AÒóî? ÁŽ~º‚~8¢9£¹3c³WÇ2Û QO§¤G Yùzš~NgÊ+/1 Fá›Æ}¥G1wѩDžEùbúœ°ÚØÒ5”Ýâð€±¢Æxòõ¯®\hüðZt­ü a‰³0¼ñÖÎoO×Úª×5‰½¨’B,«`krº‹ãB'y¸w¶í¯Á)CàN&2Õ9ž—#Ü|!àƒ‰?è´ÃÓ±#õât_ÁIj£(­€FémA”陦¹u·•u²? ë-VÓ:÷٘kÞ0<ÀFܟ}uT@Ðæ9‹D;ÇßüxÉTҟ0>¤ÿ´à¿„þäè:p›’òGdµ…J|¦ò‡ŠøQ¡¤m¤å¨" ¥†RZ¸fE©å!“X҈ì}tK®´µÌ„&wÍ._âË7§[g8ˆ$µTÏV)+•Jqq܍ÞVKnއ›ì)§ ýúP‹z Ьéƒ(bÒ ɕ9ºÊºsd<ñ â?òÚP_¾ÒÐÔ!Ý]W‡k¨¢štúÂ"]¶°xõ‚í˜{:äô7'¬ïn[Ξ;p ·à·rö¤Vå쉥r¸¡Ï?ÈrH•Ù(Ëá³=²œRh·kRQƒ +þ /ñY³ÜL;’ʵýMŸÜ¦lƒH +¹7î±~Êõ•d7*!bœ,ßX»äÑ7”ft“½xäêQ³ e"K‹»ÐªwŠL*a"¾pŸÿ¦#ôyQs€<6|8ᦄv|CÙøú÷4¶Y6kSýjSK°š/m,ً«¹Ÿ_ºB}‹Må "õÀ]¸VðŒp°¶¢œ6 +½’K½NˆØübRæüœûވ͆ +±ÜÆûçÍ´¿¥ŒnH(Ûnˆès?0šrÿWJu*üPTõqÅûíG.b¯cø@áÙ\síÁ˜«©¬¬6ç—êŠÅÍe‹LëñDIȨŒ?ŠËJڔ»#DPüù«F߸Y#šèûN#û1ê}?Ã`0L®¬¦•qtÊ͑µ.v¨á š•™•]ˆwubãÜêx&~aÔḑ)ù²BæsºŠªúú³Íô¿6`1xà÷òf¾§ý ãXzc¡)¿De2“!¶?ž¤×\Lùv׉¾"Ïük ‚ƒcen‚|y2˜”êwêq¾WfENUµ¹´¡F€¨g¡z*?ÖIÏü±…¥wdå¤+kw•€•ôßÛ¡Ò Êя@ٛÚW_ÿËbúúrIâÉÊÐT}nΤµ‘k.6Jði&ªî*yXu|íê!Òz‡U›t˜ÙG»®§7”<¼ॶw¼Îžn8ŽÛ¹ÏœŸ·j•Ÿ¼§s¹eúòÊ2Su™pàã«ï\Á܅SkB·Í(qÌ´©‹ÆÌÈ\â) ¨öz±¹·fz:JÞ…­6黼æ"e/ù›‡Þ?¯pŸ†œœœ¼!BHÖê²±ŽË1é+Ì%Ŗ*áxÓ)똻Ô>kNȤõëóD}~AÎãd¬Í7_.‡/|rò¸i#ÃVÕ['èK Šiš³²³²²-Yéâ»±-™'1GÜà_5øƒMmÁû{ÌæPŠÂ³t4ˆ#CùŒ=ÎaècÐ~Ê¡ö%ݔöyDÎw{æ¬aᐍ'ZGép±Gw¶Vš\ žUûjm@ËÀ=&£ÑXRj.?ìêÊ0ÿ…ß +N +endstream +endobj + +681 0 obj +<> +stream +xœUTiPTg}¦τ" Ž´8Ý]S–— ˆ J ˆ²4È&K£ìÝØ@cÓlaQ65·eîfS¤Ù5 +†EŒ Œ ;”Œ2à¨#55šßcžS5’©TêýùêÞ¯î;ç»ç34Àp7ɧe*y¸ä/žR…<>I¶VÝJ™ãÔfêO, •«ã«)l0f±¡~³‘Ôe~†‚?A®Ÿb,·Ž>—’ ŒR ¿ôóöß¶cÇÎß+V–»ö¥)ÿïd‰òH¥p+s8-SÄÆÅȔ*yŒ4)Qè#Q& +݄޲È$…$A袒(äáèaö™26!Q•$‘†Ÿ”yFDÊB óļ0ÌóÃÄØaìæ€9bN˜%按0WÌÃ1S†*fˆùc½Øð2ü•µÁ?Y!¬ C[Ã&ö6vñ ±‡8Jx &ªª)Ÿ©¨jS*ui#§¹P5\šõ¡†ýœBw؜qdP;1öÒ¼§=*øBîٜT=ýŽi>£ŒóËÕW œ7+îÞoç'öçs,ËGØÌHÕµs¶„¦—XÔçT÷+8 9nr4š6€}$͚³C†ogoÍ=äß{܂pø;ô}ÛÕ*o Ñ2ݖ;s7è€Wškì ÌÉ/Ò H‡ÐŠ} m¬¿ý &h‘Až\ý]µ)§‰~G=€®×!“É!dÃ«|Áæ/¥ŸSgšƒÐx7’s¨âgâÅuˆ¦C¯KH¼ä¾¼‘3M•Q&ÜÂu ”“.]Û²„æ_²¨î^nvnäŸQRê|Á0zP¤€RޘSçŸi¡µÍJæ?'Jê‹5ÍÕkw4w¼‘“wžá|1%[šœyø<Þ”!É +PøGLjáùõƒÀåöÛÅåíü¦2]é÷mäy£#I}bו|(ªå_‚¢KEj’Ɛ7Ý}·x7Þ§jï´Tµ4V +jµ —{Šß”_VwhÉuIúUSsȖúEFèǑl5†K[ü7†ýQ1Ïæti¾£œ1…ÉÆÎ®ÑÉDÀ{Ú=ß* ÍH‰á§f¥g(¢P½ÙKºš;Ýá¿÷Xª›2P uövÒ9¬çé_+Çê»·žôÝdôù›ÆðÙ%ÔϬ² ý›«Ö2_äó %åv¼^Ñ,ÕzÃ.°‘‰‚<ä4¨ ¬j ëHŸa¼áރºF]«î!iMÄð =¨,¬{~¸f¶å¾àæ£îÆ! ߊò¾çó²¤9Ͱì\å3,­ OºõÔHxðhúSڊޤ7ŒÚýÒÑY~ý¶ ±²\«©'9Éivì‚RRîOwOìóLóŒñpD'äæöa=‹ýºÇõ½‚›ã]ÍÃðë›"ѲœÃŸÿêôA”ÎÕÌç×B3¹²†Æö엊ŽóƒÜ"h6¿Zߛ7äiý€ŒáýLÂV‘NâHCº_\U—h•SܺW3à @NvÊÎ;—›&0Y­R-PÿZÀyK5¼eQ­«)\fæv‰Í>+ 1½èÏaÞî^pWà‚ò¼ú‘´áå,ÚÈ +žÆÎ†vt¬·2Æ;ñ­¡ú(Mx‚…b‡ø°—Íîàíà +nõQq7ÎþàUÓJßÌÀÓåÞ¬EM5.£hóNŒ%÷Úñ²Þ +]ðŠ¡Êsµãf)™ê4°Ÿ4ûd+3Ç5§NÛ?3‘´B³׌H+§wŠÚ1Çäí ³3ê*3.õ›3-Yš$óº*þT™Üܾ‘¸û‰"h|EÒø¤Ý»Çú¾^~N²’Þ,öÏ9\ß)(2ºlm”—’—IÛñ{6Q{·“ßÓ¥Ò#É1{Mes¢èlwîÈ÷‡ÜS3íá%óR$é~6¨n¼/h{òcÛÚBQÂz ¾{# ƒÊšø-P‘ꆒ¶0çõK«ŽÕ8\/®h­Üd¢ºFÕh‘÷ßkÄß>ZþøzUA~~qqaa¡±1†ý%هA +endstream +endobj + +682 0 obj +<> +endobj + +683 0 obj +<> +endobj + +684 0 obj +<> +endobj + +685 0 obj +<> +endobj + +686 0 obj +<> +endobj + +687 0 obj +<> +endobj + +688 0 obj +<> +endobj + +689 0 obj +<> +endobj + +690 0 obj +<> +endobj + +691 0 obj +<> +endobj + +692 0 obj +<> +endobj + +693 0 obj +<> +endobj + +694 0 obj +<> +endobj + +695 0 obj +<> +endobj + +696 0 obj +<> +endobj + +697 0 obj +<> +endobj + +698 0 obj +<> +endobj + +699 0 obj +<> +endobj + +700 0 obj +<> +endobj + +701 0 obj +<> +endobj + +702 0 obj +<> +endobj + +703 0 obj +<> +endobj + +704 0 obj +<> +endobj + +705 0 obj +<> +endobj + +706 0 obj +<> +endobj + +707 0 obj +<> +endobj + +708 0 obj +<> +endobj + +709 0 obj +<> +endobj + +710 0 obj +<> +endobj + +711 0 obj +<> +endobj + +712 0 obj +<> +endobj + +713 0 obj +<> +endobj + +714 0 obj +<> +endobj + +715 0 obj +<> +endobj + +716 0 obj +<> +endobj + +717 0 obj +<> +endobj + +718 0 obj +<> +endobj + +719 0 obj +<> +endobj + +720 0 obj +<> +endobj + +721 0 obj +<> +endobj + +722 0 obj +<> +endobj + +xref +0 723 +0000000000 65536 f +0000000018 00000 n +0000000356 00000 n +0000001183 00000 n +0000001554 00000 n +0000005103 00000 n +0000005158 00000 n +0000005317 00000 n +0000005450 00000 n +0000006289 00000 n +0000006777 00000 n +0000006971 00000 n +0000007157 00000 n +0000007431 00000 n +0000007713 00000 n +0000007899 00000 n +0000008109 00000 n +0000008327 00000 n +0000008505 00000 n +0000008819 00000 n +0000009029 00000 n +0000009231 00000 n +0000009473 00000 n +0000009755 00000 n +0000009973 00000 n +0000010135 00000 n +0000010329 00000 n +0000010563 00000 n +0000010781 00000 n +0000011071 00000 n +0000011273 00000 n +0000011577 00000 n +0000011763 00000 n +0000011963 00000 n +0000012149 00000 n +0000012349 00000 n +0000012591 00000 n +0000012865 00000 n +0000013131 00000 n +0000013333 00000 n +0000013519 00000 n +0000013817 00000 n +0000013979 00000 n +0000014197 00000 n +0000014359 00000 n +0000014585 00000 n +0000014747 00000 n +0000014925 00000 n +0000015103 00000 n +0000015313 00000 n +0000016003 00000 n +0000016685 00000 n +0000016911 00000 n +0000017008 00000 n +0000017161 00000 n +0000017523 00000 n +0000017571 00000 n +0000017606 00000 n +0000017709 00000 n +0000017844 00000 n +0000017976 00000 n +0000018111 00000 n +0000018243 00000 n +0000018378 00000 n +0000018513 00000 n +0000018645 00000 n +0000018780 00000 n +0000018912 00000 n +0000019047 00000 n +0000019179 00000 n +0000019314 00000 n +0000019446 00000 n +0000019581 00000 n +0000019713 00000 n +0000019848 00000 n +0000019980 00000 n +0000020115 00000 n +0000020247 00000 n +0000020382 00000 n +0000020514 00000 n +0000020649 00000 n +0000020781 00000 n +0000020916 00000 n +0000021051 00000 n +0000021186 00000 n +0000021318 00000 n +0000021453 00000 n +0000021585 00000 n +0000021720 00000 n +0000021852 00000 n +0000021987 00000 n +0000022122 00000 n +0000022257 00000 n +0000022389 00000 n +0000022524 00000 n +0000022659 00000 n +0000022794 00000 n +0000022926 00000 n +0000023061 00000 n +0000023193 00000 n +0000023328 00000 n +0000023464 00000 n +0000023597 00000 n +0000023733 00000 n +0000023869 00000 n +0000024002 00000 n +0000024138 00000 n +0000024271 00000 n +0000024407 00000 n +0000024540 00000 n +0000024676 00000 n +0000024809 00000 n +0000024945 00000 n +0000025078 00000 n +0000025214 00000 n +0000025347 00000 n +0000025483 00000 n +0000025616 00000 n +0000025752 00000 n +0000025885 00000 n +0000026021 00000 n +0000026154 00000 n +0000026290 00000 n +0000026424 00000 n +0000026560 00000 n +0000026694 00000 n +0000026830 00000 n +0000026964 00000 n +0000027100 00000 n +0000027234 00000 n +0000027370 00000 n +0000027504 00000 n +0000027640 00000 n +0000027774 00000 n +0000027910 00000 n +0000028044 00000 n +0000028180 00000 n +0000028314 00000 n +0000028450 00000 n +0000028584 00000 n +0000028720 00000 n +0000028854 00000 n +0000028990 00000 n +0000029124 00000 n +0000029260 00000 n +0000029394 00000 n +0000029530 00000 n +0000029664 00000 n +0000030562 00000 n +0000030637 00000 n +0000030773 00000 n +0000030907 00000 n +0000031043 00000 n +0000031177 00000 n +0000031313 00000 n +0000031447 00000 n +0000031583 00000 n +0000031717 00000 n +0000031853 00000 n +0000031987 00000 n +0000032123 00000 n +0000032257 00000 n +0000032393 00000 n +0000032527 00000 n +0000032663 00000 n +0000032797 00000 n +0000032933 00000 n +0000033067 00000 n +0000033203 00000 n +0000033337 00000 n +0000033473 00000 n +0000033607 00000 n +0000033743 00000 n +0000033877 00000 n +0000034013 00000 n +0000034147 00000 n +0000034283 00000 n +0000034417 00000 n +0000034553 00000 n +0000034687 00000 n +0000034823 00000 n +0000034957 00000 n +0000035093 00000 n +0000035227 00000 n +0000035363 00000 n +0000035497 00000 n +0000035633 00000 n +0000035767 00000 n +0000035903 00000 n +0000036037 00000 n +0000036468 00000 n +0000036618 00000 n +0000036779 00000 n +0000036943 00000 n +0000039236 00000 n +0000039311 00000 n +0000039466 00000 n +0000039650 00000 n +0000041569 00000 n +0000041705 00000 n +0000041840 00000 n +0000041976 00000 n +0000042112 00000 n +0000042248 00000 n +0000042384 00000 n +0000042520 00000 n +0000042656 00000 n +0000042792 00000 n +0000042928 00000 n +0000043063 00000 n +0000043275 00000 n +0000043411 00000 n +0000046078 00000 n +0000046166 00000 n +0000046302 00000 n +0000046437 00000 n +0000046573 00000 n +0000046709 00000 n +0000046845 00000 n +0000046981 00000 n +0000047117 00000 n +0000047253 00000 n +0000047388 00000 n +0000047524 00000 n +0000047660 00000 n +0000047796 00000 n +0000047932 00000 n +0000048068 00000 n +0000050501 00000 n +0000050602 00000 n +0000050737 00000 n +0000050873 00000 n +0000054114 00000 n +0000054250 00000 n +0000054386 00000 n +0000054547 00000 n +0000054707 00000 n +0000054843 00000 n +0000058411 00000 n +0000058547 00000 n +0000058683 00000 n +0000058819 00000 n +0000058955 00000 n +0000059091 00000 n +0000059227 00000 n +0000061994 00000 n +0000062189 00000 n +0000064463 00000 n +0000064551 00000 n +0000064687 00000 n +0000064823 00000 n +0000064959 00000 n +0000065095 00000 n +0000065231 00000 n +0000065367 00000 n +0000065503 00000 n +0000065639 00000 n +0000065775 00000 n +0000065911 00000 n +0000066047 00000 n +0000066183 00000 n +0000066319 00000 n +0000066455 00000 n +0000066591 00000 n +0000066727 00000 n +0000066863 00000 n +0000066999 00000 n +0000068274 00000 n +0000068336 00000 n +0000068472 00000 n +0000068608 00000 n +0000068744 00000 n +0000068880 00000 n +0000069016 00000 n +0000069737 00000 n +0000069838 00000 n +0000069974 00000 n +0000070110 00000 n +0000070246 00000 n +0000070382 00000 n +0000071253 00000 n +0000071389 00000 n +0000071525 00000 n +0000071661 00000 n +0000071797 00000 n +0000071933 00000 n +0000072069 00000 n +0000072205 00000 n +0000072341 00000 n +0000072475 00000 n +0000074095 00000 n +0000074231 00000 n +0000074367 00000 n +0000074503 00000 n +0000074639 00000 n +0000074775 00000 n +0000074911 00000 n +0000075047 00000 n +0000075183 00000 n +0000075319 00000 n +0000075455 00000 n +0000075591 00000 n +0000075727 00000 n +0000075863 00000 n +0000075999 00000 n +0000078302 00000 n +0000078438 00000 n +0000078574 00000 n +0000078710 00000 n +0000078846 00000 n +0000078982 00000 n +0000079118 00000 n +0000081686 00000 n +0000081774 00000 n +0000083377 00000 n +0000083513 00000 n +0000083647 00000 n +0000083783 00000 n +0000085069 00000 n +0000085205 00000 n +0000085341 00000 n +0000085477 00000 n +0000085613 00000 n +0000085749 00000 n +0000085885 00000 n +0000086021 00000 n +0000086157 00000 n +0000087477 00000 n +0000087613 00000 n +0000087749 00000 n +0000087885 00000 n +0000088021 00000 n +0000088157 00000 n +0000088293 00000 n +0000089412 00000 n +0000089548 00000 n +0000089684 00000 n +0000089820 00000 n +0000089956 00000 n +0000090092 00000 n +0000090228 00000 n +0000090364 00000 n +0000090500 00000 n +0000090636 00000 n +0000090772 00000 n +0000090908 00000 n +0000091044 00000 n +0000091180 00000 n +0000091315 00000 n +0000091451 00000 n +0000093403 00000 n +0000093539 00000 n +0000093675 00000 n +0000093811 00000 n +0000093947 00000 n +0000094511 00000 n +0000094573 00000 n +0000094636 00000 n +0000094772 00000 n +0000094908 00000 n +0000095044 00000 n +0000095180 00000 n +0000095316 00000 n +0000095452 00000 n +0000095588 00000 n +0000095724 00000 n +0000095860 00000 n +0000095996 00000 n +0000096132 00000 n +0000096268 00000 n +0000098488 00000 n +0000098537 00000 n +0000098586 00000 n +0000098722 00000 n +0000098858 00000 n +0000100533 00000 n +0000100589 00000 n +0000101028 00000 n +0000101090 00000 n +0000101217 00000 n +0000101279 00000 n +0000101734 00000 n +0000101848 00000 n +0000102162 00000 n +0000102211 00000 n +0000102299 00000 n +0000102435 00000 n +0000102571 00000 n +0000102707 00000 n +0000102843 00000 n +0000102979 00000 n +0000103115 00000 n +0000103251 00000 n +0000103387 00000 n +0000103523 00000 n +0000105703 00000 n +0000105839 00000 n +0000105975 00000 n +0000106111 00000 n +0000106247 00000 n +0000106383 00000 n +0000106519 00000 n +0000106655 00000 n +0000106791 00000 n +0000106927 00000 n +0000107061 00000 n +0000107197 00000 n +0000107333 00000 n +0000107469 00000 n +0000108967 00000 n +0000109103 00000 n +0000109239 00000 n +0000109375 00000 n +0000109511 00000 n +0000109647 00000 n +0000109783 00000 n +0000109919 00000 n +0000110055 00000 n +0000110191 00000 n +0000110327 00000 n +0000110463 00000 n +0000110599 00000 n +0000112507 00000 n +0000112643 00000 n +0000112778 00000 n +0000112914 00000 n +0000113050 00000 n +0000114140 00000 n +0000114276 00000 n +0000114412 00000 n +0000115219 00000 n +0000115355 00000 n +0000115491 00000 n +0000115627 00000 n +0000115763 00000 n +0000115899 00000 n +0000116035 00000 n +0000116171 00000 n +0000116307 00000 n +0000116443 00000 n +0000116579 00000 n +0000116715 00000 n +0000116851 00000 n +0000116987 00000 n +0000117123 00000 n +0000117259 00000 n +0000117395 00000 n +0000119098 00000 n +0000119385 00000 n +0000119521 00000 n +0000119657 00000 n +0000119793 00000 n +0000119929 00000 n +0000120065 00000 n +0000120200 00000 n +0000122489 00000 n +0000122832 00000 n +0000122968 00000 n +0000123104 00000 n +0000123240 00000 n +0000123376 00000 n +0000123587 00000 n +0000123799 00000 n +0000123933 00000 n +0000125837 00000 n +0000126260 00000 n +0000126309 00000 n +0000126445 00000 n +0000128939 00000 n +0000129074 00000 n +0000133201 00000 n +0000133276 00000 n +0000133428 00000 n +0000133591 00000 n +0000135055 00000 n +0000135130 00000 n +0000135266 00000 n +0000135470 00000 n +0000135674 00000 n +0000135880 00000 n +0000136086 00000 n +0000136289 00000 n +0000136493 00000 n +0000136696 00000 n +0000136898 00000 n +0000137034 00000 n +0000137170 00000 n +0000137306 00000 n +0000137442 00000 n +0000137578 00000 n +0000137714 00000 n +0000137850 00000 n +0000137986 00000 n +0000138122 00000 n +0000138258 00000 n +0000138394 00000 n +0000138530 00000 n +0000138666 00000 n +0000138802 00000 n +0000138938 00000 n +0000139074 00000 n +0000139210 00000 n +0000139411 00000 n +0000139613 00000 n +0000139749 00000 n +0000139885 00000 n +0000140021 00000 n +0000140157 00000 n +0000140293 00000 n +0000140427 00000 n +0000140561 00000 n +0000140695 00000 n +0000140829 00000 n +0000140962 00000 n +0000141096 00000 n +0000141230 00000 n +0000141364 00000 n +0000141498 00000 n +0000141632 00000 n +0000141766 00000 n +0000141900 00000 n +0000142098 00000 n +0000142232 00000 n +0000142366 00000 n +0000142499 00000 n +0000142633 00000 n +0000142767 00000 n +0000142901 00000 n +0000143035 00000 n +0000143169 00000 n +0000143303 00000 n +0000143436 00000 n +0000143570 00000 n +0000143704 00000 n +0000143838 00000 n +0000143972 00000 n +0000144106 00000 n +0000144240 00000 n +0000144374 00000 n +0000144508 00000 n +0000144642 00000 n +0000145769 00000 n +0000145831 00000 n +0000145967 00000 n +0000146103 00000 n +0000146239 00000 n +0000146375 00000 n +0000146511 00000 n +0000146647 00000 n +0000146783 00000 n +0000146919 00000 n +0000147055 00000 n +0000147191 00000 n +0000147327 00000 n +0000147463 00000 n +0000147599 00000 n +0000147735 00000 n +0000147871 00000 n +0000148007 00000 n +0000148143 00000 n +0000148344 00000 n +0000148480 00000 n +0000148616 00000 n +0000148752 00000 n +0000148888 00000 n +0000149024 00000 n +0000149160 00000 n +0000149296 00000 n +0000149432 00000 n +0000149568 00000 n +0000149704 00000 n +0000149840 00000 n +0000149976 00000 n +0000150112 00000 n +0000150248 00000 n +0000150384 00000 n +0000150518 00000 n +0000150652 00000 n +0000150786 00000 n +0000150981 00000 n +0000151115 00000 n +0000151249 00000 n +0000151383 00000 n +0000151516 00000 n +0000151650 00000 n +0000151784 00000 n +0000151918 00000 n +0000152052 00000 n +0000152185 00000 n +0000152319 00000 n +0000152519 00000 n +0000152652 00000 n +0000152786 00000 n +0000152920 00000 n +0000153053 00000 n +0000153186 00000 n +0000153320 00000 n +0000153454 00000 n +0000153651 00000 n +0000153785 00000 n +0000153918 00000 n +0000154112 00000 n +0000154246 00000 n +0000154440 00000 n +0000154574 00000 n +0000154768 00000 n +0000154902 00000 n +0000155976 00000 n +0000156112 00000 n +0000156308 00000 n +0000156444 00000 n +0000156640 00000 n +0000156776 00000 n +0000156972 00000 n +0000157108 00000 n +0000157421 00000 n +0000157559 00000 n +0000157701 00000 n +0000158153 00000 n +0000158668 00000 n +0000158730 00000 n +0000159241 00000 n +0000159713 00000 n +0000160180 00000 n +0000160637 00000 n +0000161006 00000 n +0000161310 00000 n +0000163761 00000 n +0000163811 00000 n +0000164664 00000 n +0000183113 00000 n +0000183414 00000 n +0000184164 00000 n +0000196468 00000 n +0000197454 00000 n +0000212691 00000 n +0000212992 00000 n +0000213040 00000 n +0000213780 00000 n +0000227686 00000 n +0000228439 00000 n +0000241464 00000 n +0000241764 00000 n +0000242704 00000 n +0000257753 00000 n +0000258579 00000 n +0000276607 00000 n +0000276907 00000 n +0000278054 00000 n +0000293524 00000 n +0000293825 00000 n +0000294126 00000 n +0000295184 00000 n +0000306837 00000 n +0000307016 00000 n +0000307185 00000 n +0000307358 00000 n +0000307462 00000 n +0000307617 00000 n +0000308021 00000 n +0000308580 00000 n +0000309188 00000 n +0000309647 00000 n +0000310059 00000 n +0000310537 00000 n +0000310805 00000 n +0000310911 00000 n +0000311018 00000 n +0000311126 00000 n +0000311277 00000 n +0000311382 00000 n +0000311501 00000 n +0000311621 00000 n +0000311734 00000 n +0000311886 00000 n +0000312006 00000 n +0000315479 00000 n +0000320727 00000 n +0000326981 00000 n +0000333304 00000 n +0000337631 00000 n +0000342317 00000 n +0000344209 00000 n +0000344314 00000 n +0000344420 00000 n +0000344553 00000 n +0000344686 00000 n +0000344824 00000 n +0000344974 00000 n +0000345089 00000 n +0000345224 00000 n +0000345341 00000 n +0000345456 00000 n +0000345570 00000 n +0000345694 00000 n +0000345807 00000 n +0000345927 00000 n +0000346042 00000 n +0000346187 00000 n +0000346301 00000 n +0000346418 00000 n +0000346577 00000 n +0000346702 00000 n +0000346817 00000 n +0000346934 00000 n +0000347044 00000 n +0000347181 00000 n +0000347304 00000 n +0000347431 00000 n +0000347544 00000 n +0000347658 00000 n +0000347782 00000 n +0000347926 00000 n +0000348048 00000 n +0000348164 00000 n +0000348277 00000 n +0000348422 00000 n +0000348560 00000 n +0000348710 00000 n +0000348827 00000 n +0000348931 00000 n +0000349035 00000 n +0000349152 00000 n + +trailer +<]>> +startxref +349269 +%%EOF + +1 0 obj +<> +endobj + +xref +1 1 +0000363882 00000 n + +trailer +<]/Prev 349269>> +startxref +364019 +%%EOF diff --git a/tests/resources/3.pdf b/tests/resources/3.pdf new file mode 100644 index 0000000..ed945a2 --- /dev/null +++ b/tests/resources/3.pdf @@ -0,0 +1,744 @@ +%PDF-1.5 +%%μῦ + +1 0 obj +<> +endobj + +2 0 obj +<> +endobj + +3 0 obj +<> +endobj + +4 0 obj +<> +stream + + + + + none + 2016-11-10T06:47:57-04:00 + + + none + + + application/pdf + + + none + + + + + none + + + + + + + + + + + + + + + + + + + + + + + + + + + +endstream +endobj + +5 0 obj +<>>> +endobj + +6 0 obj +<>/NM(05b46baf-8873-4881-b973382e67de9b94)/Name/Sold/Rect[38.308668 747.53 283.51768 811.89108]/Subj(Something Special)/Subtype/Stamp/CreationDate(D:20161104051921-04'00')>> +endobj + +7 0 obj +<>/NM(04e49b05-c0fe-4e18-a442eb62db809d70)/RC(

this is a comment

)/Name/Comment/Rect[339.5959 772.56729 359.5959 790.56729]/Subj(Kommentar)/Popup 8 0 R/Subtype/Text/Contents(this is a comment)/CreationDate(D:20161104051939-04'00')>> +endobj + +8 0 obj +<> +endobj + +9 0 obj +<>/BS<>/DA(1.000 0.000 0.000 rg)/IT/FreeTextTypewriter/LE/None/NM(7f35f3bb-14ee-4900-b2ca8f24a6d68ea2)/RC(

typewriter text

)/RD[0 0 0 0]/Rect[396.85334 754.1775 476.07408 788.5933]/Subj(Schreibmaschine)/Subtype/FreeText/Contents(typewriter text)/CreationDate(D:20161104052009-04'00')>> +endobj + +10 0 obj +<>/BE<>/BS<>/DA(0.000 0.000 0.000 rg)/LE/None/NM(75e29b8c-3e27-49f5-9d1691add0d9e97b)/RC(

modified text field

)/RD[8.56139 8.564017 8.561391 8.561395]/Rect[49.683259 675.1755 166.80605 710.3009]/Subj(Text-Box)/Subtype/FreeText/Contents(modified text field)/CreationDate(D:20161104052030-04'00')>> +endobj + +11 0 obj +<>/CL[212.24744 704.70046 231.19762 699.76559 243.19762 699.76559]/DA(1.000 0.000 0.000 rg)/IT/FreeTextCallout/LE/OpenArrow/NM(46f3ee9e-f8d1-4348-a57b21efbbded44c)/RC(

explanation text

)/RD[31.917883 -.000018 .00002 .000019]/Rect[211.27973 690.76559 343.19764 708.76559]/Subj(Erl\344uterung)/Subtype/FreeText/Contents(explanation text)/CreationDate(D:20161104052053-04'00')>> +endobj + +12 0 obj +<>/IT/LineArrow/LE[/RClosedArrow/Diamond]/NM(6404d971-e409-4762-8f4157381a50c0b4)/Rect[385.39955 696.20608 477.3782 730.7424]/Subj(Pfeil)/Popup 22 0 R/Subtype/Line/CreationDate(D:20161104052133-04'00')>> +endobj + +13 0 obj +<>/BS<>/IC[.752943 .752943 .752943]/NM(55705d19-8e3a-4d87-884ff485467f387d)/RD[2 2 2 2]/Rect[65.779178 605.25326 202.0124 651.6411]/Subj(Rechteck)/Subtype/Square/CreationDate(D:20161104052208-04'00')>> +endobj + +14 0 obj +<>/BS<>/IC[.752942 1 1]/NM(2a1c3549-37e0-428b-93ee9b69a3ce9df9)/RC(

comment in circle

)/RD[1 1 1 1]/Rect[247.78653 581.0016 343.54469 671.71279]/Subj(Oval)/Popup 23 0 R/Subtype/Circle/Contents(comment in circle)/CreationDate(D:20161106044139-04'00')>> +endobj + +15 0 obj +<>/LE[/Square/OpenArrow]/NM(d2a8b21c-d4c1-4719-86a11a64a4e0b6ec)/Rect[393.5004 632.15127 504.1475 662.9966]/Subj(Linienzug)/Subtype/PolyLine/Vertices[397.84056 656.3386 404.75093 632.65127 438.3156 632.65127 447.2004 657.3256 479.07295 635.72817 503.47065 662.2605]/CreationDate(D:20161104052251-04'00')>> +endobj + +16 0 obj +<>/NM(a60c24b6-1dc6-4e09-bdec495c6894fdc0)/Rect[69.89996 524.0557 213.81572 586.2813]/Subj(Polygon)/Subtype/Polygon/Vertices[70.477687 567.9854 78.37527 526.5325 158.33824 524.55856 212.63411 550.2199 135.6327 585.75106]/CreationDate(D:20161104052317-04'00')>> +endobj + +17 0 obj +<>/NM(66b71715-3eb4-4097-b2e2f5c4eb0c4d52)/Rect[283.2077 522.9725 404.2485 571.8722]/Subj(Stift)/InkList[[283.32566 563.5629 309.97999 557.6411 340.5831 556.6541 345.51908 551.71926 341.57029 537.9016 345.51908 534.9407 367.23744 536.9146 393.89176 535.9276 397.84056 540.8625 400.80213 539.87557 403.7637 549.7453][395.86616 558.62808 385.99418 557.6411][367.23744 550.73226 337.6215 530.0058 321.82633 524.0839 310.96717 525.0709 308.99278 530.0058 305.04399 535.9276 305.04399 549.7453 308.00559 553.6932 312.94157 558.62808 327.7495 563.5629 355.39106 565.53689 366.2502 570.47177 383.03257 570.47177 387.96858 566.52389 398.82774 563.5629]]/Subtype/Ink/CreationDate(D:20161104052347-04'00')>> +endobj + +18 0 obj +<>/NM(3cba4ea0-034d-4505-bd857d15063c675e)/Rect[108.57578 435.81523 178.93265 476.0276]/Subj(Stift)/InkList[[108.70982 463.30189 128.95235 469.29933 139.44847 475.29676 139.44847 463.30189 143.94681 458.05415][121.45512 454.30574 130.45178 454.30574 132.70096 456.55479 131.95124 452.0567 134.20041 448.3083 136.44957 437.81278 144.69652 437.81278 164.93904 436.31343][161.19043 469.29933 168.68766 469.29933 173.93572 465.55094 176.93462 461.05287 178.43405 446.05928]]/Subtype/Ink/CreationDate(D:20161105124026-04'00')>> +endobj + +19 0 obj +<>/NM(6fe9e08e-7ff0-45e0-8fc78c7d63822c11)/Rect[161.19043 454.55543 209.92243 455.55543]/Subj(Stift)/InkList[[209.92243 455.05543 161.19043 455.05543]]/Subtype/Ink/CreationDate(D:20161105124033-04'00')>> +endobj + +20 0 obj +<>/NM(1d9e69b5-b0bc-43be-8817c8e56e66d22c)/Rect[188.24126 442.2735 193.92711 474.60557]/Subj(Stift)/InkList[[189.6799 474.5471 188.93018 464.80125 191.92906 458.05415 193.42852 442.31086]]/Subtype/Ink/CreationDate(D:20161105124035-04'00')>> +endobj + +21 0 obj +<>/NM(83464ab8-6479-45c4-83e04e59025f9b68)/Rect[88.038318 436.9787 245.77399 474.80393]/Subj(Stift)/InkList[[235.413 469.29933 240.66106 465.55094 242.1605 459.5535 245.1594 458.05415 243.65995 453.55607 243.65995 449.80766 222.66771 440.06184 211.42186 438.56248 209.1727 437.81278 214.42076 452.0567 218.16938 455.80509 218.9191 462.55223 216.66992 464.05158 186.68102 464.05158 143.94681 468.54966 135.69985 465.55094 125.95345 464.80125 118.45622 461.80253 104.21149 461.80253 94.46509 461.05287 92.965648 466.3006 88.46731 474.5471]]/Subtype/Ink/CreationDate(D:20161105124037-04'00')>> +endobj + +22 0 obj +<> +endobj + +23 0 obj +<> +endobj + +24 0 obj +<>/FS 44 0 R/NM(0ac8a6cb-a18f-4526-b5e2ca95632913df)/Rect[313.39085 381.29405 327.39085 401.29405]/Popup 25 0 R/Subtype/FileAttachment/Contents/CreationDate(D:20161109142650-04'00')>> +endobj + +25 0 obj +<> +endobj + +26 0 obj +<> +stream +xœ+T0Ð3T0A(œË¥dh` ^ÌÈh{ +endstream +endobj + +27 0 obj +<> +endobj + +28 0 obj +<>>>>> +stream +xÚ3T0BC=CCK '9—Kß-×@Á%Ÿ U¸ +endstream +endobj + +29 0 obj +<>>> +stream +xÚM‘9R1 EsŸB'PYޝ’‘pW±U“p}ä/yèšäý-϶P¤Ÿ÷iÿ^ŸƒÐW(ô"7úÕϟA&·Dk¢o•{",‰ +§FÒ8 Ã&Ǿ¹Ò +ÏL‘sRn³Õ\!s?(<š±Ö5 ÂÆÀR8w-¶ÐX:¡O:·L{šÑ +¦bI‡Ãђ©{³1¦®û ¯@áɯCï%óè»®ˆ{À™+PWÑI2ÏjRÚ¾‹ÇAø‰‡ ²ç/å"ÞëìCiÌ: a£€ë.Š3¼ùᔏ:Ø…Ï!â~¢c¹73v±ãà1~±æä~è}ðÌÿæ{™7@Á&×]ó.>8V÷¯ó”C½k}¸éÎ¥š3H÷'ł*gŽbAQªõĸ&B«±Ú¦×MÎmÿA‹œ´ +endstream +endobj + +30 0 obj +<>/ProcSet[/PDF]>>>> +stream +xÚMÝ +Â0 …ïóyeiÓ® Áé•JÁ{ÿ܍>¾é6DÊiÓöœ„©Š2Õ"ó>\Á ëÚ­Éã¤dX0xGÎ ‘‚5(Bµ©p8Þàeú`»éà5ÇÕ ¹èEœ&ù ?> ÈÐBìV”Pî‡ÚãÚ>/ProcSet[/PDF]>>>> +stream +xÚ]—ËŽ&5 …÷õyÉÄqn–‹ ¬X€~‰=ÍMHƒÄlàñ9Ç©îvZ£_ÓN9Îçԉ’$©ä5WúúÇUÿýôý¥éß«¯P‹$,3l¹óªF³æŽ\²uzKK֊Ç6à6S¯Ú,‹{k¶ZÓ,ȱՄ¥¬4˜‚”±ÔÊEÅMÓA÷ ¶¾DˆÚй9[ãìÑ·{kr“¯ža®-F_HT7§*ÜGÇÆ-XÐRnÂç‰Â *LÍs2:¤ðy˝3L÷ÅêâVçÚ¶iÌt,ÊÅ 9!ãæÛD“Û4—ïâ,3«Ý-W݋7@®’EÄ£‰¹Ù h`Ál ,Ô@dYدÀH`Á›¬‹+áoV‹qó# z`)ÁFd±š-° ,a´FðvG ‘Rs8´íàáÈ @´51âŠHG LBÁD(‘;‡W(©yE(ÁâJñ„‚‡E(Ø=BÁ.”@ÄJ ç>@Õzgñ +Uq4#l‹Pµ¿S4G¢¤9#jšö!j©ãPµh9d-ÚÞéR8„-:eÓ>¥Í‘¨mFˆâ¦}¨[tò–V}Këï.m +—6‰Ó>5Α(rÚQåŒxÈ\Ú:t.¨|‡ÐYËN(”+µ]½ÖDÈþrŠ¡ÔaÚ,Ô,weªÏÀˆí¸ÖžQ+#ì@»TñÍXO5pXAI”À¡^ž'À¬…żûŽrBË K ۅ4Àzóæ0Ðud I̱»ƒ×`ÀëÓ=Êä¨'»{€æ¡ÜåUþ ý ?‹cAzÜ·¡.ó&íŽßÙa¼„N$É7ÁÆ7sã>ßý 5pº?6¾ ÆGIŶAÈÀÜ  Í„¿"Á=¡HÛ2ýÍ ·ŠšÇ¯³zü>}OÑ[ÍGðF¹§”ÊÂïïKZCFÝpmèkŒR¦ V—úÕ# b΄æä3 éÛÃêžÑ{ßI×Ð׬û-ñ’gÝ·ƒÆmâ©òm•ú™p¨}ˆ*^›n&tGQì÷\›i©Ÿôy2ñìG&ÌÐȤ»Ì¦j÷«»™X~*Z9˜*n ‘ %Ð"ì~2±HF&DÈTË}‰ze’µ/,/L¬ÓIÊÄ “ŸßÀÄÛDd‚­'»Id‚m‘©ìs˜P$2ñ<¿1ñ°F$ý%¡£öSv܀s[·.^`P +,°°Ù¿¡à̶ˆ²p_ (¸hh@Yý¬y (¼œ;5’%Í;Ð +Žÿ!h8E=Ã)Ê×±SͼŸ¸G-s‰eê¡d^ ßPÆ8uÌPPÑ¢Šaž"Æ@Ô0fG Ã<h²ïðv?Í«£ÛWêRm»/\Чßêý«·üwÏWúóúåúøø/=ýðéúçÿ"RTtÎf»W¨u <¹>hú`‰eµ¦¯¿qöÏéïëéqÊÄošO|²ôôøõúø]çŒÇï×7¥ô‚_-(îø}ÂÏîÿ×+ŠçsÛýiÿ]ôÛôøëúü¸~¼>ƒô²©ä +endstream +endobj + +32 0 obj +<>/ProcSet[/PDF]>>>> +stream +xÚ]ÍJCA …÷yмÀM“ÌOf@º¸¥uåBpïo+ؕï±½](!$9ð$Ɔ8¾‘±"î®ÉÍÅsph–ày2±Þ¸ö.Q+çôO¹Õ¤– +­IjþËýñå*Ý3æív¦àTON&§Þق/Ä{z¤ÕøæùfC_Ëqƞ 6G5‡MÅòRrá§MƓ†•Õ…àOšé ŃzyPy꒣ðx¦Õï;WºRÍMµÌȄ~‡Ìèm©ºAõ³¦iÑ.L¬y¼ÓvÐ-mqóàEDª +endstream +endobj + +33 0 obj +<>>> +stream +xÚ]޽ CAƒ{¦`‹ŽŸ Òç-‘âRe)èIi"*[ŸËÜóAÖŽÊæC.ã7yJ7§&V5ºÆ´¥3¿)E'§ $ö@žÑZ÷[;¢ÆpØôâ}È*°4æÝFɞ¢ÿg˜Bôâèý‹~Hå#9 +endstream +endobj + +34 0 obj +<>>> +stream +xÚ3Ð375V0@"‹Ò¹ ôL,Œ`$H0ȝËD¡œ+ÚBÁc R¸ÌÌõÌÍ-Ì ÌõŒ€J ôŒŒLŒôŒ-,ŠR¹2¸’¸ªÏK +endstream +endobj + +35 0 obj +<>>> +stream +xÚ]O11 ÛóмÀJÓ:齀'оO%hOBYb˱C²j™óº‹©Íåz×·Ôæ`£†*SŸ'CCgÓêdh¤!KU?ˆˆñ8ÉSÑ:²çvØø—1/c=t°;Ì|G,ä&,4/³ +endstream +endobj + +36 0 obj +<>>> +stream +xÚe»‚1 ƒûL‘ tNüŠ' ‡%( bÿ;ä¿âŽ&/’¥VÅ1®}¾ß†VâØšá՚ïabH'эàþ¦ºâ—XbӓÐ}ς¤RáÈ}H\–œ—ä1¬ çP䏎ú¹,¸Jú!/›Ò¶:Œ%òÐ¥åüCWfÕaÖ¶hbØU$ZÅ,ˆ7˜Ïñ_tõ2‹ +endstream +endobj + +37 0 obj +<>>> +stream +xÚʱ Ä@Dќ*¨`ìÎ\~U8ðE׿d¬ÉÞ|W›}?ÂF¬PžÆ.êON£•Q°9nñ°ž$Œ5æXUJ:Òûmb¤²Á½ä/àD­ +endstream +endobj + +38 0 obj +<>>> +stream +xÚ]”Í‘\1„ïŋ€B ôïv>;ÿ«¿fìY—kkë H@74ó÷ýÛ+NZÆzj¥ñÿüzÅ]¶*ñ¸-Gö<õT]‹uŸôk÷`n[s¹Q–¥œÓöyÛ5.ö°ÍWä®ñßé‡ÃjcNs}ÌÜv¬ûe›¯ +…c{ÆWœvß$)-O%€ÖÐ7Ÿ\Û"wÛ’DhÍÙEÇádŒÎªq!y£kÞ8Š “'²s¤qAªqÉ4°·)’ng5{@E„èPuºÓZÕ<Ü̶Gg8{1=Í7ӂqÀgzðÍîԏŽ÷"Á¼¶§hüx ÆYH Ž-àüÂãv³#àæÐÿ(€ÛSnò䨰·É¤dSŒÀ³¡ú…,m[Ý.7wAO~т§ÐÓOBÓ lé'cÐ>pÆ4?­£±¬„$È-卂ìh["à^{ÊNßD‰º:΍(@`иί¨~rˆ Tf‹<p”ª m{«à3eµ¸§H¦n¤]í“&òuÿ3yfªhU@»µØA”¢¢Xӧގ´ê…6©‘tn^5F¶–ƒ‡ÉUò+â04)ˆ”W †‡2 ½ÖËaû×}CˆÖ+èý¸Qk¼W2õv- Z¬×Ñ~ˆÿxý¾1úç +endstream +endobj + +39 0 obj +<>>> +stream +xÚM’M’[Aƒ÷>Å;Õ4?MŸ û̲Îý·ùÀה.ñjôYü~ÿzé*9úxšØÚÏߗjIYW’Š>ªWô\pˆW>ºKnlð•}ïóç¥æ^¥Ãµ”Ró¦ÚŸïã4÷¬DãÍ(Y»51ƒ›ãÛU3–ìñ¹DãQWÉ[óØäúy< +‡ðõҍjŽHöj;°nŸŠîÄ*a‚9Æ.ûó½WS¹ïB°«Ú»-pdé|ÈÈ#%ÕÌP ©Û»«DÖ(h¾ñf@L­©Ý±dYÏ0Ò¼ƒ-»àÀ K5ý!/ÎÊÛÏô{uǒN‚ð¥íð6ýdŠ!haDM[&räÝIòü$`µvNe#ذýW".ŸÎ Q§ŠÞÏIôÿ¦ïŠr<{œ¥¦yè¬úyDOZÌ xûlÜQça]ó–s†A€Í bÍ9’ì£áÜÌß§œøo|GQ‰z6;Å3Îs3[ÏÁƒŒèþž ö¤ÿÎbÿ šï +endstream +endobj + +40 0 obj +<>>> +stream +xÚ3T0 w.#K=K##SS=SS…\.C S=SS3¸ßÌPÏÐÎMæ +æI‡Ü +endstream +endobj + +41 0 obj +<>>> +stream +xÚ%»Ä@CsWA ± \~.áâë?5‹‡€y’@JÒóý\šÅ+ ØôÁ%« ÒL¶DÕ`y#8Eé7y(ZQ¶pÒê­>¼üå:"Y碜±¬aÏì„uS k.`ìz:î빺!Õ +endstream +endobj + +42 0 obj +<>>> +stream +xÚMSK®AÛÏ)êˆÁ ²Ï;BÖ¹ÿ6†nE£Y´ ¸À†‘ÃøýþõQ r±ãÙ¤Ýç/"El‘K®q·ðÄû¨3e +pP„œ?u%EÄH2·âò`¡.?“—aDƒa/£c"E˜Nå½ÚóBDožÃ—n=‘DE-ƒïࠊ™Ê0 ¡E.ÁÈt +„®½O*„†óúOð¦â‚9¥B÷¼ÌÝFŽé…53ª‚ ŽS‡!—,ã8Rè̌ÖämÀuóñ2˜ãMÄð–2¼EKKêiÉMrçK%öüî 9CãE®ÁAVP-Žã£Â§»øT˜)±3IÙc cRØÅëÇ20­éz}cTGì.’m-`¤ÂÚG*9wßWs+V7pÏPI9÷Ÿzªq‘;¼NϽà+BÉCp[:Îâk7Ï`‘Ø›ìaÀ_p“1"–§úò¢¸ÇèLˆ+à ûu²bŒ|oËH;ƒó> KQ¯¼m¾x:¿"ñ<+[ÌãLðí±<»,+dþ8Œã†`ÓXã+ì8ùbt“Ùbho˜:·0kÓXô";jí.t Ís›®pHv>êm§oÐfë—!·Nã? ¹ÐáZaôÙØ…>Wôóùېʪ +endstream +endobj + +43 0 obj +<>>> +stream +xÚM;Rƒ1 „{B@cÉï–&U8B +H&4ar{vý»È¸YÙÚO+»:Îï—$åù8‰ëMŠž%YsýÃýUšµ¬ÍrÕ閊&ËMïÐ帾˻¸Û¬êÓê@›=¬²NëC®O9ŠdA€Ë]»9Bd«Z }Ð݉ö¦¾fD`f ¾»^ä-±¾® äÅ«€Ś8YGžX¤%6}ȝšY_ò_D¿±¨øB¡{5Kk—àø-™xXŠUpÝVwÿS>a¯+ v¤AÀ°Çܒö²ô„{´ÝN÷?"¼N¶ +endstream +endobj + +44 0 obj +<>/UF/Type/F>> +endobj + +45 0 obj +<> +endobj + +46 0 obj +<>/Filter[/FlateDecode]/Length 16/Matrix[1 0 0 1 -.001419 .000039]/Subtype/Form/FormType 1/Resources<>>>>> +stream +xÚÓwË5PpÉç ð +endstream +endobj + +47 0 obj +<>/FontDescriptor 53 0 R>>]>> +endobj + +48 0 obj +<>/FontDescriptor 55 0 R>>]>> +endobj + +49 0 obj +<>/FontDescriptor 57 0 R>>]>> +endobj + +50 0 obj +<> +stream +xœåZaoÇý.@ÿá/Ly±3»;»c  +»R4­‘äCÃñd³¥H—¢ã8Eÿ{ßÜޑ}”iG®Øbó4Üۛ}óæ½9±ËN›/gíÕôÕbsÑ¿i®Vëfó¢m¶AÓf1ÿq=]¿¹8?ûñÕ|±iŽ;Ê¢^ +î¬çg¸ç?¸ßj=k×Mýc—þØüç¿çg—«Åj}sûò¤OÊäa󔜿h|ÿ¿gÍäj¾XØžáËW‹éóá»ý—ãùÙ|yµjšƒ%/WHÚҖœL°Êõjöxºiññ‹Ç-D>úDÊ၏ï'_ êrÝN7óÕòH(í….§×2ù~µ˜Ù 6óÍ¢»ð—ÕúŸÍ?&Øíb¾lÿ¼œÝd`Ý^n¶{5ßüâ¾Ãõ/CqÁaVŽÅ§‹†Õ©ŽžC Ù.•àe•œsˆ@FâRH”„ÓWçg›7/ۃ=%Æ^¿ßL¯_N€§ŸÚõf~Ùîïðé³ßèt0py76/æ7 þ›6—«ëk»xAGàñhwûBHP—4•"|”bxHäsNA=Žþ¢ފݏ9dzòCûóæ“ƒcr³yS³e©ûi=Í_Ý Û«+d¢Ã>½¸ýÃ×óÙæ>t2™Mo^´7dޅ7ï)ËàÞp¦Yؐ§Áþ¨22꥔±k÷ÃTv$¯×ó ²±±S8‚Ì‚îb:™ìý2ß’8ŒéºÛàßV˶;å¬[ã`Uq ¤Øj)3Ç +ꢢq¸äÅ ÂØ×ϒBŽÀÕÈìëuÛþÐ'kø÷Û<þ^€LøôhrœÃÁyè¥t_½pœ_ÍÛY‡Ot[»ßJ“WRHSQü§BiTÔ= Šv++…R —ŠQàҁ’XöâcJ R–$õZ&`´¤™@´§ õScò½:ï½A§ýùåbºìp'»èîÆM +÷…›¿¿l—Z¯W¯ß½^áŸïvâPA +N‹ä©c´ÀNÉÃ[ˆïŒÄE‚¨€Ÿ®$‡0ÀA"û¢%ÇmÀ–þ:¯dl×}A+ éJ.$KN¹¡5€^]ÕHFÌØL ª( +j®#áCLÿ.Pè]N¬È £çàaJ²S®Ý?~ N¶ñTüâÔ~ßKÈ¿œ•2€×›R&+ê è¨DFœ{‚ÿ5%Ÿ£d$ÀÆåNÍÿûÕtÝ~ò^üëß§2ƒàV|0ðz§Ý̗Íå|}¹hït6¤§ B,Ò¨ç¾G$¢{º\Ð>µ'H0eï(£ß‹ßuöcÅÔùŒxWJ‰Ü_9Ãd¢¿æãSÃð·ÕPe‚ç“ )MæîÃ:ìờnvA` •”М (Åæ% ©US‘Щ¨ ÅÅÀB™͈ èхµêX2võdµxÓõÓ£-4»m6 )ÂA«YiqØÃÞ (Z»4s–€t}ìœc"êù5Ú(ˆ'ˆ[,UŽ…¡BÍJ ôÏ …èà¦ç’ ŒL=ÆL(_‡*õÁÑÂÜÖBÖb7VIh÷1û=]0†E­kß >ӊ0¶'ŸZòGnÑꊱanœ¶_ Ùd뙡u¡ÖÜ@ô‰­¹"—Úµw¤üóÕòÞÁÄ1g)}ù@1&ÉN ȚÌJÇרB ‰%{ –Ũå~àD©X•ÀÃíÜMâè À»+ ‚¡’€N Q»/#*A™ªW‘¬Œ) øÈ= wë èÈc'u_Ÿ)Ž;p¦Óq?2ŽíM +û lÈNYVä]‡<ÜS&ä'è[@¥}ÁÆÃ'e ¬u×HÝ^ïM¾Yþ놟ڌ.Sð7ÂQ;ÜY¹WUðê4«n%qËJܛO\]*ˆÓê÷ ÂÉæª +Yºñ«E%…åÁüEì«EpY0°e»¹dåBZÌñَêª;—ÝíÞ^(D§°iy·ʑCŽ\]ÿ +±l\õtAÁûÉ´¨äðøè>Û¾5ÒÜ­-–\{”uKžiïm Œ%š“†áŽÑŒ¥žn±‘d€=r`¤~w@È´0îècu1ö$:§–´2Q`4T`–¡;¹Ô”¯|Áó„]”‰ àU‚M(êî990¨¸·H-@ْë7ãø=¾ c¾>ãÈ!D&¶ÞË'°#kÚQ¦à”…·™Û"©ë%Ôxî=êN‘ŒZSP2©¸Uí@š€”Ýl,ˆ8NÀN¨jÉvÕu8QŸ+ØYç÷äM<…z{Ù{Ë!6mÄ¼Ý˜#ÌDámÛ9Üþ³Ï´U˜ÎD¢Oi](ËÇmp§`EÀç•lPeˆIÿìUtýŒ3Fÿïô± æ"ª Uaö¡­Â¶€Ú- §QúREKrÁúZÖ%Æ ÖR|W(¥Žu ÀnD­.F+@² ŽcîÞ»Ù`Pü£a#w„V»Ìre<„¡VÁÖÊ0ñ6$WDÇ!âm‚'±`Ê(¾*Žº£¦Òí.<Æ6ûÓT†7Œ‡:K8õÃ‰õm(‚+mǚ‰±?ÈBڍ: lßÍt%v¿“ñ8¼ã±8AZ4 d ÷… Ö8¢`7–––RƒcÊ㐽‘0AßDßÛ6j‹‚mÃnv&­KžØ¬ÞÇátÞ>U?¢—€åKæcA‡ Êcf£ßl1 C¤uëV-J̘ +j¢§[ åŸÐ8$œfpfë(ÕÑ\—0À=)Žm˜Ö}Þvҋ·C?…àÇæÿBNcŠŒMg¢MrÜƚ٭°–º#‹ƒ €ð$H_\‡(?ˆø¼ÑtÒ õš>2šJqöÖJ¡2D'qÁ† œr%7ˆ{ˆ´®,Q*UUgow~âƒ;bÁJ°EžzÚ ›`Üaî‡_²Q\±®ìwª;MÀwúW +£è î³ ]¿Ñ·{X} hêJß^ˆ1F[Ccsßó?oŒÆ“^ÞWŒ~ds÷èihÑ4Èè/Öޗ¾×q´7NTRv¼ϯ€'%8ӎ‰>˜õ`“P&0ˆ¡ŽƒÆš2rá`4·äñ¦ ¸94d€ŒwºÃ~ÙÖ[3Óúr}쾨dÌ”í¥¾$lŒw® +`9ñâ³öï„GÃàê ÷v:aÌx|†¸ð½5Šx,«(.ûCAH@0yÒ\g"Öi3ƒ´Äaä`͂Ðp´èÖ/ˆ'¤«Ùè„všxDÚá†Hž»ŸJ¥ØëûØÛá¦è> +õX;vŒ”0CŒåíØ^ð¬Û–etb(ïÏÑ\'ÜßÓ@|£qcÚ:¾YRÿZ öSEµçƒøu³c² þGd~»ÿµŒq²ÃcFó°ŒXªÜä]ñŒ{檥fƒ/Ðö ãí0ÅHêËiˆ9PŠ +3/ö¹é†$ùá! mÃP°Q2ô¶&™e„΍[ÏϚM{³q/gWÍËéó¶ñMó i(5Óårµéçæ¢i—³f…ˆõêùzz}~ö?;ÿ +endstream +endobj + +51 0 obj +<>>>>> +stream +xÚ3г0UHç2TÈäÒwËUpÉ2 ôL €0›«PÁÌ2TÐ5Ò35Qɹ +¥ +\“ò ë +endstream +endobj + +52 0 obj +<> +stream +xÚ]‘OkÄ Åï~ +ÛCɟƵË. 9´]šRz[²: BcÄJ¾}ÕÉn¡Bò˜ßøô1&‡æØhåhr¶“hÁÑ^iiaž+€^aPšd9•J¸­Š1v†$ÞÜ®³ƒ±ÑýDªŠ$ï¾9;»ÒÝùxú:|*ø›§$y³¬ÒÝ5´Sn ´]Œù†Ñš’º&zìKg^»h®¹<æi™1^Ìç¸õ?Vë ƒ‰IÂl:¶Ó*õ«¦Õɯš€–ÿú]×þoû“ß~—<­,žcµÉž!Qöe„ }›p´3†…ç9B^ ì♜ÇÌ·t!~˜ø}2b±Ö->KœG˜„Òp93™àŠß/_G +endstream +endobj + +53 0 obj +<> +endobj + +54 0 obj +<> +stream +xÚ]‘MkÄ †ïþ +ÛC‰I󱁐Ë. 9´]º¥ô¶$: BcÄJþ}ÕIS¨ /óŒ3¯Ñ©97JZ]ÍÄo`i/•00O‹á@;¤"qB…äv‹ÂÉÇV“ÈßÖÙÂØ¨~"UE¢7—œ­Yéáz¾|ž>$|ƒI؉^#Õ@e¥]=½-ZÁèe¤®‰€Þµ}nõK;ü3÷DŽ•qVdّÑ=ÿ¾j IˆcŒOfÝr0­€TÌ­šV·jJüËXÕõןÜõ]V{˜!JË yА#DÉË3¢Mr0Aˆ’÷±ç&öÌ:„(Å1Ìü;ß;¾;Ãcœiá[‚Þ ©`ÿ9=i_öˆíŸ +endstream +endobj + +55 0 obj +<> +endobj + +56 0 obj +<> +stream +xÚ]‘MkÄ †ïþ +»'“l>6rÙe!‡¶K·”ÞJ¢“ 4FŒ¡äßW4… +ú2Ï|¨3ìÒ\%-ew3ñXÚK% ÌÓb8ЩHœP!¹Ý¬pò±Õ„¹äÇ:[ÕO¤ª{uÎٚ•î×ÛÇå]Â7˜$:öb©zh(+íêécÑú FhDêšè]Ù§V?·#Pæ¯ùŒÓ8IËòTætw¿­hìßÅ'³n9˜V @ªÈ­šV7·jJüóŸ1«ëÿÂO.|—$ª=LÓ`m’Ç „%Â!Gˆ’—{„(90‹ƒ•áE9 ̛X3ë¢çð‘ß'û?ù)ìÝâ‹1®‘aT¡I¾=RÁ>M=iŸö½‘ð +endstream +endobj + +57 0 obj +<> +endobj + +58 0 obj +<> +endobj + +59 0 obj +<> +stream +259.521 417.906 m +257.935 417.332 256.569 416.132 256.599 414.411 c +256.635 412.353 257.791 411.113 262.822 409.268 c +270.304 406.543 272.053 402.625 272.12 398.761 c +272.234 392.252 266.891 387.916 260.678 387.682 c +260.635 390.118 l +262.64 390.783 264.346 391.737 264.389 394.048 c +264.427 396.695 261.301 397.732 259.27 398.579 c +253.94 400.712 248.985 403.02 248.87 409.613 c +248.758 415.996 253.309 420.024 259.478 420.342 c +259.521 417.906 l +270.765 411.465 m +270.781 410.499 270.632 409.447 269.414 409.425 c +268.406 409.408 268.014 410.199 267.874 411.037 c +267.358 414.136 264.698 417.282 261.41 417.939 c +261.368 420.375 l +264.014 420.379 266.856 418.832 267.318 418.84 c +268.409 418.859 268.299 420.369 269.391 420.389 c +270.693 420.411 270.634 418.982 270.639 418.646 c +270.765 411.465 l +249.081 397.519 m +249.065 398.401 249.213 399.58 250.389 399.6 c +251.229 399.615 251.828 398.953 251.967 398.242 c +252.613 394.892 255.037 390.776 258.746 390.085 c +258.788 387.649 l +255.133 387.669 254.31 389.125 252.798 389.099 c +251.454 389.075 251.604 387.692 250.555 387.673 c +249.253 387.651 249.226 389.204 249.222 389.414 c +249.081 397.519 l +f +287.488 418.394 m +285.527 417.646 285.381 416.383 285.399 415.333 c +285.779 393.581 l +285.797 392.531 285.987 391.274 287.973 390.595 c +288.016 388.159 l +279.099 388.802 274.817 395.91 274.671 404.267 c +274.525 412.624 278.557 419.877 287.446 420.83 c +287.488 418.394 l +289.863 390.628 m +291.824 391.376 291.97 392.639 291.952 393.689 c +291.572 415.441 l +291.554 416.491 291.364 417.748 289.378 418.427 c +289.336 420.863 l +298.252 420.22 302.534 413.112 302.68 404.755 c +302.826 396.399 298.794 389.145 289.905 388.192 c +289.863 390.628 l +f +318.198 389.316 m +307.112 389.123 l +306.944 389.12 305.557 389.18 305.535 390.398 c +305.525 390.985 305.896 391.37 306.439 391.548 c +307.901 392.035 307.855 392.286 307.828 393.84 c +307.444 415.844 l +307.417 417.398 307.454 417.651 305.977 418.087 c +305.428 418.246 305.043 418.617 305.033 419.205 c +305.012 420.423 306.396 420.531 306.564 420.534 c +318.448 420.741 l +318.616 420.744 320.003 420.684 320.024 419.466 c +320.035 418.879 319.663 418.494 319.12 418.316 c +317.659 417.829 317.705 417.578 317.732 416.024 c +318.198 389.316 l +320.045 391.785 m +323.439 392.306 325.319 395.279 326.133 399.2 c +326.327 400.128 326.826 400.388 327.708 400.404 c +328.674 400.421 328.772 399.624 328.786 398.826 c +328.948 389.504 l +320.088 389.349 l +320.045 391.785 l +f +344.021 421.188 m +351.159 421.396 357.208 416.545 357.901 405.719 c +358.535 395.859 351.54 389.898 344.569 389.777 c +344.527 392.212 l +347.082 392.593 347.167 394.989 347.139 396.585 c +346.827 414.474 l +346.799 416.069 346.631 418.461 344.064 418.752 c +344.021 421.188 l +342.68 389.744 m +332.013 389.558 l +331.089 389.542 330.038 389.607 330.017 390.825 c +330.007 391.413 330.378 391.797 330.921 391.975 c +332.383 392.463 332.336 392.714 332.309 394.267 c +331.926 416.272 l +331.914 416.902 331.979 418.037 331.176 418.275 c +330.331 418.555 329.531 418.667 329.513 419.716 c +329.494 420.808 330.542 420.952 331.382 420.967 c +342.132 421.155 l +342.68 389.744 l +f +191.56 374.148 m +186.591 374.062 182.49 378.02 182.404 382.99 c +181.705 422.984 l +181.619 427.954 185.578 432.053 190.548 432.139 c +414.512 436.05 l +419.482 436.136 423.582 432.178 423.668 427.208 c +424.367 387.214 l +424.453 382.244 420.494 378.145 415.524 378.059 c +191.56 374.148 l +409.058 382.81 m +414.027 382.897 417.987 386.996 417.9 391.966 c +417.368 422.42 l +417.281 427.39 413.181 431.348 408.212 431.261 c +196.865 427.571 l +191.896 427.485 187.936 423.386 188.023 418.416 c +188.555 387.962 l +188.642 382.992 192.742 379.034 197.711 379.12 c +409.058 382.81 l +f +endstream +endobj + +60 0 obj +<> +stream +xÚí]y`”Õµ?ç~3Éd’If&“Y²Î’I„Ȅ$ˆ ’ HØ£ÈdqE¢Ö¥€‚;UZ­Uq™$XhAÁµ"ZŸK µ€m5U[é†É¼ß½3BÐÖ?ޟùNÎÝ׳Ýs¿L2ÄDd¤fÒÈ=oõJ÷“© l(yŠ(qÃÂe‹– ûIõ‡D†J¢„Т+®Y8ðÅýz¢Ô«‰Ì¡¦sæýl^3Qn;úT6¡Àø @þ+ä󛖬¼ú•­¿L”‡1³¿½bé¼9tâ'ÍDÿX2çêeé¹I­úíÝ˖®XÙU@ӈVg«ü• –=»Ï1ù y@÷‘înÊBœ«Í¥\¢ÈÑ~ÚuêPßÕ‰ˆÐ{j £ÏTÀ}*œÊ¢1ͧô„î¢P6˜ß¦'(Di(?L×Sî¡«è}šù¥z„¾¤NM‘.²ÐZêâ5ô èUIïÑÚ$‚š_÷91ó@mßD¥e*ÝO:„‹#FäÛDŽ¢×Túµ6ÛPù+ïÓ½™K?ã 8¢{†Þ¢öê¨ëæÈúÈÖÈ6J¥o´œÎý‘A‘%è5i]4ÓCtÄH±7òc¬©kXKÏÓ¯Ù¯#]#Yé"´þm¦]ô+:DÿK'˜9‹¸™ßãÃzê<Ðu r~dnd)Õх4‰šQ›Ãý¸ZÌÐfhOktþ¡ëX$cO¥Õt5]Gií èCú˜5aSÅ4íiÊ¢‘4ƒæ‚š÷`MOÐt” \Á#8Ä·òSbµNë<ÓQ(8VQÿ.Ú +šþœž¥ô½‹1¿M5v±Ÿ§ñL^÷ð|/ÿœŸâgøs¡ÿ«iڍºWuŸw‰#FžÀ¼Y”MnêÎTÒàçAú3öWÌ%\Å¿~Q¢±.¥³«kpä¼ÈÚÈ+‘ÈG…h;’j±ç 4«¾†n¦=ô*ú¤·é$ýTÒØÈVÐÂÍ>¾ˆ§ð*¬âiþ’;…ü«WˆVqXókuÓuÏtîìÊèjíú²+Ù GöGÞRüŠyjÀY´ŒV(Ž=‡y^¡ãô':…98kËã±ßÍÿ( q2ˆÄS"¢Ô6ioè\ºÍ]v-éÚÜÕ©ˆL€li¤'UF@š¦Qƾ Ô|„žgÚ =Gè/ìä\ÈçóÅ\ύÜÄKy/çëøzPõ ÞÉ{øÌ:‘ 2@'¿˜'n÷ˆâ€8"Žk¤MÑêµåÚuÚ=ÚNíí:³®D7P7Aר»Fw­žôZ‚ÝðÖ·Žo—tÎí|°s׀®Ú®Å]ë»^ê:Òõi$9²7r‚h ÖØ@‹°Æ5Øÿ­t'= ùxkü=}FŸƒç-4NâL¬8Oñ­란•Oç^hâËAÿfÞÁ­üïã—ø þ5ÿ†?á/cõhÁ4±{xPìañ!à”ø—V •håÚ`m”ÖˆÝܦݎý< }¢Ð ]†nnŠn­î5½¦Ÿ¯¿_¿U@ÿºþÏ æ„Kb6âŒÁ£½%^ҍҮ í4IhڟÅoD×ˆÓü ‘Ã/a¶m’6IԈ Þ)_B¶Ä­ ž°‘9±QŽ!¶ˆRmº®@K¡•Ð73Ä­¢‘ãè´ I[­ÛÅlm«înÝ(þ€ÖbN&þ;US5ïÞ£åàP©ö¬îm9¢Þ }«_"L‘ÛtŸé…öØÁ‘,´7ywð$aµâNò!oæÄçC?„äïâéT©;¦mãÄÇ(»‚îᗰÇ=t…ØÃ?_*¡Wò$Þ¦ ¢x9¨1œ.÷’W,^Èó4úßÄÐÜÓàM¾XH:Í$æÑaÑ®¿ÃV1€o€œ.¡õ¼ŽJ¸“÷Ñ[â.Ê ´_}ëê,üm·hc©…OëÞн!té%Ps ¬GòlÄ4h¦G+€ÔT’^”@þgÁ^@qНWÐe¼Yûÿ\TÓDZ ­cøþ®Sºjm0(¶Ö¤&a¸ôA}Ž®ÿŒFAáµëºÔ 붺4–>b;_ʓu1^‰\L;ijºO"Na½†u=ÇAΏ¸yy$™'CÂ/Mx¢s‹n½îÝ*Ýõ8›NÃjÞJwӃô2N“Gqn‚Ž€š3a{.Ã1Êiv7ŠFÃ*ºIt1ìi#¬äBúZËûœÉ-8¡Æƒ—¢ßBºå+pB]G7@ÿo£ °÷Ócô®xR<¬yÄíâ±Z\FÑGÚkZˆ/¦ÃºëÖÒʧɜŽ™‡Kyè·!òfëOY°þÐRÈ}äóȑÈ㝇0ÞcXûÝ £éó„*¢‰üw]&ëC£§M U Œ^9lHÅàòAË”–ø‹ûôË÷y=î¼Üœì¬L—Óaϰ¥[-æ´TSJ²1ɐ˜ ×i‚©¤Î7¦Ñ.h ë +|cǖʼo +æô(h »Q4æì6aw£jæ>»e-öjж u·d³;HÁÒwÏ>Xës·óŒÉõHßQëkp‡;Tz‚JoRiÒ:¸ëœMµî07ºëÂcV7­«k¬Åp-ÉÆ_Íci µ“‘LF*ìð-kaÇ(V á¨Ñ"È`¢™¾Úº°ËW+WÖúÕ͙ž4¹¾®6Ëãi(- sÍ<ßÜ0ùF‡Óüª Õ¨i 5áD5û2¹Zïn)Ù·nC»™æ6úSæûæÏ™YÖæ4È9,~Ì[v\{Üy&‹Á­5õ·õ¬ÍÒÖÕ9/sËìºu·¹ÃÛ'×÷¬õȰ¡c ¯è7¦qÝL½D?ōÙÄ- õa¾SºåN䮢û[૓%—»ÃI¾Ñ¾¦u—7‚5™ëÂtÑ5žÖÌÌЮÈ1ʬs¯›Zï󄫲| sj³[l´î¢kÚ\!·ëìšÒ’³%JؖԴX"ÅÔ3± »N¥Ts™Q7eY®Èw>"ìžçÆJê}ØS¥ TÒºy•h†§Ñ+<¹,œTӸΞûýáâb)"‰5à)Ö8J凔–¬n—ù–™Ýˆ@>šÚÎiQò{<’ÁëÛC4™póäúhÞMs³Z)Tæo‹FY³/^“1MÖ4Çkº»7ú É;IzýaCA÷OšÙž^×4"ÌöÿP½ Z?~Šoüäõîºu1ڎŸzV.Z_Ù]K…Ókêµ,K‰,MÕB(gv7–™ú”°®~”PÏoO4@*U »Ç„͍c£aƒÑãùÚ#_É^*:Ó-¶ÌðÿÙùÀYù³–—²NÂubüÔëÖϪ ´nÝŸ{̺ÆusÚ#Ís}n³oÝ.x ë–Õ5Æ9ÚÙ½>+êêw¹‰BªTÈRY(3n™¡ñ Ao…ç(Ûgí +5«Z*PùyíLªÌ/cš×.¢eæèDj¢|×yíºhM(ÞZ‡2C´¬9Úº(Öڀ³¬Ù ·¬Œ>ÒjÔL­ï)JÉJьš#GueZ3|Y7½ºÌhߒ!ÊÅhq<ÑWÅ«éoº>²~äú$ëÎyÿ¶›\ÙÅÙ¢2w\Öy3³fä-ͺ" Y[²·ä>¯O[eߝ}@;`}#ûÜÃ+–L·›˜-9G¢ÎcIN™šØN¼ »nç!‡×àÀv/µíµ²µél.OñSÎv.¹Å¡ù›YË'tt˜ÿ>kyÇqªê¨ê°/ vÍƒÒøpʔñá|l¨ÕnK€Lí̲ååŠöÈ•òi YË?»}pùÐaC—ãôKLHðyiH .§Ä‚Ÿ7!QWúíãöO\úvuzªÙixêÆÿí:Êi¯¿ÍÆé®÷ï¹çp&ÿä‘×F NsY,æòéœõÆóœÐõ·×?óÔ’z/Âϳë-ð†²im¨Øë*w…\¹æ¹Vº~äJL7™ëm6¯)!%©^¯÷¦Ø³]÷edx³µWD;ßûËìSŠ‘p˜þ‚…Ru:½;c¢m®œÉkŠÁ Øq'¶MUÁª¿w˜;Ø|êpzŃò,Îð I·xäN|oÁŠ¡ƒ=_4!6]¿–Çåefæu:s33syܩܬÌ<½åû&ûWGºÕé´¦;Ä.„N¹³Ò®tÓ!ýh—†ªӆ‡”Næib–i>/ ›VòuÅWHޟ°Ïøaâ‡I~8èd £Á‡ÿºÄ Úí)ÜP²ÛÅ3¡dWYŽË•ãµg¤©¼õõT«5-՛Ñ/O櫽e¹^o^®·›ú—¥2²v{Fj™'ÙØßÃ÷è)/Ð/¡À“f`CæàJuç¦åL̙³4G—ã*¿ôÎ3r#‰¼’3¡‚¬:ᩂà˜;,ÖáÃ͝#!INî›ô½™³…¬(eàîÈ7Tùf§?ÅäæÝ‘OiPä·-…¾ÊØÓÀËgÑröû9c¨”7°|ÞBËà³28ސ˜a“òÉÅãŸZuýoVtu¾øû oIöt-•áòå2Ô~òÞæ-‡oyà°6wË%3Wºò¹®Èó] ’gŽôt‡.àDªë²»½³é®wA·v@*ghW–²]ŸÊ%I—[¯±þØzÂOÒ³½Šüy¯ûòò¼>ovVÆnñ 99J²96o–¿Ÿl1±èÂü¢¢~ù^rªMùŒúD\V[ªÙ˜ß/@þc•Ù£KÌdyÙÙYƴįEbf)ÙÜùi¾I¾fß&ßvßW¾Ÿ«¤óÎ3ò|¡ùä,ˆóɐªp¦3¾HÖX¬Žál>œ~Z¤eÛEÙז•_Áí‘c­–Ì +òûû,1ö=—nKµ[³•aXŽ x”C’-ނ¨%è¥8Ý|âÑGêÆßèJ7¦¦û*\öîå•J–äfºò~½U†ÚÜÃ÷N[™÷eÖïèªP¬±ZâÉpänXـîn*¡7Bù§³Ø”•™%5>g|Ùøžñ¸Q¿:õÖÔûRK}5ùHr‚ÃÀ‰’#:¾2”aÐé ^6ے2,if‹Õ¦w¥ôoçGB–Ü@~~b€™R<®dÛíºv~"d+)1$¹ <¯R¶9۝½,{o¶Úw¢­´øi§qÄš¿QÛÒ!‰nÙ­Ã{)@Í5¡ÔÌ,crrfR³Rò"å¿ñFZ> ÂÎqŠYl½lÁ8ùË弛_W‚]¹jù´W‡ÙLf§Éýå÷<³U–n•VG›+‰Õùîùs»M.KšÉ3aÝ*Q& ÿ)èX‰èîÕæR‘va(\d/tܪ=iÿ¹£]ì²ïtGèZûFû³ö_ُڻì†í", Í 3d8uΌ"Ñ_W”Qè¨ÔUfŒÕÍ˜®›n«Ï¨wÕ-䟦ŒEŽE®EE×é®ÎØl¿ßñ˜Ø¡{³×göT .gOE¹Ïl5»¹ÜÆ\^QcµZݞ +›ÇS!àd¦ÌczÀð܁ÌAò@~À(¨ T¡ššªÊÊ*Ÿ¯pÀ€Âª}E;Øé®y°ÊÜ©ÎbÖ§x<ö”=ÙÙnÏáÓôK!™uå¨oó=XhUí<6¤å”ÅÜ}Ž«ÖhÌ4'NîæDȪÂÃò,œpÜõ³ÃeF ׄãNpm–ex™ h‘Díņ̃ù¸,”±8“œæ<½ýmü¸†í"käÍ6Wy•µ=r¸ÍQ*ã§ÚlE2þ{›Õ'ãOÛR2þ]kVpTTf vRú0BÈgŠþæt6‡ÐÓlD7s.ú˜sMö*³·»—ê–†'z?gqšÒ*ÃÆµ"ŽÅÒ5šµ\-lpäÓP’5¹Ê’›l­B«OC㐰íŽQ£Å:ª¦:×ZÅ2¨–m©bÔ Ë2#… ÆæJ«bxŒ9îQiÊm®¬Qf›9£ªîbk,®ih3ÛFÁ/82!á "ðȀýç<Ý Í‚øØÏò˜Ïrâ༱·à,­`>KM|b;ßX`KËÌëú«TŠõ]»ºölPà—¹™ié|cדùé¨?‘çråÍç,Ι/U脬ÍçWº6&ÚMQŸœ‡w½õÏMöD8/c ªFú|_²%ªU)v´ê>øì›¡UåÅj'9­N¯ßäq á!–‰¦ãtú¿¼ÉIéãÓÇy›¸ÉruúÕÞÛÓo÷î²¼˜¾Ûûª÷oª×™”6zšO…¤Ž«ÔÑÓ@½?†ÒT±µÜj)O7šå1’k2•YL&³Å Æeys›s9w‹7N|–×ç„&; ÀÚÙJ.0`P¹×_žž$Ô¡§×oa½^°7‰)Ó¦N%Ç@;ÊÒ[º73½¼8_–.),,óæû¼Å>ozy¹Ûçµù|^ ´›ØFÖtârTX-L†\½5‰ŒÞ@V–-™ …cRB~ xPÀï/N¥ÜI¹bYî±Ü¯rµÜ̊Iz&½YïÖ/ÓÓ¥OлïV†^Ý"ŽÏZ7uy·ŸìÞ’ü©RΒcøm†~}Ì`#úOæzVÙ9Vù;«ãYsï։sÐä˜)_>˸ÿ ‚g í`¸¢ëZWn¦)Ã~R].x:_¤.'ò2Ͷ_ܬä3[]a¿­¦Œ$eÀ'Š–¨˜AO¿¿wÈ_{ÈWT 'Þ|ñeg§O’ êw4ô›ä{ÑÆ¢~S]¯fš‚‘ÎÀô‹ÂîÈÑâpI ¾åÉXÅõ´‘—ªU+²ð®ÂÌW +î¥òjžÆób—|Yƒ1eëµØ§Ä"'Ä̹IáCÈwböf…Í9Ž@7‰M Z=úI”ãlG$N%b +×b‡ò7åÏqm¦wèºÈ ¶"J‚×ÄQ†ôhµ…6‰) "GäÈ0Šñ‡× V¶VÏ÷¥¿ÿ‹â õ)†(>~¦Þ US;v)°¿‡9 ëNWP ~íAàËø2z²!i§\œJQJ­éÆÅÝÅ4 +tÞÓ_D¬½ UœžÍ1zÆi¥çµÝ´Œc?È»äéj~+$n-ƒVÊò8¢ò¤Û±ú´K¦,a€|ìa…"ßb?Ց¿Sqä0ýUiêÌø¾ÒÒPCêè=XÇ|ÈͬafÈ¡ jçÑ\pm=ï¡é¬£1|1­§6‘I©¦©4Žë°ö7±îéàa­â"¤î®R’¼°KÉñòþº +7ôùjÒZŒ£úÈiº’ŠW¡…+Š®b-VQ¢ÖÑ@ýI¼›é¶c½›@»ë W3ې¸šSúß”–ä1¬ÿ*ìs!`X[ Ù4é¿Ò¿éwô3z…ž¢·h;¸| j÷Ò?è¾ +íï‹tD:Ðî-ÐKâ; +â#/â=ƽE)Gìc<„ºÓS¢†7p#çókü–oªù~ø ?|“?æy>,Û7¼–§ò06p"Òýü5ãø]þ›¸-àìý{S^JqEý?Ê;x _„²m<—!{ýT“dJP-ÍX‡|6òR·H}îʨâ'a)¿¢€_¡ÕCÐV"ít´ü¾™ßÇÊç7Ñ>|ðwÇñôÿõoc#ËUÙ åFú5(ô$¿ÀÿTëTÆéØþøuþQ÷^ãe±½ž?ē%*HLˆÒ¦;îý¤Äè‹9“³{ÆqÚBz¨x'ô]Öh¹Š[¹U•wAªeþoX«|°µ—'iµÊ/‚ŽÞD?¥m°$@á·!4‡.=>†l˜ €³ÈMzðáMÀûàÆÍ¨•³l£müg>ŧ ß‹ù9þ†ÿÀb¨†ÞTSCÉø/üF| Txs}¿ám:ȗóJ¬ð ½€5!Ë?†Zè/ö¯Ñƒ°·ò,À¯/ðƒ|ô µ»© %塘TÈݞ¨§¿ÑGüOðK~VDÚSØM¬a oæükÞ;ø +$wû¡N¾”kµ5ôºêÿ0¿È?çýü<À¯ HA¤€=óg`4Z»ÏÏŠ=ώï°Jò̈Ÿ?{Ÿ=qžò;¢(× çøž>\Çÿ¶öÙ;zµÂŀ¹è/q$»?l«<ïFcÍ ò°/á±¼0VÁUJ‹¤$Æ¥±—ýÐø{µí¿háwâÀ­=4ôû°·æþ >Gcÿ[,5:Žz€òËcV3¦åçÄqkú_ânëð=qÜZü·¸›ž°*ð:ÿ¦Òˆ¯wóõû0 Z³¦1þ¯Ž«qv+'nõ8Uöñvèð +ț‘ÿ"l°4ûx¿Å+OÓ eþÂûzs!NuXòVE= 'ý6z>nçz"Æó×»EXEÖp'ý‹MÊy@ù*ðƒ¬·Éð>t@éEÛQ[ªP¶ØÿX–4ÓsÐÔ+1m3î#Ц?(ïn¬`J¥g„vÙѯMyvà;Ý Ë*ýå ´l$ZIOù§ +>†7r2w7•âNó-ÀÂ0b=èk"@~~šËeÝ~`Üç”3ÇmÀOid%ÚWÖ±émö¶=Q³û,TbÜĽû€¨O{+}¦VEj|ÑYöGږ&Üንv9Rò>w¡:á›è6ÀÀzm§©Ï§½_RzÈ{p«´€r1ê@‹ qÊÜE+ì…>Ax'àîYޕŸØÂJÛÁy'¬F®7³ ô4$l§úœhüÄÛÔvÑÿÀ³kV5ÆÌíN=Û¤°„K¹PJÄiÈðpkãN‘*Rqß +©[àµt­Šå„AœS/ȳ@µØ¬ 9NåÁ<x‡âö‡w yw«‚î8ˆÞï#sôÓ\j¬èŸMîUö?¿›«9=r4Õ÷AyšFcìý$úùIÎåý‚0ß XgF7È~ª÷1bô|»œŸ)P!ry°ƒ‡³N¼ *p ‰î|¼YRŸ#/ÇY-y½|x`Neɹ¨¬¬­wá&²_ÝÙo„Ô¼ R;Ñoý ²S„üèùýðˇ+ûi‘7.XÀþòÓ¢ˆ¯FæàF!gÊw%æÂ¿Ñlô³a§²÷ZŒ¹T +“0©wEw:-TšÛ* ¡›ÔÉå€ß/oäFèÑtè·¼Ám„ÝMÈSL[%ñD÷yçÃ}bq d 'åÁ k‘Ô>©8ùT9Ï~ÐAÎ/1®7Âã*VÄQŽ$0ÖJh†;’Z=vШôÕ¦è„uÁÏ~œ?vx]÷óùZ{©ûµðË_|=Yø%uüÂíhÿ ïÖFD¾æ—Ñ« yü[¾.f-â6,jÇvțþ9ø]žÈð›gnµg£ôP¤‘Ö'Ž=ßHtB*â‡Ðó]BOlS¶²´Ûõ|ÏÐãïz¿è‰fÈŒÄøYz,¥•Š¿§8 ý+Q¶ {Û z<‘¬H÷€³<€õ½ W?aâàÒý +½^ÅI¹½«È>Û"ÔÙÔ(²;(òEäbÀ €¬H¢\»Z#Ö"?Q®Æ®îå«þÛÿÛ^~ÈÜ=@j¼»[ £CAÈe±E «Ó½ئ¨+_WÊ÷¨‹ÖtSàM€ŒçdOx4°nE=Ö3(Š`¶@Vã|§Xû6‚NÊw¸ÏþŒZ`'€">ÈЁ´°çóqØÓ¸!ÈVN‘GJi÷~DùAÂ&zžztVJž^òov@Ú|œ¯¨ÿÝ xŒ.Ɗœ8…ä‰Õ^aÔmFn1êr`s~OGpû¶°ÖØ¡nç ቟f¦¯á)Yy<_ÀCÙÇÉô[¥åýFýEÉ@ØëA ¶¼6<‹ 6€±.€|ŸBÏê„gîÆ)7 vށ2Y2H–ôÊMð«nå»ùô…{á¯D&|ûø½6þŒ ìV.Nüø:¹ðNJ%€6mTÕÝJþ­ÕZiAáùž°(Ô Í=\­­²x;Zù”—%a3?*ì"7ˆzúwÁOÕ­â dá#¬óÿëÑó®ó+{ß¿¿×«{ê½âø}¼÷½üÏ:î‰÷¾mν_!”'úVœw öº]ð9 ~æqHßÅ4á àhZ÷[òR%‹-¥h?<¹<¨Ä؉êýc zo€t ç4܂ñ|€Oa’È«sáé(üÐwé}”Û ;6žÊ*éË鸭Ÿâå +*¸FJ ;¨ü‡HßðTž‹kq*ô²2) +)QèmÙXèY.=ö¡Űåiê,’ÄTÄiHI¾CÁõÆ.nÛå9Œ“›§G^¢—À_è.ö.uu%Ú/ƒoR¯|íÅêïŠöªS z»½Ž_åc\¢´?ˆ¹P3¯¾E竹 ¶ôj@3÷ÉլN•Uô)Òx™ D)ÿ°ð¹‚ Ùø5þ%·£×B~§önÌ|þÍ5”D) ÷œâŽs +ç7ðÎÞ|Ä'ùcœú"|NXá]þ»·ÜvË +ä¦»ÎÆpXéËîWï8~++ƒlÏ+ÇošçA£G‘ƒ'Ñø.ÅT*üˆÿÁøÝ7¹Þ:ð=sófHßlèûvº6£¥·÷<͇.%Ár¤"N'oá %£f±âŽ|{ô RTþ¾§VÄ;bŝà.X°%t +þÀû1›µçë'ˆàEêÍñ#òý§|ƒ üø/¾KÁþ#š/ãeô8} hM\”u> Þ1EÃ^DyŽ)羝;ç­\Ï;¯|nÄLòM£xÿ5?Â/óS 9y:ÂöN¾ÅÛøwü;ÁQà-ðt÷ó£ê+ßîžû^ó5œðW°šê½1m‡/¸ú³÷NN¤gQ!Õâ¾s7dö|èÓëðñ\ð–2Àƞ‚•ZpC†” eËéúBýN¤‰¶ðKÿ§<Š_€ /]¿—jp¯š¨ôu9¼óº‚spG¾3N£ßÂ#¾}ÿù=ý5£ÁÁ¿âÔ¿ÚfƒÅZ ·Ñœ€·`õòÍ ¼·ƒ ‘`¶©³cXäÍnKÞ¦ìu÷o1c6w™(Vºðnè»#‘˜Í¯Q^|j Ò°J+vþ2õ=?ì=ù¨³Q8¿µÃQÔ/ŒbÂ3gÐÉ7½EdՇ}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}¨‰R¯Õ~DçÓ&J Af*£‹‰´?ë§«ÿ+ýËeêò©ïAìýri7MìÓöµNjG4BEm©ùåÍ2N6©¸5ipUu™¶–Ÿêh6µ±òVeéFU¿]ÛCaà>à;@Y²%»Q²%»QR¥µkÏk¿lÍÏÃÔ;Û\ùå_Vgjm +í.m=y0ö¥±xv,ވ¸ñ¦X|‡¶¾5—V„<ӗ#@½mk=obù.•T‰­ñ’­m(É«viÛ°ªmXÕ6¬jVõ%Bƨ[Q¾å[Q¾U•o•ß‚¡<ýcCÅÛZÓì±$ªZƒv1•cˆúX<]»¸µÓ´e¦é¾™¦ú™¦ gšÆÌ4•Í4µóܐÃoúØoÚä7]ì7 õ›†øMƒý¦þ~Sµ…x:™èW*­ÂrzU˜ÃÓ[M”ô_B4€ wznÌ;ái×qkÞ͞v¢›¢¹K¢Q@þ2o gQ^I´¤ å{^ÔašÆOQ"ûC%‰o$ÎN %OXšX”X˜èKÌK´¬³!Րb0 †ƒÎ d°µGŽ…üò±%˜e” “¡N¥Íò?ÉW$ÁAã(œ®㧌æñá}óhü\wøïS|ílœ<#¬÷æ°u<Ÿ:Úæߞ¹(\éNštI} ó ȅÅííLSëÛ9"‹nɒß|¹‹˜Kn¹#+74È>õ-:¾ã޲¯®rVYGY†©ýŽ 1öøÎgÏ/p+É ß?~J}øÉœ†p¹LDrƃrò‹2w‰J1´®v—&£†ú]ÆfQYw‘,76×6œiGn”×î"ŒT;rËväîÕ.W “íúÉ(Ú.WµË=«]ËHO]m‹Ço3Rµyv›Eg·Y¤Ú,ŠµÑ¢m<=Ú$#jãIû҂ƹóšd|­¬ç«m¡kë¦Ö·\ZPÛ:.4®Î7§¶¡í¼9ÅOŸ5ݏãÓµÏùŽÁæÈÁŠå\ç=ýÕOËêóä\O˹ž–s:OÍ¥¤bi Ñ 53£q›H6B€³< £íæe£”4<βv눧dC8Å7:lʪÒêÒjY-“U©òËdcUΞ¬Ýüx¬ÊŒb‹o49ë.«Åϊ±ÄüY!Ÿ•—®¸TÅêgÅÊU@õ­&+hÅJªS”U΃}Ê2K‹,­¶¶bEÃÊ藟¬XEr¼•283|wjFæg}gʊޏ” ?EíXÅê›Uˆ Žü§~ Cr‘±QþÂa©¤ +endstream +endobj + +61 0 obj +<> +stream +xÚí¼ xUÖ|nUuõ’tÒÙW蝄¥Y-Cš„Mv ‚HU@ÐÁÅ("AqcpWÔ±"t>˜qqÁ™QGqßutÜHýï¹UšÅù¾ùŸÿyþ'ÝyëÜåÜýÜsϽu;$ˆÈC ¤R`Þ«í{ëj„>bÚ¶Ðøˆã™*HA­ˆî£‡ÄBzˆöÑÄHõ0í¡ú#eP9ÝBÑ ´žtš+i2¾„ß ²ŒêC·£_n§À;ÖÐ^J™ÆÇt ­Sÿ‚TëÈK]h8M¤e´IœiœO3é-m- ¤3é\Z.Œjãjã:ã.º›ö¨4ŽQeÓ<|_0>süÕxƒz!ōt½%®s?Ba”ÒÎ[é<Ú¦Î҄qŽñjG¿D4G/ˆýJ¹×Ӈ"S\¤Ž@.wãIpåÒ,Z@Ûh¯ F*yŽ™Æ8ãJG«‘ëMÔL»ñm¥ßÑk"Þñ…q—ñeQOö´Ð‹b¿ÚvìÒ¶Rô˜½Ô#fý=C/‰ ø½²Ìï(r„¿2^¦TêGÓPÛ{‘òñoe ¾—¨Ok•F% _®åÞ¦§èm‘-úˆ ¢Jé®,SnSÏ#Jì‡o-DoEîoŠØ­Ä+Õ;µ´ïõNm‡ŒH!ÝL·Òï…- ˆ•â2ñŠxW¡ÌVnVÞQoÐî×þ윃VŸMKi=@ÿÉb˜$Î ÄEb½¸VÜ$^/‰”áÊTe±ò¹º@]¡þN+ÃwжR[ë¸Âq•þQ[uۓmjû·Qd\A“ —¢ö7ÒmhÙ:HÃ÷-zG8DœHÀ7 òÄ4ñk|׈MâqŸ¸_´ ”—Ä;âcñ¥øJ|¯¾º’£ä)]ð *ç)¿TnPnQâû’òå[5Cí¢†Ôj‰Z£.C­Ö«›ñ}D}[ËÖjú¹È±Å±ÝqŸãÇ_èñÎË\äzþ‡;õ8öfµmhÛÒÖÜÖb¼MiÃlô‚ŸJPû9ø.ÂxoÄ=Lñè»lÑC g¢gf‹Eb…Xž¼\lw˺ÿV<Ž^zU|Ž:{•\YçÞÊ¥L™€ïÙJ½²BÙ¬\§´(¯(ß©N5NMTÓÔêHu–Z¯®R/T·¨õyõïê;ê×êøšGók]´B-¤Ôfkçk·ij:f:žs¼¯{ô¥úz«þOçÎaΉÎIÎYÎkœ»/»j!OÐ#ô(E}ÄaõRµB}„®Vе,åEåEÈólªSÇ)Tå>±A¹X´(ùŽÕúPe¨O_h…è맕íÊ×ÊPuœ+¦Ð"¥Ÿ™›žªí)ў #ÚãhۋÈyµ/Ö(ŸëñÔ,HŒ2ŸRûj!õ9zM}K8µÛéuÍ#2Äå^u"¤àwÚ0G5å©·ÐoÕâbzD©€ +ýÞµr<^ì„^˜*ŠÄ7ªAª2R4P}—ÖÒbå¯tóxýFÔiçÐÕT,.¢é̊îŽsõzšxVY¨5*)¢…í~´n°Èª#•.³ÔmúçÊßè|:¨yèMõAÔþ ò[uœö…c²X€p1]A+ŒKéBGµögq©¢Š +´ÃÐn©EZè%Ð*3¡Óvcvï…®ŽCH&$çLÈÅ4hˆmøn…žÐ A 1ǧC‹½H-úT¥•Îq$h"í¹¶É4ø‡n2Ρsë¨ôÁzã"äx½O×Ð}b]Û¯i9uÆÌySœé¨T:*^J£ò7eвåÄñEoˆLúßßÂ3Ìñ5j¯Ò*56‡ ÝÝ ao¢¹4†ÞC+?C £ÔýTÜ6^i2*Õåhï[4ɸ×ð -0–Ðzœîv:hŽ3„1Žˆ?£½¿¦ze²±J­o[ˆ~¸½Foýs¥¶B[«}K1ç·@ßìÀ¼Ù‰™ÃsŸÂg­[µò¼˗»tÉâE œ3¿~î¬êéUÓ¦N?<\:ì%C‡ 4p@ÿâ¢~}ûôîÕ3Ô£{·®…ùÁ.yçN¹9ÙY™éi©)ÉI¾Äo|œÇírêMUõ¬VÖ"…µ­08jT/öç `NT@m$€ Êy"ZÉ8‘3 Îù1œa“3ÜÎ)|*éÕ3P D^(ZŌIÕpo*Ö"G¤{œto–n/ÜyyH¨È\PˆˆÚ@E¤ò‚µåÈ®)Î3"8¢ÞÓ«'5yâàŒƒ+’\Þ$2† éP2*†4)äò¢R‘ì`yE$+XÎ5ˆ¨sê"'UW”çäåÕôê#æçF(XI I!‹‰è#"NYL`!·†® +4õÜ߸±ÕGskCñuÁº93«#êœ.#)„rË#¿z/ó¸™'¨^›£6Vd. °·±q} ²cRutl?kjÒ*•µ•(z#:qì”JSÖÕTGÄ:à–p«ÌöÕ+8¤vQ â–4.ªÅÐd7Fhò…yÍÙÙá=ÆaÊ®4N­æEJs‚5sÊs›R©qò…»²Â¬czõlò%™۔h9â½ÑŽúö8é’ìì;¹½g×(8 Ì  &ÕA´i?êQã¼A`çF U¤#²0âQÛèÂáœ>â(ð_$ xä'†Ì±BôßWÄN–“vQC¼íŽ„B‘=XDœ#0¦¨ã0éЫç­J0¸ÜA÷ÑDô휚!}Ðýyy<ÀWµ†i.<‘†IÕ¦?@ssš)Ü'TQj9f¿“6cì˜öäµAHr ±É›q¶ÿ%úÒS* ‰ˆôŸˆ®7ãÇN Ž4£:PÑXkõíØ©'øÌøAíq–+’2¢ZÍQ,—’£ÊXåÌvföTÇG´üéR¨ëZ.H¥ Êˆ¯v”ù¬ñäåýÌD­ÆœJ’ãɬjF†„Nô=ÁBõâUTËëØ©3='ÄAÔÌG[OS«ó#"4 3³­ÆþAŒšœH]6‚ få=1Çr×àÃÒÙ«g%]cce0PÙXÛ8§Õh˜ ø‚{”?(h\^Qk N«±÷ªœHåÆôÕ1¤WÏ Ç46Ö5‘Z€bÂ9MB:ޏª&2!TŒÌ ó‚ÕõhKÓŠÏ›Z;.…Êš‚b䦰Ø0eFõv%¦V7+BQ[VӔ¸ê=,2TáPdO€=4V kš—äÏÙ&j±š þy­‚d˜Ë4¯U1Ã|fA…² 0 Ëy­š¶¹5„¹Ì°“»›ÅíBŒcöV’‘æ§ ž©ÕaÏÀððÐð0¥TApP3Bö‚w¨ ]ÃD©ÈiBž“ep«hhÎÙ#sšlq6€“ÃÚÃPsf‹Êå™ Ÿv¼ÓfTïFÈ_>ÁQÆÖ´¨Dô’Љå|z¨:^i;ȑžA9ž¨è'Œˆ`dvpu·.R¼0ÁHÚLM42·¦±1€o½2¯ªÚ|r”虋œj" smޜ\ÈÄqo<’J¹Ú•Ë:¤½´_Û¥‡ÒØÑh™wÊÒPûˆ8‹ŸòOV¿é +šåc•6 mœÙ8ò˜éÄ[õ€7!·F怚l•5rqš›`>Ï¥+9¨Éà˜&e|HR!iã˜`E8Xt`°òu5ÌäIÂÿ£L"Љ™y£o¨í–Ïœ¾‘sNô.h÷V2`£ô6ÕÚ"§l^dQNdIM¨e·¹s{Oð!2ñHF-–‘‘†ysPE¬7£ç0ê¹fòBÝȖӼ9Hƽl•97tB–Ð * +qs" µ5Zè1 ˆ8@óa>ç°Þ˜h¶g"”?ȜÆ)HK‘¦&Äù¡9âœIäIËUã2‡{ŒoÈK^¦*ʄ;žâE¸¥*³XÐcƗGq쥂¤Äá¾(ÖDã¨É*Ý_8¿*±Àç{)Iø’ÂIµI Iš?§Ló‡½^eZR²Ï‡g«q4œ”˜—ž€g¦Œk5¾kœ>-)ÁçÓÙÿYK|¼t|ÓâõÂñ˜]»ÝUI«’]^o+×,9>>Þt$Äŵrdr¾Óg…9}’+<´jŸó ó-§áÔüÎRç§êìÌõrfÆÇãٙkàŒçҝñ\–3›‹vfuî?134Þw4$?³V„BãŽÀq,tü3kE‰Ã|ÇB%ï…BTz¤´„‘48)yp¿¾4K¬˜E+ršÔ´VµOسD ä‰Ë·eIœ3‘(³´4TZœ<¸4Ô·_MÞ=Ø¥°p@ÿä3Š‹Ò3’Š“DjzqÑú»èê ú'/9tþ¢—×Öné³ëXàÁó/¸û¾_¯¾ýŠÛ6~çv¡6N®$|W©$?à÷O¿öü“Xi¬ñ‘ÖY†j'%]J\†ŸrӔiê,Ç,÷´¸zu±c™»>ΕÖj¼gv5áÉìê”ËÏ®És|—úu¶Ö/yHV¿ÜáÉ㲇çNJž™59wNòÒì9¹«õÕi_+_gú(]$z32&¦×¦/OWÓs7ûvøŸOËÉõ8i¯²“„±¿…Å@Àn +Ë¡ö !nLÉÕâ2 a_´KU†-UÒmIÞ®ªŒ°·Õx£…GÌË2Âõƒã),^ÎÔݵGÿˆWx³ýðí*(ìÏôÑÎÁþ}ýŸþ˜ñƒ™ÕîªôbŸËŸ)ÏKbsÁŽ¥ ##;¡`s’HÒ¤]•)mªVãá8iW¥²üÀÿQ8ƒe(I‘ÖU¼´®tV^ˆû.ÆÆj³veڋb¦­[3͹öVelÎ9²¤œö’rdIðNâ’r4.)ÇÃ%!´-ÇyçÄs™ðÿ ËÌAQ»I)څm´xjU°@¼Db3í ÅO¥4ª‰³ë$5´OjhŸÔÎñ\,¥[:ú[G §J%-g%p-(+¿ U¬Þ•dz%4þh(z¾˜+³/*öÑ(E~l|E}ù+Î#Vߘ[ã|G|G’2óK6yB|jJaj|RŽHö¦å‚]j¯à?:ñŸ'ÛC¡ÇÁPì»–¡[Õ;ÃÙ¼D—‹vQô|i©©æ‹Óΐæ!?Ғ‚Iý ¥²—.8àZ{Ñ=‹.ø́Ûvî +ζü†–êº3/¢Þ8~öÜê½ï>ÖU¹uÉì!7Þuì7JóêÕ·]{ìo¼”Ã^ìŠyí¥,aðÌޝ–É]—Q’R’È2UÏ®,‘ìôdŏÔG¹ªô×9úB—«¿oHòô™¾±ÉcÓ+2g:fº'ûf%ÏJŸœ¹Ô±Ô]ç[š¼4½.ó—"Í­;¼g©SS=gÅ/Qëõž%ñžŒ\͙™O­FR£ö©ö´ +ûªRósäþ"Gî5œÐ’æþÂ)wNŸúE‹´ÎØ!M3v°xH‡4Ù¤Iš_п¯SÓç Àì{ eZ梳ß[}æ‰ccî[dlë0Á”æððª„|ŠO`{"YÊi¼”Ó\)§ÒJ´ÄQNGJ—FÑ,ç +Å[ù’/K¸ÜÜP¿l6(¥žŸÕ.>Ò®XšõuhÖñÀeU–ÖçMȈ™Õa÷Ç÷\Ç\·&fՐ½¦¸$s/§eH#Q³ŒDVþ¾1JK•öaJÔN¤ü®+Ÿz]¤ÿúÓ«Þj;²§yýͻ֭oVRD׫/h{ûØ Ÿ^&: ïóÏ=ÿ§§ž;€Õ"ßøRéḠ6à¹rµœ ã¢Ü®(·3Ê­G¹=0肅ýÝ<ùp4daý÷z„Jé>w(Ñ£§cw›èëB]„÷ø^ÑÖ5ÉSÅt©J.ˆ†ÓUᮨu.w687;5ÂÀïpFœû/9u'ï1X½:YgK©‚QØÂÈijMË!w¬Py@Y¤ÂqR¸tK²Ì©ãÜ«,¢LqFÓüèµÊçè{¾#%¬~J|ï-‘»ÈcØC&N*.ö=Ë˶­)šTXùE-¼‘$8Âî%Âãõ&%xܰ aê—èN §ÎVSÜiÞlß1$îRquœ«Oòt­ÆYWð±Õ³5îQ¥5þqž÷½¦rÿÉûºï}Orr“.ëOÉI‰™^ Î*,]‰:)^òx:ÛvI!Ó^Ê Ï×uÕér»…®»š +JôaڊÄD¯½ïV¼qj¼Ï£'*‰ßÓô´[ñ;•È­*Þ§±-ˆW±6ª7ÖqØõ^o|vŽ\“àuƒ_•Î9¬m<ðÑw÷+Yáíß&Ý +â{"Ê«t¶¶’Ò€ÑÎNôKGU‹õ4CÙI1ÔNÖ¤óÀ»þá {9-ø§o%@m…æSØÞ=œy,ç|$]I3\~Zæ¨2Ž¡¼-Žgh>pÜwhïÒ}ú`Z +ÿ]H·O#Ȩ;ªn±XƒÑj15€.r€IÊ ´T;“úë&Çû¤2 yÜOo¿Ðêh<üõœâh¡mìÆI¬4Ži·Ðõ( Bܯô-hGú»ð5õQþA½ôºòUŽü/nCžIy¨£©(¿7h±ö¾”¡+€(ës»Ÿ¸oà¿ã:eýÀ3é§#1. À®ÊïÃ}Îã.ªÚƒ÷=ðÌd ŽgЇ¨›”Y³ ·Éñ4çÌíV^\Nžþ -¶Çyò|a™E]šì¼yN±ÌØTÊ÷b)÷Ÿq;Y¦Ú)æžö)ä:È9Ù²)Ï;ԙçÃp$}Ö²ÌrýlÊý²&ûs¢%Qmí+ç¨J´d}­Mí¾h§ è.äY«Ï…NÙA£´U4J½–æj_P¹Úz;ú" íoDù”&»öS1Ærü7ÅЭ ç!±È±í|ýyˆnEŸ®Ð)]´CÂáxÀøØAâYÇÊé>‰ÆBì7ã˜2¢ãþÓðÿ ”W@g>`|â8dhÏu<'œŸŠ¾@À¦o€®ØêZ,ZØ8êDGeZ˜†8Â4PۏñIƒžÇ\@ø4ÇÛ´O݄±>düM4Pƒ‚<œi4Gٝ†²”Wh-ƒó]%G'È\¬,ÙԖ×XÊ:ߒ)?¨Žù÷¢…÷,| |9 ™Ìⵁõ³\ £+,y]Ô.ŸÏÒÝ WÙò#§‹bä3>V.c©\[ ßíyв®´ÛÏú‘uëHÖs¬glþX•¾QÙ 9f=üͰæu cPÇw¬¹=Œñžnz¥q¯Þbܧ&÷éEpÿp÷¢/V·¯©ÕF›µžv·×R3œâìuÔQLK-}v—Ô7_Ò r­’õsëÓ%Žï1îЁ²¾;¬9ˆþD½kµèóm´íÈR×c>"˜É}"ǂ(“×^ÕÑϼm¢µêë°8m1%Éõ¢”¦£îÏÊ0¬©L9Ì1îÐ?¥"mtí~ªã±âvp}xì]ç“ו=qˆúi÷ƒ'<àÛ!û L÷J¹à´‹‰¸/œóÈ ™Îïv™&LÉVÜ%ûB¦‡-Â2Ì}<õ4š,í‰Oi»cMǺÝÙ@·cãJ˜÷!»‘n ×é²åz}#…ùµºitIùŸa|¯>€ö¬†^ÔôÑ”éh@.–m/×L»žçº“ +YFô¡‡Ùž¸‘µUè‹iÂ69 'QîU»ó7„¹{%Òû-½M(ûJ„sÚR¶eØFàùâ SŠÞ í’u`;å«ÓíêÚ9îºý°ŽzÑÏúZ÷Ea`/ª>ºt RLF qpóºG»”jUT¤öÃÜM¢^ڟ0W¿¥›ÕDš­ ›µVÚÈ~-…º©´¿¶%‡¤‰®üþ­4C+Aú t®6›VªM½—É£ÍÇX#ãjÈI>Ò‰|-ˆwi†Z…¹uÜßbŸ,£ÅÍÐFQ/™. +²®6bꬌE«Æ`LQ_vŸP_Ôµ½žvOQ?ÙNÎé˜G»™JÐOo&m›¤l¢€Êk4BGŠûŒ½èäÊŒŠökÄE@om= +\ +wOÐÿ6ý°ÝÐëÀ:ä½tï JÁa·[çì¸hp9§ +†#ÇØ{‚ÿ¬5€8jìeÄò£ŸÏ@ygh¿0ö2 ‹cú%”ê¼€RÕ®ïŒt1~GæÓ#”¯’ñïÓÕ駀O¿¨~ G·ÑÐôŸ7¢h€©µ6Ðÿ¥~ÿ`|“€¾²?£4S†(E¼b¼ +Z%^¡$õ|È oøSìþ´Ç á×Ëð˜ñSʌ6îóØðX츞ίì¢ÙѰå ]®£a ­ü@¬ßõ, cèO!ýÚ½§Á ê¡nã:A»žì×'PW†’ºfsÌ9 Ý:`^™ÞK#9‹Öƒ.ù¡pôÐTÐW†Õ»­;à…; aç€ÞJôýWpŸ‡ðC& EË¡–]™…°ÝVZ—•ß3ý÷$úî(ð°™þûÀ"¸ÿ `=ÿþï ¿Ý +þOîrÐ?˜ñÇfÃð8üŸÂ¿¨†{3hhO HFú- ¶GNڇþ×é©÷?—Âf™‡zúùÌ ô¢Ø=ÄϦöxž†Æî5ìñ?:3ˆ¡f?`Ïôì¾HôÞç§ö86Åx¶EC›fƒMÏv4Û²l?KûÑ¢rÿ&íX”K”jS¶Ù~eۙíWÐÛ噁CÖgïóe½¬u#Z·Š£tàr,º<ß*]¡{!ß_aot~Èßp·bíJÄZ·z÷+Ðàïú•½¦Ùºõ${š5í¿íÿO×ÈÿŚZdav ~,ÜÆ £±kñŠÓ­ÝÿëµüGÖèèuúÿê·×yîaTÄp†½ŒX»ô$;à4þÓÙ¹ÿ©?Öîøý1v‰íÅIñ±²gÛ3ٔݎ˜y÷Ÿ‚÷Ú#Çm»±ó¸}¾Y~ôQE4 ºYkèÀ¿ 3:X£Œëà_ãúŠ\Qü¬‹F)PÇq gˆM|¾mƒÿ2ø}Ú ’·ÚBÝéä9VnÙ>—ö!úLêÁÍ\ê ’&`©=Ö¼‡Dه¬º¼ÏÕf_i/16àiéZ<"ü‰ÐÅ©zôv˜îåóxP¨ú}Òñ3>ã˜þ+É3Fž-¯¢QÐóçj‡øìËxRžéµQ¢3^¾GY‹5ÔoŸÓÁŸÆgCΟ—­Öù\­þ%ÖÁéXݼv Ü*ùNh±Æç¸_Ò j•[gÈ©öY2ŸOñz¥÷&Ÿ<Lj>G~¶ñL*J­÷TÓøüE}_¾«YÏçîêxzÜz¿ñì¤ÛÜÏÐm®:ªt]"ß7mQo¡µ»Åy5Ý¢‡äû•iöºÊkâ)Îþø,3»ýLÓjs¬M ë7“Îäó˜èrít®J¬¥_Ês(óó4¶ ÖøF Î|_a|}êóNãyëÜsµÆ_оæÇžÓϤIêìûì3Ù{@_¡³µ+«cëb—…~9öc¶m›À=]žõ™ï{ø *%ê=\¥ìçåxæ1sx1‡yü=Öû¹2m5øÊÒ>̳ÇõÖ{»,`ºò7ð߆9z.æ +dP»^¾Ã»Üx{dº%æ{3} +PŠzÍGºüîÈ­;ã=m5JÈs5ã%ÕØzžòœ|ǘh½ ÌÒ6ÒTy¦yü`¦ÖMž[wÓ¦àBøóeÛ-*û*Œt‰4Z¶‘Ïæz!Î¥µÎH-^ç£Té C^ã¨Ò±‹òÕe°_öC×åbìÆ`\i­úuÖÑ<5‰ê¢ÒxQ| + +K¡|‚p¾u-üüî÷U:Û~¯fžOÓ÷`+Ö»\F=CÙ)ò¬÷„5–»“éFØ`Ú-a籓î‰øŒw€ï•PvÕ)­(cê‚rTæ_ f®…nV9#µé˜c'bD,–iŸX œiA,¬ðìX œiY,^vŠzüߏÕãÇ cðÂÿB=~,ß`,ü‰úÂÇþõø±~ΏÂó¢ãcðñ±õ€~Â>¶íiìMý«µÞ z&(¤¯íI>Çæ[þ¿Z|¿°ÿ5n°W6Ê,@ç¼^úûjcÒq´= škž‘Ûå×=€*³,NÛö˜Y¶„UfÛ.3ý±‡@ÿãO>0˓e³îÝ ¶YíÛ`•1ëÞvýqþ¶\³2]ä8 ˜Œô~Ð)ÇÑöˆ ã Ðß|.úŒU/vw¶úƒÛü(çu\/ÐwÚ6èŒZ"¬Õ©Î&Õ~MgJ{ð„µj¹Ô‡ïÒ}RßÐ}%T¤{a‡ÜJel7°wÔKþ«uX›ö li/&‡öe9Þ§ÙÚ¹T®î†]<úeÈ÷2țõ6Ûê•4ï*å;!~w²šÖ{Z¤ýâOªö!ê{íÞmƒ£šÒëÎÞðoƺ~;­vüš~åZJûô/ø)ÍÇzå×gÓ`Çe4ÊÞÛêKÉ툇]`Q×Všçì‰ðÐ> \÷zØu/ÑDôÙ@»ìöw÷NJEø=æùŠ”?à‡p¦¬3ê ;LÃÞ:Õ¾7à˜…>©“õ/ß9ÝOöèäøk÷hêætÃöêCܙ´CÿíÐa§†ä{ùùVß÷å÷OÎs¨Ÿc=Ú{wý=ôóTòؔßÇÙç°Ýn×H{1Y¾×²ÎÚ©¿ok |W"Ö®±í¨v›Â:#h?s°ÛÊëg{û-eo˜g +ûaŸ¦QˆßãÉ3‘XjÕI¾ÇÛY²ìYç>ãTAï¡ùú4Å1ý’BSœOP²s$e²}ætJ»n)¯ÑŽoa‹N¡BŒÍ{ +c‘ù^̨±æ8Ÿ¹½ +ÌÄd<Û +ã³ +Œ¹‡ðiVZÄç›û ÉÃïÏ-÷ u&§=öw‹ÿ¡¨³š·LÈ}H ÚNµîR]q=þîžå§ò´ôgž¡ñæ;U§xÇK¯]`ûa罅9zÒݶ£c©õ¾I¥mÈôn‹Þɲƶ^,½¿òc÷Y~Ž5ç™MO¼÷bÓ³-ZØ~/ç44úžÌqj–?áçžÝYgnÙ6=ÅýóLî8ÕOÚ?ES9&¤Zv,Ûïcä{~¾›óh¿ÃudàDT1ø>Á© c%a8—œËÎÿQè× àòÇÂøu¾Ô„q³…O-ÜÁPöҀvm,ŒIœú~]¹~+Ê\½L8Ÿ5!íÿŸú€œXI]ɒê¼þ$`e0œŸ[¸Ê†a0ì~·ûÑî´í´{A{íò­|ÿ¯ãø—ÿV»ªîѰîèٔïî駬7ÆGâ_&ä]š”bAG¿><°p=s%›ï*©õ§zy_±=ÍIr° {S†å·îßè:,;g¦9øî ª9Uÿ8ëMùsv5ûIÞÛ1m¯÷ѯuÇv¾¥ûòÝév랬Ÿu Ö]žç}µßÓüm>cйŸ6îÀ:é’cU*Ïw:~ð…ñGÇ%°”u¹…g-ì0m?ãaë¤.ïï¤û£½mgó˜ë¤q·eo³{ž‰¶Íðãõ²u¯ú Úñ=eÉû¥a¹¿ž¨-Ğ~!e©Ÿ"ö¿oRçÐp^3Ô3`[ñ›ÕÖ}Y>{xԄý2Q½/j~óý¾WÈ;9UªkAûÀŽnìS‰6ƒ’0¦^`ÆúEkÿÀû¦ƒz˸ þ'Ý °ß“[Ôñ-tü‚z9ŽÁ>xrp˜J_Ó͎Rê¦OÄ:ö }s‰ï˻ćŒí³oz5¥¹Ÿ¢‘Câû6Uà‹¡½Óäzdþ_Bì¶è3OyڜkÒÎu–ÓZÌãJ`”uï{¾ù~ 6(æžfÞSí¦ÝM“bî¡ÚÐ[χ)Ð íg¯LùN˖e ²ù ògÞ×¢.ÙÆ^e"u¶ÒžeîK >¯¾à3Ë[¢Þ?maüý~+ö=ԏ½/:Ý݌ÓÝÕ8Éÿ¾S‰½»qº»§õǼs9Ýû2È*ÛȕXWöé;Cð? +\ ýzC#Ð磦½v¥‡¹½ +{Ðєo‰ò9igè¯ÎÚFy¦…™¥@7•™góÆÖïäy*ŸÍ±]ªfÊßAd[¿kèfý.a”ý»‰ösÚþ4u-ëT¹fðÝnìÓ oêX·(ÏR±òƒ©ƒÄ! b]$Ï%ËPÇ2I¥[éaé”2r+ÅhËõ&ÔDãY©“L¥òke}†õ×ÔWÔlS)/›:Hy<6ŽŸð»ÞOË=5ïÍî—kÓw¦ž”ºÏ!ᖿG1÷O‰<ùw0§³—,Ûòú˜MOgZi°ÒœÌo½»ÁZ’"×äg¨;ßímßwË»ÑÈýÊ(ij rÜηÏÛå8aŒÌwû"v_Àïsxlí=½ynÖörmB®Ó܏Â.ó`Ý=S–'ß÷¬4ŽZõäýIäôªö½Ÿ½—³÷DCµÛè.õØB}ùN’\ïÚßÞŐwHž¥»å]fP„½¾Qæº!א§€—€?Ÿ¯˜çTÇþÆ¿â~ißmçûm{o ¿ž&·ëLÊÒ÷šöŠÚ@çñ¹8ƒWÀ¿²±“ïÕÈ»PC­{„¼¯/·(t.–z~¥|¿1SM†}0rRI¿€¿Ü¿Ð.†­ÞU¾§ªÒ.”¿‰™¦f¡Žÿ¾ªHþ¾j øúÈû½S´_Ó4ÇS´Èñšçø†îq¡{@oQênþ~B;*yŸ»b½âÁ~m%ÇúÛg×EÖü'çí +¬iWÒVí Ä}º paëÿ´U|B[Օ'ð¨Ë{Ó[µ€öCüR‹¾Ž°¥Ð>ðý®ÕK¯ÎYF.m @~{*虙ÈcÒô“å|ˆ5ñ Ú,ëp*p–Yu² >1Ž¢NWƒî^³ë Yhp=bóŽÆ‡V}bÊcp_DƒûEû’z£ü-ÀQ§aÀzLjû+\×v|ub½eÚྌ÷­«ŸOî÷hÈv/9>í@ð˜È±°d@ý-Êf7·›y¾0ëÈ2 ed)öøC&ϔõþ@Öw«V@‹dÝPŽ£ºc¾`žÉíyšòtµLÇ|ˆ“cÈuã~~˜ºË:<#ek —ËñܟúQJÔwƒç5”‘žy”esÞW˜õ“iB‡!/}*âýX«ÞG#݌“õ·ÚÕ^w®;òtxͺÖ܊9z¦Þ yuÿE°+YF¦OS¥þ°«d5H[¡ºDÿ^ È°Âø·a£@±ågÚEÎ㟠žï?ßH[NÖ1(Š Óҍ£ý¬?€ñÊBл¥Ûyº|XG±~:°ŽÝkë¯Ø2X—1`$´ëµhì éQý/ûžïBkGèV†›æ­s¼Kë”BèõBä[H=ÎÀ< /t²Ð݊+´ü. [üªLˆç5À؛ðgIÙöƌ2°1n> këÙ6`,ìÄ'Ä+ÆlЏA×ýØ]—óÇÞ¥‰½sºzd“ÆÞkj1;È8¬m1>ÒÞ3>r΄Mø9@Shpœüãm=å!"Z ¬d[ϟ{ïÿ綛ï J›âsÏÅûxùÎàAËþXI3°/åýþø;9KÉzeêãé6ÇÿÐzçýäÖ_k¿Ãr…k#y)”éNÀ:û¢õŽû|ǝ°¿–ɳÒù[b¶¿»Ð>µds/ôÊ*ØR5XWn&Üò~ð Ø0›ù7¢ŸÕ”³íÄïèÙnµ~ÛÌ¿a^¨—Ðθ*ã®1Fr\<AÎÊOس¾BЏOÞå¯4Ã([™„}Ø}Ô=*l”E»[Ô?Wғî[ß*½i“¼wyö +ûä>žm‘DØÐ) ­«ño÷ùOAk |†ú>Æ0Ê}ÚýbÌýÓÞÁ?͝ûÓΑG1÷öü=ì¥JÈz¥t?ûᏠ ß«è.WAwkƒéng=Ý ™¾2{3th‰ãj”é¶Ñͺi£k;8nÂÜâ¼¶Òz}:ø>B|'«,èKÇHØ:Ká^L µò̤‰Ž‹iŽ=Ÿž…úö£ÝòÊYƅb›q»â'¿xÍhÑr©L¿Ÿ.ƒ]¹^»vôý Ksh”ú9(ÂÓ­8¸±'\¯?ÿtø—šñ°W*¥{5] ÿeâƝÚRãIõ쏯YFfkke.ï2ý«ÜT†¾\/ýçßhËў¡-rîïQ¶R—BKŽ—¨Òuˆ.“xɤqiH·’r]ÇC7.vˆ³ézûL$öîàIgeHw= ±× ¾S ÿwÂ3Æ~m±q§çN"×ÕÐ'Ó {ցZû9áúØ8•”¢ë&´±ØS~I¥úYüŸÎ:>?ýQ°ïՎ¢·‡ëÀ‰ˆ¿ÃDÂ7'Ã×|"R +;Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@þ+D¾MÊ#TBÈI +ù(LW92ÿ üªõ_/ºšÿ·4öÓ4uÝð8µ'•.ԉüjHíÌüjf½“¿Uí¶«0ÓÿÒãjw: (j÷æP'ÿµ«Ú©y¨?ܪw%§%ï¥P•>òÀsð0°Ðh¶Úá>U#¡*ª :òûUÑìM*îQ åsJ&¿ò™rČQŽìJH*Ú>|Œò= ìTå|ßVÞ¦K”ÃÜçx–ہ}ÀAàs@Wãû¾o*oR¢òwꔳíÀ>àsÀ©üOŸò‹š|²»P”7ðô)¯£Y¯ã™¨¼×kÊk¨Ú_š.Ú#¡>–Ã_`92r,GrzQ«òçæo»C¢ +1Ґ¨ÇÔ.4ŒŠÕ.Íý ~™Í% ý­Ê»»!ÿŽá}•—)(¨ÉË(ùe +Z`9 Ãõ +\¯P°ØDHž>  ž^¡¾@˜¸”—šQL«r°¹°Ì?<]yQy†2Ðã/(”ôyåiIŸSž’ôYÐΠ”§›;ûixâ i|ü_m@û Þ¡ü~W~²ßž¤ìCßùñ씀ÙÀ5€®ìSº4×ù“‘ÉctÀEàl¦%½‡îpQx‘?\8àGá_À…ÇöÀöB%\¸å&xùQxõupñ£ðòpñ£ðW—ÂŏÂ%ÀŏºEpñ£pÆl¸øQ8a*\x´*·=šßÕ?pÂbž¨ü½ôKôÒ/ÑK¿$Mù%é[ëvssè±máP÷þ†½¢áqÑ0Y4Ü!êEÃÑp©h( g‹†hÈ ECX4<&¡+D¸åïàp¦h8  +EC¡h( ù¢! †[•¼æÑŒTH²k8O:Ð_ ƒöITòУyù<è„}x é ƒ)ÐÅdÎêÌ´Ë®¥¦¿÷¢e˜>O á†'è-@Ã=1z™< ñ,fûÏÐÁÝ¿F>ñ씳K€Ï]Vçs@¡eV–ãJ÷±*>Д'ðí‚ož’îäËõ…|£ÔkrEbg1¡³ÑYHééPÙÉI®¤VáÝýoï7ÿö’{¸[¹Z¹†U·²Ù¢×4 Õ-¶6>æž&~C5HžL…¢t­”þ”ëbڟr•@‹šs«,±¹°§¯HàT»ýßæ¾çÿ8·Uó£ÜÇü¯Z5Ñì?„vû_νÒÿlŸVB/l {’uOî ÿC$륈ØÖì_Ãd·ÿâܑþŹ2¢ÞŒ8{%|áDÿäÂþQȯ·› +$_Û-ù4Á|M+ó+ʛòó%OF€VJž•hžà)(<é t@òHo`žÈ0ɒ› –ι’EdS®dÉْ¥ê8K‹åÊv–+eIª8ΓkòxÛ<ÞÃà ýÜO}Y($v ­™7³¢>XQ¬¨j#W]° 3Ò07hšWÁˆZX;wÞ¦sê#5Áúòȼ`y ièÌSDÏäè¡Áò&šY1µºif¸¾¼yhxhEpNyÍ®‘û<¡¬+ÛËê?ñ™MäÌúsY#ž"z Gä²rY¹¬‘ᑲ,’¢>±ºÉEe5#fšt—çØÖæäՔ¥û–“2<4/sMÎ^˜.÷Q\¨&,‹xŽê5¼×pŽÂÔâ¨'ZQ™k†æåì÷YQ>'Ë(´êü•çSfÅÂróo%>Zu>w¸ù ­ü±â*"á9å+Wô˜26R:iFu“Ó‰ÐZnRdˆWÑjì7{#pªj;#‡•p˜Ûm1ž<þç[tςå±]"ÜY¬¢•5j¤óØ© +4ÂÔhëÌÕ{aXñZ±² \)Bb¥‡UíPˆL?q›m¬:ßrY}±Ê¢fJ$YiwIû‡;+ÔÞc«d¶²;C3«‡'¨g¨}h8lç¾ ½@{©}ÂɅ~Uèw»úã<å~§^î·s­ Ñÿ1˜Ú +endstream +endobj + +62 0 obj +<> +stream +xÚí½ x”Õõ|î»Í’Ìd²¯0& ˁ„-É’°É!A°Ê* hµu‰"‚—j¥¸TpWÔ: Ú?´î+¶ÛZZw-•Z¤u!ï÷;÷}'ûÿ÷{¾çùžÌä÷ž»œ»Ÿ{î¹÷½$ˆÈM¤R`áëì{÷:„«Õ˜•äE¿ü”{›ž¡¿ˆQ,&‹Z¥·²Z¹C=œ(q ¾‹hú{rG„Än%Q9 Þ­=¤}ctk?dz1"Etý‚~-n¯kºý·íÿ2KÌ«h*äárÔþfº-ÛCèOø¾KºH^|"_Ì?Á÷Rq­¸K< ­(å5ñWñ‰øB|)¾Q_CÉUò•ø•ó”)?SnWàûšò7å+5Sí¡†ÔÁj¹Z¯®F­6©7àû˜ú-G; ™èç}«¾]@Hÿ~ÄHt\á$çËßÞ}¼ÏñwÚ©}sûÖö–öVó/”Ž1ÌA/ø©µŸïrŒ÷VHÜ£ô{‘ˆ¾Ë}ÄHq&zfžX.֊ ѓWŠ[޲î¿O¢—þ >G=Jž¬se°R©LÆ÷le±²V¹A¹QiUÞP¾Vj‚𤦫}Ô1ê\u±º^½HݪFԗշտªÇÔoñ55·æ×zhEZH£ÍÓÎ×îÐ>Ò>Òçè/énc•q•ÑfüÃ1Ä1Ò1Å1Õ1×q½c·ãug¤ó)zŒ§˜8¤^®V«ÑuJ©–­¼ª¼ +yžG‹Ô‰ +$Uy@lV.­J~¡1B!&Ñ­}ý¬²]9¦ŒP'Š b:-WZ¹iÚNrí):¬=‰¶½Šœ/4Å¥ÊçF"µRÊPæ3ê-¤¾Doªï +‡v'ýYs‹LqX¹_)ø•6R¯£|õvú¥ºV\B)ÕP¡ß8·@Ž'‰Ð 3D‰ø·j’ªL‚ Uߣ ´Bù#Æ<ÞL?‹´sè:*ÓGtfEoý\£‘.^P–iMJªh%E{­+BÕÓèJ1W½Õø\ùO47½£>ŒÚP~©NԎèÓÄR̀Kè*Zk^NéuÚïÄ9¤ŠZ*ÔA»]¬–hù —A«ÌNۍٽz`”:!Yœ3!3¡!nÅwô„ Z†9> ZìUj5f(mtŽîÐ:DÚKíÓh¶yÝbžCçš7R?èƒMæÅÈñú€®§ÄÆöŸÐꎙóŽ8S¯Qè5f?¥Iù“2]Ùzâø¢· E}Šï/á©?AMÚh:U˜[̃î^а·ÐOG cÕýTÚ>Ii6kÔ5hï»4Õ¼ßô 7-5WÒdz’îuè4ßÂGÄïÐޟÐbeš¹^]ܾ ýp=z!ŒÞ:úçjm­¶Aûж`Îo…¾Ùy³3‡ç>…ÏÚ¸~Ýyk׬>wÕÊ˗-=gÉâsëfÕΜ1yÒ¨pÅÈ3ÊG /6tð Ò’Šû÷ëêÓ»WϢ‚`ü€¿{·¼Üœì¬ÌŒô´Ô”d_’ד˜àv9†®©Š ¾ÕÁš†@¤¨!¢ǎíÇþà|̏ hˆTs"O$Ð Ù'r†Á¹$Ž3lq†;8…/PNåýúªƒÈ+UÁ@›˜=µîk«‚õÈaéž(Ý7H·îü|$Tg-­ +DDC :RsÁÒ¦ê†*dלà½ØÝ¯/5»àL€+’\Ó,2G +éP2«‡7+äô R‘œ`Uu$;XÅ5ˆ¨…ÕóE¦L­«®ÊÍϯï×7"F/ .ˆP°2’’,4Z1FG²˜À2n ]hiK›4„͟SQç×sÉ!”[ÉüñûY^dž2ºnSll®ÚTµ,ÀÞ¦¦MÈŽ©u±±ùü¬¯GH«Ö44Õ è-èÄ Ó(MÙX_Qd€[­²Ú·8XÍ! ËW°2¸´iy†&§)BÓ.ÊoÉÉ ï1QNu iF]0?R‘¬Ÿ_•לFMÓ.ڕdŸÓ¯o³/ÙêØfo’íHôÄ:wÄI—dgׄi=+¸FÁqˆH`a5© ¢MÃø±x5-6|êREaD–E\£š|Ã9œÓGôB_0Ðô%A‚‡ÿvbÈ|;Ä(ô}Iìd9é5ÄGݑP(Ò§‹ˆc4Æu)ýƒûõ½ M ×ø è>š‚¾_?¼ݟŸÏ|M[˜ÀiœZgù´ ·…ÂÅ¡úˆÒÀ1û£1é39¦1ӑ¼!In%6yÓ#΢Ž¿$_FjõÒá‘ñ=ы­ø Ӄ¦Î® T75Ø};aÆ >+~XGœíФޮSsÛ¥äª2B9§ƒ™=u‰­†êEm'¤R†ˆ@MÄ×0ÖzÖ»óó`¢6ó§’¤3™]ÍÈðЉþ'øO¨^b“Š +cy0cvS“û„8ˆšUà8›@âiF]~`t„fbfâ¯ÍÜ?ŒQŸ £ËF3äÏ +²½'0æÚîz|X:ûõ­¢kjª jššæ·™ ‚_°iòå7Mkª¢‚Ófî½&7R³¥}µT ï×7È1MM‹šI-D1áÜf!CG_S™ªF„‚ùÁºÅhKópJ̟Ñ0.…*›ƒbóÔæ°Ø<}vÝv%›gÔµ(BÝPYß\€¸º=,2TáPdO€=4A kZ§äÏÝ&j”±š þ…m‚d˜3&ha›b…ù¬‚ŠdAa– Û4+&åÖæ´Â-î^6·1>ŽÙKXqHFZŸfxfԅÝCÃÃÃ#Â#• +=ÂA-Ù Þ‚v"·yN“Ám¢±yD8wÌišÍÙNkìC͙-&#”g5|fg fήÛ5’¿|‚£’?¬iQ‰Ø9$Ëù¬P]¢Ò4a:$#ÝÃrÝ1ÑNÁȼà…ùܺHmð¢|#hk05Ә¼ú¦¦¾AôÊÂÚ:ëÉQ¢orª4.ˆòææA&:½‰H*åjW됎Ò~-í<”ÆŽ¦hq‘…§, µˆ³ø)ÿdõ›‡PÐ*«´UhӜ¦ÙÇüH7.Ø®¼Þ¼z™j²MÖDÈÅi!l‚%<—¬ä &ƒã›•I!I…¤MãƒÕ‹ÀÁÀ¢;ƒ•XTÏ\Až4,øßÉ$b˜x!‘™7ùFD}ÂöYÓ·)rΉޥÞl”Âþ–š@[ä”͏,ύ¬¬u°Ìç67anç >\&ÃhÀ²3&Ò¸p>ªˆõfÜ Æ# P·ÀêA^¨›ØrZ8ɸ—í’"ç†NÈ:A@E!#nN¤qJ ¡>Ð"¦¢³s4°æSp>ë)V{¦@ùƒÌošŽ´ÄÖq@Ÿ-™¿8ÈÊ5Âònõ>×QCíhz]„r›š‚!T±°ÌȾ(bc‚¿5¡àüÅlÙ-aÃn±er º²w8·Üê`~=X”Bٗè8L´üXØÄvã܆z"¹)¥)Pք ?ºJ+ZXÛ½ðjr¨çç‡NǾzdd1º +™éå_QdU¨y®£°3Dþ­YÌN™«4""S¢,ùÇÚPDɆHn¼˜6[® (î<½pº7 ©ÊåԘE3ìeÃJ?Ž“æFÌJ†úèyo.›§ÄjÂ9‘” ÓÎÊEÇöCØxóc-O‰]ÃPµGdc¨.Ü×åqõÉöäôéíéÓ§Ì3$}hîð>ãúÌõÌí³Ü³¬OÀ&ÏU½o͸-çAOz¯6óãքcfO8ÂÙìº/{g¯ÝÙOôz:û@¯ß¥¿ÝËY•!º·™GÃɉ‰ÆÌ”~ê‰üÜf +Of—?ӟêÛgP™VÖwœ6¶o­³>´Ä¹,tAâ¦Ä¿ò|J:È+4_qÁ Ì’ü´¬y½W÷Vzç{+¼×{·{M¯¾Ýû¨÷s¯ê}Âüš(A„¯õ&&%)3½mæ§­>Ÿt §ù|ÆLo¢Çƒ§‘”„g‘ÇÓ&­µÞ¬ÄD8«õzóÔÌ6e箬¾  {k³úºÝ•3³nNËËsPG[¨º§»$OMè=ß7ŸFùÌcVÁTKdþ›)ÑvKò Ÿ„Z2<e&æ´™“•bG8C ´ÄDé*GeÏÂñV8«] + ÿ·­\|A›rVØÛ3LE¾¢@р¢G‹ô2,þ­^¯2³¨Í|Ãr8mpÞ,e†{ZÚô¼s”Eúb×´†¼ýþ×õƒ©ogúAÚ癟eÐíßôgøý¡œòŒòœ 9kü7øý•OÿŒáÊ`Ï¥ÚS“6.o–»ÖsŽç㣌¯ÅQ¯O¤«Þ_,€G2¹Ó1%²F¹1¤Üa +dE§CkmV© 'Ì/¬™ÒZK…ÉI˜9¬I×èÌIŠò… j“ +}¾×’…/9œÜÜ˜¬ùÃ,Jþ0 Wr +Ϥd9/Yä’ –üä,×f~V\²—Åþ¿ËÙÇ¿[Y“ŸˆÖnwmòú”¨à§D?Åüݵ)ŸæðI®ðˆÚ}ŽŽw¦Có;*“ª£;×Ë!ÅÝѝkàâî*ǑÃE;²»š’šä;ÙPh"‹üñPì$(÷q˜ïx¨ü}H.Ľœ‘Ì¢ÍB †87«éذÒ+¼^r'äÂÛº2Á‘D`HoiJYE‚›?˜’›2‚š yR:bùU‡-~ú²ƒç/}CÃÖâ]ÇŸÁ½üäÂ;¯ºcË7wojÓÔQŠ÷ë%ååýì›/?Í7+Gw¬鐸 )q™~ÊKWfªsõ¹®™ ‹Õúj×âg:ë4ÙÕp„§±«[?{¦üIÿ:íXŽ60exöÀ¼Q)sFåMM™“=-o~ʪœùy¦SŽeù(C$y23§d4d¬ÉP3ò’nðíð)>Ÿ–›çvÐ^e' h>Á*NµOqsjž– ;Ò!U™Q©’îG•cfØEÛÊ#æaáúyxéàóp¦®ž}E<“ã‡oWaÑ ¦³2õ ÆQ5¿»6£Ôç´Åçµõ£Ï’«pj­¯À.è3(*/Q1ca†P­##ByR„¼R„ò¤ðdHA‚ !ÈKh"‹Ïûƒ8[ËaR¨ 9Ç¡úÞ¯8 -9·üøÚrÁʑ…H̕ºQ¬=/7܍h +­¡Fºô¶c?½FŒ"_˘g¥||Jªêsk©RÌ´w®Ô—n©/+B)eóΞ[J.-ž»B'2Y]R²JK(9͑ŸÁ"'ò‹¤ÒTÏÞÛ÷ï{>iÿ\¤½uPxÅ·»[6.ÜrüMejâ°Ú«/~PÔfÞÝ*üB‰¢Wû;í_ùî]*n¾jôÒû{¾Tˆa£þ{ÊgKùëžæIÙÅÙ²ÃÙk²oK¼Ýó Ç™ãéå‰dïÏÖ²yý9þAݜ51)Ï-ҕPZª¦äޞ&ÒÌTk°¯M k™ÑËŒ*„Lk¤°Šgj¤*7 +A,‡ båùÝ@";Ì*(;ì +¢4–HêÅ!ԃ•õ•€TJ<¾”Æ#K–‰"¶ò"Ç׏Kãî¬ì'Å^ʧcÂMY¡Ð±XåÐw´ÜW.ÅáÐá¹TQQ^^~š¢,<ú"<ɆËa8±Vû\)¹”l$劐õ¹ür‚ +9/÷1rg¤ª +ÖÀ֕ªáNjSK0 ÂÐҒ +ŒcirppéàAC‡@wd:xìÒÓKӃÉ-Û·§æl¸àÌ9¹ÃJ¦U8 ÞºeíŠA5³R~á®iX°åÛ%¬'6c¨Ê±2©äPÜ8¾ »Yþ´IÎ1X«:ͳ¹kσ|±œUIMÂ`]rÞÚÐw}r×ܺAaÁ‹RÎ+,S©ƒKÓU,C›[[[µÏø&]+úæMÌê —¡,/âò“¥e”ç»e#N:XO1âq#“ëIãøx­.‡KçÉ>tØ I ¶è€íQ(i¸0=sP’î×·ëïêÚd<Žèª__£7ꦮ¡õnE-´ÔÆP[m¤c^m'±ŸŽ`ªP÷i6ÖánRsÈa#9l¶ÖpÚ*Ã38L¹XQÇàÑ$íÄÁãÑcšÇ‡Œ}'Õ.rËq²&~iò†V}ï×5¬o7©bÅÏJ}›ª«Fªò€¯Í÷žúQêõXªÉ9î‘àt‘Oló½–u(ËÌÒÎ4oZFJžîF†Çíñ&zO0ú¼1K²7j†ój½Yaî…,iì%ôbwB÷Fo›’Y—&È~Iè!9Ø”Æ^‚ÜçÁÿo®àr³V…ÿXXn@Â¥C™  “²xr É:’¥¬ÉڑÉڟ¥e©JizFTýgD„ŒèŠ!GòXkr²5N–yҀi¶šÿš-T¸9<š=nûÃ)¨Î>G +À¤Lß±¹1Jn{Ê¡ëyG4÷ęÆÚ‹aJVN.)–æÏ0’]n§ÛáV _lâ\‘äNÉåíÁ'ˆCnØåqg¸…¡ê)mꀖ•ºµó©ˆú{_“LT$Uò¦»Î»áÎ)>wkŸc×ݯýüÑê5K.9¾N¹êÜU£n|ùø“æ*؇=!-Ê&ËËîô,np*Ÿ°#‰Ï ³+[F¤8ÜىcŒ±ÎZ£ÞyޱÌéäž2Ã}VâJu±¾Ø½2ѝ™§9’óÒ n"–³ÇH‹ŠaØW›V+÷¹RÜЊÖ~Â!wŸzDnË¥CšbìàA•i¢I´ pЇ ‡Ï€™×± ß]ëøn®Èež6áöF…Ì•-¯½Û…9@‰^¶R¤å(…*O +•´ +Iî£)Qê‚ )Vaí§ + A¢/Eó¥D{3Cs؀”ªaî ’óqî±Ðܹ'Êï?Ceð¦côœº°kº>ݵ@_àÒÄÜz’z£9!ÙÚ{$h™Ò(Ôl£•½o(äˆÒÓ¤=˜³ó¨ºçêgþ,2~òÙ5ï¶ÞӲ骖]7µ(©¢çu´ÿåø+Ÿ]!º ÏË/½üÛg^zMÚÔ¾LˇT¥PwqPj¡õ‰¾~¾3||ZE PüÞ‰Án%é%Ý*»­ ÜpÏž;>s|n½ó¬Ä9™sr—;W$.ó­Ê\‘»?ðû´·³ÞÎù}÷÷ÓÞï~(`2‚ZÈJ¬ ÷Õhã}³}$|֭ݗìŦ#Ï`ý•çM oö •#PÙ•W›]ðš[øÜawƒ»Ñ­¤X¤ˆ¹aè…X¸ÜY¶ÿëVZ7ï=x\Ý|þÃbåæY’Äë^/RK•ÒÎ-jT%Ù{ÕpvmJ!Ñ~!n;DDš_TˆÉ°ŸyIËˆðqqÂÇe )ÑBîOk2–!ɚÁ yH'RX¾D¶ÌÐ,»á–ÀD륣ïûŽŸxxƒõå°TFö^¼Ø¬¶’7ÙË{‰ÇWz rŒ<-+¨Ê •IÀ•ŽÔ:éi +oZ{&«1³éžá7.ÝüÚòóßýÉìëû'ßwÁ…Ý¿~]sû2ýWMS§n1·ÝÝþÍ5g?þzÏ+O¿tð¥ÿ½Z`~¡ôÑoÁNá\–™QJ4Û;Æ,!ÆíŒq;bÜFŒÛõ;X4ÈÅs¸ŽÆlXi‰·P)Ãç +%¹!%jB’¯õžS —Û®®Da:œÕ®êÇG£ã‡FP;Ç~ÇkÃÁÒÀˊÒéøBì9¬ó Û!÷ŠÖd)".¸ [Y +×±WYNYbHó’8£xØZa|ï-—g ÇËyð’KK}/°¥5šUŒ_I+7a×Jáöx’½n—J·ÁCYZRRlOüÂLëØ7ÉC“yӐƛ@ŗsfù‚•}¯¼r×c¥†zu¿s»oä⻔…[„ceûµ[Žß4±oÛI°9þ¡‘O ñÈ=IžÎÓ#Þ]ÿWg ŠÀ=جÈ7véI"ÁÐ6@7¹“²¸“ŠYD+*’ù€0÷ñ¤‘Ô#»Ìh3ß OÉ.›´UÛê¼Å{kÒ~}¿±ßñR’+)œQ–£¦ºÒ=9¾ÁbxÂåâºgqÊ,­ÞQŸPçý¹ØæÞ–ð¸Ò–ø|‹ޗ}oª]¿õüÙ÷;%¥ÙõHH¤”ä¤,Èà)íeW’AЇÜnÅàÕ»œMéeU熗†êpº\Â0\º¦B ’|Pö")ÉãCï»O‚šèsIJ’Û÷,=ëR|…äJ#r©ŠçYð&ªi‰‰ªÛåRUìþ<žÄDrON)ã<—&öp'Í7\—†Ým"÷ñ°1ÅhÄ|lSF‡½õR¥Çdtý¸ä‹ŸfA™{ôpNöñ¹Çs²û>ð=ü!¶™°7|åÖs“Þ?4wÓ%OoꟚ{‰ïiâê'%mr>½Éë{Úz‚8¼¾òrgy=¤ H«7«[Y÷wB·²Ä™e*Àþ–ü2Ï+wz™è‘_æ +ç•Eå´^š,蟹õRɲû0¨<¸l±b@Q6.B”ffd:® ÚS$‰+ÛoùËÝýóúîúCûOÅ5o¿9¼ý¥—hÿj̀ÊÒoڏ¿*Æ×·Ï%y›ÁøpÉòOӃó’Ê¿tæ:å­«»ÞëهéoñÞñ¯=~ŽœSáu_Xײˆ#Û'Ñh}ýè×?ö‘Þññ]kØA|gÎFDù­­£t`œ£ýH¯¥:±‰f+;éb†ÚÂÚÃtxwÂ? +t/§ÿLà] ¨r찉À|`:ûÁ»‡Ó"5œ¤ëh¶ÓO«õZó8ÊÛª?GK€;à¾K{0Êhü÷ Ý>h(ó ÍVc'mCøíˆ_ˆ°;@ëà¿î9H7Àv»×R6SÀ@xoäsÝޞê¯iˆ¶Îü ÚR<ÇW¡Œ) 5Àð¤‚V›Äs´Yçqµíeà}d~ª4M´çFžŽÏÄ ¥o>ÎØæ\!Ú3ÉgVka®‡i¨¶ã“=¹€ð™ú_hŸz-Æú ù'ÑH +òp¤Ó|e+tÊRÞ  ÎtMŒ sñ²¥Qy§¬óm™òƒ˜¯Úx߯1àKÈÑÈd6¯ ¬Ÿåú \eËëòù|î½&*Ÿqrºãå2žÊµú=:OQÖÕÑö³~dÇ:’õë™(<Iߤ섳~…fÛ󺇍ñ¨ã_í¹=ŒñžešFy¿Ñj> ¦˜%pÿÐÍûÑv¬©uf»½žöŽ®¥V8%D×Q½”VÙúì©o¾ ŸÉu´VÖÏeyœéÐi ö xÒÉ ¾²Ât¿” N»‚ˆû±ÙIàáüî”i”b÷Ç=²/dzØ",ÃÜÈÓH§iҞøŒ¶ë3iæÐŽFºÓ˜‰9—N {‘n<×éräz}3…ùµºi3tIùŸm~£>„ö\½¨è£‡(KoD®m¯Ò,»‰çº“ŠXFŒ›¡‡Ùž¸™š´U+èZ„]«CO¢Ükv%æos÷j¤÷Ûz›PöÕç´l˰ÀóŦT£QÚ$ëÀv +ÊW?¡;Õñ´r<Êy3úa#õ£ô1¶Ó|UõÑ% C•RúJH€›×Ð=Úå´L«¥u æn2õÓ~‹¹úݦ&Ñ<íEºMk£-ì×R©—Aû[a[røšÂáÊïàßF³µr¤ßLçjóhÚ Ù{ÜÚŒ5Òé×AN +þ äkC¼G³ÕZÌ­«àþ +ë ød­æ8†6–úÉt1u"®ÎÊ´j<Æõe÷ õE];ê­ã)ê'ÛÉù"óh·Q9úé- Ð¢íS•ké!`‡ò&V'ÒEâs/:¹&ccýÚ`q1Ð_L—ÃÝô€G-?l·Áôg`#òÞº‹÷ ¥’†0EØÀ6à¥h\,¸œS…ÇBÏ5÷žà k Žš{ñüèç!(oˆv†¹—YÏ0.£4Ç”¦öDxw¤‹ó빘OQJæ¿NW§ï>cú1ÛÆèx€fü¼CLíµþ/õûß㛠ýûwJ·dˆRÅæ@kÅ”¬žàïj´?£ã„ð›dxÜø)•f;÷y|x¼?~\OçWvѼXDå Cn¤‘ ­ü@¼ßùdÏ î™“ýÚý§Álê£ÞÊu‚ ö<ÙoL¦ž ¥uÍá4˜s@‡ÿtÀ¼2½‡Æ0xî2”Vì׀ŽøÁTÍèìWÂýªÞjÅGÇ':.ñãƒú Ô^¥Q =A‡ƒN¥±s6~ÞÆ‡EuÉ©xâæÆÀïÊóÿOÀÜyxxöÿí²AV`¼;¤väAØ'gñéãÐ%ß÷AÍý°z·÷uƒº¡ß§vžñ™ÇKžñòly=…ž?W;Èg_æÓòL¯’‰ò=ʬ¡þè9üé|6äðy‰ÙfŸÏ5_`œ…õÐÅkÊ­•ï„Vh|ŽûýLM *û 9-z–ÌçS¼^ýÉ'Ï1bϑ߃m<‡ª€ +û=ÕL>Q?ïj6ñ¹»:‰ž´ßoEÜ;é×st‡sÕ8/“ï›¶ª·Ó„Ýn7BòýÊÌèºÊkâ)Îþø,3§ãLÓns¼M ë7‡Îäó˜Ør£éœ5XK¿çPÖ9æil¬ñMÀ"ë}…yìÔçæËö¹çR{¿ c͏?§ŸCSÕK±ï‹žÉÞú­]Ø}_—hYè—ãße Em¸gɳ>ë}ŸA¥Æ¼‡«‘ýü‰¯qΟ|h•'ËfÝ»4Üj·o³]nĪ{ûMüíyVeºH'L˜†ô~Ðéĥùè/>}ή»»ÛýÁm~œóêÔ ôµv+tFÖê4ÇN‹j?¡3¥Î=pÂZµFêÃ÷è©ïLè¾r*1<°C~A•l7°×KþkôEX›ö li/"]{†²õhžv.U©»a¾Eò½ òf½Í6‡z5Mä»JùNˆß\H›Ü­Ò~ñ'Mûõ½…öa϶Y¯#ô†£?ü7`]¿“.ÔB?v®¢}Æ~gJK°^ùyT¦_Ac£{[c¹ôDØ6un£…޾ßIíCÊsm‚]÷MAŸ –ÝñîÞAi¿Ï:_‘ò|ΔuF}a‡iØ[§Eï èsÑ'‹d}&ÉwN’†=:éŸcíG½.Ø^ŴٕE;Œch‡;5$ßË/±û~¿rœCõMTÝ»g;Jù}\ô<¶ÛÚRi/¦È÷Zöy@æÁïÛi ߕˆ·k¢vT‡MaŸtœ9DÛÊëgGûmcoXg +ûaŸ¦SˆßãÉ3‘xj×I¾ÇÛY²íYÇ>ïPAï£%ÆU4]Ÿˆ~I¥éާ(Å1†²Ø>s8¤]·Š×hý+Ø¢Ó©c3ÀžÂ\n½3ëí9Îgnæ`2žm‡ñYÆÜL@øL;-âÍó­}†äá÷gM¶{´E§=þ¶ÍÿHÌYÍ»ä>$k§Úw©®:‰v¾»gù©9-ýgh<‡ùNÕ)ÞñÇӛ@—Fý°óÞŽi€µ£ã©ý¾ÿR‹Jېé½6½›em½xå»î³|kͳ(=ñÞK”žmÓ¢Ž{9§¡±÷d:©iÚ~ï=»³ÏÜr¢ô÷¬3¹Njœ´Š¥rLHµíX¶ßÇË÷ü|7ç{Ðq‡ë +ÈÀ‰¨eð}‚SÁÀJÂp¬<¶ÿ0®G:À鏇ùOê|¹ó6ŸÙ¸‹¡ +ì¥í§ñ0ÿ)qêûuUÆ/P.àìgÁñ‚iÿÐäÀJêL‘Ôàµð{+ƒáøÜÆ5Q˜&#ÚïÑ~Œö Úö!Ú½´£ÎÑòí|ÿ¯ãø—ÿV»¿¯î±°ïèE)ßÝ3NYoŒÄ?-È»4;)Ն~}xxÑÆM ̕¾«¤.†<-–÷;Ҝ$×boʰýöýÀeçȲæßý±@õ§êÇbKþ=­~’÷v,Ûë´Ãcß±]bë¾×ºÓ¾'ëg݂u—çùí×´äD›Ïœní§Í»°NêàOÖ×Sò’y·þcè„#æóúe°”u¥lì°l?óQû¤!ïï¤c½mwóXë¤y¯mo³{ž…ö¬ðÎzEu¯úo´ãʖ÷KÃr=E[†=ý2ÊV?C<ì~ߤΧQ¼f¨C`[ñ› íû²|öð¨úeŠú@Ìüæû5|¯wrxœžÅÀüÏÊôÑý}/y¾´züÏä—w'ïô ¾ëÄv‘Š…>r1¼SÍߪÛ@ÇÚø7p.ê[K˔+©Ÿºûá×`ï¤#|-°î,Ð$ ¸¸€Êðo '_ƒP5ø_Õ±·×ö•-8^î·wÓ"ØÄ‹ŸÅwP¦±`Ð"ñYÖ"µùOÁNI…E¡¦Ûnñ‘nŸµçsæ—qQW'ãªq/¡uh1ìˆQæ^ñ •k³)cêc¬_µ÷¼o: ·Ì;àñ¤{Ñ÷ä6Õ¡eúÔO?ûà-ÈÁ!*׏Ñmzõ2¦`{˜Î‹½¹Ä÷‰å]âƒæ«Ñ³ï(Œ:Jw=Cc0†Ä÷7¢TyˆXŽöΔë‘õoÐb·EYyÊûÓÖ\“v®£Š6`×cí{ßK¬÷c°A1÷4ëžj/í^ꆜkՎÞ2y>L‡nè8{eÊwÚX¶l[ṁ•ßñ¾uÉ1÷*S¨»ö,k_jòyõÏ>³¼=æýÓVÆÿ×ï·âßC}×û¢ÓÝÍ8Ý]“üÿá;•ø»§»ËqZÜ;—Ó½/ƒ¬²\ƒueŸ±Ó<ÿãÀO¡_ïahdšò|Ô²×®V0·×c:Ž +ì3Q>'íýÕ]Û"Ïô¯²ò£Tè¦JëlÞüÖþƒC»¶zOùžªV»Hþ&f¦š~èü}U‰ü}Õ¥à+–÷{§k?¡™ú3´\ÿ=-ÔÿM÷¹ÆÓ} ·« +ÐGY¿ŸÐΣÞ§Á®Ø¤¸±_[G“°>xaûläºÈú€Ÿãä¼]‹5íjÚ¦=…¸@WN¬cÅð¡mâSÚ¦®Ã8G}Rޛަý t âWÙôÏ[ýàßÛôSm)9zèœÕäÔV^òØSAÏÌAÐf ,ç#¬‰OÑ ²§×iµ]'âSó(êtènàÍh]â!ë ®G|Þ±øÈ®O\y î‹Xp¿h_P”¿øð:ê4ؤ>±¿bÁuíÀ—'Ö[öaܗñྍÂk÷ó)Àý ÙãÐô‰ [Ô_¢lvs»™çˆUG–)#³I‰Ž?dòLYïe}·i…´\Ö åè5Ð{ôóLëÈӒ§ëd:æCœC®÷ó£Ô[Öá9)[ã¹\Žçþ4ŽR’±èû{- Óã߆ú¥¶Ÿi9(x¾ÿPü[ê„XÜ~:°>ˆCI|˜–a>ëgýLR–Þ+ݎÓåÃ:ŠõÓé€uìþ¨þŠ/ƒu6€·C¯Åb͊éÙ÷|Z;L¿` °iÒFý=Ú¨A¯!ß"ê t€\ ›Þv\‘íw½·R7‘×s¯÷w’²íebcÞv:8Þ֋ڀñ|°Ÿo˜ó@?Ýø]w]¾Ë—&þNÌéêu’M¯©Õ<¤“yHÛj~¬½o~옛ðM*qxAS©,Aþoí}å¿pD´XÇ4¾ž?ôÞÿm7ß”6Å+֞‹÷ñòÁöý±Žfc_ÊûýKáïæø%¥é”eL¢;ôÿ¡MŽÉe¼Ùq‡å*çò8R)ËåÅ:ûªýŽû|ýnØ_«åYiªü-1Ûß=hŸZ ÙÜ ½²¶T=֕ÛÈ-÷‡¼| 6Ì üQ“ÏjªØvâwôl·Ú¿mæß0/3ÊigB­ùçx3%!‘J gU'ìYß E< ïò×Xa”£LÅ>ìê6Ö¦½m ?Wғî[š_)ýéZyïòìöÉ}<Û"I°¡SZOó_ îóïƒÖH õŒaŒû´ûŸ;ú§½ƒš;÷§#cî1¢ódÕ0 CÝ-û’©<š úècÖûRsLœ›¬÷vLåYV0ËÆq(¶Þ™ƒ€Qûž÷ö±¿=âßÙ{þ>ÑßiU°ÏâiuŒ›ûüjOèÁ”&~Lý‘Ç|Æ¡ +ù{ØK5õé~öÃó /B¾×Ó=2®šîÕÊè^Çbº2}dö6èÐrý>j’én¥Û ÒF|7»,èK} lUp¯ eZyfÑýf`Ïgd£¾i·¼ƒr–y‘¸Õ¼Sñ“_¼i¶jyTi@£yjw„ûð¼ hö¯žVہC£vSóZ~ߨžj6Òf£AIj&}˜€Šzf¢ÔLš Ì®¶†äãÕÀeÀ>àˆŒ «™-7–¢î™-×H²kùÊéoyç̕Þ]³ê-:qªE«ÆYlÃ-¶ƒ¬àþ•íÙ×¢)…%Lݞ’ý£2Ô 42_ƒ§Pž¦$!ÈO;ÔtŠŠjØ!a5eWAQÉö}ªFBUTA‹ÈoîWE‹'¹d”[1•Ï)…üÊߕÃVŒrx—7¹dû¨ñÊ_éQ` *Å÷/Ê_è2å÷9žÀv`pø0”Cø¾‹ï;Ê;”¤¼MÅ@0Øì>ÊÛxú”·XÔä“Ý€¢¼…§Où3šõg<“”7ázSyUû}Ëв’=Ò*¶þBۑ™k;R2Jڔߵ|ÕU„‘†D=¡ö ‘Tªöh)ñËj)_æoSÞÛùwŒ ¼N@AM^GɯS˜4k®7àzƒ€@€”áéʋÀËÀ4S§òZ ŠiS´UúGe(¯*ÏQ&züåyI_Vž•ô%åI_íú¢òlKw?J@ôÏb ˜ Ì® eŸÒ£e‘?™
!†¡+E¸õoY8K4¾(ëDc‘h,¢1 ††Û”ü–q¥’TK²kO:Ð3FBû$)ùèÑ|È|>tÂ><¦ô…Áèa1gwgÚcWŸ +ËßxÉjLŸ§ð) ÃSô. a€ž‚=…LžBIxVó€ýÀç€ à_/ŸIxÀ<à2àsÀÕùPhµ]ÅGeŸÒÅvÅ'šò¾=ðÍWòÃÝ|y¾o¬z}žHê.&w7»+C)#*;%ٙÜ&<»ÿåù÷¿<äåR®S®gÕ­Ü`Óë[¾‚êÛZŠžðJ?§î$O”Q‘(Fë¤0å9™¢<å!В–¼Z$Kj)êëß+¼œj·ÿ«¼÷ýŸäµ)p~œ÷„ÿ6M´ø"ä¡Ýþ×ó®ö¿PÜæDȓEmdo@²îÉæäEÉz9"nmñ_Êd·ÿ’¼1þy2b±qö:øÂIþiE³ýc‘_UÞxòÜí¯È;Û_nq æ4»ýP…åìƒÊöΓ…»#¤Õ?xæÌ¡mbi¸¯c«£Î1Ù1ÄQâèëÈwøÝ¹Ž4gŠÓçô:n§Ói85§â$gZ›y(" `šácÂÿj¯ Mº} +?å?AŽy-œ +§Hª:A™0½RLˆì_H"Ǧۄ{ê숬‘” 4aFedXhB›ÃœšqL9«®YˆëêQ6· šQ×&LژI]·‡„HÞxm.Ó^¯­¯§¬Œ *²*RF&—ÕTâÑ`?CŸ¬ÜÝ*#['L¯k¼sg·ÊúH‰t›&Ü"7MÌ©Û#¾Gª«öˆ0©¯Û£Ž_TOãpudU}ý„6Q+ù( þ>ˆÎ?$Ÿ«4óQÀÙÝâ»Õâ+Dzð0ŸËE…’¯Ðå’|š`¾æuÕUÍ’'3@ë$ϺÌ@,ϋ…à),”<ô¢äy1£‘y"#%K^XºçI‘Cy’%OäH–ÚN–b›åê–«eIªèäɳx<‡¢<žCà ýÐÏâÊPHìQ¿pNõâ`uC°z1й悥Y‘Ɓ@óÂzŽDÔ¢† —2¿8R\\Y¬ +4˜sŠè9="XÕLsªgÔ5Ï /®jQœ_U¿k̔ACO(ëꎲM9EfS8³A\֘¡§ˆÊÑc¸¬¡\ÖP.kLxŒ,‹¤¨O©kvReýè9Ý¥$¸!¶ ¹ùõ•¾5#¥ ÈϺ4w/L—(!TI VF<GõÕoGajq”ÁIvTÖ¥#òs÷Šì(‚“ƒ•Zþºó)«zY•õ·­?Ÿ;Üz†Ö}×qՑðüªuë‰&DúLŸ©˜:»®Ùá@h7)2<–PÝfî·û#p8ªj#‡•s˜Ëe3ž<þçÛt4ςFå‰]"Ü]¬§uõj¤û„ +4ŒÙhëœÙu{aXñZ±® \'Bb]4»Ú¡Y~â6G±þ|Ûe÷Åz›Z)‘d]´K:>ÜY¡Ž[/³•ÝšS7Ê«Q‹ilç ý@û–€–¨Åá”"¿ª õ»œCý î*¿Ã¨òGs­Ñÿ@óª½ +endstream +endobj + +xref +0 63 +0000000000 65536 f +0000000018 00000 n +0000000263 00000 n +0000000324 00000 n +0000000376 00000 n +0000003436 00000 n +0000003713 00000 n +0000003977 00000 n +0000004663 00000 n +0000004842 00000 n +0000005610 00000 n +0000006384 00000 n +0000007213 00000 n +0000007548 00000 n +0000007880 00000 n +0000008591 00000 n +0000009006 00000 n +0000009358 00000 n +0000010147 00000 n +0000010761 00000 n +0000011056 00000 n +0000011388 00000 n +0000012070 00000 n +0000012249 00000 n +0000012430 00000 n +0000012724 00000 n +0000012905 00000 n +0000013008 00000 n +0000013042 00000 n +0000013298 00000 n +0000013777 00000 n +0000014190 00000 n +0000015646 00000 n +0000016087 00000 n +0000016412 00000 n +0000016700 00000 n +0000017032 00000 n +0000017388 00000 n +0000017678 00000 n +0000018505 00000 n +0000019105 00000 n +0000019367 00000 n +0000019686 00000 n +0000020390 00000 n +0000020777 00000 n +0000020880 00000 n +0000020941 00000 n +0000021218 00000 n +0000021572 00000 n +0000021920 00000 n +0000022278 00000 n +0000025321 00000 n +0000025581 00000 n +0000025933 00000 n +0000026132 00000 n +0000026486 00000 n +0000026674 00000 n +0000027032 00000 n +0000027220 00000 n +0000027253 00000 n +0000031015 00000 n +0000042377 00000 n +0000057909 00000 n + +trailer +<]>> +startxref +74555 +%%EOF diff --git a/tests/resources/4.pdf b/tests/resources/4.pdf new file mode 100644 index 0000000..5ee9ab4 --- /dev/null +++ b/tests/resources/4.pdf @@ -0,0 +1,758 @@ +%PDF-1.5 +%%μῦ + +1 0 obj +<> +endobj + +2 0 obj +<> +endobj + +3 0 obj +<> +endobj + +4 0 obj +<> +stream + + + + + none + 2016-11-10T06:47:57-04:00 + + + none + + + application/pdf + + + none + + + + + none + + + + + + + + + + + + + + + + + + + + + + + + + + + +endstream +endobj + +5 0 obj +<>>> +endobj + +6 0 obj +<>/NM(05b46baf-8873-4881-b973382e67de9b94)/Name/Sold/Rect[19.154334 653.17007 141.75884 781.8921]/Subj(Something Special)/Subtype/Stamp/CreationDate(D:20161104051921-04'00')>> +endobj + +7 0 obj +<>/NM(04e49b05-c0fe-4e18-a442eb62db809d70)/RC(

this is a comment

)/Name/Comment/Rect[169.79795 703.2445 179.79795 739.2445]/Subj(Kommentar)/Popup 8 0 R/Subtype/Text/Contents(this is a comment)/CreationDate(D:20161104051939-04'00')>> +endobj + +8 0 obj +<> +endobj + +9 0 obj +<>/BS<>/DA(1.000 0.000 0.000 rg)/IT/FreeTextTypewriter/LE/None/NM(7f35f3bb-14ee-4900-b2ca8f24a6d68ea2)/RC(

typewriter text

)/RD[0 0 0 0]/Rect[198.42667 666.46499 238.03704 735.29666]/Subj(Schreibmaschine)/Subtype/FreeText/Contents(typewriter text)/CreationDate(D:20161104052009-04'00')>> +endobj + +10 0 obj +<>/BE<>/BS<>/DA(0.000 0.000 0.000 rg)/LE/None/NM(75e29b8c-3e27-49f5-9d1691add0d9e97b)/RC(

modified text field

)/RD[8.56139 8.564017 8.561391 8.561395]/Rect[24.841629 508.46095 83.40302 578.7118]/Subj(Text-Box)/Subtype/FreeText/Contents(modified text field)/CreationDate(D:20161104052030-04'00')>> +endobj + +11 0 obj +<>/CL[212.24744 704.70046 231.19762 699.76559 243.19762 699.76559]/DA(1.000 0.000 0.000 rg)/IT/FreeTextCallout/LE/OpenArrow/NM(46f3ee9e-f8d1-4348-a57b21efbbded44c)/RC(

explanation text

)/RD[31.917883 -.000018 .00002 .000019]/Rect[105.63986 539.6411 171.59882 575.6411]/Subj(Erl\344uterung)/Subtype/FreeText/Contents(explanation text)/CreationDate(D:20161104052053-04'00')>> +endobj + +12 0 obj +<>/IT/LineArrow/LE[/RClosedArrow/Diamond]/NM(6404d971-e409-4762-8f4157381a50c0b4)/Rect[192.69977 550.5221 238.6891 619.5947]/Subj(Pfeil)/Popup 22 0 R/Subtype/Line/CreationDate(D:20161104052133-04'00')>> +endobj + +13 0 obj +<>/BS<>/IC[.752943 .752943 .752943]/NM(55705d19-8e3a-4d87-884ff485467f387d)/RD[2 2 2 2]/Rect[32.889589 368.61647 101.0062 461.3922]/Subj(Rechteck)/Subtype/Square/CreationDate(D:20161104052208-04'00')>> +endobj + +14 0 obj +<>/BS<>/IC[.752942 1 1]/NM(2a1c3549-37e0-428b-93ee9b69a3ce9df9)/RC(

comment in circle

)/RD[1 1 1 1]/Rect[123.893268 320.11317 171.77234 501.53553]/Subj(Oval)/Popup 23 0 R/Subtype/Circle/Contents(comment in circle)/CreationDate(D:20161106044139-04'00')>> +endobj + +15 0 obj +<>/LE[/Square/OpenArrow]/NM(d2a8b21c-d4c1-4719-86a11a64a4e0b6ec)/Rect[196.7502 422.41249 252.07375 484.10316]/Subj(Linienzug)/Subtype/PolyLine/Vertices[397.84056 656.3386 404.75093 632.65127 438.3156 632.65127 447.2004 657.3256 479.07295 635.72817 503.47065 662.2605]/CreationDate(D:20161104052251-04'00')>> +endobj + +16 0 obj +<>/NM(a60c24b6-1dc6-4e09-bdec495c6894fdc0)/Rect[34.94998 206.22144 106.90786 330.6726]/Subj(Polygon)/Subtype/Polygon/Vertices[70.477687 567.9854 78.37527 526.5325 158.33824 524.55856 212.63411 550.2199 135.6327 585.75106]/CreationDate(D:20161104052317-04'00')>> +endobj + +17 0 obj +<>/NM(66b71715-3eb4-4097-b2e2f5c4eb0c4d52)/Rect[141.60385 204.05493 202.12425 301.85438]/Subj(Stift)/InkList[[283.32566 563.5629 309.97999 557.6411 340.5831 556.6541 345.51908 551.71926 341.57029 537.9016 345.51908 534.9407 367.23744 536.9146 393.89176 535.9276 397.84056 540.8625 400.80213 539.87557 403.7637 549.7453][395.86616 558.62808 385.99418 557.6411][367.23744 550.73226 337.6215 530.0058 321.82633 524.0839 310.96717 525.0709 308.99278 530.0058 305.04399 535.9276 305.04399 549.7453 308.00559 553.6932 312.94157 558.62808 327.7495 563.5629 355.39106 565.53689 366.2502 570.47177 383.03257 570.47177 387.96858 566.52389 398.82774 563.5629]]/Subtype/Ink/CreationDate(D:20161104052347-04'00')>> +endobj + +18 0 obj +<>/NM(3cba4ea0-034d-4505-bd857d15063c675e)/Rect[54.28789 29.740418 89.466327 110.16516]/Subj(Stift)/InkList[[108.70982 463.30189 128.95235 469.29933 139.44847 475.29676 139.44847 463.30189 143.94681 458.05415][121.45512 454.30574 130.45178 454.30574 132.70096 456.55479 131.95124 452.0567 134.20041 448.3083 136.44957 437.81278 144.69652 437.81278 164.93904 436.31343][161.19043 469.29933 168.68766 469.29933 173.93572 465.55094 176.93462 461.05287 178.43405 446.05928]]/Subtype/Ink/CreationDate(D:20161105124026-04'00')>> +endobj + +19 0 obj +<>/NM(6fe9e08e-7ff0-45e0-8fc78c7d63822c11)/Rect[80.595218 67.220829 104.96121 69.220829]/Subj(Stift)/InkList[[209.92243 455.05543 161.19043 455.05543]]/Subtype/Ink/CreationDate(D:20161105124033-04'00')>> +endobj + +20 0 obj +<>/NM(1d9e69b5-b0bc-43be-8817c8e56e66d22c)/Rect[94.12063 42.656984 96.963558 107.321109]/Subj(Stift)/InkList[[189.6799 474.5471 188.93018 464.80125 191.92906 458.05415 193.42852 442.31086]]/Subtype/Ink/CreationDate(D:20161105124035-04'00')>> +endobj + +21 0 obj +<>/NM(83464ab8-6479-45c4-83e04e59025f9b68)/Rect[44.019159 32.067384 122.88699 107.717838]/Subj(Stift)/InkList[[235.413 469.29933 240.66106 465.55094 242.1605 459.5535 245.1594 458.05415 243.65995 453.55607 243.65995 449.80766 222.66771 440.06184 211.42186 438.56248 209.1727 437.81278 214.42076 452.0567 218.16938 455.80509 218.9191 462.55223 216.66992 464.05158 186.68102 464.05158 143.94681 468.54966 135.69985 465.55094 125.95345 464.80125 118.45622 461.80253 104.21149 461.80253 94.46509 461.05287 92.965648 466.3006 88.46731 474.5471]]/Subtype/Ink/CreationDate(D:20161105124037-04'00')>> +endobj + +22 0 obj +<> +endobj + +23 0 obj +<> +endobj + +24 0 obj +<>/FS 44 0 R/NM(0ac8a6cb-a18f-4526-b5e2ca95632913df)/Rect[156.69542 -79.30194 163.69542 -39.30194]/Popup 25 0 R/Subtype/FileAttachment/Contents/CreationDate(D:20161109142650-04'00')>> +endobj + +25 0 obj +<> +endobj + +26 0 obj +<> +stream +xœ+T0Ð3T0A(œË¥dh` ^ÌÈh{ +endstream +endobj + +27 0 obj +<> +endobj + +28 0 obj +<>>>>> +stream +xÚ3T0BC=CCK '9—Kß-×@Á%Ÿ U¸ +endstream +endobj + +29 0 obj +<>>> +stream +xÚM‘9R1 EsŸB'PYޝ’‘pW±U“p}ä/yèšäý-϶P¤Ÿ÷iÿ^ŸƒÐW(ô"7úÕϟA&·Dk¢o•{",‰ +§FÒ8 Ã&Ǿ¹Ò +ÏL‘sRn³Õ\!s?(<š±Ö5 ÂÆÀR8w-¶ÐX:¡O:·L{šÑ +¦bI‡Ãђ©{³1¦®û ¯@áɯCï%óè»®ˆ{À™+PWÑI2ÏjRÚ¾‹ÇAø‰‡ ²ç/å"ÞëìCiÌ: a£€ë.Š3¼ùᔏ:Ø…Ï!â~¢c¹73v±ãà1~±æä~è}ðÌÿæ{™7@Á&×]ó.>8V÷¯ó”C½k}¸éÎ¥š3H÷'ł*gŽbAQªõĸ&B«±Ú¦×MÎmÿA‹œ´ +endstream +endobj + +30 0 obj +<>/ProcSet[/PDF]>>>> +stream +xÚMÝ +Â0 …ïóyeiÓ® Áé•JÁ{ÿ܍>¾é6DÊiÓöœ„©Š2Õ"ó>\Á ëÚ­Éã¤dX0xGÎ ‘‚5(Bµ©p8Þàeú`»éà5ÇÕ ¹èEœ&ù ?> ÈÐBìV”Pî‡ÚãÚ>/ProcSet[/PDF]>>>> +stream +xÚ]—ËŽ&5 …÷õyÉÄqn–‹ ¬X€~‰=ÍMHƒÄlàñ9Ç©îvZ£_ÓN9Îçԉ’$©ä5WúúÇUÿýôý¥éß«¯P‹$,3l¹óªF³æŽ\²uzKK֊Ç6à6S¯Ú,‹{k¶ZÓ,ȱՄ¥¬4˜‚”±ÔÊEÅMÓA÷ ¶¾DˆÚй9[ãìÑ·{kr“¯ža®-F_HT7§*ÜGÇÆ-XÐRnÂç‰Â *LÍs2:¤ðy˝3L÷ÅêâVçÚ¶iÌt,ÊÅ 9!ãæÛD“Û4—ïâ,3«Ý-W݋7@®’EÄ£‰¹Ù h`Ál ,Ô@dYدÀH`Á›¬‹+áoV‹qó# z`)ÁFd±š-° ,a´FðvG ‘Rs8´íàáÈ @´51âŠHG LBÁD(‘;‡W(©yE(ÁâJñ„‚‡E(Ø=BÁ.”@ÄJ ç>@Õzgñ +Uq4#l‹Pµ¿S4G¢¤9#jšö!j©ãPµh9d-ÚÞéR8„-:eÓ>¥Í‘¨mFˆâ¦}¨[tò–V}Këï.m +—6‰Ó>5Α(rÚQåŒxÈ\Ú:t.¨|‡ÐYËN(”+µ]½ÖDÈþrŠ¡ÔaÚ,Ô,weªÏÀˆí¸ÖžQ+#ì@»TñÍXO5pXAI”À¡^ž'À¬…żûŽrBË K ۅ4Àzóæ0Ðud I̱»ƒ×`ÀëÓ=Êä¨'»{€æ¡ÜåUþ ý ?‹cAzÜ·¡.ó&íŽßÙa¼„N$É7ÁÆ7sã>ßý 5pº?6¾ ÆGIŶAÈÀÜ  Í„¿"Á=¡HÛ2ýÍ ·ŠšÇ¯³zü>}OÑ[ÍGðF¹§”ÊÂïïKZCFÝpmèkŒR¦ V—úÕ# b΄æä3 éÛÃêžÑ{ßI×Ð׬û-ñ’gÝ·ƒÆmâ©òm•ú™p¨}ˆ*^›n&tGQì÷\›i©Ÿôy2ñìG&ÌÐȤ»Ì¦j÷«»™X~*Z9˜*n ‘ %Ð"ì~2±HF&DÈTË}‰ze’µ/,/L¬ÓIÊÄ “ŸßÀÄÛDd‚­'»Id‚m‘©ìs˜P$2ñ<¿1ñ°F$ý%¡£öSv܀s[·.^`P +,°°Ù¿¡à̶ˆ²p_ (¸hh@Yý¬y (¼œ;5’%Í;Ð +Žÿ!h8E=Ã)Ê×±SͼŸ¸G-s‰eê¡d^ ßPÆ8uÌPPÑ¢Šaž"Æ@Ô0fG Ã<h²ïðv?Í«£ÛWêRm»/\Чßêý«·üwÏWúóúåúøø/=ýðéúçÿ"RTtÎf»W¨u <¹>hú`‰eµ¦¯¿qöÏéïëéqÊÄošO|²ôôøõúø]çŒÇï×7¥ô‚_-(îø}ÂÏîÿ×+ŠçsÛýiÿ]ôÛôøëúü¸~¼>ƒô²©ä +endstream +endobj + +32 0 obj +<>/ProcSet[/PDF]>>>> +stream +xÚ]ÍJCA …÷yмÀM“ÌOf@º¸¥uåBpïo+ؕï±½](!$9ð$Ɔ8¾‘±"î®ÉÍÅsph–ày2±Þ¸ö.Q+çôO¹Õ¤– +­IjþËýñå*Ý3æív¦àTON&§Þق/Ä{z¤ÕøæùfC_Ëqƞ 6G5‡MÅòRrá§MƓ†•Õ…àOšé ŃzyPy꒣ðx¦Õï;WºRÍMµÌȄ~‡Ìèm©ºAõ³¦iÑ.L¬y¼ÓvÐ-mqóàEDª +endstream +endobj + +33 0 obj +<>>> +stream +xÚ]޽ CAƒ{¦`‹ŽŸ Òç-‘âRe)èIi"*[ŸËÜóAÖŽÊæC.ã7yJ7§&V5ºÆ´¥3¿)E'§ $ö@žÑZ÷[;¢ÆpØôâ}È*°4æÝFɞ¢ÿg˜Bôâèý‹~Hå#9 +endstream +endobj + +34 0 obj +<>>> +stream +xÚ3Ð375V0@"‹Ò¹ ôL,Œ`$H0ȝËD¡œ+ÚBÁc R¸ÌÌõÌÍ-Ì ÌõŒ€J ôŒŒLŒôŒ-,ŠR¹2¸’¸ªÏK +endstream +endobj + +35 0 obj +<>>> +stream +xÚ]O11 ÛóмÀJÓ:齀'оO%hOBYb˱C²j™óº‹©Íåz×·Ôæ`£†*SŸ'CCgÓêdh¤!KU?ˆˆñ8ÉSÑ:²çvØø—1/c=t°;Ì|G,ä&,4/³ +endstream +endobj + +36 0 obj +<>>> +stream +xÚe»‚1 ƒûL‘ tNüŠ' ‡%( bÿ;ä¿âŽ&/’¥VÅ1®}¾ß†VâØšá՚ïabH'эàþ¦ºâ—XbӓÐ}ς¤RáÈ}H\–œ—ä1¬ çP䏎ú¹,¸Jú!/›Ò¶:Œ%òÐ¥åüCWfÕaÖ¶hbØU$ZÅ,ˆ7˜Ïñ_tõ2‹ +endstream +endobj + +37 0 obj +<>>> +stream +xÚʱ Ä@Dќ*¨`ìÎ\~U8ðE׿d¬ÉÞ|W›}?ÂF¬PžÆ.êON£•Q°9nñ°ž$Œ5æXUJ:Òûmb¤²Á½ä/àD­ +endstream +endobj + +38 0 obj +<>>> +stream +xÚ]”Í‘\1„ïŋ€B ôïv>;ÿ«¿fìY—kkë H@74ó÷ýÛ+NZÆzj¥ñÿüzÅ]¶*ñ¸-Gö<õT]‹uŸôk÷`n[s¹Q–¥œÓöyÛ5.ö°ÍWä®ñßé‡ÃjcNs}ÌÜv¬ûe›¯ +…c{ÆWœvß$)-O%€ÖÐ7Ÿ\Û"wÛ’DhÍÙEÇádŒÎªq!y£kÞ8Š “'²s¤qAªqÉ4°·)’ng5{@E„èPuºÓZÕ<Ü̶Gg8{1=Í7ӂqÀgzðÍîԏŽ÷"Á¼¶§hüx ÆYH Ž-àüÂãv³#àæÐÿ(€ÛSnò䨰·É¤dSŒÀ³¡ú…,m[Ý.7wAO~т§ÐÓOBÓ lé'cÐ>pÆ4?­£±¬„$È-卂ìh["à^{ÊNßD‰º:΍(@`иί¨~rˆ Tf‹<p”ª m{«à3eµ¸§H¦n¤]í“&òuÿ3yfªhU@»µØA”¢¢Xӧގ´ê…6©‘tn^5F¶–ƒ‡ÉUò+â04)ˆ”W †‡2 ½ÖËaû×}CˆÖ+èý¸Qk¼W2õv- Z¬×Ñ~ˆÿxý¾1úç +endstream +endobj + +39 0 obj +<>>> +stream +xÚM’M’[Aƒ÷>Å;Õ4?MŸ û̲Îý·ùÀה.ñjôYü~ÿzé*9úxšØÚÏߗjIYW’Š>ªWô\pˆW>ºKnlð•}ïóç¥æ^¥Ãµ”Ró¦ÚŸïã4÷¬DãÍ(Y»51ƒ›ãÛU3–ìñ¹DãQWÉ[óØäúy< +‡ðõҍjŽHöj;°nŸŠîÄ*a‚9Æ.ûó½WS¹ïB°«Ú»-pdé|ÈÈ#%ÕÌP ©Û»«DÖ(h¾ñf@L­©Ý±dYÏ0Ò¼ƒ-»àÀ K5ý!/ÎÊÛÏô{uǒN‚ð¥íð6ýdŠ!haDM[&räÝIòü$`µvNe#ذýW".ŸÎ Q§ŠÞÏIôÿ¦ïŠr<{œ¥¦yè¬úyDOZÌ xûlÜQça]ó–s†A€Í bÍ9’ì£áÜÌß§œøo|GQ‰z6;Å3Îs3[ÏÁƒŒèþž ö¤ÿÎbÿ šï +endstream +endobj + +40 0 obj +<>>> +stream +xÚ3T0 w.#K=K##SS=SS…\.C S=SS3¸ßÌPÏÐÎMæ +æI‡Ü +endstream +endobj + +41 0 obj +<>>> +stream +xÚ%»Ä@CsWA ± \~.áâë?5‹‡€y’@JÒóý\šÅ+ ØôÁ%« ÒL¶DÕ`y#8Eé7y(ZQ¶pÒê­>¼üå:"Y碜±¬aÏì„uS k.`ìz:î빺!Õ +endstream +endobj + +42 0 obj +<>>> +stream +xÚMSK®AÛÏ)êˆÁ ²Ï;BÖ¹ÿ6†nE£Y´ ¸À†‘ÃøýþõQ r±ãÙ¤Ýç/"El‘K®q·ðÄû¨3e +pP„œ?u%EÄH2·âò`¡.?“—aDƒa/£c"E˜Nå½ÚóBDožÃ—n=‘DE-ƒïࠊ™Ê0 ¡E.ÁÈt +„®½O*„†óúOð¦â‚9¥B÷¼ÌÝFŽé…53ª‚ ŽS‡!—,ã8Rè̌ÖämÀuóñ2˜ãMÄð–2¼EKKêiÉMrçK%öüî 9CãE®ÁAVP-Žã£Â§»øT˜)±3IÙc cRØÅëÇ20­éz}cTGì.’m-`¤ÂÚG*9wßWs+V7pÏPI9÷Ÿzªq‘;¼NϽà+BÉCp[:Îâk7Ï`‘Ø›ìaÀ_p“1"–§úò¢¸ÇèLˆ+à ûu²bŒ|oËH;ƒó> KQ¯¼m¾x:¿"ñ<+[ÌãLðí±<»,+dþ8Œã†`ÓXã+ì8ùbt“Ùbho˜:·0kÓXô";jí.t Ís›®pHv>êm§oÐfë—!·Nã? ¹ÐáZaôÙØ…>Wôóùېʪ +endstream +endobj + +43 0 obj +<>>> +stream +xÚM;Rƒ1 „{B@cÉï–&U8B +H&4ar{vý»È¸YÙÚO+»:Îï—$åù8‰ëMŠž%YsýÃýUšµ¬ÍrÕ閊&ËMïÐ帾˻¸Û¬êÓê@›=¬²NëC®O9ŠdA€Ë]»9Bd«Z }Ð݉ö¦¾fD`f ¾»^ä-±¾® äÅ«€Ś8YGžX¤%6}ȝšY_ò_D¿±¨øB¡{5Kk—àø-™xXŠUpÝVwÿS>a¯+ v¤AÀ°Çܒö²ô„{´ÝN÷?"¼N¶ +endstream +endobj + +44 0 obj +<>/UF/Type/F>> +endobj + +45 0 obj +<> +endobj + +46 0 obj +<>/Filter[/FlateDecode]/Length 16/Matrix[1 0 0 1 -.001419 .000039]/Subtype/Form/FormType 1/Resources<>>>>> +stream +xÚÓwË5PpÉç ð +endstream +endobj + +47 0 obj +<>/FontDescriptor 53 0 R>>]>> +endobj + +48 0 obj +<>/FontDescriptor 55 0 R>>]>> +endobj + +49 0 obj +<>/FontDescriptor 57 0 R>>]>> +endobj + +50 0 obj +<> +stream +xœåZaoÇý.@ÿá/Ly±3»;»c  +»R4­‘äCÃñd³¥H—¢ã8Eÿ{ßÜޑ}”iG®Øbó4Üۛ}óæ½9±ËN›/gíÕôÕbsÑ¿i®Vëfó¢m¶AÓf1ÿq=]¿¹8?ûñÕ|±iŽ;Ê¢^ +î¬çg¸ç?¸ßj=k×Mýc—þØüç¿çg—«Åj}sûò¤OÊäa󔜿h|ÿ¿gÍäj¾XØžáËW‹éóá»ý—ãùÙ|yµjšƒ%/WHÚҖœL°Êõjöxºiññ‹Ç-D>úDÊ၏ï'_ êrÝN7óÕòH(í….§×2ù~µ˜Ù 6óÍ¢»ð—ÕúŸÍ?&Øíb¾lÿ¼œÝd`Ý^n¶{5ßüâ¾Ãõ/CqÁaVŽÅ§‹†Õ©ŽžC Ù.•àe•œsˆ@FâRH”„ÓWçg›7/ۃ=%Æ^¿ßL¯_N€§ŸÚõf~Ùîïðé³ßèt0py76/æ7 þ›6—«ëk»xAGàñhwûBHP—4•"|”bxHäsNA=Žþ¢ފݏ9dzòCûóæ“ƒcr³yS³e©ûi=Í_Ý Û«+d¢Ã>½¸ýÃ×óÙæ>t2™Mo^´7dޅ7ï)ËàÞp¦Yؐ§Áþ¨22꥔±k÷ÃTv$¯×ó ²±±S8‚Ì‚îb:™ìý2ß’8ŒéºÛàßV˶;å¬[ã`Uq ¤Øj)3Ç +ꢢq¸äÅ ÂØ×ϒBŽÀÕÈìëuÛþÐ'kø÷Û<þ^€LøôhrœÃÁyè¥t_½pœ_ÍÛY‡Ot[»ßJ“WRHSQü§BiTÔ= Šv++…R —ŠQàҁ’XöâcJ R–$õZ&`´¤™@´§ õScò½:ï½A§ýùåbºìp'»èîÆM +÷…›¿¿l—Z¯W¯ß½^áŸïvâPA +N‹ä©c´ÀNÉÃ[ˆïŒÄE‚¨€Ÿ®$‡0ÀA"û¢%ÇmÀ–þ:¯dl×}A+ éJ.$KN¹¡5€^]ÕHFÌØL ª( +j®#áCLÿ.Pè]N¬È £çàaJ²S®Ý?~ N¶ñTüâÔ~ßKÈ¿œ•2€×›R&+ê è¨DFœ{‚ÿ5%Ÿ£d$ÀÆåNÍÿûÕtÝ~ò^üëß§2ƒàV|0ðz§Ý̗Íå|}¹hït6¤§ B,Ò¨ç¾G$¢{º\Ð>µ'H0eï(£ß‹ßuöcÅÔùŒxWJ‰Ü_9Ãd¢¿æãSÃð·ÕPe‚ç“ )MæîÃ:ìờnvA` •”М (Åæ% ©US‘Щ¨ ÅÅÀB™͈ èхµêX2võdµxÓõÓ£-4»m6 )ÂA«YiqØÃÞ (Z»4s–€t}ìœc"êù5Ú(ˆ'ˆ[,UŽ…¡BÍJ ôÏ …èà¦ç’ ŒL=ÆL(_‡*õÁÑÂÜÖBÖb7VIh÷1û=]0†E­kß >ӊ0¶'ŸZòGnÑꊱanœ¶_ Ùd뙡u¡ÖÜ@ô‰­¹"—Úµw¤üóÕòÞÁÄ1g)}ù@1&ÉN ȚÌJÇרB ‰%{ –Ũå~àD©X•ÀÃíÜMâè À»+ ‚¡’€N Q»/#*A™ªW‘¬Œ) øÈ= wë èÈc'u_Ÿ)Ž;p¦Óq?2ŽíM +û lÈNYVä]‡<ÜS&ä'è[@¥}ÁÆÃ'e ¬u×HÝ^ïM¾Yþ놟ڌ.Sð7ÂQ;ÜY¹WUðê4«n%qËJܛO\]*ˆÓê÷ ÂÉæª +Yºñ«E%…åÁüEì«EpY0°e»¹dåBZÌñَêª;—ÝíÞ^(D§°iy·ʑCŽ\]ÿ +±l\õtAÁûÉ´¨äðøè>Û¾5ÒÜ­-–\{”uKžiïm Œ%š“†áŽÑŒ¥žn±‘d€=r`¤~w@È´0îècu1ö$:§–´2Q`4T`–¡;¹Ô”¯|Áó„]”‰ àU‚M(êî990¨¸·H-@ْë7ãø=¾ c¾>ãÈ!D&¶ÞË'°#kÚQ¦à”…·™Û"©ë%Ôxî=êN‘ŒZSP2©¸Uí@š€”Ýl,ˆ8NÀN¨jÉvÕu8QŸ+ØYç÷äM<…z{Ù{Ë!6mÄ¼Ý˜#ÌDámÛ9Üþ³Ï´U˜ÎD¢Oi](ËÇmp§`EÀç•lPeˆIÿìUtýŒ3Fÿïô± æ"ª Uaö¡­Â¶€Ú- §QúREKrÁúZÖ%Æ ÖR|W(¥Žu ÀnD­.F+@² ŽcîÞ»Ù`Pü£a#w„V»Ìre<„¡VÁÖÊ0ñ6$WDÇ!âm‚'±`Ê(¾*Žº£¦Òí.<Æ6ûÓT†7Œ‡:K8õÃ‰õm(‚+mǚ‰±?ÈBڍ: lßÍt%v¿“ñ8¼ã±8AZ4 d ÷… Ö8¢`7–––RƒcÊ㐽‘0AßDßÛ6j‹‚mÃnv&­KžØ¬ÞÇátÞ>U?¢—€åKæcA‡ Êcf£ßl1 C¤uëV-J̘ +j¢§[ åŸÐ8$œfpfë(ÕÑ\—0À=)Žm˜Ö}Þvҋ·C?…àÇæÿBNcŠŒMg¢MrÜƚ٭°–º#‹ƒ €ð$H_\‡(?ˆø¼ÑtÒ õš>2šJqöÖJ¡2D'qÁ† œr%7ˆ{ˆ´®,Q*UUgow~âƒ;bÁJ°EžzÚ ›`Üaî‡_²Q\±®ìwª;MÀwúW +£è î³ ]¿Ñ·{X} hêJß^ˆ1F[Ccsßó?oŒÆ“^ÞWŒ~ds÷èihÑ4Èè/Öޗ¾×q´7NTRv¼ϯ€'%8ӎ‰>˜õ`“P&0ˆ¡ŽƒÆš2rá`4·äñ¦ ¸94d€ŒwºÃ~ÙÖ[3Óúr}쾨dÌ”í¥¾$lŒw® +`9ñâ³öï„GÃàê ÷v:aÌx|†¸ð½5Šx,«(.ûCAH@0yÒ\g"Öi3ƒ´Äaä`͂Ðp´èÖ/ˆ'¤«Ùè„všxDÚá†Hž»ŸJ¥ØëûØÛá¦è> +õX;vŒ”0CŒåíØ^ð¬Û–etb(ïÏÑ\'ÜßÓ@|£qcÚ:¾YRÿZ öSEµçƒøu³c² þGd~»ÿµŒq²ÃcFó°ŒXªÜä]ñŒ{檥fƒ/Ðö ãí0ÅHêËiˆ9PŠ +3/ö¹é†$ùá! mÃP°Q2ô¶&™e„΍[ÏϚM{³q/gWÍËéó¶ñMó i(5Óårµéçæ¢i—³f…ˆõêùzz}~ö?;ÿ +endstream +endobj + +51 0 obj +<>>>>> +stream +xÚ3г0UHç2TÈäÒwËUpÉ2 ôL €0›«PÁÌ2TÐ5Ò35Qɹ +¥ +\“ò ë +endstream +endobj + +52 0 obj +<> +stream +xÚ]‘OkÄ Åï~ +ÛCɟƵË. 9´]šRz[²: BcÄJ¾}ÕÉn¡Bò˜ßøô1&‡æØhåhr¶“hÁÑ^iiaž+€^aPšd9•J¸­Š1v†$ÞÜ®³ƒ±ÑýDªŠ$ï¾9;»ÒÝùxú:|*ø›§$y³¬ÒÝ5´Sn ´]Œù†Ñš’º&zìKg^»h®¹<æi™1^Ìç¸õ?Vë ƒ‰IÂl:¶Ó*õ«¦Õɯš€–ÿú]×þoû“ß~—<­,žcµÉž!Qöe„ }›p´3†…ç9B^ ì♜ÇÌ·t!~˜ø}2b±Ö->KœG˜„Òp93™àŠß/_G +endstream +endobj + +53 0 obj +<> +endobj + +54 0 obj +<> +stream +xÚ]‘MkÄ †ïþ +ÛC‰I󱁐Ë. 9´]º¥ô¶$: BcÄJþ}ÕIS¨ /óŒ3¯Ñ©97JZ]ÍÄo`i/•00O‹á@;¤"qB…äv‹ÂÉÇV“ÈßÖÙÂØ¨~"UE¢7—œ­Yéáz¾|ž>$|ƒI؉^#Õ@e¥]=½-ZÁèe¤®‰€Þµ}nõK;ü3÷DŽ•qVdّÑ=ÿ¾j IˆcŒOfÝr0­€TÌ­šV·jJüËXÕõןÜõ]V{˜!JË yА#DÉË3¢Mr0Aˆ’÷±ç&öÌ:„(Å1Ìü;ß;¾;Ãcœiá[‚Þ ©`ÿ9=i_öˆíŸ +endstream +endobj + +55 0 obj +<> +endobj + +56 0 obj +<> +stream +xÚ]‘MkÄ †ïþ +»'“l>6rÙe!‡¶K·”ÞJ¢“ 4FŒ¡äßW4… +ú2Ï|¨3ìÒ\%-ew3ñXÚK% ÌÓb8ЩHœP!¹Ý¬pò±Õ„¹äÇ:[ÕO¤ª{uÎٚ•î×ÛÇå]Â7˜$:öb©zh(+íêécÑú FhDêšè]Ù§V?·#Pæ¯ùŒÓ8IËòTætw¿­hìßÅ'³n9˜V @ªÈ­šV7·jJüóŸ1«ëÿÂO.|—$ª=LÓ`m’Ç „%Â!Gˆ’—{„(90‹ƒ•áE9 ̛X3ë¢çð‘ß'û?ù)ìÝâ‹1®‘aT¡I¾=RÁ>M=iŸö½‘ð +endstream +endobj + +57 0 obj +<> +endobj + +58 0 obj +<> +endobj + +59 0 obj +<> +stream +259.521 417.906 m +257.935 417.332 256.569 416.132 256.599 414.411 c +256.635 412.353 257.791 411.113 262.822 409.268 c +270.304 406.543 272.053 402.625 272.12 398.761 c +272.234 392.252 266.891 387.916 260.678 387.682 c +260.635 390.118 l +262.64 390.783 264.346 391.737 264.389 394.048 c +264.427 396.695 261.301 397.732 259.27 398.579 c +253.94 400.712 248.985 403.02 248.87 409.613 c +248.758 415.996 253.309 420.024 259.478 420.342 c +259.521 417.906 l +270.765 411.465 m +270.781 410.499 270.632 409.447 269.414 409.425 c +268.406 409.408 268.014 410.199 267.874 411.037 c +267.358 414.136 264.698 417.282 261.41 417.939 c +261.368 420.375 l +264.014 420.379 266.856 418.832 267.318 418.84 c +268.409 418.859 268.299 420.369 269.391 420.389 c +270.693 420.411 270.634 418.982 270.639 418.646 c +270.765 411.465 l +249.081 397.519 m +249.065 398.401 249.213 399.58 250.389 399.6 c +251.229 399.615 251.828 398.953 251.967 398.242 c +252.613 394.892 255.037 390.776 258.746 390.085 c +258.788 387.649 l +255.133 387.669 254.31 389.125 252.798 389.099 c +251.454 389.075 251.604 387.692 250.555 387.673 c +249.253 387.651 249.226 389.204 249.222 389.414 c +249.081 397.519 l +f +287.488 418.394 m +285.527 417.646 285.381 416.383 285.399 415.333 c +285.779 393.581 l +285.797 392.531 285.987 391.274 287.973 390.595 c +288.016 388.159 l +279.099 388.802 274.817 395.91 274.671 404.267 c +274.525 412.624 278.557 419.877 287.446 420.83 c +287.488 418.394 l +289.863 390.628 m +291.824 391.376 291.97 392.639 291.952 393.689 c +291.572 415.441 l +291.554 416.491 291.364 417.748 289.378 418.427 c +289.336 420.863 l +298.252 420.22 302.534 413.112 302.68 404.755 c +302.826 396.399 298.794 389.145 289.905 388.192 c +289.863 390.628 l +f +318.198 389.316 m +307.112 389.123 l +306.944 389.12 305.557 389.18 305.535 390.398 c +305.525 390.985 305.896 391.37 306.439 391.548 c +307.901 392.035 307.855 392.286 307.828 393.84 c +307.444 415.844 l +307.417 417.398 307.454 417.651 305.977 418.087 c +305.428 418.246 305.043 418.617 305.033 419.205 c +305.012 420.423 306.396 420.531 306.564 420.534 c +318.448 420.741 l +318.616 420.744 320.003 420.684 320.024 419.466 c +320.035 418.879 319.663 418.494 319.12 418.316 c +317.659 417.829 317.705 417.578 317.732 416.024 c +318.198 389.316 l +320.045 391.785 m +323.439 392.306 325.319 395.279 326.133 399.2 c +326.327 400.128 326.826 400.388 327.708 400.404 c +328.674 400.421 328.772 399.624 328.786 398.826 c +328.948 389.504 l +320.088 389.349 l +320.045 391.785 l +f +344.021 421.188 m +351.159 421.396 357.208 416.545 357.901 405.719 c +358.535 395.859 351.54 389.898 344.569 389.777 c +344.527 392.212 l +347.082 392.593 347.167 394.989 347.139 396.585 c +346.827 414.474 l +346.799 416.069 346.631 418.461 344.064 418.752 c +344.021 421.188 l +342.68 389.744 m +332.013 389.558 l +331.089 389.542 330.038 389.607 330.017 390.825 c +330.007 391.413 330.378 391.797 330.921 391.975 c +332.383 392.463 332.336 392.714 332.309 394.267 c +331.926 416.272 l +331.914 416.902 331.979 418.037 331.176 418.275 c +330.331 418.555 329.531 418.667 329.513 419.716 c +329.494 420.808 330.542 420.952 331.382 420.967 c +342.132 421.155 l +342.68 389.744 l +f +191.56 374.148 m +186.591 374.062 182.49 378.02 182.404 382.99 c +181.705 422.984 l +181.619 427.954 185.578 432.053 190.548 432.139 c +414.512 436.05 l +419.482 436.136 423.582 432.178 423.668 427.208 c +424.367 387.214 l +424.453 382.244 420.494 378.145 415.524 378.059 c +191.56 374.148 l +409.058 382.81 m +414.027 382.897 417.987 386.996 417.9 391.966 c +417.368 422.42 l +417.281 427.39 413.181 431.348 408.212 431.261 c +196.865 427.571 l +191.896 427.485 187.936 423.386 188.023 418.416 c +188.555 387.962 l +188.642 382.992 192.742 379.034 197.711 379.12 c +409.058 382.81 l +f +endstream +endobj + +60 0 obj +<> +stream +xÚí]y`”Õµ?ç~3Éd’If&“Y²Î’I„Ȅ$ˆ ’ HØ£ÈdqE¢Ö¥€‚;UZ­Uq™$XhAÁµ"ZŸK µ€m5U[é†É¼ß½3BÐÖ?ޟùNÎÝ׳Ýs¿L2ÄDd¤fÒÈ=oõJ÷“© l(yŠ(qÃÂe‹– ûIõ‡D†J¢„Т+®Y8ðÅýz¢Ô«‰Ì¡¦sæýl^3Qn;úT6¡Àø @þ+ä󛖬¼ú•­¿L”‡1³¿½bé¼9tâ'ÍDÿX2çêeé¹I­úíÝ˖®XÙU@ӈVg«ü• –=»Ï1ù y@÷‘înÊBœ«Í¥\¢ÈÑ~ÚuêPßÕ‰ˆÐ{j £ÏTÀ}*œÊ¢1ͧô„î¢P6˜ß¦'(Di(?L×Sî¡«è}šù¥z„¾¤NM‘.²ÐZêâ5ô èUIïÑÚ$‚š_÷91ó@mßD¥e*ÝO:„‹#FäÛDŽ¢×Túµ6ÛPù+ïÓ½™K?ã 8¢{†Þ¢öê¨ëæÈúÈÖÈ6J¥o´œÎý‘A‘%è5i]4ÓCtÄH±7òc¬©kXKÏÓ¯Ù¯#]#Yé"´þm¦]ô+:DÿK'˜9‹¸™ßãÃzê<Ðu r~dnd)Õх4‰šQ›Ãý¸ZÌÐfhOktþ¡ëX$cO¥Õt5]Gií èCú˜5aSÅ4íiÊ¢‘4ƒæ‚š÷`MOÐt” \Á#8Ä·òSbµNë<ÓQ(8VQÿ.Ú +šþœž¥ô½‹1¿M5v±Ÿ§ñL^÷ð|/ÿœŸâgøs¡ÿ«iڍºWuŸw‰#FžÀ¼Y”MnêÎTÒàçAú3öWÌ%\Å¿~Q¢±.¥³«kpä¼ÈÚÈ+‘ÈG…h;’j±ç 4«¾†n¦=ô*ú¤·é$ýTÒØÈVÐÂÍ>¾ˆ§ð*¬âiþ’;…ü«WˆVqXókuÓuÏtîìÊèjíú²+Ù GöGÞRüŠyjÀY´ŒV(Ž=‡y^¡ãô':…98kËã±ßÍÿ( q2ˆÄS"¢Ô6ioè\ºÍ]v-éÚÜÕ©ˆL€li¤'UF@š¦Qƾ Ô|„žgÚ =Gè/ìä\ÈçóÅ\ύÜÄKy/çëøzPõ ÞÉ{øÌ:‘ 2@'¿˜'n÷ˆâ€8"Žk¤MÑêµåÚuÚ=ÚNíí:³®D7P7Aר»Fw­žôZ‚ÝðÖ·Žo—tÎí|°s׀®Ú®Å]ë»^ê:Òõi$9²7r‚h ÖØ@‹°Æ5Øÿ­t'= ùxkü=}FŸƒç-4NâL¬8Oñ­란•Oç^hâËAÿfÞÁ­üïã—ø þ5ÿ†?á/cõhÁ4±{xPìañ!à”ø—V •håÚ`m”ÖˆÝܦݎý< }¢Ð ]†nnŠn­î5½¦Ÿ¯¿_¿U@ÿºþÏ æ„Kb6âŒÁ£½%^ҍҮ í4IhڟÅoD×ˆÓü ‘Ã/a¶m’6IԈ Þ)_B¶Ä­ ž°‘9±QŽ!¶ˆRmº®@K¡•Ð73Ä­¢‘ãè´ I[­ÛÅlm«înÝ(þ€ÖbN&þ;US5ïÞ£åàP©ö¬îm9¢Þ }«_"L‘ÛtŸé…öØÁ‘,´7ywð$aµâNò!oæÄçC?„äïâéT©;¦mãÄÇ(»‚îᗰÇ=t…ØÃ?_*¡Wò$Þ¦ ¢x9¨1œ.÷’W,^Èó4úßÄÐÜÓàM¾XH:Í$æÑaÑ®¿ÃV1€o€œ.¡õ¼ŽJ¸“÷Ñ[â.Ê ´_}ëê,üm·hc©…OëÞн!té%Ps ¬GòlÄ4h¦G+€ÔT’^”@þgÁ^@qНWÐe¼Yûÿ\TÓDZ ­cøþ®Sºjm0(¶Ö¤&a¸ôA}Ž®ÿŒFAáµëºÔ 붺4–>b;_ʓu1^‰\L;ijºO"Na½†u=ÇAΏ¸yy$™'CÂ/Mx¢s‹n½îÝ*Ýõ8›NÃjÞJwӃô2N“Gqn‚Ž€š3a{.Ã1Êiv7ŠFÃ*ºIt1ìi#¬äBúZËûœÉ-8¡Æƒ—¢ßBºå+pB]G7@ÿo£ °÷Ócô®xR<¬yÄíâ±Z\FÑGÚkZˆ/¦ÃºëÖÒʧɜŽ™‡Kyè·!òfëOY°þÐRÈ}äóȑÈ㝇0ÞcXûÝ £éó„*¢‰üw]&ëC£§M U Œ^9lHÅàòAË”–ø‹ûôË÷y=î¼Üœì¬L—Óaϰ¥[-æ´TSJ²1ɐ˜ ×i‚©¤Î7¦Ñ.h ë +|cǖʼo +æô(h »Q4æì6aw£jæ>»e-öjж u·d³;HÁÒwÏ>Xës·óŒÉõHßQëkp‡;Tz‚JoRiÒ:¸ëœMµî07ºëÂcV7­«k¬Åp-ÉÆ_Íci µ“‘LF*ìð-kaÇ(V á¨Ñ"È`¢™¾Úº°ËW+WÖúÕ͙ž4¹¾®6Ëãi(- sÍ<ßÜ0ùF‡Óüª Õ¨i 5áD5û2¹Zïn)Ù·nC»™æ6úSæûæÏ™YÖæ4È9,~Ì[v\{Üy&‹Á­5õ·õ¬ÍÒÖÕ9/sËìºu·¹ÃÛ'×÷¬õȰ¡c ¯è7¦qÝL½D?ōÙÄ- õa¾SºåN䮢û[૓%—»ÃI¾Ñ¾¦u—7‚5™ëÂtÑ5žÖÌÌЮÈ1ʬs¯›Zï󄫲| sj³[l´î¢kÚ\!·ëìšÒ’³%JؖԴX"ÅÔ3± »N¥Ts™Q7eY®Èw>"ìžçÆJê}ØS¥ TÒºy•h†§Ñ+<¹,œTӸΞûýáâb)"‰5à)Ö8J凔–¬n—ù–™Ýˆ@>šÚÎiQò{<’ÁëÛC4™póäúhÞMs³Z)Tæo‹FY³/^“1MÖ4Çkº»7ú É;IzýaCA÷OšÙž^×4"ÌöÿP½ Z?~Šoüäõîºu1ڎŸzV.Z_Ù]K…Ókêµ,K‰,MÕB(gv7–™ú”°®~”PÏoO4@*U »Ç„͍c£aƒÑãùÚ#_É^*:Ó-¶ÌðÿÙùÀYù³–—²NÂubüÔëÖϪ ´nÝŸ{̺ÆusÚ#Ís}n³oÝ.x ë–Õ5Æ9ÚÙ½>+êêw¹‰BªTÈRY(3n™¡ñ Ao…ç(Ûgí +5«Z*PùyíLªÌ/cš×.¢eæèDj¢|×yíºhM(ÞZ‡2C´¬9Úº(Öڀ³¬Ù ·¬Œ>ÒjÔL­ï)JÉJьš#GueZ3|Y7½ºÌhߒ!ÊÅhq<ÑWÅ«éoº>²~äú$ëÎyÿ¶›\ÙÅÙ¢2w\Öy3³fä-ͺ" Y[²·ä>¯O[eߝ}@;`}#ûÜÃ+–L·›˜-9G¢ÎcIN™šØN¼ »nç!‡×àÀv/µíµ²µél.OñSÎv.¹Å¡ù›YË'tt˜ÿ>kyÇqªê¨ê°/ vÍƒÒøpʔñá|l¨ÕnK€Lí̲ååŠöÈ•òi YË?»}pùÐaC—ãôKLHðyiH .§Ä‚Ÿ7!QWúíãöO\úvuzªÙixêÆÿí:Êi¯¿ÍÆé®÷ï¹çp&ÿä‘×F NsY,æòéœõÆóœÐõ·×?óÔ’z/Âϳë-ð†²im¨Øë*w…\¹æ¹Vº~äJL7™ëm6¯)!%©^¯÷¦Ø³]÷edx³µWD;ßûËìSŠ‘p˜þ‚…Ru:½;c¢m®œÉkŠÁ Øq'¶MUÁª¿w˜;Ø|êpzŃò,Îð I·xäN|oÁŠ¡ƒ=_4!6]¿–Çåefæu:s33syܩܬÌ<½åû&ûWGºÕé´¦;Ä.„N¹³Ò®tÓ!ýh—†ªӆ‡”Næib–i>/ ›VòuÅWHޟ°Ïøaâ‡I~8èd £Á‡ÿºÄ Úí)ÜP²ÛÅ3¡dWYŽË•ãµg¤©¼õõT«5-՛Ñ/O櫽e¹^o^®·›ú—¥2²v{Fj™'ÙØßÃ÷è)/Ð/¡À“f`CæàJuç¦åL̙³4G—ã*¿ôÎ3r#‰¼’3¡‚¬:ᩂà˜;,ÖáÃ͝#!INî›ô½™³…¬(eàîÈ7Tùf§?ÅäæÝ‘OiPä·-…¾ÊØÓÀËgÑröû9c¨”7°|ÞBËà³28ސ˜a“òÉÅãŸZuýoVtu¾øû oIöt-•áòå2Ô~òÞæ-‡oyà°6wË%3Wºò¹®Èó] ’gŽôt‡.àDªë²»½³é®wA·v@*ghW–²]ŸÊ%I—[¯±þØzÂOÒ³½Šüy¯ûòò¼>ovVÆnñ 99J²96o–¿Ÿl1±èÂü¢¢~ù^rªMùŒúD\V[ªÙ˜ß/@þc•Ù£KÌdyÙÙYƴįEbf)ÙÜùi¾I¾fß&ßvßW¾Ÿ«¤óÎ3ò|¡ùä,ˆóɐªp¦3¾HÖX¬Žál>œ~Z¤eÛEÙז•_Áí‘c­–Ì +òûû,1ö=—nKµ[³•aXŽ x”C’-ނ¨%è¥8Ý|âÑGêÆßèJ7¦¦û*\öîå•J–äfºò~½U†ÚÜÃ÷N[™÷eÖïèªP¬±ZâÉpänXـîn*¡7Bù§³Ø”•™%5>g|Ùøžñ¸Q¿:õÖÔûRK}5ùHr‚ÃÀ‰’#:¾2”aÐé ^6ے2,if‹Õ¦w¥ôoçGB–Ü@~~b€™R<®dÛíºv~"d+)1$¹ <¯R¶9۝½,{o¶Úw¢­´øi§qÄš¿QÛÒ!‰nÙ­Ã{)@Í5¡ÔÌ,crrfR³Rò"å¿ñFZ> ÂÎqŠYl½lÁ8ùË弛_W‚]¹jù´W‡ÙLf§Éýå÷<³U–n•VG›+‰Õùîùs»M.KšÉ3aÝ*Q& ÿ)èX‰èîÕæR‘va(\d/tܪ=iÿ¹£]ì²ïtGèZûFû³ö_ُڻì†í", Í 3d8uΌ"Ñ_W”Qè¨ÔUfŒÕÍ˜®›n«Ï¨wÕ-䟦ŒEŽE®EE×é®ÎØl¿ßñ˜Ø¡{³×göT .gOE¹Ïl5»¹ÜÆ\^QcµZݞ +›ÇS!àd¦ÌczÀð܁ÌAò@~À(¨ T¡ššªÊÊ*Ÿ¯pÀ€Âª}E;Øé®y°ÊÜ©ÎbÖ§x<ö”=ÙÙnÏáÓôK!™uå¨oó=XhUí<6¤å”ÅÜ}Ž«ÖhÌ4'NîæDȪÂÃò,œpÜõ³ÃeF ׄãNpm–ex™ h‘Díņ̃ù¸,”±8“œæ<½ýmü¸†í"käÍ6Wy•µ=r¸ÍQ*ã§ÚlE2þ{›Õ'ãOÛR2þ]kVpTTf vRú0BÈgŠþæt6‡ÐÓlD7s.ú˜sMö*³·»—ê–†'z?gqšÒ*ÃÆµ"ŽÅÒ5šµ\-lpäÓP’5¹Ê’›l­B«OC㐰íŽQ£Å:ª¦:×ZÅ2¨–m©bÔ Ë2#… ÆæJ«bxŒ9îQiÊm®¬Qf›9£ªîbk,®ih3ÛFÁ/82!á "ðȀýç<Ý Í‚øØÏò˜Ïrâ༱·à,­`>KM|b;ßX`KËÌëú«TŠõ]»ºölPà—¹™ié|cדùé¨?‘çråÍç,Ι/U脬ÍçWº6&ÚMQŸœ‡w½õÏMöD8/c ªFú|_²%ªU)v´ê>øì›¡UåÅj'9­N¯ßäq á!–‰¦ãtú¿¼ÉIéãÓÇy›¸ÉruúÕÞÛÓo÷î²¼˜¾Ûûª÷oª×™”6zšO…¤Ž«ÔÑÓ@½?†ÒT±µÜj)O7šå1’k2•YL&³Å Æeys›s9w‹7N|–×ç„&; ÀÚÙJ.0`P¹×_žž$Ô¡§×oa½^°7‰)Ó¦N%Ç@;ÊÒ[º73½¼8_–.),,óæû¼Å>ozy¹Ûçµù|^ ´›ØFÖtârTX-L†\½5‰ŒÞ@V–-™ …cRB~ xPÀï/N¥ÜI¹bYî±Ü¯rµÜ̊Iz&½YïÖ/ÓÓ¥OлïV†^Ý"ŽÏZ7uy·ŸìÞ’ü©RΒcøm†~}Ì`#úOæzVÙ9Vù;«ãYsï։sÐä˜)_>˸ÿ ‚g í`¸¢ëZWn¦)Ã~R].x:_¤.'ò2Ͷ_ܬä3[]a¿­¦Œ$eÀ'Š–¨˜AO¿¿wÈ_{ÈWT 'Þ|ñeg§O’ êw4ô›ä{ÑÆ¢~S]¯fš‚‘ÎÀô‹ÂîÈÑâpI ¾åÉXÅõ´‘—ªU+²ð®ÂÌW +î¥òjžÆób—|Yƒ1eëµØ§Ä"'Ä̹IáCÈwböf…Í9Ž@7‰M Z=úI”ãlG$N%b +×b‡ò7åÏqm¦wèºÈ ¶"J‚×ÄQ†ôhµ…6‰) "GäÈ0Šñ‡× V¶VÏ÷¥¿ÿ‹â õ)†(>~¦Þ US;v)°¿‡9 ëNWP ~íAàËø2z²!i§\œJQJ­éÆÅÝÅ4 +tÞÓ_D¬½ UœžÍ1zÆi¥çµÝ´Œc?È»äéj~+$n-ƒVÊò8¢ò¤Û±ú´K¦,a€|ìa…"ßb?Ց¿Sqä0ýUiêÌø¾ÒÒPCêè=XÇ|ÈͬafÈ¡ jçÑ\pm=ï¡é¬£1|1­§6‘I©¦©4Žë°ö7±îéàa­â"¤î®R’¼°KÉñòþº +7ôùjÒZŒ£úÈiº’ŠW¡…+Š®b-VQ¢ÖÑ@ýI¼›é¶c½›@»ë W3ې¸šSúß”–ä1¬ÿ*ìs!`X[ Ù4é¿Ò¿éwô3z…ž¢·h;¸| j÷Ò?è¾ +íï‹tD:Ðî-ÐKâ; +â#/â=ƽE)Gìc<„ºÓS¢†7p#çókü–oªù~ø ?|“?æy>,Û7¼–§ò06p"Òýü5ãø]þ›¸-àìý{S^JqEý?Ê;x _„²m<—!{ýT“dJP-ÍX‡|6òR·H}îʨâ'a)¿¢€_¡ÕCÐV"ít´ü¾™ßÇÊç7Ñ>|ðwÇñôÿõoc#ËUÙ åFú5(ô$¿ÀÿTëTÆéØþøuþQ÷^ãe±½ž?ē%*HLˆÒ¦;îý¤Äè‹9“³{ÆqÚBz¨x'ô]Öh¹Š[¹U•wAªeþoX«|°µ—'iµÊ/‚ŽÞD?¥m°$@á·!4‡.=>†l˜ €³ÈMzðáMÀûàÆÍ¨•³l£müg>ŧ ß‹ù9þ†ÿÀb¨†ÞTSCÉø/üF| Txs}¿ám:ȗóJ¬ð ½€5!Ë?†Zè/ö¯Ñƒ°·ò,À¯/ðƒ|ô µ»© %塘TÈݞ¨§¿ÑGüOðK~VDÚSØM¬a oæükÞ;ø +$wû¡N¾”kµ5ôºêÿ0¿È?çýü<À¯ HA¤€=óg`4Z»ÏÏŠ=ώï°Jò̈Ÿ?{Ÿ=qžò;¢(× çøž>\Çÿ¶öÙ;zµÂŀ¹è/q$»?l«<ïFcÍ ò°/á±¼0VÁUJ‹¤$Æ¥±—ýÐø{µí¿háwâÀ­=4ôû°·æþ >Gcÿ[,5:Žz€òËcV3¦åçÄqkú_ânëð=qÜZü·¸›ž°*ð:ÿ¦Òˆ¯wóõû0 Z³¦1þ¯Ž«qv+'nõ8Uöñvèð +ț‘ÿ"l°4ûx¿Å+OÓ eþÂûzs!NuXòVE= 'ý6z>nçz"Æó×»EXEÖp'ý‹MÊy@ù*ðƒ¬·Éð>t@éEÛQ[ªP¶ØÿX–4ÓsÐÔ+1m3î#Ц?(ïn¬`J¥g„vÙѯMyvà;Ý Ë*ýå ´l$ZIOù§ +>†7r2w7•âNó-ÀÂ0b=èk"@~~šËeÝ~`Üç”3ÇmÀOid%ÚWÖ±émö¶=Q³û,TbÜĽû€¨O{+}¦VEj|ÑYöGږ&Üንv9Rò>w¡:á›è6ÀÀzm§©Ï§½_RzÈ{p«´€r1ê@‹ qÊÜE+ì…>Ax'àîYޕŸØÂJÛÁy'¬F®7³ ô4$l§úœhüÄÛÔvÑÿÀ³kV5ÆÌíN=Û¤°„K¹PJÄiÈðpkãN‘*Rqß +©[àµt­Šå„AœS/ȳ@µØ¬ 9NåÁ<x‡âö‡w yw«‚î8ˆÞï#sôÓ\j¬èŸMîUö?¿›«9=r4Õ÷AyšFcìý$úùIÎåý‚0ß XgF7È~ª÷1bô|»œŸ)P!ry°ƒ‡³N¼ *p ‰î|¼YRŸ#/ÇY-y½|x`Neɹ¨¬¬­wá&²_ÝÙo„Ô¼ R;Ñoý ²S„üèùýðˇ+ûi‘7.XÀþòÓ¢ˆ¯FæàF!gÊw%æÂ¿Ñlô³a§²÷ZŒ¹T +“0©wEw:-TšÛ* ¡›ÔÉå€ß/oäFèÑtè·¼Ám„ÝMÈSL[%ñD÷yçÃ}bq d 'åÁ k‘Ô>©8ùT9Ï~ÐAÎ/1®7Âã*VÄQŽ$0ÖJh†;’Z=vШôÕ¦è„uÁÏ~œ?vx]÷óùZ{©ûµðË_|=Yø%uüÂíhÿ ïÖFD¾æ—Ñ« yü[¾.f-â6,jÇvțþ9ø]žÈð›gnµg£ôP¤‘Ö'Ž=ßHtB*â‡Ðó]BOlS¶²´Ûõ|ÏÐãïz¿è‰fÈŒÄøYz,¥•Š¿§8 ý+Q¶ {Û z<‘¬H÷€³<€õ½ W?aâàÒý +½^ÅI¹½«È>Û"ÔÙÔ(²;(òEäbÀ €¬H¢\»Z#Ö"?Q®Æ®îå«þÛÿÛ^~ÈÜ=@j¼»[ £CAÈe±E «Ó½ئ¨+_WÊ÷¨‹ÖtSàM€ŒçdOx4°nE=Ö3(Š`¶@Vã|§Xû6‚NÊw¸ÏþŒZ`'€">ÈЁ´°çóqØÓ¸!ÈVN‘GJi÷~DùAÂ&zžztVJž^òov@Ú|œ¯¨ÿÝ xŒ.Ɗœ8…ä‰Õ^aÔmFn1êr`s~OGpû¶°ÖØ¡nç ቟f¦¯á)Yy<_ÀCÙÇÉô[¥åýFýEÉ@ØëA ¶¼6<‹ 6€±.€|ŸBÏê„gîÆ)7 vށ2Y2H–ôÊMð«nå»ùô…{á¯D&|ûø½6þŒ ìV.Nüø:¹ðNJ%€6mTÕÝJþ­ÕZiAáùž°(Ô Í=\­­²x;Zù”—%a3?*ì"7ˆzúwÁOÕ­â dá#¬óÿëÑó®ó+{ß¿¿×«{ê½âø}¼÷½üÏ:î‰÷¾mν_!”'úVœw öº]ð9 ~æqHßÅ4á àhZ÷[òR%‹-¥h?<¹<¨Ä؉êýc zo€t ç4܂ñ|€Oa’È«sáé(üÐwé}”Û ;6žÊ*éË鸭Ÿâå +*¸FJ ;¨ü‡HßðTž‹kq*ô²2) +)QèmÙXèY.=ö¡Űåiê,’ÄTÄiHI¾CÁõÆ.nÛå9Œ“›§G^¢—À_è.ö.uu%Ú/ƒoR¯|íÅêïŠöªS z»½Ž_åc\¢´?ˆ¹P3¯¾E竹 ¶ôj@3÷ÉլN•Uô)Òx™ D)ÿ°ð¹‚ Ùø5þ%·£×B~§önÌ|þÍ5”D) ÷œâŽs +ç7ðÎÞ|Ä'ùcœú"|NXá]þ»·ÜvË +ä¦»ÎÆpXéËîWï8~++ƒlÏ+ÇošçA£G‘ƒ'Ñø.ÅT*üˆÿÁøÝ7¹Þ:ð=sófHßlèûvº6£¥·÷<͇.%Ár¤"N'oá %£f±âŽ|{ô RTþ¾§VÄ;bŝà.X°%t +þÀû1›µçë'ˆàEêÍñ#òý§|ƒ üø/¾KÁþ#š/ãeô8} hM\”u> Þ1EÃ^DyŽ)羝;ç­\Ï;¯|nÄLòM£xÿ5?Â/óS 9y:ÂöN¾ÅÛøwü;ÁQà-ðt÷ó£ê+ßîžû^ó5œðW°šê½1m‡/¸ú³÷NN¤gQ!Õâ¾s7dö|èÓëðñ\ð–2Àƞ‚•ZpC†” eËéúBýN¤‰¶ðKÿ§<Š_€ /]¿—jp¯š¨ôu9¼óº‚spG¾3N£ßÂ#¾}ÿù=ý5£ÁÁ¿âÔ¿ÚfƒÅZ ·Ñœ€·`õòÍ ¼·ƒ ‘`¶©³cXäÍnKÞ¦ìu÷o1c6w™(Vºðnè»#‘˜Í¯Q^|j Ò°J+vþ2õ=?ì=ù¨³Q8¿µÃQÔ/ŒbÂ3gÐÉ7½EdՇ}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}؇}¨‰R¯Õ~DçÓ&J Af*£‹‰´?ë§«ÿ+ýËeêò©ïAìýri7MìÓöµNjG4BEm©ùåÍ2N6©¸5ipUu™¶–Ÿêh6µ±òVeéFU¿]ÛCaà>à;@Y²%»Q²%»QR¥µkÏk¿lÍÏÃÔ;Û\ùå_Vgjm +í.m=y0ö¥±xv,ވ¸ñ¦X|‡¶¾5—V„<ӗ#@½mk=obù.•T‰­ñ’­m(É«viÛ°ªmXÕ6¬jVõ%Bƨ[Q¾å[Q¾U•o•ß‚¡<ýcCÅÛZÓì±$ªZƒv1•cˆúX<]»¸µÓ´e¦é¾™¦ú™¦ gšÆÌ4•Í4µóܐÃoúØoÚä7]ì7 õ›†øMƒý¦þ~Sµ…x:™èW*­ÂrzU˜ÃÓ[M”ô_B4€ wznÌ;ái×qkÞ͞v¢›¢¹K¢Q@þ2o gQ^I´¤ å{^ÔašÆOQ"ûC%‰o$ÎN %OXšX”X˜èKÌK´¬³!Րb0 †ƒÎ d°µGŽ…üò±%˜e” “¡N¥Íò?ÉW$ÁAã(œ®㧌æñá}óhü\wøïS|ílœ<#¬÷æ°u<Ÿ:Úæߞ¹(\éNštI} ó ȅÅííLSëÛ9"‹nɒß|¹‹˜Kn¹#+74È>õ-:¾ã޲¯®rVYGY†©ýŽ 1öøÎgÏ/p+É ß?~J}øÉœ†p¹LDrƃrò‹2w‰J1´®v—&£†ú]ÆfQYw‘,76×6œiGn”×î"ŒT;rËväîÕ.W “íúÉ(Ú.WµË=«]ËHO]m‹Ço3Rµyv›Eg·Y¤Ú,ŠµÑ¢m<=Ú$#jãIû҂ƹóšd|­¬ç«m¡kë¦Ö·\ZPÛ:.4®Î7§¶¡í¼9ÅOŸ5ݏãÓµÏùŽÁæÈÁŠå\ç=ýÕOËêóä\O˹ž–s:OÍ¥¤bi Ñ 53£q›H6B€³< £íæe£”4<βv눧dC8Å7:lʪÒêÒjY-“U©òËdcUΞ¬Ýüx¬ÊŒb‹o49ë.«Åϊ±ÄüY!Ÿ•—®¸TÅêgÅÊU@õ­&+hÅJªS”U΃}Ê2K‹,­¶¶bEÃÊ藟¬XEr¼•283|wjFæg}gʊޏ” ?EíXÅê›Uˆ Žü§~ Cr‘±QþÂa©¤ +endstream +endobj + +61 0 obj +<> +stream +xÚí¼ xUÖ|nUuõ’tÒÙW蝄¥Y-Cš„Mv ‚HU@ÐÁÅ("AqcpWÔ±"t>˜qqÁ™QGqßutÜHýï¹UšÅù¾ùŸÿyþ'ÝyëÜåÜýÜsϽu;$ˆÈC ¤R`Þ«í{ëj„>bÚ¶Ðøˆã™*HA­ˆî£‡ÄBzˆöÑÄHõ0í¡ú#eP9ÝBÑ ´žtš+i2¾„ß ²ŒêC·£_n§À;ÖÐ^J™ÆÇt ­Sÿ‚TëÈK]h8M¤e´IœiœO3é-m- ¤3é\Z.Œjãjã:ã.º›ö¨4ŽQeÓ<|_0>süÕxƒz!ōt½%®s?Ba”ÒÎ[é<Ú¦Î҄qŽñjG¿D4G/ˆýJ¹×Ӈ"S\¤Ž@.wãIpåÒ,Z@Ûh¯ F*yŽ™Æ8ãJG«‘ëMÔL»ñm¥ßÑk"Þñ…q—ñeQOö´Ð‹b¿ÚvìÒ¶Rô˜½Ô#fý=C/‰ ø½²Ìï(r„¿2^¦TêGÓPÛ{‘òñoe ¾—¨Ok•F% _®åÞ¦§èm‘-úˆ ¢Jé®,SnSÏ#Jì‡o-DoEîoŠØ­Ä+Õ;µ´ïõNm‡ŒH!ÝL·Òï…- ˆ•â2ñŠxW¡ÌVnVÞQoÐî×þ윃VŸMKi=@ÿÉb˜$Î ÄEb½¸VÜ$^/‰”áÊTe±ò¹º@]¡þN+ÃwжR[ë¸Âq•þQ[uۓmjû·Qd\A“ —¢ö7ÒmhÙ:HÃ÷-zG8DœHÀ7 òÄ4ñk|׈MâqŸ¸_´ ”—Ä;âcñ¥øJ|¯¾º’£ä)]ð *ç)¿TnPnQâû’òå[5Cí¢†Ôj‰Z£.C­Ö«›ñ}D}[ËÖjú¹È±Å±ÝqŸãÇ_èñÎË\äzþ‡;õ8öfµmhÛÒÖÜÖb¼MiÃlô‚ŸJPû9ø.ÂxoÄ=Lñè»lÑC g¢gf‹Eb…Xž¼\lw˺ÿV<Ž^zU|Ž:{•\YçÞÊ¥L™€ïÙJ½²BÙ¬\§´(¯(ß©N5NMTÓÔêHu–Z¯®R/T·¨õyõïê;ê×êøšGók]´B-¤Ôfkçk·ij:f:žs¼¯{ô¥úz«þOçÎaΉÎIÎYÎkœ»/»j!OÐ#ô(E}ÄaõRµB}„®Vе,åEåEÈólªSÇ)Tå>±A¹X´(ùŽÕúPe¨O_h…è맕íÊ×ÊPuœ+¦Ð"¥Ÿ™›žªí)ў #ÚãhۋÈyµ/Ö(ŸëñÔ,HŒ2ŸRûj!õ9zM}K8µÛéuÍ#2Äå^u"¤àwÚ0G5å©·ÐoÕâbzD©€ +ýÞµr<^ì„^˜*ŠÄ7ªAª2R4P}—ÖÒbå¯tóxýFÔiçÐÕT,.¢é̊îŽsõzšxVY¨5*)¢…í~´n°Èª#•.³ÔmúçÊßè|:¨yèMõAÔþ ò[uœö…c²X€p1]A+ŒKéBGµögq©¢Š +´ÃÐn©EZè%Ð*3¡Óvcvï…®ŽCH&$çLÈÅ4hˆmøn…žÐ A 1ǧC‹½H-úT¥•Îq$h"í¹¶É4ø‡n2Ρsë¨ôÁzã"äx½O×Ð}b]Û¯i9uÆÌySœé¨T:*^J£ò7eвåÄñEoˆLúßßÂ3Ìñ5j¯Ò*56‡ ÝÝ ao¢¹4†ÞC+?C £ÔýTÜ6^i2*Õåhï[4ɸ×ð -0–Ðzœîv:hŽ3„1Žˆ?£½¿¦ze²±J­o[ˆ~¸½Foýs¥¶B[«}K1ç·@ßìÀ¼Ù‰™ÃsŸÂg­[µò¼˗»tÉâE œ3¿~î¬êéUÓ¦N?<\:ì%C‡ 4p@ÿâ¢~}ûôîÕ3Ô£{·®…ùÁ.yçN¹9ÙY™éi©)ÉI¾Äo|œÇírêMUõ¬VÖ"…µ­08jT/öç `NT@m$€ Êy"ZÉ8‘3 Îù1œa“3ÜÎ)|*éÕ3P D^(ZŌIÕpo*Ö"G¤{œto–n/ÜyyH¨È\PˆˆÚ@E¤ò‚µåÈ®)Î3"8¢ÞÓ«'5yâàŒƒ+’\Þ$2† éP2*†4)äò¢R‘ì`yE$+XÎ5ˆ¨sê"'UW”çäåÕôê#æçF(XI I!‹‰è#"NYL`!·†® +4õÜ߸±ÕGskCñuÁº93«#êœ.#)„rË#¿z/ó¸™'¨^›£6Vd. °·±q} ²cRutl?kjÒ*•µ•(z#:qì”JSÖÕTGÄ:à–p«ÌöÕ+8¤vQ â–4.ªÅÐd7Fhò…yÍÙÙá=ÆaÊ®4N­æEJs‚5sÊs›R©qò…»²Â¬czõlò%™۔h9â½ÑŽúö8é’ìì;¹½g×(8 Ì  &ÕA´i?êQã¼A`çF U¤#²0âQÛèÂáœ>â(ð_$ xä'†Ì±BôßWÄN–“vQC¼íŽ„B‘=XDœ#0¦¨ã0éЫç­J0¸ÜA÷ÑDô휚!}Ðýyy<ÀWµ†i.<‘†IÕ¦?@ssš)Ü'TQj9f¿“6cì˜öäµAHr ±É›q¶ÿ%úÒS* ‰ˆôŸˆ®7ãÇN Ž4£:PÑXkõíØ©'øÌøAíq–+’2¢ZÍQ,—’£ÊXåÌvföTÇG´üéR¨ëZ.H¥ Êˆ¯v”ù¬ñäåýÌD­ÆœJ’ãɬjF†„Nô=ÁBõâUTËëØ©3='ÄAÔÌG[OS«ó#"4 3³­ÆþAŒšœH]6‚ få=1Çr×àÃÒÙ«g%]cce0PÙXÛ8§Õh˜ ø‚{”?(h\^Qk N«±÷ªœHåÆôÕ1¤WÏ Ç46Ö5‘Z€bÂ9MB:ޏª&2!TŒÌ ó‚ÕõhKÓŠÏ›Z;.…Êš‚b䦰Ø0eFõv%¦V7+BQ[VӔ¸ê=,2TáPdO€=4V kš—äÏÙ&j±š þy­‚d˜Ë4¯U1Ã|fA…² 0 Ëy­š¶¹5„¹Ì°“»›ÅíBŒcöV’‘æ§ ž©ÕaÏÀððÐð0¥TApP3Bö‚w¨ ]ÃD©ÈiBž“ep«hhÎÙ#sšlq6€“ÃÚÃPsf‹Êå™ Ÿv¼ÓfTïFÈ_>ÁQÆÖ´¨Dô’Љå|z¨:^i;ȑžA9ž¨è'Œˆ`dvpu·.R¼0ÁHÚLM42·¦±1€o½2¯ªÚ|r”虋œj" smޜ\ÈÄqo<’J¹Ú•Ë:¤½´_Û¥‡ÒØÑh™wÊÒPûˆ8‹ŸòOV¿é +šåc•6 mœÙ8ò˜éÄ[õ€7!·F怚l•5rqš›`>Ï¥+9¨Éà˜&e|HR!iã˜`E8Xt`°òu5ÌäIÂÿ£L"Љ™y£o¨í–Ïœ¾‘sNô.h÷V2`£ô6ÕÚ"§l^dQNdIM¨e·¹s{Oð!2ñHF-–‘‘†ysPE¬7£ç0ê¹fòBÝȖӼ9Hƽl•97tB–Ð * +qs" µ5Zè1 ˆ8@óa>ç°Þ˜h¶g"”?ȜÆ)HK‘¦&Äù¡9âœIäIËUã2‡{ŒoÈK^¦*ʄ;žâE¸¥*³XÐcƗGq쥂¤Äá¾(ÖDã¨É*Ý_8¿*±Àç{)Iø’ÂIµI Iš?§Ló‡½^eZR²Ï‡g«q4œ”˜—ž€g¦Œk5¾kœ>-)ÁçÓÙÿYK|¼t|ÓâõÂñ˜]»ÝUI«’]^o+×,9>>Þt$Äŵrdr¾Óg…9}’+<´jŸó ó-§áÔüÎRç§êìÌõrfÆÇãٙkàŒçҝñ\–3›‹vfuî?134Þw4$?³V„BãŽÀq,tü3kE‰Ã|ÇB%ï…BTz¤´„‘48)yp¿¾4K¬˜E+ršÔ´VµOسD ä‰Ë·eIœ3‘(³´4TZœ<¸4Ô·_MÞ=Ø¥°p@ÿä3Š‹Ò3’Š“DjzqÑú»èê ú'/9tþ¢—×Öné³ëXàÁó/¸û¾_¯¾ýŠÛ6~çv¡6N®$|W©$?à÷O¿öü“Xi¬ñ‘ÖY†j'%]J\†ŸrӔiê,Ç,÷´¸zu±c™»>ΕÖj¼gv5áÉìê”ËÏ®És|—úu¶Ö/yHV¿ÜáÉ㲇çNJž™59wNòÒì9¹«õÕi_+_gú(]$z32&¦×¦/OWÓs7ûvøŸOËÉõ8i¯²“„±¿…Å@Àn +Ë¡ö !nLÉÕâ2 a_´KU†-UÒmIÞ®ªŒ°·Õx£…GÌË2Âõƒã),^ÎÔݵGÿˆWx³ýðí*(ìÏôÑÎÁþ}ýŸþ˜ñƒ™ÕîªôbŸËŸ)ÏKbsÁŽ¥ ##;¡`s’HÒ¤]•)mªVãá8iW¥²üÀÿQ8ƒe(I‘ÖU¼´®tV^ˆû.ÆÆj³veڋb¦­[3͹öVelÎ9²¤œö’rdIðNâ’r4.)ÇÃ%!´-ÇyçÄs™ðÿ ËÌAQ»I)څm´xjU°@¼Db3í ÅO¥4ª‰³ë$5´OjhŸÔÎñ\,¥[:ú[G §J%-g%p-(+¿ U¬Þ•dz%4þh(z¾˜+³/*öÑ(E~l|E}ù+Î#Vߘ[ã|G|G’2óK6yB|jJaj|RŽHö¦å‚]j¯à?:ñŸ'ÛC¡ÇÁPì»–¡[Õ;ÃÙ¼D—‹vQô|i©©æ‹Óΐæ!?Ғ‚Iý ¥²—.8àZ{Ñ=‹.ø́Ûvî +ζü†–êº3/¢Þ8~öÜê½ï>ÖU¹uÉì!7Þuì7JóêÕ·]{ìo¼”Ã^ìŠyí¥,aðÌޝ–É]—Q’R’È2UÏ®,‘ìôdŏÔG¹ªô×9úB—«¿oHòô™¾±ÉcÓ+2g:fº'ûf%ÏJŸœ¹Ô±Ô]ç[š¼4½.ó—"Í­;¼g©SS=gÅ/Qëõž%ñžŒ\͙™O­FR£ö©ö´ +ûªRósäþ"Gî5œÐ’æþÂ)wNŸúE‹´ÎØ!M3v°xH‡4Ù¤Iš_п¯SÓç Àì{ eZ梳ß[}æ‰ccî[dlë0Á”æððª„|ŠO`{"YÊi¼”Ó\)§ÒJ´ÄQNGJ—FÑ,ç +Å[ù’/K¸ÜÜP¿l6(¥žŸÕ.>Ò®XšõuhÖñÀeU–ÖçMȈ™Õa÷Ç÷\Ç\·&fՐ½¦¸$s/§eH#Q³ŒDVþ¾1JK•öaJÔN¤ü®+Ÿz]¤ÿúÓ«Þj;²§yýͻ֭oVRD׫/h{ûØ Ÿ^&: ïóÏ=ÿ§§ž;€Õ"ßøRéḠ6à¹rµœ ã¢Ü®(·3Ê­G¹=0肅ýÝ<ùp4daý÷z„Jé>w(Ñ£§cw›èëB]„÷ø^ÑÖ5ÉSÅt©J.ˆ†ÓUᮨu.w687;5ÂÀïpFœû/9u'ï1X½:YgK©‚QØÂÈijMË!w¬Py@Y¤ÂqR¸tK²Ì©ãÜ«,¢LqFÓüèµÊçè{¾#%¬~J|ï-‘»ÈcØC&N*.ö=Ë˶­)šTXùE-¼‘$8Âî%Âãõ&%xܰ aê—èN §ÎVSÜiÞlß1$îRquœ«Oòt­ÆYWð±Õ³5îQ¥5þqž÷½¦rÿÉûºï}Orr“.ëOÉI‰™^ Î*,]‰:)^òx:ÛvI!Ó^Ê Ï×uÕér»…®»š +JôaڊÄD¯½ïV¼qj¼Ï£'*‰ßÓô´[ñ;•È­*Þ§±-ˆW±6ª7ÖqØõ^o|vŽ\“àuƒ_•Î9¬m<ðÑw÷+Yáíß&Ý +â{"Ê«t¶¶’Ò€ÑÎNôKGU‹õ4CÙI1ÔNÖ¤óÀ»þá {9-ø§o%@m…æSØÞ=œy,ç|$]I3\~Zæ¨2Ž¡¼-Žgh>pÜwhïÒ}ú`Z +ÿ]H·O#Ȩ;ªn±XƒÑj15€.r€IÊ ´T;“úë&Çû¤2 yÜOo¿Ðêh<üõœâh¡mìÆI¬4Ži·Ðõ( Bܯô-hGú»ð5õQþA½ôºòUŽü/nCžIy¨£©(¿7h±ö¾”¡+€(ës»Ÿ¸oà¿ã:eýÀ3é§#1. À®ÊïÃ}Îã.ªÚƒ÷=ðÌd ŽgЇ¨›”Y³ ·Éñ4çÌíV^\Nžþ -¶Çyò|a™E]šì¼yN±ÌØTÊ÷b)÷Ÿq;Y¦Ú)æžö)ä:È9Ù²)Ï;ԙçÃp$}Ö²ÌrýlÊý²&ûs¢%Qmí+ç¨J´d}­Mí¾h§ è.äY«Ï…NÙA£´U4J½–æj_P¹Úz;ú" íoDù”&»öS1Ærü7ÅЭ ç!±È±í|ýyˆnEŸ®Ð)]´CÂáxÀøØAâYÇÊé>‰ÆBì7ã˜2¢ãþÓðÿ ”W@g>`|â8dhÏu<'œŸŠ¾@À¦o€®ØêZ,ZØ8êDGeZ˜†8Â4PۏñIƒžÇ\@ø4ÇÛ´O݄±>düM4Pƒ‚<œi4Gٝ†²”Wh-ƒó]%G'È\¬,ÙԖ×XÊ:ߒ)?¨Žù÷¢…÷,| |9 ™Ìⵁõ³\ £+,y]Ô.ŸÏÒÝ WÙò#§‹bä3>V.c©\[ ßíyв®´ÛÏú‘uëHÖs¬glþX•¾QÙ 9f=üͰæu cPÇw¬¹=Œñžnz¥q¯Þbܧ&÷éEpÿp÷¢/V·¯©ÕF›µžv·×R3œâìuÔQLK-}v—Ô7_Ò r­’õsëÓ%Žï1îЁ²¾;¬9ˆþD½kµèóm´íÈR×c>"˜É}"ǂ(“×^ÕÑϼm¢µêë°8m1%Éõ¢”¦£îÏÊ0¬©L9Ì1îÐ?¥"mtí~ªã±âvp}xì]ç“ו=qˆúi÷ƒ'<àÛ!û L÷J¹à´‹‰¸/œóÈ ™Îïv™&LÉVÜ%ûB¦‡-Â2Ì}<õ4š,í‰Oi»cMǺÝÙ@·cãJ˜÷!»‘n ×é²åz}#…ùµºitIùŸa|¯>€ö¬†^ÔôÑ”éh@.–m/×L»žçº“ +YFô¡‡Ùž¸‘µUè‹iÂ69 'QîU»ó7„¹{%Òû-½M(ûJ„sÚR¶eØFàùâ SŠÞ í’u`;å«ÓíêÚ9îºý°ŽzÑÏúZ÷Ea`/ª>ºt RLF qpóºG»”jUT¤öÃÜM¢^ڟ0W¿¥›ÕDš­ ›µVÚÈ~-…º©´¿¶%‡¤‰®üþ­4C+Aú t®6›VªM½—É£ÍÇX#ãjÈI>Ò‰|-ˆwi†Z…¹uÜßbŸ,£ÅÍÐFQ/™. +²®6bꬌE«Æ`LQ_vŸP_Ôµ½žvOQ?ÙNÎé˜G»™JÐOo&m›¤l¢€Êk4BGŠûŒ½èäÊŒŠökÄE@om= +\ +wOÐÿ6ý°ÝÐëÀ:ä½tï JÁa·[çì¸hp9§ +†#ÇØ{‚ÿ¬5€8jìeÄò£ŸÏ@ygh¿0ö2 ‹cú%”ê¼€RÕ®ïŒt1~GæÓ#”¯’ñïÓÕ駀O¿¨~ G·ÑÐôŸ7¢h€©µ6Ðÿ¥~ÿ`|“€¾²?£4S†(E¼b¼ +Z%^¡$õ|È oøSìþ´Ç á×Ëð˜ñSʌ6îóØðX츞ίì¢ÙѰå ]®£a ­ü@¬ßõ, cèO!ýÚ½§Á ê¡nã:A»žì×'PW†’ºfsÌ9 Ý:`^™ÞK#9‹Öƒ.ù¡pôÐTÐW†Õ»­;à…; aç€ÞJôýWpŸ‡ðC& EË¡–]™…°ÝVZ—•ß3ý÷$úî(ð°™þûÀ"¸ÿ `=ÿþï ¿Ý +þOîrÐ?˜ñÇfÃð8üŸÂ¿¨†{3hhO HFú- ¶GNڇþ×é©÷?—Âf™‡zúùÌ ô¢Ø=ÄϦöxž†Æî5ìñ?:3ˆ¡f?`Ïôì¾HôÞç§ö86Åx¶EC›fƒMÏv4Û²l?KûÑ¢rÿ&íX”K”jS¶Ù~eۙíWÐÛ噁CÖgïóe½¬u#Z·Š£tàr,º<ß*]¡{!ß_aot~Èßp·bíJÄZ·z÷+Ðàïú•½¦Ùºõ${š5í¿íÿO×ÈÿŚZdav ~,ÜÆ £±kñŠÓ­ÝÿëµüGÖèèuúÿê·×yîaTÄp†½ŒX»ô$;à4þÓÙ¹ÿ©?Öîøý1v‰íÅIñ±²gÛ3ٔݎ˜y÷Ÿ‚÷Ú#Çm»±ó¸}¾Y~ôQE4 ºYkèÀ¿ 3:X£Œëà_ãúŠ\Qü¬‹F)PÇq gˆM|¾mƒÿ2ø}Ú ’·ÚBÝéä9VnÙ>—ö!úLêÁÍ\ê ’&`©=Ö¼‡Dه¬º¼ÏÕf_i/16àiéZ<"ü‰ÐÅ©zôv˜îåóxP¨ú}Òñ3>ã˜þ+É3Fž-¯¢QÐóçj‡øìËxRžéµQ¢3^¾GY‹5ÔoŸÓÁŸÆgCΟ—­Öù\­þ%ÖÁéXݼv Ü*ùNh±Æç¸_Ò j•[gÈ©öY2ŸOñz¥÷&Ÿ<Lj>G~¶ñL*J­÷TÓøüE}_¾«YÏçîêxzÜz¿ñì¤ÛÜÏÐm®:ªt]"ß7mQo¡µ»Åy5Ý¢‡äû•iöºÊkâ)Îþø,3»ýLÓjs¬M ë7“Îäó˜èrít®J¬¥_Ês(óó4¶ ÖøF Î|_a|}êóNãyëÜsµÆ_оæÇžÓϤIêìûì3Ù{@_¡³µ+«cëb—…~9öc¶m›À=]žõ™ï{ø *%ê=\¥ìçåxæ1sx1‡yü=Öû¹2m5øÊÒ>̳ÇõÖ{»,`ºò7ð߆9z.æ +dP»^¾Ã»Üx{dº%æ{3} +PŠzÍGºüîÈ­;ã=m5JÈs5ã%ÕØzžòœ|ǘh½ ÌÒ6ÒTy¦yü`¦ÖMž[wÓ¦àBøóeÛ-*û*Œt‰4Z¶‘Ïæz!Î¥µÎH-^ç£Té C^ã¨Ò±‹òÕe°_öC×åbìÆ`\i­úuÖÑ<5‰ê¢ÒxQ| + +K¡|‚p¾u-üüî÷U:Û~¯fžOÓ÷`+Ö»\F=CÙ)ò¬÷„5–»“éFØ`Ú-a籓î‰øŒw€ï•PvÕ)­(cê‚rTæ_ f®…nV9#µé˜c'bD,–iŸX œiA,¬ðìX œiY,^vŠzüߏÕãÇ cðÂÿB=~,ß`,ü‰úÂÇþõø±~ΏÂó¢ãcðñ±õ€~Â>¶íiìMý«µÞ z&(¤¯íI>Çæ[þ¿Z|¿°ÿ5n°W6Ê,@ç¼^úûjcÒq´= škž‘Ûå×=€*³,NÛö˜Y¶„UfÛ.3ý±‡@ÿãO>0˓e³îÝ ¶YíÛ`•1ëÞvýqþ¶\³2]ä8 ˜Œô~Ð)ÇÑöˆ ã Ðß|.úŒU/vw¶úƒÛü(çu\/ÐwÚ6èŒZ"¬Õ©Î&Õ~MgJ{ð„µj¹Ô‡ïÒ}RßÐ}%T¤{a‡ÜJel7°wÔKþ«uX›ö li/&‡öe9Þ§ÙÚ¹T®î†]<úeÈ÷2țõ6Ûê•4ï*å;!~w²šÖ{Z¤ýâOªö!ê{íÞmƒ£šÒëÎÞðoƺ~;­vüš~åZJûô/ø)ÍÇzå×gÓ`Çe4ÊÞÛêKÉ툇]`Q×Všçì‰ðÐ> \÷zØu/ÑDôÙ@»ìöw÷NJEø=æùŠ”?à‡p¦¬3ê ;LÃÞ:Õ¾7à˜…>©“õ/ß9ÝOöèäøk÷hêætÃöêCܙ´CÿíÐa§†ä{ùùVß÷å÷OÎs¨Ÿc=Ú{wý=ôóTòؔßÇÙç°Ýn×H{1Y¾×²ÎÚ©¿ok |W"Ö®±í¨v›Â:#h?s°ÛÊëg{û-eo˜g +ûaŸ¦QˆßãÉ3‘XjÕI¾ÇÛY²ìYç>ãTAï¡ùú4Å1ý’BSœOP²s$e²}ætJ»n)¯ÑŽoa‹N¡BŒÍ{ +c‘ù^̨±æ8Ÿ¹½ +ÌÄd<Û +ã³ +Œ¹‡ðiVZÄç›û ÉÃïÏ-÷ u&§=öw‹ÿ¡¨³š·LÈ}H ÚNµîR]q=þîžå§ò´ôgž¡ñæ;U§xÇK¯]`ûa罅9zÒݶ£c©õ¾I¥mÈôn‹Þɲƶ^,½¿òc÷Y~Ž5ç™MO¼÷bÓ³-ZØ~/ç44úžÌqj–?áçžÝYgnÙ6=ÅýóLî8ÕOÚ?ES9&¤Zv,Ûïcä{~¾›óh¿ÃudàDT1ø>Á© c%a8—œËÎÿQè× àòÇÂøu¾Ô„q³…O-ÜÁPöҀvm,ŒIœú~]¹~+Ê\½L8Ÿ5!íÿŸú€œXI]ɒê¼þ$`e0œŸ[¸Ê†a0ì~·ûÑî´í´{A{íò­|ÿ¯ãø—ÿV»ªîѰîèٔïî駬7ÆGâ_&ä]š”bAG¿><°p=s%›ï*©õ§zy_±=ÍIr° {S†å·îßè:,;g¦9øî ª9Uÿ8ëMùsv5ûIÞÛ1m¯÷ѯuÇv¾¥ûòÝév랬Ÿu Ö]žç}µßÓüm>cйŸ6îÀ:é’cU*Ïw:~ð…ñGÇ%°”u¹…g-ì0m?ãaë¤.ïï¤û£½mgó˜ë¤q·eo³{ž‰¶Íðãõ²u¯ú Úñ=eÉû¥a¹¿ž¨-Ğ~!e©Ÿ"ö¿oRçÐp^3Ô3`[ñ›ÕÖ}Y>{xԄý2Q½/j~óý¾WÈ;9UªkAûÀŽnìS‰6ƒ’0¦^`ÆúEkÿÀû¦ƒz˸ þ'Ý °ß“[Ôñ-tü‚z9ŽÁ>xrp˜J_Ó͎Rê¦OÄ:ö }s‰ï˻ćŒí³oz5¥¹Ÿ¢‘Câû6Uà‹¡½Óäzdþ_Bì¶è3OyڜkÒÎu–ÓZÌãJ`”uï{¾ù~ 6(æžfÞSí¦ÝM“bî¡ÚÐ[χ)Ð íg¯LùN˖e ²ù ògÞ×¢.ÙÆ^e"u¶ÒžeîK >¯¾à3Ë[¢Þ?maüý~+ö=ԏ½/:Ý݌ÓÝÕ8Éÿ¾S‰½»qº»§õǼs9Ýû2È*ÛȕXWöé;Cð? +\ ýzC#Ð磦½v¥‡¹½ +{Ðєo‰ò9igè¯ÎÚFy¦…™¥@7•™góÆÖïäy*ŸÍ±]ªfÊßAd[¿kèfý.a”ý»‰ösÚþ4u-ëT¹fðÝnìÓ oêX·(ÏR±òƒ©ƒÄ! b]$Ï%ËPÇ2I¥[éaé”2r+ÅhËõ&ÔDãY©“L¥òke}†õ×ÔWÔlS)/›:Hy<6ŽŸð»ÞOË=5ïÍî—kÓw¦ž”ºÏ!ᖿG1÷O‰<ùw0§³—,Ûòú˜MOgZi°ÒœÌo½»ÁZ’"×äg¨;ßímßwË»ÑÈýÊ(ij rÜηÏÛå8aŒÌwû"v_Àïsxlí=½ynÖörmB®Ó܏Â.ó`Ý=S–'ß÷¬4ŽZõäýIäôªö½Ÿ½—³÷DCµÛè.õØB}ùN’\ïÚßÞŐwHž¥»å]fP„½¾Qæº!א§€—€?Ÿ¯˜çTÇþÆ¿â~ißmçûm{o ¿ž&·ëLÊÒ÷šöŠÚ@çñ¹8ƒWÀ¿²±“ïÕÈ»PC­{„¼¯/·(t.–z~¥|¿1SM†}0rRI¿€¿Ü¿Ð.†­ÞU¾§ªÒ.”¿‰™¦f¡Žÿ¾ªHþ¾j øúÈû½S´_Ó4ÇS´Èñšçø†îq¡{@oQênþ~B;*yŸ»b½âÁ~m%ÇúÛg×EÖü'çí +¬iWÒVí Ä}º paëÿ´U|B[Օ'ð¨Ë{Ó[µ€öCüR‹¾Ž°¥Ð>ðý®ÕK¯ÎYF.m @~{*虙ÈcÒô“å|ˆ5ñ Ú,ëp*p–Yu² >1Ž¢NWƒî^³ë Yhp=bóŽÆ‡V}bÊcp_DƒûEû’z£ü-ÀQ§aÀzLjû+\×v|ub½eÚྌ÷­«ŸOî÷hÈv/9>í@ð˜È±°d@ý-Êf7·›y¾0ëÈ2 ed)öøC&ϔõþ@Öw«V@‹dÝPŽ£ºc¾`žÉíyšòtµLÇ|ˆ“cÈuã~~˜ºË:<#ek —ËñܟúQJÔwƒç5”‘žy”esÞW˜õ“iB‡!/}*âýX«ÞG#݌“õ·ÚÕ^w®;òtxͺÖ܊9z¦Þ yuÿE°+YF¦OS¥þ°«d5H[¡ºDÿ^ È°Âø·a£@±ågÚEÎ㟠žï?ßH[NÖ1(Š Óҍ£ý¬?€ñÊBл¥Ûyº|XG±~:°ŽÝkë¯Ø2X—1`$´ëµhì éQý/ûžïBkGèV†›æ­s¼Kë”BèõBä[H=ÎÀ< /t²Ð݊+´ü. [üªLˆç5À؛ðgIÙöƌ2°1n> këÙ6`,ìÄ'Ä+ÆlЏA×ýØ]—óÇÞ¥‰½sºzd“ÆÞkj1;È8¬m1>ÒÞ3>r΄Mø9@Shpœüãm=å!"Z ¬d[ϟ{ïÿ綛ï J›âsÏÅûxùÎàAËþXI3°/åýþø;9KÉzeêãé6ÇÿÐzçýäÖ_k¿Ãr…k#y)”éNÀ:û¢õŽû|ǝ°¿–ɳÒù[b¶¿»Ð>µds/ôÊ*ØR5XWn&Üò~ð Ø0›ù7¢ŸÕ”³íÄïèÙnµ~ÛÌ¿a^¨—Ðθ*ã®1Fr\<AÎÊOس¾BЏOÞå¯4Ã([™„}Ø}Ô=*l”E»[Ô?Wғî[ß*½i“¼wyö +ûä>žm‘DØÐ) ­«ño÷ùOAk |†ú>Æ0Ê}ÚýbÌýÓÞÁ?͝ûÓΑG1÷öü=ì¥JÈz¥t?ûᏠ ß«è.WAwkƒéng=Ý ™¾2{3th‰ãj”é¶Ñͺi£k;8nÂÜâ¼¶Òz}:ø>B|'«,èKÇHØ:Ká^L µò̤‰Ž‹iŽ=Ÿž…úö£ÝòÊYƅb›q»â'¿xÍhÑr©L¿Ÿ.ƒ]¹^»vôý Ksh”ú9(ÂÓ­8¸±'\¯?ÿtø—šñ°W*¥{5] ÿeâƝÚRãIõ쏯YFfkke.ï2ý«ÜT†¾\/ýçßhËў¡-rîïQ¶R—BKŽ—¨Òuˆ.“xɤqiH·’r]ÇC7.vˆ³ézûL$öîàIgeHw= ±× ¾S ÿwÂ3Æ~m±q§çN"×ÕÐ'Ó {ցZû9áúØ8•”¢ë&´±ØS~I¥úYüŸÎ:>?ýQ°ïՎ¢·‡ëÀ‰ˆ¿ÃDÂ7'Ã×|"R +;Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@:Ёt è@þ+D¾MÊ#TBÈI +ù(LW92ÿ üªõ_/ºšÿ·4öÓ4uÝð8µ'•.ԉüjHíÌüjf½“¿Uí¶«0ÓÿÒãjw: (j÷æP'ÿµ«Ú©y¨?ܪw%§%ï¥P•>òÀsð0°Ðh¶Úá>U#¡*ª :òûUÑìM*îQ åsJ&¿ò™rČQŽìJH*Ú>|Œò= ìTå|ßVÞ¦K”ÃÜçx–ہ}ÀAàs@Wãû¾o*oR¢òwꔳíÀ>àsÀ©üOŸò‹š|²»P”7ðô)¯£Y¯ã™¨¼×kÊk¨Ú_š.Ú#¡>–Ã_`92r,GrzQ«òçæo»C¢ +1Ґ¨ÇÔ.4ŒŠÕ.Íý ~™Í% ý­Ê»»!ÿŽá}•—)(¨ÉË(ùe +Z`9 Ãõ +\¯P°ØDHž>  ž^¡¾@˜¸”—šQL«r°¹°Ì?<]yQy†2Ðã/(”ôyåiIŸSž’ôYÐΠ”§›;ûixâ i|ü_m@û Þ¡ü~W~²ßž¤ìCßùñ씀ÙÀ5€®ìSº4×ù“‘ÉctÀEàl¦%½‡îpQx‘?\8àGá_À…ÇöÀöB%\¸å&xùQxõupñ£ðòpñ£ðW—ÂŏÂ%ÀŏºEpñ£pÆl¸øQ8a*\x´*·=šßÕ?pÂbž¨ü½ôKôÒ/ÑK¿$Mù%é[ëvssè±máP÷þ†½¢áqÑ0Y4Ü!êEÃÑp©h( g‹†hÈ ECX4<&¡+D¸åïàp¦h8  +EC¡h( ù¢! †[•¼æÑŒTH²k8O:Ð_ ƒöITòУyù<è„}x é ƒ)ÐÅdÎêÌ´Ë®¥¦¿÷¢e˜>O á†'è-@Ã=1z™< ñ,fûÏÐÁÝ¿F>ñ씳K€Ï]Vçs@¡eV–ãJ÷±*>Д'ðí‚ož’îäËõ…|£ÔkrEbg1¡³ÑYHééPÙÉI®¤VáÝýoï7ÿö’{¸[¹Z¹†U·²Ù¢×4 Õ-¶6>æž&~C5HžL…¢t­”þ”ëbڟr•@‹šs«,±¹°§¯HàT»ýßæ¾çÿ8·Uó£ÜÇü¯Z5Ñì?„vû_νÒÿlŸVB/l {’uOî ÿC$륈ØÖì_Ãd·ÿâܑþŹ2¢ÞŒ8{%|áDÿäÂþQȯ·› +$_Û-ù4Á|M+ó+ʛòó%OF€VJž•hžà)(<é t@òHo`žÈ0ɒ› –ι’EdS®dÉْ¥ê8K‹åÊv–+eIª8ΓkòxÛ<ÞÃà ýÜO}Y($v ­™7³¢>XQ¬¨j#W]° 3Ò07hšWÁˆZX;wÞ¦sê#5Áúòȼ`y ièÌSDÏäè¡Áò&šY1µºif¸¾¼yhxhEpNyÍ®‘û<¡¬+ÛËê?ñ™MäÌúsY#ž"z Gä²rY¹¬‘ᑲ,’¢>±ºÉEe5#fšt—çØÖæäՔ¥û–“2<4/sMÎ^˜.÷Q\¨&,‹xŽê5¼×pŽÂÔâ¨'ZQ™k†æåì÷YQ>'Ë(´êü•çSfÅÂróo%>Zu>w¸ù ­ü±â*"á9å+Wô˜26R:iFu“Ó‰ÐZnRdˆWÑjì7{#pªj;#‡•p˜Ûm1ž<þç[tςå±]"ÜY¬¢•5j¤óØ© +4ÂÔhëÌÕ{aXñZ±² \)Bb¥‡UíPˆL?q›m¬:ßrY}±Ê¢fJ$YiwIû‡;+ÔÞc«d¶²;C3«‡'¨g¨}h8lç¾ ½@{©}ÂɅ~Uèw»úã<å~§^î·s­ Ñÿ1˜Ú +endstream +endobj + +62 0 obj +<> +stream +xÚí½ x”Õõ|î»Í’Ìd²¯0& ˁ„-É’°É!A°Ê* hµu‰"‚—j¥¸TpWÔ: Ú?´î+¶ÛZZw-•Z¤u!ï÷;÷}'ûÿ÷{¾çùžÌä÷ž»œ»Ÿ{î¹÷½$ˆÈM¤R`áëì{÷:„«Õ˜•äE¿ü”{›ž¡¿ˆQ,&‹Z¥·²Z¹C=œ(q ¾‹hú{rG„Än%Q9 Þ­=¤}ctk?dz1"Etý‚~-n¯kºý·íÿ2KÌ«h*äárÔþfº-ÛCèOø¾KºH^|"_Ì?Á÷Rq­¸K< ­(å5ñWñ‰øB|)¾Q_CÉUò•ø•ó”)?SnWàûšò7å+5Sí¡†ÔÁj¹Z¯®F­6©7àû˜ú-G; ™èç}«¾]@Hÿ~ÄHt\á$çËßÞ}¼ÏñwÚ©}sûÖö–öVó/”Ž1ÌA/ø©µŸïrŒ÷VHÜ£ô{‘ˆ¾Ë}ÄHq&zfžX.֊ ѓWŠ[޲î¿O¢—þ >G=Jž¬se°R©LÆ÷le±²V¹A¹QiUÞP¾Vj‚𤦫}Ô1ê\u±º^½HݪFԗշտªÇÔoñ55·æ×zhEZH£ÍÓÎ×îÐ>Ò>Òçè/énc•q•ÑfüÃ1Ä1Ò1Å1Õ1×q½c·ãug¤ó)zŒ§˜8¤^®V«ÑuJ©–­¼ª¼ +yžG‹Ô‰ +$Uy@lV.­J~¡1B!&Ñ­}ý¬²]9¦ŒP'Š b:-WZ¹iÚNrí):¬=‰¶½Šœ/4Å¥ÊçF"µRÊPæ3ê-¤¾Doªï +‡v'ýYs‹LqX¹_)ø•6R¯£|õvú¥ºV\B)ÕP¡ß8·@Ž'‰Ð 3D‰ø·j’ªL‚ Uߣ ´Bù#Æ<ÞL?‹´sè:*ÓGtfEoý\£‘.^P–iMJªh%E{­+BÕÓèJ1W½Õø\ùO47½£>ŒÚP~©NԎèÓÄR̀Kè*Zk^NéuÚïÄ9¤ŠZ*ÔA»]¬–hù —A«ÌNۍٽz`”:!Yœ3!3¡!nÅwô„ Z†9> ZìUj5f(mtŽîÐ:DÚKíÓh¶yÝbžCçš7R?èƒMæÅÈñú€®§ÄÆöŸÐꎙóŽ8S¯Qè5f?¥Iù“2]Ùzâø¢· E}Šï/á©?AMÚh:U˜[̃î^а·ÐOG cÕýTÚ>Ii6kÔ5hï»4Õ¼ßô 7-5WÒdz’îuè4ßÂGÄïÐޟÐbeš¹^]ܾ ýp=z!ŒÞ:úçjm­¶Aûж`Îo…¾Ùy³3‡ç>…ÏÚ¸~Ýyk׬>wÕÊ˗-=gÉâsëfÕΜ1yÒ¨pÅÈ3ÊG /6tð Ò’Šû÷ëêÓ»WϢ‚`ü€¿{·¼Üœì¬ÌŒô´Ô”d_’ד˜àv9†®©Š ¾ÕÁš†@¤¨!¢ǎíÇþà|̏ hˆTs"O$Ð Ù'r†Á¹$Ž3lq†;8…/PNåýúªƒÈ+UÁ@›˜=µîk«‚õÈaéž(Ý7H·îü|$Tg-­ +DDC :RsÁÒ¦ê†*dלà½ØÝ¯/5»àL€+’\Ó,2G +éP2«‡7+äô R‘œ`Uu$;XÅ5ˆ¨…ÕóE¦L­«®ÊÍϯï×7"F/ .ˆP°2’’,4Z1FG²˜À2n ]hiK›4„͟SQç×sÉ!”[ÉüñûY^dž2ºnSll®ÚTµ,ÀÞ¦¦MÈŽ©u±±ùü¬¯GH«Ö44Õ è-èÄ Ó(MÙX_Qd€[­²Ú·8XÍ! ËW°2¸´iy†&§)BÓ.ÊoÉÉ ï1QNu iF]0?R‘¬Ÿ_•לFMÓ.ڕdŸÓ¯o³/ÙêØfo’íHôÄ:wÄI—dgׄi=+¸FÁqˆH`a5© ¢MÃø±x5-6|êREaD–E\£š|Ã9œÓGôB_0Ðô%A‚‡ÿvbÈ|;Ä(ô}Iìd9é5ÄGݑP(Ò§‹ˆc4Æu)ýƒûõ½ M ×ø è>š‚¾_?¼ݟŸÏ|M[˜ÀiœZgù´ ·…ÂÅ¡úˆÒÀ1û£1é39¦1ӑ¼!In%6yÓ#΢Ž¿$_FjõÒá‘ñ=ы­ø Ӄ¦Î® T75Ø};aÆ >+~XGœíФޮSsÛ¥äª2B9§ƒ™=u‰­†êEm'¤R†ˆ@MÄ×0ÖzÖ»óó`¢6ó§’¤3™]ÍÈðЉþ'øO¨^b“Š +cy0cvS“û„8ˆšUà8›@âiF]~`t„fbfâ¯ÍÜ?ŒQŸ £ËF3äÏ +²½'0æÚîz|X:ûõ­¢kjª jššæ·™ ‚_°iòå7Mkª¢‚Ófî½&7R³¥}µT ï×7È1MM‹šI-D1áÜf!CG_S™ªF„‚ùÁºÅhKópJ̟Ñ0.…*›ƒbóÔæ°Ø<}vÝv%›gÔµ(BÝPYß\€¸º=,2TáPdO€=4A kZ§äÏÝ&j”±š þ…m‚d˜3&ha›b…ù¬‚ŠdAa– Û4+&åÖæ´Â-î^6·1>ŽÙKXqHFZŸfxfԅÝCÃÃÃ#Â#• +=ÂA-Ù Þ‚v"·yN“Ám¢±yD8wÌišÍÙNkìC͙-&#”g5|fg fήÛ5’¿|‚£’?¬iQ‰Ø9$Ëù¬P]¢Ò4a:$#ÝÃrÝ1ÑNÁȼà…ùܺHmð¢|#hk05Ә¼ú¦¦¾AôÊÂÚ:ëÉQ¢orª4.ˆòææA&:½‰H*åjW됎Ò~-í<”ÆŽ¦hq‘…§, µˆ³ø)ÿdõ›‡PÐ*«´UhӜ¦ÙÇüH7.Ø®¼Þ¼z™j²MÖDÈÅi!l‚%<—¬ä &ƒã›•I!I…¤MãƒÕ‹ÀÁÀ¢;ƒ•XTÏ\Až4,øßÉ$b˜x!‘™7ùFD}ÂöYÓ·)rΉޥÞl”Âþ–š@[ä”͏,ύ¬¬u°Ìç67anç >\&ÃhÀ²3&Ò¸p>ªˆõfÜ Æ# P·ÀêA^¨›ØrZ8ɸ—í’"ç†NÈ:A@E!#nN¤qJ ¡>Ð"¦¢³s4°æSp>ë)V{¦@ùƒÌošŽ´ÄÖq@Ÿ-™¿8ÈÊ5Âònõ>×QCíhz]„r›š‚!T±°ÌȾ(bc‚¿5¡àüÅlÙ-aÃn±er º²w8·Üê`~=X”Bٗè8L´üXØÄvã܆z"¹)¥)Pք ?ºJ+ZXÛ½ðjr¨çç‡NǾzdd1º +™éå_QdU¨y®£°3Dþ­YÌN™«4""S¢,ùÇÚPDɆHn¼˜6[® (î<½pº7 ©ÊåԘE3ìeÃJ?Ž“æFÌJ†úèyo.›§ÄjÂ9‘” ÓÎÊEÇöCØxóc-O‰]ÃPµGdc¨.Ü×åqõÉöäôéíéÓ§Ì3$}hîð>ãúÌõÌí³Ü³¬OÀ&ÏU½o͸-çAOz¯6óãքcfO8ÂÙìº/{g¯ÝÙOôz:û@¯ß¥¿ÝËY•!º·™GÃɉ‰ÆÌ”~ê‰üÜf +Of—?ӟêÛgP™VÖwœ6¶o­³>´Ä¹,tAâ¦Ä¿ò|J:È+4_qÁ Ì’ü´¬y½W÷Vzç{+¼×{·{M¯¾Ýû¨÷s¯ê}Âüš(A„¯õ&&%)3½mæ§­>Ÿt §ù|ÆLo¢Çƒ§‘”„g‘ÇÓ&­µÞ¬ÄD8«õzóÔÌ6e箬¾  {k³úºÝ•3³nNËËsPG[¨º§»$OMè=ß7ŸFùÌcVÁTKdþ›)ÑvKò Ÿ„Z2<e&æ´™“•bG8C ´ÄDé*GeÏÂñV8«] + ÿ·­\|A›rVØÛ3LE¾¢@р¢G‹ô2,þ­^¯2³¨Í|Ãr8mpÞ,e†{ZÚô¼s”Eúb×´†¼ýþ×õƒ©ogúAÚ癟eÐíßôgøý¡œòŒòœ 9kü7øý•OÿŒáÊ`Ï¥ÚS“6.o–»ÖsŽç㣌¯ÅQ¯O¤«Þ_,€G2¹Ó1%²F¹1¤Üa +dE§CkmV© 'Ì/¬™ÒZK…ÉI˜9¬I×èÌIŠò… j“ +}¾×’…/9œÜÜ˜¬ùÃ,Jþ0 Wr +Ϥd9/Yä’ –üä,×f~V\²—Åþ¿ËÙÇ¿[Y“ŸˆÖnwmòú”¨à§D?Åüݵ)ŸæðI®ðˆÚ}ŽŽw¦Có;*“ª£;×Ë!ÅÝѝkàâî*ǑÃE;²»š’šä;ÙPh"‹üñPì$(÷q˜ïx¨ü}H.Ľœ‘Ì¢ÍB †87«éذÒ+¼^r'äÂÛº2Á‘D`HoiJYE‚›?˜’›2‚š yR:bùU‡-~ú²ƒç/}CÃÖâ]ÇŸÁ½üäÂ;¯ºcË7wojÓÔQŠ÷ë%ååýì›/?Í7+Gw¬鐸 )q™~ÊKWfªsõ¹®™ ‹Õúj×âg:ë4ÙÕp„§±«[?{¦üIÿ:íXŽ60exöÀ¼Q)sFåMM™“=-o~ʪœùy¦SŽeù(C$y23§d4d¬ÉP3ò’nðíð)>Ÿ–›çvÐ^e' h>Á*NµOqsjž– ;Ò!U™Q©’îG•cfØEÛÊ#æaáúyxéàóp¦®ž}E<“ã‡oWaÑ ¦³2õ ÆQ5¿»6£Ôç´Åçµõ£Ï’«pj­¯À.è3(*/Q1ca†P­##ByR„¼R„ò¤ðdHA‚ !ÈKh"‹Ïûƒ8[ËaR¨ 9Ç¡úÞ¯8 -9·üøÚrÁʑ…H̕ºQ¬=/7܍h +­¡Fºô¶c?½FŒ"_˘g¥||Jªêsk©RÌ´w®Ô—n©/+B)eóΞ[J.-ž»B'2Y]R²JK(9͑ŸÁ"'ò‹¤ÒTÏÞÛ÷ï{>iÿ\¤½uPxÅ·»[6.ÜrüMejâ°Ú«/~PÔfÞÝ*üB‰¢Wû;í_ùî]*n¾jôÒû{¾Tˆa£þ{ÊgKùëžæIÙÅÙ²ÃÙk²oK¼Ýó Ç™ãéå‰dïÏÖ²yý9þAݜ51)Ï-ҕPZª¦äޞ&ÒÌTk°¯M k™ÑËŒ*„Lk¤°Šgj¤*7 +A,‡ båùÝ@";Ì*(;ì +¢4–HêÅ!ԃ•õ•€TJ<¾”Æ#K–‰"¶ò"Ç׏Kãî¬ì'Å^ʧcÂMY¡Ð±XåÐw´ÜW.ÅáÐá¹TQQ^^~š¢,<ú"<ɆËa8±Vû\)¹”l$劐õ¹ür‚ +9/÷1rg¤ª +ÖÀ֕ªáNjSK0 ÂÐҒ +ŒcirppéàAC‡@wd:xìÒÓKӃÉ-Û·§æl¸àÌ9¹ÃJ¦U8 ÞºeíŠA5³R~á®iX°åÛ%¬'6c¨Ê±2©äPÜ8¾ »Yþ´IÎ1X«:ͳ¹kσ|±œUIMÂ`]rÞÚÐw}r×ܺAaÁ‹RÎ+,S©ƒKÓU,C›[[[µÏø&]+úæMÌê —¡,/âò“¥e”ç»e#N:XO1âq#“ëIãøx­.‡KçÉ>tØ I ¶è€íQ(i¸0=sP’î×·ëïêÚd<Žèª__£7ꦮ¡õnE-´ÔÆP[m¤c^m'±ŸŽ`ªP÷i6ÖánRsÈa#9l¶ÖpÚ*Ã38L¹XQÇàÑ$íÄÁãÑcšÇ‡Œ}'Õ.rËq²&~iò†V}ï×5¬o7©bÅÏJ}›ª«Fªò€¯Í÷žúQêõXªÉ9î‘àt‘Oló½–u(ËÌÒÎ4oZFJžîF†Çíñ&zO0ú¼1K²7j†ój½Yaî…,iì%ôbwB÷Fo›’Y—&È~Iè!9Ø”Æ^‚ÜçÁÿo®àr³V…ÿXXn@Â¥C™  “²xr É:’¥¬ÉڑÉڟ¥e©JizFTýgD„ŒèŠ!GòXkr²5N–yҀi¶šÿš-T¸9<š=nûÃ)¨Î>G +À¤Lß±¹1Jn{Ê¡ëyG4÷ęÆÚ‹aJVN.)–æÏ0’]n§ÛáV _lâ\‘äNÉåíÁ'ˆCnØåqg¸…¡ê)mꀖ•ºµó©ˆú{_“LT$Uò¦»Î»áÎ)>wkŸc×ݯýüÑê5K.9¾N¹êÜU£n|ùø“æ*؇=!-Ê&ËËîô,np*Ÿ°#‰Ï ³+[F¤8ÜىcŒ±ÎZ£ÞyޱÌéäž2Ã}VâJu±¾Ø½2ѝ™§9’óÒ n"–³ÇH‹ŠaØW›V+÷¹RÜЊÖ~Â!wŸzDnË¥CšbìàA•i¢I´ pЇ ‡Ï€™×± ß]ëøn®Èež6áöF…Ì•-¯½Û…9@‰^¶R¤å(…*O +•´ +Iî£)Qê‚ )Vaí§ + A¢/Eó¥D{3Cs؀”ªaî ’óqî±Ðܹ'Êï?Ceð¦côœº°kº>ݵ@_àÒÄÜz’z£9!ÙÚ{$h™Ò(Ôl£•½o(äˆÒÓ¤=˜³ó¨ºçêgþ,2~òÙ5ï¶ÞӲ骖]7µ(©¢çu´ÿåø+Ÿ]!º ÏË/½üÛg^zMÚÔ¾LˇT¥PwqPj¡õ‰¾~¾3||ZE PüÞ‰Án%é%Ý*»­ ÜpÏž;>s|n½ó¬Ä9™sr—;W$.ó­Ê\‘»?ðû´·³ÞÎù}÷÷ÓÞï~(`2‚ZÈJ¬ ÷Õhã}³}$|֭ݗìŦ#Ï`ý•çM oö •#PÙ•W›]ðš[øÜawƒ»Ñ­¤X¤ˆ¹aè…X¸ÜY¶ÿëVZ7ï=x\Ý|þÃbåæY’Äë^/RK•ÒÎ-jT%Ù{ÕpvmJ!Ñ~!n;DDš_TˆÉ°ŸyIËˆðqqÂÇe )ÑBîOk2–!ɚÁ yH'RX¾D¶ÌÐ,»á–ÀD륣ïûŽŸxxƒõå°TFö^¼Ø¬¶’7ÙË{‰ÇWz rŒ<-+¨Ê •IÀ•ŽÔ:éi +oZ{&«1³éžá7.ÝüÚòóßýÉìëû'ßwÁ…Ý¿~]sû2ýWMS§n1·ÝÝþÍ5g?þzÏ+O¿tð¥ÿ½Z`~¡ôÑoÁNá\–™QJ4Û;Æ,!ÆíŒq;bÜFŒÛõ;X4ÈÅs¸ŽÆlXi‰·P)Ãç +%¹!%jB’¯õžS —Û®®Da:œÕ®êÇG£ã‡FP;Ç~ÇkÃÁÒÀˊÒéøBì9¬ó Û!÷ŠÖd)".¸ [Y +×±WYNYbHó’8£xØZa|ï-—g ÇËyð’KK}/°¥5šUŒ_I+7a×Jáöx’½n—J·ÁCYZRRlOüÂLëØ7ÉC“yӐƛ@ŗsfù‚•}¯¼r×c¥†zu¿s»oä⻔…[„ceûµ[Žß4±oÛI°9þ¡‘O ñÈ=IžÎÓ#Þ]ÿWg ŠÀ=جÈ7véI"ÁÐ6@7¹“²¸“ŠYD+*’ù€0÷ñ¤‘Ô#»Ìh3ß OÉ.›´UÛê¼Å{kÒ~}¿±ßñR’+)œQ–£¦ºÒ=9¾ÁbxÂåâºgqÊ,­ÞQŸPçý¹ØæÞ–ð¸Ò–ø|‹ޗ}oª]¿õüÙ÷;%¥ÙõHH¤”ä¤,Èà)íeW’AЇÜnÅàÕ»œMéeU熗†êpº\Â0\º¦B ’|Pö")ÉãCï»O‚šèsIJ’Û÷,=ëR|…äJ#r©ŠçYð&ªi‰‰ªÛåRUìþ<žÄDrON)ã<—&öp'Í7\—†Ým"÷ñ°1ÅhÄ|lSF‡½õR¥Çdtý¸ä‹ŸfA™{ôpNöñ¹Çs²û>ð=ü!¶™°7|åÖs“Þ?4wÓ%OoꟚ{‰ïiâê'%mr>½Éë{Úz‚8¼¾òrgy=¤ H«7«[Y÷wB·²Ä™e*Àþ–ü2Ï+wz™è‘_æ +ç•Eå´^š,蟹õRɲû0¨<¸l±b@Q6.B”ffd:® ÚS$‰+ÛoùËÝýóúîúCûOÅ5o¿9¼ý¥—hÿj̀ÊÒoڏ¿*Æ×·Ï%y›ÁøpÉòOӃó’Ê¿tæ:å­«»ÞëهéoñÞñ¯=~ŽœSáu_Xײˆ#Û'Ñh}ýè×?ö‘Þññ]kØA|gÎFDù­­£t`œ£ýH¯¥:±‰f+;éb†ÚÂÚÃtxwÂ? +t/§ÿLà] ¨r찉À|`:ûÁ»‡Ó"5œ¤ëh¶ÓO«õZó8ÊÛª?GK€;à¾K{0Êhü÷ Ý>h(ó ÍVc'mCøíˆ_ˆ°;@ëà¿î9H7Àv»×R6SÀ@xoäsÝޞê¯iˆ¶Îü ÚR<ÇW¡Œ) 5Àð¤‚V›Äs´Yçqµíeà}d~ª4M´çFžŽÏÄ ¥o>ÎØæ\!Ú3ÉgVka®‡i¨¶ã“=¹€ð™ú_hŸz-Æú ù'ÑH +òp¤Ó|e+tÊRÞ  ÎtMŒ sñ²¥Qy§¬óm™òƒ˜¯Úx߯1àKÈÑÈd6¯ ¬Ÿåú \eËëòù|î½&*Ÿqrºãå2žÊµú=:OQÖÕÑö³~dÇ:’õë™(<Iߤ섳~…fÛ󺇍ñ¨ã_í¹=ŒñžešFy¿Ñj> ¦˜%pÿÐÍûÑv¬©uf»½žöŽ®¥V8%D×Q½”VÙúì©o¾ ŸÉu´VÖÏeyœéÐi ö xÒÉ ¾²Ât¿” N»‚ˆû±ÙIàáüî”i”b÷Ç=²/dzØ",ÃÜÈÓH§iҞøŒ¶ë3iæÐŽFºÓ˜‰9—N {‘n<×éräz}3…ùµºi3tIùŸm~£>„ö\½¨è£‡(KoD®m¯Ò,»‰çº“ŠXFŒ›¡‡Ùž¸™š´U+èZ„]«CO¢Ükv%æos÷j¤÷Ûz›PöÕç´l˰ÀóŦT£QÚ$ëÀv +ÊW?¡;Õñ´r<Êy3úa#õ£ô1¶Ó|UõÑ% C•RúJH€›×Ð=Úå´L«¥u æn2õÓ~‹¹úݦ&Ñ<íEºMk£-ì×R©—Aû[a[røšÂáÊïàßF³µr¤ßLçjóhÚ Ù{ÜÚŒ5Òé×AN +þ äkC¼G³ÕZÌ­«àþ +ë ød­æ8†6–úÉt1u"®ÎÊ´j<Æõe÷ õE];ê­ã)ê'ÛÉù"óh·Q9úé- Ð¢íS•ké!`‡ò&V'ÒEâs/:¹&ccýÚ`q1Ð_L—ÃÝô€G-?l·Áôg`#òÞº‹÷ ¥’†0EØÀ6à¥h\,¸œS…ÇBÏ5÷žà k Žš{ñüèç!(oˆv†¹—YÏ0.£4Ç”¦öDxw¤‹ó빘OQJæ¿NW§ï>cú1ÛÆèx€fü¼CLíµþ/õûß㛠ýûwJ·dˆRÅæ@kÅ”¬žàïj´?£ã„ð›dxÜø)•f;÷y|x¼?~\OçWvѼXDå Cn¤‘ ­ü@¼ßùdÏ î™“ýÚý§Álê£ÞÊu‚ ö<ÙoL¦ž ¥uÍá4˜s@‡ÿtÀ¼2½‡Æ0xî2”Vì׀ŽøÁTÍèìWÂýªÞjÅGÇ':.ñãƒú Ô^¥Q =A‡ƒN¥±s6~ÞÆ‡EuÉ©xâæÆÀïÊóÿOÀÜyxxöÿí²AV`¼;¤väAØ'gñéãÐ%ß÷AÍý°z·÷uƒº¡ß§vžñ™ÇKžñòly=…ž?W;Èg_æÓòL¯’‰ò=ʬ¡þè9üé|6äðy‰ÙfŸÏ5_`œ…õÐÅkÊ­•ï„Vh|ŽûýLM *û 9-z–ÌçS¼^ýÉ'Ï1bϑ߃m<‡ª€ +û=ÕL>Q?ïj6ñ¹»:‰ž´ßoEÜ;é×st‡sÕ8/“ï›¶ª·Ó„Ýn7BòýÊÌèºÊkâ)Îþø,3§ãLÓns¼M ë7‡Îäó˜Ør£éœ5XK¿çPÖ9æil¬ñMÀ"ë}…yìÔçæËö¹çR{¿ c͏?§ŸCSÕK±ï‹žÉÞú­]Ø}_—hYè—ãße Em¸gɳ>ë}ŸA¥Æ¼‡«‘ýü‰¯qΟ|h•'ËfÝ»4Üj·o³]nĪ{ûMüíyVeºH'L˜†ô~Ðéĥùè/>}ή»»ÛýÁm~œóêÔ ôµv+tFÖê4ÇN‹j?¡3¥Î=pÂZµFêÃ÷è©ïLè¾r*1<°C~A•l7°×KþkôEX›ö li/"]{†²õhžv.U©»a¾Eò½ òf½Í6‡z5Mä»JùNˆß\H›Ü­Ò~ñ'Mûõ½…öa϶Y¯#ô†£?ü7`]¿“.ÔB?v®¢}Æ~gJK°^ùyT¦_Ac£{[c¹ôDØ6un£…޾ßIíCÊsm‚]÷MAŸ –ÝñîÞAi¿Ï:_‘ò|ΔuF}a‡iØ[§Eï èsÑ'‹d}&ÉwN’†=:éŸcíG½.Ø^ŴٕE;Œch‡;5$ßË/±û~¿rœCõMTÝ»g;Jù}\ô<¶ÛÚRi/¦È÷Zöy@æÁïÛi ߕˆ·k¢vT‡MaŸtœ9DÛÊëgGûmcoXg +ûaŸ¦SˆßãÉ3‘xj×I¾ÇÛY²íYÇ>ïPAï£%ÆU4]Ÿˆ~I¥éާ(Å1†²Ø>s8¤]·Š×hý+Ø¢Ó©c3ÀžÂ\n½3ëí9Îgnæ`2žm‡ñYÆÜL@øL;-âÍó­}†äá÷gM¶{´E§=þ¶ÍÿHÌYÍ»ä>$k§Úw©®:‰v¾»gù©9-ýgh<‡ùNÕ)ÞñÇӛ@—Fý°óÞŽi€µ£ã©ý¾ÿR‹Jېé½6½›em½xå»î³|kͳ(=ñÞK”žmÓ¢Ž{9§¡±÷d:©iÚ~ï=»³ÏÜr¢ô÷¬3¹Njœ´Š¥rLHµíX¶ßÇË÷ü|7ç{Ðq‡ë +ÈÀ‰¨eð}‚SÁÀJÂp¬<¶ÿ0®G:À鏇ùOê|¹ó6ŸÙ¸‹¡ +ì¥í§ñ0ÿ)qêûuUÆ/P.àìgÁñ‚iÿÐäÀJêL‘Ôàµð{+ƒáøÜÆ5Q˜&#ÚïÑ~Œö Úö!Ú½´£ÎÑòí|ÿ¯ãø—ÿV»¿¯î±°ïèE)ßÝ3NYoŒÄ?-È»4;)Ն~}xxÑÆM ̕¾«¤.†<-–÷;Ҝ$×boʰýöýÀeçȲæßý±@õ§êÇbKþ=­~’÷v,Ûë´Ãcß±]bë¾×ºÓ¾'ëg݂u—çùí×´äD›Ïœní§Í»°NêàOÖ×Sò’y·þcè„#æóúe°”u¥lì°l?óQû¤!ïï¤c½mwóXë¤y¯mo³{ž…ö¬ðÎzEu¯úo´ãʖ÷KÃr=E[†=ý2ÊV?C<ì~ߤΧQ¼f¨C`[ñ› íû²|öð¨úeŠú@Ìüæû5|¯wrxœžÅÀüÏÊôÑý}/y¾´züÏä—w'ïô ¾ëÄv‘Š…>r1¼SÍߪÛ@ÇÚø7p.ê[K˔+©Ÿºûá×`ï¤#|-°î,Ð$ ¸¸€Êðo '_ƒP5ø_Õ±·×ö•-8^î·wÓ"ØÄ‹ŸÅwP¦±`Ð"ñYÖ"µùOÁNI…E¡¦Ûnñ‘nŸµçsæ—qQW'ãªq/¡uh1ìˆQæ^ñ •k³)cêc¬_µ÷¼o: ·Ì;àñ¤{Ñ÷ä6Õ¡eúÔO?ûà-ÈÁ!*׏Ñmzõ2¦`{˜Î‹½¹Ä÷‰å]âƒæ«Ñ³ï(Œ:Jw=Cc0†Ä÷7¢TyˆXŽöΔë‘õoÐb·EYyÊûÓÖ\“v®£Š6`×cí{ßK¬÷c°A1÷4ëžj/í^ꆜkՎÞ2y>L‡nè8{eÊwÚX¶l[ṁ•ßñ¾uÉ1÷*S¨»ö,k_jòyõÏ>³¼=æýÓVÆÿ×ï·âßC}×û¢ÓÝÍ8Ý]“üÿá;•ø»§»ËqZÜ;—Ó½/ƒ¬²\ƒueŸ±Ó<ÿãÀO¡_ïahdšò|Ô²×®V0·×c:Ž +ì3Q>'íýÕ]Û"Ïô¯²ò£Tè¦JëlÞüÖþƒC»¶zOùžªV»Hþ&f¦š~èü}U‰ü}Õ¥à+–÷{§k?¡™ú3´\ÿ=-ÔÿM÷¹ÆÓ} ·« +ÐGY¿ŸÐΣÞ§Á®Ø¤¸±_[G“°>xaûläºÈú€Ÿãä¼]‹5íjÚ¦=…¸@WN¬cÅð¡mâSÚ¦®Ã8G}Rޛަý t âWÙôÏ[ýàßÛôSm)9zèœÕäÔV^òØSAÏÌAÐf ,ç#¬‰OÑ ²§×iµ]'âSó(êtènàÍh]â!ë ®G|Þ±øÈ®O\y î‹Xp¿h_P”¿øð:ê4ؤ>±¿bÁuíÀ—'Ö[öaܗñྍÂk÷ó)Àý ÙãÐô‰ [Ô_¢lvs»™çˆUG–)#³I‰Ž?dòLYïe}·i…´\Ö åè5Ð{ôóLëÈӒ§ëd:æCœC®÷ó£Ô[Öá9)[ã¹\Žçþ4ŽR’±èû{- Óã߆ú¥¶Ÿi9(x¾ÿPü[ê„XÜ~:°>ˆCI|˜–a>ëgýLR–Þ+ݎÓåÃ:ŠõÓé€uìþ¨þŠ/ƒu6€·C¯Åb͊éÙ÷|Z;L¿` °iÒFý=Ú¨A¯!ß"ê t€\ ›Þv\‘íw½·R7‘×s¯÷w’²íebcÞv:8Þ֋ڀñ|°Ÿo˜ó@?Ýø]w]¾Ë—&þNÌéêu’M¯©Õ<¤“yHÛj~¬½o~옛ðM*qxAS©,Aþoí}å¿pD´XÇ4¾ž?ôÞÿm7ß”6Å+֞‹÷ñòÁöý±Žfc_ÊûýKáïæø%¥é”eL¢;ôÿ¡MŽÉe¼Ùq‡å*çò8R)ËåÅ:ûªýŽû|ýnØ_«åYiªü-1Ûß=hŸZ ÙÜ ½²¶T=֕ÛÈ-÷‡¼| 6Ì üQ“ÏjªØvâwôl·Ú¿mæß0/3ÊigB­ùçx3%!‘J gU'ìYß E< ïò×Xa”£LÅ>ìê6Ö¦½m ?Wғî[š_)ýéZyïòìöÉ}<Û"I°¡SZOó_ îóïƒÖH õŒaŒû´ûŸ;ú§½ƒš;÷§#cî1¢ódÕ0 CÝ-û’©<š úècÖûRsLœ›¬÷vLåYV0ËÆq(¶Þ™ƒ€Qûž÷ö±¿=âßÙ{þ>ÑßiU°ÏâiuŒ›ûüjOèÁ”&~Lý‘Ç|Æ¡ +ù{ØK5õé~öÃó /B¾×Ó=2®šîÕÊè^Çbº2}dö6èÐrý>j’én¥Û ÒF|7»,èK} lUp¯ eZyfÑýf`Ïgd£¾i·¼ƒr–y‘¸Õ¼Sñ“_¼i¶jyTi@£yjw„ûð¼ hö¯žVہC£vSóZ~ߨžj6Òf£AIj&}˜€Šzf¢ÔLš Ì®¶†äãÕÀeÀ>àˆŒ «™-7–¢î™-×H²kùÊéoyç̕Þ]³ê-:qªE«ÆYlÃ-¶ƒ¬àþ•íÙ×¢)…%Lݞ’ý£2Ô 42_ƒ§Pž¦$!ÈO;ÔtŠŠjØ!a5eWAQÉö}ªFBUTA‹ÈoîWE‹'¹d”[1•Ï)…üÊߕÃVŒrx—7¹dû¨ñÊ_éQ` *Å÷/Ê_è2å÷9žÀv`pø0”Cø¾‹ï;Ê;”¤¼MÅ@0Øì>ÊÛxú”·XÔä“Ý€¢¼…§Où3šõg<“”7ázSyUû}Ëв’=Ò*¶þBۑ™k;R2Jڔߵ|ÕU„‘†D=¡ö ‘Tªöh)ñËj)_æoSÞÛùwŒ ¼N@AM^GɯS˜4k®7àzƒ€@€”áéʋÀËÀ4S§òZ ŠiS´UúGe(¯*ÏQ&züåyI_Vž•ô%åI_íú¢òlKw?J@ôÏb ˜ Ì® eŸÒ£e‘?™
!†¡+E¸õoY8K4¾(ëDc‘h,¢1 ††Û”ü–q¥’TK²kO:Ð3FBû$)ùèÑ|È|>tÂ><¦ô…Áèa1gwgÚcWŸ +ËßxÉjLŸ§ð) ÃSô. a€ž‚=…LžBIxVó€ýÀç€ à_/ŸIxÀ<à2àsÀÕùPhµ]ÅGeŸÒÅvÅ'šò¾=ðÍWòÃÝ|y¾o¬z}žHê.&w7»+C)#*;%ٙÜ&<»ÿåù÷¿<äåR®S®gÕ­Ü`Óë[¾‚êÛZŠžðJ?§î$O”Q‘(Fë¤0å9™¢<å!В–¼Z$Kj)êëß+¼œj·ÿ«¼÷ýŸäµ)p~œ÷„ÿ6M´ø"ä¡Ýþ×ó®ö¿PÜæDȓEmdo@²îÉæäEÉz9"nmñ_Êd·ÿ’¼1þy2b±qö:øÂIþiE³ýc‘_UÞxòÜí¯È;Û_nq æ4»ýP…åìƒÊöΓ…»#¤Õ?xæÌ¡mbi¸¯c«£Î1Ù1ÄQâèëÈwøÝ¹Ž4gŠÓçô:n§Ói85§â$gZ›y(" `šácÂÿj¯ Mº} +?å?AŽy-œ +§Hª:A™0½RLˆì_H"Ǧۄ{ê숬‘” 4aFedXhB›ÃœšqL9«®YˆëêQ6· šQ×&LژI]·‡„HÞxm.Ó^¯­¯§¬Œ *²*RF&—ÕTâÑ`?CŸ¬ÜÝ*#['L¯k¼sg·ÊúH‰t›&Ü"7MÌ©Û#¾Gª«öˆ0©¯Û£Ž_TOãpudU}ý„6Q+ù( þ>ˆÎ?$Ÿ«4óQÀÙÝâ»Õâ+Dzð0ŸËE…’¯Ðå’|š`¾æuÕUÍ’'3@ë$ϺÌ@,ϋ…à),”<ô¢äy1£‘y"#%K^XºçI‘Cy’%OäH–ÚN–b›åê–«eIªèäɳx<‡¢<žCà ýÐÏâÊPHìQ¿pNõâ`uC°z1й悥Y‘Ɓ@óÂzŽDÔ¢† —2¿8R\\Y¬ +4˜sŠè9="XÕLsªgÔ5Ï /®jQœ_U¿k̔ACO(ëꎲM9EfS8³A\֘¡§ˆÊÑc¸¬¡\ÖP.kLxŒ,‹¤¨O©kvReýè9Ý¥$¸!¶ ¹ùõ•¾5#¥ ÈϺ4w/L—(!TI VF<GõÕoGajq”ÁIvTÖ¥#òs÷Šì(‚“ƒ•Zþºó)«zY•õ·­?Ÿ;Üz†Ö}×qՑðüªuë‰&DúLŸ©˜:»®Ùá@h7)2<–PÝfî·û#p8ªj#‡•s˜Ëe3ž<þçÛt4ςFå‰]"Ü]¬§uõj¤û„ +4ŒÙhëœÙu{aXñZ±® \'Bb]4»Ú¡Y~â6G±þ|Ûe÷Åz›Z)‘d]´K:>ÜY¡Ž[/³•ÝšS7Ê«Q‹ilç ý@û–€–¨Åá”"¿ª õ»œCý î*¿Ã¨òGs­Ñÿ@óª½ +endstream +endobj + +xref +0 63 +0000000000 65536 f +0000000018 00000 n +0000000263 00000 n +0000000324 00000 n +0000000376 00000 n +0000003436 00000 n +0000003713 00000 n +0000003979 00000 n +0000004665 00000 n +0000004844 00000 n +0000005614 00000 n +0000006388 00000 n +0000007215 00000 n +0000007549 00000 n +0000007881 00000 n +0000008594 00000 n +0000009011 00000 n +0000009364 00000 n +0000010157 00000 n +0000010771 00000 n +0000011066 00000 n +0000011399 00000 n +0000012083 00000 n +0000012262 00000 n +0000012443 00000 n +0000012737 00000 n +0000012918 00000 n +0000013021 00000 n +0000013055 00000 n +0000013311 00000 n +0000013790 00000 n +0000014203 00000 n +0000015659 00000 n +0000016100 00000 n +0000016425 00000 n +0000016713 00000 n +0000017045 00000 n +0000017401 00000 n +0000017691 00000 n +0000018518 00000 n +0000019118 00000 n +0000019380 00000 n +0000019699 00000 n +0000020403 00000 n +0000020790 00000 n +0000020893 00000 n +0000020954 00000 n +0000021231 00000 n +0000021585 00000 n +0000021933 00000 n +0000022291 00000 n +0000025334 00000 n +0000025594 00000 n +0000025946 00000 n +0000026145 00000 n +0000026499 00000 n +0000026687 00000 n +0000027045 00000 n +0000027233 00000 n +0000027266 00000 n +0000031028 00000 n +0000042390 00000 n +0000057922 00000 n + +trailer +<]>> +startxref +74568 +%%EOF + +5 0 obj +<>/Rotate 90>> +endobj + +xref +5 1 +0000075978 00000 n + +trailer +<]/Prev 74568>> +startxref +76265 +%%EOF diff --git a/tests/resources/Bezier.epub b/tests/resources/Bezier.epub new file mode 100644 index 0000000..0a9ec16 Binary files /dev/null and b/tests/resources/Bezier.epub differ diff --git a/tests/resources/PragmaticaC.otf b/tests/resources/PragmaticaC.otf new file mode 100644 index 0000000..7a18689 Binary files /dev/null and b/tests/resources/PragmaticaC.otf differ diff --git a/tests/resources/bug1945.pdf b/tests/resources/bug1945.pdf new file mode 100644 index 0000000..6e4b842 Binary files /dev/null and b/tests/resources/bug1945.pdf differ diff --git a/tests/resources/bug1971.pdf b/tests/resources/bug1971.pdf new file mode 100644 index 0000000..21bf69d Binary files /dev/null and b/tests/resources/bug1971.pdf differ diff --git a/tests/resources/circular-toc.pdf b/tests/resources/circular-toc.pdf new file mode 100644 index 0000000..265f50d --- /dev/null +++ b/tests/resources/circular-toc.pdf @@ -0,0 +1,74 @@ +%PDF-1.7 +%µ¶ + +1 0 obj +<> +endobj + +2 0 obj +<> +endobj + +3 0 obj +<>>>/Contents[9 0 R]/MediaBox[0 0 612 792]>> +endobj + +4 0 obj +<>>>/Contents[10 0 R]/MediaBox[0 0 612 792]>> +endobj + +5 0 obj +<> +endobj + +6 0 obj +<> +endobj + +7 0 obj +<> +endobj + +8 0 obj +<> +endobj + +9 0 obj +<> +stream +BT +/F1 20 Tf +100 600 TD (Page1)Tj +ET +endstream +endobj + +10 0 obj +<> +stream +BT +/F1 20 Tf +100 600 TD (Page2)Tj +ET +endstream +endobj + +xref +0 11 +0000000000 65536 f +0000000016 00000 n +0000000077 00000 n +0000000135 00000 n +0000000249 00000 n +0000000364 00000 n +0000000443 00000 n +0000000519 00000 n +0000000585 00000 n +0000000645 00000 n +0000000730 00000 n + +trailer +<> +startxref +816 +%%EOF diff --git a/tests/resources/full_toc.txt b/tests/resources/full_toc.txt new file mode 100644 index 0000000..bc7d216 --- /dev/null +++ b/tests/resources/full_toc.txt @@ -0,0 +1 @@ +[1, 'HAUPTÜBERSICHT', -1, {'kind': 3, 'xref': 2, 'file': '../SDW2006.PDF', 'zoom': 0.0}][1, 'Januar 01/2006', -1, {'kind': 3, 'xref': 3, 'file': '01004INH.pdf', 'collapse': False, 'zoom': 0.0}][2, 'SPEKTROGRAMM', -1, {'kind': 0, 'xref': 4, 'page': -1, 'collapse': False, 'zoom': 0.0}][3, 'Urzeit-Godzilla', -1, {'kind': 5, 'xref': 87, 'file': '01008SP.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Frühchristliche Mosaike im Knast', -1, {'kind': 5, 'xref': 102, 'file': '01008SP.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Evolution auf Eis', -1, {'kind': 5, 'xref': 100, 'file': '01008SP.pdf', 'page': 1, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Entwarnung bei Kondensstreifen', -1, {'kind': 5, 'xref': 98, 'file': '01008SP.pdf', 'page': 1, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Spermatausch beim Schnecken-Sex', -1, {'kind': 5, 'xref': 96, 'file': '01008SP.pdf', 'page': 1, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Mehr Monde für Pluto', -1, {'kind': 5, 'xref': 94, 'file': '01008SP.pdf', 'page': 2, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Endlich ein Malaria-Impfstoff', -1, {'kind': 5, 'xref': 92, 'file': '01008SP.pdf', 'page': 2, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Spuren der ersten Sterne', -1, {'kind': 5, 'xref': 90, 'file': '01008SP.pdf', 'page': 2, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Bild des Monats', -1, {'kind': 5, 'xref': 88, 'file': '01008SP.pdf', 'page': 3, 'to': Point(0.0, 0.0), 'zoom': 0.0}][2, 'FORSCHUNG AKTUELL', -1, {'kind': 0, 'xref': 23, 'page': -1, 'collapse': False, 'zoom': 0.0}][3, 'Der Super-Teilchenfänger in der Pampa', -1, {'kind': 5, 'xref': 24, 'file': '01012FA.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Auf der Fährte der Lepra', -1, {'kind': 5, 'xref': 29, 'file': '01012FA.pdf', 'page': 2, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Vampire gegen Schlaganfall', -1, {'kind': 5, 'xref': 27, 'file': '01012FA.pdf', 'page': 4, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Der Flug des Kolibris', -1, {'kind': 5, 'xref': 25, 'file': '01012FA.pdf', 'page': 7, 'to': Point(0.0, 0.0), 'zoom': 0.0}][2, 'THEMEN', -1, {'kind': 0, 'xref': 20, 'page': -1, 'collapse': False, 'zoom': 0.0}][3, 'Entwicklung von Spiralgalaxien', -1, {'kind': 3, 'xref': 21, 'file': '01022HA.pdf', 'zoom': 0.0}][3, 'Geschichtsträchtige Genspuren', -1, {'kind': 3, 'xref': 46, 'file': '01030HA.pdf', 'zoom': 0.0}][3, 'Was Sedimente verraten', -1, {'kind': 3, 'xref': 44, 'file': '01042HA.pdf', 'zoom': 0.0}][3, 'Von Baumringen und Regenmengen', -1, {'kind': 3, 'xref': 42, 'file': '01050HA.pdf', 'zoom': 0.0}][3, 'Software-Agenten in Not', -1, {'kind': 3, 'xref': 40, 'file': '01056HA.pdf', 'zoom': 0.0}][3, 'Künstlicher kalter Antiwasserstoff', -1, {'kind': 3, 'xref': 38, 'file': '01062HA.pdf', 'zoom': 0.0}][3, 'Rüsten gegen eine Pandemie', -1, {'kind': 3, 'xref': 36, 'file': '01072HA.pdf', 'zoom': 0.0}][3, 'Satelliten zeigen Lawinengefahr', -1, {'kind': 3, 'xref': 34, 'file': '01084HA.pdf', 'zoom': 0.0}][3, 'Provokante Verheißung: Update für den Menschen', -1, {'kind': 3, 'xref': 22, 'file': '01100HA.pdf', 'zoom': 0.0}][2, 'KOMMENTAR', -1, {'kind': 0, 'xref': 18, 'page': -1, 'collapse': False, 'zoom': 0.0}][3, 'Springers Einwüfe: Holland, die Hydrometropole', -1, {'kind': 5, 'xref': 19, 'file': '01012FA.pdf', 'page': 8, 'to': Point(0.0, 0.0), 'zoom': 0.0}][2, 'WISSENSCHAFT IM ...', -1, {'kind': 0, 'xref': 15, 'page': -1, 'collapse': False, 'zoom': 0.0}][3, 'Alltag: Eine Decke für die Straße', -1, {'kind': 5, 'xref': 16, 'file': '01040WA.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Rückblick: Mozarts Ohr • Per Auto zum Südpol u.a.', -1, {'kind': 5, 'xref': 17, 'file': '01081IR.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][2, 'JUNGE WISSENSCHAFT', -1, {'kind': 0, 'xref': 13, 'page': -1, 'collapse': False, 'zoom': 0.0}][3, 'Ein Putzroboter für die Mama', -1, {'kind': 5, 'xref': 14, 'file': '01082JW.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][2, 'REZENSIONEN', -1, {'kind': 0, 'xref': 10, 'page': -1, 'collapse': False, 'zoom': 0.0}][3, 'Vulkanismus verstehen und erleben', -1, {'kind': 5, 'xref': 11, 'file': '01090RE.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Warum der Mensch glaubt', -1, {'kind': 5, 'xref': 72, 'file': '01090RE.pdf', 'page': 1, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Biomedizin und Ethik', -1, {'kind': 5, 'xref': 70, 'file': '01090RE.pdf', 'page': 2, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Mythos Meer', -1, {'kind': 5, 'xref': 68, 'file': '01090RE.pdf', 'page': 3, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Warum Frauen nicht schwach ... sind', -1, {'kind': 5, 'xref': 66, 'file': '01090RE.pdf', 'page': 4, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'PISA, Bach, Pythagoras', -1, {'kind': 5, 'xref': 12, 'file': '01090RE.pdf', 'page': 5, 'to': Point(0.0, 0.0), 'zoom': 0.0}][2, 'MATHEMATISCHE UNTERHALTUNGEN', -1, {'kind': 0, 'xref': 8, 'page': -1, 'collapse': False, 'zoom': 0.0}][3, 'Himmliches Ballett', -1, {'kind': 5, 'xref': 9, 'file': '01098MU.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][2, 'WEITERE RUBRIKEN', -1, {'kind': 0, 'xref': 5, 'page': -1, 'collapse': False, 'zoom': 0.0}][3, 'Editorial', -1, {'kind': 5, 'xref': 6, 'file': '01003ED.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Leserbriefe/Impressum', -1, {'kind': 5, 'xref': 81, 'file': '01006LB.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Preisrätsel', -1, {'kind': 5, 'xref': 79, 'file': '01090RE.pdf', 'page': 6, 'to': Point(0.0, 0.0), 'zoom': 0.0}][3, 'Vorschau', -1, {'kind': 5, 'xref': 7, 'file': '01106VO.pdf', 'page': 0, 'to': Point(0.0, 0.0), 'zoom': 0.0}] \ No newline at end of file diff --git a/tests/resources/github_sample.pdf b/tests/resources/github_sample.pdf new file mode 100644 index 0000000..58f38b8 Binary files /dev/null and b/tests/resources/github_sample.pdf differ diff --git a/tests/resources/has-bad-fonts.pdf b/tests/resources/has-bad-fonts.pdf new file mode 100644 index 0000000..8636635 Binary files /dev/null and b/tests/resources/has-bad-fonts.pdf differ diff --git a/tests/resources/image-file1.pdf b/tests/resources/image-file1.pdf new file mode 100644 index 0000000..5b896b0 Binary files /dev/null and b/tests/resources/image-file1.pdf differ diff --git a/tests/resources/img-transparent.png b/tests/resources/img-transparent.png new file mode 100644 index 0000000..ad6d6dd Binary files /dev/null and b/tests/resources/img-transparent.png differ diff --git a/tests/resources/joined.pdf b/tests/resources/joined.pdf new file mode 100644 index 0000000..f4379c5 Binary files /dev/null and b/tests/resources/joined.pdf differ diff --git a/tests/resources/metadata.txt b/tests/resources/metadata.txt new file mode 100644 index 0000000..1d7c0a9 --- /dev/null +++ b/tests/resources/metadata.txt @@ -0,0 +1 @@ +{"format": "PDF 1.6", "title": "RUBRIK_Editorial_01-06.indd", "author": "Natalie Schaefer", "subject": "", "keywords": "", "creator": "", "producer": "Acrobat Distiller 7.0.5 (Windows)", "creationDate": "D:20070113191400+01'00'", "modDate": "D:20070120104154+01'00'", "trapped": "", "encryption": null} \ No newline at end of file diff --git a/tests/resources/nur-ruhig.jpg b/tests/resources/nur-ruhig.jpg new file mode 100644 index 0000000..d6b3357 Binary files /dev/null and b/tests/resources/nur-ruhig.jpg differ diff --git a/tests/resources/quad-calc-0.pdf b/tests/resources/quad-calc-0.pdf new file mode 100644 index 0000000..0b81fba Binary files /dev/null and b/tests/resources/quad-calc-0.pdf differ diff --git a/tests/resources/simple_toc.txt b/tests/resources/simple_toc.txt new file mode 100644 index 0000000..b7b171a --- /dev/null +++ b/tests/resources/simple_toc.txt @@ -0,0 +1 @@ +[1, 'HAUPTÜBERSICHT', -1][1, 'Januar 01/2006', -1][2, 'SPEKTROGRAMM', -1][3, 'Urzeit-Godzilla', -1][3, 'Frühchristliche Mosaike im Knast', -1][3, 'Evolution auf Eis', -1][3, 'Entwarnung bei Kondensstreifen', -1][3, 'Spermatausch beim Schnecken-Sex', -1][3, 'Mehr Monde für Pluto', -1][3, 'Endlich ein Malaria-Impfstoff', -1][3, 'Spuren der ersten Sterne', -1][3, 'Bild des Monats', -1][2, 'FORSCHUNG AKTUELL', -1][3, 'Der Super-Teilchenfänger in der Pampa', -1][3, 'Auf der Fährte der Lepra', -1][3, 'Vampire gegen Schlaganfall', -1][3, 'Der Flug des Kolibris', -1][2, 'THEMEN', -1][3, 'Entwicklung von Spiralgalaxien', -1][3, 'Geschichtsträchtige Genspuren', -1][3, 'Was Sedimente verraten', -1][3, 'Von Baumringen und Regenmengen', -1][3, 'Software-Agenten in Not', -1][3, 'Künstlicher kalter Antiwasserstoff', -1][3, 'Rüsten gegen eine Pandemie', -1][3, 'Satelliten zeigen Lawinengefahr', -1][3, 'Provokante Verheißung: Update für den Menschen', -1][2, 'KOMMENTAR', -1][3, 'Springers Einwüfe: Holland, die Hydrometropole', -1][2, 'WISSENSCHAFT IM ...', -1][3, 'Alltag: Eine Decke für die Straße', -1][3, 'Rückblick: Mozarts Ohr • Per Auto zum Südpol u.a.', -1][2, 'JUNGE WISSENSCHAFT', -1][3, 'Ein Putzroboter für die Mama', -1][2, 'REZENSIONEN', -1][3, 'Vulkanismus verstehen und erleben', -1][3, 'Warum der Mensch glaubt', -1][3, 'Biomedizin und Ethik', -1][3, 'Mythos Meer', -1][3, 'Warum Frauen nicht schwach ... sind', -1][3, 'PISA, Bach, Pythagoras', -1][2, 'MATHEMATISCHE UNTERHALTUNGEN', -1][3, 'Himmliches Ballett', -1][2, 'WEITERE RUBRIKEN', -1][3, 'Editorial', -1][3, 'Leserbriefe/Impressum', -1][3, 'Preisrätsel', -1][3, 'Vorschau', -1] \ No newline at end of file diff --git a/tests/resources/symbol-list.pdf b/tests/resources/symbol-list.pdf new file mode 100644 index 0000000..5f9058f Binary files /dev/null and b/tests/resources/symbol-list.pdf differ diff --git a/tests/resources/symbols.txt b/tests/resources/symbols.txt new file mode 100644 index 0000000..90e07c0 --- /dev/null +++ b/tests/resources/symbols.txt @@ -0,0 +1,713 @@ +[{'closePath': True, + 'color': (1.0, 1.0, 1.0), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (1.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('l', (50.0, 50.0), (50.0, 100.0)), + ('l', (50.0, 100.0), (100.0, 75.0))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (50.0, 50.0, 100.0, 100.0), + 'seqno': 0, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 1.0}, + {'closePath': True, + 'color': (1.0, 1.0, 1.0), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (1.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('c', + (50.0, 135.0), + (63.807098388671875, 135.0), + (75.0, 123.8070068359375), + (75.0, 110.0)), + ('c', + (75.0, 110.0), + (75.0, 123.8070068359375), + (86.19290161132812, 135.0), + (100.0, 135.0)), + ('c', + (100.0, 135.0), + (86.19290161132812, 135.0), + (75.0, 146.1929931640625), + (75.0, 160.0)), + ('c', + (75.0, 160.0), + (75.0, 146.1929931640625), + (63.807098388671875, 135.0), + (50.0, 135.0))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (50.0, 110.0, 100.0, 160.0), + 'seqno': 2, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 1.0}, + {'closePath': True, + 'color': (0.0, 1.0, 0.0), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (0.0, 1.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('c', (75.0, 195.0), (50.0, 170.0), (100.0, 170.0), (75.0, 195.0)), + ('c', (75.0, 195.0), (100.0, 170.0), (100.0, 220.0), (75.0, 195.0)), + ('c', (75.0, 195.0), (50.0, 220.0), (50.0, 170.0), (75.0, 195.0)), + ('c', (75.0, 195.0), (100.0, 220.0), (50.0, 220.0), (75.0, 195.0))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (50.0, 170.0, 100.0, 220.0), + 'seqno': 4, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 0.30000001192092896}, + {'closePath': True, + 'color': (1.0, 1.0, 1.0), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (1.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('l', (75.0, 230.0), (100.0, 255.0)), + ('l', (100.0, 255.0), (75.0, 280.0)), + ('l', (75.0, 280.0), (50.0, 255.0))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (50.0, 230.0, 100.0, 280.0), + 'seqno': 6, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 1.0}, + {'closePath': True, + 'color': (1.0, 1.0, 1.0), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (0.8039219975471497, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('c', + (50.0, 315.0), + (50.0, 328.8070068359375), + (61.192901611328125, 340.0), + (75.0, 340.0)), + ('c', + (75.0, 340.0), + (88.80709838867188, 340.0), + (100.0, 328.8070068359375), + (100.0, 315.0)), + ('c', + (100.0, 315.0), + (100.0, 301.1929931640625), + (88.80709838867188, 290.0), + (75.0, 290.0)), + ('c', + (75.0, 290.0), + (61.192901611328125, 290.0), + (50.0, 301.1929931640625), + (50.0, 315.0))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (50.0, 290.0, 100.0, 340.0), + 'seqno': 8, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 2.0}, + {'closePath': True, + 'color': (0.0, 0.0, 0.0), + 'dashes': '[] 0', + 'items': [('c', + (50.0, 315.0), + (50.0, 328.8070068359375), + (61.192901611328125, 340.0), + (75.0, 340.0)), + ('c', + (75.0, 340.0), + (88.80709838867188, 340.0), + (100.0, 328.8070068359375), + (100.0, 315.0)), + ('c', + (100.0, 315.0), + (100.0, 301.1929931640625), + (88.80709838867188, 290.0), + (75.0, 290.0)), + ('c', + (75.0, 290.0), + (61.192901611328125, 290.0), + (50.0, 301.1929931640625), + (50.0, 315.0))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (50.0, 290.0, 100.0, 340.0), + 'seqno': 10, + 'stroke_opacity': 1.0, + 'type': 's', + 'width': 1.0}, + {'closePath': False, + 'color': (1.0, 1.0, 1.0), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (1.0, 1.0, 1.0), + 'fill_opacity': 1.0, + 'items': [('re', (56.5, 312.5, 93.5, 317.5), 1)], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (56.5, 312.5, 93.5, 317.5), + 'seqno': 11, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 3.0}, + {'closePath': True, + 'even_odd': False, + 'fill': (1.0, 1.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('c', + (50.0, 375.0), + (50.0, 388.8070068359375), + (61.192901611328125, 400.0), + (75.0, 400.0)), + ('c', + (75.0, 400.0), + (88.80709838867188, 400.0), + (100.0, 388.8070068359375), + (100.0, 375.0)), + ('c', + (100.0, 375.0), + (100.0, 361.1929931640625), + (88.80709838867188, 350.0), + (75.0, 350.0)), + ('c', + (75.0, 350.0), + (61.192901611328125, 350.0), + (50.0, 361.1929931640625), + (50.0, 375.0))], + 'layer': '', + 'rect': (50.0, 350.0, 100.0, 400.0), + 'seqno': 13, + 'type': 'f'}, + {'closePath': True, + 'even_odd': False, + 'fill': (0.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('c', + (60.0, 368.75), + (60.0, 372.2019958496094), + (62.23860168457031, 375.0), + (65.0, 375.0)), + ('c', + (65.0, 375.0), + (67.76139831542969, 375.0), + (70.0, 372.2019958496094), + (70.0, 368.75)), + ('c', + (70.0, 368.75), + (70.0, 365.2980041503906), + (67.76139831542969, 362.5), + (65.0, 362.5)), + ('c', + (65.0, 362.5), + (62.23860168457031, 362.5), + (60.0, 365.2980041503906), + (60.0, 368.75)), + ('c', + (80.0, 368.75), + (80.0, 372.2019958496094), + (82.23860168457031, 375.0), + (85.0, 375.0)), + ('c', + (85.0, 375.0), + (87.76139831542969, 375.0), + (90.0, 372.2019958496094), + (90.0, 368.75)), + ('c', + (90.0, 368.75), + (90.0, 365.2980041503906), + (87.76139831542969, 362.5), + (85.0, 362.5)), + ('c', + (85.0, 362.5), + (82.23860168457031, 362.5), + (80.0, 365.2980041503906), + (80.0, 368.75))], + 'layer': '', + 'rect': (60.0, 362.5, 90.0, 375.0), + 'seqno': 14, + 'type': 'f'}, + {'closePath': False, + 'color': (0.0, 0.0, 0.0), + 'dashes': '[] 0', + 'items': [('c', + (60.0, 387.5), + (68.2843017578125, 380.59600830078125), + (81.7156982421875, 380.59600830078125), + (90.0, 387.5))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (60.0, 380.59600830078125, 90.0, 387.5), + 'seqno': 15, + 'stroke_opacity': 1.0, + 'type': 's', + 'width': 1.0}, + {'closePath': False, + 'color': (1.0, 0.6470590233802795, 0.0), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (1.0, 0.8274509906768799, 0.6078429818153381), + 'fill_opacity': 1.0, + 'items': [('c', + (50.0, 433.6669921875), + (60.30929946899414, 433.6669921875), + (68.66670227050781, 426.50299072265625), + (68.66670227050781, 417.6669921875)), + ('c', + (68.66670227050781, 417.6669921875), + (74.55770111083984, 416.1940002441406), + (74.55770111083984, 423.35699462890625), + (68.66670227050781, 433.6669921875)), + ('l', + (68.66670227050781, 433.6669921875), + (95.33329772949219, 433.6669921875)), + ('c', + (95.33329772949219, 433.6669921875), + (100.66699981689453, 433.6669921875), + (100.66699981689453, 439.0), + (95.33329772949219, 439.0)), + ('l', (95.33329772949219, 439.0), (79.33329772949219, 439.0)), + ('l', (79.33329772949219, 439.0), (87.33329772949219, 439.0)), + ('c', + (87.33329772949219, 439.0), + (92.66670227050781, 439.0), + (92.66670227050781, 444.3330078125), + (87.33329772949219, 444.3330078125)), + ('l', + (87.33329772949219, 444.3330078125), + (79.33329772949219, 444.3330078125)), + ('l', + (79.33329772949219, 444.3330078125), + (84.66670227050781, 444.3330078125)), + ('c', + (84.66670227050781, 444.3330078125), + (90.0, 444.3330078125), + (90.0, 449.6669921875), + (84.66670227050781, 449.6669921875)), + ('l', + (84.66670227050781, 449.6669921875), + (79.33329772949219, 449.6669921875)), + ('l', + (79.33329772949219, 449.6669921875), + (83.33329772949219, 449.6669921875)), + ('c', + (83.33329772949219, 449.6669921875), + (88.66670227050781, 449.6669921875), + (88.66670227050781, 455.0), + (83.33329772949219, 455.0)), + ('l', (83.33329772949219, 455.0), (50.0, 455.0))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (50.0, 416.1940002441406, 100.66699981689453, 455.0), + 'seqno': 16, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 1.0}, + {'closePath': True, + 'color': (1.0, 0.0, 0.0), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (1.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('c', (75.0, 485.0), (62.5, 470.0), (50.0, 490.0), (75.0, 510.0)), + ('c', (75.0, 485.0), (87.5, 470.0), (100.0, 490.0), (75.0, 510.0))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (50.0, 470.0, 100.0, 510.0), + 'seqno': 18, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 1.0}, + {'closePath': False, + 'color': (0.9333329796791077, 0.8470590114593506, 0.6823530197143555), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391), + 'fill_opacity': 1.0, + 'items': [('re', + (56.52170181274414, + 547.753173828125, + 85.5072021484375, + 562.2459716796875), + 1)], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (56.52170181274414, + 547.753173828125, + 85.5072021484375, + 562.2459716796875), + 'seqno': 20, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 0.07246380299329758}, + {'closePath': False, + 'color': (0.9333329796791077, 0.8470590114593506, 0.6823530197143555), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (0.9333329796791077, 0.8470590114593506, 0.6823530197143555), + 'fill_opacity': 1.0, + 'items': [('l', + (56.52170181274414, 547.7540283203125), + (59.4202995300293, 550.6519775390625)), + ('l', + (59.4202995300293, 550.6519775390625), + (59.4202995300293, 559.3480224609375)), + ('l', + (59.4202995300293, 559.3480224609375), + (56.52170181274414, 562.2459716796875)), + ('l', + (85.5072021484375, 547.7540283203125), + (82.60870361328125, 550.6519775390625)), + ('l', + (82.60870361328125, 550.6519775390625), + (82.60870361328125, 559.3480224609375)), + ('l', + (82.60870361328125, 559.3480224609375), + (85.5072021484375, 562.2459716796875))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (56.52170181274414, + 547.7540283203125, + 85.5072021484375, + 562.2459716796875), + 'seqno': 22, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 0.07246380299329758}, + {'closePath': False, + 'color': (0.8039219975471497, 0.7294120192527771, 0.5882350206375122), + 'dashes': '[] 0', + 'items': [('l', + (59.4202995300293, 550.6519775390625), + (82.60870361328125, 550.6519775390625)), + ('l', + (59.4202995300293, 559.3480224609375), + (82.60870361328125, 559.3480224609375))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (59.4202995300293, + 550.6519775390625, + 82.60870361328125, + 559.3480224609375), + 'seqno': 24, + 'stroke_opacity': 1.0, + 'type': 's', + 'width': 0.07246380299329758}, + {'even_odd': False, + 'fill': (0.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('re', + (56.52170181274414, + 547.753173828125, + 63.76808166503906, + 562.2459716796875), + 1)], + 'layer': '', + 'rect': (56.52170181274414, + 547.753173828125, + 63.76808166503906, + 562.2459716796875), + 'seqno': 25, + 'type': 'f'}, + {'even_odd': False, + 'fill': (1.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('c', + (56.52170181274414, 547.7540283203125), + (47.82609939575195, 547.7540283203125), + (47.82609939575195, 562.2459716796875), + (56.52170181274414, 562.2459716796875))], + 'layer': '', + 'rect': (47.82609939575195, + 547.7540283203125, + 56.52170181274414, + 562.2459716796875), + 'seqno': 26, + 'type': 'f'}, + {'even_odd': False, + 'fill': (0.9333329796791077, 0.8470590114593506, 0.6823530197143555), + 'fill_opacity': 1.0, + 'items': [('l', (85.5072021484375, 547.7540283203125), (100.0, 555.0)), + ('l', (100.0, 555.0), (85.5072021484375, 562.2459716796875))], + 'layer': '', + 'rect': (85.5072021484375, 547.7540283203125, 100.0, 562.2459716796875), + 'seqno': 27, + 'type': 'f'}, + {'closePath': True, + 'color': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391), + 'fill_opacity': 1.0, + 'items': [('c', + (85.5072021484375, 547.7540283203125), + (86.30770111083984, 548.553955078125), + (85.00990295410156, 549.8519897460938), + (82.60870361328125, 550.6519775390625))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (82.60870361328125, + 547.7540283203125, + 86.30770111083984, + 550.6519775390625), + 'seqno': 28, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 0.07246380299329758}, + {'closePath': True, + 'color': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391), + 'fill_opacity': 1.0, + 'items': [('c', + (82.60870361328125, 550.6519775390625), + (87.2510986328125, 553.052978515625), + (87.2510986328125, 556.947021484375), + (82.60870361328125, 559.3480224609375))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (82.60870361328125, + 550.6519775390625, + 87.2510986328125, + 559.3480224609375), + 'seqno': 30, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 0.07246380299329758}, + {'closePath': True, + 'color': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391), + 'dashes': '[] 0', + 'even_odd': False, + 'fill': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391), + 'fill_opacity': 1.0, + 'items': [('c', + (82.60870361328125, 559.3480224609375), + (85.00990295410156, 560.1480102539062), + (86.30770111083984, 561.446044921875), + (85.5072021484375, 562.2459716796875))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (82.60870361328125, + 559.3480224609375, + 86.30770111083984, + 562.2459716796875), + 'seqno': 32, + 'stroke_opacity': 1.0, + 'type': 'fs', + 'width': 0.07246380299329758}, + {'even_odd': False, + 'fill': (0.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('l', (94.2029037475586, 552.1010131835938), (100.0, 555.0)), + ('l', (100.0, 555.0), (94.2029037475586, 557.8989868164062)), + ('c', + (94.2029037475586, 552.1010131835938), + (92.60209655761719, 553.7020263671875), + (92.60209655761719, 556.2979736328125), + (94.2029037475586, 557.8989868164062))], + 'layer': '', + 'rect': (92.60209655761719, 552.1010131835938, 100.0, 557.8989868164062), + 'seqno': 34, + 'type': 'f'}, + {'closePath': False, + 'color': (0.7215690016746521, 0.5254899859428406, 0.04313730075955391), + 'dashes': '[] 0', + 'items': [('l', + (85.5072021484375, 547.7540283203125), + (82.60870361328125, 550.6519775390625)), + ('l', + (82.60870361328125, 550.6519775390625), + (82.60870361328125, 559.3480224609375)), + ('l', + (82.60870361328125, 559.3480224609375), + (85.5072021484375, 562.2459716796875))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (82.60870361328125, + 547.7540283203125, + 85.5072021484375, + 562.2459716796875), + 'seqno': 35, + 'stroke_opacity': 1.0, + 'type': 's', + 'width': 0.07246380299329758}, + {'closePath': False, + 'color': (0.0, 0.0, 0.0), + 'dashes': '[] 0', + 'items': [('l', + (63.76810073852539, 547.7540283203125), + (85.5072021484375, 547.7540283203125)), + ('l', (85.5072021484375, 547.7540283203125), (100.0, 555.0)), + ('l', (100.0, 555.0), (85.5072021484375, 562.2459716796875)), + ('l', + (85.5072021484375, 562.2459716796875), + (63.76810073852539, 562.2459716796875))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (63.76810073852539, 547.7540283203125, 100.0, 562.2459716796875), + 'seqno': 36, + 'stroke_opacity': 1.0, + 'type': 's', + 'width': 1.0}, + {'even_odd': False, + 'fill': (0.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('re', + (65.94200134277344, + 552.826171875, + 73.18838500976562, + 557.1740112304688), + 1), + ('c', + (73.18840026855469, 552.8259887695312), + (75.18939971923828, 554.0269775390625), + (75.18939971923828, 555.9730224609375), + (73.18840026855469, 557.1740112304688)), + ('c', + (65.94200134277344, 552.8259887695312), + (63.941001892089844, 554.0269775390625), + (63.941001892089844, 555.9730224609375), + (65.94200134277344, 557.1740112304688))], + 'layer': '', + 'rect': (63.941001892089844, + 552.826171875, + 75.18939971923828, + 557.1740112304688), + 'seqno': 37, + 'type': 'f'}, + {'closePath': True, + 'color': (1.0, 1.0, 1.0), + 'dashes': '[] 0', + 'items': [('l', + (58.937198638916016, 548.47802734375), + (58.937198638916016, 561.52197265625)), + ('l', + (61.352699279785156, 548.47802734375), + (61.352699279785156, 561.52197265625))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (58.937198638916016, + 548.47802734375, + 61.352699279785156, + 561.52197265625), + 'seqno': 38, + 'stroke_opacity': 1.0, + 'type': 's', + 'width': 1.1594200134277344}, + {'closePath': True, + 'even_odd': False, + 'fill': (1.0, 1.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('c', + (50.0, 615.0), + (50.0, 628.8070068359375), + (61.192901611328125, 640.0), + (75.0, 640.0)), + ('c', + (75.0, 640.0), + (88.80709838867188, 640.0), + (100.0, 628.8070068359375), + (100.0, 615.0)), + ('c', + (100.0, 615.0), + (100.0, 601.1929931640625), + (88.80709838867188, 590.0), + (75.0, 590.0)), + ('c', + (75.0, 590.0), + (61.192901611328125, 590.0), + (50.0, 601.1929931640625), + (50.0, 615.0))], + 'layer': '', + 'rect': (50.0, 590.0, 100.0, 640.0), + 'seqno': 39, + 'type': 'f'}, + {'closePath': True, + 'even_odd': False, + 'fill': (0.0, 0.0, 0.0), + 'fill_opacity': 1.0, + 'items': [('c', + (60.0, 608.75), + (60.0, 612.2020263671875), + (62.23860168457031, 615.0), + (65.0, 615.0)), + ('c', + (65.0, 615.0), + (67.76139831542969, 615.0), + (70.0, 612.2020263671875), + (70.0, 608.75)), + ('c', + (70.0, 608.75), + (70.0, 605.2979736328125), + (67.76139831542969, 602.5), + (65.0, 602.5)), + ('c', + (65.0, 602.5), + (62.23860168457031, 602.5), + (60.0, 605.2979736328125), + (60.0, 608.75)), + ('c', + (80.0, 608.75), + (80.0, 612.2020263671875), + (82.23860168457031, 615.0), + (85.0, 615.0)), + ('c', + (85.0, 615.0), + (87.76139831542969, 615.0), + (90.0, 612.2020263671875), + (90.0, 608.75)), + ('c', + (90.0, 608.75), + (90.0, 605.2979736328125), + (87.76139831542969, 602.5), + (85.0, 602.5)), + ('c', + (85.0, 602.5), + (82.23860168457031, 602.5), + (80.0, 605.2979736328125), + (80.0, 608.75))], + 'layer': '', + 'rect': (60.0, 602.5, 90.0, 615.0), + 'seqno': 40, + 'type': 'f'}, + {'closePath': False, + 'color': (0.0, 0.0, 0.0), + 'dashes': '[] 0', + 'items': [('c', + (60.0, 624.375), + (68.2843017578125, 633.0040283203125), + (81.7156982421875, 633.0040283203125), + (90.0, 624.375))], + 'layer': '', + 'lineCap': (0, 0, 0), + 'lineJoin': 0.0, + 'rect': (60.0, 624.375, 90.0, 633.0040283203125), + 'seqno': 41, + 'stroke_opacity': 1.0, + 'type': 's', + 'width': 1.0}] diff --git a/tests/resources/test-2333.pdf b/tests/resources/test-2333.pdf new file mode 100644 index 0000000..6529933 Binary files /dev/null and b/tests/resources/test-2333.pdf differ diff --git a/tests/resources/test-2462.pdf b/tests/resources/test-2462.pdf new file mode 100644 index 0000000..b5126fe Binary files /dev/null and b/tests/resources/test-2462.pdf differ diff --git a/tests/resources/test2093.pdf b/tests/resources/test2093.pdf new file mode 100644 index 0000000..38e7722 Binary files /dev/null and b/tests/resources/test2093.pdf differ diff --git a/tests/resources/test2182.pdf b/tests/resources/test2182.pdf new file mode 100644 index 0000000..3a28c70 Binary files /dev/null and b/tests/resources/test2182.pdf differ diff --git a/tests/resources/test2238.pdf b/tests/resources/test2238.pdf new file mode 100644 index 0000000..6871fe1 --- /dev/null +++ b/tests/resources/test2238.pdf @@ -0,0 +1,68 @@ +Here should not be anything - it is not correct +But some readers can still read it +---------------------------------- +__%PDF-1.1 +1 0 obj + << + /Type /Catalog + /Pages 2 0 R + >> +endobj +2 0 obj + << + /Type /Pages + /Kids [3 0 R] + /Count 1 + /MediaBox [0 0 300 144] + >> +endobj +3 0 obj + << + /Type /Page + /Parent 2 0 R + /Resources + << + /Font + << + /F1 + << + /Type /Font + /Subtype /Type1 + /BaseFont /Times-Roman + >> + >> + >> + /Contents 4 0 R + >> +endobj +4 0 obj + << + /Length 55 + >> + stream + BT + /F1 18 Tf + 0 0 Td + (Hello World) Tj + ET + endstream +endobj +xref +0 5 +0000000000 65535 f +0000000107 00000 n +0000000162 00000 n +0000000253 00000 n +0000000456 00000 n +trailer + << + /Root 1 0 R + /Size 5 + >> +startxref +564 +%%EOF +----------------------------------- +Here should not be anything as well +But some readers can still read it +42 diff --git a/tests/resources/test_1645_expected.pdf b/tests/resources/test_1645_expected.pdf new file mode 100644 index 0000000..8b21fe8 Binary files /dev/null and b/tests/resources/test_1645_expected.pdf differ diff --git a/tests/resources/test_1645_expected_1.22.pdf b/tests/resources/test_1645_expected_1.22.pdf new file mode 100644 index 0000000..b149b10 Binary files /dev/null and b/tests/resources/test_1645_expected_1.22.pdf differ diff --git a/tests/resources/test_1824.pdf b/tests/resources/test_1824.pdf new file mode 100644 index 0000000..362fc48 Binary files /dev/null and b/tests/resources/test_1824.pdf differ diff --git a/tests/resources/test_2108.pdf b/tests/resources/test_2108.pdf new file mode 100644 index 0000000..61f565f Binary files /dev/null and b/tests/resources/test_2108.pdf differ diff --git a/tests/resources/test_2270.pdf b/tests/resources/test_2270.pdf new file mode 100644 index 0000000..b6a75c1 Binary files /dev/null and b/tests/resources/test_2270.pdf differ diff --git a/tests/resources/type3font.pdf b/tests/resources/type3font.pdf new file mode 100644 index 0000000..5feb960 Binary files /dev/null and b/tests/resources/type3font.pdf differ diff --git a/tests/resources/v110-changes.pdf b/tests/resources/v110-changes.pdf new file mode 100644 index 0000000..7644d24 Binary files /dev/null and b/tests/resources/v110-changes.pdf differ diff --git a/tests/resources/widgettest.pdf b/tests/resources/widgettest.pdf new file mode 100644 index 0000000..8233384 Binary files /dev/null and b/tests/resources/widgettest.pdf differ diff --git a/tests/test_annots.py b/tests/test_annots.py new file mode 100644 index 0000000..320d735 --- /dev/null +++ b/tests/test_annots.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +""" +Test PDF annotation insertions. +""" +import fitz +import os + +fitz.TOOLS.set_annot_stem("jorj") + +red = (1, 0, 0) +blue = (0, 0, 1) +gold = (1, 1, 0) +green = (0, 1, 0) + +displ = fitz.Rect(0, 50, 0, 50) +r = fitz.Rect(72, 72, 220, 100) +t1 = u"têxt üsès Lätiñ charß,\nEUR: €, mu: µ, super scripts: ²³!" +rect = fitz.Rect(100, 100, 200, 200) + + +def test_caret(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_caret_annot(rect.tl) + assert annot.type == (14, "Caret") + annot.update(rotate=20) + page.annot_names() + page.annot_xrefs() + + +def test_freetext(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_freetext_annot( + rect, + t1, + fontsize=10, + rotate=90, + text_color=blue, + fill_color=gold, + align=fitz.TEXT_ALIGN_CENTER, + ) + annot.set_border(width=0.3, dashes=[2]) + annot.update(text_color=blue, fill_color=gold) + assert annot.type == (2, "FreeText") + + +def test_text(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_text_annot(r.tl, t1) + assert annot.type == (0, "Text") + + +def test_highlight(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_highlight_annot(rect) + assert annot.type == (8, "Highlight") + + +def test_underline(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_underline_annot(rect) + assert annot.type == (9, "Underline") + + +def test_squiggly(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_squiggly_annot(rect) + assert annot.type == (10, "Squiggly") + + +def test_strikeout(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_strikeout_annot(rect) + assert annot.type == (11, "StrikeOut") + page.delete_annot(annot) + + +def test_polyline(): + doc = fitz.open() + page = doc.new_page() + rect = page.rect + (100, 36, -100, -36) + cell = fitz.make_table(rect, rows=10) + for i in range(10): + annot = page.add_polyline_annot((cell[i][0].bl, cell[i][0].br)) + annot.set_line_ends(i, i) + annot.update() + for i, annot in enumerate(page.annots()): + assert annot.line_ends == (i, i) + assert annot.type == (7, "PolyLine") + + +def test_polygon(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_polygon_annot([rect.bl, rect.tr, rect.br, rect.tl]) + assert annot.type == (6, "Polygon") + + +def test_line(): + doc = fitz.open() + page = doc.new_page() + rect = page.rect + (100, 36, -100, -36) + cell = fitz.make_table(rect, rows=10) + for i in range(10): + annot = page.add_line_annot(cell[i][0].bl, cell[i][0].br) + annot.set_line_ends(i, i) + annot.update() + for i, annot in enumerate(page.annots()): + assert annot.line_ends == (i, i) + assert annot.type == (3, "Line") + + +def test_square(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_rect_annot(rect) + assert annot.type == (4, "Square") + + +def test_circle(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_circle_annot(rect) + assert annot.type == (5, "Circle") + + +def test_fileattachment(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_file_annot(rect.tl, b"just anything for testing", "testdata.txt") + assert annot.type == (17, "FileAttachment") + + +def test_stamp(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_stamp_annot(r, stamp=10) + assert annot.type == (13, "Stamp") + annot_id = annot.info["id"] + annot_xref = annot.xref + a1 = page.load_annot(annot_id) + a2 = page.load_annot(annot_xref) + page = doc.reload_page(page) + + +def test_redact(): + doc = fitz.open() + page = doc.new_page() + annot = page.add_redact_annot(r, text="Hello") + annot.update( + cross_out=True, + rotate=-1, + ) + assert annot.type == (12, "Redact") + x = annot._get_redact_values() + pix = annot.get_pixmap() + info = annot.info + annot.set_info(info) + assert not annot.has_popup + annot.set_popup(r) + s = annot.popup_rect + assert s == r + page.apply_redactions() + +def test_1645(): + ''' + Test fix for #1645. + ''' + path_in = os.path.abspath( f'{__file__}/../resources/symbol-list.pdf') + if fitz.mupdf_version_tuple[:2] >= (1, 22): + path_expected = os.path.abspath( f'{__file__}/../resources/test_1645_expected_1.22.pdf') + else: + path_expected = os.path.abspath( f'{__file__}/../resources/test_1645_expected.pdf') + path_out = os.path.abspath( f'{__file__}/../test_1645_out.pdf') + doc = fitz.open(path_in) + page = doc[0] + page_bounds = page.bound() + annot_loc = fitz.Rect(page_bounds.x0, page_bounds.y0, page_bounds.x0 + 75, page_bounds.y0 + 15) + page.add_freetext_annot(annot_loc * page.derotation_matrix, "TEST", fontsize=18, + fill_color=fitz.utils.getColor("FIREBRICK1"), rotate=page.rotation) + doc.save(path_out, garbage=1, deflate=True, no_new_id=True) + print(f'Have created {path_out}. comparing with {path_expected}.') + with open( path_out, 'rb') as f: + out = f.read() + with open( path_expected, 'rb') as f: + expected = f.read() + assert out == expected, f'Files differ: {path_out} {path_expected}' + +def test_1824(): + ''' + Test for fix for #1824: SegFault when applying redactions overlapping a + transparent image. + ''' + path = os.path.abspath( f'{__file__}/../resources/test_1824.pdf') + doc=fitz.open(path) + page=doc[0] + page.apply_redactions() + +def test_2270(): + ''' + https://github.com/pymupdf/PyMuPDF/issues/2270 + ''' + path = os.path.abspath( f'{__file__}/../resources/test_2270.pdf') + with fitz.open(path) as document: + for page_number, page in enumerate(document): + for textBox in page.annots(types=(fitz.PDF_ANNOT_FREE_TEXT,fitz.PDF_ANNOT_TEXT)): + print("textBox.type :", textBox.type) + print("textBox.get_text('words') : ", textBox.get_text('words')) + print("textBox.get_text('text') : ", textBox.get_text('text')) + print("textBox.get_textbox(textBox.rect) : ", textBox.get_textbox(textBox.rect)) + print("textBox.info['content'] : ", textBox.info['content']) + assert textBox.type == (2, 'FreeText') + assert textBox.get_text('words')[0][4] == 'abc123' + assert textBox.get_text('text') == 'abc123\n' + assert textBox.get_textbox(textBox.rect) == 'abc123' + assert textBox.info['content'] == 'abc123' + diff --git a/tests/test_badfonts.py b/tests/test_badfonts.py new file mode 100644 index 0000000..dc55a83 --- /dev/null +++ b/tests/test_badfonts.py @@ -0,0 +1,15 @@ +""" +Ensure we can deal with non-Latin font names. +""" +import os + +import fitz + + +def test_survive_names(): + scriptdir = os.path.abspath(os.path.dirname(__file__)) + filename = os.path.join(scriptdir, "resources", "has-bad-fonts.pdf") + doc = fitz.open(filename) + print("File '%s' uses the following fonts on page 0:" % doc.name) + for f in doc.get_page_fonts(0): + print(f) diff --git a/tests/test_crypting.py b/tests/test_crypting.py new file mode 100644 index 0000000..81b3a93 --- /dev/null +++ b/tests/test_crypting.py @@ -0,0 +1,39 @@ +""" +Check PDF encryption: +* make a PDF with owber and user passwords +* open and decrypt as owner or user +""" +import fitz + + +def test_encryption(): + text = "some secret information" # keep this data secret + perm = int( + fitz.PDF_PERM_ACCESSIBILITY # always use this + | fitz.PDF_PERM_PRINT # permit printing + | fitz.PDF_PERM_COPY # permit copying + | fitz.PDF_PERM_ANNOTATE # permit annotations + ) + owner_pass = "owner" # owner password + user_pass = "user" # user password + encrypt_meth = fitz.PDF_ENCRYPT_AES_256 # strongest algorithm + doc = fitz.open() # empty pdf + page = doc.new_page() # empty page + page.insert_text((50, 72), text) # insert the data + tobytes = doc.tobytes( + encryption=encrypt_meth, # set the encryption method + owner_pw=owner_pass, # set the owner password + user_pw=user_pass, # set the user password + permissions=perm, # set permissions + ) + doc.close() + doc = fitz.open("pdf", tobytes) + assert doc.needs_pass + assert doc.is_encrypted + rc = doc.authenticate("owner") + assert rc == 4 + assert not doc.is_encrypted + doc.close() + doc = fitz.open("pdf", tobytes) + rc = doc.authenticate("user") + assert rc == 2 diff --git a/tests/test_docs_samples.py b/tests/test_docs_samples.py new file mode 100644 index 0000000..1f48977 --- /dev/null +++ b/tests/test_docs_samples.py @@ -0,0 +1,47 @@ +''' +Test sample scripts in docs/samples/. +''' + +import glob +import os +import pytest +import runpy + +# We only look at sample scripts that can run standalone (i.e. don't require +# sys.argv). +# +root = os.path.abspath(f'{__file__}/../..') +samples = [] +for p in glob.glob(f'{root}/docs/samples/*.py'): + if os.path.basename(p) in ( + 'make-bold.py', # Needs sys.argv[1]. + 'multiprocess-gui.py', # GUI. + 'multiprocess-render.py', # Needs sys.argv[1]. + 'text-lister.py', # Needs sys.argv[1]. + ): + print(f'Not testing: {p}') + else: + samples.append(p) + +def _test_all(): + # Allow runnings tests directly without pytest. + import subprocess + import sys + e = 0 + for sample in samples: + print( f'Running: {sample}') + sys.stdout.flush() + try: + subprocess.check_call( f'{sys.executable} {sample}', shell=1, text=1) + except Exception: + print( f'Failed: {sample}') + e += 1 + if e: + raise Exception( f'Errors: {e}') + +# We use pytest.mark.parametrize() to run sample scripts via a fn, which +# ensures that pytest treats each script as a test. +# +@pytest.mark.parametrize('sample', samples) +def test_docs_samples(sample): + runpy.run_path(sample) diff --git a/tests/test_drawings.py b/tests/test_drawings.py new file mode 100644 index 0000000..e03ee36 --- /dev/null +++ b/tests/test_drawings.py @@ -0,0 +1,177 @@ +""" +Extract drawings of a PDF page and compare with stored expected result. +""" +import io +import os +import sys +import pprint + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "symbol-list.pdf") +symbols = os.path.join(scriptdir, "resources", "symbols.txt") + + +def test_drawings1(): + symbols_text = open(symbols).read() # expected result + doc = fitz.open(filename) + page = doc[0] + paths = page.get_cdrawings() + out = io.StringIO() # pprint output goes here + pprint.pprint(paths, stream=out) + assert symbols_text == out.getvalue() + + +def test_drawings2(): + delta = (0, 20, 0, 20) + doc = fitz.open() + page = doc.new_page() + + r = fitz.Rect(100, 100, 200, 200) + page.draw_circle(r.br, 2, color=0) + r += delta + + page.draw_line(r.tl, r.br, color=0) + r += delta + + page.draw_oval(r, color=0) + r += delta + + page.draw_rect(r, color=0) + r += delta + + page.draw_quad(r.quad, color=0) + r += delta + + page.draw_polyline((r.tl, r.tr, r.br), color=0) + r += delta + + page.draw_bezier(r.tl, r.tr, r.br, r.bl, color=0) + r += delta + + page.draw_curve(r.tl, r.tr, r.br, color=0) + r += delta + + page.draw_squiggle(r.tl, r.br, color=0) + r += delta + + rects = [p["rect"] for p in page.get_cdrawings()] + bboxes = [b[1] for b in page.get_bboxlog()] + for i, r in enumerate(rects): + assert fitz.Rect(r) in fitz.Rect(bboxes[i]) + + +def _dict_difference(a, b): + ''' + Returns `(keys_a, keys_b, key_values)`, information about differences + between dicts `a` and `b`. + + `keys_a` is the set of keys that are in `a` but not in `b`. + + `keys_b` is the set of keys that are in `b` but not in `a`. + + `key_values` is a dict with keys that are in both `a` and `b` but where the + values differ; the values in this dict are `(value_a, value_b)`. + ''' + keys_a = set() + keys_b = set() + key_values = dict() + for key in a: + if key not in b: + keys_a.add( key) + for key in b: + if key not in a: + keys_b.add( key) + for key, va in a.items(): + if key in b: + vb = b[key] + if va != vb: + key_values[key] = (va, vb) + return keys_a, keys_b, key_values + + +def test_drawings3(): + doc = fitz.open() + + page1 = doc.new_page() + shape1 = page1.new_shape() + shape1.draw_line((10, 10), (10, 50)) + shape1.draw_line((10, 50), (100, 100)) + shape1.finish(closePath=False, color=(0,0,0), width=5) + shape1.commit() + drawings1 = list(page1.get_drawings()) + + page2 = doc.new_page() + shape2 = page2.new_shape() + shape2.draw_line((10, 10), (10, 50)) + shape2.draw_line((10, 50), (100, 100)) + shape2.finish(closePath=True, color=(0,0,0), width=5) + shape2.commit() + drawings2 = list(page2.get_drawings()) + + page3 = doc.new_page() + shape3 = page3.new_shape() + shape3.draw_line((10, 10), (10, 50)) + shape3.draw_line((10, 50), (100, 100)) + shape3.draw_line((100, 100), (50, 70)) + shape3.finish(closePath=False, color=(0,0,0), width=5) + shape3.commit() + drawings3 = list(page3.get_drawings()) + + page4 = doc.new_page() + shape4 = page4.new_shape() + shape4.draw_line((10, 10), (10, 50)) + shape4.draw_line((10, 50), (100, 100)) + shape4.draw_line((100, 100), (50, 70)) + shape4.finish(closePath=True, color=(0,0,0), width=5) + shape4.commit() + drawings4 = list(page4.get_drawings()) + + assert len(drawings1) == len(drawings2) == 1 + drawings1 = drawings1[0] + drawings2 = drawings2[0] + diff = _dict_difference( drawings1, drawings2) + assert diff == (set(), set(), {'closePath': (False, True)}) + + assert len(drawings3) == len(drawings4) == 1 + drawings3 = drawings3[0] + drawings4 = drawings4[0] + diff = _dict_difference( drawings3, drawings4) + assert diff == (set(), set(), {'closePath': (False, True)}) + +def test_2365(): + """Draw a filled rectangle on a new page. + + Then extract the page's vector graphics and confirm that only one path + was generated which has all the right properties.""" + doc = fitz.open() + page = doc.new_page() + rect = fitz.Rect(100, 100, 200, 200) + page.draw_rect( + rect, color=fitz.pdfcolor["black"], fill=fitz.pdfcolor["yellow"], width=3 + ) + paths = page.get_drawings() + assert len(paths) == 1 + path = paths[0] + assert path["type"] == "fs" + assert path["fill"] == fitz.pdfcolor["yellow"] + assert path["fill_opacity"] == 1 + assert path["color"] == fitz.pdfcolor["black"] + assert path["stroke_opacity"] == 1 + assert path["width"] == 3 + assert path["rect"] == rect + +def test_2462(): + """ + Assertion happens, if this code does NOT bring down the interpreter. + + Background: + We previously ignored clips for non-vector-graphics. However, ending + a clip does not refer back the object(s) that have been clipped. + In order to correctly compute the "scissor" rectangle, we now keep track + of the clipped object type. + """ + doc = fitz.open(f"{scriptdir}/resources/test-2462.pdf") + page = doc[0] + vg = page.get_drawings(extended=True) diff --git a/tests/test_embeddedfiles.py b/tests/test_embeddedfiles.py new file mode 100644 index 0000000..613c986 --- /dev/null +++ b/tests/test_embeddedfiles.py @@ -0,0 +1,24 @@ +""" +Tests for PDF EmbeddedFiles functions. +""" +import fitz + + +def test_embedded1(): + doc = fitz.open() + buffer = b"123456678790qwexcvnmhofbnmfsdg4589754uiofjkb-" + doc.embfile_add( + "file1", + buffer, + filename="testfile.txt", + ufilename="testfile-u.txt", + desc="Description of some sort", + ) + assert doc.embfile_count() == 1 + assert doc.embfile_names() == ["file1"] + assert doc.embfile_info(0)["name"] == "file1" + doc.embfile_upd(0, filename="new-filename.txt") + assert doc.embfile_info(0)["filename"] == "new-filename.txt" + assert doc.embfile_get(0) == buffer + doc.embfile_del(0) + assert doc.embfile_count() == 0 \ No newline at end of file diff --git a/tests/test_extractimage.py b/tests/test_extractimage.py new file mode 100644 index 0000000..4be817f --- /dev/null +++ b/tests/test_extractimage.py @@ -0,0 +1,50 @@ +""" +Extract images from a PDF file, confirm number of images found. +""" +import os +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "joined.pdf") +known_image_count = 21 + + +def test_extract_image(): + doc = fitz.open(filename) + + image_count = 1 + for xref in range(1, doc.xref_length() - 1): + if doc.xref_get_key(xref, "Subtype")[1] != "/Image": + continue + img = doc.extract_image(xref) + if isinstance(img, dict): + image_count += 1 + + assert image_count == known_image_count # this number is know about the file + +def test_2348(): + + pdf_path = f'{scriptdir}/test_2348.pdf' + document = fitz.open() + page = document.new_page(width=500, height=842) + rect = fitz.Rect(20, 20, 480, 820) + page.insert_image(rect, filename=f'{scriptdir}/resources/nur-ruhig.jpg') + page = document.new_page(width=500, height=842) + page.insert_image(rect, filename=f'{scriptdir}/resources/img-transparent.png') + document.ez_save(pdf_path) + document.close() + + document = fitz.open(pdf_path) + page = document[0] + imlist = page.get_images() + image = document.extract_image(imlist[0][0]) + jpeg_extension = image['ext'] + + page = document[1] + imlist = page.get_images() + image = document.extract_image(imlist[0][0]) + png_extension = image['ext'] + + print(f'jpeg_extension={jpeg_extension!r} png_extension={png_extension!r}') + assert jpeg_extension == 'jpeg' + assert png_extension == 'png' diff --git a/tests/test_font.py b/tests/test_font.py new file mode 100644 index 0000000..81d7fd3 --- /dev/null +++ b/tests/test_font.py @@ -0,0 +1,25 @@ +""" +Tests for the Font class. +""" +import fitz + + +def test_font1(): + text = "PyMuPDF" + font = fitz.Font("helv") + assert font.name == "Helvetica" + tl = font.text_length(text, fontsize=20) + cl = font.char_lengths(text, fontsize=20) + assert len(text) == len(cl) + assert abs(sum(cl) - tl) < fitz.EPSILON + for i in range(len(cl)): + assert cl[i] == font.glyph_advance(ord(text[i])) * 20 + font2 = fitz.Font(fontbuffer=font.buffer) + assert font2.valid_codepoints() == font.valid_codepoints() + + +def test_font2(): + """Old and new length computation must be the same.""" + font = fitz.Font("helv") + text = "PyMuPDF" + assert font.text_length(text) == fitz.get_text_length(text) \ No newline at end of file diff --git a/tests/test_general.py b/tests/test_general.py new file mode 100644 index 0000000..42a3383 --- /dev/null +++ b/tests/test_general.py @@ -0,0 +1,429 @@ +# encoding utf-8 +""" +* Confirm sample doc has no links and no annots. +* Confirm proper release of file handles via Document.close() +* Confirm properly raising exceptions in document creation +""" +import io +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "001003ED.pdf") + + +def test_haslinks(): + doc = fitz.open(filename) + assert doc.has_links() == False + + +def test_hasannots(): + doc = fitz.open(filename) + assert doc.has_annots() == False + + +def test_haswidgets(): + doc = fitz.open(filename) + assert doc.is_form_pdf == False + + +def test_isrepaired(): + doc = fitz.open(filename) + assert doc.is_repaired == False + fitz.TOOLS.mupdf_warnings() + + +def test_isdirty(): + doc = fitz.open(filename) + assert doc.is_dirty == False + + +def test_cansaveincrementally(): + doc = fitz.open(filename) + assert doc.can_save_incrementally() == True + + +def test_iswrapped(): + doc = fitz.open(filename) + page = doc[0] + assert page.is_wrapped + + +def test_wrapcontents(): + doc = fitz.open(filename) + page = doc[0] + page.wrap_contents() + xref = page.get_contents()[0] + cont = page.read_contents() + doc.update_stream(xref, cont) + page.set_contents(xref) + assert len(page.get_contents()) == 1 + page.clean_contents() + + +def test_config(): + assert fitz.TOOLS.fitz_config["py-memory"] in (True, False) + + +def test_glyphnames(): + name = "infinity" + infinity = fitz.glyph_name_to_unicode(name) + assert fitz.unicode_to_glyph_name(infinity) == name + + +def test_rgbcodes(): + sRGB = 0xFFFFFF + assert fitz.sRGB_to_pdf(sRGB) == (1, 1, 1) + assert fitz.sRGB_to_rgb(sRGB) == (255, 255, 255) + + +def test_pdfstring(): + fitz.get_pdf_now() + fitz.get_pdf_str("Beijing, chinesisch 北京") + fitz.get_text_length("Beijing, chinesisch 北京", fontname="china-s") + fitz.get_pdf_str("Latin characters êßöäü") + + +def test_open_exceptions(): + try: + doc = fitz.open(filename, filetype="xps") + except RuntimeError as e: + assert repr(e).startswith("FileDataError") + + try: + doc = fitz.open(filename, filetype="xxx") + except Exception as e: + assert repr(e).startswith("ValueError") + + try: + doc = fitz.open("x.y") + except Exception as e: + assert repr(e).startswith("FileNotFoundError") + + try: + doc = fitz.open("pdf", b"") + except RuntimeError as e: + assert repr(e).startswith("EmptyFileError") + + +def test_bug1945(): + pdf = fitz.open(f'{scriptdir}/resources/bug1945.pdf') + buffer_ = io.BytesIO() + pdf.save(buffer_, clean=True) + + +def test_bug1971(): + for _ in range(2): + doc = fitz.Document(f'{scriptdir}/resources/bug1971.pdf') + page = next(doc.pages()) + page.get_drawings() + doc.close() + +def test_default_font(): + f = fitz.Font() + assert str(f) == "Font('Noto Serif Regular')" + assert repr(f) == "Font('Noto Serif Regular')" + +def test_add_ink_annot(): + import math + document = fitz.Document() + page = document.new_page() + line1 = [] + line2 = [] + for a in range( 0, 360*2, 15): + x = a + c = 300 + 200 * math.cos( a * math.pi/180) + s = 300 + 100 * math.sin( a * math.pi/180) + line1.append( (x, c)) + line2.append( (x, s)) + page.add_ink_annot( [line1, line2]) + page.insert_text((100, 72), 'Hello world') + page.add_text_annot((200,200), "Some Text") + page.get_bboxlog() + path = f'{scriptdir}/resources/test_add_ink_annot.pdf' + document.save( path) + print( f'Have saved to: path={path!r}') + +def test_techwriter_append(): + print(fitz.__doc__) + doc = fitz.open() + page = doc.new_page() + tw = fitz.TextWriter(page.rect) + text = "Red rectangle = TextWriter.text_rect, blue circle = .last_point" + r = tw.append((100, 100), text) + print(f'r={r!r}') + tw.write_text(page) + page.draw_rect(tw.text_rect, color=fitz.pdfcolor["red"]) + page.draw_circle(tw.last_point, 2, color=fitz.pdfcolor["blue"]) + path = f"{scriptdir}/resources/test_techwriter_append.pdf" + doc.ez_save(path) + print( f'Have saved to: {path}') + +def test_opacity(): + doc = fitz.open() + page = doc.new_page() + + annot1 = page.add_circle_annot((50, 50, 100, 100)) + annot1.set_colors(fill=(1, 0, 0), stroke=(1, 0, 0)) + annot1.set_opacity(2 / 3) + annot1.update(blend_mode="Multiply") + + annot2 = page.add_circle_annot((75, 75, 125, 125)) + annot2.set_colors(fill=(0, 0, 1), stroke=(0, 0, 1)) + annot2.set_opacity(1 / 3) + annot2.update(blend_mode="Multiply") + outfile = f'{scriptdir}/resources/opacity.pdf' + doc.save(outfile, expand=True, pretty=True) + print("saved", outfile) + +def test_get_text_dict(): + import json + doc=fitz.open(f'{scriptdir}/resources/v110-changes.pdf') + page=doc[0] + blocks=page.get_text("dict")["blocks"] + # Check no opaque types in `blocks`. + json.dumps( blocks, indent=4) + +def test_font(): + font = fitz.Font() + print(repr(font)) + bbox = font.glyph_bbox( 65) + print( f'bbox={bbox!r}') + +def test_insert_font(): + doc=fitz.open(f'{scriptdir}/resources/v110-changes.pdf') + page = doc[0] + i = page.insert_font() + print( f'page.insert_font() => {i}') + +def test_2173(): + from fitz import IRect, Pixmap, CS_RGB, Colorspace + for i in range( 100): + #print( f'i={i!r}') + image = Pixmap(Colorspace(CS_RGB), IRect(0, 0, 13, 37)) + print( 'test_2173() finished') + +def test_texttrace(): + import time + document = fitz.Document( f'{scriptdir}/resources/joined.pdf') + t = time.time() + for page in document: + tt = page.get_texttrace() + t = time.time() - t + print( f'test_texttrace(): t={t!r}') + + # Repeat, this time writing data to file. + import json + path = f'{scriptdir}/resources/test_texttrace.txt' + print( f'Writing to: {path}') + with open( path, 'w') as f: + for i, page in enumerate(document): + tt = page.get_texttrace() + print( f'page {i} json:\n{json.dumps(tt, indent=" ")}', file=f) + +def test_2108(): + doc = fitz.open(f'{scriptdir}/resources/test_2108.pdf') + page = doc[0] + areas = page.search_for("{sig}") + rect = areas[0] + page.add_redact_annot(rect) + page.apply_redactions() + text = page.get_text() + + text_expected = b'Frau\nClaire Dunphy\nTeststra\xc3\x9fe 5\n12345 Stadt\nVertragsnummer: 12345\nSehr geehrte Frau Dunphy,\nText\nMit freundlichen Gr\xc3\xbc\xc3\x9fen\nTestfirma\nVertrag:\n 12345\nAnsprechpartner:\nJay Pritchet\nTelefon:\n123456\nE-Mail:\ntest@test.de\nDatum:\n07.12.2022\n'.decode('utf8') + + if 1: + # Verbose info. + print(f'test_2108(): text is:\n{text}') + print(f'') + print(f'test_2108(): repr(text) is:\n{text!r}') + print(f'') + print(f'test_2108(): repr(text.encode("utf8")) is:\n{text.encode("utf8")!r}') + print(f'') + print(f'test_2108(): text_expected is:\n{text_expected}') + print(f'') + print(f'test_2108(): repr(text_expected) is:\n{text_expected!r}') + print(f'') + print(f'test_2108(): repr(text_expected.encode("utf8")) is:\n{text_expected.encode("utf8")!r}') + + ok1 = (text == text_expected) + ok2 = (text.encode("utf8") == text_expected.encode("utf8")) + ok3 = (repr(text.encode("utf8")) == repr(text_expected.encode("utf8"))) + + print(f'') + print(f'ok1={ok1}') + print(f'ok2={ok2}') + print(f'ok3={ok3}') + + print(f'') + + print(f'fitz.mupdf_version_tuple={fitz.mupdf_version_tuple}') + if fitz.mupdf_version_tuple >= (1, 21, 2): + print('Asserting text==text_expected') + assert text == text_expected + else: + print('Asserting text!=text_expected') + assert text != text_expected + + +def test_2238(): + filepath = f'{scriptdir}/resources/test2238.pdf' + doc = fitz.open(filepath) + + first_page = doc.load_page(0).get_text('text', fitz.INFINITE_RECT()) + last_page = doc.load_page(-1).get_text('text', fitz.INFINITE_RECT()) + + print(f'first_page={first_page!r}') + print(f'last_page={last_page!r}') + assert first_page == 'Hello World\n' + assert last_page == 'Hello World\n' + + first_page = doc.load_page(0).get_text('text') + last_page = doc.load_page(-1).get_text('text') + + print(f'first_page={first_page!r}') + print(f'last_page={last_page!r}') + assert first_page == 'Hello World\n' + assert last_page == 'Hello World\n' + + +def test_2093(): + doc = fitz.open(f'{scriptdir}/resources/test2093.pdf') + + def average_color(page): + pixmap = page.get_pixmap() + p_average = [0] * pixmap.n + for y in range(pixmap.height): + for x in range(pixmap.width): + p = pixmap.pixel(x, y) + for i in range(pixmap.n): + p_average[i] += p[i] + for i in range(pixmap.n): + p_average[i] /= (pixmap.height * pixmap.width) + return p_average + + page = doc.load_page(0) + pixel_average_before = average_color(page) + + rx=135.123 + ry=123.56878 + rw=69.8409 + rh=9.46397 + + x0 = rx + y0 = ry + x1 = rx + rw + y1 = ry + rh + + rect = fitz.Rect(x0, y0, x1, y1) + + font = fitz.Font("Helvetica") + fill_color=(0,0,0) + page.add_redact_annot( + quad=rect, + #text="null", + fontname=font.name, + fontsize=12, + align=fitz.TEXT_ALIGN_CENTER, + fill=fill_color, + text_color=(1,1,1), + ) + + page.apply_redactions() + pixel_average_after = average_color(page) + + print(f'pixel_average_before={pixel_average_before!r}') + print(f'pixel_average_after={pixel_average_after!r}') + + # Before this bug was fixed: + # pixel_average_before=[130.864323120088, 115.23577810900859, 92.9268559996174] + # pixel_average_after=[138.68844553555772, 123.05687162237561, 100.74275056194105] + # After fix: + # pixel_average_before=[130.864323120088, 115.23577810900859, 92.9268559996174] + # pixel_average_after=[130.8889209934799, 115.25722751837269, 92.94327384463327] + # + if fitz.mupdf_version_tuple[:2] >= (1, 22): + for i in range(len(pixel_average_before)): + diff = pixel_average_before[i] - pixel_average_after[i] + assert abs(diff) < 0.1 + + out = f'{scriptdir}/resources/test2093-out.pdf' + doc.save(out) + print(f'Have written to: {out}') + + +def test_2182(): + print(f'test_2182() started') + doc = fitz.open(f'{scriptdir}/resources/test2182.pdf') + page = doc[0] + for annot in page.annots(): + print(annot) + print(f'test_2182() finished') + + +def test_2246(): + """ + Test / confirm identical text positions generated by + * page.insert_text() + versus + * TextWriter.write_text() + + ... under varying situations as follows: + + 1. MediaBox does not start at (0, 0) + 2. CropBox origin is different from that of MediaBox + 3. Check for all 4 possible page rotations + + The test writes the same text at the same positions using `page.insert_text()`, + respectively `TextWriter.write_text()`. + Then extracts the text spans and confirms that they all occupy the same bbox. + This ensures coincidence of text positions of page.of insert_text() + (which is assumed correct) and TextWriter.write_text(). + """ + def bbox_count(rot): + """Make a page and insert identical text via different methods. + + Desired page rotation is a parameter. MediaBox and CropBox are chosen + to be "awkward": MediaBox does not start at (0,0) and CropBox is a + true subset of MediaBox. + """ + # bboxes of spans on page: same text positions are represented by ONE bbox + bboxes = set() + doc = fitz.open() + # prepare a page with desired MediaBox / CropBox peculiarities + mediabox = fitz.paper_rect("letter") + page = doc.new_page(width=mediabox.width, height=mediabox.height) + xref = page.xref + newmbox = list(map(float, doc.xref_get_key(xref, "MediaBox")[1][1:-1].split())) + newmbox = fitz.Rect(newmbox) + mbox = newmbox + (10, 20, 10, 20) + cbox = mbox + (10, 10, -10, -10) + doc.xref_set_key(xref, "MediaBox", "[%g %g %g %g]" % tuple(mbox)) + doc.xref_set_key(xref, "CrobBox", "[%g %g %g %g]" % tuple(cbox)) + # set page to desired rotation + page.set_rotation(rot) + page.insert_text((50, 50), "Text inserted at (50,50)") + tw = fitz.TextWriter(page.rect) + tw.append((50, 50), "Text inserted at (50,50)") + tw.write_text(page) + blocks = page.get_text("dict")["blocks"] + for b in blocks: + for l in b["lines"]: + for s in l["spans"]: + # store bbox rounded to 3 decimal places + bboxes.add(fitz.Rect(fitz.JM_TUPLE3(s["bbox"]))) + return len(bboxes) # should be 1! + + # the following tests must all pass + assert bbox_count(0) == 1 + assert bbox_count(90) == 1 + assert bbox_count(180) == 1 + assert bbox_count(270) == 1 + + +def test_2430(): + """Confirm that multiple font property checks will not destroy Py_None.""" + font = fitz.Font("helv") + for i in range(1000): + _ = font.flags diff --git a/tests/test_geometry.py b/tests/test_geometry.py new file mode 100644 index 0000000..642c3f3 --- /dev/null +++ b/tests/test_geometry.py @@ -0,0 +1,330 @@ +""" +* Check various construction methods of rects, points, matrices +* Check matrix inversions in variations +* Check algebra constructs +""" +import fitz + + +def test_rect(): + assert tuple(fitz.Rect()) == (0, 0, 0, 0) + p1 = fitz.Point(10, 20) + p2 = fitz.Point(100, 200) + p3 = fitz.Point(150, 250) + r = fitz.Rect(10, 20, 100, 200) + r_tuple = tuple(r) + assert tuple(fitz.Rect(p1, p2)) == r_tuple + assert tuple(fitz.Rect(p1, 100, 200)) == r_tuple + assert tuple(fitz.Rect(10, 20, p2)) == r_tuple + assert tuple(r.include_point(p3)) == (10, 20, 150, 250) + r = fitz.Rect(10, 20, 100, 200) + assert tuple(r.include_rect((100, 200, 110, 220))) == (10, 20, 110, 220) + r = fitz.Rect(10, 20, 100, 200) + # include empty rect makes no change + assert tuple(r.include_rect((0, 0, 0, 0))) == r_tuple + # include invalid rect makes no change + assert tuple(r.include_rect((1, 1, -1, -1))) == r_tuple + r = fitz.Rect() + for i in range(4): + r[i] = i + 1 + assert r == fitz.Rect(1, 2, 3, 4) + assert fitz.Rect() / 5 == fitz.Rect() + assert fitz.Rect(1, 1, 2, 2) / fitz.Identity == fitz.Rect(1, 1, 2, 2) + failed = False + try: + r = fitz.Rect(1) + except: + failed = True + assert failed + failed = False + try: + r = fitz.Rect(1, 2, 3, 4, 5) + except: + failed = True + assert failed + failed = False + try: + r = fitz.Rect((1, 2, 3, 4, 5)) + except: + failed = True + assert failed + failed = False + try: + r = fitz.Rect(1, 2, 3, "x") + except: + failed = True + assert failed + failed = False + try: + r = fitz.Rect() + r[5] = 1 + except: + failed = True + assert failed + + +def test_irect(): + p1 = fitz.Point(10, 20) + p2 = fitz.Point(100, 200) + p3 = fitz.Point(150, 250) + r = fitz.IRect(10, 20, 100, 200) + r_tuple = tuple(r) + assert tuple(fitz.IRect(p1, p2)) == r_tuple + assert tuple(fitz.IRect(p1, 100, 200)) == r_tuple + assert tuple(fitz.IRect(10, 20, p2)) == r_tuple + assert tuple(r.include_point(p3)) == (10, 20, 150, 250) + r = fitz.IRect(10, 20, 100, 200) + assert tuple(r.include_rect((100, 200, 110, 220))) == (10, 20, 110, 220) + r = fitz.IRect(10, 20, 100, 200) + # include empty rect makes no change + assert tuple(r.include_rect((0, 0, 0, 0))) == r_tuple + r = fitz.IRect() + for i in range(4): + r[i] = i + 1 + assert r == fitz.IRect(1, 2, 3, 4) + + failed = False + try: + r = fitz.IRect(1) + except: + failed = True + assert failed + failed = False + try: + r = fitz.IRect(1, 2, 3, 4, 5) + except: + failed = True + assert failed + failed = False + try: + r = fitz.IRect((1, 2, 3, 4, 5)) + except: + failed = True + assert failed + failed = False + try: + r = fitz.IRect(1, 2, 3, "x") + except: + failed = True + assert failed + failed = False + try: + r = fitz.IRect() + r[5] = 1 + except: + failed = True + assert failed + + +def test_inversion(): + alpha = 255 + m1 = fitz.Matrix(alpha) + m2 = fitz.Matrix(-alpha) + m3 = m1 * m2 # should equal identity matrix + assert abs(m3 - fitz.Identity) < fitz.EPSILON + m = fitz.Matrix(1, 0, 1, 0, 1, 0) # not invertible! + # inverted matrix must be zero + assert ~m == fitz.Matrix() + + +def test_matrix(): + assert tuple(fitz.Matrix()) == (0, 0, 0, 0, 0, 0) + m45p = fitz.Matrix(45) + m45m = fitz.Matrix(-45) + m90 = fitz.Matrix(90) + assert abs(m90 - m45p * m45p) < fitz.EPSILON + assert abs(fitz.Identity - m45p * m45m) < fitz.EPSILON + assert abs(m45p - ~m45m) < fitz.EPSILON + assert fitz.Matrix(2, 3, 1) == fitz.Matrix(1, 3, 2, 1, 0, 0) + m = fitz.Matrix(2, 3, 1) + m.invert() + assert abs(m * fitz.Matrix(2, 3, 1) - fitz.Identity) < fitz.EPSILON + assert fitz.Matrix(1, 1).pretranslate(2, 3) == fitz.Matrix(1, 0, 0, 1, 2, 3) + assert fitz.Matrix(1, 1).prescale(2, 3) == fitz.Matrix(2, 0, 0, 3, 0, 0) + assert fitz.Matrix(1, 1).preshear(2, 3) == fitz.Matrix(1, 3, 2, 1, 0, 0) + assert abs(fitz.Matrix(1, 1).prerotate(30) - fitz.Matrix(30)) < fitz.EPSILON + small = 1e-6 + assert fitz.Matrix(1, 1).prerotate(90 + small) == fitz.Matrix(90) + assert fitz.Matrix(1, 1).prerotate(180 + small) == fitz.Matrix(180) + assert fitz.Matrix(1, 1).prerotate(270 + small) == fitz.Matrix(270) + assert fitz.Matrix(1, 1).prerotate(small) == fitz.Matrix(0) + assert fitz.Matrix(1, 1).concat( + fitz.Matrix(1, 2), fitz.Matrix(3, 4) + ) == fitz.Matrix(3, 0, 0, 8, 0, 0) + assert fitz.Matrix(1, 2, 3, 4, 5, 6) / 1 == fitz.Matrix(1, 2, 3, 4, 5, 6) + assert m[0] == m.a + assert m[1] == m.b + assert m[2] == m.c + assert m[3] == m.d + assert m[4] == m.e + assert m[5] == m.f + m = fitz.Matrix() + for i in range(6): + m[i] = i + 1 + assert m == fitz.Matrix(1, 2, 3, 4, 5, 6) + failed = False + try: + m = fitz.Matrix(1, 2, 3) + except: + failed = True + assert failed + failed = False + try: + m = fitz.Matrix(1, 2, 3, 4, 5, 6, 7) + except: + failed = True + assert failed + + failed = False + try: + m = fitz.Matrix((1, 2, 3, 4, 5, 6, 7)) + except: + failed = True + assert failed + + failed = False + try: + m = fitz.Matrix(1, 2, 3, 4, 5, "x") + except: + failed = True + assert failed + + failed = False + try: + m = fitz.Matrix(1, 0, 1, 0, 1, 0) + n = fitz.Matrix(1, 1) / m + except: + failed = True + assert failed + + +def test_point(): + assert tuple(fitz.Point()) == (0, 0) + assert fitz.Point(1, -1).unit == fitz.Point(5, -5).unit + assert fitz.Point(-1, -1).abs_unit == fitz.Point(1, 1).unit + assert fitz.Point(1, 1).distance_to(fitz.Point(1, 1)) == 0 + assert fitz.Point(1, 1).distance_to(fitz.Rect(1, 1, 2, 2)) == 0 + assert fitz.Point().distance_to((1, 1, 2, 2)) > 0 + failed = False + try: + p = fitz.Point(1, 2, 3) + except: + failed = True + assert failed + + failed = False + try: + p = fitz.Point((1, 2, 3)) + except: + failed = True + assert failed + + failed = False + try: + p = fitz.Point(1, "x") + except: + failed = True + assert failed + + failed = False + try: + p = fitz.Point() + p[3] = 1 + except: + failed = True + assert failed + + +def test_algebra(): + p = fitz.Point(1, 2) + m = fitz.Matrix(1, 2, 3, 4, 5, 6) + r = fitz.Rect(1, 1, 2, 2) + assert p + p == p * 2 + assert p - p == fitz.Point() + assert m + m == m * 2 + assert m - m == fitz.Matrix() + assert r + r == r * 2 + assert r - r == fitz.Rect() + assert p + 5 == fitz.Point(6, 7) + assert m + 5 == fitz.Matrix(6, 7, 8, 9, 10, 11) + assert r.tl in r + assert r.tr not in r + assert r.br not in r + assert r.bl not in r + assert p * m == fitz.Point(12, 16) + assert r * m == fitz.Rect(9, 12, 13, 18) + assert (fitz.Rect(1, 1, 2, 2) & fitz.Rect(2, 2, 3, 3)).is_empty + assert not fitz.Rect(1, 1, 2, 2).intersects((2, 2, 4, 4)) + failed = False + try: + x = m + p + except: + failed = True + assert failed + failed = False + try: + x = m + r + except: + failed = True + assert failed + failed = False + try: + x = p + r + except: + failed = True + assert failed + failed = False + try: + x = r + m + except: + failed = True + assert failed + assert m not in r + + +def test_quad(): + r = fitz.Rect(10, 10, 20, 20) + q = r.quad + assert q.is_rectangular + assert not q.is_empty + assert q.is_convex + q *= fitz.Matrix(1, 1).preshear(2, 3) + assert not q.is_rectangular + assert not q.is_empty + assert q.is_convex + assert r.tl not in q + assert r not in q + assert r.quad not in q + failed = False + try: + q[5] = fitz.Point() + except: + failed = True + assert failed + + failed = False + try: + q /= (1, 0, 1, 0, 1, 0) + except: + failed = True + assert failed + + +def test_pageboxes(): + """Tests concerning ArtBox, TrimBox, BleedBox.""" + doc = fitz.open() + page = doc.new_page() + assert page.cropbox == page.artbox == page.bleedbox == page.trimbox + rect_methods = ( + page.set_cropbox, + page.set_artbox, + page.set_bleedbox, + page.set_trimbox, + ) + keys = ("CropBox", "ArtBox", "BleedBox", "TrimBox") + rect = fitz.Rect(100, 200, 400, 700) + for f in rect_methods: + f(rect) + for key in keys: + assert doc.xref_get_key(page.xref, key) == ("array", "[100 142 400 642]") + assert page.cropbox == page.artbox == page.bleedbox == page.trimbox diff --git a/tests/test_imagebbox.py b/tests/test_imagebbox.py new file mode 100644 index 0000000..07653a8 --- /dev/null +++ b/tests/test_imagebbox.py @@ -0,0 +1,48 @@ +""" +Ensure equality of bboxes computed via +* page.get_image_bbox() +* page.get_image_info() +* page.get_bboxlog() + +""" +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "image-file1.pdf") +image = os.path.join(scriptdir, "resources", "img-transparent.png") +doc = fitz.open(filename) + + +def test_image_bbox(): + page = doc[0] + imglist = page.get_images(True) + bbox_list = [] + for item in imglist: + bbox_list.append(page.get_image_bbox(item, transform=False)) + infos = page.get_image_info(xrefs=True) + for im in infos: + bbox1 = im["bbox"] + match = False + for bbox2 in bbox_list: + abs_bbox = (bbox2 - bbox1).norm() + if abs_bbox < 1e-4: + match = True + break + assert match + + +def test_bboxlog(): + doc = fitz.open() + page = doc.new_page() + xref = page.insert_image(page.rect, filename=image) + img_info = page.get_image_info(xrefs=True) + assert len(img_info) == 1 + info = img_info[0] + assert info["xref"] == xref + bbox_log = page.get_bboxlog() + assert len(bbox_log) == 1 + box_type, bbox = bbox_log[0] + assert box_type == "fill-image" + assert bbox == info["bbox"] diff --git a/tests/test_insertimage.py b/tests/test_insertimage.py new file mode 100644 index 0000000..45f5225 --- /dev/null +++ b/tests/test_insertimage.py @@ -0,0 +1,27 @@ +""" +* Insert same image with different rotations in two places of a page. +* Extract bboxes and transformation matrices +* Assert image locations are inside given rectangles +""" +import json +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +imgfile = os.path.join(scriptdir, "resources", "nur-ruhig.jpg") + + +def test_insert(): + doc = fitz.open() + page = doc.new_page() + r1 = fitz.Rect(50, 50, 100, 100) + r2 = fitz.Rect(50, 150, 200, 400) + page.insert_image(r1, filename=imgfile) + page.insert_image(r2, filename=imgfile, rotate=270) + info_list = page.get_image_info() + assert len(info_list) == 2 + bbox1 = fitz.Rect(info_list[0]["bbox"]) + bbox2 = fitz.Rect(info_list[1]["bbox"]) + assert bbox1 in r1 + assert bbox2 in r2 diff --git a/tests/test_insertpdf.py b/tests/test_insertpdf.py new file mode 100644 index 0000000..75131e1 --- /dev/null +++ b/tests/test_insertpdf.py @@ -0,0 +1,115 @@ +""" +* Join multiple PDFs into a new one. +* Compare with stored earlier result: + - must have identical object definitions + - must have different trailers +* Try inserting files in a loop. +""" +import os +import re +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +resources = os.path.join(scriptdir, "resources") + +def approx_parse( text): + ''' + Splits into sequence of (text, number) pairs. Where sequence of + [0-9.] is not convertible to a number (e.g. '4.5.6'), will be + None. + ''' + ret = [] + for m in re.finditer('([^0-9]+)([0-9.]*)', text): + text = m.group(1) + try: + number = float( m.group(2)) + except Exception: + text += m.group(2) + number = None + ret.append( (text, number)) + return ret + +def approx_compare( a, b, max_delta): + ''' + Compares and , allowing numbers to differ by up to . + ''' + aa = approx_parse( a) + bb = approx_parse( b) + if len(aa) != len(bb): + return 1 + ret = 1 + for (at, an), (bt, bn) in zip( aa, bb): + if at != bt: + break + if an is not None and bn is not None: + if abs( an - bn) >= max_delta: + print( f'diff={an-bn}: an={an} bn={bn}') + break + elif (an is None) != (bn is None): + break + else: + ret = 0 + if ret: + print( f'Differ:\n a={a!r}\n b={b!r}') + return ret + + +def test_insert(): + all_text_original = [] # text on input pages + all_text_combined = [] # text on resulting output pages + # prepare input PDFs + doc1 = fitz.open() + for i in range(5): # just arbitrary number of pages + text = f"doc 1, page {i}" # the 'globally' unique text + page = doc1.new_page() + page.insert_text((100, 72), text) + all_text_original.append(text) + + doc2 = fitz.open() + for i in range(4): + text = f"doc 2, page {i}" + page = doc2.new_page() + page.insert_text((100, 72), text) + all_text_original.append(text) + + doc3 = fitz.open() + for i in range(3): + text = f"doc 3, page {i}" + page = doc3.new_page() + page.insert_text((100, 72), text) + all_text_original.append(text) + + doc4 = fitz.open() + for i in range(6): + text = f"doc 4, page {i}" + page = doc4.new_page() + page.insert_text((100, 72), text) + all_text_original.append(text) + + new_doc = fitz.open() # make combined PDF of input files + new_doc.insert_pdf(doc1) + new_doc.insert_pdf(doc2) + new_doc.insert_pdf(doc3) + new_doc.insert_pdf(doc4) + # read text from all pages and store in list + for page in new_doc: + all_text_combined.append(page.get_text().replace("\n", "")) + # the lists must be equal + assert all_text_combined == all_text_original + + +def test_issue1417_insertpdf_in_loop(): + """Using a context manager instead of explicitly closing files""" + f = os.path.join(resources, "1.pdf") + big_doc = fitz.open() + fd1 = os.open( f, os.O_RDONLY) + os.close( fd1) + for n in range(0, 1025): + with fitz.open(f) as pdf: + big_doc.insert_pdf(pdf) + # Create a raw file descriptor. If the above fitz.open() context leaks + # a file descriptor, fd will be seen to increment. + fd2 = os.open( f, os.O_RDONLY) + assert fd2 == fd1 + os.close( fd2) + big_doc.close() diff --git a/tests/test_linequad.py b/tests/test_linequad.py new file mode 100644 index 0000000..9f15116 --- /dev/null +++ b/tests/test_linequad.py @@ -0,0 +1,30 @@ +""" +Check approx. equality of search quads versus quads recovered from +text extractions. +""" +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "quad-calc-0.pdf") + + +def test_quadcalc(): + text = " angle 327" # search for this text + doc = fitz.open(filename) + page = doc[0] + # This special page has one block with one line, and + # its last span contains the searched text. + block = page.get_text("dict", flags=0)["blocks"][0] + line = block["lines"][0] + # compute quad of last span in line + lineq = fitz.recover_line_quad(line, spans=line["spans"][-1:]) + + # let text search find the text returning quad coordinates + rl = page.search_for(text, quads=True) + searchq = rl[0] + assert abs(searchq.ul - lineq.ul) <= 1e-4 + assert abs(searchq.ur - lineq.ur) <= 1e-4 + assert abs(searchq.ll - lineq.ll) <= 1e-4 + assert abs(searchq.lr - lineq.lr) <= 1e-4 diff --git a/tests/test_metadata.py b/tests/test_metadata.py new file mode 100644 index 0000000..9538ef9 --- /dev/null +++ b/tests/test_metadata.py @@ -0,0 +1,26 @@ +""" +1. Read metadata and compare with stored expected result. +2. Erase metadata and assert object has indeed been deleted. +""" +import json +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "001003ED.pdf") +metafile = os.path.join(scriptdir, "resources", "metadata.txt") +doc = fitz.open(filename) + + +def test_metadata(): + assert json.dumps(doc.metadata) == open(metafile).read() + + +def test_erase_meta(): + doc.set_metadata({}) + # Check PDF trailer and assert that there is no more /Info object + # or is set to "null". + statement1 = doc.xref_get_key(-1, "Info")[1] == "null" + statement2 = "Info" not in doc.xref_get_keys(-1) + assert statement2 or statement1 diff --git a/tests/test_nonpdf.py b/tests/test_nonpdf.py new file mode 100644 index 0000000..b56761e --- /dev/null +++ b/tests/test_nonpdf.py @@ -0,0 +1,35 @@ +""" +* Check EPUB document is no PDF +* Check page access using (chapter, page) notation +* Re-layout EPUB ensuring a previous location is memorized +""" +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "Bezier.epub") +doc = fitz.open(filename) + + +def test_isnopdf(): + assert not doc.is_pdf + + +def test_pageids(): + assert doc.chapter_count == 7 + assert doc.last_location == (6, 1) + assert doc.prev_location((6, 0)) == (5, 11) + assert doc.next_location((5, 11)) == (6, 0) + # Check page numbers have no gaps: + i = 0 + for chapter in range(doc.chapter_count): + for cpno in range(doc.chapter_page_count(chapter)): + assert doc.page_number_from_location((chapter, cpno)) == i + i += 1 + +def test_layout(): + """Memorize a page location, re-layout with ISO-A4, assert pre-determined location.""" + loc = doc.make_bookmark((5, 11)) + doc.layout(fitz.Rect(fitz.paper_rect("a4"))) + assert doc.find_bookmark(loc) == (5, 6) diff --git a/tests/test_object_manipulation.py b/tests/test_object_manipulation.py new file mode 100644 index 0000000..f3edf1b --- /dev/null +++ b/tests/test_object_manipulation.py @@ -0,0 +1,38 @@ +""" +Check some low-level PDF object manipulations: +1. Set page rotation and compare with string in object definition. +2. Set page rotation via string manipulation and compare with result of + proper page property. +3. Read the PDF trailer and verify it has the keys "/Root", "/ID", etc. +""" +import fitz +import os + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +resources = os.path.join(scriptdir, "resources") +filename = os.path.join(resources, "001003ED.pdf") + + +def test_rotation1(): + doc = fitz.open() + page = doc.new_page() + page.set_rotation(270) + assert doc.xref_get_key(page.xref, "Rotate") == ("int", "270") + + +def test_rotation2(): + doc = fitz.open() + page = doc.new_page() + doc.xref_set_key(page.xref, "Rotate", "270") + assert page.rotation == 270 + + +def test_trailer(): + """Access PDF trailer information.""" + doc = fitz.open(filename) + xreflen = doc.xref_length() + _, xreflen_str = doc.xref_get_key(-1, "Size") + assert xreflen == int(xreflen_str) + trailer_keys = doc.xref_get_keys(-1) + assert "ID" in trailer_keys + assert "Root" in trailer_keys diff --git a/tests/test_optional_content.py b/tests/test_optional_content.py new file mode 100644 index 0000000..4548546 --- /dev/null +++ b/tests/test_optional_content.py @@ -0,0 +1,63 @@ +""" +Test of Optional Content code. +""" +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "joined.pdf") + + +def test_oc1(): + """Arbitrary calls to OC code to get coverage.""" + doc = fitz.open() + ocg1 = doc.add_ocg("ocg1") + ocg2 = doc.add_ocg("ocg2") + ocg3 = doc.add_ocg("ocg3") + ocmd1 = doc.set_ocmd(xref=0, ocgs=(ocg1, ocg2)) + doc.set_layer(-1) + doc.add_layer("layer1") + test = doc.get_layer() + test = doc.get_layers() + test = doc.get_ocgs() + test = doc.layer_ui_configs() + doc.switch_layer(0) + + +def test_oc2(): + # source file with at least 4 pages + src = fitz.open(filename) + + # new PDF with one page + doc = fitz.open() + page = doc.new_page() + + # define the 4 rectangle quadrants to receive the source pages + r0 = page.rect / 2 + r1 = r0 + (r0.width, 0, r0.width, 0) + r2 = r0 + (0, r0.height, 0, r0.height) + r3 = r2 + (r2.width, 0, r2.width, 0) + + # make 4 OCGs - one for each source page image. + # only first is ON initially + ocg0 = doc.add_ocg("ocg0", on=True) + ocg1 = doc.add_ocg("ocg1", on=False) + ocg2 = doc.add_ocg("ocg2", on=False) + ocg3 = doc.add_ocg("ocg3", on=False) + + ocmd0 = doc.set_ocmd(ve=["and", ocg0, ["not", ["or", ocg1, ocg2, ocg3]]]) + ocmd1 = doc.set_ocmd(ve=["and", ocg1, ["not", ["or", ocg0, ocg2, ocg3]]]) + ocmd2 = doc.set_ocmd(ve=["and", ocg2, ["not", ["or", ocg1, ocg0, ocg3]]]) + ocmd3 = doc.set_ocmd(ve=["and", ocg3, ["not", ["or", ocg1, ocg2, ocg0]]]) + ocmds = (ocmd0, ocmd1, ocmd2, ocmd3) + # insert the 4 source page images, each connected to one OCG + page.show_pdf_page(r0, src, 0, oc=ocmd0) + page.show_pdf_page(r1, src, 1, oc=ocmd1) + page.show_pdf_page(r2, src, 2, oc=ocmd2) + page.show_pdf_page(r3, src, 3, oc=ocmd3) + xobj_ocmds = [doc.get_oc(item[0]) for item in page.get_xobjects() if item[1] != 0] + assert set(ocmds) <= set(xobj_ocmds) + assert set((ocg0, ocg1, ocg2, ocg3)) == set(tuple(doc.get_ocgs().keys())) + doc.get_ocmd(ocmd0) + page.get_oc_items() diff --git a/tests/test_pagedelete.py b/tests/test_pagedelete.py new file mode 100644 index 0000000..42be88f --- /dev/null +++ b/tests/test_pagedelete.py @@ -0,0 +1,69 @@ +""" +---------------------------------------------------- +This tests correct functioning of multi-page delete +---------------------------------------------------- +Create a PDF in memory with 100 pages with a unique text each. +Also create a TOC with a bookmark per page. +On every page after the first to-be-deleted page, also insert a link, which +points to this page. +The bookmark text equals the text on the page for easy verification. + +Then delete some pages and verify: +- the new TOC has empty items exactly for every deleted page +- the remaining TOC items still point to the correct page +- the document has no more links at all +""" +import fitz + +page_count = 100 # initial document length +r = range(5, 35, 5) # contains page numbers we will delete +# insert this link on pages after first deleted one +link = { + "from": fitz.Rect(100, 100, 120, 120), + "kind": fitz.LINK_GOTO, + "page": r[0], + "to": fitz.Point(100, 100), +} + + +def test_deletion(): + # First prepare the document. + doc = fitz.open() + toc = [] + for i in range(page_count): + page = doc.new_page() # make a page + page.insert_text((100, 100), "%i" % i) # insert unique text + if i > r[0]: # insert a link + page.insert_link(link) + toc.append([1, "%i" % i, i + 1]) # TOC bookmark to this page + + doc.set_toc(toc) # insert the TOC + assert doc.has_links() # check we did insert links + + # Test page deletion. + # Delete pages in range and verify result + del doc[r] + assert not doc.has_links() # verify all links have gone + assert doc.page_count == page_count - len(r) # correct number deleted? + toc_new = doc.get_toc() # this is the modified TOC + # verify number of emptied items (have page number -1) + assert len([item for item in toc_new if item[-1] == -1]) == len(r) + # Deleted page numbers must correspond to TOC items with page number -1. + for i in r: + assert toc_new[i][-1] == -1 + # Remaining pages must be correctly pointed to by the non-empty TOC items + for item in toc_new: + pno = item[-1] + if pno == -1: # one of the emptied items + continue + pno -= 1 # PDF page number + text = doc[pno].get_text().replace("\n", "") + # toc text must equal text on page + assert text == item[1] + + doc.delete_page(0) # just for the coverage stats + del doc[5:10] + doc.select(range(doc.page_count)) + doc.copy_page(0) + doc.move_page(0) + doc.fullcopy_page(0) diff --git a/tests/test_pagelabels.py b/tests/test_pagelabels.py new file mode 100644 index 0000000..539df46 --- /dev/null +++ b/tests/test_pagelabels.py @@ -0,0 +1,40 @@ +""" +Define some page labels in a PDF. +Check success in various aspects. +""" +import fitz + + +def make_doc(): + """Makes a PDF with 10 pages.""" + doc = fitz.open() + for i in range(10): + page = doc.new_page() + return doc + + +def make_labels(): + """Return page label range rules. + - Rule 1: labels like "A-n", page 0 is first and has "A-1". + - Rule 2: labels as capital Roman numbers, page 4 is first and has "I". + """ + return [ + {"startpage": 0, "prefix": "A-", "style": "D", "firstpagenum": 1}, + {"startpage": 4, "prefix": "", "style": "R", "firstpagenum": 1}, + ] + + +def test_setlabels(): + """Check setting and inquiring page labels. + - Make a PDF with 10 pages + - Label pages + - Inquire labels of pages + - Get list of page numbers for a given label. + """ + doc = make_doc() + doc.set_page_labels(make_labels()) + page_labels = [p.get_label() for p in doc] + answer = ["A-1", "A-2", "A-3", "A-4", "I", "II", "III", "IV", "V", "VI"] + assert page_labels == answer, f'page_labels={page_labels}' + assert doc.get_page_numbers("V") == [8] + assert doc.get_page_labels() == make_labels() diff --git a/tests/test_pixmap.py b/tests/test_pixmap.py new file mode 100644 index 0000000..889858b --- /dev/null +++ b/tests/test_pixmap.py @@ -0,0 +1,131 @@ +""" +Pixmap tests +* make pixmap of a page and assert bbox size +* make pixmap from a PDF xref and compare with extracted image +* pixmap from file and from binary image and compare +""" +import os +import tempfile + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +epub = os.path.join(scriptdir, "resources", "Bezier.epub") +pdf = os.path.join(scriptdir, "resources", "001003ED.pdf") +imgfile = os.path.join(scriptdir, "resources", "nur-ruhig.jpg") + + +def test_pagepixmap(): + # pixmap from an EPUB page + doc = fitz.open(epub) + page = doc[0] + pix = page.get_pixmap() + assert pix.irect == page.rect.irect + pix = page.get_pixmap(alpha=True) + assert pix.alpha + assert pix.n == pix.colorspace.n + pix.alpha + + +def test_pdfpixmap(): + # pixmap from xref in a PDF + doc = fitz.open(pdf) + # take first image item of first page + img = doc.get_page_images(0)[0] + # make pixmap of it + pix = fitz.Pixmap(doc, img[0]) + # assert pixmap properties + assert pix.width == img[2] + assert pix.height == img[3] + # extract image and compare metadata + extractimg = doc.extract_image(img[0]) + assert extractimg["width"] == pix.width + assert extractimg["height"] == pix.height + + +def test_filepixmap(): + # pixmaps from file and from stream + # should lead to same result + pix1 = fitz.Pixmap(imgfile) + stream = open(imgfile, "rb").read() + pix2 = fitz.Pixmap(stream) + assert repr(pix1) == repr(pix2) + assert pix1.digest == pix2.digest + + +def test_pilsave(): + # pixmaps from file then save to pillow image + # make pixmap from this and confirm equality + pix1 = fitz.Pixmap(imgfile) + try: + stream = pix1.pil_tobytes("JPEG") + pix2 = fitz.Pixmap(stream) + assert repr(pix1) == repr(pix2) + except: + pass + + +def test_save(tmpdir): + # pixmaps from file then save to image + # make pixmap from this and confirm equality + pix1 = fitz.Pixmap(imgfile) + outfile = os.path.join(tmpdir, "foo.png") + pix1.save(outfile, output="png") + # read it back + pix2 = fitz.Pixmap(outfile) + assert repr(pix1) == repr(pix2) + + +def test_setalpha(): + # pixmap from JPEG file, then add an alpha channel + # with 30% transparency + pix1 = fitz.Pixmap(imgfile) + opa = int(255 * 0.3) # corresponding to 30% transparency + alphas = [opa] * (pix1.width * pix1.height) + alphas = bytearray(alphas) + pix2 = fitz.Pixmap(pix1, 1) # add alpha channel + pix2.set_alpha(alphas) # make image 30% transparent + samples = pix2.samples # copy of samples + # confirm correct the alpha bytes + t = bytearray([samples[i] for i in range(3, len(samples), 4)]) + assert t == alphas + +def test_color_count(): + pm = fitz.Pixmap(imgfile) + assert pm.color_count() == 40624 + +def test_memoryview(): + pm = fitz.Pixmap(imgfile) + samples = pm.samples_mv + assert isinstance( samples, memoryview) + print( f'samples={samples} samples.itemsize={samples.itemsize} samples.nbytes={samples.nbytes} samples.ndim={samples.ndim} samples.shape={samples.shape} samples.strides={samples.strides}') + assert samples.itemsize == 1 + assert samples.nbytes == 659817 + assert samples.ndim == 1 + assert samples.shape == (659817,) + assert samples.strides == (1,) + + color = pm.pixel( 100, 100) + print( f'color={color}') + assert color == (83, 66, 40) + +def test_samples_ptr(): + pm = fitz.Pixmap(imgfile) + samples = pm.samples_ptr + print( f'samples={samples}') + assert isinstance( samples, int) + +def test_2369(): + + width, height = 13, 37 + image = fitz.Pixmap(fitz.csGRAY, width, height, b"\x00" * (width * height), False) + + with fitz.Document(stream=image.tobytes(output="pam"), filetype="pam") as doc: + test_pdf_bytes = doc.convert_to_pdf() + + with fitz.Document(stream=test_pdf_bytes) as doc: + page = doc[0] + img_xref = page.get_images()[0][0] + img = doc.extract_image(img_xref) + img_bytes = img["image"] + fitz.Pixmap(img_bytes) + diff --git a/tests/test_showpdfpage.py b/tests/test_showpdfpage.py new file mode 100644 index 0000000..ace2112 --- /dev/null +++ b/tests/test_showpdfpage.py @@ -0,0 +1,31 @@ +""" +Tests: + * Convert some image to a PDF + * Insert it rotated in some rectangle of a PDF page + * Assert PDF Form XObject has been created + * Assert that image contained in inserted PDF is inside given retangle +""" +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +imgfile = os.path.join(scriptdir, "resources", "nur-ruhig.jpg") + + +def test_insert(): + doc = fitz.open() + page = doc.new_page() + rect = fitz.Rect(50, 50, 100, 100) # insert in here + img = fitz.open(imgfile) # open image + tobytes = img.convert_to_pdf() # get its PDF version (bytes object) + src = fitz.open("pdf", tobytes) # open as PDF + xref = page.show_pdf_page(rect, src, 0, rotate=-23) # insert in rectangle + # extract just inserted image info + img = page.get_images(True)[0] + assert img[-1] == xref # xref of Form XObject! + img = page.get_image_info()[0] # read the page's images + + # Multiple computations may have lead to rounding deviations, so we need + # some generosity here: enlarge rect by 1 point in each direction. + assert img["bbox"] in rect + (-1, -1, 1, 1) diff --git a/tests/test_story.py b/tests/test_story.py new file mode 100644 index 0000000..fa4535f --- /dev/null +++ b/tests/test_story.py @@ -0,0 +1,32 @@ +import fitz +import os + + +def test_story(): + otf = os.path.abspath(f'{__file__}/../resources/PragmaticaC.otf') + CSS = f""" + @font-face {{font-family: test; src: url({otf});}} + """ + + HTML = """ +

We shall meet again at a place where there is no darkness.

+ """ + + MEDIABOX = fitz.paper_rect("letter") + WHERE = MEDIABOX + (36, 36, -36, -36) + # the font files are located in /home/chinese + arch = fitz.Archive(".") + # if not specfied user_css, the output pdf has content + story = fitz.Story(HTML, user_css=CSS, archive=arch) + + writer = fitz.DocumentWriter("output.pdf") + + more = 1 + + while more: + device = writer.begin_page(MEDIABOX) + more, _ = story.place(WHERE) + story.draw(device) + writer.end_page() + + writer.close() diff --git a/tests/test_textbox.py b/tests/test_textbox.py new file mode 100644 index 0000000..b08615e --- /dev/null +++ b/tests/test_textbox.py @@ -0,0 +1,123 @@ +""" +Fill a given text in a rectangle on some PDF page using +1. TextWriter object +2. Basic text output + +Check text is indeed contained in given rectangle. +""" +import fitz + +text = """Der Kleine Schwertwal (Pseudorca crassidens), auch bekannt als Unechter oder Schwarzer Schwertwal, ist eine Art der Delfine (Delphinidae) und der einzige rezente Vertreter der Gattung Pseudorca. + +Er ähnelt dem Orca in Form und Proportionen, ist aber einfarbig schwarz und mit einer Maximallänge von etwa sechs Metern deutlich kleiner. + +Kleine Schwertwale bilden Schulen von durchschnittlich zehn bis fünfzig Tieren, wobei sie sich auch mit anderen Delfinen vergesellschaften und sich meistens abseits der Küsten aufhalten. + +Sie sind in allen Ozeanen gemäßigter, subtropischer und tropischer Breiten beheimatet, sind jedoch vor allem in wärmeren Jahreszeiten auch bis in die gemäßigte bis subpolare Zone südlich der Südspitze Südamerikas, vor Nordeuropa und bis vor Kanada anzutreffen.""" + + +def test_textbox1(): + """Use TextWriter for text insertion.""" + doc = fitz.open() + page = doc.new_page() + rect = fitz.Rect(50, 50, 400, 400) + blue = (0, 0, 1) + tw = fitz.TextWriter(page.rect, color=blue) + tw.fill_textbox( + rect, + text, + align=fitz.TEXT_ALIGN_LEFT, + fontsize=12, + ) + tw.write_text(page, morph=(rect.tl, fitz.Matrix(1, 1))) + # check text containment + assert page.get_text() == page.get_text(clip=rect) + page.write_text(writers=tw) + + +def test_textbox2(): + """Use basic text insertion.""" + doc = fitz.open() + ocg = doc.add_ocg("ocg1") + page = doc.new_page() + rect = fitz.Rect(50, 50, 400, 400) + blue = fitz.utils.getColor("lightblue") + red = fitz.utils.getColorHSV("red") + page.insert_textbox( + rect, + text, + align=fitz.TEXT_ALIGN_LEFT, + fontsize=12, + color=blue, + oc=ocg, + ) + # check text containment + assert page.get_text() == page.get_text(clip=rect) + + +def test_textbox3(): + """Use TextWriter for text insertion.""" + doc = fitz.open() + page = doc.new_page() + font = fitz.Font("cjk") + rect = fitz.Rect(50, 50, 400, 400) + blue = (0, 0, 1) + tw = fitz.TextWriter(page.rect, color=blue) + tw.fill_textbox( + rect, + text, + align=fitz.TEXT_ALIGN_LEFT, + font=font, + fontsize=12, + right_to_left=True, + ) + tw.write_text(page, morph=(rect.tl, fitz.Matrix(1, 1))) + # check text containment + assert page.get_text() == page.get_text(clip=rect) + doc.scrub() + doc.subset_fonts() + + +def test_textbox4(): + """Use TextWriter for text insertion.""" + doc = fitz.open() + ocg = doc.add_ocg("ocg1") + page = doc.new_page() + rect = fitz.Rect(50, 50, 400, 600) + blue = (0, 0, 1) + tw = fitz.TextWriter(page.rect, color=blue) + tw.fill_textbox( + rect, + text, + align=fitz.TEXT_ALIGN_LEFT, + fontsize=12, + font=fitz.Font("cour"), + right_to_left=True, + ) + tw.write_text(page, oc=ocg, morph=(rect.tl, fitz.Matrix(1, 1))) + # check text containment + assert page.get_text() == page.get_text(clip=rect) + + +def test_textbox5(): + """Using basic text insertion.""" + fitz.TOOLS.set_small_glyph_heights(True) + doc = fitz.open() + page = doc.new_page() + r = fitz.Rect(100, 100, 150, 150) + text = "words and words and words and more words..." + rc = -1 + fontsize = 12 + page.draw_rect(r) + while rc < 0: + rc = page.insert_textbox( + r, + text, + fontsize=fontsize, + align=fitz.TEXT_ALIGN_JUSTIFY, + ) + fontsize -= 0.5 + + blocks = page.get_text("blocks") + bbox = fitz.Rect(blocks[0][:4]) + assert bbox in r diff --git a/tests/test_textextract.py b/tests/test_textextract.py new file mode 100644 index 0000000..3f5d6ec --- /dev/null +++ b/tests/test_textextract.py @@ -0,0 +1,28 @@ +""" +Exract page text in various formats. +No checks performed - just contribute to code coverage. +""" +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "symbol-list.pdf") + + +def test_extract1(): + doc = fitz.open(filename) + page = doc[0] + text = page.get_text("text") + blocks = page.get_text("blocks") + words = page.get_text("words") + d1 = page.get_text("dict") + d2 = page.get_text("json") + d3 = page.get_text("rawdict") + d3 = page.get_text("rawjson") + text = page.get_text("html") + text = page.get_text("xhtml") + text = page.get_text("xml") + rects = fitz.get_highlight_selection(page, start=page.rect.tl, stop=page.rect.br) + text = fitz.ConversionHeader("xml") + text = fitz.ConversionTrailer("xml") diff --git a/tests/test_textsearch.py b/tests/test_textsearch.py new file mode 100644 index 0000000..f0082fd --- /dev/null +++ b/tests/test_textsearch.py @@ -0,0 +1,37 @@ +""" +"test_search1": +Search for some text on a PDF page, and compare content of returned hit +rectangle with the searched text. + +"test_search2": +Text search with 'clip' parameter - clip rectangle contains two occurrences +of searched text. Confirm search locations are inside clip. +""" +import os + +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename1 = os.path.join(scriptdir, "resources", "2.pdf") +filename2 = os.path.join(scriptdir, "resources", "github_sample.pdf") + + +def test_search1(): + doc = fitz.open(filename1) + page = doc[0] + needle = "mupdf" + rlist = page.search_for(needle) + assert rlist != [] + for rect in rlist: + assert needle in page.get_textbox(rect).lower() + + +def test_search2(): + doc = fitz.open(filename2) + page = doc[0] + needle = "the" + clip = fitz.Rect(40.5, 228.31436157226562, 346.5226135253906, 239.5338592529297) + rl = page.search_for(needle, clip=clip) + assert len(rl) == 2 + for r in rl: + assert r in clip diff --git a/tests/test_toc.py b/tests/test_toc.py new file mode 100644 index 0000000..767c11d --- /dev/null +++ b/tests/test_toc.py @@ -0,0 +1,86 @@ +""" +* Verify equality of generated TOCs and expected results. +* Verify TOC deletion works +* Verify manipulation of single TOC item works +* Verify stability against circular TOC items +""" +import os +import sys +import fitz + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "001003ED.pdf") +filename2 = os.path.join(scriptdir, "resources", "2.pdf") +circular = os.path.join(scriptdir, "resources", "circular-toc.pdf") +full_toc = os.path.join(scriptdir, "resources", "full_toc.txt") +simple_toc = os.path.join(scriptdir, "resources", "simple_toc.txt") +doc = fitz.open(filename) + + +def test_simple_toc(): + simple_lines = open(simple_toc, "rb").read() + toc = b"".join([str(t).encode() for t in doc.get_toc(True)]) + assert toc == simple_lines + + +def test_full_toc(): + full_lines = open(full_toc, "rb").read() + toc = b"".join([str(t).encode() for t in doc.get_toc(False)]) + assert toc == full_lines + + +def test_erase_toc(): + doc.set_toc([]) + assert doc.get_toc() == [] + + +def test_replace_toc(): + toc = doc.get_toc(False) + doc.set_toc(toc) + + +def test_setcolors(): + doc = fitz.open(filename2) + toc = doc.get_toc(False) + for i in range(len(toc)): + d = toc[i][3] + d["color"] = (1, 0, 0) + d["bold"] = True + d["italic"] = True + doc.set_toc_item(i, dest_dict=d) + + toc2 = doc.get_toc(False) + assert len(toc2) == len(toc) + + for t in toc2: + d = t[3] + assert d["bold"] + assert d["italic"] + assert d["color"] == (1, 0, 0) + + +def test_circular(): + """The test file contains circular bookmarks.""" + doc = fitz.open(circular) + toc = doc.get_toc(False) # this must not loop + +def test_2355(): + + # Create a test PDF with toc. + doc = fitz.Document() + for _ in range(10): + doc.new_page(doc.page_count) + doc.set_toc([[1, 'test', 1], [1, 'test2', 5]]) + + path = 'test_2355.pdf' + doc.save(path) + + # Open many times + for i in range(10): + with fitz.open(path) as new_doc: + new_doc.get_toc() + + # Open once and read many times + with fitz.open(path) as new_doc: + for i in range(10): + new_doc.get_toc() diff --git a/tests/test_widgets.py b/tests/test_widgets.py new file mode 100644 index 0000000..fc0245e --- /dev/null +++ b/tests/test_widgets.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +""" +Test PDF field (widget) insertion. +""" +import fitz +import os + +scriptdir = os.path.abspath(os.path.dirname(__file__)) +filename = os.path.join(scriptdir, "resources", "widgettest.pdf") +file_2333 = os.path.join(scriptdir, "resources", "test-2333.pdf") + + +doc = fitz.open() +page = doc.new_page() +gold = (1, 1, 0) # define some colors +blue = (0, 0, 1) +gray = (0.9, 0.9, 0.9) +fontsize = 11.0 # define a fontsize +lineheight = fontsize + 4.0 +rect = fitz.Rect(50, 72, 400, 200) + + +def test_text(): + doc = fitz.open() + page = doc.new_page() + widget = fitz.Widget() # create a widget object + widget.border_color = blue # border color + widget.border_width = 0.3 # border width + widget.border_style = "d" + widget.border_dashes = (2, 3) + widget.field_name = "Textfield-1" # field name + widget.field_label = "arbitrary text - e.g. to help filling the field" + widget.field_type = fitz.PDF_WIDGET_TYPE_TEXT # field type + widget.fill_color = gold # field background + widget.rect = rect # set field rectangle + widget.text_color = blue # rext color + widget.text_font = "TiRo" # use font Times-Roman + widget.text_fontsize = fontsize # set fontsize + widget.text_maxlen = 50 # restrict number of characters + widget.field_value = "Times-Roman" + page.add_widget(widget) # create the field + field = page.first_widget + assert field.field_type_string == "Text" + + +def test_checkbox(): + doc = fitz.open() + page = doc.new_page() + widget = fitz.Widget() + widget.border_style = "b" + widget.field_name = "Button-1" + widget.field_label = "a simple check box button" + widget.field_type = fitz.PDF_WIDGET_TYPE_CHECKBOX + widget.fill_color = gold + widget.rect = rect + widget.text_color = blue + widget.text_font = "ZaDb" + widget.field_value = True + page.add_widget(widget) # create the field + field = page.first_widget + assert field.field_type_string == "CheckBox" + + # Check #2350 - setting checkbox to readonly. + # + widget.field_flags |= fitz.PDF_FIELD_IS_READ_ONLY + widget.update() + path = f'{scriptdir}/test_checkbox.pdf' + doc.save(path) + + doc = fitz.open(path) + page = doc[0] + widget = page.first_widget + assert widget + assert widget.field_flags == fitz.PDF_FIELD_IS_READ_ONLY + + +def test_listbox(): + doc = fitz.open() + page = doc.new_page() + widget = fitz.Widget() + widget.field_name = "ListBox-1" + widget.field_label = "is not a drop down: scroll with cursor in field" + widget.field_type = fitz.PDF_WIDGET_TYPE_LISTBOX + widget.field_flags = fitz.PDF_CH_FIELD_IS_COMMIT_ON_SEL_CHANGE + widget.fill_color = gold + widget.choice_values = ( + "Frankfurt", + "Hamburg", + "Stuttgart", + "Hannover", + "Berlin", + "München", + "Köln", + "Potsdam", + ) + widget.rect = rect + widget.text_color = blue + widget.text_fontsize = fontsize + widget.field_value = widget.choice_values[-1] + print("About to add '%s'" % widget.field_name) + page.add_widget(widget) # create the field + field = page.first_widget + assert field.field_type_string == "ListBox" + + +def test_combobox(): + doc = fitz.open() + page = doc.new_page() + widget = fitz.Widget() + widget.field_name = "ComboBox-1" + widget.field_label = "an editable combo box ..." + widget.field_type = fitz.PDF_WIDGET_TYPE_COMBOBOX + widget.field_flags = ( + fitz.PDF_CH_FIELD_IS_COMMIT_ON_SEL_CHANGE + | fitz.PDF_CH_FIELD_IS_EDIT + ) + widget.fill_color = gold + widget.choice_values = ( + "Spanien", + "Frankreich", + "Holland", + "Dänemark", + "Schweden", + "Norwegen", + "England", + "Polen", + "Russland", + "Italien", + "Portugal", + "Griechenland", + ) + widget.rect = rect + widget.text_color = blue + widget.text_fontsize = fontsize + widget.field_value = widget.choice_values[-1] + page.add_widget(widget) # create the field + field = page.first_widget + assert field.field_type_string == "ComboBox" + + +def test_text2(): + doc = fitz.open() + doc.new_page() + page = [p for p in doc.pages()][0] + widget = fitz.Widget() + widget.field_name = "textfield-2" + widget.field_label = "multi-line text with tabs is also possible!" + widget.field_flags = fitz.PDF_TX_FIELD_IS_MULTILINE + widget.field_type = fitz.PDF_WIDGET_TYPE_TEXT + widget.fill_color = gray + widget.rect = rect + widget.text_color = blue + widget.text_font = "TiRo" + widget.text_fontsize = fontsize + widget.field_value = "This\n\tis\n\t\ta\n\t\t\tmulti-\n\t\tline\n\ttext." + page.add_widget(widget) # create the field + widgets = [w for w in page.widgets()] + field = widgets[0] + assert field.field_type_string == "Text" + + +def test_2333(): + doc = fitz.open(file_2333) + page = doc[0] + + def values(): + return set( + ( + doc.xref_get_key(635, "AS")[1], + doc.xref_get_key(636, "AS")[1], + doc.xref_get_key(637, "AS")[1], + doc.xref_get_key(638, "AS")[1], + doc.xref_get_key(127, "V")[1], + ) + ) + + for i, xref in enumerate((635, 636, 637, 638)): + w = page.load_widget(xref) + w.field_value = True + w.update() + assert values() == set(("/Off", f"{i}", f"/{i}")) + w.field_value=False + w.update() + assert values() == set(("Off", "/Off")) + + +def test_2411(): + """Add combobox values in different formats.""" + doc = fitz.open() + page = doc.new_page() + rect = fitz.Rect(100, 100, 300, 200) + + widget = fitz.Widget() + widget.field_flags = ( + fitz.PDF_CH_FIELD_IS_COMBO + | fitz.PDF_CH_FIELD_IS_EDIT + | fitz.PDF_CH_FIELD_IS_COMMIT_ON_SEL_CHANGE + ) + widget.field_name = "ComboBox-1" + widget.field_label = "an editable combo box ..." + widget.field_type = fitz.PDF_WIDGET_TYPE_COMBOBOX + widget.fill_color = fitz.pdfcolor["gold"] + widget.rect = rect + widget.choice_values = [ + ["Spain", "ES"], # double value as list + ("Italy", "I"), # double value as tuple + "Portugal", # single value + ] + page.add_widget(widget) + +def test_2391(): + """Confirm that multiple times setting a checkbox to ON/True/Yes will work.""" + doc = fitz.open(f'{scriptdir}/resources/widgettest.pdf') + page = doc[0] + # its work when we update first-time + for field in page.widgets(types=[fitz.PDF_WIDGET_TYPE_CHECKBOX]): + field.field_value = True + field.update() + + for i in range(5): + pdfdata = doc.tobytes() + doc.close() + doc = fitz.open("pdf", pdfdata) + page = doc[0] + for field in page.widgets(types=[fitz.PDF_WIDGET_TYPE_CHECKBOX]): + assert field.field_value == field.on_state() + field_field_value = field.on_state() + field.update() + +# def test_deletewidget(): +# pdf = fitz.open(filename) +# page = pdf[0] +# field = page.first_widget +# page.delete_widget(field)