[
  {
    "path": ".git-blame-ignore-revs",
    "content": "# This file helps us to ignore style / formatting / doc changes\n# in git blame. That is useful when we're trying to find the root cause of an\n# error.\n\n# Docstring formatting\na89ff74d8c0203278a039d9496a3d8df4d134f84\n\n# STY: Apply pre-commit (black, isort) + use snake_case variables (#832)\neef03d935dfeacaa75848b39082cf94d833d3174\n\n# STY: Apply black and isort\nbaeb7d23278de0f8d00ca9f2b656bf0674f08937\n\n# STY: Documentation, Variable names (#839)\n444fca22836df061d9d23e71ffb7d68edcdfa766\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug-report.md",
    "content": "---\nname: Report a bug\nabout: Something broke!\ntitle: ''\nlabels: Bug\nassignees: ''\n\n---\n\nReplace this: What happened? What were you trying to achieve?\n\n## Environment\n\nWhich environment were you using when you encountered the problem?\n\n```bash\n$ python -m platform\n# TODO: Your output goes here\n\n$ python -c \"import pypdf;print(pypdf._debug_versions)\"\n# TODO: Your output goes here\n```\n\n## Code + PDF\n\nThis is a minimal, complete example that shows the issue:\n\n```python\n# TODO: Your code goes here\n```\n\nShare here the PDF file(s) that cause the issue. The smaller they are, the\nbetter. Let us know if we may add them to our tests!\n\n## Traceback\n\nThis is the complete traceback I see:\n\n```\n# TODO: Your traceback goes here (if applicable)\n```\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature-request.md",
    "content": "---\nname: Request a Feature\nabout: What do you think is missing in pypdf?\ntitle: ''\nlabels: Feature Request\nassignees: ''\n\n---\n\n## Explanation\n\nExplain briefly what you want to achieve.\n\n## Code Example\n\nHow would your feature be used? (Remove this if it is not applicable.)\n\n```python\nfrom pypdf import PdfReader, PdfWriter\n\n...  # your new feature in action!\n```\n"
  },
  {
    "path": ".github/SECURITY.md",
    "content": "# Security Policy\n\n## Supported Versions\n\nSecurity fixes are applied to the latest version.\n\n## Reporting a Vulnerability\n\nIf you find a potential security issue, please report it using the\n[private vulnerability reporting](https://docs.github.com/en/code-security/security-advisories/guidance-on-reporting-and-writing-information-about-vulnerabilities/privately-reporting-a-security-vulnerability) feature of GitHub to\nautomatically inform all relevant team members. Otherwise, please\nget in touch with stefan6419846 through e-mail (current maintainer,\naddress in GitHub profile).\n\nPlease have a look at our [corresponding user documentation](https://pypdf.readthedocs.io/en/stable/user/security.html)\nas well, which includes some information about possibly invalid reports as well.\n\nWe will try to find a fix in a timely manner and will then issue a security\nadvisory together with the update via GitHub, as well as requesting a CVE\n([example](https://github.com/py-pdf/pypdf/security/advisories/GHSA-xcjx-m2pj-8g79)).\n\nIf you do not get a reaction within 30 days, please open a public issue on GitHub.\n"
  },
  {
    "path": ".github/dependabot.yaml",
    "content": "# Set update schedule for GitHub Actions\n\nversion: 2\nupdates:\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n    commit-message:\n      prefix: \"DEV\"\n"
  },
  {
    "path": ".github/scripts/check_gh_pages_updates.py",
    "content": "\"\"\"Check that all GitHub pages JavaScript dependencies are up-to-date.\"\"\"  # noqa: INP001\n\nimport base64\nimport hashlib\nimport json\nimport re\nimport sys\nimport urllib.request\nfrom pathlib import Path\n\nJSDELIVR_RE = re.compile(\n    r\"(https://cdn\\.jsdelivr\\.net/npm/\"\n    r\"(?P<name>[^@/]+)@(?P<version>[^/]+)\"\n    r\"/(?P<path>[^\\\"']+))\"\n)\n\n\ndef fetch_json(url: str) -> dict:\n    \"\"\"Retrieve JSON data from the given URL.\"\"\"\n    with urllib.request.urlopen(url, timeout=15) as resp:  # noqa: S310  # Controlled input.\n        return json.load(resp)\n\n\ndef fetch_bytes(url: str) -> bytes:\n    \"\"\"Retrieve bytes data from the given URL.\"\"\"\n    with urllib.request.urlopen(url, timeout=30) as resp:  # noqa: S310  # Controlled input.\n        return resp.read()\n\n\ndef get_latest_version(pkg: str) -> str:\n    \"\"\"Get the latest version for this package.\"\"\"\n    data = fetch_json(f\"https://registry.npmjs.org/{pkg}\")\n    return data[\"dist-tags\"][\"latest\"]\n\n\ndef sri_hash(content: bytes) -> str:\n    \"\"\"Calculate the SRI hash for the given content.\"\"\"\n    digest = hashlib.sha384(content).digest()\n    return \"sha384-\" + base64.b64encode(digest).decode(\"ascii\")\n\n\ndef scan_html(path: Path) -> list[re.Match[str]]:\n    \"\"\"Scan the given HTML file for external JavaScript includes.\"\"\"\n    text = path.read_text(encoding=\"utf-8\", errors=\"ignore\")\n    return list(JSDELIVR_RE.finditer(text))\n\n\ndef main() -> None:\n    \"\"\"Perform the checks.\"\"\"\n    outdated_found = False\n\n    for html_path in sorted(Path(\"gh-pages\").rglob(\"*.html\"), key=str):\n        matches = scan_html(html_path)\n        if not matches:\n            continue\n\n        sys.stdout.write(f\"\\n📄 {html_path} ...\\n\\n\")\n\n        for m in matches:\n            pkg = m.group(\"name\")\n            current_version = m.group(\"version\")\n            full_url = m.group(1)\n\n            try:\n                latest_version = get_latest_version(pkg)\n            except Exception as e:\n                sys.stdout.write(f\"  ⚠️  {pkg}: npm lookup failed ({e})\\n\")\n                continue\n\n            if current_version == latest_version:\n                sys.stdout.write(f\"  ✅ {pkg} {current_version}\\n\")\n                continue\n\n            outdated_found = True\n            latest_url = full_url.replace(\n                f\"@{current_version}/\", f\"@{latest_version}/\"\n            )\n\n            try:\n                latest_bytes = fetch_bytes(latest_url)\n                latest_sri = sri_hash(latest_bytes)\n            except Exception as e:\n                sys.stdout.write(f\"  ⚠️  {pkg}: failed to fetch latest file ({e})\\n\")\n                continue\n\n            sys.stdout.write(f\"  ❌ {pkg}\\n\")\n            sys.stdout.write(f\"     Current: {current_version}\\n\")\n            sys.stdout.write(f\"     Latest:  {latest_version}\\n\")\n            sys.stdout.write(f\"     Latest SRI: {latest_sri}\\n\")\n            sys.stdout.write(\"\\n\")\n\n    if outdated_found:\n        sys.stdout.write(\"\\n❗ Outdated dependencies detected\\n\")\n        sys.exit(1)\n\n    sys.stdout.write(\"\\n🎉 All CDN dependencies are up to date\\n\")\n\n\nif __name__ == \"__main__\":\n    main()\n"
  },
  {
    "path": ".github/scripts/check_pr_title.py",
    "content": "\"\"\"Check that all PR titles follow the desired scheme.\"\"\"  # noqa: INP001\n\nimport os\nimport sys\n\nKNOWN_PREFIXES = (\n    \"SEC: \",\n    \"BUG: \",\n    \"ENH: \",\n    \"DEP: \",\n    \"PI: \",\n    \"ROB: \",\n    \"DOC: \",\n    \"TST: \",\n    \"DEV: \",\n    \"STY: \",\n    \"MAINT: \",\n    \"REL: \",  # For internal use only.\n)\nPR_TITLE = os.getenv(\"PR_TITLE\", \"\")\n\nif not PR_TITLE.startswith(KNOWN_PREFIXES) or not PR_TITLE.split(\": \", maxsplit=1)[1]:\n    sys.stderr.write(\n        f\"The PR title '{PR_TITLE}' does not follow the projects naming scheme: \"\n        \"https://pypdf.readthedocs.io/en/latest/dev/intro.html#commit-messages\\n\",\n    )\n    sys.stderr.write(\n        \"If you do not know which one to choose or if multiple apply, make a best guess. \"\n        \"Nobody will complain if it does not quite fit :-)\\n\",\n    )\n    sys.exit(1)\nelse:\n    sys.stdout.write(f\"PR title '{PR_TITLE}' appears to be valid.\\n\")\n"
  },
  {
    "path": ".github/scripts/check_urls.py",
    "content": "\"\"\"Check that all test data URLs are still accessible.\"\"\"  # noqa: INP001\nimport ast\nimport sys\nfrom collections.abc import Iterator\nfrom operator import itemgetter\nfrom pathlib import Path\n\nfrom tests import _get_data_from_url, read_yaml_to_list_of_dicts\n\nURL_PREFIXES_TO_IGNORE = (\n    \"http://ns.adobe.com/tiff/1.0/\",\n    \"http://www.example.com\",\n    \"https://example.com\",\n    \"https://martin-thoma.com\",\n    \"https://pypdf.readthedocs.io/\",\n    \"https://www.example.com\",\n)\n\nPDF_URLS_WHICH_DO_NOT_LOOK_LIKE_PDFS = {\n    \"https://github.com/user-attachments/files/18381726/tika-957721.pdf\",\n}\n\n\ndef get_urls_from_test_files() -> Iterator[str]:\n    \"\"\"Retrieve all URLs defined the test files.\"\"\"\n    tests_directory = Path(__file__).parent.parent.parent / \"tests\"\n    for test_file in sorted(tests_directory.rglob(\"test_*.py\")):\n        tree = ast.parse(source=test_file.read_text(encoding=\"utf-8\"), filename=str(test_file))\n        for node in ast.walk(tree):\n            if not isinstance(node, ast.Constant):\n                continue\n            if not isinstance(node.value, str):\n                continue\n            if not node.value.startswith((\"http://\", \"https://\")):\n                continue\n            yield node.value\n\n\ndef get_urls_from_example_files() -> Iterator[str]:\n    \"\"\"Retrieve all URLs defined in the `example_files.yaml`.\"\"\"\n    pdfs = read_yaml_to_list_of_dicts(Path(__file__).parent.parent.parent / \"tests\" / \"example_files.yaml\")\n    yield from map(itemgetter(\"url\"), pdfs)\n\n\ndef check_url(url: str) -> bool:\n    \"\"\"Check if the given URL appears to still be valid.\"\"\"\n    if url.startswith(URL_PREFIXES_TO_IGNORE):\n        return True\n\n    try:\n        data = _get_data_from_url(url)\n    except Exception as exception:\n        sys.stderr.write(f\"Error getting data from {url}: {exception}\\n\")\n        return False\n\n    if len(data) < 75:\n        sys.stderr.write(f\"Not enough data from {url}: {data}\\n\")\n        return False\n\n    if (\n            url.lower().endswith(\".pdf\") and\n            url not in PDF_URLS_WHICH_DO_NOT_LOOK_LIKE_PDFS and\n            not data.startswith(b\"%PDF-\")\n    ):\n        sys.stderr.write(f\"The file at {url} does not look like a PDF: {data[:50]}\\n\")\n        return False\n\n    sys.stdout.write(f\"URL {url} looks good.\\n\")\n    return True\n\n\ndef main() -> bool:\n    \"\"\"Check if there are invalid URLs.\"\"\"\n    urls: set[str] = set()\n    for url in get_urls_from_test_files():\n        urls.add(url)\n    for url in get_urls_from_example_files():\n        urls.add(url)\n\n    is_valid = True\n    for url in sorted(urls):\n        is_valid &= check_url(url)\n    return not is_valid\n\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n"
  },
  {
    "path": ".github/workflows/benchmark.yaml",
    "content": "name: Benchmarking pypdf\non:\n  push:\n    branches:\n      - main\n\npermissions:\n  contents: write\n  deployments: write\n\njobs:\n  benchmark:\n    name: \"Benchmark ${{ matrix.name }}\"\n    runs-on: ubuntu-latest\n    strategy:\n      matrix:\n        python-version: ['3.x']\n        include:\n          - python-version: '3.x'\n            name: 'CPython'\n          - python-version: 'pypy3.11'\n            name: 'PyPy 3.11'\n    steps:\n    - name: Checkout Code\n      uses: actions/checkout@v6\n      with:\n        submodules: 'recursive'\n    - name: Setup Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: ${{ matrix.python-version }}\n    - name: Install requirements\n      run: |\n        pip install -r requirements/ci-3.11.txt\n    - name: Install pypdf\n      run: |\n        pip install .\n    - name: Run benchmark\n      run: |\n        pytest tests/bench.py --benchmark-json output.json\n    - name: Store benchmark result\n      uses: benchmark-action/github-action-benchmark@v1\n      with:\n        name: \"${{ matrix.name }} Benchmark\"\n        tool: 'pytest'\n        output-file-path: output.json\n        # Use personal access token instead of GITHUB_TOKEN due to https://github.community/t/github-action-not-triggering-gh-pages-upon-push/16096\n        github-token: ${{ secrets.GITHUB_TOKEN }}\n        auto-push: true\n        # Show alert with commit comment on detecting possible performance regression\n        alert-threshold: '200%'\n        comment-on-alert: true\n        fail-on-alert: true\n"
  },
  {
    "path": ".github/workflows/create-github-release.yaml",
    "content": "name: Create a GitHub release page\n\non:\n  push:\n    tags:\n      - '*.*.*'\n  workflow_dispatch:\n\npermissions:\n  contents: write\n\njobs:\n  build_and_publish:\n    name: Create a GitHub release page\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v6\n      - name: Prepare variables\n        id: prepare_variables\n        run: |\n          git fetch --tags --force\n          latest_tag=$(git describe --tags --abbrev=0)\n          echo \"latest_tag=${latest_tag}\" >> \"$GITHUB_ENV\"\n          echo \"date=$(date +'%Y-%m-%d')\" >> \"$GITHUB_ENV\"\n          EOF=$(dd if=/dev/urandom bs=15 count=1 status=none | base64)\n          echo \"tag_body<<$EOF\" >> \"$GITHUB_ENV\"\n          git --no-pager tag -l \"${latest_tag}\" --format='%(contents:body)' >> \"$GITHUB_ENV\"\n          echo \"$EOF\" >> \"$GITHUB_ENV\"\n      - name: Create GitHub Release 🚀\n        uses: softprops/action-gh-release@v2\n        with:\n          tag_name: ${{ env.latest_tag }}\n          name: Version ${{ env.latest_tag }}, ${{ env.date }}\n          draft: false\n          prerelease: false\n          body: ${{ env.tag_body }}\n"
  },
  {
    "path": ".github/workflows/gh-pages-check.yaml",
    "content": "name: 'GitHub Pages Check'\non:\n  workflow_dispatch:\n  schedule:\n    - cron: 0 6 * * 1\n\njobs:\n  url-check:\n    name: GitHub Pages check\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout GitHub Pages\n        uses: actions/checkout@v6\n        with:\n          ref: 'gh-pages'\n          path: 'gh-pages'\n      - name: Checkout main (tools)\n        uses: actions/checkout@v6\n        with:\n          ref: main\n          path: main\n      - name: Setup Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.x'\n      - name: Check GitHub Pages\n        run: |\n          export PYTHONPATH=\"$GITHUB_WORKSPACE\"\n          python main/.github/scripts/check_gh_pages_updates.py\n"
  },
  {
    "path": ".github/workflows/github-ci.yaml",
    "content": "# This workflow will install Python dependencies, run tests and lint with a variety of Python versions\n# For more information see: https://docs.github.com/en/actions/tutorials/build-and-test-code/python\n\nname: CI\n\non:\n  push:\n    branches:\n      - main\n    paths-ignore:\n      - '**/*.md'\n      - '**/*.rst'\n  pull_request:\n    branches:\n      - main\n    paths-ignore:\n      - '**/*.md'\n      - '**/*.rst'\n  workflow_dispatch:\n\njobs:\n  test_windows:\n    name: pytest on windows\n    runs-on: windows-latest\n    steps:\n    - name: Checkout Code\n      uses: actions/checkout@v6\n      with:\n        submodules: 'recursive'\n    - name: Cache Downloaded Files\n      id: cache-downloaded-files-windows\n      uses: actions/cache@v5\n      if: github.ref == 'refs/heads/main'\n      with:\n        path: '**/tests/pdf_cache/*'\n        key: cache-downloaded-files-main-${{ github.run_id }}\n        restore-keys: |\n          cache-downloaded-files-main-\n          cache-downloaded-files\n        enableCrossOsArchive: true\n    - name: Restore Downloaded Files\n      uses: actions/cache/restore@v5\n      if: github.ref != 'refs/heads/main'\n      with:\n        path: '**/tests/pdf_cache/*'\n        key: cache-downloaded-files-main-\n        restore-keys: |\n          cache-downloaded-files-main-\n          cache-downloaded-files\n        enableCrossOsArchive: true\n    - name: Setup Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: '3.x'\n        allow-prereleases: true\n    - name: Upgrade pip\n      run: |\n        python -m pip install --upgrade pip\n    - name: Install requirements (Python 3.11+)\n      run: |\n        pip install -r requirements/ci-3.11.txt\n    - name: Install cryptography\n      run: |\n        pip install cryptography\n    - name: Install pypdf\n      run: |\n        pip install .\n    - name: Prepare\n      run: |\n        python -c \"from tests import download_test_pdfs; download_test_pdfs()\"\n    - name: Test with pytest\n      run: |\n        python -m pytest tests --cov=pypdf --cov-append -n auto -vv -p no:benchmark\n\n  test_macos:\n    name: pytest on macOS\n    runs-on: macos-latest\n    steps:\n    - name: Checkout Code\n      uses: actions/checkout@v6\n      with:\n        submodules: 'recursive'\n    - name: Cache Downloaded Files\n      id: cache-downloaded-files-mac\n      uses: actions/cache@v5\n      if: github.ref == 'refs/heads/main'\n      with:\n        path: '**/tests/pdf_cache/*'\n        key: cache-downloaded-files-main-${{ github.run_id }}\n        restore-keys: |\n          cache-downloaded-files-main-\n          cache-downloaded-files\n    - name: Restore Downloaded Files\n      uses: actions/cache/restore@v5\n      if: github.ref != 'refs/heads/main'\n      with:\n        path: '**/tests/pdf_cache/*'\n        key: cache-downloaded-files-main-\n        restore-keys: |\n          cache-downloaded-files-main-\n          cache-downloaded-files\n    - name: Setup Python (3.11+)\n      uses: actions/setup-python@v6\n      with:\n        python-version: '3.x'\n        allow-prereleases: true\n    - name: Upgrade pip\n      run: |\n        python -m pip install --upgrade pip\n    - name: Install requirements (Python 3.11+)\n      run: |\n        pip install -r requirements/ci-3.11.txt\n    - name: Install cryptography\n      run: |\n        pip install cryptography\n    - name: Install OS dependencies\n      run:\n        brew install ghostscript jbig2dec poppler\n    - name: Install pypdf\n      run: |\n        pip install .\n    - name: Prepare\n      run: |\n        python -c \"from tests import download_test_pdfs; download_test_pdfs()\"\n    - name: Test with pytest\n      run: |\n        python -m pytest tests --cov=pypdf --cov-append -n auto -vv -p no:benchmark\n\n  tests:\n    name: \"pytest on ${{ matrix.python-version }} (crypto-lib: ${{ matrix.use-crypto-lib }})\"\n    runs-on: ubuntu-24.04\n    strategy:\n      matrix:\n        python-version: ['3.9', '3.10', '3.11', '3.12', '3.13', '3.14', 'pypy3.11']\n        use-crypto-lib: ['cryptography']\n        include:\n          - python-version: '3.9'\n            use-crypto-lib: 'pycryptodome'\n          - python-version: '3.9'\n            use-crypto-lib: 'none'\n    steps:\n    - name: Update APT packages\n      run:\n        sudo apt-get update\n    - name: Install APT dependencies\n      run:\n        sudo apt-get install ghostscript jbig2dec poppler-utils\n    - name: Checkout Code\n      uses: actions/checkout@v6\n      with:\n        submodules: 'recursive'\n    - name: Cache Downloaded Files\n      id: cache-downloaded-files\n      uses: actions/cache@v5\n      if: github.ref == 'refs/heads/main'\n      with:\n        path: '**/tests/pdf_cache/*'\n        key: cache-downloaded-files-main-${{ github.run_id }}\n        restore-keys: |\n          cache-downloaded-files-main-\n          cache-downloaded-files\n    - name: Restore Downloaded Files\n      uses: actions/cache/restore@v5\n      if: github.ref != 'refs/heads/main'\n      with:\n        path: '**/tests/pdf_cache/*'\n        key: cache-downloaded-files-main-\n        restore-keys: |\n          cache-downloaded-files-main-\n          cache-downloaded-files\n    - name: Setup Python\n      uses: actions/setup-python@v6\n      if: matrix.python-version == '3.9' || matrix.python-version == '3.10'\n      with:\n        python-version: ${{ matrix.python-version }}\n        cache: 'pip'\n        cache-dependency-path: '**/requirements/ci.txt'\n    - name: Setup Python (3.11+)\n      uses: actions/setup-python@v6\n      if: matrix.python-version != '3.9' && matrix.python-version != '3.10'\n      with:\n        python-version: ${{ matrix.python-version }}\n        allow-prereleases: true\n        cache: 'pip'\n        cache-dependency-path: '**/requirements/ci-3.11.txt'\n    - name: Upgrade pip\n      run: |\n        python -m pip install --upgrade pip\n    - name: Install requirements (Python 3)\n      run: |\n        pip install -r requirements/ci.txt\n      if: matrix.python-version == '3.9' || matrix.python-version == '3.10'\n    - name: Install requirements (Python 3.11+)\n      run: |\n        pip install -r requirements/ci-3.11.txt\n      if: matrix.python-version != '3.9' && matrix.python-version != '3.10'\n    - name: Remove pycryptodome and cryptography\n      run: |\n        pip uninstall pycryptodome cryptography -y\n    - name: Install cryptography\n      run: |\n        pip install cryptography\n      if: matrix.use-crypto-lib == 'cryptography'\n    - name: Install pycryptodome\n      run: |\n        pip install pycryptodome\n      if: matrix.use-crypto-lib == 'pycryptodome'\n    - name: Install pypdf\n      run: |\n        pip install .\n    - name: Download test files\n      run: |\n        python -c \"from tests import download_test_pdfs; download_test_pdfs()\"\n    - name: Test with pytest\n      run: |\n        python -m pytest tests --cov=pypdf --cov-append -n auto -vv -p no:benchmark\n      if: ${{ !startsWith(matrix.python-version, 'pypy') }}\n    - name: Test with pytest (PyPy, no coverage)\n      # Coverage on PyPy is skipped because running coverage with PyPy is slow and CPython test already provides\n      # complete coverage data for the same code\n      run: |\n        python -m pytest tests -n auto -vv -p no:benchmark -o faulthandler_timeout=60 --dist=loadfile\n      if: ${{ startsWith(matrix.python-version, 'pypy') }}\n    - name: Rename coverage data file\n      run: mv .coverage \".coverage.$RANDOM\"\n      if: ${{ !startsWith(matrix.python-version, 'pypy') }}\n    - name: Upload coverage data\n      uses: actions/upload-artifact@v7\n      if: ${{ !startsWith(matrix.python-version, 'pypy') }}\n      with:\n        name: coverage-data.${{ matrix.python-version }}-${{ matrix.use-crypto-lib }}\n        path: .coverage.*\n        if-no-files-found: ignore\n        include-hidden-files: true\n\n  codestyle:\n    name: Check code style issues\n    runs-on: ubuntu-24.04\n    steps:\n    - name: Checkout Code\n      uses: actions/checkout@v6\n      with:\n        submodules: 'recursive'\n    - name: Setup Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: '3.x'\n        cache: 'pip'\n        cache-dependency-path: '**/requirements/ci-3.11.txt'\n    - name: Upgrade pip\n      run: |\n        python -m pip install --upgrade pip\n    - name: Install requirements\n      run: |\n        pip install -r requirements/ci-3.11.txt\n    - name: Install pypdf\n      run: |\n        pip install .\n    - name: Test with ruff\n      run: |\n        echo `ruff --version`\n        ruff check .\n    - name: Test with mypy\n      run : |\n        mypy pypdf\n    - name: Install docs requirements\n      run: |\n        pip install -r requirements/docs.txt\n    - name: Test docs build\n      working-directory: ./docs\n      run: |\n        sphinx-build --nitpicky --fail-on-warning --keep-going --show-traceback -d _build/doctrees --builder html . _build/html\n    - name: Test docs examples\n      working-directory: ./docs\n      run: |\n        sphinx-build -d _build/doctrees --builder doctest . _build/doctest\n    - name: Check with pre-commit\n      run: |\n        pip install -r requirements/dev.txt\n        pre-commit run --all-files --show-diff-on-failure\n\n  package:\n    name: Build & verify package\n    runs-on: ubuntu-latest\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-python@v6\n        with:\n          python-version: '3.x'\n\n      - run: python -m pip install flit check-wheel-contents\n      - run: flit build\n      - run: ls -l dist\n\n      - name: Test CHANGELOG.md present in sdist\n        run: tar -tzf dist/*.tar.gz | grep -q 'CHANGELOG.md'\n\n      - name: Test of bdist\n        run: check-wheel-contents dist/*.whl\n\n      - name: Test installing package\n        run: python -m pip install .\n\n      - name: Test running installed package\n        working-directory: /tmp\n        run: python -c \"import pypdf;print(pypdf.__version__)\"\n\n  coverage:\n    name: Combine & check coverage.\n    runs-on: ubuntu-latest\n    needs: tests\n\n    steps:\n      - uses: actions/checkout@v6\n      - uses: actions/setup-python@v6\n        with:\n          python-version: '3.x'\n\n      - run: python -m pip install --upgrade coverage[toml]\n\n      - uses: actions/download-artifact@v8\n        with:\n          pattern: coverage-data*\n          merge-multiple: true\n\n      - name: Check Number of Downloaded Files\n        run: |\n          downloaded_files_count=$(find \\.coverage* -type f | wc -l)\n          if [ $downloaded_files_count -eq 8 ]; then\n            echo \"The expected number of files (8) were downloaded.\"\n          else\n            echo \"ERROR: Expected 8 files, but found $downloaded_files_count files.\"\n            exit 1\n          fi\n\n      - name: Combine coverage & create xml report\n        run: |\n          python -m coverage combine\n          python -m coverage xml\n      - name: Upload Coverage to Codecov\n        uses: codecov/codecov-action@v5\n        with:\n          token: ${{ secrets.CODECOV_TOKEN }}\n          files: ./coverage.xml\n"
  },
  {
    "path": ".github/workflows/publish-to-pypi.yaml",
    "content": "name: Publish Python Package to PyPI\n\non:\n  push:\n    tags:\n      - '*.*.*'\n  workflow_dispatch:\n\njobs:\n  build:\n    name: Build distribution\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v6\n    - name: Set up Python\n      uses: actions/setup-python@v6\n      with:\n        python-version: '3.x'\n    - name: Install pypa/build\n      run: >-\n        python3 -m\n        pip install\n        build\n        --user\n    - name: Build a binary wheel and a source tarball\n      run: python3 -m build\n    - name: Store the distribution packages\n      uses: actions/upload-artifact@v7\n      with:\n        name: python-package-distributions\n        path: dist/\n\n  publish-to-pypi:\n    name: Publish Python distribution to PyPI\n    needs:\n    - build\n    runs-on: ubuntu-latest\n    environment:\n      name: pypi\n      url: https://pypi.org/p/pypdf\n    permissions:\n      id-token: write  # IMPORTANT: mandatory for trusted publishing\n\n    steps:\n    - name: Download all the dists\n      uses: actions/download-artifact@v8\n      with:\n        name: python-package-distributions\n        path: dist/\n    - name: Publish distribution to PyPI\n      uses: pypa/gh-action-pypi-publish@release/v1\n"
  },
  {
    "path": ".github/workflows/release.yaml",
    "content": "# This action assumes that there is a REL-commit which already has a\n# Markdown-formatted git tag. Hence, the CHANGELOG is already adjusted,\n# and it's decided what should be in the release.\n# This action only ensures the release is done with the proper contents\n# and that it's announced with a GitHub release.\nname: Create git tag\n# Disable for now and uses dummy `workflow_dispatch` trigger we usually do not use anyway.\n# To activate this again, we have to fix https://github.com/py-pdf/pypdf/issues/2753\non:\n    workflow_dispatch:\n#   push:\n#     branches:\n#       - main\n\npermissions:\n  contents: write\n\nenv:\n  HEAD_COMMIT_MESSAGE: ${{ github.event.head_commit.message }}\n\njobs:\n  build_and_publish:\n    name: Publish a new version\n    runs-on: ubuntu-latest\n    if: \"${{ startsWith(github.event.head_commit.message, 'REL: ') }}\"\n    steps:\n      - name: Checkout Repository\n        uses: actions/checkout@v6\n\n      - name: Extract version from commit message\n        id: extract_version\n        run: |\n          VERSION=$(echo \"$HEAD_COMMIT_MESSAGE\" | grep -oP '(?<=REL: )\\d+\\.\\d+\\.\\d+')\n          echo \"version=$VERSION\" >> $GITHUB_OUTPUT\n\n      - name: Extract tag message from commit message\n        id: extract_message\n        run: |\n          VERSION=\"${{ steps.extract_version.outputs.version }}\"\n          delimiter=\"$(openssl rand -hex 8)\"\n          MESSAGE=$(echo \"$HEAD_COMMIT_MESSAGE\" | sed \"0,/REL: $VERSION/s///\" )\n          echo \"message<<${delimiter}\" >> $GITHUB_OUTPUT\n          echo \"$MESSAGE\" >> $GITHUB_OUTPUT\n          echo \"${delimiter}\" >> $GITHUB_OUTPUT\n\n      - name: Create Git Tag\n        run: |\n          VERSION=\"${{ steps.extract_version.outputs.version }}\"\n          MESSAGE=\"${{ steps.extract_message.outputs.message }}\"\n          git config user.name github-actions\n          git config user.email github-actions@github.com\n          git tag \"$VERSION\" -m \"$MESSAGE\"\n          git push origin $VERSION\n"
  },
  {
    "path": ".github/workflows/title-check.yaml",
    "content": "name: 'PR Title Check'\non:\n  pull_request:\n    # check when PR\n    # * is created,\n    # * title is edited, and\n    # * new commits are added (to ensure failing title blocks merging)\n    types: [opened, reopened, edited, synchronize]\n\njobs:\n  title-check:\n    name: Title check\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v6\n      - name: Check PR title\n        env:\n          PR_TITLE: ${{ github.event.pull_request.title }}\n        run: python .github/scripts/check_pr_title.py\n"
  },
  {
    "path": ".github/workflows/urls-check.yaml",
    "content": "name: 'URL Check'\non:\n  workflow_dispatch:\n  schedule:\n    - cron: 0 6 * * 1\n\njobs:\n  url-check:\n    name: URL check\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout Code\n        uses: actions/checkout@v6\n      - name: Setup Python\n        uses: actions/setup-python@v6\n        with:\n          python-version: '3.x'\n      - name: Install requirements\n        run:\n          pip install pyyaml Pillow\n      - name: Check URLs\n        run: |\n          export PYTHONPATH=\"$GITHUB_WORKSPACE\"\n          python .github/scripts/check_urls.py\n"
  },
  {
    "path": ".gitignore",
    "content": "*.pyc\n*.swp\n.DS_Store\n.tox\nbuild\n.idea/*\n*.egg-info/\ndist/*\n__pycache__/\n\n# in-project virtual environments\nvenv/\n.venv/\n\n# Code coverage artifacts\n.coverage*\ncoverage.xml\n\n# Editors / IDEs\n.vscode/\n\n# Docs\ndocs/_build/\n\n.cspell/\n\n# Files generated by some of the scripts\ndont_commit_*.pdf\npypdf-output.pdf\nannotated-pdf-link.pdf\nImage9.png\npypdf_pdfLocation.txt\n\n.python-version\ntests/pdf_cache/\ndocs/meta/CHANGELOG.md\ndocs/meta/CONTRIBUTORS.md\nextracted-images/\n\nRELEASE_COMMIT_MSG.md\nRELEASE_TAG_MSG.md\n.envrc\n"
  },
  {
    "path": ".gitmodules",
    "content": "[submodule \"sample-files\"]\n\tpath = sample-files\n\turl = https://github.com/py-pdf/sample-files\n"
  },
  {
    "path": ".pre-commit-config.yaml",
    "content": "# pre-commit run --all-files\nrepos:\n-   repo: https://github.com/pre-commit/pre-commit-hooks\n    rev: v6.0.0\n    hooks:\n    -   id: check-ast\n    -   id: check-case-conflict\n    -   id: check-docstring-first\n    -   id: check-yaml\n    -   id: debug-statements\n    -   id: end-of-file-fixer\n        exclude: \"resources/.*|docs/make.bat\"\n    -   id: fix-byte-order-marker\n    -   id: trailing-whitespace\n    -   id: mixed-line-ending\n        args: ['--fix=lf']\n        exclude: \"docs/make.bat\"\n    -   id: check-added-large-files\n        args: ['--maxkb=1000']\n\n-   repo: https://github.com/astral-sh/ruff-pre-commit\n    rev: v0.15.0\n    hooks:\n    -   id: ruff-check\n        args: ['--fix']\n\n-   repo: https://github.com/asottile/pyupgrade\n    rev: v3.21.2\n    hooks:\n    -   id: pyupgrade\n        args: [--py39-plus]\n\n-   repo: https://github.com/pre-commit/mirrors-mypy\n    rev: v1.17.1\n    hooks:\n      - id: mypy\n        files: ^pypdf/.*\n"
  },
  {
    "path": ".readthedocs.yaml",
    "content": "# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details\nversion: 2\n\n\nbuild:\n  os: ubuntu-lts-latest\n  tools:\n    python: \"latest\"\n\n# Build documentation in the \"docs/\" directory with Sphinx\nsphinx:\n   configuration: docs/conf.py\n\n# If using Sphinx, optionally build your docs in additional formats such as PDF\nformats: all\n\n# Optionally declare the Python requirements required to build your docs\npython:\n  install:\n    - requirements: requirements/docs.txt\n    - method: pip\n      path: .\n      extra_requirements:\n        - full\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# CHANGELOG\n\n## Version 6.9.1, 2026-03-17\n\n### Security (SEC)\n- Improve performance and limit length of array-based content streams (#3686)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.9.0...6.9.1)\n\n## Version 6.9.0, 2026-03-15\n\n### New Features (ENH)\n- Expose /Perms verification result on Encryption object (#3672)\n\n### Performance Improvements (PI)\n- Fix O(n²) performance in NameObject read/write (#3679)\n- Batch-parse all objects in ObjStm on first access (#3677)\n\n### Bug Fixes (BUG)\n- Avoid sharing array-based content streams between pages (#3681)\n- Avoid accessing invalid page when inserting blank page under some conditions (#3529)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.8.0...6.9.0)\n\n## Version 6.8.0, 2026-03-09\n\n### Security (SEC)\n- Limit allowed `/Length` value of stream  (#3675)\n\n### New Features (ENH)\n- Add /IRT (in-reply-to) support for markup annotations (#3631)\n\n### Documentation (DOC)\n- Avoid using `PageObject.replace_contents` on PdfReader (#3669)\n- Document how to disable jbig2dec calls\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.7.5...6.8.0)\n\n## Version 6.7.5, 2026-03-02\n\n### Security (SEC)\n- Improve the performance of the ASCIIHexDecode filter (#3666)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.7.4...6.7.5)\n\n## Version 6.7.4, 2026-02-27\n\n### Security (SEC)\n- Allow limiting output length for RunLengthDecode filter (#3664)\n\n### Robustness (ROB)\n- Deal with invalid annotations in extract_links (#3659)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.7.3...6.7.4)\n\n## Version 6.7.3, 2026-02-24\n\n### Security (SEC)\n- Use zlib decompression limit when retrieving XFA data (#3658)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.7.2...6.7.3)\n\n## Version 6.7.2, 2026-02-22\n\n### Security (SEC)\n- Prevent infinite loop from circular xref /Prev references (#3655)\n\n### Bug Fixes (BUG)\n- Fix wrong LUT size error (#3651)\n- Fix handling of page boxes defined on `/Pages` (#3650)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.7.1...6.7.2)\n\n## Version 6.7.1, 2026-02-17\n\n### Security (SEC)\n- Detect cyclic references when accessing TreeObject.children (#3645)\n- Limit size of `/ToUnicode` entries (#3646)\n- Limit FlateDecode recovery attempts (#3644)\n\n### Bug Fixes (BUG)\n- Avoid own object replacement logic in `PageObject.replace_contents` (#3638)\n- Fix UnboundLocalError when update_page_form_field_values with /Sig (#3634)\n\n### Robustness (ROB)\n- Avoid divison by zero when decoding FlateDecode PNG prediction (#3641)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.7.0...6.7.1)\n\n## Version 6.7.0, 2026-02-08\n\n### Deprecations (DEP)\n- Deprecate support for abbreviations in decode_stream_data (#3617)\n\n### New Features (ENH)\n- Add ability to add font resources for 14 Adobe Core fonts in text widget annotations (#3624)\n\n### Bug Fixes (BUG)\n- Avoid invalid load for ICCBased FlateDecode images in mode 1 (#3619)\n\n### Robustness (ROB)\n- Fix AESV2 decryption when /Length missing in encrypt dict (#3629)\n- Fix merging when annotations point to NullObject (#3613)\n- Check for `self._info` being None in `compress_identical_objects` (#3612)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.6.2...6.7.0)\n\n## Version 6.6.2, 2026-01-26\n\n### Security (SEC)\n- Detect cyclic references when retrieving outlines (#3610)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.6.1...6.6.2)\n\n## Version 6.6.1, 2026-01-25\n\n### Robustness (ROB)\n- `/AcroForm` might be NullObject (#3601)\n- Handle missing font bounding boxes gracefully (#3600)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.6.0...6.6.1)\n\n## Version 6.6.0, 2026-01-09\n\n### Security (SEC)\n- Improve handling of partially broken PDF files (#3594)\n\n### Deprecations (DEP)\n- Block common page content modifications when assigned to reader (#3582)\n\n### New Features (ENH)\n- Embellishments to generated text appearance streams (#3571)\n\n### Bug Fixes (BUG)\n- Do not consider multi-byte BOM-like sequences as BOMs (#3589)\n\n### Robustness (ROB)\n- Avoid empty FlateDecode outputs without warning (#3579)\n\n### Documentation (DOC)\n- Add outlines documentation and link it in User Guide (#3511)\n\n### Developer Experience (DEV)\n- Add PyPy 3.11 to test matrix and benchmarks (#3574)\n\n### Maintenance (MAINT)\n- Fix compatibility with Pillow >= 12.1.0 (#3590)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.5.0...6.6.0)\n\n## Version 6.5.0, 2025-12-21\n\n### New Features (ENH)\n- Limit jbig2dec memory usage (#3576)\n- FontDescriptor: Initiate from embedded font resource (#3551)\n\n### Robustness (ROB)\n- Allow fallback to PBM files for jbig2dec without PNG support (#3567)\n- Use warning instead of error for early EOD for RunLengthDecode (#3548)\n\n### Developer Experience (DEV)\n- Test with macOS as well (#3401)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.4.2...6.5.0)\n\n## Version 6.4.2, 2025-12-14\n\n### Bug Fixes (BUG)\n- Fix KeyError when flattening form field without /Font in resources (#3554)\n\n### Robustness (ROB)\n- Allow deleting non-existent annotations (#3559)\n\n### Documentation (DOC)\n- Fix level of attachment heading (#3560)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.4.1...6.4.2)\n\n## Version 6.4.1, 2025-12-07\n\n### Performance Improvements (PI)\n- Optimize loop for layout mode text extraction (#3543)\n\n### Bug Fixes (BUG)\n- Do not fail on choice field without /Opt key (#3540)\n\n### Documentation (DOC)\n- Document possible issues with merge_page and clipping (#3546)\n- Add some notes about library security (#3545)\n\n### Maintenance (MAINT)\n- Use CORE_FONT_METRICS for widths where possible (#3526)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.4.0...6.4.1)\n\n## Version 6.4.0, 2025-11-23\n\n### Security (SEC)\n- Reduce default limit for LZW decoding\n\n### New Features (ENH)\n- Parse and format comb fields in text widget annotations (#3519)\n\n### Robustness (ROB)\n- Silently ignore Adobe Ascii85 whitespace for suffix detection (#3528)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.3.0...6.4.0)\n\n## Version 6.3.0, 2025-11-16\n\n### New Features (ENH)\n- Wrap and align text in flattened PDF forms (#3465)\n\n### Bug Fixes (BUG)\n- Fix missing \"PreventGC\" when cloning (#3520)\n- Preserve JPEG image quality by default (#3516)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.2.0...6.3.0)\n\n## Version 6.2.0, 2025-11-09\n\n### New Features (ENH)\n- Add 'strict' parameter to PDFWriter (#3503)\n\n### Bug Fixes (BUG)\n- PdfWriter.append fails when there are articles being None (#3509)\n\n### Documentation (DOC)\n- Execute docs examples in CI (#3507)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.1.3...6.2.0)\n\n## Version 6.1.3, 2025-10-22\n\n### Security (SEC)\n- Allow limiting size of LZWDecode streams (#3502)\n- Avoid infinite loop when reading broken DCT-based inline images (#3501)\n\n### Bug Fixes (BUG)\n- PageObject.scale() scales media box incorrectly (#3489)\n\n### Robustness (ROB)\n- Fail with explicit exception when image mode is an empty array (#3500)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.1.2...6.1.3)\n\n## Version 6.1.2, 2025-10-19\n\n### Bug Fixes (BUG)\n- Fix handling of zero-length StreamObject (#3485)\n\n### Robustness (ROB)\n- Deal with wrong size for incremental PDF files (#3495)\n- Improve handling for malformed cross-reference tables (#3483)\n\n### Developer Experience (DEV)\n- Use released Python 3.14\n- Use Mapping instead of dict in type hint of update_page_form_field_values (#3490)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.1.1...6.1.2)\n\n## Version 6.1.1, 2025-09-28\n\n### Bug Fixes (BUG)\n- Insert new embedded files in a sorted manner (#3477)\n- Fix name tree handling for embedded files with Kids-based inputs (#3475)\n- Make embedding files not break PDF/A-3 compliance (#3472)\n\n### Documentation (DOC)\n- Document AFRelationship handling for PDF/A and provide constants (#3478)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.1.0...6.1.1)\n\n## Version 6.1.0, 2025-09-21\n\n### New Features (ENH)\n- Enhance XMP metadata handling with creation and setter methods (#3410)\n- Add all font metrics for base 14 Type 1 PDF fonts (#3363)\n- Allow deleting embedded files (#3461)\n- Add support for Python in FIPS mode for document identifier (#3438)\n\n### Bug Fixes (BUG)\n- Fix handling of UTF-16 encoded destination titles (#3463)\n- Guard empty input to prevent IndexError (#3448)\n\n### Developer Experience (DEV)\n- Fix type hint for XMP metadata setter to add bytes type (#3464)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/6.0.0...6.1.0)\n\n## Version 6.0.0, 2025-08-11\n\n### Security (SEC)\n- Limit decompressed size for FlateDecode filter (#3430)\n\n### Deprecations (DEP)\n- Drop Python 3.8 support (#3412)\n\n### New Features (ENH)\n- Move BlackIs1 functionality to tiff_header (#3421)\n\n### Robustness (ROB)\n- Skip Go-To actions without a destination (#3420)\n\n### Developer Experience (DEV)\n- Update code style related libraries (#3414)\n- Update mypy to 1.17.0 (#3413)\n- Stop testing on Python 3.8 and start testing on Python 3.14 (#3411)\n\n### Maintenance (MAINT)\n- Cleanup deprecations (#3424)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.9.0...6.0.0)\n\n## Version 5.9.0, 2025-07-27\n\n### New Features (ENH)\n- Automatically preserve links in added pages (#3298)\n- Allow writing/updating all properties of an embedded file (#3374)\n\n### Bug Fixes (BUG)\n- Fix XMP handling dropping indirect references (#3392)\n\n### Robustness (ROB)\n- Deal with DecodeParms being empty list (#3388)\n\n### Documentation (DOC)\n- Document how to read and modify XMP metadata (#3383)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.8.0...5.9.0)\n\n## Version 5.8.0, 2025-07-13\n\n### New Features (ENH)\n- Implement flattening for writer (#3312)\n\n### Bug Fixes (BUG)\n- Unterminated object when using PdfWriter with incremental=True (#3345)\n\n### Robustness (ROB)\n- Resolve some image extraction edge cases (#3371)\n- Ignore faulty trailing newline during RLE decoding (#3355)\n- Gracefully handle odd-length strings in parse_bfchar (#3348)\n\n### Developer Experience (DEV)\n- Modernize license specifiers (#3338)\n\n### Maintenance (MAINT)\n- Reduce max-complexity of tool.ruff.lint.mccabe (#3365)\n- Refactor text extraction code\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.7.0...5.8.0)\n\n## Version 5.7.0, 2025-06-29\n\n### Performance Improvements (PI)\n- Performance optimization for LZW decoding (#3329)\n\n### Robustness (ROB)\n- Flate decoding for streams with faulty tail bytes (#3332)\n- dc_creator could be a Bag as well (#3333)\n- Handle tree being NullObject when retrieving named destinations (#3331)\n\n### Maintenance (MAINT)\n- Move inline-image mappings to constants (#3328)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.6.1...5.7.0)\n\n## Version 5.6.1, 2025-06-22\n\n### New Features (ENH)\n- Add PDF/A XMP metadata support (#3314)\n\n### Robustness (ROB)\n- Deal with annotations not being lists on merge (#3321)\n- Handle NullObject for cmap encoding Differences entry (#3317)\n\n### Developer Experience (DEV)\n- Update ruff to 0.12.0 (#3316)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.6.0...5.6.1)\n\n## Version 5.6.0, 2025-06-01\n\n### New Features (ENH)\n- Add basic support for JBIG2 by using jbig2dec (#3163)\n\n### Bug Fixes (BUG)\n- Fix crashes by removing unnecessary line (#3293)\n- Add delimiters to NameObject.renumber_table (#3286)\n\n### Robustness (ROB)\n- Handle DecodeParms being a NullObject (#3285)\n\n### Code Style (STY)\n- Update to mypy 1.16.0 (#3300)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.5.0...5.6.0)\n\n## Version 5.5.0, 2025-05-11\n\n### New Features (ENH)\n- Add support for IndirectObject.__iter__ (#3228)\n- Allow filtering by font when removing text (#3216)\n\n### Bug Fixes (BUG)\n- Add missing named destinations being ByteStringObjects (#3282)\n- Get font information more reliably when removing text (#3252)\n- T* 2D Translation consistent with PDF 1.7 Spec (#3250)\n- Add font stack to q/Q operations in layout mode (#3225)\n- Avoid completely hiding image loading issues like exceeding image size limits (#3221)\n- Using compress_identical_objects on transformed content duplicates differing content (#3197)\n- Consider BlackIs1 parameter for CCITTFaxDecode filter (#3196)\n\n### Robustness (ROB)\n- Deal with insufficient cm matrix during text extraction (#3283)\n- Allow merging when annotations miss D entry (#3281)\n- Fix merging documents if there are no Dests (#3280)\n- Fix crash on malformed action in outline (#3278)\n- Fix compression issues for removed images which might be None (#3246)\n- Attempt to deal with non-rectangular FlateDecode streams (#3245)\n- Handle some None values for broken PDF files (#3230)\n\n### Developer Experience (DEV)\n- Multiple style improvements\n- Update ruff to 0.11.0\n\n### Maintenance (MAINT)\n- Conform ASCIIHexDecode implementation to specification (#3274)\n- Modify comments of filters that do not use decode_parms (#3260)\n\n### Code Style (STY)\n- Simplify warnings & debugging in layout mode text extraction (#3271)\n- Standardize mypy assert statements (#3276)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.4.0...5.5.0)\n\n## Version 5.4.0, 2025-03-16\n\n### New Features (ENH)\n- Add support for `IndirectObject.__contains__` (#3155)\n\n### Bug Fixes (BUG)\n- Fix detection of inline images followed by names or numbers (#3173)\n\n### Robustness (ROB)\n- Consider root objects without catalog type as fallback (#3175)\n- Raise proper error on infinite loop when reading objects (#3169)\n\n### Documentation (DOC)\n- Mention memory consumption of text extraction (#3168)\n\n### Developer Experience (DEV)\n- Upgrade to ruff 0.10.0 (#3191)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.3.1...5.4.0)\n\n## Version 5.3.1, 2025-03-02\n\n### Bug Fixes (BUG)\n- Use the correct name StandardEncoding for the predefined cmap (#3156)\n- Handle inline images containing `EI ` sequences (#3152)\n- Fix check box value which should be name object (#3124)\n- Fix stream position on inline image fallback extraction (#3120)\n- Fix object count for incremental writer (#3117)\n\n### Robustness (ROB)\n- Avoid index errors on empty lines in xref table (#3162)\n- Improve handling of LZW decoder table overflow (#3159)\n- Ignore non-numbers for width when building font width map (#3158)\n- Avoid negative seek values when reading partially broken files (#3157)\n\n### Documentation (DOC)\n- Fixed PageObject.images example usage for replacing image (#3149)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.3.0...5.3.1)\n\n## Version 5.3.0, 2025-02-09\n\n### New Features (ENH)\n- Handle attachments in /Kids and provide object-oriented API (#3108)\n\n### Bug Fixes (BUG)\n- Handle annotations being None on merging (#3111)\n\n### Robustness (ROB)\n- Prevent excessive layout mode text output from Type3 fonts (#3082)\n\n### Documentation (DOC)\n- stefan6419846 becomes BDFL of pypdf (#3078)\n- Tidy the visitor function description (#3086)\n\n### Developer Experience (DEV)\n- Remove ignoring multiple Ruff rules\n- Remove unused mutmut configuration (#3092)\n\n### Testing (TST)\n- Fix warning assertions to use `pytest.warns()` (#3083)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.2.0...5.3.0)\n\n## Version 5.2.0, 2025-01-26\n\n### Deprecations (DEP)\n- Deprecate with replacement CCITParameters (#3019)\n- Correct deprecation of interiour_color (#2947)\n\n### New Features (ENH)\n- Support alternative (U)F names for embedded file retrieval (#3072)\n- Adding support for reading .metadata.keywords (#2939)\n\n### Bug Fixes (BUG)\n- Handle further Tf operators in text extraction layout mode (#3073)\n- Ensure `add_metadata` can deal with `_info = None` (#3040)\n- Handle IndirectObject in CCITTFaxDecode filter (#2965)\n- Handle chained colorspace for inline images when no filter is set (#3008)\n- Avoid extracting inline images twice and dropping other operators (#3002)\n- Fixed reference of value with `str.__new__` in TextStringObject (#2952)\n- Handle indirect objects in font width calculations (#2967)\n- Title sometimes is bytes and not str (#2930)\n- Fix undefined variable for text extraction (regression) (#2934)\n- Don't close stream passed to PdfWriter.write() (#2909)\n\n### Robustness (ROB)\n- Handle zero height fonts when extracting text (#3075)\n- Deal with content streams not containing streams (#3005)\n- Gracefully handle some text operators when the operands are missing (#3006)\n- Fall back to non-Adobe Ascii85 format for missing end markers (#3007)\n- Ignore odd-length strings when processing cmap lines (#3009)\n- Skip annotation destination being NullObject in PdfWriter (#2964)\n- Skip destination page being None in PdfWriter (#2963)\n- Fix infinite loop case when reading null objects within an Array\n- Fixing infinite loop in ArrayObject read_from_stream (#2928)\n\n### Documentation (DOC)\n- Add note about default line colors (#3014)\n\n### Developer Experience (DEV)\n- Remove ignoring Ruff rule PGH004 (#3071)\n- Tidy ignore array in tool.ruff.lint (#3069)\n- Move Windows CI to Python 3.13 (#3003)\n- Move to Ubuntu 22.04 (#3004)\n\n### Maintenance (MAINT)\n- Fix formatting of warning message and include exception message (#3076)\n- Narrow return type for `ContentStream.operations` (#2941)\n\n### Testing (TST)\n- Fix image similarity for upcoming Ubuntu 24.04 (#3039)\n- Replace broken Apache Tika Corpora urls (#3041)\n\n### Code Style (STY)\n- Add form feed to WHITESPACES (#3054)\n- Lots of small internal changes\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.1.0...5.2.0)\n\n## Version 5.1.0, 2024-10-27\n\n### New Features (ENH)\n- Add `layout_mode_font_height_weight` argument to `PageObject.extract_text()` (#2920)\n\n### Bug Fixes (BUG)\n- Fix font specificier for FreeText annotation (#2893)\n- Line breaks are not generated due to incorrect calculation of text leading (#2890)\n- Improve handling of spaces in text extraction (#2882)\n\n### Robustness (ROB)\n- Soft failure for flate encode image mode 1 with wrong LUT size (#2900)\n\n### Documentation (DOC)\n- Use latest package versions (#2907)\n- Correct example of reading FileAttachment annotation (#2906)\n\n### Developer Experience (DEV)\n- Update pinned requirements (#2918)\n- Make make_release.py compatible with Windows environment (#2894)\n\n### Maintenance (MAINT)\n- Remove references to outdated Python versions (#2919)\n- Generalize the method of obtaining space_code (#2891)\n- Unnecessary character mapping process (#2888)\n- New LZW decoding implementation (#2887)\n\n### Testing (TST)\n- Add LzwCodec for encoding (#2883)\n\n### Code Style (STY)\n- Capitalize error messages (#2903)\n- Modify error messages in PdfWriter (#2902)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.0.1...5.1.0)\n\n## Version 5.0.1, 2024-09-29\n\n### New Features (ENH)\n- Add `full` parameter to PdfWriter constructor (#2865)\n\n### Bug Fixes (BUG)\n- Update pyproject.toml with minimum Python version of 3.8 (#2859)\n- Cope with unbalanced delimiters in dictionary object (#2878)\n- Cope with encoding with too many differences (#2873)\n- Missing spaces in extract_text() method (#1328) (#2868)\n- Tolerate truncated files and no warning when jumping startxref (#2855)\n\n### Robustness (ROB)\n- Repair PDF with invalid Root object (#2880)\n- Continue parsing dictionary object when error is detected (#2872)\n- Merge documents with invalid pages in named destinations (#2857)\n- Tolerate comments in arrays (#2856)\n\n### Developer Experience (DEV)\n- Use latest Python version for benchmarking (#2879)\n\n### Maintenance (MAINT)\n- Add tests to source distributions (#2874)\n- Refactor _update_field_annotation (#2862)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/5.0.0...5.0.1)\n\n## Version 5.0.0, 2024-09-15\n\nThis version drops support for Python 3.7 (not maintained since July 2023), PdfMerger (use PdfWriter instead) and AnnotationBuilder (use annotations instead).\n\n### Deprecations (DEP)\n- Remove the deprecated PdfMerger and AnnotationBuilder classes and other deprecations cleanup (#2813)\n- Drop Python 3.7 support (#2793)\n\n### New Features (ENH)\n- Add capability to remove /Info from PDF (#2820)\n- Add incremental capability to PdfWriter (#2811)\n- Add UniGB-UTF16 encodings (#2819)\n- Accept utf strings for metadata (#2802)\n- Report PdfReadError instead of RecursionError (#2800)\n- Compress PDF files merging identical objects (#2795)\n\n### Bug Fixes (BUG)\n- Fix sheared image (#2801)\n\n### Robustness (ROB)\n- Robustify .set_data() (#2821)\n- Raise PdfReadError when missing /Root in trailer (#2808)\n- Fix extract_text() issues on damaged PDFs (#2760)\n- Handle images with empty data when processing an image from bytes (#2786)\n\n### Developer Experience (DEV)\n- Fix coverage uploads (#2832)\n- Test against Python 3.13 (#2776)\n\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/4.3.1...5.0.0)\n\n## Version 4.3.1, 2024-07-21\n\n### Bug Fixes (BUG)\n- Cope with Matrix entry in field annotations (#2736)\n\n### Robustness (ROB)\n- Cope with fields with upside down box/rectangle (#2729)\n\n### Maintenance (MAINT)\n- Add deprecate_with_replacement to StreamObject.initializeFromD… (#2728)\n- Deal with cryptography>=43 moving ARC4 (#2765)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/4.3.0...4.3.1)\n\n## Version 4.3.0, 2024-06-23\n\n### New Features (ENH)\n- Accept ETen-B5 and UniCNS-UTF16 encodings (#2721)\n- Add decode_as_image() to ContentStreams (#2615)\n- context manager for PdfReader (#2666)\n- Add capability to set font and size in fields (#2636)\n- Allow to pass input file without named argument (#2576)\n\n### Bug Fixes (BUG)\n- Fix deprecation for Ressources when using old constants (#2705)\n- Fix images issue 4 bits encoding and LUT starting with UTF16_BOM (#2675)\n- Reading large compressed images takes huge time to process (#2644)\n- Highlighted Text Cannot Be Printed (#2604)\n- Fix UnboundLocalError on malformed pdf (#2619)\n\n### Robustness (ROB)\n- Cope with missing Standard 14 fonts in fields (#2677)\n- Improve inline image extraction (#2622)\n- Cope with loops in Fields tree (#2656)\n- Discard /I in choice fields for compatibility with Acrobat (#2614)\n- Cope with some issues in pillow (#2595)\n- Cope with some image extraction issues (#2591)\n\n### Documentation (DOC)\n- Various improvements on docstrings and examples\n\n### Maintenance (MAINT)\n- Deprecate interiour_color with replacement interior_color (#2706)\n- Add deprecate_with_replacement to PdfWriter.find_bookmark (#2674)\n\n### Code Style (STY)\n- Change Link to be a non-markup annotation (#2714)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/4.2.0...4.3.0)\n\n## Version 4.2.0, 2024-04-07\n\n### New Features (ENH)\n- Allow multiple charsets for NameObject.read_from_stream (#2585)\n- Add support for /Kids in page labels (#2562)\n- Allow to update fields on many pages (#2571)\n- Tolerate PDF with invalid xref pointed objects (#2335)\n- Add Enforce from PDF2.0 in viewer_preferences (#2511)\n- Add += and -= operators to ArrayObject (#2510)\n\n### Bug Fixes (BUG)\n- Fix merge_page sometimes generating unknown operator 'QQ' (#2588)\n- Fix fields update where annotations are kids of field (#2570)\n- Process CMYK images without a filter correctly (#2557)\n- Extract text in layout mode without finding resources (#2555)\n- Prevent recursive loop in some PDF files (#2505)\n\n### Robustness (ROB)\n- Tolerate \"truncated\" xref (#2580)\n- Replace error by warning for EOD in RunLengthDecode/ASCIIHexDecode (#2334)\n- Rebuild xref table if one entry is invalid (#2528)\n- Robustify stream extraction (#2526)\n\n### Documentation (DOC)\n- Update release process for latest changes (#2564)\n- Encryption/decryption: Clone document instead of copying all pages (#2546)\n- Minor improvements (#2542)\n- Update annotation list (#2534)\n- Update references and formatting (#2529)\n- Correct threads reference, plus minor changes (#2521)\n- Minor readability increases (#2515)\n- Simplify PaperSize examples (#2504)\n- Minor improvements (#2501)\n\n### Developer Experience (DEV)\n- Remove unused dependencies (#2572)\n- Remove page labels PR link from message (#2561)\n- Fix changelog generator regarding whitespace and handling of \"Other\" group (#2492)\n- Add REL to known PR prefixes (#2554)\n- Release using the REL commit instead of git tag (#2500)\n- Unify code between PdfReader and PdfWriter (#2497)\n- Bump softprops/action-gh-release from 1 to 2 (#2514)\n\n### Maintenance (MAINT)\n- Ressources → Resources (and internal name childs) (#2550)\n- Fix typos found by codespell (#2549)\n- Update Read the Docs configuration (#2538)\n- Add root_object, _info and _ID to PdfReader (#2495)\n\n### Testing (TST)\n- Allow loading truncated images if required (#2586)\n- Fix download issues from #2562 (#2578)\n- Improve test_get_contents_from_nullobject to show real use-case (#2524)\n- Add missing test annotations (#2507)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/4.1.0...4.2.0)\n\n## Version 4.1.0, 2024-03-03\n\nGenerating name objects (`NameObject`) without a leading slash\nis considered deprecated now. Previously, just a plain warning\nwould be logged, leading to possibly invalid PDF files. According\nto our deprecation policy, this will log a *DeprecationWarning*\nfor now.\n\n### New Features (ENH)\n- Add get_pages_from_field  (#2494)\n- Add reattach_fields function (#2480)\n- Automatic access to pointed object for IndirectObject (#2464)\n\n### Bug Fixes (BUG)\n- Missing error on name without leading / (#2387)\n- encode_pdfdocencoding() always returns bytes (#2440)\n- BI in text content identified as image tag (#2459)\n\n### Robustness (ROB)\n- Missing basefont entry in type 3 font (#2469)\n\n### Documentation (DOC)\n- Improve lossless compression example (#2488)\n- Amend robustness documentation (#2479)\n\n### Developer Experience (DEV)\n- Fix changelog for UTF-8 characters (#2462)\n\n### Maintenance (MAINT)\n- Add _get_page_number_from_indirect in writer (#2493)\n- Remove user assignment for feature requests (#2483)\n- Remove reference to old 2.0.0 branch (#2482)\n\n### Testing (TST)\n- Fix benchmark failures (#2481)\n- Broken test due to expired test file URL (#2468)\n- Resolve file naming conflict in test_iss1767 (#2445)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/4.0.2...4.1.0)\n\n## Version 4.0.2, 2024-02-18\n\n### Bug Fixes (BUG)\n-  Use NumberObject for /Border elements of annotations (#2451)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/4.0.1...4.0.2)\n\n## Version 4.0.1, 2024-01-28\n\n### Bug Fixes (BUG)\n-  layout mode text extraction ZeroDivisionError (#2417)\n\n### Testing (TST)\n-  Skip tests using fpdf2 if it's not installed (#2419)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/4.0.0...4.0.1)\n\n## Version 4.0.0, 2024-01-19\n\n### Deprecations (DEP)\n-  Drop Python 3.6 support (#2369)\n-  Remove deprecated code (#2367)\n-  Remove deprecated XMP properties (#2386)\n\n### New Features (ENH)\n-  Add \"layout\" mode for text extraction (#2388)\n-  Add Jupyter Notebook integration for PdfReader (#2375)\n-  Improve/rewrite PDF permission retrieval (#2400)\n\n### Bug Fixes (BUG)\n-  PdfWriter.add_uri was setting the wrong type (#2406)\n-  Add support for GBK2K cmaps (#2385)\n\n### Maintenance (MAINT)\n-  Return None instead of -1 when page is not attached (#2376)\n-  Complete FileSpecificationDictionaryEntries constants (#2416)\n-  Replace warning with logging.error (#2377)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.17.4...4.0.0)\n\n## Version 3.17.4, 2023-12-24\n\n### Bug Fixes (BUG)\n-  Handle IndirectObject as image filter (#2355)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.17.3...3.17.4)\n\n## Version 3.17.3, 2023-12-17\n\n### Robustness (ROB)\n-  Out-of-bounds issue in handle_tj (text extraction) (#2342)\n\n### Developer Experience (DEV)\n-  Make make_release.py easier to configure (#2348)\n\n### Maintenance (MAINT)\n-  Bump actions/download-artifact from 3 to 4 (#2344)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.17.2...3.17.3)\n\n## Version 3.17.2, 2023-12-10\n\n### Bug Fixes (BUG)\n-  Cope with deflated images with CMYK Black Only (#2322)\n-  Handle indirect objects as parameters for CCITTFaxDecode (#2307)\n-  check words length in _cmap type1_alternative function (#2310)\n\n### Robustness (ROB)\n-  Relax flate decoding for too many lookup values (#2331)\n-  Let _build_destination skip in case of missing /D key (#2018)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.17.1...3.17.2)\n\n## Version 3.17.1, 2023-11-14\n\n### Bug Fixes (BUG)\n-  Mediabox expansion size when applying non-right angle rotation (#2282)\n\n### Robustness (ROB)\n-  MissingWidth is IndirectObject (#2288)\n-  Initialize states array with an empty value (#2280)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.17.0...3.17.1)\n\n## Version 3.17.0, 2023-10-29\n\n### Security (SEC)\n-  Infinite recursion when using PdfWriter(clone_from=reader) (#2264)\n\n### New Features (ENH)\n-  Add parameter to select images to be removed (#2214)\n\n### Bug Fixes (BUG)\n-  Correctly handle image mode 1 with FlateDecode (#2249)\n-  Error when filling a value with parentheses #2268 (#2269)\n-  Handle empty root outline (#2239)\n\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.16.4...3.17.0)\n\n## Version 3.16.4, 2023-10-10\n\n### Bug Fixes (BUG)\n-  Avoid exceeding recursion depth when retrieving image mode (#2251)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.16.3...3.16.4)\n\n## Version 3.16.3, 2023-10-08\n\n### Bug Fixes (BUG)\n-  Invalid cm/tm in visitor functions (#2206)\n-  Encrypt / decrypt Stream object dictionaries (#2228)\n-  Support nested color spaces for the /DeviceN color space (#2241)\n-  Images property fails if NullObject in list (#2215)\n\n### Developer Experience (DEV)\n-  Unify mypy options and warn redundant workarounds (#2223)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.16.2...3.16.3)\n\n## Version 3.16.2, 2023-09-24\n\n### Bug Fixes (BUG)\n-  PDF size increases because of too high float writing precision (#2213)\n-  Fix test_watermarking_reportlab_rendering() (#2203)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.16.1...3.16.2)\n\n## Version 3.16.1, 2023-09-17\n\n⚠️ The 'rename PdfWriter.create_viewer_preference to\nPdfWriter.create_viewer_preferences (#2190)' could be a breaking change for you,\nif you use it. As it was only introduced last week I'm confident enough that\nnobody will be affected though. Hence only the patch update.\n\n### Bug Fixes (BUG)\n-  Missing new line in extract_text with cm operations (#2142)\n-  _get_fonts not processing properly CIDFonts and annotations (#2194)\n\n### Maintenance (MAINT)\n-  Rename PdfWriter.create_viewer_preference to PdfWriter.create_viewer_preferences (#2190)\n\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.16.0...3.16.1)\n\n## Version 3.16.0, 2023-09-10\n\n### Security (SEC)\n-  Infinite recursion caused by IndirectObject clone (#2156)\n\n### New Features (ENH)\n-  Ease access to ViewerPreferences (#2144)\n\n### Bug Fixes (BUG)\n-  Catch the case where w[0] is an IndirectObject instead of an int (#2154)\n-  Cope with indirect objects in filters and remove deprecated code (#2177)\n-  Accept tabs in cmaps (#2174) / cope with extra space (#2151)\n-  Merge pages without resources (#2150)\n-  getcontents() shall return None if contents is NullObject (#2161)\n-  Fix conversion from 1 to LA (#2175)\n\n### Robustness (ROB)\n-  Accept XYZ with no arguments (#2178)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.15.5...3.16.0)\n\n## Version 3.15.5, 2023-09-03\n\n### Bug Fixes (BUG)\n-  Cope with missing /I in articles (#2134)\n-  Fix image look-up table in EncodedStreamObject (#2128)\n-  remove_images not operating in sub level forms (#2133)\n\n### Robustness (ROB)\n-  Cope with damaged PDF (#2129)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.15.4...3.15.5)\n\n## Version 3.15.4, 2023-08-27\n\n### Performance Improvements (PI)\n-  Making pypdf as fast as pdfrw (#2086)\n\n### Maintenance (MAINT)\n-  Relax typing_extensions version (#2104)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.15.3...3.15.4)\n\n## Version 3.15.3, 2023-08-26\n\n### Bug Fixes (BUG)\n-  Check version of crypt provider (#2115)\n-  TypeError: can't concat str to bytes (#2114)\n-  Require flit_core >= 3.9 (#2091)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.15.2...3.15.3)\n\n## Version 3.15.2, 2023-08-20\n\n### Security (SEC)\n-  Avoid endless recursion of reading damaged PDF file (#2093)\n\n### Performance Improvements (PI)\n-  Reuse content stream (#2101)\n\n### Maintenance (MAINT)\n-  Make ParseError inherit from PyPdfError (#2097)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.15.1...3.15.2)\n\n## Version 3.15.1, 2023-08-13\n\n### Performance Improvements (PI)\n-  optimize _decode_png_prediction (#2068)\n\n### Bug Fixes (BUG)\n-  Fix incorrect tm_matrix in call to visitor_text (#2060)\n-  Writing German characters into form fields (#2047)\n-  Prevent stall when accessing image in corrupted pdf (#2081)\n-  append() fails when articles do not have /T (#2080)\n\n### Robustness (ROB)\n-  Cope with xref not followed by separator (#2083)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.15.0...3.15.1)\n\n## Version 3.15.0, 2023-08-06\n\n### New Features (ENH)\n-  Add `level` parameter to compress_content_streams (#2044)\n-  Process /uniHHHH for text_extract (#2043)\n\n### Bug Fixes (BUG)\n-  Fix AnnotationBuilder.link (#2066)\n-  JPX image without ColorSpace  (#2062)\n-  Added check for field /Info when cloning reader document (#2055)\n-  Fix indexed/CMYK images (#2039)\n\n### Maintenance (MAINT)\n-  Cryptography as primary dependency (#2053)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.14.0...3.15.0)\n\n## Version 3.14.0, 2023-07-29\n\n### New Features (ENH)\n-  Accelerate image list keys generation (#2014)\n-  Use `cryptography` for encryption/decryption as a fallback for PyCryptodome (#2000)\n-  Extract LaTeX characters (#2016)\n-  ASCIIHexDecode.decode now returns bytes instead of str (#1994)\n\n### Bug Fixes (BUG)\n-  Add RunLengthDecode filter (#2012)\n-  Process /Separation ColorSpace (#2007)\n-  Handle single element ColorSpace list (#2026)\n-  Process lookup decoded as TextStringObjects (#2008)\n\n### Robustness (ROB)\n-  Cope with garbage collector during cloning (#1841)\n\n### Maintenance (MAINT)\n-  Cleanup of annotations (#1745)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.13.0...3.14.0)\n\n## Version 3.13.0, 2023-07-23\n\n### New Features (ENH)\n-  Add is_open in outlines in PdfReader and PdfWriter (#1960)\n\n### Bug Fixes (BUG)\n-  Search /DA in hierarchy fields (#2002)\n-  Cope with different ISO date length (#1999)\n-  Decode Black only/CMYK deviceN images (#1984)\n-  Process CMYK in deflate images (#1977)\n\n### Developer Experience (DEV)\n-  Add mypy to pre-commit (#2001)\n-  Release automation (#1991, #1985)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.12.2...3.13.0)\n\n## Version 3.12.2, 2023-07-16\n\n### Bug Fixes (BUG)\n-  Accept calRGB and calGray color_spaces (#1968)\n-  Process 2bits and 4bits images (#1967)\n-  Check for AcroForm and ensure it is not None (#1965)\n\n### Developer Experience (DEV)\n-  Automate the release process (#1970)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.12.1...3.12.2)\n\n## Version 3.12.1, 2023-07-09\n\n### Bug Fixes (BUG)\n-  Prevent updating page contents after merging page (stamping/watermarking) (#1952)\n-  % to be hex encoded in names (#1958)\n-  Inverse color in CMYK images (#1947)\n-  Dates conversion not working with Z00\\'00\\' (#1946)\n-  Support UTF-16-LE Strings (#1884)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.12.0...3.12.1)\n\n## Version 3.12.0, 2023-07-02\n\n### New Features (ENH)\n-  Add AES support for encrypting PDF files (#1918, #1935, #1936, #1938)\n-  Add page deletion feature to PdfWriter (#1843)\n\n### Bug Fixes (BUG)\n-  PdfReader.get_fields() attempts to delete non-existing index \"/Off\" (#1933)\n-  Remove unused objects when cloning_from (#1926)\n-  Add the TK.SIZE into the trailer (#1911)\n-  add_named_destination() maintains named destination list sort order (#1930)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.11.1...3.12.0)\n\n## Version 3.11.1, 2023-06-25\n\n### Bug Fixes (BUG)\n- Cascaded filters in image objects (#1913)\n- Append pdf with named destination using numbers for pages (#1858)\n- Ignore \"/B\" fields only on pages in PdfWriter.append() (#1875)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.11.0...3.11.1)\n\n## Version 3.11.0, 2023-06-23\n\n### New Features (ENH)\n-  Add page_number property (#1856)\n\n### Bug Fixes (BUG)\n- File expansion when updating with Page Contents (#1906)\n- Missing Alternate in indexed/ICCbased colorspaces (#1896)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.10.0...3.11.0)\n\n## Version 3.10.0, 2023-06-18\n\n### New Features (ENH)\n-  Extraction of inline images (#1850)\n-  Add capability to replace image (#1849)\n-  Extend images interface by returning an ImageFile(File) class (#1848)\n-  Add set_data to EncodedStreamObject (#1854)\n\n### Bug Fixes (BUG)\n-  Fix RGB FlateEncode Images(PNG) and transparency (#1834)\n-  Generate static appearance for fields (#1864)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.9.1...3.10.0)\n\n## Version 3.9.1, 2023-06-04\n\n### Deprecations (DEP)\n-  Deprecate PdfMerger (#1866)\n\n### Bug Fixes (BUG)\n-  Ignore UTF-8 decode errors (#1865)\n\n### Robustness (ROB)\n-  Handle missing /Type entry in Page tree (#1859)\n\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.9.0...3.9.1)\n\n## Version 3.9.0, 2023-05-21\n\n### New Features (ENH)\n-  Simplify metadata input (Document Information Dictionary) (#1851)\n-  Extend cmap compatibility to GBK_EUC_H/V (#1812)\n\n### Bug Fixes (BUG)\n-  Prevent infinite loop when no character follows after a comment (#1828)\n-  get_contents does not return ContentStream (#1847)\n-  Accept XYZ destination with zoom missing (default to zoom=0.0) (#1844)\n-  Cope with 1 Bit images (#1815)\n\n### Robustness (ROB)\n-  Handle missing /Type entry in Page tree (#1845)\n\n### Documentation (DOC)\n-  Expand file size explanations (#1835)\n-  Add comparison with pdfplumber (#1837)\n-  Clarify that PyPDF2 is dead (#1827)\n-  Add Hunter King as Contributor for #1806\n\n### Maintenance (MAINT)\n-  Refactor internal Encryption class (#1821)\n-  Add R parameter to generate_values (#1820)\n-  Make encryption_key parameter of write_to_stream optional (#1819)\n-  Prepare for adding AES encryption support (#1818)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.8.1...3.9.0)\n\n\n## Version 3.8.1, 2023-04-23\n\n### Bug Fixes (BUG)\n-  Convert color space before saving (#1802)\n\n### Documentation (DOC)\n-  PDF/A (#1807)\n-  Use append instead of add_page\n-  Document core mechanics of pypdf (#1783)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.8.0...3.8.1)\n\n## Version 3.8.0, 2023-04-16\n\n### New Features (ENH)\n-  Add transform method to Transformation class (#1765)\n-  Cope with UC2 fonts in text_extraction (#1785)\n\n### Robustness (ROB)\n-  Invalid startxref pointing 1 char before (#1784)\n\n### Maintenance (MAINT)\n-  Mark code handling old parameters as deprecated (#1798)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.7.1...3.8.0)\n\n\n## Version 3.7.1, 2023-04-09\n\n### Security (SEC)\n-  Warn about PDF encryption security (#1755)\n\n### Robustness (ROB)\n-  Prevent loop in Cloning (#1770)\n-  Capture UnicodeDecodeError at PdfReader.pdf_header (#1768)\n\n### Documentation (DOC)\n-  Add .readthedocs.yaml and bump docs dependencies using `tox -e deps` (#1750, #1752)\n\n### Developer Experience (DEV)\n-  Make make_changelog.py idempotent\n\n### Maintenance (MAINT)\n-  Move generation of file identifiers to a method (#1760)\n\n### Testing (TST)\n-  Add xmp test (#1775)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.7.0...3.7.1)\n\n## Version 3.7.0, 2023-03-26\n\n### Security (SEC)\n-  Use Python's secrets module instead of random module (#1748)\n\n### New Features (ENH)\n-  Add AnnotationBuilder.highlight text markup annotation (#1740)\n-  Add AnnotationBuilder.popup (#1665)\n-  Add AnnotationBuilder.polyline annotation support (#1726)\n-  Add clone_from parameter in PdfWriter constructor (#1703)\n\n### Bug Fixes (BUG)\n-  'DictionaryObject' object has no attribute 'indirect_reference' (#1729)\n\n### Robustness (ROB)\n-  Handle params NullObject in decode_stream_data (#1738)\n\n### Documentation (DOC)\n-  Project scope (#1743)\n\n### Maintenance (MAINT)\n-  Add AnnotationFlag (#1746)\n-  Add LazyDict.__str__ (#1727)\n\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.6.0...3.7.0)\n\n\n## Version 3.6.0, 2023-03-18\n\n### New Features (ENH)\n-  Extend PdfWriter.append() to PageObjects (#1704)\n-  Support qualified names in update_page_form_field_values (#1695)\n\n### Robustness (ROB)\n-  Tolerate streams without length field (#1717)\n-  Accept DictionaryObject in /D of NamedDestination (#1720)\n-  Widths def in cmap calls IndirectObject (#1719)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.5.2...3.6.0)\n\n\n## Version 3.5.2, 2023-03-12\n\n⚠️ We discovered that compress_content_stream has to be applied to a page of\n  the PdfWriter. It may not be applied to a page of the PdfReader!\n\n### Bug Fixes (BUG)\n-  compress_content_stream not readable in Adobe Acrobat (#1698)\n-  Pass logging parameters correctly in set_need_appearances_writer (#1697)\n-  Write /Root/AcroForm in set_need_appearances_writer (#1639)\n\n### Robustness (ROB)\n-  Allow more whitespaces within linearized file (#1701)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.5.1...3.5.2)\n\n\n## Version 3.5.1, 2023-03-05\n\n### Robustness (ROB)\n-  Some attributes not copied in DictionaryObject._clone (#1635)\n-  Allow merging multiple time pages with annots (#1624)\n\n### Testing (TST)\n-  Replace pytest.mark.external by enable_socket (#1657)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.5.0...3.5.1)\n\n## Version 3.5.0, 2023-02-26\n\n### New Features (ENH)\n-  Add reader.attachments public interface (#1611, #1661)\n-  Add PdfWriter.remove_objects_from_page(page: PageObject, to_delete: ObjectDeletionFlag) (#1648)\n-  Allow free-text annotation to have transparent border/background (#1664)\n\n### Bug Fixes (BUG)\n-  Allow decryption with empty password for AlgV5 (#1663)\n-  Let PdfWriter.pages return PageObject after calling `clone_document_from_reader()` (#1613)\n-  Invalid font pointed during merge_resources (#1641)\n\n### Robustness (ROB)\n-  Cope with invalid objects in IndirectObject.clone (#1637)\n-  Improve tolerance to invalid Names/Dests (#1658)\n-  Decode encoded values in get_fields (#1636)\n-  Let PdfWriter.merge cope with missing \"/Fields\" (#1628)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.4.1...3.5.0)\n\n\n## Version 3.4.1, 2023-02-12\n\n### Bug Fixes (BUG)\n-  Switch from trimbox to cropbox when merging pages (#1622)\n-  Text extraction not working with one glyph to char sequence (#1620)\n\n### Robustness (ROB)\n-  Fix 2 cases of \"object has no attribute \\'indirect_reference\\'\" (#1616)\n\n### Testing (TST)\n-  Add multiple retry on get_url for external PDF downloads (#1626)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.4.0...3.4.1)\n\n## Version 3.4.0, 2023-02-05\n\nNOTICE: pypdf changed the way it represents numbers parsed from PDF files.\n  pypdf<3.4.0 represented numbers as Decimal, pypdf>=3.4.0 represents them as\n  floats. Several other PDF libraries to this, as well as many PDF viewers.\n  We hope to fix issues with too high precision like this and get a speed boost.\n  In case your PDF documents rely on more than 18 decimals of precision you\n  should check if it still works as expected.\n  To clarify: This does not affect the text shown in PDF documents. It affects\n  numbers, e.g. when graphics are drawn on the PDF or very exact positions are\n  used. Typically, 5 decimals should be enough.\n\n### New Features (ENH)\n-  Enable merging forms with overlapping names (#1553)\n-  Add 'over' parameter to merge_transformend_page & co (#1567)\n\n### Bug Fixes (BUG)\n-  Fix getter of the PageObject.rotation property with an indirect object (#1602)\n-  Restore merge_transformed_page & co (#1567)\n-  Replace decimal by float (#1563)\n\n### Robustness (ROB)\n-  PdfWriter.remove_images: /Contents might not be in page_ref (#1598)\n\n### Developer Experience (DEV)\n-  Introduce ruff (#1586, #1609)\n\n### Maintenance (MAINT)\n-  Remove decimal (#1608)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.3.0...3.4.0)\n\n## Version 3.3.0, 2023-01-22\n\n### New Features (ENH)\n-  Add page label support to PdfWriter (#1558)\n-  Accept inline images with space before EI (#1552)\n-  Add circle annotation support (#1556)\n-  Add polygon annotation support (#1557)\n-  Make merging pages produce a deterministic PDF (#1542, #1543)\n\n### Bug Fixes (BUG)\n-  Fix error in cmap extraction (#1544)\n-  Remove erroneous assertion check (#1564)\n-  Fix dictionary access of optional page label keys (#1562)\n\n### Robustness (ROB)\n-  Set ignore_eof=True for read_until_regex (#1521)\n\n### Documentation (DOC)\n-  Paper size (#1550)\n\n### Developer Experience (DEV)\n-  Fix broken combination of dependencies of docs.txt\n-  Annotate tests appropriately (#1551)\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.2.1...3.3.0)\n\n\n## Version 3.2.1, 2023-01-08\n\n### Bug Fixes (BUG)\n-  Accept hierarchical fields (#1529)\n\n### Documentation (DOC)\n-  Use google style docstrings (#1534)\n-  Fix linked markdown documents (#1537)\n\n### Developer Experience (DEV)\n-  Update docs config (#1535)\n\n## Version 3.2.0, 2022-12-31\n\n### Performance Improvement (PI)\n-  Help the specializing adaptive interpreter (#1522)\n\n### New Features (ENH)\n-  Add support for page labels (#1519)\n\n### Bug Fixes (BUG)\n-  upgrade clone_document_root (#1520)\n\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.1.0...3.1.1)\n\n## Version 3.1.0, 2022-12-23\n\nMove PyPDF2 to pypdf (#1513). This now it's all lowercase, no number in the\nname. For installation and for import. PyPDF2 will no longer receive updates.\nThe community should move back to its roots.\n\nIf you were still using pyPdf or PyPDF2 < 2.0.0, I recommend reading the\nmigration guide: https://pypdf.readthedocs.io/en/latest/user/migration-1-to-2.html\n\npypdf==3.1.0 is only different from PyPDF2==3.0.0 in the package name.\nReplacing \"PyPDF2\" by \"pypdf\" should be enough if you migrate from\n`PyPDF2==3.0.0` to `pypdf==3.1.0`.\n\n[Full Changelog](https://github.com/py-pdf/pypdf/compare/3.0.0...3.1.0)\n\n## Version 3.0.0, 2022-12-22\n\n### BREAKING CHANGES ⚠️\n-  Deprecate features with PyPDF2==3.0.0 (#1489)\n-  Refactor Fit / Zoom parameters (#1437)\n\n### New Features (ENH)\n-  Add Cloning  (#1371)\n-  Allow int for indirect_reference in PdfWriter.get_object (#1490)\n\n### Documentation (DOC)\n-  How to read PDFs from S3 (#1509)\n-  Make MyST parse all links as simple hyperlinks (#1506)\n-  Changed 'latest' for 'stable' generated docs (#1495)\n-  Adjust deprecation procedure (#1487)\n\n### Maintenance (MAINT)\n-  Use typing.IO for file streams (#1498)\n\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.12.1...3.0.0)\n\n## Version 2.12.1, 2022-12-10\n\n### Documentation (DOC)\n-  Deduplicate extract_text docstring (#1485)\n-  How to cite PyPDF2 (#1476)\n\n### Maintenance (MAINT)\nConsistency changes:\n  -  indirect_ref/ido ➔ indirect_reference, dest➔ page_destination (#1467)\n  -  owner_pwd/user_pwd ➔ owner_password/user_password (#1483)\n  -  position ➜ page_number in Merger.merge (#1482)\n  -  indirect_ref ➜ indirect_reference (#1484)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.12.0...2.12.1)\n\n\n## Version 2.12.0, 2022-12-10\n\n### New Features (ENH)\n-  Add support to extract gray scale images (#1460)\n-  Add 'threads' property to PdfWriter (#1458)\n-  Add 'open_destination' property to PdfWriter (#1431)\n-  Make PdfReader.get_object accept integer arguments (#1459)\n\n### Bug Fixes (BUG)\n-  Scale PDF annotations (#1479)\n\n### Robustness (ROB)\n-  Padding issue with AES encryption (#1469)\n-  Accept empty object as null objects (#1477)\n\n### Documentation (DOC)\n-  Add module documentation the PaperSize class (#1447)\n\n### Maintenance (MAINT)\n-  Use 'page_number' instead of 'pagenum' (#1365)\n-  Add List of pages to PageRangeSpec (#1456)\n\n### Testing (TST)\n-  Cleanup temporary files (#1454)\n-  Mark test_tounicode_is_identity as external (#1449)\n-  Use Ubuntu 20.04 for running CI test suite (#1452)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.11.2...2.12.0)\n\n\n## Version 2.11.2, 2022-11-20\n\n### New Features (ENH)\n-  Add remove_from_tree (#1432)\n-  Add AnnotationBuilder.rectangle (#1388)\n\n### Bug Fixes (BUG)\n-  JavaScript executed twice (#1439)\n-  ToUnicode stores /Identity-H instead of stream (#1433)\n-  Declare Pillow as optional dependency (#1392)\n\n### Developer Experience (DEV)\n-  Link 'Full Changelog' automatically\n-  Modify read_string_from_stream to a benchmark (#1415)\n-  Improve error reporting of read_object (#1412)\n-  Test Python 3.11 (#1404)\n-  Extend Flake8 ignore list (#1410)\n-  Use correct pytest markers (#1407)\n-  Move project configuration to pyproject.toml (#1382)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.11.1...2.11.2)\n\n## Version 2.11.1, 2022-10-09\n\n### Bug Fixes (BUG)\n- td matrix (#1373)\n- Cope with cmap from #1322 (#1372)\n\n### Robustness (ROB)\n-  Cope with str returned from get_data in cmap (#1380)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.11.0...2.11.1)\n\n## Version 2.11.0, 2022-09-25\n\n### New Features (ENH)\n-  Addition of optional visitor-functions in extract_text() (#1252)\n-  Add metadata.creation_date and modification_date (#1364)\n-  Add PageObject.images attribute (#1330)\n\n### Bug Fixes (BUG)\n-  Lookup index in _xobj_to_image can be ByteStringObject (#1366)\n-  'IndexError: index out of range' when using extract_text (#1361)\n-  Errors in transfer_rotation_to_content() (#1356)\n\n### Robustness (ROB)\n-  Ensure update_page_form_field_values does not fail if no fields (#1346)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.10.9...2.11.0)\n\n## Version 2.10.9, 2022-09-18\n\n### New Features (ENH)\n-  Add rotation property and transfer_rotate_to_content (#1348)\n\n### Performance Improvements (PI)\n-  Avoid string concatenation with large embedded base64-encoded images (#1350)\n\n### Bug Fixes (BUG)\n-  Format floats using their intrinsic decimal precision (#1267)\n\n### Robustness (ROB)\n-  Fix merge_page for pages without resources (#1349)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.10.8...2.10.9)\n\n## Version 2.10.8, 2022-09-14\n\n### New Features (ENH)\n-  Add PageObject.user_unit property (#1336)\n\n### Robustness (ROB)\n-  Improve NameObject reading/writing (#1345)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.10.7...2.10.8)\n\n## Version 2.10.7, 2022-09-11\n\n### Bug Fixes (BUG)\n-  Fix Error in transformations (#1341)\n-  Decode #23 in NameObject (#1342)\n\n### Testing (TST)\n-  Use pytest.warns() for warnings, and .raises() for exceptions (#1325)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.10.6...2.10.7)\n\n\n## Version 2.10.6, 2022-09-09\n\n### Robustness (ROB)\n-  Fix infinite loop due to Invalid object (#1331)\n-  Fix image extraction issue with superfluous whitespaces (#1327)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.10.5...2.10.6)\n\n## Version 2.10.5, 2022-09-04\n\n### New Features (ENH)\n-  Process XRefStm (#1297)\n-  Auto-detect RTL for text extraction (#1309)\n\n### Bug Fixes (BUG)\n-  Avoid scaling cropbox twice (#1314)\n\n### Robustness (ROB)\n-  Fix offset correction in revised PDF (#1318)\n-  Crop data of /U and /O in encryption dictionary to 48 bytes (#1317)\n-  MultiLine bfrange in cmap (#1299)\n-  Cope with 2 digit codes in bfchar (#1310)\n-  Accept '/annn' charset as ASCII code (#1316)\n-  Log errors during Float / NumberObject initialization (#1315)\n-  Cope with corrupted entries in xref table (#1300)\n\n### Documentation (DOC)\n-  Migration guide (PyPDF2 1.x ➔ 2.x) (#1324)\n-  Creating a coverage report (#1319)\n-  Fix AnnotationBuilder.free_text example (#1311)\n-  Fix usage of page.scale by replacing it with page.scale_by (#1313)\n\n### Maintenance (MAINT)\n-  PdfReaderProtocol (#1303)\n-  Throw PdfReadError if Trailer can't be read (#1298)\n-  Remove catching OverflowException (#1302)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.10.4...2.10.5)\n\n\n## Version 2.10.4, 2022-08-28\n\n### Robustness (ROB)\n-  Fix errors/warnings on no /Resources within extract_text (#1276)\n-  Add required line separators in ContentStream ArrayObjects (#1281)\n\n### Maintenance (MAINT)\n-  Use NameObject idempotency (#1290)\n\n### Testing (TST)\n-  Rectangle deletion (#1289)\n-  Add workflow tests (#1287)\n-  Remove files after tests ran (#1286)\n\n### Packaging (PKG)\n-  Add minimum version for typing_extensions requirement (#1277)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.10.3...2.10.4)\n\n## Version 2.10.3, 2022-08-21\n\n### Robustness (ROB)\n-  Decrypt returns empty bytestring (#1258)\n\n### Developer Experience (DEV)\n-  Modify CI to better verify built package contents (#1244)\n\n### Maintenance (MAINT)\n-  Remove 'mine' as PdfMerger always creates the stream (#1261)\n-  Let PdfMerger._create_stream raise NotImplemented (#1251)\n-  password param of _security._alg32(...) is only a string, not bytes (#1259)\n-  Remove unreachable code in read_block_backwards (#1250)\n   and sign function in _extract_text (#1262)\n\n### Testing (TST)\n-  Delete annotations (#1263)\n-  Close PdfMerger in tests (#1260)\n-  PdfReader.xmp_metadata workflow (#1257)\n-  Various PdfWriter (Layout, Bookmark deprecation) (#1249)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.10.2...2.10.3)\n\n## Version 2.10.2, 2022-08-15\n\nBUG: Add PyPDF2.generic to PyPI distribution\n\n## Version 2.10.1, 2022-08-15\n\n### Bug Fixes (BUG)\n-  TreeObject.remove_child had a non-PdfObject assignment for Count (#1233, #1234)\n-  Fix stream truncated prematurely (#1223)\n\n### Documentation (DOC)\n-  Fix docstring formatting (#1228)\n\n### Maintenance (MAINT)\n-  Split generic.py (#1229)\n\n### Testing (TST)\n-  Decrypt AlgV4 with owner password (#1239)\n-  AlgV5.generate_values (#1238)\n-  TreeObject.remove_child / empty_tree (#1235, #1236)\n-  create_string_object (#1232)\n-  Free-Text annotations (#1231)\n-  generic._base (#1230)\n-  Strict get fonts (#1226)\n-  Increase PdfReader coverage (#1219, #1225)\n-  Increase PdfWriter coverage (#1237)\n-  100% coverage for utils.py (#1217)\n-  PdfWriter exception non-binary stream (#1218)\n-  Don't check coverage for deprecated code (#1216)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.10.0...2.10.1)\n\n\n## Version 2.10.0, 2022-08-07\n\n### New Features (ENH)\n-  \"with\" support for PdfMerger and PdfWriter (#1193)\n-  Add AnnotationBuilder.text(...) to build text annotations (#1202)\n\n### Bug Fixes (BUG)\n-  Allow IndirectObjects as stream filters (#1211)\n\n### Documentation (DOC)\n-  Font scrambling\n-  Page vs Content scaling (#1208)\n-  Example for orientation parameter of extract_text (#1206)\n-  Fix AnnotationBuilder parameter formatting (#1204)\n\n### Developer Experience (DEV)\n-  Add flake8-print (#1203)\n\n### Maintenance (MAINT)\n-  Introduce WrongPasswordError / FileNotDecryptedError / EmptyFileError  (#1201)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.9.0...2.10.0)\n\n## Version 2.9.0, 2022-07-31\n\n### New Features (ENH)\n-  Add ability to add hex encoded colors to outline items (#1186)\n-  Add support for pathlib.Path in PdfMerger.merge (#1190)\n-  Add link annotation (#1189)\n-  Add capability to filter text extraction by orientation (#1175)\n\n### Bug Fixes (BUG)\n-  Named Dest in PDF1.1 (#1174)\n-  Incomplete Graphic State save/restore (#1172)\n\n### Documentation (DOC)\n-  Update changelog url in package metadata (#1180)\n-  Mention camelot for table extraction (#1179)\n-  Mention pyHanko for signing PDF documents (#1178)\n-  Weow have CMAP support since a while (#1177)\n\n### Maintenance (MAINT)\n-  Consistent usage of warnings / log messages (#1164)\n-  Consistent terminology for outline items (#1156)\n\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.8.1...2.9.0)\n\n## Version 2.8.1, 2022-07-25\n\n### Bug Fixes (BUG)\n-  u_hash in AlgV4.compute_key (#1170)\n\n### Robustness (ROB)\n-  Fix loading of file from #134 (#1167)\n-  Cope with empty DecodeParams (#1165)\n\n### Documentation (DOC)\n-  Typo in merger deprecation warning message (#1166)\n\n### Maintenance (MAINT)\n-  Package updates; solve mypy strict remarks (#1163)\n\n### Testing (TST)\n-  Add test from #325 (#1169)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.8.0...2.8.1)\n\n\n## Version 2.8.0, 2022-07-24\n\n### New Features (ENH)\n-  Add writer.add_annotation, page.annotations, and generic.AnnotationBuilder (#1120)\n\n### Bug Fixes (BUG)\n-  Set /AS for /Btn form fields in writer (#1161)\n-  Ignore if /Perms verify failed (#1157)\n\n### Robustness (ROB)\n-  Cope with utf16 character for space calculation (#1155)\n-  Cope with null params for FitH / FitV destination (#1152)\n-  Handle outlines without valid destination (#1076)\n\n### Developer Experience (DEV)\n-  Introduce _utils.logger_warning (#1148)\n\n### Maintenance (MAINT)\n-  Break up parse_to_unicode (#1162)\n-  Add diagnostic output to exception in read_from_stream (#1159)\n-  Reduce PdfReader.read complexity (#1151)\n\n### Testing (TST)\n-  Add workflow tests found by arc testing (#1154)\n-  Decrypt file which is not encrypted (#1149)\n-  Test CryptRC4 encryption class; test image extraction filters (#1147)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.7.0...2.8.0)\n\n## Version 2.7.0, 2022-07-21\n\n### New Features (ENH)\n-  Add `outline_count` property (#1129)\n\n### Bug Fixes (BUG)\n-  Make reader.get_fields also return dropdowns with options (#1114)\n-  Add deprecated EncodedStreamObject functions back until PyPDF2==3.0.0 (#1139)\n\n### Robustness (ROB)\n-  Cope with missing /W entry (#1136)\n-  Cope with invalid parent xref (#1133)\n\n### Documentation (DOC)\n-  Contributors file (#1132)\n-  Fix type in signature of PdfWriter.add_uri (#1131)\n\n### Developer Experience (DEV)\n-  Add .git-blame-ignore-revs (#1141)\n\n### Code Style (STY)\n-  Fixing typos (#1137)\n-  Reuse code via get_outlines_property in tests (#1130)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.6.0...2.7.0)\n\n## Version 2.6.0, 2022-07-17\n\n### New Features (ENH)\n-  Add color and font_format to PdfReader.outlines[i] (#1104)\n-  Extract Text Enhancement (whitespaces) (#1084)\n\n### Bug Fixes (BUG)\n-  Use `build_destination` for named destination outlines (#1128)\n-  Avoid a crash when a ToUnicode CMap has an empty dstString in beginbfchar (#1118)\n-  Prevent deduplication of PageObject (#1105)\n-  None-check in DictionaryObject.read_from_stream (#1113)\n-  Avoid IndexError in _cmap.parse_to_unicode (#1110)\n\n### Documentation (DOC)\n-  Explanation for git submodule\n-  Watermark and stamp (#1095)\n\n### Maintenance (MAINT)\n-  Text extraction improvements (#1126)\n-  Destination.color returns ArrayObject instead of tuple as fallback (#1119)\n-  Use add_bookmark_destination in add_bookmark (#1100)\n-  Use add_bookmark_destination in add_bookmark_dict (#1099)\n\n### Testing (TST)\n-  Add test for arab text (#1127)\n-  Add xfail for decryption fail (#1125)\n-  Add xfail test for IndexError when extracting text (#1124)\n-  Add MCVE showing outline title issue (#1123)\n\n### Code Style (STY)\n-  Use IntFlag for permissions_flag / update_page_form_field_values (#1094)\n-  Simplify code (#1101)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.5.0...2.6.0)\n\n## Version 2.5.0, 2022-07-10\n\n### New Features (ENH)\n-  Add support for indexed color spaces / BitsPerComponent for decoding PNGs (#1067)\n-  Add PageObject._get_fonts (#1083)\n\n### Performance Improvements (PI)\n-  Use iterative DFS in PdfWriter._sweep_indirect_references (#1072)\n\n### Bug Fixes (BUG)\n-  Let Page.scale also scale the crop-/trim-/bleed-/artbox (#1066)\n-  Column default for CCITTFaxDecode (#1079)\n\n### Robustness (ROB)\n-  Guard against None-value in _get_outlines (#1060)\n\n### Documentation (DOC)\n-  Stamps and watermarks (#1082)\n-  OCR vs PDF text extraction (#1081)\n-  Python Version support\n-  Formatting of CHANGELOG\n\n### Developer Experience (DEV)\n-  Cache downloaded files (#1070)\n-  Speed-up for CI (#1069)\n\n### Maintenance (MAINT)\n-  Set page.rotate(angle: int) (#1092)\n-  Issue #416 was fixed by #1015 (#1078)\n\n### Testing (TST)\n-  Image extraction (#1080)\n-  Image extraction (#1077)\n\n### Code Style (STY)\n-  Apply black\n-  Typo in Changelog\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.4.2...2.5.0)\n\n## Version 2.4.2, 2022-07-05\n\n### New Features (ENH)\n-  Add PdfReader.xfa attribute (#1026)\n\n### Bug Fixes (BUG)\n-  Wrong page inserted when PdfMerger.merge is done (#1063)\n-  Resolve IndirectObject when it refers to a free entry (#1054)\n\n### Developer Experience (DEV)\n-  Added {posargs} to tox.ini (#1055)\n\n### Maintenance (MAINT)\n-  Remove PyPDF2._utils.bytes_type (#1053)\n\n### Testing (TST)\n-  Scale page (indirect rect object) (#1057)\n-  Simplify pathlib PdfReader test (#1056)\n-  IndexError of VirtualList (#1052)\n-  Invalid XML in xmp information (#1051)\n-  No pycryptodome (#1050)\n-  Increase test coverage (#1045)\n\n### Code Style (STY)\n-  DOC of compress_content_streams (#1061)\n-  Minimize diff for #879 (#1049)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.4.1...2.4.2)\n\n## Version 2.4.1, 2022-06-30\n\n### New Features (ENH)\n-  Add writer.pdf_header property (getter and setter) (#1038)\n\n### Performance Improvements (PI)\n-  Remove b_ call in FloatObject.write_to_stream (#1044)\n-  Check duplicate objects in writer._sweep_indirect_references (#207)\n\n### Documentation (DOC)\n-  How to surppress exceptions/warnings/log messages (#1037)\n-  Remove hyphen from lossless (#1041)\n-  Compression of content streams (#1040)\n-  Fix inconsistent variable names in add-watermark.md (#1039)\n-  File size reduction\n-  Add CHANGELOG to the rendered docs (#1023)\n\n### Maintenance (MAINT)\n-  Handle XML error when reading XmpInformation (#1030)\n-  Deduplicate Code / add mutmut config (#1022)\n\n### Code Style (STY)\n-  Use unnecessary one-line function / class attribute (#1043)\n-  Docstring formatting (#1033)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.4.0...2.4.1)\n\n## Version 2.4.0, 2022-06-26\n\n### New Features (ENH):\n-  Support R6 decrypting (#1015)\n-  Add PdfReader.pdf_header (#1013)\n\n### Performance Improvements (PI):\n-  Remove ord_ calls (#1014)\n\n### Bug Fixes (BUG):\n-  Fix missing page for bookmark (#1016)\n\n### Robustness (ROB):\n-  Deal with invalid Destinations (#1028)\n\n### Documentation (DOC):\n-  get_form_text_fields does not extract dropdown data (#1029)\n-  Adjust PdfWriter.add_uri docstring\n-  Mention crypto extra_requires for installation (#1017)\n\n### Developer Experience (DEV):\n-  Use /n line endings everywhere (#1027)\n-  Adjust string formatting to be able to use mutmut (#1020)\n-  Update Bug report template\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.3.1...2.4.0)\n\n## Version 2.3.1, 2022-06-19\n\nBUG: Forgot to add the internal `_codecs` subpackage.\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.3.0...2.3.1)\n\n## Version 2.3.0, 2022-06-19\n\nThe highlight of this release is improved support for file encryption\n(AES-128 and AES-256, R5 only). See #749 for the amazing work of\n@exiledkingcc 🎊 Thank you 🤗\n\n### Deprecations (DEP)\n-  Rename names to be PEP8-compliant (#967)\n  - `PdfWriter.get_page`: the pageNumber parameter is renamed to page_number\n  - `PyPDF2.filters`:\n    * For all classes, a parameter rename: decodeParms ➔ decode_parms\n    * decodeStreamData ➔ decode_stream_data\n  - `PyPDF2.xmp`:\n    * XmpInformation.rdfRoot ➔ XmpInformation.rdf_root\n    * XmpInformation.xmp_createDate ➔ XmpInformation.xmp_create_date\n    * XmpInformation.xmp_creatorTool ➔ XmpInformation.xmp_creator_tool\n    * XmpInformation.xmp_metadataDate ➔ XmpInformation.xmp_metadata_date\n    * XmpInformation.xmp_modifyDate ➔ XmpInformation.xmp_modify_date\n    * XmpInformation.xmpMetadata ➔ XmpInformation.xmp_metadata\n    * XmpInformation.xmpmm_documentId ➔ XmpInformation.xmpmm_document_id\n    * XmpInformation.xmpmm_instanceId ➔ XmpInformation.xmpmm_instance_id\n  - `PyPDF2.generic`:\n    * readHexStringFromStream ➔ read_hex_string_from_stream\n    * initializeFromDictionary ➔ initialize_from_dictionary\n    * createStringObject ➔ create_string_object\n    * TreeObject.hasChildren ➔ TreeObject.has_children\n    * TreeObject.emptyTree ➔ TreeObject.empty_tree\n\n### New Features (ENH)\n-  Add decrypt support for V5 and AES-128, AES-256 (R5 only) (#749)\n\n### Robustness (ROB)\n-  Fix corrupted (wrongly) linear PDF (#1008)\n\n### Maintenance (MAINT)\n-  Move PDF_Samples folder into resources\n-  Fix typos (#1007)\n\n### Testing (TST)\n-  Improve encryption/decryption test (#1009)\n-  Add merger test cases with real PDFs (#1006)\n-  Add mutmut config\n\n### Code Style (STY)\n-  Put pure data mappings in separate files (#1005)\n-  Make encryption module private, apply pre-commit (#1010)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.2.1...2.3.0)\n\n## Version 2.2.1, 2022-06-17\n\n### Performance Improvements (PI)\n-  Remove b_ calls (#992, #986)\n-  Apply improvements to _utils suggested by perflint (#993)\n\n### Robustness (ROB)\n-  utf-16-be codec can't decode (...) (#995)\n\n### Documentation (DOC)\n-  Remove reference to Scripts (#987)\n\n### Developer Experience (DEV)\n-  Fix type annotations for add_bookmarks (#1000)\n\n### Testing (TST)\n-  Add test for PdfMerger (#1001)\n-  Add tests for XMP information (#996)\n-  reader.get_fields / zlib issue / LZW decode issue (#1004)\n-  reader.get_fields with report generation (#1002)\n-  Improve test coverage by extracting texts (#998)\n\n### Code Style (STY)\n-  Apply fixes suggested by pylint (#999)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.2.0...2.2.1)\n\n## Version 2.2.0, 2022-06-13\n\nThe 2.2.0 release improves text extraction again via (#969):\n\n* Improvements around /Encoding / /ToUnicode\n* Extraction of CMaps improved\n* Fallback for font def missing\n* Support for /Identity-H and /Identity-V: utf-16-be\n* Support for /GB-EUC-H / /GB-EUC-V / GBp/c-EUC-H / /GBpc-EUC-V (beta release for evaluation)\n* Arabic (for evaluation)\n* Whitespace extraction improvements\n\nThose changes should mainly improve the text extraction for non-ASCII alphabets,\ne.g. Russian / Chinese / Japanese / Korean / Arabic.\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.1.1...2.2.0)\n\n## Version 2.1.1, 2022-06-12\n\n### New Features (ENH)\n-  Add support for pathlib as input for PdfReader (#979)\n\n### Performance Improvements (PI)\n-  Optimize read_next_end_line (#646)\n\n### Bug Fixes (BUG)\n-  Adobe Acrobat 'Would you like to save this file?' (#970)\n\n### Documentation (DOC)\n-  Notes on annotations (#982)\n-  Who uses PyPDF2\n-  intendet \\xe2\\x9e\\x94 in robustness page  (#958)\n\n### Maintenance (MAINT)\n-  pre-commit / requirements.txt updates (#977)\n-  Mark read_next_end_line as deprecated (#965)\n-  Export `PageObject` in PyPDF2 root (#960)\n\n### Testing (TST)\n-  Add MCVE of issue #416 (#980)\n-  FlateDecode.decode decodeParms (#964)\n-  Xmp module (#962)\n-  utils.paeth_predictor (#959)\n\n### Code Style (STY)\n-  Use more tuples and list/dict comprehensions (#976)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.1.0...2.1.1)\n\n\n## Version 2.1.0, 2022-06-06\n\nThe highlight of the 2.1.0 release is the most massive improvement to the\ntext extraction capabilities of PyPDF2 since 2016 🥳🎊 A very big thank you goes\nto [pubpub-zz](https://github.com/pubpub-zz) who took a lot of time and\nknowledge about the PDF format to finally get those improvements into PyPDF2.\nThank you 🤗💚\n\nIn case the new function causes any issues, you can use `_extract_text_old`\nfor the old functionality. Please also open a bug ticket in that case.\n\nThere were several people who have attempted to bring similar improvements to\nPyPDF2. All of those were valuable. The main reason why they didn't get merged\nis the big amount of open PRs / issues. pubpub-zz was the most comprehensive\nPR which also incorporated the latest changes of PyPDF2 2.0.0.\n\nThank you to [VictorCarlquist](https://github.com/VictorCarlquist) for #858 and\n[asabramo](https://github.com/asabramo) for #464 🤗\n\n### New Features (ENH)\n-  Massive text extraction improvement (#924). Closed many open issues:\n    - Exceptions / missing spaces in extract_text() method (#17) 🕺\n      - Whitespace issues in extract_text() (#42) 💃\n      - pypdf2 reads the hifenated words in a new line (#246)\n    - PyPDF2 failing to read unicode character (#37)\n      - Unable to read bullets (#230)\n    - ExtractText yields nothing for apparently good PDF (#168) 🎉\n    - Encoding issue in extract_text() (#235)\n    - extractText() doesn't work on Chinese PDF (#252)\n    - encoding error (#260)\n    - Trouble with apostophes in names in text \"O'Doul\" (#384)\n    - extract_text works for some PDF files, but not the others (#437)\n    - Euro sign not being recognized by extractText (#443)\n    - Failed extracting text from French texts (#524)\n    - extract_text doesn't extract ligatures correctly (#598)\n    - reading spanish text - mark convert issue (#635)\n    - Read PDF changed from text to random symbols (#654)\n    - .extractText() reads / as 1. (#789)\n-  Update glyphlist (#947) - inspired by #464\n-  Allow adding PageRange objects (#948)\n\n### Bug Fixes (BUG)\n-  Delete .python-version file (#944)\n-  Compare StreamObject.decoded_self with None (#931)\n\n### Robustness (ROB)\n-  Fix some conversion errors on non conform PDF (#932)\n\n### Documentation (DOC)\n-  Elaborate on PDF text extraction difficulties (#939)\n-  Add logo (#942)\n-  rotate vs Transformation().rotate (#937)\n-  Example how to use PyPDF2 with AWS S3 (#938)\n-  How to deprecate (#930)\n-  Fix typos on robustness page (#935)\n-  Remove scripts (pdfcat) from docs (#934)\n\n### Developer Experience (DEV)\n-  Ignore .python-version file\n-  Mark deprecated code with no-cover (#943)\n-  Automatically create Github releases from tags (#870)\n\n### Testing (TST)\n-  Text extraction for non-latin alphabets (#954)\n-  Ignore PdfReadWarning in benchmark (#949)\n-  writer.remove_text (#946)\n-  Add test for Tree and _security (#945)\n\n### Code Style (STY)\n-  black, isort, Flake8, splitting buildCharMap (#950)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/2.0.0...2.1.0)\n\n## Version 2.0.0, 2022-06-01\n\nThe 2.0.0 release of PyPDF2 includes three core changes:\n\n1. Dropping support for Python 3.5 and older.\n2. Introducing type annotations.\n3. Interface changes, mostly to have PEP8-compliant names\n\nWe introduced a [deprecation process](https://github.com/py-pdf/PyPDF2/pull/930)\nthat hopefully helps users to avoid unexpected breaking changes.\n\n### Breaking Changes (DEP)\n- PyPDF2 2.0 requires Python 3.6+. Python 2.7 and 3.5 support were dropped.\n- PdfFileReader: The \"warndest\" parameter was removed\n- PdfFileReader and PdfFileMerger no longer have the `overwriteWarnings`\n  parameter. The new behavior is `overwriteWarnings=False`.\n- merger: OutlinesObject was removed without replacement.\n- merger.py ➔ _merger.py: You must import PdfFileMerger from PyPDF2 directly.\n- utils:\n  * `ConvertFunctionsToVirtualList` was removed\n  * `formatWarning` was removed\n  * `isInt(obj)`: Use `instance(obj, int)` instead\n  * `u_(s)`: Use `s` directly\n  * `chr_(c)`: Use `chr(c)` instead\n  * `barray(b)`: Use `bytearray(b)` instead\n  * `isBytes(b)`: Use `instance(b, type(bytes()))` instead\n  * `xrange_fn`: Use `range` instead\n  * `string_type`: Use `str` instead\n  * `isString(s)`: Use `instance(s, str)` instead\n  * `_basestring`: Use `str` instead\n  * All Exceptions are now in `PyPDF2.errors`:\n    - PageSizeNotDefinedError\n    - PdfReadError\n    - PdfReadWarning\n    - PyPdfError\n- `PyPDF2.pdf` (the `pdf` module) no longer exists. The contents were moved with\n  the library. You should most likely import directly from `PyPDF2` instead.\n  The `RectangleObject` is in `PyPDF2.generic`.\n- The `Resources`, `Scripts`, and `Tests` will no longer be part of the distribution\n  files on PyPI. This should have little to no impact on most people. The\n  `Tests` are renamed to `tests`, the `Resources` are renamed to `resources`.\n  Both are still in the git repository. The `Scripts` are now in\n  [cpdf](https://github.com/py-pdf/cpdf). `Sample_Code` was moved to the `docs`.\n\nFor a full list of deprecated functions, please see the changelog of version\n1.28.0.\n\n### New Features (ENH)\n-  Improve space setting for text extraction (#922)\n-  Allow setting the decryption password in `PdfReader.__init__` (#920)\n-  Add Page.add_transformation (#883)\n\n### Bug Fixes (BUG)\n-  Fix error adding transformation to page without /Contents (#908)\n\n### Robustness (ROB)\n-  Cope with invalid length in streams (#861)\n\n### Documentation (DOC)\n-  Fix style of 1.25 and 1.27 patch notes (#927)\n-  Transformation (#907)\n\n### Developer Experience (DEV)\n-  Create flake8 config file (#916)\n-  Use relative imports (#875)\n\n### Maintenance (MAINT)\n-  Use Python 3.6 language features (#849)\n-  Add wrapper function for PendingDeprecationWarnings (#928)\n-  Use new PEP8 compliant names (#884)\n-  Explicitly represent transformation matrix (#878)\n-  Inline PAGE_RANGE_HELP string (#874)\n-  Remove unnecessary generics imports (#873)\n-  Remove star imports (#865)\n-  merger.py ➔ _merger.py (#864)\n-  Type annotations for all functions/methods (#854)\n-  Add initial type support with mypy (#853)\n\n### Testing (TST)\n-  Regression test for xmp_metadata converter (#923)\n-  Checkout submodule sample-files for benchmark\n-  Add text extracting performance benchmark\n-  Use new PyPDF2 API in benchmark (#902)\n-  Make test suite fail for uncaught warnings (#892)\n-  Remove -OO testrun from CI (#901)\n-  Improve tests for convert_to_int (#899)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.28.4...2.0.0)\n\n## PyPDF2 1.X\n\nSee [CHANGELOG PyPDF2 1.X](changelog-v1.md)\n"
  },
  {
    "path": "CONTRIBUTING.md",
    "content": "Please check the [documentation page dedicated to development](https://pypdf.readthedocs.io/en/stable/dev/intro.html).\n\n## Creating issues / tickets\n\nPlease go here: https://github.com/py-pdf/pypdf/issues\n\nTypically you should not send e-mails. E-mails might only reach one person and\nit could go into spam or that person might be busy. Please create issues on\nGitHub instead.\n\nPlease use the templates provided.\n\nKeep in mind that although PDF has an official specification, there are tons of\nvariations which might require special handling. Thus, please always provide a\nreproducing example file for us to work with. Otherwise, we have to guess possible\nissues, leading to unnecessary overhead - especially since most of the contributions\nhappen during our free time.\n\nIf you already know a fix, consider opening a pull request after reporting the issue\nto make life easier for everyone.\n\n## Creating Pull Requests\n\nWe appreciate if people make PRs, but please be aware that pypdf is used by many\npeople. That means:\n\n* We rarely make breaking changes and have a [deprecation process](https://pypdf.readthedocs.io/en/latest/dev/deprecations.html).\n* New features, especially adding to the public interface, typically need to be\n  discussed first.\n\nBefore you make bigger changes, open an issue to make the suggestion.\nNote which interface changes you want to make.\n"
  },
  {
    "path": "CONTRIBUTORS.md",
    "content": "# Contributors\n\npypdf had a lot of contributors since it started as pyPdf in 2005. We are\na free software project without any company affiliation. We cannot pay\ncontributors, but we do value their contributions. A lot of time, effort, and\nexpertise went into this project. With this list, we recognize these awesome\npeople 🤗\n\nThe list is definitely not complete. You can find more contributors via the git\nhistory and [GitHub's 'Contributors' feature](https://github.com/py-pdf/pypdf/graphs/contributors).\n\n## Contributors to the pypdf (formerly pyPdf / PyPDF2) project\n\n* [abyesilyurt](https://github.com/abyesilyurt)\n* [ArkieCoder](https://github.com/ArkieCoder)\n* [Beers, PJ](https://github.com/PJBrs)\n* [Clauss, Christian](https://github.com/cclauss)\n* [DL6ER](https://github.com/DL6ER)\n* [Duy, Phan Thanh](https://github.com/zuypt)\n* [ediamondscience](https://github.com/ediamondscience)\n* [Ermeson, Felipe](https://github.com/FelipeErmeson)\n* [Freitag, François](https://github.com/francoisfreitag)\n* [Gagnon, William G.](https://github.com/williamgagnon)\n* [Gillard, James](https://github.com/jgillard)\n* [Górny, Michał](https://github.com/mgorny)\n* [Grillo, Miguel](https://github.com/Ineffable22)\n* [Gutteridge, David H.](https://github.com/dhgutteridge)\n* [Hale, Joseph](https://github.com/thehale)\n* [harshhes](https://github.com/harshhes)\n* [Jackowitz, Noah](https://github.com/hackowitz-af) | [LinkedIn](https://www.linkedin.com/in/noah-jackowitz/)\n* [JianzhengLuo](https://github.com/JianzhengLuo)\n* [Karvonen, Harry](https://github.com/Hatell/)\n* [King, Hunter](https://github.com/neversphere)\n* [Kotler, Mitchell](https://github.com/mitchelljkotler)\n* [KourFrost](https://github.com/KourFrost)\n* [Lightup1](https://github.com/Lightup1)\n* [Majumder, Jonah](https://github.com/jonahmajumder)\n* [Manini, Lorenzo](https://github.com/lorenzomanini)\n* [maxbeer99](https://github.com/maxbeer99)\n* [McNeil, Karen](https://github.com/karenlmcneil): Arabic Language Support\n* [Mérino, Antoine](https://github.com/Merinorus)\n* [Murphy, Kevin](https://github.com/kmurphy4)\n* [nalin-udhaar](https://github.com/nalin-udhaar)\n* [Noah-Houghton](https://github.com/Noah-Houghton) | [LinkedIn](https://www.linkedin.com/in/noah-h-554992a0/)\n* [Paramonov, Alexey](https://github.com/alexey-v-paramonov)\n* [Paternault, Louis](https://framagit.org/spalax)\n* [Perrensen, Olsen](https://github.com/olsonperrensen)\n* [pilotandy](https://github.com/pilotandy)\n* [Pinheiro, Arthur](https://github.com/xilopaint)\n* [pmiller66](https://github.com/pmiller66)\n* [Poddar, Arka](https://github.com/postmeback)\n* [programmarchy](https://github.com/programmarchy)\n* [pubpub-zz](https://github.com/pubpub-zz): involved in community development\n* [Ramos, Leodanis Pozo](https://github.com/lpozo)\n* [RitchieP](https://github.com/RitchieP) | [LinkedIn](https://www.linkedin.com/in/ritchie-p-892b31115/) | [StackOverflow](https://stackoverflow.com/users/13328625/casual-r?tab=profile)\n* [robbiebusinessacc](https://github.com/robbiebusinessacc)\n* [Roder, Thomas](https://github.com/MrTomRod)\n* [Rogmann, Sascha](https://github.com/srogmann)\n* [Röthenbacher, Thomas](https://github.com/troethe)\n* [shartzog](https://github.com/shartzog)\n* [stefan6419846](https://github.com/stefan6419846): Maintainer of pypdf since January 2025\n* [sietzeberends](https://github.com/sietzeberends)\n* [Stober, Marc](https://github.com/marcstober)\n* [Stüber, Timo](https://github.com/omit66)\n* [Thoma, Martin](https://github.com/MartinThoma): Maintainer of pypdf from April 2022 to January 2025. I hope to build a great community with many awesome contributors. [LinkedIn](https://www.linkedin.com/in/martin-thoma/) | [StackOverflow](https://stackoverflow.com/users/562769/martin-thoma) | [Blog](https://martin-thoma.com/)\n* [Thomas, Reuben](https://github.com/rrthomas)\n* [Tobeabellwether](https://github.com/Tobeabellwether)\n* [van Alst, Ludo](https://github.com/LudovA)\n* [WevertonGomes](https://github.com/WevertonGomesCosta)\n* [Wilson, Huon](https://github.com/huonw)\n* ztravis\n\n## Adding a new contributor\n\nContributors are:\n\n* Anybody who has a commit in `main` - no matter how small or how many. Also if it's via *co-authored-by*.\n* People who opened helpful issues:\n\n  1. Bugs: with complete MCVE\n  2. Well-described feature requests\n  3. Potentially some more.\n\n  The maintainers of pypdf have the last call on that one.\n* Community work: This is exceptional. If the maintainers of pypdf see people\n  being super helpful in answering issues / discussions or being very active on\n  Stackoverflow, we also consider them being contributors to pypdf.\n\nContributors can add themselves or ask via an GitHub Issue to be added.\n\nPlease use the following format:\n\n```\n* Last name, First name: 140-characters of text; links to LinkedIn / GitHub / other profiles and personal pages are ok\n```\n\nOR\n\n```\n* GitHub Username: 140-characters of text; links to LinkedIn / GitHub / other profiles and personal pages are ok\n```\n\nand add the entry in the alphabetical order. The 140 characters are everything visible after the `Name:`.\n\nPlease don't use images.\n"
  },
  {
    "path": "LICENSE",
    "content": "Copyright (c) 2006-2008, Mathieu Fenniak\nSome contributions copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com>\nSome contributions copyright (c) 2014, Steve Witham <switham_github@mac-guyver.com>\n\nAll rights reserved.\n\nRedistribution and use in source and binary forms, with or without\nmodification, are permitted provided that the following conditions are\nmet:\n\n* Redistributions of source code must retain the above copyright notice,\nthis list of conditions and the following disclaimer.\n* Redistributions in binary form must reproduce the above copyright notice,\nthis list of conditions and the following disclaimer in the documentation\nand/or other materials provided with the distribution.\n* The name of the author may not be used to endorse or promote products\nderived from this software without specific prior written permission.\n\nTHIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\nAND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\nIMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\nARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\nLIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\nCONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\nSUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\nINTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\nCONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\nARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\nPOSSIBILITY OF SUCH DAMAGE.\n"
  },
  {
    "path": "Makefile",
    "content": "maint:\n\tpre-commit autoupdate\n\tpip-compile -U requirements/ci.in\n\tpip-compile -U requirements/dev.in\n\tpip-compile -U requirements/docs.in\n\nrelease:\n\tpython make_release.py\n\tgit commit -eF RELEASE_COMMIT_MSG.md\n\nclean:\n\tpython -m pip install pyclean\n\tpyclean .\n\trm -rf tests/__pycache__ pypdf/__pycache__ htmlcov docs/_build dist pypdf.egg-info .pytest_cache .mypy_cache .benchmarks\n\ntest:\n\tpytest tests --cov --cov-report term-missing -vv --cov-report html --durations=3 --timeout=60 pypdf\n\ntesttype:\n\tpytest tests --cov --cov-report term-missing -vv --cov-report html --durations=3 --timeout=30 --typeguard-packages=pypdf\n\nbenchmark:\n\tpytest tests/bench.py\n\nmypy:\n\tmypy pypdf --ignore-missing-imports --check-untyped --strict\n\nruff:\n\truff check pypdf tests make_release.py\n"
  },
  {
    "path": "README.md",
    "content": "[![PyPI version](https://badge.fury.io/py/pypdf.svg)](https://badge.fury.io/py/pypdf)\n[![Python Support](https://img.shields.io/pypi/pyversions/pypdf.svg)](https://pypi.org/project/pypdf/)\n[![](https://img.shields.io/badge/-documentation-green)](https://pypdf.readthedocs.io/en/stable/)\n[![GitHub last commit](https://img.shields.io/github/last-commit/py-pdf/pypdf)](https://github.com/py-pdf/pypdf)\n[![codecov](https://codecov.io/gh/py-pdf/pypdf/branch/main/graph/badge.svg?token=id42cGNZ5Z)](https://codecov.io/gh/py-pdf/pypdf)\n\n# pypdf\n\npypdf is a free and open-source pure-python PDF library capable of splitting,\n[merging](https://pypdf.readthedocs.io/en/stable/user/merging-pdfs.html),\n[cropping, and transforming](https://pypdf.readthedocs.io/en/stable/user/cropping-and-transforming.html)\nthe pages of PDF files. It can also add\ncustom data, viewing options, and\n[passwords](https://pypdf.readthedocs.io/en/stable/user/encryption-decryption.html)\nto PDF files. pypdf can\n[retrieve text](https://pypdf.readthedocs.io/en/stable/user/extract-text.html)\nand\n[metadata](https://pypdf.readthedocs.io/en/stable/user/metadata.html)\nfrom PDFs as well.\n\nSee [pdfly](https://github.com/py-pdf/pdfly) for a CLI application that uses pypdf to interact with PDFs.\n\n## Installation\n\nInstall pypdf using pip:\n\n```\npip install pypdf\n```\n\nFor using pypdf with AES encryption or decryption, install extra dependencies:\n\n```\npip install pypdf[crypto]\n```\n\n> **NOTE**: `pypdf` 3.1.0 and above include significant improvements compared to\n> previous versions. Please refer to [the migration\n> guide](https://pypdf.readthedocs.io/en/latest/user/migration-1-to-2.html) for\n> more information.\n\n## Usage\n\n```python\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\nnumber_of_pages = len(reader.pages)\npage = reader.pages[0]\ntext = page.extract_text()\n```\n\npypdf can do a lot more, e.g. splitting, merging, reading and creating annotations, decrypting and encrypting. Check out the\n[documentation](https://pypdf.readthedocs.io/en/stable/) for additional usage\nexamples!\n\nFor questions and answers, visit\n[StackOverflow](https://stackoverflow.com/questions/tagged/pypdf)\n(tagged with [pypdf](https://stackoverflow.com/questions/tagged/pypdf)).\n\n## Contributions\n\nMaintaining pypdf is a collaborative effort. You can support the project by\nwriting documentation, helping to narrow down issues, and submitting code.\nSee the [CONTRIBUTING.md](https://github.com/py-pdf/pypdf/blob/main/CONTRIBUTING.md) file for more information.\n\n### Q&A\n\nThe experience pypdf users have covers the whole range from beginner to expert. You can contribute to the pypdf community by answering questions\non [StackOverflow](https://stackoverflow.com/questions/tagged/pypdf),\nhelping in [discussions](https://github.com/py-pdf/pypdf/discussions),\nand asking users who report issues for [MCVE](https://stackoverflow.com/help/minimal-reproducible-example)'s (Code + example PDF!).\n\n\n### Issues\n\nA good bug ticket includes a MCVE - a minimal complete verifiable example.\nFor pypdf, this means that you must upload a PDF that causes the bug to occur\nas well as the code you're executing with all of the output. Use\n`print(pypdf.__version__)` to tell us which version you're using.\n\n### Code\n\nAll code contributions are welcome, but smaller ones have a better chance to\nget included in a timely manner. Adding unit tests for new features or test\ncases for bugs you've fixed help us to ensure that the Pull Request (PR) is fine.\n\npypdf includes a test suite which can be executed with `pytest`:\n\n```bash\n$ pytest\n===================== test session starts =====================\nplatform linux -- Python 3.6.15, pytest-7.0.1, pluggy-1.0.0\nrootdir: /home/moose/GitHub/Martin/pypdf\nplugins: cov-3.0.0\ncollected 233 items\n\ntests/test_basic_features.py ..                         [  0%]\ntests/test_constants.py .                               [  1%]\ntests/test_filters.py .................x.....           [ 11%]\ntests/test_generic.py ................................. [ 25%]\n.............                                           [ 30%]\ntests/test_javascript.py ..                             [ 31%]\ntests/test_merger.py .                                  [ 32%]\ntests/test_page.py .........................            [ 42%]\ntests/test_pagerange.py ................                [ 49%]\ntests/test_papersizes.py ..................             [ 57%]\ntests/test_reader.py .................................. [ 72%]\n...............                                         [ 78%]\ntests/test_utils.py ....................                [ 87%]\ntests/test_workflows.py ..........                      [ 91%]\ntests/test_writer.py .................                  [ 98%]\ntests/test_xmp.py ...                                   [100%]\n\n========== 232 passed, 1 xfailed, 1 warning in 4.52s ==========\n```\n"
  },
  {
    "path": "docs/Makefile",
    "content": "# Minimal makefile for Sphinx documentation\n#\n\n# You can set these variables from the command line, and also\n# from the environment for the first two.\nSPHINXOPTS    ?=\nSPHINXBUILD   ?= sphinx-build\nSOURCEDIR     = .\nBUILDDIR      = _build\n\n# Put it first so that \"make\" without argument is like \"make help\".\nhelp:\n\t@$(SPHINXBUILD) -M help \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n\n.PHONY: help Makefile\n\n# Catch-all target: route all unknown targets to Sphinx using the new\n# \"make mode\" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).\n%: Makefile\n\t@$(SPHINXBUILD) -M $@ \"$(SOURCEDIR)\" \"$(BUILDDIR)\" $(SPHINXOPTS) $(O)\n"
  },
  {
    "path": "docs/_static/releasing.drawio",
    "content": "<mxfile host=\"Electron\" type=\"device\">\n  <diagram name=\"Seite-1\" id=\"xmn08oupI2gSAHxAwkuE\">\n    <mxGraphModel dx=\"394\" dy=\"220\" grid=\"1\" gridSize=\"10\" guides=\"1\" tooltips=\"1\" connect=\"1\" arrows=\"1\" fold=\"1\" page=\"1\" pageScale=\"1\" pageWidth=\"827\" pageHeight=\"1169\" math=\"0\" shadow=\"0\">\n      <root>\n        <mxCell id=\"0\" />\n        <mxCell id=\"1\" parent=\"0\" />\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-33\" value=\"\" style=\"rounded=1;whiteSpace=wrap;html=1;fillColor=#d5e8d4;strokeColor=#82b366;arcSize=21;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"130\" y=\"790\" width=\"280\" height=\"290\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-21\" value=\"\" style=\"rounded=1;whiteSpace=wrap;html=1;fillColor=#f5f5f5;fontColor=#333333;strokeColor=#666666;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"60\" y=\"330\" width=\"480\" height=\"250\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-4\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-1\" target=\"Sy3GnD-ZVnJThFurnhwo-3\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-1\" value=\"python make_release.py\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"180\" y=\"80\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-6\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-3\" target=\"Sy3GnD-ZVnJThFurnhwo-5\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-3\" value=\"Manually adjust CHANGELOG.md changes\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"180\" y=\"170\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-9\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-5\" target=\"Sy3GnD-ZVnJThFurnhwo-8\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-10\" value=\"Yes\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"Sy3GnD-ZVnJThFurnhwo-9\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"0.1768\" y=\"-2\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-12\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-5\" target=\"Sy3GnD-ZVnJThFurnhwo-11\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-13\" value=\"No\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"Sy3GnD-ZVnJThFurnhwo-12\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"0.3105\" y=\"2\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-5\" value=\"Is there a breaking change\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"180\" y=\"260\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-24\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-7\" target=\"Sy3GnD-ZVnJThFurnhwo-23\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-7\" value=\"Adjust the CHANGELOG.md\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"170\" y=\"600\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-17\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-8\" target=\"Sy3GnD-ZVnJThFurnhwo-7\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\">\n            <Array as=\"points\">\n              <mxPoint x=\"150\" y=\"460\" />\n              <mxPoint x=\"230\" y=\"460\" />\n            </Array>\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-8\" value=\"Major version bump in _version.py\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"90\" y=\"370\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-15\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-11\" target=\"Sy3GnD-ZVnJThFurnhwo-14\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-16\" value=\"Yes\" style=\"edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];\" parent=\"Sy3GnD-ZVnJThFurnhwo-15\" vertex=\"1\" connectable=\"0\">\n          <mxGeometry x=\"-0.2562\" y=\"3\" relative=\"1\" as=\"geometry\">\n            <mxPoint as=\"offset\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-20\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-11\" target=\"Sy3GnD-ZVnJThFurnhwo-19\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-11\" value=\"Is there a new feature?\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"250\" y=\"370\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-18\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-14\" target=\"Sy3GnD-ZVnJThFurnhwo-7\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-14\" value=\"Minor version bump\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"250\" y=\"490\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-35\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-19\" target=\"Sy3GnD-ZVnJThFurnhwo-23\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-19\" value=\"Patch version bump\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"400\" y=\"490\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-22\" value=\"Semantic Versioning\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontStyle=1;fontSize=18;fontColor=#6E6E6E;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"450\" y=\"350\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-27\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-23\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"230\" y=\"810\" as=\"targetPoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-23\" value=\"git commit -eF RELEASE_COMMIT_MSG.md\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"75\" y=\"700\" width=\"310\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-30\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" target=\"Sy3GnD-ZVnJThFurnhwo-28\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\">\n            <mxPoint x=\"230\" y=\"870\" as=\"sourcePoint\" />\n          </mxGeometry>\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-31\" style=\"edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;entryX=0.5;entryY=0;entryDx=0;entryDy=0;\" parent=\"1\" source=\"Sy3GnD-ZVnJThFurnhwo-28\" target=\"Sy3GnD-ZVnJThFurnhwo-29\" edge=\"1\">\n          <mxGeometry relative=\"1\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-28\" value=\"Build and push packages to PyPI\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"170\" y=\"910\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-29\" value=\"Create release on GitHub\" style=\"rounded=1;whiteSpace=wrap;html=1;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"170\" y=\"1010\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"Sy3GnD-ZVnJThFurnhwo-36\" value=\"GitHub Action\" style=\"text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontStyle=1;fontSize=18;fontColor=#6F9958;\" parent=\"1\" vertex=\"1\">\n          <mxGeometry x=\"325\" y=\"813\" width=\"60\" height=\"30\" as=\"geometry\" />\n        </mxCell>\n        <mxCell id=\"srRZveQdFgRCeiaoivwE-1\" value=\"Create tag on&lt;div&gt;GitHub&lt;/div&gt;\" style=\"rounded=1;whiteSpace=wrap;html=1;\" vertex=\"1\" parent=\"1\">\n          <mxGeometry x=\"170\" y=\"810\" width=\"120\" height=\"60\" as=\"geometry\" />\n        </mxCell>\n      </root>\n    </mxGraphModel>\n  </diagram>\n</mxfile>\n"
  },
  {
    "path": "docs/conf.py",
    "content": "\"\"\"\nConfiguration file for the Sphinx documentation builder.\n\nThis file only contains a selection of the most common options.\nFor a full list see the documentation:\nhttps://www.sphinx-doc.org/en/master/usage/configuration.html\n\"\"\"\n# -- Path setup --------------------------------------------------------------\n\n# If extensions (or modules to document with autodoc) are in another directory,\n# add these directories to sys.path here. If the directory is relative to the\n# documentation root, use os.path.abspath to make it absolute, like shown here.\nimport datetime\nimport os\nimport shutil\nimport sys\nfrom pathlib import Path\n\nsys.path.insert(0, os.path.abspath(\".\"))\nsys.path.insert(0, os.path.abspath(\"../\"))\n\nimport pypdf as py_pkg\n\nshutil.copyfile(\"../CHANGELOG.md\", \"meta/CHANGELOG.md\")\nshutil.copyfile(\"../CONTRIBUTORS.md\", \"meta/CONTRIBUTORS.md\")\n\n# -- Project information -----------------------------------------------------\n\nproject = py_pkg.__name__\ncopyright = f\"2006 - {datetime.datetime.now(tz=datetime.timezone.utc).year}, Mathieu Fenniak and pypdf contributors\"\nauthor = \"Mathieu Fenniak\"\n\n# The version info for the project you're documenting, acts as replacement for\n# |version| and |release|, also used in various other places throughout the\n# built documents.\n#\n# The short X.Y version.\nversion = py_pkg.__version__\n# The full version, including alpha/beta/rc tags.\nrelease = py_pkg.__version__\n\n# -- General configuration ---------------------------------------------------\n# If your documentation needs a minimal Sphinx version, state it here.\nneeds_sphinx = \"4.0.0\"\n\n# Add any Sphinx extension module names here, as strings. They can be\n# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom\n# ones.\nextensions = [\n    \"sphinx.ext.autodoc\",\n    \"sphinx.ext.intersphinx\",\n    \"sphinx.ext.autosummary\",\n    \"sphinx.ext.coverage\",\n    \"sphinx.ext.mathjax\",\n    \"sphinx.ext.viewcode\",\n    \"sphinx.ext.napoleon\",\n    \"sphinx.ext.doctest\",\n    # External\n    \"myst_parser\",\n]\n\npython_version = \".\".join(map(str, sys.version_info[:2]))\nintersphinx_mapping = {\n    \"python\": (f\"https://docs.python.org/{python_version}\", None),\n    \"Pillow\": (\"https://pillow.readthedocs.io/en/latest/\", None),\n}\n\nnitpick_ignore_regex = [\n    # For reasons unclear at this stage, the io module prefixes everything with _io\n    # and this confuses sphinx\n    (\n        r\"py:class\",\n        r\"(_io.(FileIO|BytesIO|Buffered(Reader|Writer))|pypdf.*PdfDocCommon)\",\n    ),\n]\n\nautodoc_default_options = {\n    \"member-order\": \"bysource\",\n    \"members\": True,\n    \"show-inheritance\": True,\n    \"undoc-members\": True,\n}\nautodoc_inherit_docstrings = False\nautodoc_typehints_format = \"short\"\npython_use_unqualified_type_names = True\n\n# Add any paths that contain templates here, relative to this directory.\ntemplates_path = [\"_templates\"]\n\n# List of patterns, relative to source directory, that match files and\n# directories to ignore when looking for source files.\n# This pattern also affects html_static_path and html_extra_path.\nexclude_patterns = [\"_build\", \"Thumbs.db\", \".DS_Store\"]\n\n# Configure MyST extension.\nmyst_all_links_external = False\nmyst_heading_anchors = 3\n\n\n# -- Options for HTML output -------------------------------------------------\n\n# The theme to use for HTML and HTML Help pages. See the documentation for\n# a list of builtin themes.\n#\nhtml_theme = \"sphinx_rtd_theme\"\n\n# Theme options are theme-specific and customize the look and feel of a theme\n# further. For a list of options available for each theme, see the\n# documentation.\nhtml_theme_options = {\n    \"canonical_url\": \"\",\n    \"analytics_id\": \"\",\n    \"logo_only\": True,\n    \"prev_next_buttons_location\": \"bottom\",\n    \"style_external_links\": False,\n    # Toc options\n    \"collapse_navigation\": True,\n    \"sticky_navigation\": True,\n    \"navigation_depth\": 4,\n    \"includehidden\": True,\n    \"titles_only\": False,\n}\nhtml_logo = \"_static/logo.png\"\n\n\n# Add any paths that contain custom static files (such as style sheets) here,\n# relative to this directory. They are copied after the builtin static files,\n# so a file named \"default.css\" will overwrite the builtin \"default.css\".\nhtml_static_path = [\"_static\"]\n\n# -- Options for Napoleon  -----------------------------------------------------\n\nnapoleon_google_docstring = True\nnapoleon_numpy_docstring = False  # Explicitly prefer Google style docstring\nnapoleon_use_param = True  # for type hint support\nnapoleon_use_rtype = False  # False, so the return type is inline with the description.\n\n# -- Options for Doctest  ------------------------------------------------------\n\n# Most of doc examples use hardcoded input and output file names.\n# To execute these examples real files need to be read and written.\n#\n# By default, documentation examples run with the working directory set to where\n# \"sphinx-build\" command was invoked. To avoid relative paths in docs and to\n# allow to run \"sphinx-build\" command from any directory, we modify the current\n# working directory in each tested file. Tests are executed against our\n# temporary directory where we have copied all nessesary resources.\n#\n# Each doc page that requires file operations must use \"testsetup\" directive\n# to call \"pypdf_test_setup\" function to prepare the test environment for that\n# page.\n#\n# def pypdf_test_setup(group: str, resources: dict[str, str] = {}) -> None\n#\n# Args:\n#   group: A unique name for group of tests. Typically we group tests by doc page.\n#       For each doc page we create a test folder under\n#       \"_build/doctest/pypdf_test/<group>\". This allows to avoid file name conflicts\n#       between different doc pages.\n#   resources: A dictionary of source files to copy into the test folder.\n#       Key is the destination file name (relative to the test folder).\n#       Value is the source file path (relative to the root folder).\n#\n# Examples:\n#   ```{testsetup}\n#   pypdf_test_setup(\"user/add-javascript\", {\n#       \"example.pdf\": \"../resources/example.pdf\",\n#   })\n#   ```\n\npypdf_test_src_root_dir = os.path.abspath(\".\")\npypdf_test_dst_root_dir = os.path.abspath(\"_build/doctest/pypdf_test\")\nif Path(pypdf_test_dst_root_dir).exists():\n   shutil.rmtree(pypdf_test_dst_root_dir)\nPath(pypdf_test_dst_root_dir).mkdir(parents=True)\n\ndoctest_global_setup = f\"\"\"\ndef pypdf_test_global_setup():\n    import os\n    import shutil\n    from pathlib import Path\n\n    src_root_dir = {pypdf_test_src_root_dir.__repr__()}\n    dst_root_dir = {pypdf_test_dst_root_dir.__repr__()}\n\n    global pypdf_test_orig_dir\n    pypdf_test_orig_dir = os.getcwd()\n    os.chdir(dst_root_dir)\n\n    global pypdf_test_setup\n    def pypdf_test_setup(group: str, resources: dict[str, str] = {{}}) -> None:\n        dst_dir = os.path.join(dst_root_dir, group)\n        Path(dst_dir).mkdir(parents=True)\n        os.chdir(dst_dir)\n\n        for (dst_path, src_path) in resources.items():\n            src = os.path.normpath(os.path.join(src_root_dir, src_path))\n            dst = os.path.join(dst_dir, dst_path)\n\n            shutil.copyfile(src, dst)\n\npypdf_test_global_setup()\n\"\"\"\n\ndoctest_global_cleanup = f\"\"\"\ndef pypdf_test_global_cleanup():\n    import os\n\n    dst_root_dir = {pypdf_test_dst_root_dir.__repr__()}\n\n    os.chdir(pypdf_test_orig_dir)\n\n    has_files = False\n    for name in os.listdir(dst_root_dir):\n        file_name = os.path.join(dst_root_dir, name)\n        if os.path.isfile(file_name):\n            if not has_files:\n                print(\"Docs page was not configured propery for running code examples\")\n                print(\"Please use 'pypdf_test_setup' function in 'testsetup' directive\")\n                print(\"Deleting unexpected file(s) in \" + dst_root_dir)\n                has_files = True\n            print(f\"- {{name}}\")\n            os.remove(file_name)  # Avoid side effects on other tests\n\npypdf_test_global_cleanup()\n\"\"\"\n"
  },
  {
    "path": "docs/dev/cmaps.md",
    "content": "# CMaps\n\nLooking at the cmap of \"crazyones\":\n\n```bash\npdftk crazyones.pdf output crazyones-uncomp.pdf uncompress\n```\n\nYou can see this:\n\n```text\nbegincmap\n/CMapName /T1Encoding-UTF16 def\n/CMapType 2 def\n/CIDSystemInfo <<\n  /Registry (Adobe)\n  /Ordering (UCS)\n  /Supplement 0\n>> def\n1 begincodespacerange\n<00> <FF>\nendcodespacerange\n1 beginbfchar\n<1B> <FB00>\nendbfchar\nendcmap\nCMapName currentdict /CMap defineresource pop\n```\n\n## codespacerange\n\nA codespacerange maps a complete sequence of bytes to a range of Unicode glyphs.\nIt defines a starting point:\n\n```text\n1 beginbfchar\n<1B> <FB00>\n```\n\nThat means that `1B` (Hex for 27) maps to the Unicode character [`FB00`](https://unicode-table.com/en/FB00/) - the ligature ﬀ (two lowercase f's).\n\nThe two numbers in `begincodespacerange` mean that it starts with an offset of\n0 (hence from `1B ➜ FB00`) up to an offset of FF (dec: 255), hence 1B+FF = 282\n➜ [FBFF](https://www.compart.com/de/unicode/U+FBFF).\n\nWithin the text stream, there is\n\n```text\n(The)-342(mis\\034ts.)\n```\n\n`\\034 ` is octal for the decimal value 28.\n"
  },
  {
    "path": "docs/dev/deprecations.md",
    "content": "# The Deprecation Process\n\npypdf strives to be an excellent library for its current users and for new\nones. We are careful with introducing potentially breaking changes, but we\nwill do them if they provide value for the community in the long run.\n\nWe hope and think that deprecations will not happen frequently. If they do,\nusers can rely on the following procedure.\n\n## Semantic Versioning\n\npypdf uses [semantic versioning](https://semver.org/). If you want to avoid\nbreaking changes, please use dependency pinning (also known as version pinning).\nIn Python, this is done by specifying the exact version you want to use in a\n`requirements.txt` file. A tool that can support you is `pip-compile` from\n[`pip-tools`](https://pypi.org/project/pip-tools/).\n\nIf you are using [Poetry](https://pypi.org/project/poetry/) it is done with the\n`poetry.lock` file.\n\n## How pypdf deprecates features\n\nAssume the current version of pypdf is `x.y.z`. After a discussion (e.g., via\nGitHub issues), we decided to remove a class / function / method. This is how\nwe do it:\n\n1. `x.y.(z+1)`: Add a DeprecationWarning. If there is a replacement,\n   the replacement is also introduced and the warning informs about the change\n   and when it will happen.\n   The docs let users know about the deprecation and when it will happen and the new function.\n   The CHANGELOG informs about it.\n2. `(x+1).0.0`: Remove / change the code in the breaking way by replacing\n   DeprecationWarnings by DeprecationErrors.\n   We do this to help people who didn't look at the warnings before.\n   The CHANGELOG informs about it.\n3. `(x+2).0.0`: The DeprecationErrors are removed.\n\nThis means the users have three warnings in the CHANGELOG, a DeprecationWarning\nuntil the next major release and a DeprecationError until the major release\nafter that.\n\nPlease note that adding warnings can be a breaking change for some users; most\nlikely just in the CI.\nThis means it needs to be properly documented.\n"
  },
  {
    "path": "docs/dev/documentation.md",
    "content": "# Documentation\n\nThis documentation is build with [Sphinx](https://www.sphinx-doc.org/) and\nhosted by [Read the Docs](https://about.readthedocs.com/)\n\n## Testing code snippets\n\nAlmost all python code snippets in documentation tested using Sphinx's extension\n[sphinx.ext.doctest](https://www.sphinx-doc.org/en/master/usage/extensions/doctest.html).\nThis allows to make sure that we have no typos, missed imports and other problems in:\n- code snippets marked with `testcode` directive in `*.md` files\n- code snippets from python's docstrings imported via `autoclass` directive in `*.rst` files\n\nCI pipeline is configured run Sphinx's `doctest` build automatically for each PR.\nIt is also possible to run it locally:\n\n1. First you need to install docs requirements\n\n   ```bash\n   pip install -r requirements/docs.txt\n   ```\n\n2. Change current directory\n\n   ```bash\n   cd docs\n   ```\n\n3. Run `doctest` build. It uses indirectly `sphinx-build` command line tool\n    installed with docs requrements. See\n   [Sphinx's docs](https://www.sphinx-doc.org/en/master/usage/quickstart.html#running-the-build)\n   for details.\n\n   ```bash\n   make doctest\n   ```\n\n4. If everything is okay you should see in output `Doctest summary` without failures\n\n## API Reference\n\n### Method / Function Docstrings\n\nWe use Google-Style Docstrings:\n\n```\ndef example(param1: int, param2: str) -> bool:\n    \"\"\"\n    Example function with PEP 484 type annotations.\n\n    Args:\n      param1: The first parameter.\n      param2: The second parameter.\n\n    Returns:\n      The return value. True for success, False otherwise.\n\n    Raises:\n      AttributeError: The ``Raises`` section is a list of all exceptions\n        that are relevant to the interface.\n      ValueError: If `param2` is equal to `param1`.\n\n    Examples:\n        Examples should be written in doctest format, and should illustrate how\n        to use the function.\n\n        >>> print([i for i in example_generator(4)])\n        [0, 1, 2, 3]\n    \"\"\"\n```\n\n* The order of sections is (1) Args (2) Returns (3) Raises (4) Examples\n* If there is no return value, remove the 'Returns' block\n* Properties should not have any sections\n\n\n## Issues and PRs\n\nAn issue can be used to discuss what we want to achieve.\n\nA PR can be used to discuss how we achieve it.\n\n## Commit Messages\n\nWe want to have descriptive commits in the `main` branch. For this reason, every\npull request (PR) is squashed. That means no matter how many commits a PR has,\nin the end only one combined commit will be in `main`.\n\nThe title of the PR will be used as the first line of that combined commit message.\n\nThe first comment within the commit will be used as the message body.\n\nSee [developer intro](intro.md#commit-messages) for more details.\n"
  },
  {
    "path": "docs/dev/intro.md",
    "content": "# Developer Intro\n\npypdf is a library and hence its users are developers. This document is not for\nthe users, but for people who want to work on pypdf itself.\n\n```{note}\nOur CI (continuous integration) validates that relevant standards are met with your contribution.\nEspecially for regular contributors or larger changes, it is highly recommended that you set up your own development environment\nto already cover the most important aspects locally. This greatly helps us to reduce the noise compared to when you open an untested\nPR early and use our CI to do your debugging and improvements from there. The maintainers usually receive a notification on every push\nto a branch where a corresponding PR is open, possibly hiding important notifications.\n```\n\n## Installing Requirements\n\n```\npip install -r requirements/dev.txt\n```\n\n## Running Tests\n\nSee [testing pypdf with pytest](testing.md).\n\n## The sample-files git submodule\nThe reason for having the submodule `sample-files` is that we want to keep\nthe size of the pypdf repository small while we also want to have an extensive\ntest suite. Those two goals contradict each other.\n\nThe `resources` folder should contain a select set of core examples that cover\nmost cases we typically want to test for. The `sample-files` might cover a lot\nmore edge cases, the behavior we get when file sizes get bigger, different\nPDF producers.\n\nTo get the sample-files folder, you need to execute:\n\n```\ngit submodule update --init\n```\n\n## Tools: git and pre-commit\n\nGit is a command line application for version control. If you don't know it,\nyou can [play ohmygit](https://ohmygit.org/) to learn it.\n\nGitHub is the service where the pypdf project is hosted. While git is free and\nopen source, GitHub is a paid service by Microsoft, but free in a lot of\ncases.\n\n[pre-commit](https://pypi.org/project/pre-commit/) is a command line application\nthat uses git hooks to automatically execute code. This allows you to avoid\nstyle issues and other code quality issues. After you entered `pre-commit install`\nonce in your local copy of pypdf, it will automatically be executed when\nyou `git commit`.\n\n## Commit Messages\n\nHaving a clean commit message helps people to quickly understand what the commit\nis about, without actually looking at the changes. The first line of the\ncommit message is used to [auto-generate the CHANGELOG](https://github.com/py-pdf/pypdf/blob/main/make_release.py).\nFor this reason, the format should be:\n\n```\nPREFIX: DESCRIPTION\n\nBODY\n```\n\nThe `PREFIX` can be:\n\n* `SEC`: Security improvements. Typically, an infinite loop that was possible.\n* `BUG`: A bug was fixed. Likely there are one or multiple issues. Then write in\n   the `BODY`: `Closes #123` where 123 is the issue number on GitHub.\n   It would be absolutely amazing if you could write a regression test in those\n   cases. That is a test that would fail without the fix.\n   A bug is always an issue for pypdf users - test code or CI that was fixed is\n   not considered a bug here.\n* `ENH`: A new feature! Describe in the body what it can be used for.\n* `DEP`: Deprecation. Either marking something as \"this is going to be removed\"\n   or actually removing it.\n* `PI`: A performance improvement. This could also be a reduction in the\n        file size of PDF files generated by pypdf.\n* `ROB`: A robustness change. Dealing better with broken PDF files.\n* `DOC`: A documentation change.\n* `TST`: Adding or adjusting tests.\n* `DEV`: Developer experience improvements, e.g., pre-commit or setting up CI.\n* `MAINT`: Quite a lot of different stuff. Performance improvements are, for sure,\n           the most interesting changes in here. Refactorings as well.\n* `STY`: A style change. Something that makes pypdf code more consistent.\n         Typically, a small change. It could also be better error messages for\n         end users.\n\nThe prefix is used to generate the CHANGELOG. Every PR must have exactly one -\nif you feel like several match, take the top one from this list that matches for\nyour PR.\n\n## Pull Request Size\n\nSmaller Pull Requests (PRs) are preferred as it's typically easier to merge\nthem. For example, if you have some typos, a few code-style changes, a new\nfeature, and a bug-fix, that could be three or four PRs.\n\nA PR must be complete. That means if you introduce a new feature, it must be\nfinished within the PR and have a test for that feature.\n\n## Benchmarks\n\nWe need to keep an eye on performance, and thus we have a few benchmarks.\n\nSee [py-pdf.github.io/pypdf/dev/bench](https://py-pdf.github.io/pypdf/dev/bench/)\n"
  },
  {
    "path": "docs/dev/pdf-format.md",
    "content": "# The PDF Format\n\nIt is recommended to look in the PDF specification for details and clarifications.\n\n* [PDF Specification Archive](https://pdfa.org/resource/pdf-specification-archive/)\n* [Portable Document Format Reference Manual, 1993. ISBN 0-201-62628-4](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.0.pdf)\n* [ISO 32000-1:2008 (PDF 1.7)](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)\n* ISO 32000-2:2020 (PDF 2.0)\n\n```{note}\nWe currently generate files with a header for PDF 1.3 by default. At the same time, we strive\nto support the PDF 1.7 specification.\n\nFeatures specific to PDF 2.0 might be available, but we always ensure that older versions do\nnot break due to the rather limited general PDF 2.0 support in the wild and to not break for\nold PDF files. For this reason, some historical aspects (like insecure encryption algorithms)\nare required to be supported, although PDF 2.0 deprecates most of them and allows more secure\nvariants.\n```\n\nBelow is only intended to give a very rough overview of the format.\n\n## Overall Structure\n\nA PDF consists of:\n\n1. Header: Contains the version of the PDF, e.g. `%PDF-1.7`\n2. Body: Contains a sequence of indirect objects\n3. Cross-reference table (xref): Contains a list of the indirect objects in the body\n4. Trailer\n\n## The xref table\n\nA cross-reference table (xref) is a table of the indirect objects in the body.\nIt allows quick access to those objects by pointing to their location in the file.\n\nIt looks like this:\n\n```text\nxref 42 5\n0000001000 65535 f\n0000001234 00000 n\n0000001987 00000 n\n0000011987 00000 n\n0000031987 00000 n\n```\n\nLet's go through it step-by-step:\n\n* `xref` is just a keyword that specifies the start of the xref table.\n* `42` is the numerical ID of the first object in this xref section; `5` is the number of entries in the xref table.\n* Now every object has 3 entries `nnnnnnnnnn ggggg n`: a 10-digit byte offset,\n  a 5-digit generation number, and a literal keyword which is either `n` or `f`.\n    * `nnnnnnnnnn` is the byte offset of the object. It tells the reader where\n      the object is in the file.\n    * `ggggg` is the generation number. It tells the reader how old the object is.\n    * `n` means that the object is a normal in-use object, `f` means that the object\n      is a free object.\n        * The first free object always has a generation number of 65535. It forms\n          the head of a linked-list of all free objects.\n        * The generation number of a normal object is always 0. The generation\n          number allows the PDF format to contain multiple versions of the same\n          object. This is a version history mechanism.\n\n## The body\n\nThe body is a sequence of indirect objects:\n\n`counter generation_number << the_object >> endobj`\n\n* `counter` (integer) is a unique identifier for the object.\n* `generation_number` (integer) is the generation number of the object.\n* `the_object` is the object itself. It can be empty. Starts with `/Keyword` to\n  specify which kind of object it is.\n* `endobj` marks the end of the object.\n\nA concrete example can be found in `test_reader.py::test_get_images_raw`:\n\n```text\n1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\n2 0 obj << >> endobj\n3 0 obj << >> endobj\n4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]\n /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R\n /Resources << /Font << >> >>\n /Rotate 0 /Type /Page >> endobj\n5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\n```\n\n## The trailer\n\nThe trailer looks like this:\n\n```text\ntrailer << /Root 5 0 R\n           /Size 6\n        >>\nstartxref 1234\n%%EOF\n```\n\nLet's go through it:\n\n* `trailer <<` indicates that the *trailer dictionary* starts. It ends with `>>`.\n* `startxref` is a keyword followed by the byte-location of the `xref` keyword.\n  As the trailer is always at the bottom of the file, this allows readers to\n  quickly find the xref table.\n* `%%EOF` is the end-of-file marker.\n\nThe trailer dictionary is a key-value list. The keys are specified in\nTable 15 of the PDF Reference 1.7, e.g. `/Root` and `/Size` (both are required).\n\n* `/Root` (dictionary) contains the document catalog.\n    * The `5` is the object number of the catalog dictionary.\n    * `0` is the generation number of the catalog dictionary.\n    * `R` is the keyword that indicates that the object is a reference to the\n      catalog dictionary.\n* `/Size` (integer) contains the total number of entries in the files xref table.\n\n\n## Reading PDF files\n\nMost PDF files are compressed. If you want to read them, first uncompress them:\n\n```bash\npdftk crazyones.pdf output crazyones-uncomp.pdf uncompress\n```\n\nThen rename `crazyones-uncomp.pdf` to `crazyones-uncomp.txt` and open it in\nyour favorite IDE / text editor.\n"
  },
  {
    "path": "docs/dev/pypdf-parsing.md",
    "content": "# How pypdf parses PDF files\n\npypdf uses {class}`~pypdf.PdfReader` to parse PDF files.\nThe method {py:meth}`PdfReader.read <pypdf.PdfReader.read>` shows the basic\nstructure of parsing:\n\n1. **Finding and reading the cross-reference tables / trailer**: The\n   cross-reference table (xref table) is a table of byte offsets that indicate\n   the locations of objects within the file. The trailer provides additional\n   information such as the root object (Catalog) and the Info object containing\n   metadata.\n2. **Parsing the objects**: After locating the xref table and the trailer, pypdf\n   proceeds to parse the objects in the PDF. Objects in a PDF can be of various\n   types such as dictionaries, arrays, streams, and simple data types (e.g.,\n   integers, strings). pypdf parses these objects and stores them in\n   {py:meth}`PdfReader.resolved_objects <pypdf.PdfReader.resolved_objects>`,\n   populated by {py:meth}`cache_indirect_object <pypdf.PdfReader.cache_indirect_object>`.\n3. **Decoding content streams**: The content of a PDF is typically stored in\n   content streams, which are sequences of PDF operators and operands. pypdf\n   decodes these content streams by applying filters (e.g., `FlateDecode`,\n   `LZWDecode`) specified in the stream's dictionary. This is only done when the\n   object is requested by {py:meth}`PdfReader.get_object <pypdf.PdfReader.get_object>`\n   which uses the `PdfReader._get_object_from_stream` method.\n\n## References\n\n[PDF 1.7 specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf):\n* 7.5 File Structure\n* 7.5.4 Cross-Reference Table\n* 7.8 Content Streams and Resources\n"
  },
  {
    "path": "docs/dev/pypdf-writing.md",
    "content": "# How pypdf writes PDF files\n\npypdf uses {py:class}`PdfWriter <pypdf.PdfWriter>` to write PDF files. pypdf has\n{py:class}`PdfObject <pypdf.generic.PdfObject>` and several subclasses with the\n{py:meth}`write_to_stream <pypdf.generic.PdfObject.write_to_stream>` method.\nThe {py:meth}`PdfWriter.write <pypdf.PdfWriter.write>` method uses the\n`write_to_stream` methods of the referenced objects.\n\nThe {py:meth}`PdfWriter.write_stream <pypdf.PdfWriter.write_stream>` method\nhas the following core steps:\n\n1. `_sweep_indirect_references`: This step ensures that any circular references\n   to objects are correctly handled. It adds the object reference numbers of any\n   circularly referenced objects to an external reference map, so that\n   self-page-referencing trees can reference the correct new object location,\n   rather than copying in a new copy of the page object.\n2. **Write the File Header and Body** with `_write_pdf_structure`: In this step,\n   the PDF header and objects are written to the output stream. This includes\n   the PDF version (e.g., %PDF-1.7) and the objects that make up the content of\n   the PDF, such as pages, annotations, and form fields. The locations (byte\n   offsets) of these objects are stored for later use in generating the xref\n   table.\n3. **Write the Cross-Reference Table** with `_write_xref_table`: Using the stored\n   object locations, this step generates and writes the cross-reference table\n   (xref table) to the output stream. The cross-reference table contains the\n   byte offsets for each object in the PDF file, allowing for quick random\n   access to objects when reading the PDF.\n4. **Write the File Trailer** with `_write_trailer`: The trailer is written to\n   the output stream in this step. The trailer contains essential information,\n   such as the number of objects in the PDF, the location of the root object\n   (Catalog), and the Info object containing metadata. The trailer also\n   specifies the location of the xref table.\n\n\n## How others do it\n\nLooking at alternative software designs and implementations can help to improve\nour choices.\n\n### fpdf2\n\n[fpdf2](https://pypi.org/project/fpdf2/) has a [`PDFObject` class](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/syntax.py)\nwith a serialize method which roughly maps to `pypdf.PdfObject.write_to_stream`.\nSome other similarities include:\n\n* [fpdf.output.OutputProducer.buffersize](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/output.py#L370-L485) vs. {py:meth}`pypdf.PdfWriter.write_stream <pypdf.PdfWriter.write_stream>`\n* [fpdpf.syntax.Name](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/syntax.py#L124) vs. {py:class}`pypdf.generic.NameObject <pypdf.generic.NameObject>`\n* [fpdf.syntax.build_obj_dict](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/syntax.py#L222) vs. {py:class}`pypdf.generic.DictionaryObject <pypdf.generic.DictionaryObject>`\n* [fpdf.structure_tree.NumberTree](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/structure_tree.py#L17) vs. {py:class}`pypdf.generic.TreeObject <pypdf.generic.TreeObject>`\n\n\n### pdfrw\n\n[pdfrw](https://pypi.org/project/pdfrw/), in contrast, seems to work more with\nthe standard Python objects (bool, float, string) and not wrap them in custom\nobjects, if possible. It still has:\n\n* [PdfArray](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfarray.py#L13)\n* [PdfDict](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfdict.py#L49)\n* [PdfName](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfname.py#L65)\n* [PdfString](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfstring.py#L322)\n* [PdfIndirect](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfindirect.py#L10)\n\nThe core classes of pdfrw are\n[PdfReader](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/pdfreader.py#L26)\nand\n[PdfWriter](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/pdfwriter.py#L224)\n"
  },
  {
    "path": "docs/dev/releasing.md",
    "content": "# Releasing\n\nA `pypdf` release contains the following artifacts:\n\n* A new [release on PyPI](https://pypi.org/project/pypdf/)\n* A [release commit](https://github.com/py-pdf/pypdf/commit/91391b18bb8ec9e6e561e2795d988e8634a01a50)\n    * Containing a changelog update\n    * A new [git tag](https://github.com/py-pdf/pypdf/tags)\n        * A [GitHub release](https://github.com/py-pdf/pypdf/releases/tag/3.15.0)\n\n## Who does it?\n\n`pypdf` should typically only be released by one of the core maintainers / the\ncore maintainer. At the moment, this usually is stefan6419846.\n\nAny owner of the py-pdf organization also has the technical permissions to\nrelease.\n\n## How is it done?\n\n### With direct push permissions\n\nThis is the typical way for the core maintainer/benevolent dictator.\n\nThe release contains the following steps:\n\n1. Update the CHANGELOG.md and the _version.py via `python make_release.py`.\n   This also prepares the release commit message.\n2. Create a release commit: `git commit -eF RELEASE_COMMIT_MSG.md`.\n3. Push commit: `git push`.\n4. Create the tag: `git tag -s 6.7.1 -eF RELEASE_COMMIT_MSG.md`.\n5. Push the tag: `git push origin 6.7.1`.\n6. CI now builds a source and a wheels package which it pushes to PyPI. It also\n   creates the corresponding GitHub release.\n\n![](../_static/releasing.drawio.png)\n\n### Using a Pull Request\n\nThis is the typical way for collaborators which do not have direct push permissions for\nthe `main` branch.\n\nThe release contains the following steps:\n\n1. Update the CHANGELOG.md and the _version.py via `python make_release.py`.\n   This also prepares the release commit message.\n2. Push the changes to a dedicated branch.\n3. Open a pull request starting with `REL: `, followed by the new version number.\n4. Wait for the approval of another eligible maintainer.\n5. Merge the pull request with the name being the PR title and the body being\n   the content of `RELEASE_COMMIT_MSG.md`.\n6. Create the tag: `git tag -s 6.7.1 -eF RELEASE_COMMIT_MSG.md`.\n7. Push the tag: `git push origin 6.7.1`.\n8. CI now builds a source and a wheels package which it pushes to PyPI. It also\n   creates the corresponding GitHub release.\n\n### The Release Tag\n\n* Use the release version as the tag name. No need for a leading \"v\".\n* Use the changelog entry as the body.\n\n\n## When are releases done?\n\nThere is no need to wait for anything. If the CI is green (all tests succeeded),\nwe can release.\n\nAt the moment, there is no fixed release cycle - except that we usually release\non Sunday.\n"
  },
  {
    "path": "docs/dev/testing.md",
    "content": "# Testing\n\npypdf uses [`pytest`](https://docs.pytest.org/en/7.1.x/) for testing.\n\nTo run the tests, you need to install the CI (Continuous Integration) requirements by running `pip install -r requirements/ci.txt` or\n`pip install -r requirements/ci-3.11.txt` if running Python ≥ 3.11.\n\n## Deselecting groups of tests\n\npypdf makes use of the following pytest markers:\n\n* `slow`: Tests that require more than 5 seconds.\n* `samples`: Tests that require [the `sample-files` git submodule](https://github.com/py-pdf/sample-files) to be initialized. As of October 2022, this is about 25 MB.\n* `enable_socket`: Tests that download PDF documents. They are stored locally and thus only need to be downloaded once. As of October 2022, this is about 200 MB.\n  * To successfully run the tests, please download most of the documents beforehand: `python -c \"from tests import download_test_pdfs; download_test_pdfs()\"`\n\nYou can disable them by `pytest -m \"not enable_socket\"` or `pytest -m \"not samples\"`.\nYou can even disable all of them: `pytest -m \"not enable_socket\" -m \"not samples\" -m \"not slow\"`.\n\nPlease note that this reduces test coverage. The CI will always test all files.\n\n## Docstrings in Unit tests\n\nThe first line of a docstring in a unit test should be written in a way that\nyou could prefix it with \"This tests ensures that ...\", e.g.\n\n* Invalid XML in xmp_metadata is gracefully handled.\n* The identity is returning its input.\n* xmp_modify_date is extracted correctly.\n\nThis way, plugins like [`pytest-testdox`](https://pypi.org/project/pytest-testdox/)\ncan generate really nice output when the tests are running. This looks similar\nto the output of [mocha.js](https://mochajs.org/).\n\nIf the test is a regression test, write\n\n> This test is a regression test for issue #1234\n\nIf the regression test is just one parameter of other tests, then add it as\na comment for that parameter.\n\n## Evaluate a PR in-progress version\n\nYou may want to test a version from a PR which has not been released yet.\nThe easiest way is to use pip and install a version from git:\n\na) Go the PR and identify the repository and branch.\n\nExample from below : repository: __pubpub-zz__ / branch: __iss2200__ :\n![PR Header example](PR_Header_example.png)\n\nb) you can then install the version using pip from git:\n\nExample:\n```\npip install git+https://github.com/pubpub-zz/pypdf.git@iss2200\n```\n"
  },
  {
    "path": "docs/index.rst",
    "content": ".. pypdf documentation main file, created by\n   sphinx-quickstart on Thu Apr  7 20:13:19 2022.\n   You can adapt this file completely to your liking, but it should at least\n   contain the root `toctree` directive.\n\nWelcome to pypdf\n=================\n\npypdf is a `free <https://en.wikipedia.org/wiki/Free_software>`_ and open\nsource pure-python PDF library capable of splitting,\nmerging, cropping, and transforming the pages of PDF files. It can also add\ncustom data, viewing options, and passwords to PDF files.\npypdf can retrieve text and metadata from PDFs as well.\n\nSee `pdfly <https://github.com/py-pdf/pdfly>`_ for a CLI application that uses pypdf to interact with PDFs.\n\nYou can contribute to `pypdf on GitHub <https://github.com/py-pdf/pypdf>`_.\n\n.. toctree::\n   :caption: User Guide\n   :maxdepth: 1\n\n   user/installation\n   user/robustness\n   user/security\n   user/suppress-warnings\n   user/metadata\n   user/extract-text\n   user/post-processing-in-text-extraction\n   user/extract-images\n   user/handle-attachments\n   user/encryption-decryption\n   user/merging-pdfs\n   user/cropping-and-transforming\n   user/reading-pdf-annotations\n   user/adding-pdf-annotations\n   user/add-watermark\n   user/add-javascript\n   user/viewer-preferences\n   user/forms\n   user/handling-outlines\n   user/streaming-data\n   user/file-size\n   user/pdf-version-support\n   user/pdfa-compliance\n\n\n\n.. toctree::\n   :caption: API Reference\n   :maxdepth: 1\n\n   modules/PdfReader\n   modules/PdfWriter\n   modules/Destination\n   modules/DocumentInformation\n   modules/Field\n   modules/Fit\n   modules/PageObject\n   modules/PageRange\n   modules/PaperSize\n   modules/RectangleObject\n   modules/Transformation\n   modules/XmpInformation\n   modules/annotations\n   modules/constants\n   modules/errors\n   modules/generic\n   modules/PdfDocCommon\n\n.. toctree::\n   :caption: Developer Guide\n   :maxdepth: 1\n\n   dev/intro\n   dev/pdf-format\n   dev/pypdf-parsing\n   dev/pypdf-writing\n   dev/cmaps\n   dev/deprecations\n   dev/documentation\n   dev/testing\n   dev/releasing\n\n.. toctree::\n   :caption: About pypdf\n   :maxdepth: 1\n\n   meta/CHANGELOG\n   meta/changelog-v1\n   meta/migration-1-to-2\n   meta/project-governance\n   meta/taking-ownership\n   meta/history\n   meta/CONTRIBUTORS\n   meta/scope-of-pypdf\n   meta/comparisons\n   meta/faq\n\nIndices and tables\n==================\n\n* :ref:`genindex`\n* :ref:`modindex`\n* :ref:`search`\n"
  },
  {
    "path": "docs/make.bat",
    "content": "@ECHO OFF\r\n\r\npushd %~dp0\r\n\r\nREM Command file for Sphinx documentation\r\n\r\nif \"%SPHINXBUILD%\" == \"\" (\r\n\tset SPHINXBUILD=sphinx-build\r\n)\r\nset SOURCEDIR=.\r\nset BUILDDIR=_build\r\n\r\n%SPHINXBUILD% >NUL 2>NUL\r\nif errorlevel 9009 (\r\n\techo.\r\n\techo.The 'sphinx-build' command was not found. Make sure you have Sphinx\r\n\techo.installed, then set the SPHINXBUILD environment variable to point\r\n\techo.to the full path of the 'sphinx-build' executable. Alternatively you\r\n\techo.may add the Sphinx directory to PATH.\r\n\techo.\r\n\techo.If you don't have Sphinx installed, grab it from\r\n\techo.https://www.sphinx-doc.org/\r\n\texit /b 1\r\n)\r\n\r\nif \"%1\" == \"\" goto help\r\n\r\n%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\ngoto end\r\n\r\n:help\r\n%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O%\r\n\r\n:end\r\npopd\r\n"
  },
  {
    "path": "docs/meta/changelog-v1.md",
    "content": "# Changelog of PyPDF2 1.X\n\n## Version 1.28.4, 2022-05-29\n\nBug Fixes (BUG):\n-  XmpInformation._converter_date was unusable (#921)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.28.3...1.28.4)\n\n## Version 1.28.3, 2022-05-28\n\n### Deprecations (DEP)\n-  PEP8 renaming (#905)\n\n### Bug Fixes (BUG)\n-  XmpInformation missing method _getText (#917)\n-  Fix PendingDeprecationWarning on _merge_page (#904)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.28.2...1.28.3)\n\n## Version 1.28.2, 2022-05-23\n\n### Bug Fixes (BUG)\n-  PendingDeprecationWarning for getContents (#893)\n-  PendingDeprecationWarning on using PdfMerger (#891)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.28.1...1.28.2)\n\n## Version 1.28.1, 2022-05-22\n\n### Bug Fixes (BUG)\n-  Incorrectly show deprecation warnings on internal usage (#887)\n\n### Maintenance (MAINT)\n-  Add stacklevel=2 to deprecation warnings (#889)\n-  Remove duplicate warnings imports (#888)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.28.0...1.28.1)\n\n## Version 1.28.0, 2022-05-22\n\nThis release adds a lot of deprecation warnings in preparation of the\nPyPDF2 2.0.0 release. The changes are mostly using snake_case function-, method-,\nand variable-names as well as using properties instead of getter-methods.\n\nMaintenance (MAINT):\n-  Remove IronPython Fallback for zlib (#868)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.12...1.27.13)\n\n### Deprecations (DEP)\n\n* Make the `PyPDF2.utils` module private\n* Rename of core classes:\n  * PdfFileReader ➔ PdfReader\n  * PdfFileWriter ➔ PdfWriter\n  * PdfFileMerger ➔ PdfMerger\n* Use PEP8 conventions for function names and parameters\n* If a property and a getter-method are both present, use the property\n\n#### Details\n\nIn many places:\n  - getObject ➔ get_object\n  - writeToStream ➔ write_to_stream\n  - readFromStream ➔ read_from_stream\n\nPyPDF2.generic\n  - readObject ➔ read_object\n  - convertToInt ➔ convert_to_int\n  - DocumentInformation.getText ➔ DocumentInformation._get_text :\n    This method should typically not be used; please let me know if you need it.\n\nPdfReader class:\n  - `reader.getPage(pageNumber)` ➔ `reader.pages[page_number]`\n  - `reader.getNumPages()` / `reader.numPages` ➔ `len(reader.pages)`\n  - getDocumentInfo ➔ metadata\n  - flattenedPages attribute ➔ flattened_pages\n  - resolvedObjects attribute ➔ resolved_objects\n  - xrefIndex attribute ➔ xref_index\n  - getNamedDestinations / namedDestinations attribute ➔ named_destinations\n  - getPageLayout / pageLayout ➔ page_layout attribute\n  - getPageMode / pageMode ➔ page_mode attribute\n  - getIsEncrypted / isEncrypted ➔ is_encrypted attribute\n  - getOutlines ➔ get_outlines\n  - readObjectHeader ➔ read_object_header\n  - cacheGetIndirectObject ➔ cache_get_indirect_object\n  - cacheIndirectObject ➔ cache_indirect_object\n  - getDestinationPageNumber ➔ get_destination_page_number\n  - readNextEndLine ➔ read_next_end_line\n  - _zeroXref ➔ _zero_xref\n  - _authenticateUserPassword ➔ _authenticate_user_password\n  - _pageId2Num attribute ➔ _page_id2num\n  - _buildDestination ➔ _build_destination\n  - _buildOutline ➔ _build_outline\n  - _getPageNumberByIndirect(indirectRef) ➔ _get_page_number_by_indirect(indirect_ref)\n  - _getObjectFromStream ➔ _get_object_from_stream\n  - _decryptObject ➔ _decrypt_object\n  - _flatten(..., indirectRef) ➔ _flatten(..., indirect_ref)\n  - _buildField ➔ _build_field\n  - _checkKids ➔ _check_kids\n  - _writeField ➔ _write_field\n  - _write_field(..., fieldAttributes) ➔ _write_field(..., field_attributes)\n  - _read_xref_subsections(..., getEntry, ...) ➔ _read_xref_subsections(..., get_entry, ...)\n\nPdfWriter class:\n  - `writer.getPage(pageNumber)` ➔ `writer.pages[page_number]`\n  - `writer.getNumPages()` ➔ `len(writer.pages)`\n  - addMetadata ➔ add_metadata\n  - addPage ➔ add_page\n  - addBlankPage ➔ add_blank_page\n  - addAttachment(fname, fdata) ➔ add_attachment(filename, data)\n  - insertPage ➔ insert_page\n  - insertBlankPage ➔ insert_blank_page\n  - appendPagesFromReader ➔ append_pages_from_reader\n  - updatePageFormFieldValues ➔ update_page_form_field_values\n  - cloneReaderDocumentRoot ➔ clone_reader_document_root\n  - cloneDocumentFromReader ➔ clone_document_from_reader\n  - getReference ➔ get_reference\n  - getOutlineRoot ➔ get_outline_root\n  - getNamedDestRoot ➔ get_named_dest_root\n  - addBookmarkDestination ➔ add_bookmark_destination\n  - addBookmarkDict ➔ add_bookmark_dict\n  - addBookmark ➔ add_bookmark\n  - addNamedDestinationObject ➔ add_named_destination_object\n  - addNamedDestination ➔ add_named_destination\n  - removeLinks ➔ remove_links\n  - removeImages(ignoreByteStringObject) ➔ remove_images(ignore_byte_string_object)\n  - removeText(ignoreByteStringObject) ➔ remove_text(ignore_byte_string_object)\n  - addURI ➔ add_uri\n  - addLink ➔ add_link\n  - getPage(pageNumber) ➔ get_page(page_number)\n  - getPageLayout / setPageLayout / pageLayout ➔ page_layout attribute\n  - getPageMode / setPageMode / pageMode ➔ page_mode attribute\n  - _addObject ➔ _add_object\n  - _addPage ➔ _add_page\n  - _sweepIndirectReferences ➔ _sweep_indirect_references\n\nPdfMerger class\n  - `__init__` parameter: strict=True ➔ strict=False (the PdfFileMerger still has the old default)\n  - addMetadata ➔ add_metadata\n  - addNamedDestination ➔ add_named_destination\n  - setPageLayout ➔ set_page_layout\n  - setPageMode ➔ set_page_mode\n\nPage class:\n  - artBox / bleedBox/ cropBox/ mediaBox / trimBox ➔ artbox / bleedbox/ cropbox/ mediabox / trimbox\n    - getWidth, getHeight  ➔ width / height\n    - getLowerLeft_x / getUpperLeft_x ➔ left\n    - getUpperRight_x / getLowerRight_x ➔ right\n    - getLowerLeft_y / getLowerRight_y ➔ bottom\n    - getUpperRight_y / getUpperLeft_y ➔ top\n    - getLowerLeft / setLowerLeft ➔ lower_left property\n    - upperRight ➔ upper_right\n  - mergePage ➔ merge_page\n  - rotateClockwise / rotateCounterClockwise ➔ rotate_clockwise\n  - _mergeResources ➔ _merge_resources\n  - _contentStreamRename ➔ _content_stream_rename\n  - _pushPopGS ➔ _push_pop_gs\n  - _addTransformationMatrix ➔ _add_transformation_matrix\n  - _mergePage ➔ _merge_page\n\nXmpInformation class:\n  - getElement(..., aboutUri, ...) ➔ get_element(..., about_uri, ...)\n  - getNodesInNamespace(..., aboutUri, ...) ➔ get_nodes_in_namespace(..., aboutUri, ...)\n  - _getText ➔ _get_text\n\nutils.py:\n  - matrixMultiply ➔ matrix_multiply\n  - RC4_encrypt is moved to the security module\n\n## Version 1.27.12, 2022-05-02\n\n### Bug Fixes (BUG)\n-  _rebuild_xref_table expects trailer to be a dict (#857)\n\n### Documentation (DOC)\n-  Security Policy\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.11...1.27.12)\n\n## Version 1.27.11, 2022-05-02\n\n### Bug Fixes (BUG)\n-  Incorrectly issued xref warning/exception (#855)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.10...1.27.11)\n\n## Version 1.27.10, 2022-05-01\n\n### Robustness (ROB)\n-  Handle missing destinations in reader (#840)\n-  warn-only in readStringFromStream (#837)\n-  Fix corruption in startxref or xref table (#788 and #830)\n\n### Documentation (DOC)\n-  Project Governance (#799)\n-  History of PyPDF2\n-  PDF feature/version support (#816)\n-  More details on text parsing issues (#815)\n\n### Developer Experience (DEV)\n-  Add benchmark command to Makefile\n-  Ignore IronPython parts for code coverage (#826)\n\n### Maintenance (MAINT)\n-  Split pdf module (#836)\n-  Separated CCITTFax param parsing/decoding (#841)\n-  Update requirements files\n\n### Testing (TST)\n-  Use external repository for larger/more PDFs for testing (#820)\n-  Swap incorrect test names (#838)\n-  Add test for PdfFileReader and page properties (#835)\n-  Add tests for PyPDF2.generic (#831)\n-  Add tests for utils, form fields, PageRange (#827)\n-  Add test for ASCII85Decode (#825)\n-  Add test for FlateDecode (#823)\n-  Add test for filters.ASCIIHexDecode (#822)\n\n### Code Style (STY)\n-  Apply pre-commit (black, isort) + use snake_case variables (#832)\n-  Remove debug code (#828)\n-  Documentation, Variable names (#839)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.9...1.27.10)\n\n## Version 1.27.9, 2022-04-24\n\nA change I would like to highlight is the performance improvement for\nlarge PDF files (#808) 🎉\n\n### New Features (ENH)\n-  Add papersizes (#800)\n-  Allow setting permission flags when encrypting (#803)\n-  Allow setting form field flags (#802)\n\n### Bug Fixes (BUG)\n-  TypeError in xmp._converter_date (#813)\n-  Improve spacing for text extraction (#806)\n-  Fix PDFDocEncoding Character Set (#809)\n\n### Robustness (ROB)\n-  Use null ID when encrypted but no ID given (#812)\n-  Handle recursion error (#804)\n\n### Documentation (DOC)\n-  CMaps (#811)\n-  The PDF Format + commit prefixes (#810)\n-  Add compression example (#792)\n\n### Developer Experience (DEV)\n-  Add Benchmark for Performance Testing (#781)\n\n### Maintenance (MAINT)\n-  Validate PDF magic byte in strict mode (#814)\n-  Make PdfFileMerger.addBookmark() behave life PdfFileWriters' (#339)\n-  Quadratic runtime while parsing reduced to linear (#808)\n\n### Testing (TST)\n-  Newlines in text extraction (#807)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.8...1.27.9)\n\n## Version 1.27.8, 2022-04-21\n\n### Bug Fixes (BUG)\n-  Use 1MB as offset for readNextEndLine (#321)\n-  'PdfFileWriter' object has no attribute 'stream' (#787)\n\n### Robustness (ROB)\n-  Invalid float object; use 0 as fallback (#782)\n\n### Documentation (DOC)\n-  Robustness (#785)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.7...1.27.8)\n\n## Version 1.27.7, 2022-04-19\n\n### Bug Fixes (BUG)\n- Import exceptions from PyPDF2.errors in PyPDF2.utils (#780)\n\n### Code Style (STY)\n-  Naming in 'make_changelog.py'\n\n## Version 1.27.6, 2022-04-18\n\n### Deprecations (DEP)\n-  Remove support for Python 2.6 and older (#776)\n\n### New Features (ENH)\n-  Extract document permissions (#320)\n\n### Bug Fixes (BUG)\n-  Clip by trimBox when merging pages, which would otherwise be ignored (#240)\n-  Add overwriteWarnings parameter PdfFileMerger (#243)\n-  IndexError for getPage() of decrypted file (#359)\n-  Handle cases where decodeParms is an ArrayObject (#405)\n-  Updated PDF fields don't show up when page is written (#412)\n-  Set Linked Form Value (#414)\n-  Fix zlib -5 error for corrupt files (#603)\n-  Fix reading more than last1K for EOF (#642)\n-  Accidental import\n\n### Robustness (ROB)\n-  Allow extra whitespace before \"obj\" in readObjectHeader (#567)\n\n### Documentation (DOC)\n-  Link to pdftoc in Sample_Code (#628)\n-  Working with annotations (#764)\n-  Structure history\n\n### Developer Experience (DEV)\n-  Add issue templates (#765)\n-  Add tool to generate changelog\n\n### Maintenance (MAINT)\n-  Use grouped constants instead of string literals (#745)\n-  Add error module (#768)\n-  Use decorators for @staticmethod (#775)\n-  Split long functions (#777)\n\n### Testing (TST)\n-  Run tests in CI once with -OO Flags (#770)\n-  Filling out forms (#771)\n-  Add tests for Writer (#772)\n-  Error cases (#773)\n-  Check Error messages (#769)\n-  Regression test for issue #88\n-  Regression test for issue #327\n\n### Code Style (STY)\n-  Make variable naming more consistent in tests\n\n\n[Full changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.5...1.27.6)\n\n## Version 1.27.5, 2022-04-15\n\n### Security (SEC)\n\n- ContentStream_readInlineImage had potential infinite loop (#740)\n\n### Bug fixes (BUG)\n\n- Fix merging encrypted files (#757)\n- CCITTFaxDecode decodeParms can be an ArrayObject (#756)\n\n### Robustness improvements (ROBUST)\n\n- title sometimes None (#744)\n\n### Documentation (DOC)\n\n- Adjust short description of the package\n\n### Tests and Test setup (TST)\n\n- Rewrite JS tests from unittest to pytest (#746)\n- Increase Test coverage, mainly with filters (#756)\n- Add test for inline images (#758)\n\n### Developer Experience Improvements (DEV)\n\n- Remove unused Travis-CI configuration (#747)\n- Show code coverage (#754, #755)\n- Add mutmut (#760)\n\n### Miscellaneous\n\n- STY: Closing file handles, explicit exports, ... (#743)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.4...1.27.5)\n\n\n## Version 1.27.4, 2022-04-12\n\n### Bug fixes (BUG)\n\n- Guard formatting of `__init__.__doc__` string (#738)\n\n### Packaging (PKG)\n\n- Add more precise license field to setup (#733)\n\n### Testing (TST)\n\n- Add test for issue #297\n\n### Miscellaneous\n\n- DOC: Miscallenious ➔ Miscellaneous (Typo)\n- TST: Fix CI triggering (master ➔ main) (#739)\n- STY: Fix various style issues (#742)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.3...1.27.4)\n\n## Version 1.27.3, 2022-04-10\n\n- PKG: Make Tests not a subpackage (#728)\n- BUG: Fix ASCII85Decode.decode assertion (#729)\n- BUG: Error in Chinese character encoding (#463)\n- BUG: Code duplication in Scripts/2-up.py\n- ROBUST: Guard 'obj.writeToStream' with 'if obj is not None'\n- ROBUST: Ignore a /Prev entry with value 0 in the trailer\n- MAINT: Remove Sample_Code (#726)\n- TST: Close file handle in test_writer (#722)\n- TST: Fix test_get_images (#730)\n- DEV: Make tox use pytest and add more Python versions (#721)\n- DOC: Many (#720, #723-725, #469)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.2...1.27.3)\n\n## Version 1.27.2, 2022-04-09\n\n- Add Scripts (including `pdfcat`), Resources, Tests, and Sample_Code back to\n  PyPDF2. It was removed by accident in 1.27.0, but might get removed with 2.0.0\n  See [discussions/718](https://github.com/py-pdf/PyPDF2/discussions/718).\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.1...1.27.2)\n\n## Version 1.27.1, 2022-04-08\n\n- Fixed project links on PyPI page after migration from mstamy2\n  to MartinThoma to the py-pdf organization on GitHub\n- Documentation is now at [pypdf2.readthedocs.io](https://pypdf2.readthedocs.io/en/latest/)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.0...1.27.1)\n\n## Version 1.27.0, 2022-04-07\n\nFeatures:\n\n - Add alpha channel support for png files in Script (#614)\n\n### Bug fixes (BUG)\n\n - Fix formatWarning for filename without slash (#612)\n - Add whitespace between words for extractText() (#569, #334)\n - \"invalid escape sequence\" SyntaxError (#522)\n - Avoid error when printing warning in pythonw (#486)\n - Stream operations can be List or Dict (#665)\n\n### Documentation (DOC)\n\n - Added Scripts/pdf-image-extractor.py\n - Documentation improvements (#550, #538, #324, #426, #394)\n\n### Tests and Test setup (TST)\n\n - Add GitHub Action which automatically runs unit tests via pytest and\n   static code analysis with Flake8 (#660)\n - Add several unit tests (#661, #663)\n - Add .coveragerc to create coverage reports\n\n### Developer Experience Improvements (DEV)\n\n - Pre commit: Developers can now `pre-commit install` to avoid tiny issues like trailing whitespaces\n\n### Miscellaneous\n\n - Add the LICENSE file to the distributed packages (#288)\n - Use setuptools instead of distutils (#599)\n - Improvements for the PyPI page (#644)\n - Python 3 changes (#504, #366)\n\n[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.26.0...1.27.0)\n\n## Version 1.26.0, 2016-05-18\n\n - NOTE: Active maintenance on PyPDF2 is resuming after a hiatus\n\n - Fixed a bug where image resources where incorrectly\n   overwritten when merging pages\n\n - Added dictionary for JavaScript actions to the root (louib)\n\n - Added unit tests for the JS functionality (louib)\n\n - Add more Python 3 compatibility when reading inline images (im2703\n   and (VyacheslavHashov)\n\n - Return NullObject instead of raising error when failing to resolve\n   object (ctate)\n\n - Don't output warning for non-zeroed xref table when strict=False\n   (BenRussert)\n\n - Remove extraneous zeroes from output formatting (speedplane)\n\n - Fix bug where reading an inline image would cut off prematurely\n   in certain cases (speedplane)\n\n## Version 1.25.1, 2015-07-20\n\n - Fix bug when parsing inline images. Occurred when merging\n   certain pages with inline images\n\n - Fixed type error when creating outlines by utilizing the\n   isString() test\n\n## Version 1.25, 2015-07-07\n\nBUGFIXES:\n\n - Added Python 3 algorithm for ASCII85Decode. Fixes issue when\n   reading reportlab-generated files with Py 3 (jerickbixly)\n\n - Recognize more escape sequence which would otherwise throw an\n   exception (manuelzs, robertsoakes)\n\n - Fixed overflow error in generic.py. Occurred\n   when reading a too-large int in Python 2 (by Raja Jamwal)\n\n - Allow access to files which were encrypted with an empty\n   password. Previously threw a \"File has not been decrypted\"\n   exception (Elena Williams)\n\n - Do not attempt to decode an empty data stream. Previously\n   would cause an error in decode algorithms (vladir)\n\n - Fixed some type issues specific to Py 2 or Py 3\n\n - Fix issue when stream data begins with whitespace (soloma83)\n\n - Recognize abbreviated filter names (AlmightyOatmeal and\n   Matthew Weiss)\n\n - Copy decryption key from PdfFileReader to PdfFileMerger.\n   Allows usage of PdfFileMerger with encrypted files (twolfson)\n\n - Fixed bug which occurred when a NameObject is present at end\n   of a file stream. Threw a \"Stream has ended unexpectedly\"\n   exception (speedplane)\n\nFEATURES:\n\n - Initial work on a test suite; to be expanded in future.\n   Tests and Resources directory added, README updated (robertsoakes)\n\n - Added document cloning methods to PdfFileWriter:\n   appendPagesFromReader, cloneReaderDocumentRoot, and\n   cloneDocumentFromReader. See official documentation (robertsoakes)\n\n - Added method for writing to form fields: updatePageFormFieldValues.\n   This will be enhanced in the future. See official documentation\n   (robertsoakes)\n\n - New addAttachment method. See documentation. Support for adding\n   and extracting embedded files to be enhanced in the future\n   (moshekaplan)\n\n - Added methods to get page number of given PageObject or\n   Destination: getPageNumber and getDestinationPageNumber.\n   See documentation (mozbugbox)\n\nOTHER ENHANCEMENTS:\n\n - Enhanced type handling (Brent Amrhein)\n\n - Enhanced exception handling in NameObject (sbywater)\n\n - Enhanced extractText method output (peircej)\n\n - Better exception handling\n\n - Enhanced regex usage in NameObject class (speedplane)\n\n\n## Version 1.24, 2014-12-31\n\n - Bugfixes for reading files in Python 3 (by Anthony Tuininga and\n   pqqp)\n\n - Appropriate errors are now raised instead of infinite loops (by\n   naure and Cyrus Vafadari)\n\n - Bugfix for parsing number tokens with leading spaces (by Maxim\n   Kamenkov)\n\n - Don't crash on bad /Outlines reference (by eshellman)\n\n - Conform tabs/spaces and blank lines to PEP 8 standards\n\n - Utilize the readUntilRegex method when reading Number Objects\n   (by Brendan Jurd)\n\n - More bugfixes for Python 3 and clearer exception handling\n\n - Fixed encoding issue in merger (with eshellman)\n\n - Created separate folder for scripts\n\n\n## Version 1.23, 2014-08-11\n\n - Documentation now available at pythonhosted.org\n\n - Bugfix in pagerange.py for when `__init__.__doc__` has no value (by\n   Vladir Cruz)\n\n - Fix typos in OutlinesObject().add() (by shilluc)\n\n - Re-added a missing return statement in a utils.py method\n\n - Corrected viewing mode names (by Jason Scheirer)\n\n - New PdfFileWriter method: addJS() (by vfigueiro)\n\n - New bookmark features: color, boldness, italics, and page fit\n   (by Joshua Arnott)\n\n - New PdfFileReader method: getFields(). Used to extract field\n   information from PDFs with interactive forms. See documentation\n   for details\n\n - Converted README file to markdown format (by Stephen Bussard)\n\n - Several improvements to overall performance and efficiency\n   (by mozbugbox)\n\n - Fixed a bug where geospatial information was not scaling along with\n   its page\n\n - Fixed a type issue and a Python 3 issue in the decryption algorithms\n   (with Francisco Vieira and koba-ninkigumi)\n\n - Fixed a bug causing an infinite loop in the ASCII 85 decoding\n   algorithm (by madmaardigan)\n\n - Annotations (links, comment windows, etc.) are now preserved when\n   pages are merged together\n\n - Used the Destination class in addLink() and addBookmark() so that\n   the page fit option could be properly customized\n\n\n## Version 1.22, 2014-05-29\n\n - Added .DS_Store to .gitignore (for Mac users) (by Steve Witham)\n\n - Removed `__init__()` implementation in NameObject (by Steve Witham)\n\n - Fixed bug (inf. loop) when merging pages in Python 3 (by commx)\n\n - Corrected error when calculating height in scaleTo()\n\n - Removed unnecessary code from DictionaryObject (by Georges Dubus)\n\n - Fixed bug where an exception was thrown upon reading a NULL string\n   (by speedplane)\n\n - Allow string literals (non-unicode strings in Python 2) to be passed\n   to PdfFileReader\n\n - Allow ConvertFunctionsToVirtualList to be indexed with slices and\n   longs (in Python 2) (by Matt Gilson)\n\n - Major improvements and bugfixes to addLink() method (see documentation\n   in source code) (by Henry Keiter)\n\n - General code clean-up and improvements (with Steve Witham and Henry Keiter)\n\n - Fixed bug that caused crash when comments are present at end of\n   dictionary\n\n\n## Version 1.21, 2014-04-21\n\n - Fix for when /Type isn't present in the Pages dictionary (by Rob1080)\n\n - More tolerance for extra whitespace in Indirect Objects\n\n - Improved Exception handling\n\n - Fixed error in getHeight() method (by Simon Kaempflein)\n\n - implement use of utils.string_type to resolve Py2-3 compatibility issues\n\n - Prevent exception for multiple definitions in a dictionary (with carlosfunk)\n   (only when strict = False)\n\n - Fixed errors when parsing a slice using pdfcat on command line (by\n   Steve Witham)\n\n - Tolerance for EOF markers within 1024 bytes of the actual end of the\n   file (with David Wolever)\n\n - Added overwriteWarnings parameter to PdfFileReader constructor, if False\n   PyPDF2 will NOT overwrite methods from Python's warnings.py module with\n   a custom implementation.\n\n - Fix NumberObject and NameObject constructors for compatibility with PyPy\n   (Rüdiger Jungbeck, Xavier Dupré, shezadkhan137, Steven Witham)\n\n - Utilize  utils.Str in pdf.py and pagerange.py to resolve type issues (by\n   egbutter)\n\n - Improvements in implementing StringIO for Python 2 and BytesIO for\n   Python 3 (by Xavier Dupré)\n\n - Added /x00 to Whitespaces, defined utils.WHITESPACES to clarify code (by\n   Maxim Kamenkov)\n\n - Bugfix for merging 3 or more resources with the same name (by lucky-user)\n\n - Improvements to Xref parsing algorithm (by speedplane)\n\n\n## Version 1.20, 2014-01-27\n\n - Official Python 3+ support (with contributions from TWAC and cgammans)\n   Support for Python versions 2.6 and 2.7 will be maintained\n\n - Command line concatenation (see pdfcat in sample code) (by Steve Witham)\n\n - New FAQ; link included in README\n\n - Allow more (although unnecessary) escape sequences\n\n - Prevent exception when reading a null object in decoding parameters\n\n - Corrected error in reading destination types (added a slash since they\n   are name objects)\n\n - Corrected TypeError in scaleTo() method\n\n - addBookmark() method in PdfFileMerger now returns bookmark (so nested\n   bookmarks can be created)\n\n - Additions to Sample Code and Sample PDFs\n\n - changes to allow 2up script to work (see sample code) (by Dylan McNamee)\n\n - changes to metadata encoding (by Chris Hiestand)\n\n - New methods for links: addLink() (by Enrico Lambertini) and removeLinks()\n\n - Bugfix to handle nested bookmarks correctly (by Jamie Lentin)\n\n - New methods removeImages() and removeText() available for PdfFileWriter\n   (by Tien Haï)\n\n - Exception handling for illegal characters in Name Objects\n\n\n## Version 1.19, 2013-10-08\n\nBUGFIXES:\n - Removed pop in sweepIndirectReferences to prevent infinite loop\n   (provided by ian-su-sirca)\n\n - Fixed bug caused by whitespace when parsing PDFs generated by AutoCad\n\n - Fixed a bug caused by reading a 'null' ASCII value in a dictionary\n   object (primarily in PDFs generated by AutoCad).\n\nFEATURES:\n - Added new folders for PyPDF2 sample code and example PDFs; see README\n   for each folder\n\n - Added a method for debugging purposes to show current location while\n   parsing\n\n - Ability to create custom metadata (by jamma313)\n\n - Ability to access and customize document layout and view mode\n   (by Joshua Arnott)\n\nOTHER:\n - Added and corrected some documentation\n\n - Added some more warnings and exception messages\n\n - Removed old test/debugging code\n\nUPCOMING:\n - More bugfixes (We have received many problematic PDFs via email, we\n   will work with them)\n\n - Documentation - It's time for PyPDF2 to get its own documentation\n   since it has grown much since the original pyPdf\n\n - A FAQ to answer common questions\n\n\n## Version 1.18, 2013-08-19\n\n - Fixed a bug where older versions of objects were incorrectly added to the\n   cache, resulting in outdated or missing pages, images, and other objects\n   (from speedplane)\n\n - Fixed a bug in parsing the xref table where new xref values were\n   overwritten; also cleaned up code (from speedplane)\n\n - New method mergeRotatedAroundPointPage which merges a page while rotating\n   it around a point (from speedplane)\n\n - Updated Destination syntax to respect PDF 1.6 specifications (from\n   jamma313)\n\n - Prevented infinite loop when a PdfFileReader object was instantiated\n   with an empty file (from Jerome Nexedi)\n\nOther Changes:\n\n - Downloads now available via PyPI\n - Installation through pip library is fixed\n\n\n## Version 1.17, 2013-07-25\n\n - Removed one (from pdf.py) of the two Destination classes. Both\n   classes had the same name, but were slightly different in content,\n   causing some errors. (from Janne Vanhala)\n\n - Corrected and Expanded README file to demonstrate PdfFileMerger\n\n - Added filter for LZW encoded streams (from Michal Horejsek)\n\n - PyPDF2 issue tracker enabled on Github to allow community\n   discussion and collaboration\n\n\n## Versions -1.16, -2013-06-30\n\n - Note: This ChangeLog has not been kept up-to-date for a while.\n   Hopefully we can keep better track of it from now on. Some of the\n   changes listed here come from previous versions 1.14 and 1.15; they\n   were only vaguely defined. With the new _version.py file we should\n   have more structured and better documented versioning from now on.\n\n - Defined `PyPDF2.__version__`\n\n - Fixed encrypt() method (from Martijn The)\n\n - Improved error handling on PDFs with truncated streams (from cecilkorik)\n\n - Python 3 support (from kushal-kumaran)\n\n - Fixed example code in README (from Jeremy Bethmont)\n\n - Fixed an bug caused by DecimalError Exception (from Adam Morris)\n\n - Many other bug fixes and features by:\n\n\tjeansch\n\tAnton Vlasenko\n\tJoseph Walton\n\tJan Oliver Oelerich\n\tFabian Henze\n\tAnd any others I missed.\n\tThanks for contributing!\n\n\n## Version 1.13, 2010-12-04\n\n - Fixed a typo in code for reading a \"\\b\" escape character in strings.\n\n - Improved `__repr__` in FloatObject.\n\n - Fixed a bug in reading octal escape sequences in strings.\n\n - Added getWidth and getHeight methods to the RectangleObject class.\n\n - Fixed compatibility warnings with Python 2.4 and 2.5.\n\n - Added addBlankPage and insertBlankPage methods on PdfFileWriter class.\n\n - Fixed a bug with circular references in page's object trees (typically\n   annotations) that prevented correctly writing out a copy of those pages.\n\n - New merge page functions allow application of a transformation matrix.\n\n - To all patch contributors: I did a poor job of keeping this ChangeLog\n   up-to-date for this release, so I am missing attributions here for any\n   changes you submitted.  Sorry!  I'll do better in the future.\n\n\n## Version 1.12, 2008-09-02\n\n - Added support for XMP metadata.\n\n - Fix reading files with xref streams with multiple /Index values.\n\n - Fix extracting content streams that use graphics operators longer than 2\n   characters.  Affects merging PDF files.\n\n\n## Version 1.11, 2008-05-09\n\n - Patch from Hartmut Goebel to permit RectangleObjects to accept NumberObject\n   or FloatObject values.\n\n - PDF compatibility fixes.\n\n - Fix to read object xref stream in correct order.\n\n - Fix for comments inside content streams.\n\n\n## Version 1.10, 2007-10-04\n\n - Text strings from PDF files are returned as Unicode string objects when\n pyPdf determines that they can be decoded (as UTF-16 strings, or as\n PDFDocEncoding strings).  Unicode objects are also written out when\n necessary.  This means that string objects in pyPdf can be either\n generic.ByteStringObject instances, or generic.TextStringObject instances.\n\n - The extractText method now returns a unicode string object.\n\n - All document information properties now return unicode string objects.  In\n the event that a document provides docinfo properties that are not decoded by\n pyPdf, the raw byte strings can be accessed with an \"_raw\" property (ie.\n title_raw rather than title)\n\n - generic.DictionaryObject instances have been enhanced to be easier to use.\n Values coming out of dictionary objects will automatically be de-referenced\n (.getObject will be called on them), unless accessed by the new \"raw_get\"\n method.  DictionaryObjects can now only contain PdfObject instances (as keys\n and values), making it easier to debug where non-PdfObject values (which\n cannot be written out) are entering dictionaries.\n\n - Support for reading named destinations and outlines in PDF files.  Original\n patch by Ashish Kulkarni.\n\n - Stream compatibility reading enhancements for malformed PDF files.\n\n - Cross reference table reading enhancements for malformed PDF files.\n\n - Encryption documentation.\n\n - Replace some \"assert\" statements with error raising.\n\n - Minor optimizations to FlateDecode algorithm increase speed when using PNG\n predictors.\n\n## Version 1.9, 2006-12-15\n\n - Fix several serious bugs introduced in version 1.8, caused by a failure to\n   run through our PDF test suite before releasing that version.\n\n - Fix bug in NullObject reading and writing.\n\n## Version 1.8, 2006-12-14\n\n - Add support for decryption with the standard PDF security handler.  This\n   allows for decrypting PDF files given the proper user or owner password.\n\n - Add support for encryption with the standard PDF security handler.\n\n - Add new pythondoc documentation.\n\n - Fix bug in ASCII85 decode that occurs when whitespace exists inside the\n   two terminating characters of the stream.\n\n## Version 1.7, 2006-12-10\n\n - Fix a bug when using a single page object in two PdfFileWriter objects.\n\n - Adjust PyPDF to be tolerant of whitespace characters that don't belong\n   during a stream object.\n\n - Add documentInfo property to PdfFileReader.\n\n - Add numPages property to PdfFileReader.\n\n - Add pages property to PdfFileReader.\n\n - Add extractText function to PdfFileReader.\n\n\n## Version 1.6, 2006-06-06\n\n - Add basic support for comments in PDF files.  This allows us to read some\n   ReportLab PDFs that could not be read before.\n\n - Add \"auto-repair\" for finding xref table at slightly bad locations.\n\n - New StreamObject backend, cleaner and more powerful.  Allows the use of\n   stream filters more easily, including compressed streams.\n\n - Add a graphics state push/pop around page merges.  Improves quality of\n   page merges when one page's content stream leaves the graphics\n   in an abnormal state.\n\n - Add PageObject.compressContentStreams function, which filters all content\n   streams and compresses them.  This will reduce the size of PDF pages,\n   especially after they could have been decompressed in a mergePage\n   operation.\n\n - Support inline images in PDF content streams.\n\n - Add support for using .NET framework compression when zlib is not\n   available.  This does not make pyPdf compatible with IronPython, but it\n   is a first step.\n\n - Add support for reading the document information dictionary, and extracting\n   title, author, subject, producer and creator tags.\n\n - Add patch to support NullObject and multiple xref streams, from Bradley\n   Lawrence.\n\n\n## Version 1.5, 2006-01-28\n\n- Fix a bug where merging pages did not work in \"no-rename\" cases when the\n  second page has an array of content streams.\n\n- Remove some debugging output that should not have been present.\n\n\n## Version 1.4, 2006-01-27\n\n- Add capability to merge pages from multiple PDF files into a single page\n  using the PageObject.mergePage function.  See example code (README or web\n  site) for more information.\n\n- Add ability to modify a page's MediaBox, CropBox, BleedBox, TrimBox, and\n  ArtBox properties through PageObject.  See example code (README or web site)\n  for more information.\n\n- Refactor pdf.py into multiple files: generic.py (contains objects like\n  NameObject, DictionaryObject), filters.py (contains filter code),\n  utils.py (various).  This does not affect importing PdfFileReader\n  or PdfFileWriter.\n\n- Add new decoding functions for standard PDF filters ASCIIHexDecode and\n  ASCII85Decode.\n\n- Change url and download_url to refer to new pybrary.net web site.\n\n\n## Version 1.3, 2006-01-23\n\n- Fix new bug introduced in 1.2 where PDF files with \\r line endings did not\n  work properly anymore.  A new test suite developed with various PDF files\n  should prevent regression bugs from now on.\n\n- Fix a bug where inheriting attributes from page nodes did not work.\n\n\n## Version 1.2, 2006-01-23\n\n- Improved support for files with CRLF-based line endings, fixing a common\n  reported problem stating \"assertion error: assert line == \"%%EOF\"\".\n\n- Software author/maintainer is now officially a proud married person, which\n  is sure to result in better software... somehow.\n\n\n## Version 1.1, 2006-01-18\n\n- Add capability to rotate pages.\n\n- Improved PDF reading support to properly manage inherited attributes from\n  /Type=/Pages nodes.  This means that page groups that are rotated or have\n  different media boxes or whatever will now work properly.\n\n- Added PDF 1.5 support.  Namely cross-reference streams and object streams.\n  This release can mangle Adobe's PDFReference16.pdf successfully.\n\n\n## Version 1.0, 2006-01-17\n\n- First distutils-capable true public release.  Supports a wide variety of PDF\n  files that I found sitting around on my system.\n\n- Does not support some PDF 1.5 features, such as object streams,\n  cross-reference streams.\n"
  },
  {
    "path": "docs/meta/comparisons.md",
    "content": "# pypdf vs X\n\npypdf is a [free] and open source pure-python PDF library capable of\nsplitting, merging, cropping, and transforming the pages of PDF files.\nIt can also add custom data, viewing options, and passwords to PDF\nfiles. pypdf can retrieve text and metadata from PDFs as well.\n\n## PyMuPDF and PikePDF\n\n[PyMuPDF] is a Python binding to [MuPDF] and [PikePDF] is the Python\nbinding to [QPDF].\n\nWhile both are excellent libraries for various use-cases, using them is\nnot always possible even when they support the use-case. Both of them\nare powered by C libraries which make installation harder and might\ncause security concerns. For MuPDF, you might also need to buy a\ncommercial license.\n\nA core feature of pypdf is that it's pure Python. That means there is\nno C dependency. It has been used for over 10 years and for this reason\na lot of support via StackOverflow and examples on the internet.\n\n## pypdf\n\nPyPDF2 was merged back into `pypdf`. The development continues at `pypdf`.\n\n## PyPDF3 and PyPDF4\n\nDeveloping and maintaining open source software is extremely\ntime-intensive and in the case of pypdf not paid at all. Having\ncontinuous support is hard.\n\npypdf was initially released in 2012 on PyPI and received releases\nuntil 2016. From 2016 to 2022, there was no update - but people were\nstill using it.\n\nAs pypdf is free software, there were attempts to fork it and continue\nthe development. PyPDF3 was first released in 2018 and still receives\nupdates. PyPDF4 has only one release from 2018.\n\nMartin Thoma has worked on bringing the community back to one path of\ndevelopment. He deprecated PyPDF2 in favor of pypdf already, and pypdf has\nmore features and a cleaner interface than PyPDF2 now. See [history of\npypdf](history.md).\n\n  [free]: https://en.wikipedia.org/wiki/Free_software\n  [PyMuPDF]: https://pypi.org/project/PyMuPDF/\n  [MuPDF]: https://mupdf.com/\n  [PikePDF]: https://pypi.org/project/pikepdf/\n  [QPDF]: https://github.com/qpdf/qpdf\n\n\n## pdfminer.six and pdfplumber\n\n[`pdfminer.six`](https://pypi.org/project/pdfminer.six/) is capable of\nextracting the [font size](https://stackoverflow.com/a/69962459/562769)\n/ font weight (bold-ness). It has no capabilities for writing PDF files.\n\n[`pdfplumber`](https://pypi.org/project/pdfplumber/) is a library focused on extracting data from PDF documents. Since `pdfplumber` is built on top of `pdfminer.six`, there are **no capabilities of exporting or modifying a PDF file** (see [#440 (discussions)](https://github.com/jsvine/pdfplumber/discussions/440#discussioncomment-803880)). However, `pdfplumber` is capable of converting a PDF file into an image, [draw lines and rectangles on the image](https://github.com/jsvine/pdfplumber#drawing-methods), and save it as an image file. Please note that the image conversion is done via ImageMagick (see [`pdfplumber`'s documentation](https://github.com/jsvine/pdfplumber#visual-debugging)).\n\nThe `pdfplumber` community is active in answering questions and the library is maintained as of May 2023.\n\n## pdfrw / pdfrw2\n\nI don't have experience with any of those libraries. Please add a\ncomparison if you know pypdf and [`pdfrw`](https://pypi.org/project/pdfrw/)!\n\nPlease be aware that there is also\n[`pdfminer`](https://pypi.org/project/pdfminer/) which is not maintained.\nThen there is [`pdfrw2`](https://pypi.org/project/pdfrw2/) which doesn't have\na large community behind it.\n\n## Document Generation\n\nThere are (Python) [tools to generate PDF documents](https://github.com/py-pdf/awesome-pdf#generators).\npypdf is not one of them.\n\n\n## CLI applications\n\npypdf is a pure Python PDF library. If you're looking for an application which\nyou can use from the terminal, give [`pdfly`](https://pdfly.readthedocs.io/en/latest/)\na shot.\n"
  },
  {
    "path": "docs/meta/faq.md",
    "content": "# Frequently Asked Questions\n\n## How is pypdf related to PyPDF2?\n\nPyPDF2 was a fork from the original pyPdf. After several years, the fork was\nmerged back into `pypdf` (now all lowercase).\n\n## Which Python versions are supported?\n\npypdf 3.0+ supports Python 3.6 and later.\nPyPDF2 2.0+ supports Python 3.6 and later.\nPyPDF2 1.27.10 supported Python 2.7 to 3.10.\n\n  [Matthew]: https://github.com/mstamy2\n  [source]: https://github.com/py-pdf/PyPDF2/commit/24b270d876518d15773224b5d0d6c2206db29f64#commitcomment-5038317\n  [this sort of thing]: https://github.com/py-pdf/PyPDF2/issues/24\n  [GitHub issue]: https://github.com/py-pdf/PyPDF2/issues\n\n## Who uses pypdf?\n\npyPdf is vendored [into](https://github.com/Buyanbat/XacCRM/tree/ee78e8df967182f661b6494a86444501e7d89c8f/report/pyPdf) [several](https://github.com/MyBook/calibre/tree/ca1efe3c21f6553e096dab745b3cdeb36244a5a9/src/pyPdf) [projects](https://github.com/Giacomo-De-Florio-Dev/Make_Your_PDF_Safe/tree/ec439f92243d12d54ae024668792470c6b40ee96/MakeYourPDFsafe_V1.3/PyPDF2). That\nmeans the code of pyPdf was copied into that project.\n\nProjects that depend on pypdf:\n\n* [Camelot](https://github.com/camelot-dev/camelot): A Python library to extract tabular data from PDFs\n* [edi](https://github.com/OCA/edi): Electronic Data Interchange modules\n* [amazon-textract-textractor](https://github.com/aws-samples/amazon-textract-textractor/blob/42444b08c672607eadbdcd64f3c5adb2d85383de/helper/setup.py): Analyze documents with Amazon Textract and generate output in multiple formats.\n* [maigret](https://github.com/soxoj/maigret): Collect a dossier on a person by username from thousands of sites\n* [deda](https://github.com/dfd-tud/deda): tracking Dots Extraction, Decoding and Anonymisation toolkit\n* [opencanary](https://github.com/thinkst/opencanary)\n* Document Conversions\n  * [rst2pdf](https://github.com/rst2pdf/rst2pdf)\n  * [xhtml2pdf](https://github.com/xhtml2pdf/xhtml2pdf)\n  * [doc2text](https://github.com/jlsutherland/doc2text)\n* [pdfalyzer](https://pypi.org/project/pdfalyzer/): A PDF analysis tool for visualizing the inner tree-like data structure of a PDF in spectacularly large and colorful diagrams as well as scanning the binary streams embedded in the PDF for hidden potentially malicious content.\n\n## How do I cite pypdf?\n\nIn BibTeX format:\n\n```\n@misc{pypdf,\n title         = {The {pypdf} library},\n author        = {Mathieu Fenniak and\n                  Matthew Stamy and\n                  pubpub-zz and\n                  Martin Thoma and\n                  Matthew Peveler and\n                  exiledkingcc and {pypdf Contributors}},\n year          = {2024},\n url           = {https://pypi.org/project/pypdf/}\n note          = {See https://pypdf.readthedocs.io/en/latest/meta/CONTRIBUTORS.html for all contributors}\n}\n```\n\n## Which License does pypdf use?\n\n`pypdf` uses the [BSD-3-Clause license](https://en.wikipedia.org/wiki/BSD_licenses#3-clause), see the LICENSE file.\n"
  },
  {
    "path": "docs/meta/history.md",
    "content": "# History of pypdf\n\n## The Origins: pyPdf (2005-2010)\n\nIn 2005, [Mathieu Fenniak] launched pyPdf \"as a PDF toolkit...\"\nfocused on\n\n-   document manipulation: by-page splitting, concatenation, and\n    merging;\n-   document introspection;\n-   page cropping; and\n-   document encryption and decryption.\n\nThe last release of PyPI was [pyPdf 1.13](https://pypi.org/project/pyPdf/#history)\nin 2010.\n\n## PyPDF2 is born (2011-2016)\n\nAt the end of 2011, after consultation with Mathieu and others, Phaseit\nsponsored PyPDF2 as a fork of pyPdf on GitHub. The initial impetus was\nto handle a wider range of input PDF instances; Phaseit\\'s commercial\nwork often encounters PDF instances \\\"in the wild\\\" that it needs to\nmanage (mostly concatenate and paginate), but that deviates so much from\nPDF standards that pyPdf can't read them. PyPDF2 reads a considerably\nwider range of real-world PDF instances.\n\nNeither pyPdf nor PyPDF2 aims to be universal, that is, to provide all\npossible PDF-related functionality. Note that the similar-appearing\n[pyfpdf] of Mariano Reingart is most comparable to [ReportLab], in that\nboth ReportLab and pyfpdf emphasize document generation. Interestingly\nenough, pyfpdf builds in a basic HTML→PDF converter while PyPDF2 has no\nknowledge of HTML.\n\nSo what is PyPDF2 truly about? Think about popular [pdftk] for a moment.\nPyPDF2 does what pdftk does, and it does so within your current Python\nprocess, and it handles a wider range of variant PDF formats\n\\[explain\\]. PyPDF2 has its own FAQ to answer other questions that have\narisen.\n\nThe Reddit [/r/python crowd chatted] obliquely and briefly about PyPDF2\nin March 2012.\n\nThe core developer / maintainer was Matthew Stamy.\n\n## PyPDF3 and PyPDF4 (2018-2022)\n\nTwo approaches were made to get PyPDF2 active again: PyPDF3 and PyPDF4.\n\nPyPDF3 had its first release in 2018 and its last one in February 2022.\nIt never got the user base from PyPDF2.\n\nPyPDF4 only had one release in 2018.\n\n## PyPDF2: Reborn (2022)\n\nMartin Thoma took over maintenance of PyPDF2 in April 2022. It had over 100\nopen PRs and 321 open issues.\n\n[pubpub-zz](https://github.com/pubpub-zz) was extremely active, especially\nfor text extraction.\n\n[Matthew Peveler](https://github.com/MasterOdin) helped a lot with reviews\nand general project decisions.\n\n[exiledkingcc](https://github.com/exiledkingcc) added support for modern\nencryption schemes.\n\n\n## pypdf: Back to the Roots (2023-2024)\n\nIn order to simplify things for beginners, PyPDF2 was merged back into\npypdf. Now all lowercase, without a number. We hope that the folks who\ndevelop PyPDF3 and PyPDF4 also join us.\n\nCompared to `PyPDF2 >= 3.0.0`, `pypdf >= 3.1.0` now offers:\n\n* AES reading and writing support. Not only with PyCryptoDome, but also with cryptography.\n* Text extraction improvements, e.g., for math content. [pypdf is now comparable with Tika, pypdfium2, and PyMuPDF](https://github.com/py-pdf/benchmarks)\n* Annotation support\n* Performance Improvements and Bugfixes\n* Page Label support\n\nstefan6419846 made his [first PR for pypdf](https://github.com/py-pdf/pypdf/pull/2022)\nin July 2023 and joined the project.\n\n\n  [Mathieu Fenniak]: https://mathieu.fenniak.net/\n  [pyfpdf]: https://github.com/reingart/pyfpdf\n  [ReportLab]: https://www.reportlab.com/software/opensource/rl-toolkit/\n  [pdftk]: https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/\n  [/r/python crowd chatted]: https://www.reddit.com/r/Python/comments/qsvfm/pypdf2_updates_pypdf_pypdf2_is_an_opensource/\n"
  },
  {
    "path": "docs/meta/migration-1-to-2.md",
    "content": "# Migration Guide: 1.x to 2.x\n\n`PyPDF2<2.0.0` ([docs](https://pypdf2.readthedocs.io/en/1.27.12/meta/history.html))\nis very different from `PyPDF2>=2.0.0` ([docs](../meta/history.md)).\n\nLuckily, most changes are simple naming adjustments. This guide helps you to\nmake the step from `PyPDF2 1.x` (or even the original PyPdf) to `PyPDF2>=2.0.0`.\n\nYou can execute your code with the updated version and show deprecation warnings\nby running `python -W all your_code.py`.\n\n## Imports and Modules\n\n* `PyPDF2.utils` no longer exists\n* `PyPDF2.pdf` no longer exists. You can import from `PyPDF2` directly or from\n  `PyPDF2.generic`\n\n## Naming Adjustments\n\n### Classes\n\nThe base classes were renamed as they also allow operating with BytesIO streams\ninstead of files. Also, the `strict` parameter changed the default value from\n`strict=True` to `strict=False`.\n\n* `PdfFileReader` ➔ `PdfReader`\n* `PdfFileWriter` ➔ `PdfWriter`\n* `PdfFileMerger` ➔ `PdfMerger`\n\nPdfFileReader and PdfFileMerger no longer have the `overwriteWarnings`\nparameter. The new behavior is `overwriteWarnings=False`.\n\n### Function, Method, and Property Names\n\nIn `PyPDF2.xmp.XmpInformation`:\n\n* `rdfRoot` ➔ `rdf_root`\n* `xmp_createDate` ➔ `xmp_create_date`\n* `xmp_creatorTool` ➔ `xmp_creator_tool`\n* `xmp_metadataDate` ➔ `xmp_metadata_date`\n* `xmp_modifyDate` ➔ `xmp_modify_date`\n* `xmpMetadata` ➔ `xmp_metadata`\n* `xmpmm_documentId` ➔ `xmpmm_document_id`\n* `xmpmm_instanceId` ➔ `xmpmm_instance_id`\n\nIn `PyPDF2.generic`:\n\n* `readObject` ➔ `read_object`\n* `convertToInt` ➔ `convert_to_int`\n* `DocumentInformation.getText` ➔ `DocumentInformation._get_text` : This method should typically not be used; please let me know if you need it.\n* `readHexStringFromStream` ➔ `read_hex_string_from_stream`\n* `initializeFromDictionary` ➔ `initialize_from_dictionary`\n* `createStringObject` ➔ `create_string_object`\n* `TreeObject.hasChildren` ➔ `TreeObject.has_children`\n* `TreeObject.emptyTree` ➔ `TreeObject.empty_tree`\n\nIn many places:\n  - `getObject` ➔ `get_object`\n  - `writeToStream` ➔ `write_to_stream`\n  - `readFromStream` ➔ `read_from_stream`\n\n\nPdfReader class:\n  - `reader.getPage(pageNumber)` ➔ `reader.pages[page_number]`\n  - `reader.getNumPages()` / `reader.numPages` ➔ `len(reader.pages)`\n  - `getDocumentInfo` ➔ `metadata`\n  - `flattenedPages` attribute ➔ `flattened_pages`\n  - `resolvedObjects` attribute ➔ `resolved_objects`\n  - `xrefIndex` attribute ➔ `xref_index`\n  - `getNamedDestinations` / `namedDestinations` attribute ➔ `named_destinations`\n  - `getPageLayout` / `pageLayout` ➔ `page_layout` attribute\n  - `getPageMode` / `pageMode` ➔ `page_mode` attribute\n  - `getIsEncrypted` / `isEncrypted` ➔ `is_encrypted` attribute\n  - `getOutlines` ➔ `get_outlines`\n  - `readObjectHeader` ➔ `read_object_header`\n  - `cacheGetIndirectObject` ➔ `cache_get_indirect_object`\n  - `cacheIndirectObject` ➔ `cache_indirect_object`\n  - `getDestinationPageNumber` ➔ `get_destination_page_number`\n  - `readNextEndLine` ➔ `read_next_end_line`\n  - `_zeroXref` ➔ `_zero_xref`\n  - `_authenticateUserPassword` ➔ `_authenticate_user_password`\n  - `_pageId2Num` attribute ➔ `_page_id2num`\n  - `_buildDestination` ➔ `_build_destination`\n  - `_buildOutline` ➔ `_build_outline`\n  - `_getPageNumberByIndirect(indirectRef)` ➔ `_get_page_number_by_indirect(indirect_ref)`\n  - `_getObjectFromStream` ➔ `_get_object_from_stream`\n  - `_decryptObject` ➔ `_decrypt_object`\n  - `_flatten(..., indirectRef)` ➔ `_flatten(..., indirect_ref)`\n  - `_buildField` ➔ `_build_field`\n  - `_checkKids` ➔ `_check_kids`\n  - `_writeField` ➔ `_write_field`\n  - `_write_field(..., fieldAttributes)` ➔ `_write_field(..., field_attributes)`\n  - `_read_xref_subsections(..., getEntry, ...)` ➔ `_read_xref_subsections(..., get_entry, ...)`\n\nPdfWriter class:\n  - `writer.getPage(pageNumber)` ➔ `writer.pages[page_number]`\n  - `writer.getNumPages()` ➔ `len(writer.pages)`\n  - `addMetadata` ➔ `add_metadata`\n  - `addPage` ➔ `add_page`\n  - `addBlankPage` ➔ `add_blank_page`\n  - `addAttachment(fname, fdata)` ➔ `add_attachment(filename, data)`\n  - `insertPage` ➔ `insert_page`\n  - `insertBlankPage` ➔ `insert_blank_page`\n  - `appendPagesFromReader` ➔ `append_pages_from_reader`\n  - `updatePageFormFieldValues` ➔ `update_page_form_field_values`\n  - `cloneReaderDocumentRoot` ➔ `clone_reader_document_root`\n  - `cloneDocumentFromReader` ➔ `clone_document_from_reader`\n  - `getReference` ➔ `get_reference`\n  - `getOutlineRoot` ➔ `get_outline_root`\n  - `getNamedDestRoot` ➔ `get_named_dest_root`\n  - `addBookmarkDestination` ➔ `add_bookmark_destination`\n  - `addBookmarkDict` ➔ `add_bookmark_dict`\n  - `addBookmark` ➔ `add_bookmark`\n  - `addNamedDestinationObject` ➔ `add_named_destination_object`\n  - `addNamedDestination` ➔ `add_named_destination`\n  - `removeLinks` ➔ `remove_links`\n  - `removeImages(ignoreByteStringObject)` ➔ `remove_images(ignore_byte_string_object)`\n  - `removeText(ignoreByteStringObject)` ➔ `remove_text(ignore_byte_string_object)`\n  - `addURI` ➔ `add_uri`\n  - `addLink` ➔ `add_link`\n  - `getPage(pageNumber)` ➔ `get_page(page_number)`\n  - `getPageLayout / setPageLayout / pageLayout` ➔ `page_layout attribute`\n  - `getPageMode / setPageMode / pageMode` ➔ `page_mode attribute`\n  - `_addObject` ➔ `_add_object`\n  - `_addPage` ➔ `_add_page`\n  - `_sweepIndirectReferences` ➔ `_sweep_indirect_references`\n\nPdfMerger class\n  - `__init__` parameter: `strict=True` ➔ `strict=False` (the `PdfFileMerger` still has the old default)\n  - `addMetadata` ➔ `add_metadata`\n  - `addNamedDestination` ➔ `add_named_destination`\n  - `setPageLayout` ➔ `set_page_layout`\n  - `setPageMode` ➔ `set_page_mode`\n\nPage class:\n  - `artBox` / `bleedBox` / `cropBox` / `mediaBox` / `trimBox` ➔ `artbox` / `bleedbox` / `cropbox` / `mediabox` / `trimbox`\n    - `getWidth`, `getHeight ` ➔ `width` / `height`\n    - `getLowerLeft_x` / `getUpperLeft_x` ➔ `left`\n    - `getUpperRight_x` / `getLowerRight_x` ➔ `right`\n    - `getLowerLeft_y` / `getLowerRight_y` ➔ `bottom`\n    - `getUpperRight_y` / `getUpperLeft_y` ➔ `top`\n    - `getLowerLeft` / `setLowerLeft` ➔ `lower_left` property\n    - `upperRight` ➔ `upper_right`\n  - `mergePage` ➔ `merge_page`\n  - `rotateClockwise` / `rotateCounterClockwise` ➔ `rotate_clockwise`\n  - `_mergeResources` ➔ `_merge_resources`\n  - `_contentStreamRename` ➔ `_content_stream_rename`\n  - `_pushPopGS` ➔ `_push_pop_gs`\n  - `_addTransformationMatrix` ➔ `_add_transformation_matrix`\n  - `_mergePage` ➔ `_merge_page`\n\nXmpInformation class:\n  - `getElement(..., aboutUri, ...)` ➔ `get_element(..., about_uri, ...)`\n  - `getNodesInNamespace(..., aboutUri, ...)` ➔ `get_nodes_in_namespace(..., aboutUri, ...)`\n  - `_getText` ➔ `_get_text`\n\nutils.py:\n  - `matrixMultiply` ➔ `matrix_multiply\n  - `RC4_encrypt` is moved to the security module\n\n### Parameter Names\n\n* `PdfWriter.get_page`: `pageNumber` ➔ `page_number`\n* `PyPDF2.filters` (all classes): `decodeParms` ➔ `decode_parms`\n* `PyPDF2.filters` (all classes): `decodeStreamData` ➔ `decode_stream_data`\n* `pagenum` ➔ `page_number`\n* `PdfMerger.merge`: `position` ➔ `page_number`\n* `PdfWriter.add_outline_item_destination`: `dest` ➔ `page_destination`\n* `PdfWriter.add_named_destination_object`: `dest` ➔ `page_destination`\n* `PdfWriter.encrypt`: `user_pwd` ➔ `user_password`\n* `PdfWriter.encrypt`: `owner_pwd` ➔ `owner_password`\n\n### Deprecations\n\nA few classes / functions were deprecated without replacement:\n\n* `PyPDF2.utils.ConvertFunctionsToVirtualList`\n* `PyPDF2.utils.formatWarning`\n* `PyPDF2.isInt(obj)`: Use `instance(obj, int)` instead\n* `PyPDF2.u_(s)`: Use `s` directly\n* `PyPDF2.chr_(c)`: Use `chr(c)` instead\n* `PyPDF2.barray(b)`: Use `bytearray(b)` instead\n* `PyPDF2.isBytes(b)`: Use `instance(b, type(bytes()))` instead\n* `PyPDF2.xrange_fn`: Use `range` instead\n* `PyPDF2.string_type`: Use `str` instead\n* `PyPDF2.isString(s)`: Use `instance(s, str)` instead\n* `PyPDF2._basestring`: Use `str` instead\n* `b_(...)` was removed. You should typically be able to use the bytes object directly, otherwise you can [copy this](https://github.com/py-pdf/PyPDF2/pull/986#issuecomment-1230698069)\n"
  },
  {
    "path": "docs/meta/project-governance.md",
    "content": "# Project Governance\n\nThis document describes how the pypdf project is managed. It describes the\ndifferent actors, their roles, and the responsibilities they have.\n\n## Terminology\n\n* The **project** is pypdf - a free and open-source pure-python PDF library\ncapable of splitting, merging, cropping, and transforming the pages of PDF files.\n  It includes the [code, issues, and discussions on GitHub](https://github.com/py-pdf/pypdf),\n  and [the documentation on ReadTheDocs](https://pypdf.readthedocs.io/en/latest/),\n  [the package on PyPI](https://pypi.org/project/pypdf/), and\n  [the website on GitHub](https://py-pdf.github.io/pypdf/dev/bench/).\n* A **maintainer** is a person who has technical permissions to change one or\n  more parts of the projects. It is a person driven to keep the project running\n  and improving.\n* A **contributor** is a person who contributes to the project. That could be\n  through writing code - in the best case through forking and creating a pull\n  request, but that is up to the maintainer. Other contributors describe issues,\n  help to ask questions on existing issues to make them easier to answer,\n  participate in discussions, and help to improve the documentation. Contributors\n  are similar to maintainers, but without technical permissions.\n* A **user** is a person who imports pypdf into their code. All pypdf users\n  are developers, but not developers who know the internals of pypdf. They only\n  use the public interface of pypdf. They will likely have less knowledge about\n  PDF than contributors.\n* The **community** is all of that - the users, the contributors, and the maintainers.\n\n\n## Governance, Leadership, and Steering pypdf forward\n\npypdf is a free and open source project with over 100 contributors and likely\n(way) more than 1000 users.\n\nAs pypdf does not have any formal relationship with any company and no funding,\nall the work done by the community are voluntary contributions. People don't\nget paid, but choose to spend their free time to create software of which\nmany more are profiting. This has to be honored and respected.\n\nDespite such a big community, the project was dormant from 2016 to 2022.\nThere were still questions asked, issues reported, and pull requests created.\nBut the maintainer didn't have the time to move pypdf forward. During that\ntime, nobody else stepped up to become the new maintainer.\n\nFor this reason, pypdf has the **Benevolent Dictator**\ngovernance model. The benevolent dictator is a maintainer with all technical permissions -\nmost importantly the permission to push new pypdf versions on PyPI.\n\nBeing benevolent, the benevolent dictator listens for decisions to the community and tries\ntheir best to make decisions from which the overall community profits - the\ncurrent one and the potential future one. Being a dictator, the benevolent dictator always has\nthe power and the right to make decisions on their own - also against some\nmembers of the community.\n\nAs pypdf is free software, parts of the community can split off (fork the code)\nand create a new community. This should limit the harm a bad benevolent dictator can do.\n\n\n## Project Language\n\nThe project language is (american) English. All documentation and issues must\nbe written in English to ensure that the community can understand it.\n\nWe appreciate the fact that large parts of the community don't have English\nas their native language. We try our best to understand others -\n[automatic translators](https://translate.google.com/) might help.\n\n\n## Expectations\n\nThe community can expect the following:\n\n* The **benevolent dictator** tries their best to make decisions from which the overall\n  community profits. The benevolent dictator is aware that his/her decisions can shape the\n  overall community. Once the benevolent dictator notices that she/he doesn't have the time\n  to advance pypdf, he/she looks for a new benevolent dictator. As it is expected\n  that the benevolent dictator will step down at some point of their choice\n  (hopefully before their death), it is NOT a benevolent dictator for life\n  (BDFL).\n* Every **maintainer** (including the benevolent dictator) is aware of their permissions and\n  the harm they could do. They value security and ensure that the project is\n  not harmed. They give their technical permissions back if they don't need them\n  any longer. Any long-time contributor can become a maintainer. Maintainers\n  can - and should! - step down from their role when they realize that they\n  can no longer commit that time. Their contribution will be honored in the\n  {doc}`history`.\n* Every **contributor** is aware that the time of maintainers and the benevolent dictator is\n  limited. Short pull requests that briefly describe the solved issue and have\n  a unit test have a higher chance to get merged soon - simply because it's\n  easier for maintainers to see that the contribution will not harm the overall\n  project. Their contributions are documented in the git history and in the\n  public issues. [Let us know](https://github.com/py-pdf/pypdf/discussions/798)\n  if you would appreciate something else!\n* Every **community member** uses a respectful language. We are all human, we\n  get upset about things we care and other things than what's visible on the\n  internet go on in our live. pypdf does not pay its contributors - keep all\n  of that in mind when you interact with others. We are here because we want to\n  help others.\n\n\n### Issues and Discussions\n\nAn issue is any technical description that aims at bringing pypdf forward:\n\n* Bugs tickets: Something went wrong because pypdf developers made a mistake.\n* Feature requests: pypdf does not support all features of the PDF specifications.\n  There are certainly also convenience methods that would help users a lot.\n* Robustness requests: There are many broken PDFs around. In some cases, we can\n  deal with that. It's kind of a mixture between a bug ticket and a feature\n  request.\n* Performance tickets: pypdf could be faster - let us know about your specific\n  scenario.\n\nAny comment that is in those technical descriptions which is not helping the\ndiscussion can be deleted. This is especially true for \"me too\" comments on bugs\nor \"bump\" comments for desired features. People can express this with 👍 / 👎\nreactions.\n\n[Discussions](https://github.com/py-pdf/pypdf/discussions) are open. No comments\nwill be deleted there - except if they are unrelated spam or only\ntry to insult people (luckily, the community was very respectful so far 🤞)\n\n\n### Releases\n\nThe maintainers follow [semantic versioning](https://semver.org/). Most\nimportantly, that means that breaking changes will have a major version bump.\n\nBe aware that unintentional breaking changes might still happen. The pypdf\nmaintainers do their best to fix that in a timely manner - please\n[report such issues](https://github.com/py-pdf/pypdf/issues)!\n\n\n## People\n\n* [stefan6419846](https://github.com/stefan6419846) is the benevolent dictator since January 2025\n* [Martin Thoma](https://github.com/MartinThoma) was the benevolent dictator from April 2022 to January 2025.\n  He still has most of the permissions as a fallback.\n* Maintainers:\n    * Matthew Stamy (mstamy2) was the benevolent dictator for a long time.\n      He still is around on GitHub once in a while and has permissions on PyPI and GitHub.\n    * Matthew Peveler (MasterOdin) is a maintainer on GitHub.\n"
  },
  {
    "path": "docs/meta/scope-of-pypdf.md",
    "content": "# Scope of pypdf\n\nWhat features should pypdf have and which features will it never have?\n\npypdf aims at simplifying interactions with PDF documents. Core tasks that\npypdf can perform are:\n\n* Document manipulation: Splitting, merging, cropping, and transforming the pages of PDF files\n* Data Extraction: Extract text and metadata from PDF documents\n* Security: Decrypt / encrypt PDF documents\n\nTypical indicators that pypdf should do something:\n\n* The task needs in-depth knowledge of the PDF format\n* It currently requires a lot of code or even is impossible to do with pypdf\n* It's neither mentioned in \"belongs in user code\" nor in \"out of scope\"\n* It already is in the issue list with the [is-feature tag](https://github.com/py-pdf/pypdf/labels/is-feature).\n\nThe [moonshot extensions](https://github.com/py-pdf/pypdf/discussions/1181) are\nfeatures we would like to have, but are currently not able to add (PRs are\nwelcome 😉)\n\n## Belongs in user code\n\nHere are a few indicators that a feature belongs into users' code (and not into pypdf):\n\n1. The use-case is very specific. Most people will not encounter the same need.\n2. It can be done without knowledge of the PDF specification\n3. It cannot be done without (non-pdf) domain knowledge. Anything that is\n   specific to your industry.\n\n## Out of scope\n\nWhile this list is infinitely long, there are a few topics that are asked\nmultiple times.\n\nThose topics are out of scope for pypdf. They will never be part of pypdf:\n\n1. **Optical Character Recognition (OCR)**: OCR is about extracting text from\n   images. That is very different from the kind of text extraction pypdf is\n   doing. Please note that images can be within PDF documents. In the case of\n   scanned documents, the whole page is an image. Some scanners automatically\n   execute OCR and add a text-layer behind the scanned page. That is something\n   pypdf can use if it's present. As a rule-of-thumb: If you cannot mark/copy\n   the text, it's likely an image. A noteworthy open source OCR project is\n   [tesseract](https://github.com/tesseract-ocr/tesseract).\n2. **Format Conversion**: Converting docx / HTML to PDF or PDF to those formats.\n   You might want to have a look at [`pdfkit`](https://pypi.org/project/pdfkit/)\n   and similar projects.\n\nOut of scope for the moment, but might be added if there are enough contributors:\n\n* **Digital Signature Support** ([reference ticket](https://github.com/py-pdf/pypdf/issues/302)): Cryptography is\n  complicated. It's important to get it right. pypdf currently doesn't have\n  enough active contributors to properly add digital signature support. For the\n  moment, [pyhanko](https://pypi.org/project/pyHanko/) seems to be the best choice.\n* **PDF Generation from Scratch**: pypdf can manipulate existing PDF documents,\n  add annotations, combine / split / crop / transform. It can add blank pages.\n  But if you want to generate invoices, you might want to have a look at\n  [`reportlab`](https://pypi.org/project/reportlab/) /\n  [`fpdf2`](https://pypi.org/project/fpdf2/) or document conversion tools like\n  [`pdfkit`](https://pypi.org/project/pdfkit/).\n* **Replacing words within a PDF**: [Extracting text from PDF is hard](../user/extract-text.md#why-text-extraction-is-hard).\n   Replacing text in a reliable way is even harder. For example, one word might\n   be split into multiple tokens. Hence, it's not a simple \"search and replace\"\n   in some cases.\n* **(Not) Extracting headers/footers/page numbers**: While you can apply\n  heuristics, there is no way to always make it work. PDF documents simply\n  don't contain the information what a header/footer/page number is.\n\n\n### Library vs. Application\n\nIt's also worth pointing out that `pypdf` is designed to be a library. It is not\nan application. That has several implications:\n\n* Execution: pypdf cannot be executed directly, but only be called from within\n  a program written by a pypdf user. In contrast, an application is executed\n  on its own.\n* Dependencies: pypdf should have a minimal set of dependencies and only\n  restrict them where it is strictly necessary. In contrast, applications should\n  be installed in environments which are isolated from other applications. They\n  can pin their dependencies.\n\nIf you're looking for a way to interact with PDF files via Shell, you should\neither write a script using pypdf or use [`pdfly`](https://pypi.org/project/pdfly/).\n"
  },
  {
    "path": "docs/meta/taking-ownership.md",
    "content": "# Taking Ownership of pypdf\n\npypdf is currently maintained by stefan6419846. We want to avoid that\npypdf ever goes unmaintained again. This document serves as a guide to avoid\nthat if I become unavailable, e.g., due to severe health issues.\n\nThis currently is just an abstract scenario. I'm fine, and I will likely do this\nfor several more years, but I have seen how projects stand still for many years\nbecause of the maintainer becoming inactive.\n\n## What belongs to pypdf?\n\nThe resources needed for maintaining pypdf are:\n\n* PyPI: [pypdf](https://pypi.org/project/pypdf/) and [PyPDF2](https://pypi.org/project/PyPDF2/)\n* GitHub: [pypdf](https://github.com/py-pdf/pypdf) (the repository, not the organization)\n* ReadTheDocs: [pypdf](https://readthedocs.org/projects/pypdf/) and [PyPDF2](https://readthedocs.org/projects/pypdf2/)\n\n## When may somebody take ownership?\n\n**No activity in 180 days**: If I don't answer e-mails (see my GitHub profile)\nand don't make any commits / merges for half a year, you can consider pypdf \"not\nmaintained.\"\n\n## Who may take ownership?\n\nPreferably, one of the owners of the GitHub `py-pdf` organization takes care of\nthat.\n\nAs of 27th of August 2023, the following people might be candidates:\n\n* [Lucas-C](https://github.com/Lucas-C): He maintains fpdf2 and is a py-pdf owner\n* [pubpub-zz](https://github.com/pubpub-zz): He is one of the most active contributors\n  to pypdf\n* [Matthew Peveler](https://github.com/MasterOdin): Less active, but he is cautious\n  about breaking changes and an experienced software developer.\n* [exiledkingcc](https://github.com/exiledkingcc): He has contributed the core\n  changes related to encryption.\n\n## How to take ownership?\n\n* PyPI: Follow [PEP 541 – Package Index Name Retention](https://peps.python.org/pep-0541/)\n* GitHub: Talk with one of the other py-pdf organization owners\n* ReadTheDocs: Follow the [Abandoned projects policy](https://docs.readthedocs.io/en/latest/abandoned-projects.html)\n"
  },
  {
    "path": "docs/modules/Destination.rst",
    "content": "The Destination Class\n---------------------\n\n.. autoclass:: pypdf.generic.Destination\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/DocumentInformation.rst",
    "content": "The DocumentInformation Class\n-----------------------------\n\n.. autoclass:: pypdf.DocumentInformation\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/Field.rst",
    "content": "The Field Class\n---------------\n\n.. autoclass:: pypdf.generic.Field\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/Fit.rst",
    "content": "The Fit Class\n-------------\n\n.. autoclass:: pypdf.generic.Fit\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/PageObject.rst",
    "content": "The PageObject Class\n--------------------\n\n.. autoclass:: pypdf._page.PageObject\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: pypdf._page.VirtualListImages\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: pypdf._page.ImageFile\n    :members:\n    :inherited-members: File\n    :undoc-members:\n\n.. autofunction:: pypdf.mult\n"
  },
  {
    "path": "docs/modules/PageRange.rst",
    "content": "The PageRange Class\n-------------------\n\n.. autoclass:: pypdf.PageRange\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/PaperSize.rst",
    "content": "The PaperSize Class\n-------------------\n\n.. autoclass:: pypdf.PaperSize\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\nAdd blank page with PaperSize\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n.. testsetup ::\n\n    pypdf_test_setup(\"modules/PaperSize\", {\n        \"example.pdf\": \"../resources/example.pdf\",\n    })\n\n.. testcode ::\n\n    from pypdf import PaperSize, PdfWriter\n\n    writer = PdfWriter(clone_from=\"example.pdf\")\n    writer.add_blank_page(PaperSize.A8.width, PaperSize.A8.height)\n    writer.write(\"out-add-page.pdf\")\n\nInsert blank page with PaperSize\n^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n.. testcode ::\n\n    from pypdf import PaperSize, PdfWriter\n\n    writer = PdfWriter(clone_from=\"example.pdf\")\n    writer.insert_blank_page(PaperSize.A8.width, PaperSize.A8.height, 1)\n    writer.write(\"out-insert-page.pdf\")\n"
  },
  {
    "path": "docs/modules/PdfDocCommon.rst",
    "content": "The PdfDocCommon Class\n----------------------\n\n**PdfDocCommon** is an abstract class which is inherited by :class:`~pypdf.PdfReader` and :class:`~pypdf.PdfWriter`.\n\nWhere identified in the API, you can use any of the derived class.\n\n.. autoclass:: pypdf._doc_common.PdfDocCommon\n    :members:\n    :inherited-members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/PdfReader.rst",
    "content": "The PdfReader Class\n-------------------\n\n.. autoclass:: pypdf.PdfReader\n    :members:\n    :inherited-members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: pypdf.PasswordType\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/PdfWriter.rst",
    "content": "The PdfWriter Class\n-------------------\n\n.. autoclass:: pypdf.PdfWriter\n    :members:\n    :inherited-members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: pypdf.ObjectDeletionFlag\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/RectangleObject.rst",
    "content": "The RectangleObject Class\n-------------------------\n\n.. autoclass:: pypdf.generic.RectangleObject\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/Transformation.rst",
    "content": "The Transformation Class\n------------------------\n\n.. autoclass:: pypdf.Transformation\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/XmpInformation.rst",
    "content": "The XmpInformation Class\n-------------------------\n\n.. autoclass:: pypdf.xmp.XmpInformation\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/annotations.rst",
    "content": "The annotations module\n----------------------\n\n.. automodule:: pypdf.annotations\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/constants.rst",
    "content": "Constants\n---------\n\n.. autoclass:: pypdf.constants.AnnotationFlag\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: pypdf.constants.ImageType\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: pypdf.constants.PageLabelStyle\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: pypdf.constants.UserAccessPermissions\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n.. autoclass:: pypdf.constants.FieldDictionaryAttributes\n       :members:\n       :undoc-members:\n       :exclude-members: FT, Parent, Kids, T, TU, TM, V, DV, AA, Opt, attributes, attributes_dict\n       :show-inheritance:\n"
  },
  {
    "path": "docs/modules/errors.rst",
    "content": "Errors\n------\n\n.. automodule:: pypdf.errors\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/modules/generic.rst",
    "content": "Generic PDF objects\n-------------------\n\n.. automodule:: pypdf.generic\n    :members:\n    :undoc-members:\n    :show-inheritance:\n    :exclude-members: Destination, Field, Fit, RectangleObject\n\n\n.. autoclass:: pypdf._protocols.PdfObjectProtocol\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\n.. autoclass:: pypdf._protocols.XmpInformationProtocol\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\n.. autoclass:: pypdf._protocols.PdfCommonDocProtocol\n       :members:\n       :undoc-members:\n       :show-inheritance:\n\n\n.. autoclass:: pypdf._protocols.PdfReaderProtocol\n    :members:\n    :undoc-members:\n    :show-inheritance:\n\n\n.. autoclass:: pypdf._protocols.PdfWriterProtocol\n    :members:\n    :undoc-members:\n    :show-inheritance:\n"
  },
  {
    "path": "docs/user/add-javascript.md",
    "content": "# Adding JavaScript to a PDF\n\nPDF readers vary in the extent they support JavaScript, with some not supporting it at all.\n\nAdobe has documentation on its support here:\n[https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/index.html](https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/index.html)\n\n## Launch print window on opening\n\n```{testsetup}\npypdf_test_setup(\"user/add-javascript\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"example.pdf\")\n\n# Add JavaScript to launch the print window on opening this PDF.\nwriter.add_js(\"this.print({bUI:true,bSilent:false,bShrinkToFit:true});\")\n\nwriter.write(\"out-print-window.pdf\")\n```\n"
  },
  {
    "path": "docs/user/add-watermark.md",
    "content": "# Adding a Stamp or Watermark to a PDF\n\nAdding stamps or watermarks are two common ways to manipulate PDF files.\nA stamp is adding something on top of the document, a watermark is in the\nbackground of the document.\n\n## Stamp (Overlay) / Watermark (Underlay)\n\nThe process of stamping and watermarking is the same, you just need to set `over` parameter to `True` for stamping and `False` for watermarking.\n\nYou can use {func}`~pypdf._page.PageObject.merge_page` if you don't need to transform the stamp:\n\n```{testsetup}\npypdf_test_setup(\"user/add-watermark\", {\n    \"crazyones.pdf\": \"../resources/crazyones.pdf\",\n    \"nup-source.png\": \"../docs/user/nup-source.png\",\n    \"jpeg.pdf\": \"../resources/jpeg.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\n\nstamp = PdfReader(\"jpeg.pdf\").pages[0]\nwriter = PdfWriter(clone_from=\"crazyones.pdf\")\nfor page in writer.pages:\n    page.merge_page(stamp, over=False)  # here set to False for watermarking\n\nwriter.write(\"out-watermark.pdf\")\n```\n\nOtherwise use {func}`~pypdf._page.PageObject.merge_transformed_page` with {class}`~pypdf.Transformation` if you need to translate, rotate, scale, etc. the stamp before merging it to the content page.\n\n```{testcode}\nfrom pathlib import Path\nfrom typing import List, Union\n\nfrom pypdf import PdfReader, PdfWriter, Transformation\n\n\ndef stamp(\n    content_pdf: Union[Path, str],\n    stamp_pdf: Union[Path, str],\n    pdf_result: Union[Path, str],\n    page_indices: Union[None, List[int]] = None,\n):\n    stamp_page = PdfReader(stamp_pdf).pages[0]\n\n    writer = PdfWriter()\n    # page_indices can be a List(array) of page, tuples are for range definition\n    reader = PdfReader(content_pdf)\n    writer.append(reader, pages=page_indices)\n\n    for content_page in writer.pages:\n        content_page.merge_transformed_page(\n            stamp_page,\n            Transformation().scale(0.5),\n        )\n\n    writer.write(pdf_result)\n\n\nstamp(\"crazyones.pdf\", \"jpeg.pdf\", \"out-scale.pdf\")\n```\n\nIf you are experiencing wrongly rotated watermarks/stamps, try to use\n{func}`~pypdf._page.PageObject.transfer_rotation_to_content` on the corresponding pages beforehand\nto fix the page boxes.\n\nExample of stamp:\n![stamp.png](stamp.png)\n\nExample of watermark:\n![watermark.png](watermark.png)\n\n\n## Stamping images directly\n\nThe above code only works for stamps that are already in PDF format.\nHowever, you can easily convert an image to PDF image using\n[Pillow](https://pypi.org/project/Pillow/).\n\n\n```{testcode}\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import List, Union\n\nfrom PIL import Image\nfrom pypdf import PageRange, PdfReader, PdfWriter, Transformation\n\n\ndef image_to_pdf(stamp_img: Union[Path, str]) -> PdfReader:\n    img = Image.open(stamp_img)\n    img_as_pdf = BytesIO()\n    img.save(img_as_pdf, \"pdf\")\n    return PdfReader(img_as_pdf)\n\n\ndef stamp_img(\n    content_pdf: Union[Path, str],\n    stamp_img: Union[Path, str],\n    pdf_result: Union[Path, str],\n    page_indices: Union[PageRange, List[int], None] = None,\n):\n    # Convert the image to a PDF\n    stamp_pdf = image_to_pdf(stamp_img)\n\n    # Then use the same stamp code from above\n    stamp_page = stamp_pdf.pages[0]\n\n    writer = PdfWriter()\n\n    reader = PdfReader(content_pdf)\n    writer.append(reader, pages=page_indices)\n    for content_page in writer.pages:\n        content_page.merge_transformed_page(\n            stamp_page,\n            Transformation(),\n        )\n\n    writer.write(pdf_result)\n\n\nstamp_img(\"crazyones.pdf\", \"nup-source.png\", \"out-image.pdf\")\n```\n"
  },
  {
    "path": "docs/user/adding-pdf-annotations.md",
    "content": "# Adding PDF Annotations\n\n```{note}\nBy default, some annotations might be invisible, for example polylines, as the default color is \"transparent\".\n\nTo circumvent this, make sure to add the `/C` entry to the annotation, being an array and each array value being in the range 0.0 to 1.0:\n\n  * With one element, a grayscale value.\n  * With three elements, a RGB definition.\n  * With four elements, a CMYK definition.\n```\n\n## Attachments\n\n```{testsetup}\npypdf_test_setup(\"user/adding-pdf-annotations\", {\n    \"crazyones.pdf\": \"../resources/crazyones.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter()\nwriter.add_blank_page(width=200, height=200)\n\ndata = b\"any bytes - typically read from a file\"\nwriter.add_attachment(\"smile.png\", data)\n\nwriter.write(\"out-attachment.pdf\")\n```\n\n\n## Free Text\n\nIf you want to add text in a box like this\n\n![](free-text-annotation.png)\n\nyou can use {class}`~pypdf.annotations.FreeText`:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import FreeText\n\n# Fill the writer with the pages you want\nreader = PdfReader(\"crazyones.pdf\")\npage = reader.pages[0]\nwriter = PdfWriter()\nwriter.add_page(page)\n\n# Create the annotation and add it\nannotation = FreeText(\n    text=\"Hello World\\nThis is the second line!\",\n    rect=(50, 550, 200, 650),\n    font=\"Arial\",\n    bold=True,\n    italic=True,\n    font_size=\"20pt\",\n    font_color=\"00ff00\",\n    border_color=\"0000ff\",\n    background_color=\"cdcdcd\",\n)\n\n# Set annotation flags to 4 for printable annotations.\n# See \"AnnotationFlag\" for other options, e.g. hidden etc.\nannotation.flags = 4\n\nwriter.add_annotation(page_number=0, annotation=annotation)\n\n# Write the annotated file to disk\nwriter.write(\"out-free-text.pdf\")\n```\n\n## Text\n\nA text annotation looks like this:\n\n![](text-annotation.png)\n\n## Line\n\nIf you want to add a line like this:\n\n![](annotation-line.png)\n\nyou can use {class}`~pypdf.annotations.Line`:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import Line\n\nreader = PdfReader(\"crazyones.pdf\")\npage = reader.pages[0]\nwriter = PdfWriter()\nwriter.add_page(page)\n\n# Add the line\nannotation = Line(\n    text=\"Hello World\\nLine2\",\n    rect=(50, 550, 200, 650),\n    p1=(50, 550),\n    p2=(200, 650),\n)\nwriter.add_annotation(page_number=0, annotation=annotation)\n\n# Write the annotated file to disk\nwriter.write(\"out-line.pdf\")\n```\n\n## PolyLine\n\nIf you want to add a line like this:\n\n![](annotation-polyline.png)\n\nyou can use {class}`~pypdf.annotations.PolyLine`:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import PolyLine\nfrom pypdf.generic import ArrayObject, FloatObject, NameObject\n\nreader = PdfReader(\"crazyones.pdf\")\npage = reader.pages[0]\nwriter = PdfWriter()\nwriter.add_page(page)\n\n# Add the polyline\n# By default, the line will be transparent. Set an explicit color.\nannotation = PolyLine(\n    vertices=[(50, 550), (200, 650), (70, 750), (50, 700)],\n)\nannotation[NameObject(\"/C\")] = ArrayObject(\n    [FloatObject(0.9), FloatObject(0.1), FloatObject(0)]\n)\nwriter.add_annotation(page_number=0, annotation=annotation)\n\n# Write the annotated file to disk\nwriter.write(\"out-polyline.pdf\")\n```\n\n## Rectangle\n\nIf you want to add a rectangle like this:\n\n![](annotation-square.png)\n\nyou can use {class}`~pypdf.annotations.Rectangle`:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import Rectangle\n\nreader = PdfReader(\"crazyones.pdf\")\npage = reader.pages[0]\nwriter = PdfWriter()\nwriter.add_page(page)\n\n# Add the rectangle\nannotation = Rectangle(\n    rect=(50, 550, 200, 650),\n)\nwriter.add_annotation(page_number=0, annotation=annotation)\n\n# Write the annotated file to disk\nwriter.write(\"out-rectangle.pdf\")\n```\n\nIf you want the rectangle to be filled, use the `interiour_color=\"ff0000\"` parameter.\n\nThis method uses the \"square\" annotation type of the PDF format.\n\n\n## Ellipse\n\nIf you want to add a circle like this:\n\n![](annotation-circle.png)\n\nyou can use {class}`~pypdf.annotations.Ellipse`:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import Ellipse\n\nreader = PdfReader(\"crazyones.pdf\")\npage = reader.pages[0]\nwriter = PdfWriter()\nwriter.add_page(page)\n\n# Add the rectangle\nannotation = Ellipse(\n    rect=(50, 550, 200, 650),\n)\nwriter.add_annotation(page_number=0, annotation=annotation)\n\n# Write the annotated file to disk\nwriter.write(\"out-ellipse.pdf\")\n```\n\n## Polygon\n\nIf you want to add a polygon like this:\n\n![](annotation-polygon.png)\n\nyou can use {class}`~pypdf.annotations.Polygon`:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import Polygon\n\nreader = PdfReader(\"crazyones.pdf\")\npage = reader.pages[0]\nwriter = PdfWriter()\nwriter.add_page(page)\n\n# Add the line\nannotation = Polygon(\n    vertices=[(50, 550), (200, 650), (70, 750), (50, 700)],\n)\nwriter.add_annotation(page_number=0, annotation=annotation)\n\n# Write the annotated file to disk\nwriter.write(\"out-polygon.pdf\")\n```\n\n## Popup\n\nManage the Popup windows for markups, looks like this:\n\n![](annotation-popup.png)\n\nyou can use {py:class}`~pypdf.annotations.Popup`:\n\n```{testcode}\nfrom pypdf import PdfWriter\nfrom pypdf.annotations import Popup, Text\n\n# Arrange\nwriter = PdfWriter()\nwriter.append(\"crazyones.pdf\", [0])\n\n# Act\ntext_annotation = writer.add_annotation(\n    0,\n    Text(\n        text=\"Hello World\\nThis is the second line!\",\n        rect=(50, 550, 200, 650),\n        open=True,\n    ),\n)\n\npopup_annotation = Popup(\n    rect=(50, 550, 200, 650),\n    open=True,\n    parent=text_annotation,  # use the output of add_annotation\n)\n\nwriter.write(\"out-popup.pdf\")\n```\n\nYou have to use the returned result from add_annotation() as it is\nthe parent annotation with which this popup annotation shall be associated.\n\n## Link\n\nIf you want to add a link, you can use {class}`~pypdf.annotations.Link`:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import Link\n\nreader = PdfReader(\"crazyones.pdf\")\npage = reader.pages[0]\nwriter = PdfWriter()\nwriter.add_page(page)\n\n# Add the link\nannotation = Link(\n    rect=(50, 550, 200, 650),\n    url=\"https://martin-thoma.com/\",\n)\nwriter.add_annotation(page_number=0, annotation=annotation)\n\n# Write the annotated file to disk\nwriter.write(\"out-link.pdf\")\n```\n\nYou can also add internal links:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import Link\nfrom pypdf.generic import Fit\n\nreader = PdfReader(\"crazyones.pdf\")\npage = reader.pages[0]\nwriter = PdfWriter()\nwriter.add_page(page)\n\n# Add the link\nannotation = Link(\n    rect=(50, 550, 200, 650),\n    target_page_index=3,\n    fit=Fit(fit_type=\"/FitH\", fit_args=(123,)),\n)\nwriter.add_annotation(page_number=0, annotation=annotation)\n\n# Write the annotated file to disk\nwriter.write(\"out-internal-link.pdf\")\n```\n\n## Text Markup Annotations\n\nText markup annotations refer to a specific piece of text within the document.\n\nThese are a bit more complicated as you need to know exactly where the text\nis, the so-called \"Quad points\".\n\n### Highlighting\n\nIf you want to highlight text like this:\n\n![](annotation-highlight.png)\n\nyou can use {class}`~pypdf.annotations.Highlight`:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import Highlight\nfrom pypdf.generic import ArrayObject, FloatObject\n\nreader = PdfReader(\"crazyones.pdf\")\npage = reader.pages[0]\nwriter = PdfWriter()\nwriter.add_page(page)\n\nrect = (50, 550, 200, 650)\nquad_points = [rect[0], rect[1], rect[2], rect[1], rect[0], rect[3], rect[2], rect[3]]\n\n# Add the highlight\nannotation = Highlight(\n    rect=rect,\n    quad_points=ArrayObject([FloatObject(quad_point) for quad_point in quad_points]),\n)\nwriter.add_annotation(page_number=0, annotation=annotation)\n\n# Write the annotated file to disk\nwriter.write(\"out-highlight.pdf\")\n```\n"
  },
  {
    "path": "docs/user/cropping-and-transforming.md",
    "content": "# Cropping and Transforming PDFs\n\n```{note}\nJust because content is no longer visible, it is not gone.\nCropping works by adjusting the viewbox. That means content that was cropped\naway can still be restored.\n```\n\n```{testsetup}\npypdf_test_setup(\"user/cropping-and-transforming\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n    \"Seige_of_Vicksburg_Sample_OCR.pdf\": \"../resources/Seige_of_Vicksburg_Sample_OCR.pdf\",\n    \"labeled-edges-center-image.pdf\": \"../resources/labeled-edges-center-image.pdf\",\n    \"side-by-side-subfig.pdf\": \"../resources/side-by-side-subfig.pdf\",\n    \"nup-source.pdf\": \"../resources/box.pdf\",\n    \"box.pdf\": \"../resources/box.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\n\nreader = PdfReader(\"Seige_of_Vicksburg_Sample_OCR.pdf\")\nwriter = PdfWriter()\n\n# Add page 1 from reader to output document, unchanged.\nwriter.add_page(reader.pages[0])\n\n# Add page 2 from reader, but rotated clockwise 90 degrees.\nwriter.add_page(reader.pages[1].rotate(90))\n\n# Add page 3 from reader, but crop it to half size.\npage3 = writer.add_page(reader.pages[2])\npage3.mediabox.upper_right = (\n    page3.mediabox.right / 2,\n    page3.mediabox.top / 2,\n)\n\nwriter.write(\"out-all-in-one.pdf\")\n```\n\n## Page rotation\n\nThe most typical rotation is a clockwise rotation of the page by multiples of\n90 degrees. That is done when the orientation of the page is wrong. You can\ndo that with the {func}`~pypdf._page.PageObject.rotate` method:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\n\nreader = PdfReader(\"example.pdf\")\nwriter = PdfWriter()\n\nwriter.add_page(reader.pages[0])\nwriter.pages[0].rotate(90)\n\nwriter.write(\"out-page-rotation.pdf\")\n```\n\nThe rotate method is typically preferred over the `page.add_transformation(Transformation().rotate())`\nmethod, because `rotate` will ensure that the page is still in the mediabox/cropbox.\nThe transformation object operates on the coordinates of the page\ncontents and does not change the mediabox or cropbox.\n\n\n\n## Plain Merge\n\n![](plain-merge.png)\n\nis the result of\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter, Transformation\n\n# Get the data\nreader_base = PdfReader(\"labeled-edges-center-image.pdf\")\npage_base = reader_base.pages[0]\n\nreader = PdfReader(\"box.pdf\")\npage_box = reader.pages[0]\n\n# Write the result back\nwriter = PdfWriter()\npage = writer.add_page(page_base)\npage.merge_page(page_box)\nwriter.write(\"out-plain-merge.pdf\")\n```\n\n## Merge with Rotation\n\n![](merge-45-deg-rot.png)\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter, Transformation\n\n# Get the data\nreader_base = PdfReader(\"labeled-edges-center-image.pdf\")\npage_base = reader_base.pages[0]\n\nreader = PdfReader(\"box.pdf\")\npage_box = reader.pages[0]\n\n# Prepare writer\nwriter = PdfWriter()\n\n# Add base page.\nwriter_page = writer.add_page(page_base)\n\n# Apply the transformation and merge the pages.\ntransformation = Transformation().rotate(45)\nwriter_page.merge_transformed_page(page_box, transformation)\n\n# Write the result back\nwriter.write(\"out-merge-with-rotation.pdf\")\n```\n\nIf you add the `expand` parameter:\n\n```{testcode}\ntransformation = Transformation().rotate(45)\nwriter_page.merge_transformed_page(page_box, transformation, expand=True)\n```\n\nyou get:\n\n![](merge-rotate-expand.png)\n\nAlternatively, you can move the merged image a bit to the right by using\n\n```{testcode}\nop = Transformation().rotate(45).translate(tx=50)\n```\n\n![](merge-translated.png)\n\n\n## Scaling\n\nIn pypdf, the content and the page can either be scaled together or separately.\nContent scaling scales the contents on a page, and page scaling scales just the page size (the canvas).\nTypically, you want to combine both.\n\n![](scaling.png)\n\n### Scaling both the Page and contents together\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\n\n# Read the input\nreader = PdfReader(\"side-by-side-subfig.pdf\")\npage = reader.pages[0]\n\n# Add to the writer\nwriter = PdfWriter()\nwriter_page = writer.add_page(page)\n\n# Scale\nwriter_page.scale_by(0.5)\n\n# Write the result to a file\nwriter.write(\"out-scale-all.pdf\")\n```\n\n### Scaling the content only\n\nThe content is scaled around the origin of the coordinate system.\nTypically, that is the lower-left corner.\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter, Transformation\n\n# Read the input\nreader = PdfReader(\"side-by-side-subfig.pdf\")\npage = reader.pages[0]\n\n# Prepare the writer\nwriter = PdfWriter()\nwriter_page = writer.add_page(page)\n\n# Scale\nop = Transformation().scale(sx=0.7, sy=0.7)\nwriter_page.add_transformation(op)\n\n# Write the result to a file\nwriter.write(\"out-scale-content.pdf\")\n```\n\n### Scaling the page only\n\nTo scale the page by `sx` in the X direction and `sy` in the Y direction:\n\n```{testcode}\npage.mediabox = page.mediabox.scale(sx=0.7, sy=0.7)\n```\n\nIf you wish to have more control, you can adjust the various page boxes directly:\n\n```{testcode}\nfrom pypdf.generic import RectangleObject\n\nmb = page.mediabox\n\npage.mediabox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))\npage.cropbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))\npage.trimbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))\npage.bleedbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))\npage.artbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))\n```\n\n### pypdf._page.MERGE_CROP_BOX\n\n`pypdf<=3.4.0` used to merge the other page with `trimbox`.\n`pypdf>3.4.0` changes this behavior to `cropbox`.\n\nIn case anybody has good reasons to use/expect `trimbox`, you can add the\nfollowing code to get the old behavior:\n\n```{testcode}\nimport pypdf\n\npypdf._page.MERGE_CROP_BOX = \"trimbox\"\n```\n\n## Transforming several copies of the same page\n\nWe have designed the following business card (A8 format) to advertise our new startup.\n\n![](nup-source.png)\n\nWe would like to copy this card sixteen times on an A4 page, to print it, cut it, and give it to all our friends. Having learned about the {func}`~pypdf._page.PageObject.merge_page` method and the {class}`~pypdf.Transformation` class, we run the following code. Notice that we had to tweak the media box of the source page to extend it, which is already a dirty hack (in this case).\n\n```{testcode}\nfrom pypdf import PaperSize, PdfReader, PdfWriter, Transformation\n\n# Read source file\nreader = PdfReader(\"nup-source.pdf\")\nsourcepage = reader.pages[0]\n\n# Create a destination file, and add a blank page to it\nwriter = PdfWriter()\ndestpage = writer.add_blank_page(width=PaperSize.A4.height, height=PaperSize.A4.width)\n\n# Copy source page to destination page, several times\nfor x in range(4):\n    for y in range(4):\n        # Translate page\n        transformation = Transformation().translate(\n            x * PaperSize.A8.height,\n            y * PaperSize.A8.width,\n        )\n        # Merge translated page\n        destpage.merge_transformed_page(sourcepage, transformation)\n\n# Write file\nwriter.write(\"out-nup-dest1.pdf\")\n```\n\n![](nup-dest2.png)\n\nThere is still some work to do, for instance, to insert margins between and around cards, but this is left as an exercise for the reader…\n\n## Possible issues\n\nEspecially when combining {func}`~pypdf._page.PageObject.merge_page` with transformations, you might end up with a cropped PDF file.\nIn these cases, consider setting `expand=True` to re-calculate the corresponding media box.\n"
  },
  {
    "path": "docs/user/encryption-decryption.md",
    "content": "# Encryption and Decryption of PDFs\n\nPDF encryption makes use of [`RC4`](https://en.wikipedia.org/wiki/RC4) and\n[`AES`](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) algorithms\nwith different key length. `pypdf` supports all of them until `PDF-2.0`, which\nis the latest PDF standard.\n\n`pypdf` use an extra dependency to do encryption or decryption for `AES` algorithms.\nWe recommend [`pyca/cryptography`](https://cryptography.io/en/latest/). Alternatively,\nyou can use [`pycryptodome`](https://pypi.org/project/pycryptodome/).\n\n```{note}\nPlease see the note in the [installation guide](installation.md)\nfor installing the extra dependencies if interacting with PDFs that use AES.\n```\n\n## Encrypt\n\nYou can encrypt a PDF by using a password:\n\n```{testsetup}\npypdf_test_setup(\"user/encryption-decryption\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n    \"encrypted-file.pdf\": \"../resources/encrypted-file.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\n\nreader = PdfReader(\"example.pdf\")\nwriter = PdfWriter(clone_from=reader)\n\n# Add a password to the new PDF\nwriter.encrypt(\"my-secret-password\", algorithm=\"AES-256\")\n\n# Save the new PDF to a file\nwriter.write(\"out-encrypt.pdf\")\n```\n\nThe algorithm can be one of `RC4-40`, `RC4-128`, `AES-128`, `AES-256-R5`, `AES-256`.\nWe recommend using `AES-256-R5`.\n\n```{warning}\npypdf uses `RC4` by default for compatibility if you omit the \"algorithm\" parameter.\nSince `RC4` is insecure, you should use `AES` algorithms.\n```\n\n## Decrypt\n\nYou can decrypt a PDF using the appropriate password:\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\n\nreader = PdfReader(\"encrypted-file.pdf\")\n\nif reader.is_encrypted:\n    reader.decrypt(\"test\")  # secret password\n\nwriter = PdfWriter(clone_from=reader)\n\n# Save the new PDF to a file\nwriter.write(\"out-decrypt.pdf\")\n```\n"
  },
  {
    "path": "docs/user/extract-images.md",
    "content": "# Extract Images\n\n```{note}\nIn order to use the following code you need to install optional\ndependencies, see [installation guide](installation.md).\n```\n\nEvery page of a PDF document can contain an arbitrary number of images.\nThe names of the files may not be unique.\n\n```{testsetup}\npypdf_test_setup(\"user/extract-images\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\npage = reader.pages[0]\n\nfor i, image_file_object in enumerate(page.images):\n    file_name = \"out-image-\" + str(i) + \"-\" + image_file_object.name\n    image_file_object.image.save(file_name)\n```\n\n## Other images\n\nSome other objects can contain images, such as stamp annotations.\n\nYou can extract the image from the annotation with the following code:\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\nim = (\n    reader.pages[0][\"/Annots\"][4][\"/Parent\"]\n    .get_object()[\"/AP\"][\"/N\"][\"/Resources\"][\"/XObject\"][\"/Im4\"]\n    .decode_as_image()\n)\n\nim.save(\"out-annotation-image.png\")\n```\n\n## Error handling\n\nIterating over `page.images` directly will raise an exception on the first issue.\nIf you expect some more or less broken PDF files, but still want to retrieve as many images as possible,\nconsider making this a multistep process:\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\nfor page in reader.pages:\n    for name in page.images.keys():\n        try:\n            # Try to retrieve actual image.\n            image = page.images[name]\n        except Exception as exception:\n            # Handle exceptions.\n            pass\n```\n"
  },
  {
    "path": "docs/user/extract-text.md",
    "content": "# Extract Text from a PDF\n\nYou can extract text from a PDF:\n\n```{testsetup}\npypdf_test_setup(\"user/extract-text\", {\n    \"test Orient.pdf\": \"../resources/test Orient.pdf\",\n    \"GeoBase_NHNC1_Data_Model_UML_EN.pdf\": \"../resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"test Orient.pdf\")\npage = reader.pages[0]\nprint(page.extract_text())\n\n# extract only text oriented up\nprint(page.extract_text(0))\n\n# extract text oriented up and turned left\nprint(page.extract_text((0, 90)))\n\n# extract text in a fixed width format that closely adheres to the rendered\n# layout in the source pdf\nprint(page.extract_text(extraction_mode=\"layout\"))\n\n# extract text preserving horizontal positioning without excess vertical\n# whitespace (removes blank and \"whitespace only\" lines)\nprint(page.extract_text(extraction_mode=\"layout\", layout_mode_space_vertically=False))\n\n# adjust horizontal spacing\nprint(page.extract_text(extraction_mode=\"layout\", layout_mode_scale_weight=1.0))\n\n# exclude (default) or include (as shown below) text rotated w.r.t. the page\nprint(page.extract_text(extraction_mode=\"layout\", layout_mode_strip_rotated=False))\n```\n\n```{testoutput}\n:options: +NORMALIZE_WHITESPACE\n:hide:\n\n\n(T) This is box text at top\nwritten down from top\n(B)  This is box text at bottom written up from bottom\n(L) This is box text on left written vertically to starboard\n(R) This is box text on righy written vertically to port\n\n\n(T) This is box text at top\nwritten down from top\n\n\n(T) This is box text at top\nwritten down from top\n(L) This is box text on left written vertically to starboard\n\n (B)\n\nThis is box text at bottom\n from bottom upwritten\n\n\n\n\n(T) This is box text at top\nwritten down from top\n (B)\nThis is box text at bottom\n from bottom upwritten\n(T) This is box text at top\nwritten down from top\n (B)\n\nThis is box text at bottom\n from bottom upwritten\n\n\n\n\n(T) This is box text at top\nwritten down from top\n (B)\n\nThis is box text at bottom\n from bottom upwritten\n\n(L) This is box textwritten vertically to starboard\n\n\n on righy\n\n\non left\n\n ) This is box text\nwritten vertically to port (R\n\n\n\n\n(T) This is box text at top\nwritten down from top\n\n```\n\nRefer to {func}`~pypdf._page.PageObject.extract_text` for more details.\n\n```{note}\nExtracting the text of a page requires parsing its whole content stream. This can require quite a lot of memory -\nwe have seen 10 GB RAM being required for an uncompressed content stream of about 300 MB (which should not occur\nvery often).\n\nTo limit the size of the content streams to process (and avoid OOM errors in your application), consider\nchecking `len(page.get_contents().get_data())` beforehand.\n```\n\n```{note}\nIf a PDF page appears to contain only an image (e.g., a scanned document), the extracted text may be minimal or visually empty.\nIn such cases, consider using OCR software such as [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) to extract text from images.\n```\n\n## Using a visitor\n\nYou can use visitor functions to control which part of a page you want to process and extract. The visitor functions\nyou provide will get called for each operator or for each text fragment.\n\nThe function provided in argument visitor_text of function extract_text has five arguments:\n* text: the current text (as long as possible, can be up to a full line)\n* user_matrix: current matrix to move from user coordinate space (also known as CTM)\n* tm_matrix: current matrix from text coordinate space\n* font_dictionary: full font dictionary\n* font_size: the size (in text coordinate space)\n\nThe matrix stores six parameters. The first four provide the rotation/scaling matrix, and the last two provide the translation (horizontal/vertical).\nIt is recommended to use the user_matrix as it takes into account all transformations.\n\nNotes :\n\n - As indicated in §8.3.3 of the PDF 1.7 or PDF 2.0 specification, the user matrix applies to text space/image space/form space/pattern space.\n - If you want to get the full transformation from text to user space, you can use the {func}`~.pypdf.mult` function as follows:\n`txt2user = mult(tm, cm)`.\nThe font size is the raw text size and affected by the `user_matrix`.\n\n\nThe `font_dictionary` may be `None` in case of unknown fonts.\nIf not `None`, it could contain something like the key `\"/BaseFont\"` with the value `\"/Arial,Bold\"`.\n\n**Caveat**: In complicated documents, the calculated positions may be difficult to determine (if you move from multiple forms to page user space, for example).\n\nThe function provided in argument visitor_operand_before has four arguments:\noperator, operand-arguments, current transformation matrix, and text matrix.\n\n### Example 1: Ignore header and footer\n\nThe following example reads the text of page four of [this PDF document](https://github.com/py-pdf/pypdf/blob/main/resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf), but ignores the header (y > 720) and footer (y < 50). In this file we also need to include new line characters (y == 0).\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"GeoBase_NHNC1_Data_Model_UML_EN.pdf\")\npage = reader.pages[3]\n\nparts = []\n\n\ndef visitor_body(text, cm, tm, font_dict, font_size):\n    y = tm[5]\n    if 50 < y < 720 or y == 0:\n        parts.append(text)\n\n\npage.extract_text(visitor_text=visitor_body)\ntext_body = \"\".join(parts)\n\nprint(text_body)\n```\n\n```{testoutput}\n:options: +NORMALIZE_WHITESPACE\n:hide:\n\nTABLE OF CONTENTS\n\n1 OVERVIEW ............................................................................................................................................ 6\n2 LRS ........................................................................................................................................................ 6\n2.1 LRS MODEL ...................................................................................................................................... 7\n3 MODEL .................................................................................................................................................. 8\n3.1 LRS MODEL ...................................................................................................................................... 9\n3.1.1 Logical view ............................................................................................................................... 9\n3.1.2 Hydro network.......................................................................................................................... 10\n3.1.3 Hydro events............................................................................................................................ 11\n3.1.4 Hydrographic ........................................................................................................................... 14\n3.1.5 Toponymy (external package) ................................................................................................. 18\n3.1.6 Metadata .................................................................................................................................. 19\n```\n\n### Example 2: Extract rectangles and texts into an SVG file\n\nThe following example converts page three of [this PDF document](https://github.com/py-pdf/pypdf/blob/main/resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf) into\nan [SVG file](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics).\n\nSuch an SVG export may help to understand what is going on in a page.\n\n% We prefer not to execute doc examples for unmaintained third-party package \"svgwrite\"\n```{testcode}\n:skipif: True\n\nfrom pypdf import PdfReader\nimport svgwrite\n\nreader = PdfReader(\"GeoBase_NHNC1_Data_Model_UML_EN.pdf\")\npage = reader.pages[2]\n\ndwg = svgwrite.Drawing(\"GeoBase_test.svg\", profile=\"tiny\")\n\n\ndef visitor_svg_rect(op, args, cm, tm):\n    if op == b\"re\":\n        (x, y, w, h) = (args[i].as_numeric() for i in range(4))\n        dwg.add(dwg.rect((x, y), (w, h), stroke=\"red\", fill_opacity=0.05))\n\n\ndef visitor_svg_text(text, cm, tm, font_dict, font_size):\n    (x, y) = (cm[4], cm[5])\n    dwg.add(dwg.text(text, insert=(x, y), fill=\"blue\"))\n\n\npage.extract_text(\n    visitor_operand_before=visitor_svg_rect, visitor_text=visitor_svg_text\n)\ndwg.save()\n```\n\nThe SVG generated here is bottom-up because the coordinate systems of PDF and SVG differ.\n\nUnfortunately, in complicated PDF documents the coordinates given to the visitor functions may be wrong.\n\n## Why Text Extraction is hard\n\n### Unclear Objective\n\nExtracting text from a PDF can be tricky. In several cases, there is no\nclear answer to what the expected result should look like:\n\n1. **Paragraphs**: Should the text of a paragraph have line breaks at the same places\n   where the original PDF had them or should it rather be one block of text?\n2. **Page numbers**: Should they be included in the extract?\n3. **Headers and Footers**: Similar to page numbers - should they be extracted?\n4. **Outlines**: Should outlines be extracted at all?\n5. **Formatting**: If the text is **bold** or *italic*, should it be included in the\n   output?\n6. **Tables**: Should the text extraction skip tables? Should it extract just the\n   text? Should the borders be shown in some Markdown-like way or should the\n   structure be present e.g. as an HTML table? How would you deal with merged\n   cells?\n7. **Captions**: Should image and table captions be included?\n8. **Ligatures**: The Unicode symbol [U+FB00](https://www.compart.com/de/unicode/U+FB00)\n   is a single symbol ﬀ for two lowercase letters 'f'. Should that be parsed as\n   the Unicode symbol 'ﬀ' or as two ASCII symbols 'ff'?\n9. **SVG images**: Should the text parts be extracted?\n10. **Mathematical Formulas**: Should they be extracted? Formulas have indices\n    and nested fractions.\n11. **Whitespace characters**: How many new lines should be extracted for 3 cm of\n    vertical whitespace? How many spaces should be extracted if there is 3 cm of\n    horizontal whitespace? When would you extract tabs and when spaces?\n12. **Footnotes**: When the text of multiple pages is extracted, where should footnotes be shown?\n13. **Hyperlinks and Metadata**: Should it be extracted at all? Where should it\n    be placed in which format?\n14. **Linearization**: Assume you have a floating figure in between a paragraph.\n    Do you first finish the paragraph, or do you put the figure text in between?\n\nThen there are issues where most people would agree on the correct output, but\nthe way PDF stores information just makes it hard to achieve that:\n\n1. **Tables**: Typically, tables are just absolutely positioned text. In the worst\n   case, every single letter could be absolutely positioned. That makes it hard\n   to tell where columns / rows are.\n2. **Images**: Sometimes PDFs do not contain the text as it is displayed, but\n    instead an image. You notice that when you cannot copy the text. Then there\n    are PDF files that contain an image and a text layer in the background.\n    That typically happens when a document was scanned. Although the scanning\n    software (OCR) is pretty good today, it still fails once in a while. pypdf\n    is no OCR software; it will not be able to detect those failures. pypdf\n    will also never be able to extract text from images.\n\nFinally, there are issues that pypdf will deal with. If you find such a\ntext extraction bug, please share the PDF with us so we can work on it!\n\n### Missing Semantic Layer\n\nThe PDF file format is all about producing the desired visual result for\nprinting. It was not created for parsing the content. PDF files don't contain a\nsemantic layer.\n\nSpecifically, there is no information what the header, footer, page numbers,\ntables, and paragraphs are. The visual appearance is there, and people might\nfind heuristics to make educated guesses, but there is no way of being certain.\n\nThis is a shortcoming of the PDF file format, not of pypdf.\n\nIt is possible to apply machine learning on PDF documents to make good\nheuristics, but that will not be part of pypdf. However, pypdf could be used to\nfeed such a machine learning system with the relevant information.\n\n### Whitespaces\n\nThe PDF format is meant for printing. It is not designed to be read by machines.\nThe text within a PDF document is absolutely positioned, meaning that every single\ncharacter could be positioned on the page.\n\nThe text\n\n> This is a test document by Ethan Nelson.\n\ncan be represented as\n\n> [(This is a )9(te)-3(st)9( do)-4(cu)13(m)-4(en)12(t )-3(b)3(y)-3( )9(Et)-2(h)3(an)4( Nels)13(o)-5(n)3(.)] TJ\n\nWhere the numbers are adjustments of vertical space. This representation used\nwithin the PDF file makes it very hard to guarantee correct whitespaces.\n\n\nMore information:\n\n* [issue #1507](https://github.com/py-pdf/pypdf/issues/1507)\n* [Negative numbers in PDF content stream text object](https://stackoverflow.com/a/28203655/562769)\n* Mark Stephens: [Understanding PDF text objects](https://blog.idrsolutions.com/understanding-pdf-text-objects/), 2010.\n\n## OCR vs. Text Extraction\n\nOptical Character Recognition (OCR) is the process of extracting text from\nimages. Software which does this is called *OCR software*. The\n[tesseract OCR engine](https://github.com/tesseract-ocr/tesseract) is the\nmost commonly known open source OCR software.\n\npypdf is **not** OCR software.\n\n### Digitally-born vs. Scanned PDF files\n\nPDF documents can contain images and text. PDF files don't store text in a\nsemantically meaningful way, but in a way that makes it easy to show the\ntext on screen or print it. For this reason, text extraction from PDFs is hard.\n\nIf you scan a document, the resulting PDF typically shows the image of the scan.\nScanners then also run OCR software and put the recognized text in the background\nof the image. pypdf can extract this result of the scanners OCR software. However,\nin such cases, it's recommended to directly use OCR software as\nerrors can accumulate: The OCR software is not perfect in recognizing the text.\nThen it stores the text in a format that is not meant for text extraction and\npypdf might make mistakes parsing that.\n\nHence, I would distinguish three types of PDF documents:\n\n* **Digitally born PDF files**: The file was created digitally on the computer.\n  It can contain images, texts, links, outline items (a.k.a., bookmarks), JavaScript, ...\n  If you Zoom in a lot, the text still looks sharp.\n* **Scanned PDF files**: Any number of pages was scanned. The images were then\n  stored in a PDF file. Hence, the file is just a container for those images.\n  You cannot copy the text, you don't have links, outline items, JavaScript.\n* **OCRed PDF files**: The scanner ran OCR software and put the recognized text\n  in the background of the image. Hence, you can copy the text, but it still looks\n  like a scan. If you zoom in enough, you can recognize pixels.\n\n### Can we just always use OCR?\n\nYou might now wonder if it makes sense to just always use OCR software. If the\nPDF file is digitally-born, you can render it to an image.\n\nI would recommend not to do that.\n\nText extraction software like pypdf can use more information from the\nPDF than just the image. It can know about fonts, encodings, typical character\ndistances and similar topics.\n\nThat means pypdf has a clear advantage when it\ncomes to characters which are easy to confuse such as `oO0ö`.\n**pypdf will never confuse characters**. It just reads what is in the file.\n\npypdf also has an edge when it comes to characters which are rare, e.g.\n🤰. OCR software will not be able to recognize smileys correctly.\n\n## Attempts to prevent text extraction\n\nIf people who share PDF documents want to prevent text extraction, they have\nmultiple ways to do so:\n\n1. Store the contents of the PDF as an image\n2. [Use a scrambled font](https://stackoverflow.com/a/43466923/562769)\n\nHowever, text extraction cannot be completely prevented if people should still\nbe able to read the document. In the worst case, people can make a screenshot,\nprint it, scan it, and run OCR over it.\n"
  },
  {
    "path": "docs/user/file-size.md",
    "content": "# Reduce PDF File Size\n\nThere are multiple ways to reduce the size of a given PDF file. The easiest\none is to remove content (e.g., images) or pages.\n\n## Removing duplication\n\nSome PDF documents contain the same object multiple times. For example, if an\nimage appears three times in a PDF, it could be embedded three times. Or it can\nbe embedded once and referenced twice.\n\nWhen adding data to a PdfWriter, the data is copied while respecting the original format.\nFor example, if two pages include the same image which is duplicated in the source document, the object will be duplicated in the PdfWriter object.\n\nAdditionally, when you delete objects in a document, pypdf cannot easily identify whether the objects are used elsewhere or not or if the user wants to keep them in. When writing the PDF file, these objects will be hidden within (part of the file, but not displayed).\n\nTo reduce the file size, use a compression call: `writer.compress_identical_objects(remove_identicals=True, remove_orphans=True)`\n\n* `remove_identicals` enables/disables compression merging identical objects.\n* `remove_orphans` enables/disables suppression of unused objects.\n\nIt is recommended to apply this process just before writing to the file/stream.\n\nIt depends on the PDF how well this works, but we have seen an 86% file\nreduction (from 5.7 MB to 0.8 MB) within a real PDF.\n\n\n## Removing Images\n\n```{testsetup}\npypdf_test_setup(\"user/file-size\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"example.pdf\")\n\nwriter.remove_images()\n\nwriter.write(\"out-no-images.pdf\")\n```\n\n## Reducing Image Quality\n\nIf we reduce the quality of the images within the PDF, we can **sometimes**\nreduce the file size of the PDF overall. That depends on how well the reduced\nquality image can be compressed.\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"example.pdf\")\n\nfor page in writer.pages:\n    for img in page.images:\n        img.replace(img.image, quality=80)\n\nwriter.write(\"out-low-quality.pdf\")\n```\n\n## Lossless Compression\n\npypdf supports the FlateDecode filter which uses the zlib/deflate compression\nmethod. It is a lossless compression, meaning the resulting PDF looks exactly\nthe same.\n\nDeflate compression can be applied to a page via\n{meth}`page.compress_content_streams <pypdf._page.PageObject.compress_content_streams>`:\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"example.pdf\")\n\nfor page in writer.pages:\n    page.compress_content_streams()  # This is CPU intensive!\n\nwriter.write(\"out-lossless.pdf\")\n```\n\n`page.compress_content_streams` uses [`zlib.compress`](https://docs.python.org/3/library/zlib.html#zlib.compress)\nand supports the `level` parameter: `level=0` means no compression,\n`level=9` refers to the highest compression.\n\nUsing this method, we have seen a reduction by 70% (from 11.8 MB to 3.5 MB)\nwith a real PDF.\n\n## Removing Sources\n\nWhen a page is removed from the page list, its content will still be present in\nthe PDF file. This means that the data may still be used elsewhere.\n\nSimply removing a page from the page list will reduce the page count but not the\nfile size. To exclude the content completely, the pages should not be\nadded to the PDF using the PdfWriter.append() function. Instead, only the\ndesired pages should be selected for inclusion\n(note: [PR #1843](https://github.com/py-pdf/pypdf/pull/1843) will add a page\ndeletion feature).\n\nThere can be issues with poor PDF formatting, such as when all pages are linked\nto the same resource. In such cases, dropping references to specific pages\nbecomes useless because there is only one source for all pages.\n\nCropping is an ineffective method for reducing the file size because it only\nadjusts the viewboxes and not the external parts of the source image. Therefore,\nthe content that is no longer visible will still be present in the PDF.\n\n## Going Further\n\nThe presentation [Putting a Squeeze on Your PDF](https://youtube.com/watch?v=tgOABUhVwFs) has other suggestions. One takeaway is that most of the significant size optimizations usually come from image and font modification. However, font optimization, such as replacing, merging, and subsetting, is not within the functionality of pypdf at the moment.\n"
  },
  {
    "path": "docs/user/forms.md",
    "content": "# Interactions with PDF Forms\n\n## Reading form fields\n\n```{testsetup}\npypdf_test_setup(\"user/forms\", {\n    \"form.pdf\": \"../resources/form.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"form.pdf\")\nfields = reader.get_form_text_fields()\nfields == {\"key\": \"value\", \"key2\": \"value2\"}\n\n# You can also get all fields:\nfields = reader.get_fields()\n```\n\n## Filling out forms\n\n```{testcode}\nfrom pypdf import PdfReader, PdfWriter\n\nreader = PdfReader(\"form.pdf\")\nwriter = PdfWriter()\n\npage = reader.pages[0]\nfields = reader.get_fields()\n\nwriter.append(reader)\n\nwriter.update_page_form_field_values(\n    writer.pages[0],\n    {\"fieldname\": \"some filled in text\"},\n    auto_regenerate=False,\n)\n\nwriter.write(\"out-filled-form.pdf\")\n```\n\nGenerally speaking, you will always want to use `auto_regenerate=False`. The\nparameter is `True` by default for legacy compatibility, but this flags the PDF\nprocessor to recompute the field's rendering, and may trigger a \"save changes\"\ndialog for users who open the generated PDF.\n\nIf you want to flatten your form, that is, keeping all form field contents while\nremoving the form fields themselves, you can set the `flatten` parameter in\n{func}`~pypdf.PdfWriter.update_page_form_field_values` to `True`. This\nwill convert form field  contents to regular PDF content. Afterwards, use\n{func}`~pypdf.PdfWriter.remove_annotations` with `subtypes=\"/Widget\"`\nto remove all form fields to get an actual flattened PDF.\n\n## Some notes about form fields and annotations\n\nPDF forms have a dual-nature approach to the fields:\n\n* Within the root object, an `/AcroForm` structure exists.\n  Inside it, you could find (optional):\n\n  - some global elements (Fonts, Resources,...)\n  - some global flags (like `/NeedAppearances` (set/cleared with `auto_regenerate` parameter in `update_page_form_field_values()`) that indicates if the reading program should re-render the visual fields upon document launch)\n  - `/XFA` that houses a form in XDP format (very specific XML that describes the form rendered by some viewers); the `/XFA` form overrides the page content\n  - `/Fields` that houses an array of indirect references that reference the upper _Field_ Objects (roots)\n\n* Within the page `/Annots`, you will spot `/Widget` annotations that define the visual rendering.\n\nTo flesh out this overview:\n\n* The core-specific properties of a field are:\n  - `/FT`: The field type (Button, Text, Choice, or Signature).\n  - `/T`:  The partial field name.\n  - `/V`:  The field’s value, whose format varies depending on the field type.\n  - `/DV`: The default value to which the field reverts when a reset-form action is executed.\n* To streamline readability, _Field_ Objects and _Widget_ Objects can be fused housing all properties.\n* Fields can be organized hierarchically, id est one field can be placed under another. In such instances, the `/Parent` will have an IndirectObject providing Bottom-Up links and `/Kids` is an array carrying IndirectObjects for Top-Down navigation; _Widget_ Objects are still required for visual rendering. To call upon them, use the *fully qualified field name* (where all the individual names of the parent objects are separated by `.`)\n\n  For instance, take two (visual) fields both called _city_, but attached below _sender_ and _receiver_; the corresponding full names will be _sender.city_ and _receiver.city_.\n* When a field is repeated on multiple pages, the Field Object will have many _Widget_ Objects in  `/Kids`. These objects are pure _widgets_, containing no _field_ specific data.\n* If Fields stores only hidden values, no _Widgets_ are required.\n\nIn _pypdf_ fields are extracted from the `/Fields` array:\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"form.pdf\")\nfields = reader.get_fields()\n```\n\n```{testcode}\nfrom pypdf import PdfReader\nfrom pypdf.constants import AnnotationDictionaryAttributes\n\nreader = PdfReader(\"form.pdf\")\nfields = []\nfor page in reader.pages:\n    for annot in page.annotations:\n        annot = annot.get_object()\n        if annot[AnnotationDictionaryAttributes.Subtype] == \"/Widget\":\n            fields.append(annot)\n```\n\nHowever, while similar, there are some relevant differences between the two above blocks of code. Most importantly, the first block will return a list of Field objects, whereas the second will return more generic dictionary-like objects. The objects lists will *mostly* reference the same object in the underlying PDF, meaning you'll find that `obj_taken_fom_first_list.indirect_reference == obj_taken_from _second_list.indirect_reference`. Field objects are generally more ergonomic, as the exposed data can be accessed via clearly named properties. However, the more generic dictionary-like objects will contain data that the Field object does not expose, such as the Rect (the widget's position on the page). Therefore, the correct approach depends on your use case.\n\nHowever, it is also important to note that the two lists do not *always* refer to the same underlying PDF object. For example, if the form contains radio buttons, you will find that `reader.get_fields()` will get the parent object (the group of radio buttons) whereas `page.annotations` will return all the child objects (the individual radio buttons).\n\n```{note}\nRemember that fields are not stored in pages; if you use `add_page()` the field structure is not copied. It is recommended to use `.append()` with the proper parameters instead.\n```\n\nIn case of missing _field_ objects in `/Fields`, `writer.reattach_fields()` will parse page(s) annotations and will reattach them. This fix cannot guess intermediate fields and will not report fields using the same _name_.\n\n## Identify pages where fields are used\n\nTo ease locating page fields you can use `get_pages_showing_field` of PdfReader or PdfWriter. This method accepts a field object, a *PdfObject* that represents a field (as extracted from `_root_object[\"/AcroForm\"][\"/Fields\"]`). The method returns a list of pages, because a field can have multiple widgets as mentioned previously (e.g., radio buttons or text displayed on multiple pages).\n\nThe page numbers can then be retrieved as usual by using `page.page_number`.\n"
  },
  {
    "path": "docs/user/handle-attachments.md",
    "content": "# Handle Attachments\n\nPDF documents can contain attachments, from time to time named embedded file as well.\n\n## Retrieve Attachments\n\nAttachments have a name, but it might not be unique. For this reason, the value of `reader.attachments[\"attachment_name\"]`\nis a list.\n\nYou can extract all attachments like this:\n\n```{testsetup}\npypdf_test_setup(\"user/handle-attachments\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\nfor name, content_list in reader.attachments.items():\n    for i, content in enumerate(content_list):\n        with open(f\"out-attachment-{i}-{name}\", \"wb\") as fp:\n            fp.write(content)\n```\n\nAlternatively, you can retrieve them in an object-oriented fashion if you need\nfurther details for these files:\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\nfor attachment in reader.attachment_list:\n    print(attachment.name, attachment.alternative_name, attachment.content)\n```\n\n## Add Attachments\n\nTo add a new attachment, use the following code:\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"example.pdf\")\nwriter.add_attachment(filename=\"test.txt\", data=b\"Hello World!\")\n```\n\nAs you can see, the basic attachment properties are its name and content. If you\nwant to modify further properties of it, the returned object provides corresponding\nsetters:\n\n```{testcode}\nimport datetime\nimport hashlib\n\nfrom pypdf import PdfWriter\nfrom pypdf.generic import create_string_object, ByteStringObject, NameObject, NumberObject\n\n\nwriter = PdfWriter(clone_from=\"example.pdf\")\nembedded_file = writer.add_attachment(filename=\"test.txt\", data=b\"Hello World!\")\n\nembedded_file.size = NumberObject(len(b\"Hello World!\"))\nembedded_file.alternative_name = create_string_object(\"test1.txt\")\nembedded_file.description = create_string_object(\"My test file\")\nembedded_file.subtype = NameObject(\"/text/plain\")\nembedded_file.checksum = ByteStringObject(hashlib.md5(b\"Hello World!\").digest())\nembedded_file.modification_date = datetime.datetime.now(tz=datetime.timezone.utc)\n# embedded_file.content = \"My new content.\"\n\nwriter.write(\"out-add-attachment.pdf\")\n```\n\nThe same functionality is available if you iterate over the attachments of a writer\nusing `writer.attachment_list`.\n\n## Delete Attachments\n\nTo delete an existing attachment, use the following code:\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"example.pdf\")\nattachment = writer.add_attachment(filename=\"test.txt\", data=b\"Hello World!\")\nattachment.delete()\nassert list(writer.attachment_list) == []\n```\n\nPlease note that this will not delete the associated file relationship\nif it exists. Deleting them as well would require us to know where this has\nbeen defined, which requires more complexity. For now, please consider looking\nfor the corresponding definition yourself and delete it from the array.\n\n## PDF/A compliance\n\nThe following example shows how to add an attachment to a PDF/A-3B compliant document\nwithout breaking compliance:\n\n```{testcode}\nfrom pypdf import PdfWriter\nfrom pypdf.constants import AFRelationship\nfrom pypdf.generic import create_string_object, ArrayObject, NameObject\n\nwriter = PdfWriter(clone_from=\"example.pdf\")\nattachment = writer.add_attachment(filename=\"test.txt\", data=\"Hello World!\")\nattachment.subtype = NameObject(\"/text/plain\")\nattachment.associated_file_relationship = NameObject(AFRelationship.SUPPLEMENT)\nattachment.alternative_name = create_string_object(attachment.name)\n\nif \"/AF\" in writer.root_object:\n    af = writer.root_object[\"/AF\"].get_object()\nelse:\n    af = ArrayObject()\n    writer.root_object[NameObject(\"/AF\")] = af\naf.append(attachment.pdf_object.indirect_reference)\n\nwriter.write(\"out-a3b.pdf\")\n```\n\nThis example marks a relationship of the attachment to the whole document.\nAlternatively, it can be added to most of the other PDF objects as well.\nFor details, see the corresponding PDF specification, like section 14.13\nof the PDF 2.0 specification.\n"
  },
  {
    "path": "docs/user/handling-outlines.md",
    "content": "# Handling Outlines\n\nPDF outlines - also known as bookmarks - provide a structured navigation panel in PDF readers. `pypdf` allows you to read, create, and modify both simple and deeply nested outlines.\n\n## Writing PDF Outlines\n\nTo add outlines, use the {meth}`~pypdf.PdfWriter.add_outline_item` method. This method returns a reference to the created outline, which you can use as a parent to create nested (hierarchical) bookmarks.\n\n### Adding a Simple Outline\n\nThe following example shows how to add a single top-level bookmark. We add an outline item pointing to the first page (index `0`) and save the result.\n\n\n```{testsetup}\npypdf_test_setup(\"user/handling-outlines\", {\n    \"crazyones.pdf\":\"../resources/crazyones.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"crazyones.pdf\")\n\n# Add a top-level bookmark\nwriter.add_outline_item(\n    title=\"Introduction\",\n    page_number=0\n)\n\nwriter.write(\"simple-example.pdf\")\n```\n\n\n### Adding Nested Outlines\n\nYou can build hierarchies (like Chapter → Section) by passing the parent outline item to the `parent` parameter of a new item.\n\nIn the example below, we create a root item \"Introduction\" and nest two sections under it.\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"crazyones.pdf\")\n\n# Add parent (Chapter)\nintroduction = writer.add_outline_item(\n    title=\"Chapter 1\",\n    page_number=0\n)\n\n# Add children (sections) nested under the introduction\nwriter.add_outline_item(\n    title=\"Section 1.1\",\n    page_number=0,\n    parent=introduction\n)\n\nwriter.add_outline_item(\n    title=\"Section 1.2\",\n    page_number=0,\n    parent=introduction\n)\n\nwriter.write(\"nested-example.pdf\")\n```\n\n\n### Advanced Styling and View Modes (Fit Options)\n\nYou can customize the appearance and behavior of bookmarks using optional parameters, such as changing the text color or applying bold and italic styles.\n\nFor detailed information on all available parameters and their formats, please refer to the {meth}`~pypdf.PdfWriter.add_outline_item` API documentation.\n\nThe ``fit`` parameter determines how the page is displayed when the user clicks the bookmark. You can use the {class}`~pypdf.generic.Fit` helper to specify modes like {meth}`~pypdf.generic.Fit.fit`, {meth}`~pypdf.generic.Fit.fit_horizontally`, or {meth}`~pypdf.generic.Fit.xyz`.\n\n\n```{testcode}\nfrom pypdf import PdfWriter\nfrom pypdf.generic import Fit\n\nwriter = PdfWriter(clone_from=\"crazyones.pdf\")\n\n# Top-level chapter (Points to Page 3, Index 2)\nchapter2 = writer.add_outline_item(\n    title=\"Chapter 2\",\n    page_number=0,\n    color=(0, 0, 1),\n    bold=True,\n    italic=False,\n    is_open=True,\n    fit=Fit.fit()\n)\n\n# Section under Chapter 2 (Points to Page 3, Index 2)\nsection2_1 = writer.add_outline_item(\n    title=\"Section 2.1\",\n    page_number=0,\n    parent=chapter2,\n    color=(0, 0.5, 0),\n    bold=False,\n    italic=True,\n    is_open=False,\n    fit=Fit.fit_horizontally(top=800)\n)\n\n# Section with custom zoom (Points to Page 3, Index 2)\nsection2_2 = writer.add_outline_item(\n    title=\"Section 2.2\",\n    page_number=0,\n    parent=chapter2,\n    color=(1, 0, 0),\n    bold=True,\n    italic=True,\n    is_open=True,\n    fit=Fit.xyz(left=0, top=800, zoom=1.25)\n)\n\nwriter.write(\"advanced-example.pdf\")\n```\n\n```{figure} complete-outlines.png\n:alt: An annotated screenshot illustrating simple, nested, and advanced PDF bookmarks.\n\nAn annotated screenshot illustrating simple, nested, and advanced PDF bookmarks in a Table of Contents.\n```\n\n## Reading PDF Outlines\n\n`pypdf` represents outlines as a list of {class}`~pypdf.generic.Destination` objects. If an outline has children, they appear as a nested list directly following their parent.\n\nTo retrieve the page number a bookmark points to, use the {meth}`~pypdf.PdfReader.get_destination_page_number` method, which returns a zero-based page index.\n\n### Reading Simple Outlines\n\nTo extract only the top-level bookmarks (ignoring nested sections), you can iterate over the {attr}`~pypdf.PdfReader.outline` property. Since nested children appear as lists within the outline structure, you must explicitly check for and skip them (`isinstance(outline, list)`) to avoid errors. The example below reads the file created in the previous section.\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"simple-example.pdf\")\n\nprint(\"Simple Outline (Top-Level Only):\")\nprint(\"-\" * 32)\n\nfor outline in reader.outline:\n    # Check if the item is a list (which represents nested children)\n    if isinstance(outline, list):\n        continue  # Skip the nested parts completely\n\n    page_number = reader.get_destination_page_number(outline)\n\n    if page_number is None:\n        print(f\"{outline.title} -> No page destination\")\n    else:\n        print(f\"{outline.title} -> page {page_number + 1}\")\n```\n\n```{testoutput}\nSimple Outline (Top-Level Only):\n--------------------------------\nIntroduction -> page 1\n```\n\n### Reading Nested Outlines\n\nWhen dealing with hierarchical bookmarks, the {attr}`~pypdf.PdfReader.outline` property may contain lists inside lists. You should use a recursive function to traverse the tree.\n\nThe following example defines a `print_outline` function that handles indentation and nested lists to display the structure of the document we created earlier.\n\n```{testcode}\nfrom typing import List, Union\n\nfrom pypdf import PdfReader\nfrom pypdf.generic import Destination\n\n\ndef print_outline(\n    outlines: List[Union[Destination, List[Destination]]],\n    reader: PdfReader,\n    level: int = 0\n) -> None:\n    \"\"\"Recursively print all outline items with indentation.\"\"\"\n    for item in outlines:\n        if isinstance(item, list):\n            # Recursively handle the nested list of children\n            print_outline(item, reader, level + 1)\n        else:\n            page_number = reader.get_destination_page_number(item)\n\n            indent = \"  \" * level\n\n            if page_number is None:\n                print(f\"{indent}- {item.title} (No page destination)\")\n            else:\n                print(f\"{indent}- {item.title} (Page {page_number + 1})\")\n\n\nreader = PdfReader(\"nested-example.pdf\")\n\nprint(\"Nested Outline Hierarchy:\")\nprint(\"-\" * 25)\n\nprint_outline(reader.outline, reader)\n```\n\n```{testoutput}\nNested Outline Hierarchy:\n-------------------------\n- Chapter 1 (Page 1)\n  - Section 1.1 (Page 1)\n  - Section 1.2 (Page 1)\n```\n"
  },
  {
    "path": "docs/user/installation.md",
    "content": "# Installation\n\nThere are several ways to install pypdf. The most common option is to use pip.\n\n## pip\n\npypdf requires Python 3.9+ to run.\n\nTypically, Python comes with `pip`, a package installer. Using it, you can\ninstall pypdf:\n\n```bash\npip install pypdf\n```\n\nIf you are not a superuser (a system administrator / root), you can also just\ninstall pypdf for your current user:\n\n```bash\npip install --user pypdf\n```\n\n### Optional dependencies\n\npypdf tries to be as self-contained as possible, but for some tasks, the amount\nof work to properly maintain the code would be too high. This is especially the\ncase for cryptography and image formats.\n\nIf you simply want to install all optional dependencies, run:\n\n```\npip install pypdf[full]\n```\n\nAlternatively, you can install just some:\n\nIf you plan to use pypdf for encrypting or decrypting PDFs that use AES, you\nwill need to install some extra dependencies. Encryption using RC4 is supported\nusing the regular installation.\n\n```\npip install pypdf[crypto]\n```\n\nIf you plan to use image extraction, you need Pillow:\n\n```\npip install pypdf[image]\n```\n\nFor JBIG2 support, you need to install a global OS-level package as well:\n[`jbig2dec`](https://github.com/ArtifexSoftware/jbig2dec) The installation procedure\ndepends on our operating system. For Ubuntu, use the following, for example:\n\n```\nsudo apt-get install jbig2dec\n```\n\n## Python Version Support\n\nSince pypdf 4.0, every release, including point releases, should work with all\nsupported versions of [Python](https://devguide.python.org/versions/). Thus,\nevery point release is designed to work with all existing Python versions,\nexcluding end-of-life versions.\n\nPrevious versions of pypdf support the following versions of Python:\n\n| Python                 | 3.11 | 3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 2.7 |\n|------------------------|:----:|:----:|:---:|:---:|:---:|:---:|:---:|\n| pypdf 3.x              |  ✅   |  ✅   |  ✅  |  ✅  |  ✅  |  ✅  |  ❌  |\n| PyPDF2 >= 2.0          |  ✅   |  ✅   |  ✅  |  ✅  |  ✅  |  ✅  |  ❌  |\n| PyPDF2 1.20.0 - 1.28.4 |  ❌   |  ✅   |  ✅  |  ✅  |  ✅  |  ✅  |  ✅  |\n| PyPDF2 1.15.0 - 1.20.0 |  ❌   |  ❌   |  ❌  |  ❌  |  ❌  |  ❌  |  ✅  |\n\n\n## Anaconda\n\nAnaconda users can [install pypdf via conda-forge](https://anaconda.org/conda-forge/pypdf).\n\n\n## Development Version\n\nIn case you want to use the current version under development:\n\n```bash\npip install git+https://github.com/py-pdf/pypdf.git\n```\n"
  },
  {
    "path": "docs/user/merging-pdfs.md",
    "content": "# Merging PDF files\n\n## Basic Example\n\n```{testsetup}\npypdf_test_setup(\"user/merging-pdfs\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n    \"hello-world.pdf\": \"../resources/hello-world.pdf\",\n    \"jpeg.pdf\": \"../resources/jpeg.pdf\",\n    \"GeoBase_NHNC1_Data_Model_UML_EN.pdf\": \"../resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf\",\n    \"Seige_of_Vicksburg_Sample_OCR.pdf\": \"../resources/Seige_of_Vicksburg_Sample_OCR.pdf\",\n    \"two-different-pages.pdf\": \"../resources/two-different-pages.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nmerger = PdfWriter()\n\nfor pdf in [\"example.pdf\", \"hello-world.pdf\", \"jpeg.pdf\"]:\n    merger.append(pdf)\n\nmerger.write(\"out-basic.pdf\")\n```\n\nFor more details, see an excellent answer on\n[StackOverflow](https://stackoverflow.com/questions/3444645/merge-pdf-files)\nby Paul Rooney.\n\n````{note}\nDealing with large PDF files might reach the recursion limit of the current\nPython interpreter. In these cases, increasing the limit might help:\n\n```{testcode}\nimport sys\n\n# Example: Increase the current limit by factor 5.\nsys.setrecursionlimit(sys.getrecursionlimit() * 5)\n```\n````\n\n## Showing more merging options\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nmerger = PdfWriter()\n\nwith (\n    open(\"Seige_of_Vicksburg_Sample_OCR.pdf\", \"rb\") as input1,\n    open(\"two-different-pages.pdf\", \"rb\") as input2,\n    open(\"example.pdf\", \"rb\") as input3\n):\n    # Add the first 3 pages of input1 document to output\n    merger.append(fileobj=input1, pages=(0, 3))\n\n    # Insert the first page of input2 into the output beginning after the second page\n    merger.merge(position=2, fileobj=input2, pages=(0, 1))\n\n    # Append entire input3 document to the end of the output document\n    merger.append(input3)\n\n    # Write to an output PDF document\n    merger.write(\"out-advanced.pdf\")\n```\n\n## append\n\n`append` has been slightly extended in `PdfWriter`. See {func}`~pypdf.PdfWriter.append` for more details.\n\n### Examples\n\n```{testcode}\nfrom pypdf import PdfWriter, PdfReader\n\nwriter = PdfWriter()\n\nsource_file_name = \"GeoBase_NHNC1_Data_Model_UML_EN.pdf\"\n\n# Append the first 10 pages from pdf file\nwriter.append(source_file_name, (0, 10))\n\nreader = PdfReader(source_file_name)\n\n# Append the first and 10th page from reader and create an outline\nwriter.append(reader, \"page 1 and 10\", [0, 9])\n```\n\nDuring merging, the relevant named destination will also be imported.\n\nIf you want to insert pages in the middle of the destination, use `merge` (which provides an insertion position).\nYou can insert the same page multiple times, if necessary, even using a list-based syntax:\n\n```{testcode}\n# Insert pages 2 and 3, with page 1 before, between, and after\nwriter.append(reader, [0, 1, 0, 2, 0])\n```\n\n## add_page / insert_page\n\nIt is recommended to use `append` or `merge` instead.\n\n## Merging forms\n\nWhen merging forms, some form fields may have the same names, preventing access to some data.\n\nA grouping field should be added before adding the source PDF to prevent that.\nThe original fields will be identified by adding the group name.\n\nFor example, after calling `reader.add_form_topname(\"form1\")`, the field\npreviously named `field1` is now identified as `form1.field1` when calling\n`reader.get_form_text_fields(True)` or `reader.get_fields()`.\n\nAfter that, you can append the input PDF completely or partially using\n`writer.append` or `writer.merge`. If you insert a set of pages, only those\nfields will be listed.\n\n## reset_translation\n\nDuring cloning, if an object has been already cloned, it will not be cloned again, and a pointer\nto this previously cloned object is returned instead. Because of that, if you add/merge a page that has\nalready been added, the same object will be added the second time. If you modify any of these two pages later,\nboth pages can be modified independently.\n\nTo reset, call  `writer.reset_translation(reader)`.\n\n## Advanced cloning\n\nTo prevent side effects between pages/objects and all objects linked cloning is done during the merge.\n\nThis process will be automatically applied if you use `PdfWriter.append/merge/add_page/insert_page`.\nIf you want to clone an object before attaching it \"manually\", use the `clone` method of any *PdfObject*:\n\n```{testcode}\nfrom pypdf.generic import NameObject, NumberObject, StreamObject\n\nstream_object = StreamObject()\n\ncloned_object = stream_object.clone(writer)\n```\n\nIf you try to clone an object already belonging to the writer, it will return the same object:\n\n```{testcode}\nassert cloned_object == stream_object.clone(writer)\n```\n\nThe same holds true if you try to clone an object twice. It will return the previously cloned object:\n\n```{testcode}\nassert stream_object.clone(writer) == stream_object.clone(writer)\n```\n\nPlease note that if you clone an object, you will clone all the objects below as well,\nincluding the objects pointed by *IndirectObject*. Due to this, if you clone a page that\nincludes some articles (`\"/B\"`), not only the first article, but also all the chained articles\nand the pages where those articles can be read will be copied.\nThis means that you may copy lots of objects which will be saved in the output PDF as well.\n\nTo prevent this, you can provide the list of fields in the dictionaries to be ignored:\n\n```{testcode}\nnew_page = writer.add_page(reader.pages[0], excluded_keys=[\"/B\"])\n```\n\n### Merging rotated pages\n\nIf you are working with rotated pages, you might want to call {func}`~pypdf._page.PageObject.transfer_rotation_to_content` on the page\nbefore merging to avoid wrongly rotated results:\n\n```{testcode}\nbackground = PdfReader(\"jpeg.pdf\").pages[0]\n\nfor page in writer.pages:\n    if page.rotation != 0:\n        page.transfer_rotation_to_content()\n    page.merge_page(background, over=False)\n```\n"
  },
  {
    "path": "docs/user/metadata.md",
    "content": "# Metadata\n\nPDF files can have two types of metadata: \"Regular\" and XMP ones. They can both exist at the same time.\n\n## Reading metadata\n\n```{testsetup}\npypdf_test_setup(\"user/metadata\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n    \"commented-xmp.pdf\": \"../resources/commented-xmp.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\nmeta = reader.metadata\n\n# All the following could be None!\nprint(meta.title)\nprint(meta.author)\nprint(meta.subject)\nprint(meta.creator)\nprint(meta.producer)\nprint(meta.creation_date)\nprint(meta.modification_date)\n```\n\n% Two last rows masked to allow to change example.pdf\n```{testoutput}\n:hide:\n\nPDF Example Document\nNone\nNone\nNone\nSkia/PDF m103 Google Docs Renderer\n...\n...\n```\n\n## Writing metadata\n\n```{testcode}\nfrom datetime import datetime\nfrom pypdf import PdfReader, PdfWriter\n\nreader = PdfReader(\"example.pdf\")\nwriter = PdfWriter()\n\n# Add all pages to the writer\nfor page in reader.pages:\n    writer.add_page(page)\n\n# If you want to add the old metadata, include these two lines\nif reader.metadata is not None:\n    writer.add_metadata(reader.metadata)\n\n# Format the current date and time for the metadata\nutc_time = \"-05'00'\"  # UTC time optional\ntime = datetime.now().strftime(f\"D\\072%Y%m%d%H%M%S{utc_time}\")\n\n# Add the new metadata\nwriter.add_metadata(\n    {\n        \"/Author\": \"Martin\",\n        \"/Producer\": \"Libre Writer\",\n        \"/Title\": \"Title\",\n        \"/Subject\": \"Subject\",\n        \"/Keywords\": \"Keywords\",\n        \"/CreationDate\": time,\n        \"/ModDate\": time,\n        \"/Creator\": \"Creator\",\n        \"/CustomField\": \"CustomField\",\n    }\n)\n\n# Save the new PDF to a file\nwriter.write(\"out-meta-create.pdf\")\n```\n\n## Updating metadata\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"example.pdf\")\n\n# Change some values\nwriter.add_metadata(\n    {\n        \"/Author\": \"Martin\",\n        \"/Producer\": \"Libre Writer\",\n        \"/Title\": \"Title\",\n    }\n)\n\n# Clear all data but keep the entry in PDF\nwriter.metadata = {}\n\n# Replace all entries with new set of entries\nwriter.metadata = {\n    \"/Author\": \"Martin\",\n    \"/Producer\": \"Libre Writer\",\n}\n\n# Save the new PDF to a file\nwriter.write(\"out-meta-update.pdf\")\n```\n\n## Removing metadata entry\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(\"example.pdf\")\n\n# Remove Metadata (/Info entry)\nwriter.metadata = None\n\n# Save the new PDF to a file\nwriter.write(\"out-meta-remove.pdf\")\n```\n\n## Reading XMP metadata\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\nmeta = reader.xmp_metadata\nif meta:\n    print(meta.dc_title)\n    print(meta.dc_description)\n    print(meta.xmp_create_date)\n```\n\n```{testoutput}\n:hide:\n\n{'x-default': 'PDF Example Document'}\n{}\n2025-10-30 09:29:55\n```\n\n## Creating XMP metadata\n\nYou can create XMP metadata easily using the `XmpInformation.create()` method:\n\n```{testcode}\nfrom pypdf import PdfWriter\nfrom pypdf.xmp import XmpInformation\n\n# Create a new XMP metadata object\nxmp = XmpInformation.create()\n\n# Set metadata fields\nxmp.dc_title = {\"x-default\": \"My Document Title\"}\nxmp.dc_creator = [\"Author One\", \"Author Two\"]\nxmp.dc_description = {\"x-default\": \"Document description\"}\nxmp.dc_subject = [\"keyword1\", \"keyword2\", \"keyword3\"]\nxmp.pdf_producer = \"pypdf\"\n\n# Create a writer and add the metadata\nwriter = PdfWriter()\nwriter.add_blank_page(612, 792)  # Add a page\nwriter.xmp_metadata = xmp\nwriter.write(\"out-xmp-create.pdf\")\n```\n\n## Setting XMP metadata fields\n\nThe `XmpInformation` class provides property-based access for all supported metadata fields:\n\n### Dublin Core fields\n\n```{testcode}\nfrom datetime import datetime\nfrom pypdf.xmp import XmpInformation\n\nxmp = XmpInformation.create()\n\n# Single value fields\nxmp.dc_coverage = \"Global coverage\"\nxmp.dc_format = \"application/pdf\"\nxmp.dc_identifier = \"unique-id-123\"\nxmp.dc_source = \"Original Source\"\n\n# Array fields (bags - unordered)\nxmp.dc_contributor = [\"Contributor One\", \"Contributor Two\"]\nxmp.dc_language = [\"en\", \"fr\", \"de\"]\nxmp.dc_publisher = [\"Publisher One\"]\nxmp.dc_relation = [\"Related Doc 1\", \"Related Doc 2\"]\nxmp.dc_subject = [\"keyword1\", \"keyword2\"]\nxmp.dc_type = [\"Document\", \"Text\"]\n\n# Sequence fields (ordered arrays)\nxmp.dc_creator = [\"Primary Author\", \"Secondary Author\"]\nxmp.dc_date = [datetime.now()]\n\n# Language alternative fields\nxmp.dc_title = {\"x-default\": \"Title\", \"en\": \"English Title\", \"fr\": \"Titre français\"}\nxmp.dc_description = {\"x-default\": \"Description\", \"en\": \"English Description\"}\nxmp.dc_rights = {\"x-default\": \"All rights reserved\"}\n```\n\n### XMP fields\n\n```{testcode}\nfrom datetime import datetime\n\n# Date fields accept both datetime objects and strings\nxmp.xmp_create_date = datetime.now()\nxmp.xmp_modify_date = datetime.fromisoformat(\"2023-12-25T10:30:45Z\")\nxmp.xmp_metadata_date = datetime.now()\n\n# Text field\nxmp.xmp_creator_tool = \"pypdf\"\n```\n\n### PDF fields\n\n```{testcode}\nxmp.pdf_keywords = \"keyword1, keyword2, keyword3\"\nxmp.pdf_pdfversion = \"1.4\"\nxmp.pdf_producer = \"pypdf\"\n```\n\n### XMP Media Management fields\n\n```{testcode}\nxmp.xmpmm_document_id = \"uuid:12345678-1234-1234-1234-123456789abc\"\nxmp.xmpmm_instance_id = \"uuid:87654321-4321-4321-4321-cba987654321\"\n```\n\n### PDF/A fields\n\n```{testcode}\nxmp.pdfaid_part = \"1\"\nxmp.pdfaid_conformance = \"B\"\n```\n\n### Clearing metadata fields\n\nYou can clear any field by assigning `None`:\n\n```{testcode}\nxmp.dc_title = None\nxmp.dc_creator = None\nxmp.pdf_producer = None\n```\n\n### Incrementally updating XMP metadata fields\n\nWhen modifying existing XMP metadata, it is often necessary to add or update individual entries while preserving existing values. The XMP properties return standard Python data structures that can be manipulated directly:\n\n```{testcode}\nfrom pypdf.xmp import XmpInformation\n\nxmp = XmpInformation.create()\n\n# Language alternative fields return dictionaries\ntitle = xmp.dc_title or {}\ntitle[\"en\"] = \"English Title\"\ntitle[\"fr\"] = \"Titre français\"\nxmp.dc_title = title\n\n# Bag fields (unordered collections) return lists\nsubjects = xmp.dc_subject or []\nsubjects.append(\"new_keyword\")\nxmp.dc_subject = subjects\n\n# Sequence fields (ordered collections) return lists\ncreators = xmp.dc_creator or []\ncreators.append(\"New Author\")\nxmp.dc_creator = creators\n```\n\nThis approach provides direct control over the data structures while maintaining the property-based interface.\n\n## Modifying XMP metadata\n\nModifying XMP metadata is a bit more complicated.\n\nAs an example, we want to add the following PDF/UA identifier section to the XMP metadata:\n\n```xml\n<rdf:Description rdf:about=\"\" xmlns:pdfuaid=\"http://www.aiim.org/pdfua/ns/id/\">\n    <pdfuaid:part>1</pdfuaid:part>\n</rdf:Description>\n```\n\nThis could be written like this:\n\n```{testcode}\nfrom pypdf import PdfWriter\n\nwriter = PdfWriter(clone_from=\"commented-xmp.pdf\")\n\nmetadata = writer.xmp_metadata\nassert metadata  # Ensure that it is not `None`.\nrdf_root = metadata.rdf_root\nxmp_meta = rdf_root.parentNode\nxmp_document = xmp_meta.parentNode\n\n# Please note that without a text node, the corresponding elements might\n# be omitted completely.\npdfuaid_description = xmp_document.createElement(\"rdf:Description\")\npdfuaid_description.setAttribute(\"rdf:about\", \"\")\npdfuaid_description.setAttribute(\"xmlns:pdfuaid\", \"http://www.aiim.org/pdfua/ns/id/\")\npdfuaid_part = xmp_document.createElement(\"pdfuaid:part\")\npdfuaid_part_text = xmp_document.createTextNode(\"1\")\npdfuaid_part.appendChild(pdfuaid_part_text)\npdfuaid_description.appendChild(pdfuaid_part)\nrdf_root.appendChild(pdfuaid_description)\n\nmetadata.stream.set_data(xmp_document.toxml().encode(\"utf-8\"))\n\nwriter.write(\"out-xmp-update.pdf\")\n```\n\nFor further details on modifying the structure, please refer to {py:mod}`xml.dom.minidom`.\n"
  },
  {
    "path": "docs/user/pdf-version-support.md",
    "content": "# PDF Version Support\n\nPDF comes in the following versions:\n\n* 1993: 1.0\n* 1994: 1.1\n* 1996: 1.2\n* 1999: 1.3\n* 2001: 1.4\n* 2003: 1.5\n* 2004: 1.6\n* 2008: 1.7, ISO 32000-1:2008\n* 2017: 2.0, ISO 32000-2:2017\n\nThe general format didn't change, but new features got added. It can be that\npypdf can do the operations you want on PDF 2.0 files without fully supporting\nall features of PDF 2.0.\n\n## PDF Feature Support by pypdf\n\n| Feature                        | PDF Version | pypdf Support |\n|--------------------------------|:-----------:|:-------------:|\n| CMaps                          |     1.4     |       ✅       |\n| Transparent Graphics           |     1.4     |       ✅       |\n| Content Stream Compression     |     1.5     |       ✅       |\n| Cross-reference Streams        |     1.5     |       ✅       |\n| Object Streams                 |     1.5     |       ✅       |\n| Optional Content Groups (OCGs) |     1.5     |       ❓       |\n| AES Encryption                 |     1.6     |       ✅       |\n\nThis table is not complete - if in doubt, consider having a look at the API documentation or\ninside the issues or try with a corresponding PDF file. In general, we are open to\nadd support for missing features. Please open a new issue if it does not exist yet, and\nkeep in mind that we rely on external contributors to support us with the implementation.\n\nOne commonly requested feature is proper support reading/handling incremental PDF files, see\n[issue #3304](https://github.com/py-pdf/pypdf/issues/3304).\n\nSee [History of PDF](https://en.wikipedia.org/wiki/History_of_PDF) for more\nfeatures.\n\nSome PDF features are not supported by pypdf, but other libraries can be used\nfor them:\n\n* [pyHanko](https://pyhanko.readthedocs.io/en/latest/index.html): Cryptographically sign a PDF ([#302](https://github.com/py-pdf/pypdf/issues/302))\n* [camelot-py](https://pypi.org/project/camelot-py/): Table Extraction ([#231](https://github.com/py-pdf/pypdf/issues/231))\n"
  },
  {
    "path": "docs/user/pdfa-compliance.md",
    "content": "# PDF/A Compliance\n\nPDF/A is a specialized, ISO-standardized version of the Portable Document Format\n(PDF) specifically designed for the long-term preservation and archiving of\nelectronic documents. It ensures that files remain accessible, readable, and\ntrue to their original appearance by embedding all necessary fonts, images, and\nmetadata within the document itself. By adhering to strict guidelines and\nminimizing dependencies on external resources or proprietary software, PDF/A\nensures the consistent and reliable reproduction of content, safeguarding it\nagainst future technological changes and obsolescence.\n\n## PDF/A Versions\n\n* **PDF/A-1**: Based on PDF 1.4, PDF/A-1 is the first version of the standard\n  and is divided into two levels: PDF/A-1a (Level A, ensuring accessibility) and\n  PDF/A-1b (Level B, ensuring visual preservation).\n    * **Level B** (Basic): Ensures visual preservation and basic requirements for archiving.\n    * **Level A** (Accessible): Everything from level B, but includes additional\n      requirements for accessibility, such as tagging, Unicode character\n      mapping, and logical structure.\n* **PDF/A-2**: Based on PDF 1.7 (ISO 32000-1), PDF/A-2 adds features and\n  improvements over PDF/A-1, while maintaining compatibility with PDF/A-1b\n  (Level B) documents.\n    * **Level B** (Basic): Like PDF/A-1b, but support for PDF 1.7 features such\n      as transparency layers.\n    * **Level U** (Unicode): Ensures Unicode mapping without the full\n      accessibility requirements of PDF/A-1a (Level A).\n    * **Level A** (Accessible): Similar to PDF/A-1a\n* **PDF/A-3**: Based on PDF 1.7 (ISO 32000-1), PDF/A-3 is similar to PDF/A-2 but\n  allows the embedding of non-PDF/A files as attachments, enabling the archiving\n  of source or supplementary data alongside the PDF/A document. This is\n  interesting for invoices which can add XML files.\n* **PDF/A-4**: Based on PDF 2.0 (ISO 32000-2), PDF/A-4 introduces new features\n  and improvements for better archiving and accessibility. The previous levels\n  are replaced by PDF/A-4f (ensuring visual preservation and allowing attachments)\n  and PDF/A-4e (Engineering, allows 3D content).\n\n## PDF/A-1b\n\nIn contrast to other PDF documents, PDF/A-1b documents must fulfill those\nrequirements:\n\n* **MarkInfo Object**: The MarkInfo object is a dictionary object within a PDF/A\n  file that provides information about the logical structure and tagging of the\n  document. The MarkInfo object indicates whether the document is tagged,\n  contains optional content, or has a structure tree that describes the logical\n  arrangement of content such as headings, paragraphs, lists, and tables. By\n  including the MarkInfo object, PDF/A ensures that electronic documents are\n  accessible to users with disabilities, such as those using screen readers or\n  other assistive technologies.\n* **Embedded fonts**: All fonts used in the document must be embedded to ensure\n  consistent text rendering across different devices and systems.\n* **Color Spaces**: DeviceRGB is a device-dependent color space that relies on\n  the specific characteristics of the output device, which can lead to\n  inconsistent color rendering across various devices. To achieve accurate and\n  consistent color representation, PDF/A requires the use of device-independent\n  color spaces, such as ICC-based color profiles.\n* **XMP (Extensible Metadata Platform) metadata**: XMP metadata provides a\n  standardized and extensible way to store essential information about a\n  document and its properties. XMP metadata is an XML-based format embedded\n  directly within a PDF/A file. It contains various types of information, such\n  as document title, author, creation and modification dates, keywords, and\n  copyright information, as well as PDF/A-specific details like conformance\n  level and OutputIntent.\n\n## Validation\n\n[VeraPDF](https://docs.verapdf.org/install/) is the go-to PDF/A validator.\n\nThere are several online validators that allow you to simply upload the document:\n\n* [pdfen.com](https://www.pdfen.com/pdf-a-validator)\n* [avepdf.com](https://avepdf.com/pdfa-validation) : Gives an error report\n* [pdfa.org](https://pdfa.org/pdfa-online-verification-service/)\n* [visual-paradigm.com](https://online.visual-paradigm.com/de/online-pdf-editor/pdfa-validator/) - can convert the PDF to a PDF/A\n* [pdf2go.com](https://www.pdf2go.com/validate-pdfa)\n* [slub-dresden.de](https://www.slub-dresden.de/veroeffentlichen/dissertationen-habilitationen/elektronische-veroeffentlichung/slub-pdfa-validator) links to relevant parts in the specification.\n\n## pypdf and PDF/A\n\nAt the moment, pypdf does not make any guarantees regarding PDF/A.\n[Support is very welcome](https://github.com/py-pdf/pypdf/labels/is-pdf%2Fa-compliance).\n"
  },
  {
    "path": "docs/user/post-processing-in-text-extraction.md",
    "content": "# Post-Processing of Text Extraction\n\nPost-processing can recognizably improve the results of text extraction. It is,\nhowever, outside the scope of pypdf itself. Hence, the library will not give\nany direct support for it. It is a natural language processing (NLP) task.\n\nThis page lists a few examples of what can be done as well as a community recipe\nthat can be used as a general purpose post-processing step. If you know more\nabout the specific domain of your documents, e.g., the language, it is likely\nthat you can find custom solutions that work better in your context.\n\n## Ligature Replacement\n\n```{testcode}\ndef replace_ligatures(text: str) -> str:\n    ligatures = {\n        \"ﬀ\": \"ff\",\n        \"ﬁ\": \"fi\",\n        \"ﬂ\": \"fl\",\n        \"ﬃ\": \"ffi\",\n        \"ﬄ\": \"ffl\",\n        \"ﬅ\": \"ft\",\n        \"ﬆ\": \"st\",\n        # \"Ꜳ\": \"AA\",\n        # \"Æ\": \"AE\",\n        \"ꜳ\": \"aa\",\n    }\n    for search, replace in ligatures.items():\n        text = text.replace(search, replace)\n    return text\n```\n\n## Dehyphenation\n\nHyphens are used to break words up so that the appearance of the page is nicer.\n\n```{testcode}\nfrom typing import List\n\n\ndef remove_hyphens(text: str) -> str:\n    \"\"\"\n\n    This fails for:\n    * Natural dashes: well-known, self-replication, use-cases, non-semantic,\n                      Post-processing, Window-wise, viewpoint-dependent\n    * Trailing math operands: 2 - 4\n    * Names: Lopez-Ferreras, VGG-19, CIFAR-100\n    \"\"\"\n    lines = [line.rstrip() for line in text.split(\"\\n\")]\n\n    # Find dashes\n    line_numbers = []\n    for line_no, line in enumerate(lines[:-1]):\n        if line.endswith(\"-\"):\n            line_numbers.append(line_no)\n\n    # Replace\n    for line_no in line_numbers:\n        lines = dehyphenate(lines, line_no)\n\n    return \"\\n\".join(lines)\n\n\ndef dehyphenate(lines: List[str], line_no: int) -> List[str]:\n    next_line = lines[line_no + 1]\n    word_suffix = next_line.split(\" \")[0]\n\n    lines[line_no] = lines[line_no][:-1] + word_suffix\n    lines[line_no + 1] = lines[line_no + 1][len(word_suffix) :]\n    return lines\n```\n\n## Header/Footer Removal\n\nThe following header/footer removal has several drawbacks:\n\n* False-positives, e.g., for the first page when there is a date like 2024.\n* False-negatives in many cases:\n    * Dynamic part, e.g., page label is in the header.\n    * Even/odd pages have different headers.\n    * Some pages, e.g., the first one or chapter pages, do not have a header.\n\n```{testcode}\ndef remove_footer(extracted_texts: list[str], page_labels: list[str]):\n    def remove_page_labels(extracted_texts, page_labels):\n        processed = []\n        for text, label in zip(extracted_texts, page_labels):\n            text_left = text.lstrip()\n            if text_left.startswith(label):\n                text = text_left[len(label) :]\n\n            text_right = text.rstrip()\n            if text_right.endswith(label):\n                text = text_right[: -len(label)]\n\n            processed.append(text)\n        return processed\n\n    extracted_texts = remove_page_labels(extracted_texts, page_labels)\n    return extracted_texts\n```\n\n## Other ideas\n\n* Whitespaces in units: Between a number and its unit should be a space.\n  ([source](https://tex.stackexchange.com/questions/20962/should-i-put-a-space-between-a-number-and-its-unit)).\n  That means: 42 ms, 42 GHz, 42 GB.\n* Percent: English style guides prescribe writing the percent sign following the number without any space between (e.g., 50%).\n* Whitespaces before dots: Should typically be removed.\n* Whitespaces after dots: Should typically be added.\n"
  },
  {
    "path": "docs/user/reading-pdf-annotations.md",
    "content": "# Reading PDF Annotations\n\nPDF 2.0 defines the following annotation types:\n\n* Text\n* Link\n* FreeText\n* Line\n* Square\n* Circle\n* Polygon\n* PolyLine\n* Highlight\n* Underline\n* Squiggly\n* StrikeOut\n* Caret\n* Stamp\n* Ink\n* Popup\n* FileAttachment\n* Sound\n* Movie\n* Screen\n* Widget\n* PrinterMark\n* TrapNet\n* Watermark\n* 3D\n* Redact\n* Projection\n* RichMedia\n\nIn general, annotations can be read like this:\n\n```{testsetup}\npypdf_test_setup(\"user/reading-pdf-annotations\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n})\n```\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\nfor page in reader.pages:\n    if \"/Annots\" in page:\n        for annotation in page[\"/Annots\"]:\n            obj = annotation.get_object()\n            print({\"subtype\": obj[\"/Subtype\"], \"location\": obj[\"/Rect\"]})\n```\n\n```{testoutput}\n:hide:\n\n{'subtype': '/Highlight', 'location': [376.771, 406.213, 413.78, 422.506]}\n{'subtype': '/Popup', 'location': [531.053, 327.965, 715.198, 422.219]}\n{'subtype': '/FileAttachment', 'location': [245.819, 223.288, 252.819, 240.288]}\n{'subtype': '/Stamp', 'location': [68.7536, 187.259, 151.442, 254.124]}\n{'subtype': '/Popup', 'location': [612, 631.925, 816, 745.925]}\n{'subtype': '/Text', 'location': [176.9, 216.719, 200.9, 240.719]}\n{'subtype': '/Popup', 'location': [596, 709.445, 780, 801.445]}\n```\n\nExamples of reading three of the most common annotations:\n\n## Text\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\nfor page in reader.pages:\n    if \"/Annots\" in page:\n        for annotation in page[\"/Annots\"]:\n            subtype = annotation.get_object()[\"/Subtype\"]\n            if subtype == \"/Text\":\n                print(annotation.get_object()[\"/Contents\"])\n```\n\n```{testoutput}\n:hide:\n\nText comment\n```\n\n## Highlights\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\nfor page in reader.pages:\n    if \"/Annots\" in page:\n        for annotation in page[\"/Annots\"]:\n            subtype = annotation.get_object()[\"/Subtype\"]\n            if subtype == \"/Highlight\":\n                coords = annotation.get_object()[\"/QuadPoints\"]\n                x1, y1, x2, y2, x3, y3, x4, y4 = coords\n```\n\n## Attachments\n\n```{testcode}\nfrom pypdf import PdfReader\n\nreader = PdfReader(\"example.pdf\")\n\nattachments = {}\nfor page in reader.pages:\n    if \"/Annots\" in page:\n        for annotation in page[\"/Annots\"]:\n            subtype = annotation.get_object()[\"/Subtype\"]\n            if subtype == \"/FileAttachment\":\n                fileobj = annotation.get_object()[\"/FS\"]\n                attachments[fileobj[\"/F\"]] = fileobj[\"/EF\"][\"/F\"].get_data()\n```\n"
  },
  {
    "path": "docs/user/robustness.md",
    "content": "# Robustness and strict=False\n\nPDF is [specified in various versions](https://pdfa.org/resource/pdf-specification-archive/).\nThe specification of PDF 2.0 has 1003 pages. This length makes it hard to get\neverything right. As a consequence, a lot of PDF files are not strictly following the\nspecification.\n\nIf a PDF file does not follow the specification, it is not always possible to\nbe certain what the intended effect would be. Think of the following broken\nPython code as an example:\n\n```{testcode}\n# Broken\nfunction (foo, bar):\n\n# Potentially intended:\ndef function(foo, bar):\n    ...\n\n# Also possible:\nfunction = (foo, bar)\n```\n\n```{testoutput}\n:hide:\n\nTraceback (most recent call last):\n    ...\nSyntaxError: invalid syntax\n```\n\nWriting a parser, you can go two paths: Either you try to be forgiving and try\nto figure out what the user intended, or you are strict and just tell the user\nthat they should fix their stuff.\n\npypdf gives you the option to be strict or not.\n\npypdf has two core objects:\n\n* {class}`~pypdf.PdfReader`\n* {class}`~pypdf.PdfWriter`\n\nPdfReader and PdfWriter both have a `strict` parameter.\n\nChoosing `strict=True` means that pypdf will raise an exception if a PDF does\nnot follow the specification.\n\nChoosing `strict=False` means that pypdf will try to be forgiving and do\nsomething reasonable, but it will log a warning message. It is a best-effort\napproach.\n"
  },
  {
    "path": "docs/user/security.md",
    "content": "# Security\n\nWe strive to provide a library with secure defaults.\n\n## Configuration\n\n### Filters\n\n*pypdf* currently employs output size limits for some filters which are known to possibly have large compression ratios.\n\nThe usual limit is at 75 MB of uncompressed data during decompression. If this is too low for your use case, and you are\naware of the possible side effects, you can modify the following constants which define the desired maximal output size in bytes:\n\n* `pypdf.filters.ZLIB_MAX_OUTPUT_LENGTH` for the *FlateDecode* filter (zlib compression)\n* `pypdf.filters.LZW_MAX_OUTPUT_LENGTH` for the *LZWDecode* filter (LZW compression)\n* `pypdf.filters.RUN_LENGTH_MAX_OUTPUT_LENGTH` for the *RunLengthDecode* filter (run-length compression)\n\nFor JBIG2 images, there is a similar parameter to limit the memory usage during decoding: `pypdf.filters.JBIG2_MAX_OUTPUT_LENGTH`\nIt defaults to 75 MB as well.\n\nFor all streams, the maximum allowed value for the `/Length` field is limited to `pypdf.filters.MAX_DECLARED_STREAM_LENGTH`, which\ndefaults to 75 MB as well.\n\nFor all array-based streams, the maximum allowed output length is limited to `pypdf.filters.MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH`,\nwhich defaults to 75 MB as well.\n\nFor the *FlateDecode* filter, the number of bytes to attempt recovery with can be set by `pypdf.filters.ZLIB_MAX_RECOVERY_INPUT_LENGTH`.\nIt defaults to 5 MB due to the much more complex recovery approach.\n\nFor the *JBIG2Decode* filter, calling the external *jbig2dec* tool can be disabled by setting `pypdf.filters.JBIG2DEC_BINARY = None`.\n\n### Reading\n\n*pypdf* currently employs the following reading limits on *PdfReader* instances:\n\n* `root_object_recovery_limit` limits the number of objects to read before stopping with Root object recovery in\n  non-strict mode. It defaults to 10 000. Setting it to `None` will fully disable this limit.\n\nIf you want to employ custom limits for the *PdfWriter* as well, the currently preferred way\nis to initialize it from the reader, id est something like\n`PdfWriter(clone_from=PdfReader(\"file.pdf\", root_object_recovery_limit=42))`.\n\n## Reporting possible vulnerabilities\n\nPlease refer to our [security policy](https://github.com/py-pdf/pypdf/security/policy).\n\n## Invalid reports\n\n### Exceptions\n\nMost exceptions raised by our code are considered bugs or robustness issues and can be reported publicly.\nWe consider it the task of the library user to catch exceptions which could cause their service to crash, although we try to\nonly raise a known set of exception types.\n\n### Cryptographic functions\n\nWe receive reports about possibly insecure cryptography from time to time. This includes the following aspects:\n\n* Using the ARC4 cipher\n* Using the AES cipher in ECB mode\n* Using MD5 for hashing\n\nThese are requirements of the PDF standard, which we need to achieve the greatest compatibility with.\nAlthough some of them might be deprecated in PDF 2.0, the PDF 2.0 adoption rate is very low and legacy documents need to be supported.\n\n### XML parsing\n\nWe use `xml.minidom` for parsing XMP information. Given recent Python versions built against recent Expat versions, the usual attacks\n(exponential entity expansion and external entity expansion) should not be possible. We have corresponding tests in place to ensure\nthis for the platforms our tests run against.\n\nFor some details, see [the official documentation](https://docs.python.org/3/library/xml.html#xml-security) and the\n[README for defusedxml](https://github.com/tiran/defusedxml/blob/main/README.md#python-xml-libraries).\n\nPlease note that automated scanners tend to still flag any direct imports of XML modules from the Python standard library as unsafe.\nThere have been discussions about this being outdated already, but they are still being flagged.\n"
  },
  {
    "path": "docs/user/streaming-data.md",
    "content": "# Streaming Data with pypdf\n\nIn some cases, you might want to avoid saving things explicitly as a file\nto disk, e.g. when you want to store the PDF in a database or AWS S3.\n\npypdf supports streaming data to a file-like object:\n\n\n```{testsetup}\npypdf_test_setup(\"user/streaming-data\", {\n    \"example.pdf\": \"../resources/example.pdf\",\n})\n```\n\n```{testcode}\nfrom io import BytesIO\nfrom pypdf import PdfReader, PdfWriter\n\n# Prepare example\nwith open(\"example.pdf\", \"rb\") as fh:\n    bytes_stream = BytesIO(fh.read())\n\n# Read from bytes_stream\nreader = PdfReader(bytes_stream)\n\n# Write to bytes_stream\nwriter = PdfWriter()\nwith BytesIO() as bytes_stream:\n    writer.write(bytes_stream)\n```\n\n## Writing a PDF directly to AWS S3\n\nSuppose you want to manipulate a PDF and write it directly to AWS S3 without having\nto write the document to a file first. We have the original PDF in `raw_bytes_data` as `bytes`\nand want to set `my-secret-password`:\n\n% We prefer not to execute doc examples which require access to cloud providers\n```{testcode}\n:skipif: True\n\nfrom io import BytesIO\n\nimport boto3\nfrom pypdf import PdfReader, PdfWriter\n\n\nreader = PdfReader(BytesIO(raw_bytes_data))\nwriter = PdfWriter()\n\n# Add all pages to the writer\nfor page in reader.pages:\n    writer.add_page(page)\n\n# Add a password to the new PDF\nwriter.encrypt(\"my-secret-password\")\n\n# Save the new PDF to a file\nwith BytesIO() as bytes_stream:\n    writer.write(bytes_stream)\n    bytes_stream.seek(0)\n    s3 = boto3.client(\"s3\")\n    s3.write_get_object_response(\n        Body=bytes_stream, RequestRoute=request_route, RequestToken=request_token\n    )\n```\n\n## Reading PDFs directly from cloud services\n\nOne option is to first download the file and then pass the local file path to `PdfReader`.\nAnother option is to get a byte stream.\n\nFor AWS S3 it works like this:\n\n% We prefer not to execute doc examples which require access to cloud providers\n```{testcode}\n:skipif: True\n\nfrom io import BytesIO\n\nimport boto3\nfrom pypdf import PdfReader\n\n\ns3 = boto3.client(\"s3\")\nobj = s3.get_object(Body=csv_buffer.getvalue(), Bucket=\"my-bucket\", Key=\"my/doc.pdf\")\nreader = PdfReader(BytesIO(obj[\"Body\"].read()))\n```\n\nTo use with Google Cloud storage:\n\n% We prefer not to execute doc examples which require access to cloud providers\n```{testcode}\n:skipif: True\n\nfrom io import BytesIO\n\nfrom google.cloud import storage\n\n# os.environ[\"GOOGLE_APPLICATION_CREDENTIALS\"] must be set\nstorage_client = storage.Client()\nblob = storage_client.bucket(\"my-bucket\").blob(\"mydoc.pdf\")\nfile_stream = BytesIO()\nblob.download_to_file(file_stream)\nreader = PdfReader(file_stream)\n```\n"
  },
  {
    "path": "docs/user/suppress-warnings.md",
    "content": "# Exceptions, Warnings, and Log messages\n\npypdf makes use of three mechanisms to show if something went wrong:\n\n* **Exceptions** are error cases that pypdf users should explicitly handle.\n  In the `strict=True` mode, most log messages with the warning level will\n  become exceptions. This can be useful in applications where you can require\n  a user to fix the broken PDF.\n* **Warnings** are avoidable issues, such as using deprecated classes /\n  functions / parameters. Another example is missing capabilities of pypdf.\n  In those cases, pypdf users should adjust their code. Warnings\n  are issued by the `warnings` module - those are different from the log-level\n  \"warning.\"\n* **Log messages** are informative messages that can be used for post-mortem\n  analysis. Most of the time, users can ignore them. They come in different\n  *levels*, such as info / warning / error indicating the severity.\n  Examples are non-standard compliant PDF files which pypdf can deal with or\n  a missing implementation that leads to a part of the text not being extracted.\n\n\n## Exceptions\n\nExceptions need to be caught if you want to handle them. For example, you could\nwant to read the text from a PDF as a part of a search function.\n\nMost PDF files do not follow the specification. In this case, pypdf needs to\nguess which kinds of mistakes were potentially done when the PDF file was created.\nSee [the robustness page](robustness.md) for the related issues.\n\nAs a user, you likely do not care about it. If it is readable in any way, you\nwant the text. You might use pdfminer.six as a fallback and do this:\n\n% We prefer not to execute doc examples for third-party package \"pdfminer.six\" used in one code snippet only\n```{testcode}\n:skipif: True\n\nfrom pypdf import PdfReader\nfrom pdfminer.high_level import extract_text as fallback_text_extraction\n\ntext = \"\"\ntry:\n    reader = PdfReader(\"example.pdf\")\n    for page in reader.pages:\n        text += page.extract_text()\nexcept Exception as exc:\n    text = fallback_text_extraction(\"example.pdf\")\n```\n\nYou could also capture [`pypdf.errors.PyPdfError`](https://github.com/py-pdf/pypdf/blob/main/pypdf/errors.py)\nif you prefer something more specific.\n\n## Warnings\n\nThe [`warnings` module](https://docs.python.org/3/library/warnings.html) allows\nyou to ignore warnings:\n\n```{testcode}\nimport warnings\n\nwarnings.filterwarnings(\"ignore\")\n```\n\nIn many cases, you actually want to start Python with the `-W` flag so that you\nsee all warnings. This is especially true for Continuous Integration (CI).\n\n## Log messages\n\nLog messages can be noisy in some cases. pypdf hopefully has a reasonable\nlevel of log messages, but you can reduce which types of messages you want to\nsee:\n\n```{testcode}\nimport logging\n\nlogger = logging.getLogger(\"pypdf\")\nlogger.setLevel(logging.ERROR)\n```\n\nThe [`logging` module](https://docs.python.org/3/library/logging.html#logging-levels)\ndefines six log levels:\n\n* CRITICAL\n* ERROR\n* WARNING\n* INFO\n* DEBUG\n* NOTSET\n"
  },
  {
    "path": "docs/user/viewer-preferences.md",
    "content": "# Adding Viewer Preferences\n\nIt is possible to set viewer preferences of a PDF file.\n§12.2 of the [PDF 1.7 specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf).\n\nNote that the `/ViewerPreferences` dictionary does not exist by default.\nIf it is not already present, it must be created by calling the\n{func}`~pypdf.PdfWriter.create_viewer_preferences` method.\n\nIf viewer preferences exist in a PDF file being read with {class}`~pypdf.PdfReader`,\nyou can access them as properties of {attr}`~pypdf.PdfReader.viewer_preferences`.\nOtherwise, the {attr}`~pypdf.PdfReader.viewer_preferences` property will be set to `None`.\n\n## Example\n\n```{testsetup}\npypdf_test_setup(\"user/viewer-preferences\")\n```\n\n```{testcode}\nfrom pypdf import PdfWriter\nfrom pypdf.generic import ArrayObject, NumberObject\n\nwriter = PdfWriter()\n\nwriter.create_viewer_preferences()\n\n# /HideToolbar\nwriter.viewer_preferences.hide_toolbar = True\n# /HideMenubar\nwriter.viewer_preferences.hide_menubar = True\n# /HideWindowUI\nwriter.viewer_preferences.hide_windowui = True\n# /FitWindow\nwriter.viewer_preferences.fit_window = True\n# /CenterWindow\nwriter.viewer_preferences.center_window = True\n# /DisplayDocTitle\nwriter.viewer_preferences.display_doctitle = True\n\n# /NonFullScreenPageMode\nwriter.viewer_preferences.non_fullscreen_pagemode = \"/UseNone\"  # default\nwriter.viewer_preferences.non_fullscreen_pagemode = \"/UseOutlines\"\nwriter.viewer_preferences.non_fullscreen_pagemode = \"/UseThumbs\"\nwriter.viewer_preferences.non_fullscreen_pagemode = \"/UseOC\"\n\n# /Direction\nwriter.viewer_preferences.direction = \"/L2R\"  # default\nwriter.viewer_preferences.direction = \"/R2L\"\n\n# /ViewArea\nwriter.viewer_preferences.view_area = \"/CropBox\"\n# /ViewClip\nwriter.viewer_preferences.view_clip = \"/CropBox\"\n# /PrintArea\nwriter.viewer_preferences.print_area = \"/CropBox\"\n# /PrintClip\nwriter.viewer_preferences.print_clip = \"/CropBox\"\n\n# /PrintScaling\nwriter.viewer_preferences.print_scaling = \"/None\"\nwriter.viewer_preferences.print_scaling = \"/AppDefault\"  # default according to PDF spec\n\n# /Duplex\nwriter.viewer_preferences.duplex = \"/Simplex\"\nwriter.viewer_preferences.duplex = \"/DuplexFlipShortEdge\"\nwriter.viewer_preferences.duplex = \"/DuplexFlipLongEdge\"\n\n# /PickTrayByPDFSize\nwriter.viewer_preferences.pick_tray_by_pdfsize = True\n# /PrintPageRange\nwriter.viewer_preferences.print_pagerange = ArrayObject(\n    [NumberObject(\"1\"), NumberObject(\"10\"), NumberObject(\"20\"), NumberObject(\"30\")]\n)\n# /NumCopies\nwriter.viewer_preferences.num_copies = 2\n\nfor i in range(40):\n    writer.add_blank_page(10, 10)\n\nwriter.write(\"out.pdf\")\n```\n\nThe names beginning with a slash character are part of the PDF file format. They are\nincluded here to ease searching the pypdf documentation\nfor these names from the PDF specification.\n"
  },
  {
    "path": "make_release.py",
    "content": "\"\"\"Internal tool to update the CHANGELOG.\"\"\"\n\nimport json\nimport subprocess\nimport urllib.request\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\n\nGH_ORG = \"py-pdf\"\nGH_PROJECT = \"pypdf\"\nVERSION_FILE_PATH = \"pypdf/_version.py\"\nCHANGELOG_FILE_PATH = \"CHANGELOG.md\"\n\n\n@dataclass(frozen=True)\nclass Change:\n    \"\"\"Capture the data of a git commit.\"\"\"\n\n    commit_hash: str\n    prefix: str\n    message: str\n    author: str\n    author_login: str\n\n\ndef main(changelog_path: str) -> None:\n    \"\"\"\n    Create a changelog.\n\n    Args:\n        changelog_path: The location of the CHANGELOG file\n\n    \"\"\"\n    changelog = get_changelog(changelog_path)\n    git_tag = get_most_recent_git_tag()\n    changes, changes_with_author = get_formatted_changes(git_tag)\n    if changes == \"\":\n        print(\"No changes\")\n        return\n\n    new_version = version_bump(git_tag)\n    new_version = get_version_interactive(new_version, changes)\n    adjust_version_py(new_version)\n\n    today = datetime.now(tz=timezone.utc)\n    header = f\"## Version {new_version}, {today:%Y-%m-%d}\\n\"\n    url = f\"https://github.com/{GH_ORG}/{GH_PROJECT}/compare/{git_tag}...{new_version}\"\n    trailer = f\"\\n[Full Changelog]({url})\\n\\n\"\n    new_entry = header + changes + trailer\n    print(new_entry)\n    write_commit_msg_file(new_version, changes_with_author + trailer)\n    # write_release_msg_file(new_version, changes_with_author + trailer, today)\n\n    # Make the script idempotent by checking if the new entry is already in the changelog\n    if new_entry in changelog:\n        print(\"Changelog is already up-to-date!\")\n        return\n\n    new_changelog = \"# CHANGELOG\\n\\n\" + new_entry + strip_header(changelog)\n    write_changelog(new_changelog, changelog_path)\n    print_instructions(new_version)\n\n\ndef print_instructions(new_version: str) -> None:\n    \"\"\"Print release instructions.\"\"\"\n    print(\"=\" * 80)\n    print(f\"☑  {VERSION_FILE_PATH} was adjusted to '{new_version}'\")\n    print(f\"☑  {CHANGELOG_FILE_PATH} was adjusted\")\n    print()\n    print(\"Now run:\")\n    print(\"  git commit -eF RELEASE_COMMIT_MSG.md\")\n    print(\"  git push\")\n\n\ndef adjust_version_py(version: str) -> None:\n    \"\"\"Adjust the __version__ string.\"\"\"\n    with open(VERSION_FILE_PATH, \"w\") as fp:\n        fp.write(f'__version__ = \"{version}\"\\n')\n\n\ndef get_version_interactive(new_version: str, changes: str) -> str:\n    \"\"\"Get the new __version__ interactively.\"\"\"\n    from rich.prompt import Prompt  # noqa: PLC0415\n\n    print(\"The changes are:\")\n    print(changes)\n    orig = new_version\n    new_version = Prompt.ask(\"New semantic version\", default=orig)\n    while not is_semantic_version(new_version):\n        new_version = Prompt.ask(\n            \"That was not a semantic version. Please enter a semantic version\",\n            default=orig,\n        )\n    return new_version\n\n\ndef is_semantic_version(version: str) -> bool:\n    \"\"\"Check if the given version is a semantic version.\"\"\"\n    # This doesn't cover the edge-cases like pre-releases\n    if version.count(\".\") != 2:\n        return False\n    try:\n        return bool([int(part) for part in version.split(\".\")])\n    except Exception:\n        return False\n\n\ndef write_commit_msg_file(new_version: str, commit_changes: str) -> None:\n    \"\"\"\n    Write a file that can be used as a commit message.\n\n    Like this:\n\n        git commit -eF RELEASE_COMMIT_MSG.md && git push\n    \"\"\"\n    with open(\"RELEASE_COMMIT_MSG.md\", \"w\") as fp:\n        fp.write(f\"REL: {new_version}\\n\\n\")\n        fp.write(\"## What's new\\n\")\n        fp.write(commit_changes)\n\n\ndef write_release_msg_file(\n    new_version: str, commit_changes: str, today: datetime\n) -> None:\n    \"\"\"\n    Write a file that can be used as a git tag message.\n\n    Like this:\n\n        git tag -eF RELEASE_TAG_MSG.md && git push\n    \"\"\"\n    with open(\"RELEASE_TAG_MSG.md\", \"w\") as fp:\n        fp.write(f\"Version {new_version}, {today:%Y-%m-%d}\\n\\n\")\n        fp.write(\"## What's new\\n\")\n        fp.write(commit_changes)\n\n\ndef strip_header(md: str) -> str:\n    \"\"\"Remove the 'CHANGELOG' header.\"\"\"\n    return md.removeprefix(\"# CHANGELOG\").lstrip()\n\n\ndef version_bump(git_tag: str) -> str:\n    \"\"\"\n    Increase the patch version of the git tag by one.\n\n    Args:\n        git_tag: Old version tag\n\n    Returns:\n        The new version where the patch version is bumped.\n\n    \"\"\"\n    # just assume a patch version change\n    major, minor, patch = git_tag.split(\".\")\n    return f\"{major}.{minor}.{int(patch) + 1}\"\n\n\ndef get_changelog(changelog_path: str) -> str:\n    \"\"\"\n    Read the changelog.\n\n    Args:\n        changelog_path: Path to the CHANGELOG file\n\n    Returns:\n        Data of the CHANGELOG\n\n    \"\"\"\n    with open(changelog_path, encoding=\"utf-8\") as fh:\n        return fh.read()\n\n\ndef write_changelog(new_changelog: str, changelog_path: str) -> None:\n    \"\"\"\n    Write the changelog.\n\n    Args:\n        new_changelog: Contents of the new CHANGELOG\n        changelog_path: Path where the CHANGELOG file is\n\n    \"\"\"\n    with open(changelog_path, \"w\", encoding=\"utf-8\") as fh:\n        fh.write(new_changelog)\n\n\ndef get_formatted_changes(git_tag: str) -> tuple[str, str]:\n    \"\"\"\n    Format the changes done since the last tag.\n\n    Args:\n        git_tag: the reference tag\n\n    Returns:\n        Changes done since git_tag\n\n    \"\"\"\n    commits = get_git_commits_since_tag(git_tag)\n\n    # Group by prefix\n    grouped = {}\n    for commit in commits:\n        if commit.prefix not in grouped:\n            grouped[commit.prefix] = []\n        grouped[commit.prefix].append(\n            {\"msg\": commit.message, \"author\": commit.author_login}\n        )\n\n    # Order prefixes\n    order = [\n        \"SEC\",\n        \"DEP\",\n        \"ENH\",\n        \"PI\",\n        \"BUG\",\n        \"ROB\",\n        \"DOC\",\n        \"DEV\",\n        \"CI\",\n        \"MAINT\",\n        \"TST\",\n        \"STY\",\n    ]\n    abbrev2long = {\n        \"SEC\": \"Security\",\n        \"DEP\": \"Deprecations\",\n        \"ENH\": \"New Features\",\n        \"BUG\": \"Bug Fixes\",\n        \"ROB\": \"Robustness\",\n        \"DOC\": \"Documentation\",\n        \"DEV\": \"Developer Experience\",\n        \"CI\": \"Continuous Integration\",\n        \"MAINT\": \"Maintenance\",\n        \"TST\": \"Testing\",\n        \"STY\": \"Code Style\",\n        \"PI\": \"Performance Improvements\",\n    }\n\n    # Create output\n    output = \"\"\n    output_with_user = \"\"\n    for prefix in order:\n        if prefix not in grouped:\n            continue\n        tmp = f\"\\n### {abbrev2long[prefix]} ({prefix})\\n\"  # header\n        output += tmp\n        output_with_user += tmp\n        for commit in grouped[prefix]:\n            output += f\"- {commit['msg']}\\n\"\n            output_with_user += f\"- {commit['msg']} by @{commit['author']}\\n\"\n        del grouped[prefix]\n\n    if grouped:\n        output += \"\\n### Other\\n\"\n        output_with_user += \"\\n### Other\\n\"\n        for prefix, commits in grouped.items():\n            for commit in commits:\n                output += f\"- {prefix}: {commit['msg']}\\n\"\n                output_with_user += (\n                    f\"- {prefix}: {commit['msg']} by @{commit['author']}\\n\"\n                )\n\n    return output, output_with_user\n\n\ndef get_most_recent_git_tag() -> str:\n    \"\"\"\n    Get the git tag most recently created.\n\n    Returns:\n        Most recently created git tag.\n\n    \"\"\"\n    return subprocess.check_output(\n        [\"git\", \"describe\", \"--tag\", \"--abbrev=0\"], stderr=subprocess.STDOUT, text=True\n    ).strip()\n\n\ndef get_author_mapping(line_count: int) -> dict[str, str]:\n    \"\"\"\n    Get the authors for each commit.\n\n    Args:\n        line_count: Number of lines from Git log output. Used for determining how\n            many commits to fetch.\n\n    Returns:\n        A mapping of long commit hashes to author login handles.\n\n    \"\"\"\n    per_page = min(line_count, 100)\n    page = 1\n    mapping: dict[str, str] = {}\n    for _ in range(0, line_count, per_page):\n        with urllib.request.urlopen(\n            f\"https://api.github.com/repos/{GH_ORG}/{GH_PROJECT}/commits?per_page={per_page}&page={page}\"\n        ) as response:\n            commits = json.loads(response.read())\n        page += 1\n        for commit in commits:\n            mapping[commit[\"sha\"]] = commit[\"author\"][\"login\"]\n    return mapping\n\n\ndef get_git_commits_since_tag(git_tag: str) -> list[Change]:\n    \"\"\"\n    Get all commits since the last tag.\n\n    Args:\n        git_tag: Reference tag from which the changes to the current commit are\n            fetched.\n\n    Returns:\n        List of all changes since git_tag.\n\n    \"\"\"\n    commits = (\n        subprocess.check_output(\n            [\n                \"git\",\n                \"--no-pager\",\n                \"log\",\n                f\"{git_tag}..HEAD\",\n                '--pretty=format:\"%H:::%s:::%aN\"',\n            ],\n            stderr=subprocess.STDOUT,\n        )\n        .decode(\"UTF-8\")\n        .strip()\n    )\n    lines = commits.splitlines()\n    authors = get_author_mapping(len(lines))\n    return [parse_commit_line(line, authors) for line in lines if line != \"\"]\n\n\ndef parse_commit_line(line: str, authors: dict[str, str]) -> Change:\n    \"\"\"\n    Parse the first line of a git commit message.\n\n    Args:\n        line: The first line of a git commit message.\n\n    Returns:\n        The parsed Change object\n\n    Raises:\n        ValueError: The commit line is not well-structured\n\n    \"\"\"\n    parts = line.strip().strip('\"\\\\').split(\":::\")\n    if len(parts) != 3:\n        raise ValueError(f\"Invalid commit line: '{line}'\")\n    commit_hash, rest, author = parts\n    if \":\" in rest:\n        prefix, message = rest.split(\": \", 1)\n    else:\n        prefix = \"\"\n        message = rest\n\n    # Standardize\n    message = message.strip()\n    commit_hash = commit_hash.strip()\n\n    author_login = authors[commit_hash]\n\n    prefix = prefix.strip()\n    if prefix == \"DOCS\":\n        prefix = \"DOC\"\n\n    return Change(\n        commit_hash=commit_hash,\n        prefix=prefix,\n        message=message,\n        author=author,\n        author_login=author_login,\n    )\n\n\nif __name__ == \"__main__\":\n    main(CHANGELOG_FILE_PATH)\n"
  },
  {
    "path": "pypdf/__init__.py",
    "content": "\"\"\"\npypdf is a free and open-source pure-python PDF library capable of splitting,\nmerging, cropping, and transforming the pages of PDF files. It can also add\ncustom data, viewing options, and passwords to PDF files. pypdf can retrieve\ntext and metadata from PDFs as well.\n\nYou can read the full docs at https://pypdf.readthedocs.io/.\n\"\"\"\n\nfrom ._crypt_providers import crypt_provider\nfrom ._doc_common import DocumentInformation\nfrom ._encryption import PasswordType\nfrom ._page import PageObject, Transformation\nfrom ._reader import PdfReader\nfrom ._text_extraction import mult\nfrom ._version import __version__\nfrom ._writer import ObjectDeletionFlag, PdfWriter\nfrom .constants import ImageType\nfrom .pagerange import PageRange, parse_filename_page_ranges\nfrom .papersizes import PaperSize\n\ntry:\n    import PIL\n\n    pil_version = PIL.__version__\nexcept ImportError:\n    pil_version = \"none\"\n\n_debug_versions = (\n    f\"pypdf=={__version__}, {crypt_provider=}, PIL={pil_version}\"\n)\n\n__all__ = [\n    \"DocumentInformation\",\n    \"ImageType\",\n    \"ObjectDeletionFlag\",\n    \"PageObject\",\n    \"PageRange\",\n    \"PaperSize\",\n    \"PasswordType\",\n    \"PdfReader\",\n    \"PdfWriter\",\n    \"Transformation\",\n    \"__version__\",\n    \"_debug_versions\",\n    \"mult\",\n    \"parse_filename_page_ranges\",\n]\n"
  },
  {
    "path": "pypdf/_cmap.py",
    "content": "import binascii\nfrom binascii import Error as BinasciiError\nfrom binascii import unhexlify\nfrom math import ceil\nfrom typing import Any, Union, cast\n\nfrom ._codecs import adobe_glyphs, charset_encoding\nfrom ._utils import logger_error, logger_warning\nfrom .errors import LimitReachedError\nfrom .generic import (\n    DecodedStreamObject,\n    DictionaryObject,\n    NullObject,\n    StreamObject,\n    is_null_or_none,\n)\n\n_predefined_cmap: dict[str, str] = {\n    \"/Identity-H\": \"utf-16-be\",\n    \"/Identity-V\": \"utf-16-be\",\n    \"/GB-EUC-H\": \"gbk\",\n    \"/GB-EUC-V\": \"gbk\",\n    \"/GBpc-EUC-H\": \"gb2312\",\n    \"/GBpc-EUC-V\": \"gb2312\",\n    \"/GBK-EUC-H\": \"gbk\",\n    \"/GBK-EUC-V\": \"gbk\",\n    \"/GBK2K-H\": \"gb18030\",\n    \"/GBK2K-V\": \"gb18030\",\n    \"/ETen-B5-H\": \"cp950\",\n    \"/ETen-B5-V\": \"cp950\",\n    \"/ETenms-B5-H\": \"cp950\",\n    \"/ETenms-B5-V\": \"cp950\",\n    \"/UniCNS-UTF16-H\": \"utf-16-be\",\n    \"/UniCNS-UTF16-V\": \"utf-16-be\",\n    \"/UniGB-UTF16-H\": \"gb18030\",\n    \"/UniGB-UTF16-V\": \"gb18030\",\n    # UCS2 in code\n}\n\n\ndef get_encoding(\n    ft: DictionaryObject\n) -> tuple[Union[str, dict[int, str]], dict[Any, Any]]:\n    encoding = _parse_encoding(ft)\n    map_dict, int_entry = _parse_to_unicode(ft)\n\n    # Apply rule from PDF ref 1.7 §5.9.1, 1st bullet:\n    #   if cmap not empty encoding should be discarded\n    #   (here transformed into identity for those characters)\n    # If encoding is a string, it is expected to be an identity translation.\n    if isinstance(encoding, dict):\n        for x in int_entry:\n            if x <= 255:\n                encoding[x] = chr(x)\n\n    return encoding, map_dict\n\n\ndef _parse_encoding(\n    ft: DictionaryObject\n) -> Union[str, dict[int, str]]:\n    encoding: Union[str, list[str], dict[int, str]] = []\n    if \"/Encoding\" not in ft:\n        if \"/BaseFont\" in ft and cast(str, ft[\"/BaseFont\"]) in charset_encoding:\n            encoding = dict(\n                zip(range(256), charset_encoding[cast(str, ft[\"/BaseFont\"])])\n            )\n        else:\n            encoding = \"charmap\"\n        return encoding\n    enc: Union[str, DictionaryObject, NullObject] = cast(\n        Union[str, DictionaryObject, NullObject], ft[\"/Encoding\"].get_object()\n    )\n    if isinstance(enc, str):\n        try:\n            # already done : enc = NameObject.unnumber(enc.encode()).decode()\n            # for #xx decoding\n            if enc in charset_encoding:\n                encoding = charset_encoding[enc].copy()\n            elif enc in _predefined_cmap:\n                encoding = _predefined_cmap[enc]\n            elif \"-UCS2-\" in enc:\n                encoding = \"utf-16-be\"\n            else:\n                raise Exception(\"not found\")\n        except Exception:\n            logger_error(\"Advanced encoding %(encoding)s not implemented yet\", source=__name__, encoding=enc)\n            encoding = enc\n    elif isinstance(enc, DictionaryObject) and \"/BaseEncoding\" in enc:\n        try:\n            encoding = charset_encoding[cast(str, enc[\"/BaseEncoding\"])].copy()\n        except Exception:\n            logger_error(\n                \"Advanced encoding %(encoding)s not implemented yet\",\n                source=__name__, encoding=encoding\n            )\n            encoding = charset_encoding[\"/StandardEncoding\"].copy()\n    else:\n        encoding = charset_encoding[\"/StandardEncoding\"].copy()\n    if isinstance(enc, DictionaryObject) and \"/Differences\" in enc:\n        x: int = 0\n        o: Union[int, str]\n        for o in cast(DictionaryObject, enc[\"/Differences\"]):\n            if isinstance(o, int):\n                x = o\n            else:  # isinstance(o, str):\n                try:\n                    if x < len(encoding):\n                        encoding[x] = adobe_glyphs[o]  # type: ignore\n                except Exception:\n                    encoding[x] = o  # type: ignore\n                x += 1\n    if isinstance(encoding, list):\n        encoding = dict(zip(range(256), encoding))\n    return encoding\n\n\ndef _parse_to_unicode(\n    ft: DictionaryObject\n) -> tuple[dict[Any, Any], list[int]]:\n    # will store all translation code\n    # and map_dict[-1] we will have the number of bytes to convert\n    map_dict: dict[Any, Any] = {}\n\n    # will provide the list of cmap keys as int to correct encoding\n    int_entry: list[int] = []\n\n    if \"/ToUnicode\" not in ft:\n        if ft.get(\"/Subtype\", \"\") == \"/Type1\":\n            return _type1_alternative(ft, map_dict, int_entry)\n        return {}, []\n    process_rg: bool = False\n    process_char: bool = False\n    multiline_rg: Union[\n        None, tuple[int, int]\n    ] = None  # tuple = (current_char, remaining size) ; cf #1285 for example of file\n    cm = prepare_cm(ft)\n    for line in cm.split(b\"\\n\"):\n        process_rg, process_char, multiline_rg = process_cm_line(\n            line.strip(b\" \\t\"),\n            process_rg,\n            process_char,\n            multiline_rg,\n            map_dict,\n            int_entry,\n        )\n\n    return map_dict, int_entry\n\n\ndef prepare_cm(ft: DictionaryObject) -> bytes:\n    tu = ft[\"/ToUnicode\"]\n    cm: bytes\n    if isinstance(tu, StreamObject):\n        cm = cast(DecodedStreamObject, ft[\"/ToUnicode\"]).get_data()\n    else:  # if (tu is None) or cast(str, tu).startswith(\"/Identity\"):\n        # the full range 0000-FFFF will be processed\n        cm = b\"beginbfrange\\n<0000> <0001> <0000>\\nendbfrange\"\n    if isinstance(cm, str):\n        cm = cm.encode()\n    # we need to prepare cm before due to missing return line in pdf printed\n    # to pdf from word\n    cm = (\n        cm.strip()\n        .replace(b\"beginbfchar\", b\"\\nbeginbfchar\\n\")\n        .replace(b\"endbfchar\", b\"\\nendbfchar\\n\")\n        .replace(b\"beginbfrange\", b\"\\nbeginbfrange\\n\")\n        .replace(b\"endbfrange\", b\"\\nendbfrange\\n\")\n        .replace(b\"<<\", b\"\\n{\\n\")  # text between << and >> not used but\n        .replace(b\">>\", b\"\\n}\\n\")  # some solution to find it back\n    )\n    ll = cm.split(b\"<\")\n    for i in range(len(ll)):\n        j = ll[i].find(b\">\")\n        if j >= 0:\n            if j == 0:\n                # string is empty: stash a placeholder here (see below)\n                # see https://github.com/py-pdf/pypdf/issues/1111\n                content = b\".\"\n            else:\n                content = ll[i][:j].replace(b\" \", b\"\")\n            ll[i] = content + b\" \" + ll[i][j + 1 :]\n    cm = (\n        (b\" \".join(ll))\n        .replace(b\"[\", b\" [ \")\n        .replace(b\"]\", b\" ]\\n \")\n        .replace(b\"\\r\", b\"\\n\")\n    )\n    return cm\n\n\ndef process_cm_line(\n    line: bytes,\n    process_rg: bool,\n    process_char: bool,\n    multiline_rg: Union[None, tuple[int, int]],\n    map_dict: dict[Any, Any],\n    int_entry: list[int],\n) -> tuple[bool, bool, Union[None, tuple[int, int]]]:\n    if line == b\"\" or line[0] == 37:  # 37 = %\n        return process_rg, process_char, multiline_rg\n    line = line.replace(b\"\\t\", b\" \")\n    if b\"beginbfrange\" in line:\n        process_rg = True\n    elif b\"endbfrange\" in line:\n        process_rg = False\n    elif b\"beginbfchar\" in line:\n        process_char = True\n    elif b\"endbfchar\" in line:\n        process_char = False\n    elif process_rg:\n        try:\n            multiline_rg = parse_bfrange(line, map_dict, int_entry, multiline_rg)\n        except binascii.Error as error:\n            logger_warning(f\"Skipping broken line {line!r}: {error}\", __name__)\n    elif process_char:\n        parse_bfchar(line, map_dict, int_entry)\n    return process_rg, process_char, multiline_rg\n\n\n# Usual values should be up to 65_536.\nMAPPING_DICTIONARY_SIZE_LIMIT = 100_000\n\n\ndef _check_mapping_size(size: int) -> None:\n    if size > MAPPING_DICTIONARY_SIZE_LIMIT:\n        raise LimitReachedError(f\"Maximum /ToUnicode size limit reached: {size} > {MAPPING_DICTIONARY_SIZE_LIMIT}.\")\n\n\ndef parse_bfrange(\n    line: bytes,\n    map_dict: dict[Any, Any],\n    int_entry: list[int],\n    multiline_rg: Union[None, tuple[int, int]],\n) -> Union[None, tuple[int, int]]:\n    lst = [x for x in line.split(b\" \") if x]\n    closure_found = False\n    entry_count = len(int_entry)\n    _check_mapping_size(entry_count)\n    if multiline_rg is not None:\n        fmt = b\"%%0%dX\" % (map_dict[-1] * 2)\n        a = multiline_rg[0]  # a, b not in the current line\n        b = multiline_rg[1]\n        for sq in lst:\n            if sq == b\"]\":\n                closure_found = True\n                break\n            entry_count += 1\n            _check_mapping_size(entry_count)\n            map_dict[\n                unhexlify(fmt % a).decode(\n                    \"charmap\" if map_dict[-1] == 1 else \"utf-16-be\",\n                    \"surrogatepass\",\n                )\n            ] = unhexlify(sq).decode(\"utf-16-be\", \"surrogatepass\")\n            int_entry.append(a)\n            a += 1\n    else:\n        a = int(lst[0], 16)\n        b = int(lst[1], 16)\n        nbi = max(len(lst[0]), len(lst[1]))\n        map_dict[-1] = ceil(nbi / 2)\n        fmt = b\"%%0%dX\" % (map_dict[-1] * 2)\n        if lst[2] == b\"[\":\n            for sq in lst[3:]:\n                if sq == b\"]\":\n                    closure_found = True\n                    break\n                entry_count += 1\n                _check_mapping_size(entry_count)\n                map_dict[\n                    unhexlify(fmt % a).decode(\n                        \"charmap\" if map_dict[-1] == 1 else \"utf-16-be\",\n                        \"surrogatepass\",\n                    )\n                ] = unhexlify(sq).decode(\"utf-16-be\", \"surrogatepass\")\n                int_entry.append(a)\n                a += 1\n        else:  # case without list\n            c = int(lst[2], 16)\n            fmt2 = b\"%%0%dX\" % max(4, len(lst[2]))\n            closure_found = True\n            range_size = max(0, b - a + 1)\n            _check_mapping_size(entry_count + range_size)  # This can be checked beforehand.\n            while a <= b:\n                map_dict[\n                    unhexlify(fmt % a).decode(\n                        \"charmap\" if map_dict[-1] == 1 else \"utf-16-be\",\n                        \"surrogatepass\",\n                    )\n                ] = unhexlify(fmt2 % c).decode(\"utf-16-be\", \"surrogatepass\")\n                int_entry.append(a)\n                a += 1\n                c += 1\n    return None if closure_found else (a, b)\n\n\ndef parse_bfchar(line: bytes, map_dict: dict[Any, Any], int_entry: list[int]) -> None:\n    lst = [x for x in line.split(b\" \") if x]\n    new_count = len(lst) // 2\n    _check_mapping_size(len(int_entry) + new_count)  # This can be checked beforehand.\n    map_dict[-1] = len(lst[0]) // 2\n    while len(lst) > 1:\n        map_to = \"\"\n        # placeholder (see above) means empty string\n        if lst[1] != b\".\":\n            try:\n                map_to = unhexlify(lst[1]).decode(\n                    \"charmap\" if len(lst[1]) < 4 else \"utf-16-be\", \"surrogatepass\"\n                )  # join is here as some cases where the code was split\n            except BinasciiError as exception:\n                logger_warning(f\"Got invalid hex string: {exception!s} ({lst[1]!r})\", __name__)\n        map_dict[\n            unhexlify(lst[0]).decode(\n                \"charmap\" if map_dict[-1] == 1 else \"utf-16-be\", \"surrogatepass\"\n            )\n        ] = map_to\n        int_entry.append(int(lst[0], 16))\n        lst = lst[2:]\n\n\ndef _type1_alternative(\n    ft: DictionaryObject,\n    map_dict: dict[Any, Any],\n    int_entry: list[int],\n) -> tuple[dict[Any, Any], list[int]]:\n    if \"/FontDescriptor\" not in ft:\n        return map_dict, int_entry\n    ft_desc = cast(DictionaryObject, ft[\"/FontDescriptor\"]).get(\"/FontFile\")\n    if is_null_or_none(ft_desc):\n        return map_dict, int_entry\n    assert ft_desc is not None, \"mypy\"\n    txt = ft_desc.get_object().get_data()\n    txt = txt.split(b\"eexec\\n\")[0]  # only clear part\n    txt = txt.split(b\"/Encoding\")[1]  # to get the encoding part\n    lines = txt.replace(b\"\\r\", b\"\\n\").split(b\"\\n\")\n    for li in lines:\n        if li.startswith(b\"dup\"):\n            words = [_w for _w in li.split(b\" \") if _w != b\"\"]\n            if len(words) > 3 and words[3] != b\"put\":\n                continue\n            try:\n                i = int(words[1])\n            except ValueError:  # pragma: no cover\n                continue\n            try:\n                v = adobe_glyphs[words[2].decode()]\n            except KeyError:\n                if words[2].startswith(b\"/uni\"):\n                    try:\n                        v = chr(int(words[2][4:], 16))\n                    except ValueError:  # pragma: no cover\n                        continue\n                else:\n                    continue\n            map_dict[chr(i)] = v\n            int_entry.append(i)\n    return map_dict, int_entry\n"
  },
  {
    "path": "pypdf/_codecs/__init__.py",
    "content": "from .adobe_glyphs import adobe_glyphs\nfrom .pdfdoc import _pdfdoc_encoding\nfrom .std import _std_encoding\nfrom .symbol import _symbol_encoding\nfrom .zapfding import _zapfding_encoding\n\n\ndef fill_from_encoding(enc: str) -> list[str]:\n    lst: list[str] = []\n    for x in range(256):\n        try:\n            lst += (bytes((x,)).decode(enc),)\n        except Exception:\n            lst += (chr(x),)\n    return lst\n\n\ndef rev_encoding(enc: list[str]) -> dict[str, int]:\n    rev: dict[str, int] = {}\n    for i in range(256):\n        char = enc[i]\n        if char == \"\\u0000\":\n            continue\n        assert char not in rev, f\"{char} at {i} already at {rev[char]}\"\n        rev[char] = i\n    return rev\n\n\n_win_encoding = fill_from_encoding(\"cp1252\")\n_mac_encoding = fill_from_encoding(\"mac_roman\")\n\n\n_pdfdoc_encoding_rev: dict[str, int] = rev_encoding(_pdfdoc_encoding)\n\n\ncharset_encoding: dict[str, list[str]] = {\n    \"/StandardEncoding\": _std_encoding,\n    \"/WinAnsiEncoding\": _win_encoding,\n    \"/MacRomanEncoding\": _mac_encoding,\n    \"/PDFDocEncoding\": _pdfdoc_encoding,\n    \"/Symbol\": _symbol_encoding,\n    \"/ZapfDingbats\": _zapfding_encoding,\n}\n\n__all__ = [\n    \"_mac_encoding\",\n    \"_pdfdoc_encoding\",\n    \"_pdfdoc_encoding_rev\",\n    \"_std_encoding\",\n    \"_symbol_encoding\",\n    \"_win_encoding\",\n    \"_zapfding_encoding\",\n    \"adobe_glyphs\",\n    \"charset_encoding\",\n]\n"
  },
  {
    "path": "pypdf/_codecs/_codecs.py",
    "content": "\"\"\"\nThis module is for codecs only.\n\nWhile the codec implementation can contain details of the PDF specification,\nthe module should not do any PDF parsing.\n\"\"\"\n\nimport io\nfrom abc import ABC, abstractmethod\n\nfrom pypdf._utils import logger_warning\nfrom pypdf.errors import LimitReachedError\n\n\nclass Codec(ABC):\n    \"\"\"Abstract base class for all codecs.\"\"\"\n\n    @abstractmethod\n    def encode(self, data: bytes) -> bytes:\n        \"\"\"\n        Encode the input data.\n\n        Args:\n            data: Data to encode.\n\n        Returns:\n            Encoded data.\n\n        \"\"\"\n\n    @abstractmethod\n    def decode(self, data: bytes) -> bytes:\n        \"\"\"\n        Decode the input data.\n\n        Args:\n            data: Data to decode.\n\n        Returns:\n            Decoded data.\n\n        \"\"\"\n\n\nclass LzwCodec(Codec):\n    \"\"\"Lempel-Ziv-Welch (LZW) adaptive compression codec.\"\"\"\n\n    CLEAR_TABLE_MARKER = 256  # Special code to indicate table reset\n    EOD_MARKER = 257  # End-of-data marker\n    INITIAL_BITS_PER_CODE = 9  # Initial code bit width\n    MAX_BITS_PER_CODE = 12  # Maximum code bit width\n\n    def __init__(self, max_output_length: int = 75_000_000) -> None:\n        self.max_output_length = max_output_length\n\n    def _initialize_encoding_table(self) -> None:\n        \"\"\"Initialize the encoding table and state to initial conditions.\"\"\"\n        self.encoding_table: dict[bytes, int] = {bytes([i]): i for i in range(256)}\n        self.next_code = self.EOD_MARKER + 1\n        self.bits_per_code = self.INITIAL_BITS_PER_CODE\n        self.max_code_value = (1 << self.bits_per_code) - 1\n\n    def _increase_next_code(self) -> None:\n        \"\"\"Update bits_per_code and max_code_value if necessary.\"\"\"\n        self.next_code += 1\n        if (\n            self.next_code > self.max_code_value\n            and self.bits_per_code < self.MAX_BITS_PER_CODE\n        ):\n            self.bits_per_code += 1\n            self.max_code_value = (1 << self.bits_per_code) - 1\n\n    def encode(self, data: bytes) -> bytes:\n        \"\"\"\n        Encode data using the LZW compression algorithm.\n\n        Taken from PDF 1.7 specs, \"7.4.4.2 Details of LZW Encoding\".\n        \"\"\"\n        result_codes: list[int] = []\n\n        # The encoder shall begin by issuing a clear-table code\n        result_codes.append(self.CLEAR_TABLE_MARKER)\n        self._initialize_encoding_table()\n\n        current_sequence = b\"\"\n        for byte in data:\n            next_sequence = current_sequence + bytes([byte])\n\n            if next_sequence in self.encoding_table:\n                # Extend current sequence if already in the table\n                current_sequence = next_sequence\n            else:\n                # Output code for the current sequence\n                result_codes.append(self.encoding_table[current_sequence])\n\n                # Add the new sequence to the table if there's room\n                if self.next_code <= (1 << self.MAX_BITS_PER_CODE) - 1:\n                    self.encoding_table[next_sequence] = self.next_code\n                    self._increase_next_code()\n                else:\n                    # If the table is full, emit a clear-table command\n                    result_codes.append(self.CLEAR_TABLE_MARKER)\n                    self._initialize_encoding_table()\n\n                # Start new sequence\n                current_sequence = bytes([byte])\n\n        # Ensure everything actually is encoded\n        if current_sequence:\n            result_codes.append(self.encoding_table[current_sequence])\n        result_codes.append(self.EOD_MARKER)\n\n        return self._pack_codes_into_bytes(result_codes)\n\n    def _pack_codes_into_bytes(self, codes: list[int]) -> bytes:\n        \"\"\"\n        Convert the list of result codes into a continuous byte stream, with codes packed as per the code bit-width.\n        The bit-width starts at 9 bits and expands as needed.\n        \"\"\"\n        self._initialize_encoding_table()\n        buffer = 0\n        bits_in_buffer = 0\n        output = bytearray()\n\n        for code in codes:\n            buffer = (buffer << self.bits_per_code) | code\n            bits_in_buffer += self.bits_per_code\n\n            # Codes shall be packed into a continuous bit stream, high-order bit\n            # first. This stream shall then be divided into bytes, high-order bit\n            # first.\n            while bits_in_buffer >= 8:\n                bits_in_buffer -= 8\n                output.append((buffer >> bits_in_buffer) & 0xFF)\n\n            if code == self.CLEAR_TABLE_MARKER:\n                self._initialize_encoding_table()\n            elif code == self.EOD_MARKER:\n                continue\n            else:\n                self._increase_next_code()\n\n        # Flush any remaining bits in the buffer\n        if bits_in_buffer > 0:\n            output.append((buffer << (8 - bits_in_buffer)) & 0xFF)\n\n        return bytes(output)\n\n    def _initialize_decoding_table(self) -> None:\n        self.max_code_value = (1 << self.MAX_BITS_PER_CODE) - 1\n        self.decoding_table = [bytes([i]) for i in range(self.CLEAR_TABLE_MARKER)] + [\n            b\"\"\n        ] * (self.max_code_value - self.CLEAR_TABLE_MARKER + 1)\n        self._table_index = self.EOD_MARKER + 1\n        self._bits_to_get = 9\n\n    def _next_code_decode(self, data: bytes) -> int:\n        self._next_data: int\n        try:\n            while self._next_bits < self._bits_to_get:\n                self._next_data = (self._next_data << 8) | (\n                    data[self._byte_pointer]\n                )\n                self._byte_pointer += 1\n                self._next_bits += 8\n\n            code = (\n                self._next_data >> (self._next_bits - self._bits_to_get)\n            ) & self._and_table[self._bits_to_get - 9]\n            self._next_bits -= self._bits_to_get\n\n            # Reduce data to get rid of the overhead,\n            # which increases performance on large streams significantly.\n            self._next_data = self._next_data & 0xFFFFF\n\n            return code\n        except IndexError:\n            return self.EOD_MARKER\n\n    # The following method has been converted to Python from PDFsharp:\n    # https://github.com/empira/PDFsharp/blob/5fbf6ed14740bc4e16786816882d32e43af3ff5d/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Filters/LzwDecode.cs\n    #\n    # Original license:\n    #\n    # -------------------------------------------------------------------------\n    # Copyright (c) 2001-2024 empira Software GmbH, Troisdorf (Cologne Area),\n    # Germany\n    #\n    # http://docs.pdfsharp.net\n    #\n    # MIT License\n    #\n    # Permission is hereby granted, free of charge, to any person obtaining a\n    # copy of this software and associated documentation files (the \"Software\"),\n    # to deal in the Software without restriction, including without limitation\n    # the rights to use, copy, modify, merge, publish, distribute, sublicense,\n    # and/or sell copies of the Software, and to permit persons to whom the\n    # Software is furnished to do so, subject to the following conditions:\n    #\n    # The above copyright notice and this permission notice shall be included\n    # in all copies or substantial portions of the Software.\n    #\n    # THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n    # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n    # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL\n    # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n    # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING\n    # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER\n    # DEALINGS IN THE SOFTWARE.\n    # --------------------------------------------------------------------------\n    def decode(self, data: bytes) -> bytes:\n        \"\"\"\n        The following code was converted to Python from the following code:\n        https://github.com/empira/PDFsharp/blob/master/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Filters/LzwDecode.cs\n        \"\"\"\n        self._and_table = [511, 1023, 2047, 4095]\n        self._table_index = 0\n        self._bits_to_get = 9\n        self._byte_pointer = 0\n        self._next_data = 0\n        self._next_bits = 0\n\n        output_stream = io.BytesIO()\n        output_length = 0\n\n        self._initialize_decoding_table()\n        self._byte_pointer = 0\n        self._next_data = 0\n        self._next_bits = 0\n        old_code = self.CLEAR_TABLE_MARKER\n\n        while True:\n            code = self._next_code_decode(data)\n            if code == self.EOD_MARKER:\n                break\n\n            if code == self.CLEAR_TABLE_MARKER:\n                self._initialize_decoding_table()\n                code = self._next_code_decode(data)\n                if code == self.EOD_MARKER:\n                    break\n                output_stream.write(decoded := self.decoding_table[code])\n                old_code = code\n            elif code < self._table_index:\n                decoded = self.decoding_table[code]\n                output_stream.write(decoded)\n                if old_code != self.CLEAR_TABLE_MARKER:\n                    self._add_entry_decode(self.decoding_table[old_code], decoded[0])\n                old_code = code\n            else:\n                # The code is not in the table and not one of the special codes\n                decoded = (\n                    self.decoding_table[old_code] + self.decoding_table[old_code][:1]\n                )\n                output_stream.write(decoded)\n                self._add_entry_decode(self.decoding_table[old_code], decoded[0])\n                old_code = code\n\n            output_length += len(decoded)\n            if output_length > self.max_output_length:\n                raise LimitReachedError(\n                    f\"Limit reached while decompressing: {output_length} > {self.max_output_length}\"\n                )\n\n        return output_stream.getvalue()\n\n    def _add_entry_decode(self, old_string: bytes, new_char: int) -> None:\n        new_string = old_string + bytes([new_char])\n        if self._table_index > self.max_code_value:\n            logger_warning(\"Ignoring too large LZW table index.\", __name__)\n            return\n        self.decoding_table[self._table_index] = new_string\n        self._table_index += 1\n\n        # Update the number of bits to get based on the table index\n        if self._table_index == 511:\n            self._bits_to_get = 10\n        elif self._table_index == 1023:\n            self._bits_to_get = 11\n        elif self._table_index == 2047:\n            self._bits_to_get = 12\n"
  },
  {
    "path": "pypdf/_codecs/adobe_glyphs.py",
    "content": "# https://raw.githubusercontent.com/adobe-type-tools/agl-aglfn/master/glyphlist.txt\n\n# converted manually to python\n# Extended with data from GlyphNameFormatter:\n#    https://github.com/LettError/glyphNameFormatter\n\n# -----------------------------------------------------------\n# Copyright 2002-2019 Adobe (http://www.adobe.com/).\n#\n# Redistribution and use in source and binary forms, with or\n# without modification, are permitted provided that the\n# following conditions are met:\n#\n# Redistributions of source code must retain the above\n# copyright notice, this list of conditions and the following\n# disclaimer.\n#\n# Redistributions in binary form must reproduce the above\n# copyright notice, this list of conditions and the following\n# disclaimer in the documentation and/or other materials\n# provided with the distribution.\n#\n# Neither the name of Adobe nor the names of its contributors\n# may be used to endorse or promote products derived from this\n# software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND\n# CONTRIBUTORS \"AS IS\" AND ANY EXPRESS OR IMPLIED WARRANTIES,\n# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF\n# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR\n# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,\n# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT\n# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)\n# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR\n# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n# -----------------------------------------------------------\n# Name:          Adobe Glyph List\n# Table version: 2.0\n# Date:          September 20, 2002\n# URL:           https://github.com/adobe-type-tools/agl-aglfn\n#\n# Format: two semicolon-delimited fields:\n#   (1) glyph name--upper/lowercase letters and digits\n#   (2) Unicode scalar value--four uppercase hexadecimal digits\n#\nadobe_glyphs = {\n    \"/A\": \"\\u0041\",\n    \"/AA\": \"\\uA732\",\n    \"/AE\": \"\\u00C6\",\n    \"/AEacute\": \"\\u01FC\",\n    \"/AEmacron\": \"\\u01E2\",\n    \"/AEsmall\": \"\\uF7E6\",\n    \"/AO\": \"\\uA734\",\n    \"/AU\": \"\\uA736\",\n    \"/AV\": \"\\uA738\",\n    \"/AVhorizontalbar\": \"\\uA73A\",\n    \"/AY\": \"\\uA73C\",\n    \"/Aacute\": \"\\u00C1\",\n    \"/Aacutesmall\": \"\\uF7E1\",\n    \"/Abreve\": \"\\u0102\",\n    \"/Abreveacute\": \"\\u1EAE\",\n    \"/Abrevecyr\": \"\\u04D0\",\n    \"/Abrevecyrillic\": \"\\u04D0\",\n    \"/Abrevedotbelow\": \"\\u1EB6\",\n    \"/Abrevegrave\": \"\\u1EB0\",\n    \"/Abrevehoi\": \"\\u1EB2\",\n    \"/Abrevehookabove\": \"\\u1EB2\",\n    \"/Abrevetilde\": \"\\u1EB4\",\n    \"/Acaron\": \"\\u01CD\",\n    \"/Acircle\": \"\\u24B6\",\n    \"/Acircleblack\": \"\\u1F150\",\n    \"/Acircumflex\": \"\\u00C2\",\n    \"/Acircumflexacute\": \"\\u1EA4\",\n    \"/Acircumflexdotbelow\": \"\\u1EAC\",\n    \"/Acircumflexgrave\": \"\\u1EA6\",\n    \"/Acircumflexhoi\": \"\\u1EA8\",\n    \"/Acircumflexhookabove\": \"\\u1EA8\",\n    \"/Acircumflexsmall\": \"\\uF7E2\",\n    \"/Acircumflextilde\": \"\\u1EAA\",\n    \"/Acute\": \"\\uF6C9\",\n    \"/Acutesmall\": \"\\uF7B4\",\n    \"/Acyr\": \"\\u0410\",\n    \"/Acyrillic\": \"\\u0410\",\n    \"/Adblgrave\": \"\\u0200\",\n    \"/Adieresis\": \"\\u00C4\",\n    \"/Adieresiscyr\": \"\\u04D2\",\n    \"/Adieresiscyrillic\": \"\\u04D2\",\n    \"/Adieresismacron\": \"\\u01DE\",\n    \"/Adieresissmall\": \"\\uF7E4\",\n    \"/Adot\": \"\\u0226\",\n    \"/Adotbelow\": \"\\u1EA0\",\n    \"/Adotmacron\": \"\\u01E0\",\n    \"/Agrave\": \"\\u00C0\",\n    \"/Agravedbl\": \"\\u0200\",\n    \"/Agravesmall\": \"\\uF7E0\",\n    \"/Ahoi\": \"\\u1EA2\",\n    \"/Ahookabove\": \"\\u1EA2\",\n    \"/Aiecyr\": \"\\u04D4\",\n    \"/Aiecyrillic\": \"\\u04D4\",\n    \"/Ainvertedbreve\": \"\\u0202\",\n    \"/Akbar\": \"\\uFDF3\",\n    \"/Alayhe\": \"\\uFDF7\",\n    \"/Allah\": \"\\uFDF2\",\n    \"/Alpha\": \"\\u0391\",\n    \"/Alphaacute\": \"\\u1FBB\",\n    \"/Alphaasper\": \"\\u1F09\",\n    \"/Alphaasperacute\": \"\\u1F0D\",\n    \"/Alphaasperacuteiotasub\": \"\\u1F8D\",\n    \"/Alphaaspergrave\": \"\\u1F0B\",\n    \"/Alphaaspergraveiotasub\": \"\\u1F8B\",\n    \"/Alphaasperiotasub\": \"\\u1F89\",\n    \"/Alphaaspertilde\": \"\\u1F0F\",\n    \"/Alphaaspertildeiotasub\": \"\\u1F8F\",\n    \"/Alphabreve\": \"\\u1FB8\",\n    \"/Alphagrave\": \"\\u1FBA\",\n    \"/Alphaiotasub\": \"\\u1FBC\",\n    \"/Alphalenis\": \"\\u1F08\",\n    \"/Alphalenisacute\": \"\\u1F0C\",\n    \"/Alphalenisacuteiotasub\": \"\\u1F8C\",\n    \"/Alphalenisgrave\": \"\\u1F0A\",\n    \"/Alphalenisgraveiotasub\": \"\\u1F8A\",\n    \"/Alphalenisiotasub\": \"\\u1F88\",\n    \"/Alphalenistilde\": \"\\u1F0E\",\n    \"/Alphalenistildeiotasub\": \"\\u1F8E\",\n    \"/Alphatonos\": \"\\u0386\",\n    \"/Alphawithmacron\": \"\\u1FB9\",\n    \"/Amacron\": \"\\u0100\",\n    \"/Amonospace\": \"\\uFF21\",\n    \"/Aogonek\": \"\\u0104\",\n    \"/Aparens\": \"\\u1F110\",\n    \"/Aring\": \"\\u00C5\",\n    \"/Aringacute\": \"\\u01FA\",\n    \"/Aringbelow\": \"\\u1E00\",\n    \"/Aringsmall\": \"\\uF7E5\",\n    \"/Asmall\": \"\\uF761\",\n    \"/Asquare\": \"\\u1F130\",\n    \"/Asquareblack\": \"\\u1F170\",\n    \"/Astroke\": \"\\u023A\",\n    \"/Atilde\": \"\\u00C3\",\n    \"/Atildesmall\": \"\\uF7E3\",\n    \"/Aturned\": \"\\u2C6F\",\n    \"/Ayahend\": \"\\u06DD\",\n    \"/Aybarmenian\": \"\\u0531\",\n    \"/B\": \"\\u0042\",\n    \"/Bcircle\": \"\\u24B7\",\n    \"/Bcircleblack\": \"\\u1F151\",\n    \"/Bdot\": \"\\u1E02\",\n    \"/Bdotaccent\": \"\\u1E02\",\n    \"/Bdotbelow\": \"\\u1E04\",\n    \"/Becyr\": \"\\u0411\",\n    \"/Becyrillic\": \"\\u0411\",\n    \"/Benarmenian\": \"\\u0532\",\n    \"/Beta\": \"\\u0392\",\n    \"/Bflourish\": \"\\uA796\",\n    \"/Bhook\": \"\\u0181\",\n    \"/BismillahArRahmanArRaheem\": \"\\uFDFD\",\n    \"/Blinebelow\": \"\\u1E06\",\n    \"/Bmonospace\": \"\\uFF22\",\n    \"/Bparens\": \"\\u1F111\",\n    \"/Brevesmall\": \"\\uF6F4\",\n    \"/Bscript\": \"\\u212C\",\n    \"/Bsmall\": \"\\uF762\",\n    \"/Bsquare\": \"\\u1F131\",\n    \"/Bsquareblack\": \"\\u1F171\",\n    \"/Bstroke\": \"\\u0243\",\n    \"/Btopbar\": \"\\u0182\",\n    \"/C\": \"\\u0043\",\n    \"/CDcircle\": \"\\u1F12D\",\n    \"/Caarmenian\": \"\\u053E\",\n    \"/Cacute\": \"\\u0106\",\n    \"/Caron\": \"\\uF6CA\",\n    \"/Caronsmall\": \"\\uF6F5\",\n    \"/Cbar\": \"\\uA792\",\n    \"/Ccaron\": \"\\u010C\",\n    \"/Ccedilla\": \"\\u00C7\",\n    \"/Ccedillaacute\": \"\\u1E08\",\n    \"/Ccedillasmall\": \"\\uF7E7\",\n    \"/Ccircle\": \"\\u24B8\",\n    \"/Ccircleblack\": \"\\u1F152\",\n    \"/Ccircumflex\": \"\\u0108\",\n    \"/Cdblstruck\": \"\\u2102\",\n    \"/Cdot\": \"\\u010A\",\n    \"/Cdotaccent\": \"\\u010A\",\n    \"/Cdotreversed\": \"\\uA73E\",\n    \"/Cedillasmall\": \"\\uF7B8\",\n    \"/Cfraktur\": \"\\u212D\",\n    \"/Chaarmenian\": \"\\u0549\",\n    \"/Cheabkhasiancyrillic\": \"\\u04BC\",\n    \"/Cheabkhcyr\": \"\\u04BC\",\n    \"/Cheabkhtailcyr\": \"\\u04BE\",\n    \"/Checyr\": \"\\u0427\",\n    \"/Checyrillic\": \"\\u0427\",\n    \"/Chedescenderabkhasiancyrillic\": \"\\u04BE\",\n    \"/Chedescendercyrillic\": \"\\u04B6\",\n    \"/Chedieresiscyr\": \"\\u04F4\",\n    \"/Chedieresiscyrillic\": \"\\u04F4\",\n    \"/Cheharmenian\": \"\\u0543\",\n    \"/Chekhakascyr\": \"\\u04CB\",\n    \"/Chekhakassiancyrillic\": \"\\u04CB\",\n    \"/Chetailcyr\": \"\\u04B6\",\n    \"/Chevertcyr\": \"\\u04B8\",\n    \"/Cheverticalstrokecyrillic\": \"\\u04B8\",\n    \"/Chi\": \"\\u03A7\",\n    \"/Chook\": \"\\u0187\",\n    \"/Circumflexsmall\": \"\\uF6F6\",\n    \"/Citaliccircle\": \"\\u1F12B\",\n    \"/Cmonospace\": \"\\uFF23\",\n    \"/Coarmenian\": \"\\u0551\",\n    \"/Con\": \"\\uA76E\",\n    \"/Cparens\": \"\\u1F112\",\n    \"/Csmall\": \"\\uF763\",\n    \"/Csquare\": \"\\u1F132\",\n    \"/Csquareblack\": \"\\u1F172\",\n    \"/Cstretched\": \"\\u0297\",\n    \"/Cstroke\": \"\\u023B\",\n    \"/Cuatrillo\": \"\\uA72C\",\n    \"/Cuatrillocomma\": \"\\uA72E\",\n    \"/D\": \"\\u0044\",\n    \"/DZ\": \"\\u01F1\",\n    \"/DZcaron\": \"\\u01C4\",\n    \"/Daarmenian\": \"\\u0534\",\n    \"/Dafrican\": \"\\u0189\",\n    \"/Dcaron\": \"\\u010E\",\n    \"/Dcedilla\": \"\\u1E10\",\n    \"/Dchecyr\": \"\\u052C\",\n    \"/Dcircle\": \"\\u24B9\",\n    \"/Dcircleblack\": \"\\u1F153\",\n    \"/Dcircumflexbelow\": \"\\u1E12\",\n    \"/Dcroat\": \"\\u0110\",\n    \"/Ddblstruckitalic\": \"\\u2145\",\n    \"/Ddot\": \"\\u1E0A\",\n    \"/Ddotaccent\": \"\\u1E0A\",\n    \"/Ddotbelow\": \"\\u1E0C\",\n    \"/Decyr\": \"\\u0414\",\n    \"/Decyrillic\": \"\\u0414\",\n    \"/Deicoptic\": \"\\u03EE\",\n    \"/Dekomicyr\": \"\\u0500\",\n    \"/Delta\": \"\\u2206\",\n    \"/Deltagreek\": \"\\u0394\",\n    \"/Dhook\": \"\\u018A\",\n    \"/Dieresis\": \"\\uF6CB\",\n    \"/DieresisAcute\": \"\\uF6CC\",\n    \"/DieresisGrave\": \"\\uF6CD\",\n    \"/Dieresissmall\": \"\\uF7A8\",\n    \"/Digamma\": \"\\u03DC\",\n    \"/Digammagreek\": \"\\u03DC\",\n    \"/Digammapamphylian\": \"\\u0376\",\n    \"/Dinsular\": \"\\uA779\",\n    \"/Djecyr\": \"\\u0402\",\n    \"/Djecyrillic\": \"\\u0402\",\n    \"/Djekomicyr\": \"\\u0502\",\n    \"/Dlinebelow\": \"\\u1E0E\",\n    \"/Dmonospace\": \"\\uFF24\",\n    \"/Dotaccentsmall\": \"\\uF6F7\",\n    \"/Dparens\": \"\\u1F113\",\n    \"/Dslash\": \"\\u0110\",\n    \"/Dsmall\": \"\\uF764\",\n    \"/Dsquare\": \"\\u1F133\",\n    \"/Dsquareblack\": \"\\u1F173\",\n    \"/Dtopbar\": \"\\u018B\",\n    \"/Dz\": \"\\u01F2\",\n    \"/Dzcaron\": \"\\u01C5\",\n    \"/Dzeabkhasiancyrillic\": \"\\u04E0\",\n    \"/Dzeabkhcyr\": \"\\u04E0\",\n    \"/Dzecyr\": \"\\u0405\",\n    \"/Dzecyrillic\": \"\\u0405\",\n    \"/Dzhecyr\": \"\\u040F\",\n    \"/Dzhecyrillic\": \"\\u040F\",\n    \"/Dzjekomicyr\": \"\\u0506\",\n    \"/Dzzhecyr\": \"\\u052A\",\n    \"/E\": \"\\u0045\",\n    \"/Eacute\": \"\\u00C9\",\n    \"/Eacutesmall\": \"\\uF7E9\",\n    \"/Ebreve\": \"\\u0114\",\n    \"/Ecaron\": \"\\u011A\",\n    \"/Ecedilla\": \"\\u0228\",\n    \"/Ecedillabreve\": \"\\u1E1C\",\n    \"/Echarmenian\": \"\\u0535\",\n    \"/Ecircle\": \"\\u24BA\",\n    \"/Ecircleblack\": \"\\u1F154\",\n    \"/Ecircumflex\": \"\\u00CA\",\n    \"/Ecircumflexacute\": \"\\u1EBE\",\n    \"/Ecircumflexbelow\": \"\\u1E18\",\n    \"/Ecircumflexdotbelow\": \"\\u1EC6\",\n    \"/Ecircumflexgrave\": \"\\u1EC0\",\n    \"/Ecircumflexhoi\": \"\\u1EC2\",\n    \"/Ecircumflexhookabove\": \"\\u1EC2\",\n    \"/Ecircumflexsmall\": \"\\uF7EA\",\n    \"/Ecircumflextilde\": \"\\u1EC4\",\n    \"/Ecyrillic\": \"\\u0404\",\n    \"/Edblgrave\": \"\\u0204\",\n    \"/Edieresis\": \"\\u00CB\",\n    \"/Edieresissmall\": \"\\uF7EB\",\n    \"/Edot\": \"\\u0116\",\n    \"/Edotaccent\": \"\\u0116\",\n    \"/Edotbelow\": \"\\u1EB8\",\n    \"/Efcyr\": \"\\u0424\",\n    \"/Efcyrillic\": \"\\u0424\",\n    \"/Egrave\": \"\\u00C8\",\n    \"/Egravedbl\": \"\\u0204\",\n    \"/Egravesmall\": \"\\uF7E8\",\n    \"/Egyptain\": \"\\uA724\",\n    \"/Egyptalef\": \"\\uA722\",\n    \"/Eharmenian\": \"\\u0537\",\n    \"/Ehoi\": \"\\u1EBA\",\n    \"/Ehookabove\": \"\\u1EBA\",\n    \"/Eightroman\": \"\\u2167\",\n    \"/Einvertedbreve\": \"\\u0206\",\n    \"/Eiotifiedcyr\": \"\\u0464\",\n    \"/Eiotifiedcyrillic\": \"\\u0464\",\n    \"/Elcyr\": \"\\u041B\",\n    \"/Elcyrillic\": \"\\u041B\",\n    \"/Elevenroman\": \"\\u216A\",\n    \"/Elhookcyr\": \"\\u0512\",\n    \"/Elmiddlehookcyr\": \"\\u0520\",\n    \"/Elsharptailcyr\": \"\\u04C5\",\n    \"/Eltailcyr\": \"\\u052E\",\n    \"/Emacron\": \"\\u0112\",\n    \"/Emacronacute\": \"\\u1E16\",\n    \"/Emacrongrave\": \"\\u1E14\",\n    \"/Emcyr\": \"\\u041C\",\n    \"/Emcyrillic\": \"\\u041C\",\n    \"/Emonospace\": \"\\uFF25\",\n    \"/Emsharptailcyr\": \"\\u04CD\",\n    \"/Encyr\": \"\\u041D\",\n    \"/Encyrillic\": \"\\u041D\",\n    \"/Endescendercyrillic\": \"\\u04A2\",\n    \"/Eng\": \"\\u014A\",\n    \"/Engecyr\": \"\\u04A4\",\n    \"/Enghecyrillic\": \"\\u04A4\",\n    \"/Enhookcyr\": \"\\u04C7\",\n    \"/Enhookcyrillic\": \"\\u04C7\",\n    \"/Enhookleftcyr\": \"\\u0528\",\n    \"/Enmiddlehookcyr\": \"\\u0522\",\n    \"/Ensharptailcyr\": \"\\u04C9\",\n    \"/Entailcyr\": \"\\u04A2\",\n    \"/Eogonek\": \"\\u0118\",\n    \"/Eopen\": \"\\u0190\",\n    \"/Eparens\": \"\\u1F114\",\n    \"/Epsilon\": \"\\u0395\",\n    \"/Epsilonacute\": \"\\u1FC9\",\n    \"/Epsilonasper\": \"\\u1F19\",\n    \"/Epsilonasperacute\": \"\\u1F1D\",\n    \"/Epsilonaspergrave\": \"\\u1F1B\",\n    \"/Epsilongrave\": \"\\u1FC8\",\n    \"/Epsilonlenis\": \"\\u1F18\",\n    \"/Epsilonlenisacute\": \"\\u1F1C\",\n    \"/Epsilonlenisgrave\": \"\\u1F1A\",\n    \"/Epsilontonos\": \"\\u0388\",\n    \"/Ercyr\": \"\\u0420\",\n    \"/Ercyrillic\": \"\\u0420\",\n    \"/Ereversed\": \"\\u018E\",\n    \"/Ereversedcyr\": \"\\u042D\",\n    \"/Ereversedcyrillic\": \"\\u042D\",\n    \"/Ereverseddieresiscyr\": \"\\u04EC\",\n    \"/Ereversedopen\": \"\\uA7AB\",\n    \"/Ertickcyr\": \"\\u048E\",\n    \"/Escript\": \"\\u2130\",\n    \"/Escyr\": \"\\u0421\",\n    \"/Escyrillic\": \"\\u0421\",\n    \"/Esdescendercyrillic\": \"\\u04AA\",\n    \"/Esh\": \"\\u01A9\",\n    \"/Esmall\": \"\\uF765\",\n    \"/Esmallturned\": \"\\u2C7B\",\n    \"/Esquare\": \"\\u1F134\",\n    \"/Esquareblack\": \"\\u1F174\",\n    \"/Estailcyr\": \"\\u04AA\",\n    \"/Estroke\": \"\\u0246\",\n    \"/Et\": \"\\uA76A\",\n    \"/Eta\": \"\\u0397\",\n    \"/Etaacute\": \"\\u1FCB\",\n    \"/Etaasper\": \"\\u1F29\",\n    \"/Etaasperacute\": \"\\u1F2D\",\n    \"/Etaasperacuteiotasub\": \"\\u1F9D\",\n    \"/Etaaspergrave\": \"\\u1F2B\",\n    \"/Etaaspergraveiotasub\": \"\\u1F9B\",\n    \"/Etaasperiotasub\": \"\\u1F99\",\n    \"/Etaaspertilde\": \"\\u1F2F\",\n    \"/Etaaspertildeiotasub\": \"\\u1F9F\",\n    \"/Etagrave\": \"\\u1FCA\",\n    \"/Etaiotasub\": \"\\u1FCC\",\n    \"/Etalenis\": \"\\u1F28\",\n    \"/Etalenisacute\": \"\\u1F2C\",\n    \"/Etalenisacuteiotasub\": \"\\u1F9C\",\n    \"/Etalenisgrave\": \"\\u1F2A\",\n    \"/Etalenisgraveiotasub\": \"\\u1F9A\",\n    \"/Etalenisiotasub\": \"\\u1F98\",\n    \"/Etalenistilde\": \"\\u1F2E\",\n    \"/Etalenistildeiotasub\": \"\\u1F9E\",\n    \"/Etarmenian\": \"\\u0538\",\n    \"/Etatonos\": \"\\u0389\",\n    \"/Eth\": \"\\u00D0\",\n    \"/Ethsmall\": \"\\uF7F0\",\n    \"/Etilde\": \"\\u1EBC\",\n    \"/Etildebelow\": \"\\u1E1A\",\n    \"/Eukrcyr\": \"\\u0404\",\n    \"/Euro\": \"\\u20AC\",\n    \"/Ezh\": \"\\u01B7\",\n    \"/Ezhcaron\": \"\\u01EE\",\n    \"/Ezhreversed\": \"\\u01B8\",\n    \"/F\": \"\\u0046\",\n    \"/Fcircle\": \"\\u24BB\",\n    \"/Fcircleblack\": \"\\u1F155\",\n    \"/Fdot\": \"\\u1E1E\",\n    \"/Fdotaccent\": \"\\u1E1E\",\n    \"/Feharmenian\": \"\\u0556\",\n    \"/Feicoptic\": \"\\u03E4\",\n    \"/Fhook\": \"\\u0191\",\n    \"/Finsular\": \"\\uA77B\",\n    \"/Fitacyr\": \"\\u0472\",\n    \"/Fitacyrillic\": \"\\u0472\",\n    \"/Fiveroman\": \"\\u2164\",\n    \"/Fmonospace\": \"\\uFF26\",\n    \"/Fourroman\": \"\\u2163\",\n    \"/Fparens\": \"\\u1F115\",\n    \"/Fscript\": \"\\u2131\",\n    \"/Fsmall\": \"\\uF766\",\n    \"/Fsquare\": \"\\u1F135\",\n    \"/Fsquareblack\": \"\\u1F175\",\n    \"/Fstroke\": \"\\uA798\",\n    \"/Fturned\": \"\\u2132\",\n    \"/G\": \"\\u0047\",\n    \"/GBsquare\": \"\\u3387\",\n    \"/Gacute\": \"\\u01F4\",\n    \"/Gamma\": \"\\u0393\",\n    \"/Gammaafrican\": \"\\u0194\",\n    \"/Gammadblstruck\": \"\\u213E\",\n    \"/Gangiacoptic\": \"\\u03EA\",\n    \"/Gbreve\": \"\\u011E\",\n    \"/Gcaron\": \"\\u01E6\",\n    \"/Gcedilla\": \"\\u0122\",\n    \"/Gcircle\": \"\\u24BC\",\n    \"/Gcircleblack\": \"\\u1F156\",\n    \"/Gcircumflex\": \"\\u011C\",\n    \"/Gcommaaccent\": \"\\u0122\",\n    \"/Gdot\": \"\\u0120\",\n    \"/Gdotaccent\": \"\\u0120\",\n    \"/Gecyr\": \"\\u0413\",\n    \"/Gecyrillic\": \"\\u0413\",\n    \"/Gehookcyr\": \"\\u0494\",\n    \"/Gehookstrokecyr\": \"\\u04FA\",\n    \"/Germandbls\": \"\\u1E9E\",\n    \"/Gestrokecyr\": \"\\u0492\",\n    \"/Getailcyr\": \"\\u04F6\",\n    \"/Geupcyr\": \"\\u0490\",\n    \"/Ghadarmenian\": \"\\u0542\",\n    \"/Ghemiddlehookcyrillic\": \"\\u0494\",\n    \"/Ghestrokecyrillic\": \"\\u0492\",\n    \"/Gheupturncyrillic\": \"\\u0490\",\n    \"/Ghook\": \"\\u0193\",\n    \"/Ghooksmall\": \"\\u029B\",\n    \"/Gimarmenian\": \"\\u0533\",\n    \"/Ginsular\": \"\\uA77D\",\n    \"/Ginsularturned\": \"\\uA77E\",\n    \"/Gjecyr\": \"\\u0403\",\n    \"/Gjecyrillic\": \"\\u0403\",\n    \"/Glottalstop\": \"\\u0241\",\n    \"/Gmacron\": \"\\u1E20\",\n    \"/Gmonospace\": \"\\uFF27\",\n    \"/Gobliquestroke\": \"\\uA7A0\",\n    \"/Gparens\": \"\\u1F116\",\n    \"/Grave\": \"\\uF6CE\",\n    \"/Gravesmall\": \"\\uF760\",\n    \"/Gsmall\": \"\\uF767\",\n    \"/Gsmallhook\": \"\\u029B\",\n    \"/Gsquare\": \"\\u1F136\",\n    \"/Gsquareblack\": \"\\u1F176\",\n    \"/Gstroke\": \"\\u01E4\",\n    \"/Gturnedsans\": \"\\u2141\",\n    \"/H\": \"\\u0048\",\n    \"/H18533\": \"\\u25CF\",\n    \"/H18543\": \"\\u25AA\",\n    \"/H18551\": \"\\u25AB\",\n    \"/H22073\": \"\\u25A1\",\n    \"/HPsquare\": \"\\u33CB\",\n    \"/HVsquare\": \"\\u1F14A\",\n    \"/Haabkhasiancyrillic\": \"\\u04A8\",\n    \"/Haabkhcyr\": \"\\u04A8\",\n    \"/Hacyr\": \"\\u0425\",\n    \"/Hadescendercyrillic\": \"\\u04B2\",\n    \"/Hahookcyr\": \"\\u04FC\",\n    \"/Hardcyr\": \"\\u042A\",\n    \"/Hardsigncyrillic\": \"\\u042A\",\n    \"/Hastrokecyr\": \"\\u04FE\",\n    \"/Hbar\": \"\\u0126\",\n    \"/Hbrevebelow\": \"\\u1E2A\",\n    \"/Hcaron\": \"\\u021E\",\n    \"/Hcedilla\": \"\\u1E28\",\n    \"/Hcircle\": \"\\u24BD\",\n    \"/Hcircleblack\": \"\\u1F157\",\n    \"/Hcircumflex\": \"\\u0124\",\n    \"/Hdblstruck\": \"\\u210D\",\n    \"/Hdescender\": \"\\u2C67\",\n    \"/Hdieresis\": \"\\u1E26\",\n    \"/Hdot\": \"\\u1E22\",\n    \"/Hdotaccent\": \"\\u1E22\",\n    \"/Hdotbelow\": \"\\u1E24\",\n    \"/Heng\": \"\\uA726\",\n    \"/Heta\": \"\\u0370\",\n    \"/Hfraktur\": \"\\u210C\",\n    \"/Hgfullwidth\": \"\\u32CC\",\n    \"/Hhalf\": \"\\u2C75\",\n    \"/Hhook\": \"\\uA7AA\",\n    \"/Hmonospace\": \"\\uFF28\",\n    \"/Hoarmenian\": \"\\u0540\",\n    \"/HonAA\": \"\\u0611\",\n    \"/HonRA\": \"\\u0612\",\n    \"/HonSAW\": \"\\u0610\",\n    \"/Horicoptic\": \"\\u03E8\",\n    \"/Hparens\": \"\\u1F117\",\n    \"/Hscript\": \"\\u210B\",\n    \"/Hsmall\": \"\\uF768\",\n    \"/Hsquare\": \"\\u1F137\",\n    \"/Hsquareblack\": \"\\u1F177\",\n    \"/Hstrokemod\": \"\\uA7F8\",\n    \"/Hturned\": \"\\uA78D\",\n    \"/Hungarumlaut\": \"\\uF6CF\",\n    \"/Hungarumlautsmall\": \"\\uF6F8\",\n    \"/Hwair\": \"\\u01F6\",\n    \"/Hzsquare\": \"\\u3390\",\n    \"/I\": \"\\u0049\",\n    \"/IAcyrillic\": \"\\u042F\",\n    \"/ICsquareblack\": \"\\u1F18B\",\n    \"/IJ\": \"\\u0132\",\n    \"/IUcyrillic\": \"\\u042E\",\n    \"/Iacute\": \"\\u00CD\",\n    \"/Iacutesmall\": \"\\uF7ED\",\n    \"/Ibreve\": \"\\u012C\",\n    \"/Icaron\": \"\\u01CF\",\n    \"/Icircle\": \"\\u24BE\",\n    \"/Icircleblack\": \"\\u1F158\",\n    \"/Icircumflex\": \"\\u00CE\",\n    \"/Icircumflexsmall\": \"\\uF7EE\",\n    \"/Icyr\": \"\\u0418\",\n    \"/Icyrillic\": \"\\u0406\",\n    \"/Idblgrave\": \"\\u0208\",\n    \"/Idieresis\": \"\\u00CF\",\n    \"/Idieresisacute\": \"\\u1E2E\",\n    \"/Idieresiscyr\": \"\\u04E4\",\n    \"/Idieresiscyrillic\": \"\\u04E4\",\n    \"/Idieresissmall\": \"\\uF7EF\",\n    \"/Idot\": \"\\u0130\",\n    \"/Idotaccent\": \"\\u0130\",\n    \"/Idotbelow\": \"\\u1ECA\",\n    \"/Iebrevecyr\": \"\\u04D6\",\n    \"/Iebrevecyrillic\": \"\\u04D6\",\n    \"/Iecyr\": \"\\u0415\",\n    \"/Iecyrillic\": \"\\u0415\",\n    \"/Iegravecyr\": \"\\u0400\",\n    \"/Ifraktur\": \"\\u2111\",\n    \"/Igrave\": \"\\u00CC\",\n    \"/Igravecyr\": \"\\u040D\",\n    \"/Igravedbl\": \"\\u0208\",\n    \"/Igravesmall\": \"\\uF7EC\",\n    \"/Ihoi\": \"\\u1EC8\",\n    \"/Ihookabove\": \"\\u1EC8\",\n    \"/Iicyrillic\": \"\\u0418\",\n    \"/Iinvertedbreve\": \"\\u020A\",\n    \"/Iishortcyrillic\": \"\\u0419\",\n    \"/Imacron\": \"\\u012A\",\n    \"/Imacroncyr\": \"\\u04E2\",\n    \"/Imacroncyrillic\": \"\\u04E2\",\n    \"/Imonospace\": \"\\uFF29\",\n    \"/Iniarmenian\": \"\\u053B\",\n    \"/Iocyr\": \"\\u0401\",\n    \"/Iocyrillic\": \"\\u0401\",\n    \"/Iogonek\": \"\\u012E\",\n    \"/Iota\": \"\\u0399\",\n    \"/Iotaacute\": \"\\u1FDB\",\n    \"/Iotaafrican\": \"\\u0196\",\n    \"/Iotaasper\": \"\\u1F39\",\n    \"/Iotaasperacute\": \"\\u1F3D\",\n    \"/Iotaaspergrave\": \"\\u1F3B\",\n    \"/Iotaaspertilde\": \"\\u1F3F\",\n    \"/Iotabreve\": \"\\u1FD8\",\n    \"/Iotadieresis\": \"\\u03AA\",\n    \"/Iotagrave\": \"\\u1FDA\",\n    \"/Iotalenis\": \"\\u1F38\",\n    \"/Iotalenisacute\": \"\\u1F3C\",\n    \"/Iotalenisgrave\": \"\\u1F3A\",\n    \"/Iotalenistilde\": \"\\u1F3E\",\n    \"/Iotatonos\": \"\\u038A\",\n    \"/Iotawithmacron\": \"\\u1FD9\",\n    \"/Iparens\": \"\\u1F118\",\n    \"/Is\": \"\\uA76C\",\n    \"/Iscript\": \"\\u2110\",\n    \"/Ishortcyr\": \"\\u0419\",\n    \"/Ishortsharptailcyr\": \"\\u048A\",\n    \"/Ismall\": \"\\uF769\",\n    \"/Isquare\": \"\\u1F138\",\n    \"/Isquareblack\": \"\\u1F178\",\n    \"/Istroke\": \"\\u0197\",\n    \"/Itilde\": \"\\u0128\",\n    \"/Itildebelow\": \"\\u1E2C\",\n    \"/Iukrcyr\": \"\\u0406\",\n    \"/Izhitsacyr\": \"\\u0474\",\n    \"/Izhitsacyrillic\": \"\\u0474\",\n    \"/Izhitsadblgravecyrillic\": \"\\u0476\",\n    \"/Izhitsagravedblcyr\": \"\\u0476\",\n    \"/J\": \"\\u004A\",\n    \"/Jaarmenian\": \"\\u0541\",\n    \"/Jallajalalouhou\": \"\\uFDFB\",\n    \"/Jcircle\": \"\\u24BF\",\n    \"/Jcircleblack\": \"\\u1F159\",\n    \"/Jcircumflex\": \"\\u0134\",\n    \"/Jcrossed-tail\": \"\\uA7B2\",\n    \"/Jecyr\": \"\\u0408\",\n    \"/Jecyrillic\": \"\\u0408\",\n    \"/Jheharmenian\": \"\\u054B\",\n    \"/Jmonospace\": \"\\uFF2A\",\n    \"/Jparens\": \"\\u1F119\",\n    \"/Jsmall\": \"\\uF76A\",\n    \"/Jsquare\": \"\\u1F139\",\n    \"/Jsquareblack\": \"\\u1F179\",\n    \"/Jstroke\": \"\\u0248\",\n    \"/K\": \"\\u004B\",\n    \"/KBsquare\": \"\\u3385\",\n    \"/KKsquare\": \"\\u33CD\",\n    \"/KORONIS\": \"\\u1FBD\",\n    \"/Kaaleutcyr\": \"\\u051E\",\n    \"/Kabashkcyr\": \"\\u04A0\",\n    \"/Kabashkircyrillic\": \"\\u04A0\",\n    \"/Kacute\": \"\\u1E30\",\n    \"/Kacyr\": \"\\u041A\",\n    \"/Kacyrillic\": \"\\u041A\",\n    \"/Kadescendercyrillic\": \"\\u049A\",\n    \"/Kahookcyr\": \"\\u04C3\",\n    \"/Kahookcyrillic\": \"\\u04C3\",\n    \"/Kaisymbol\": \"\\u03CF\",\n    \"/Kappa\": \"\\u039A\",\n    \"/Kastrokecyr\": \"\\u049E\",\n    \"/Kastrokecyrillic\": \"\\u049E\",\n    \"/Katailcyr\": \"\\u049A\",\n    \"/Kaverticalstrokecyr\": \"\\u049C\",\n    \"/Kaverticalstrokecyrillic\": \"\\u049C\",\n    \"/Kcaron\": \"\\u01E8\",\n    \"/Kcedilla\": \"\\u0136\",\n    \"/Kcircle\": \"\\u24C0\",\n    \"/Kcircleblack\": \"\\u1F15A\",\n    \"/Kcommaaccent\": \"\\u0136\",\n    \"/Kdescender\": \"\\u2C69\",\n    \"/Kdiagonalstroke\": \"\\uA742\",\n    \"/Kdotbelow\": \"\\u1E32\",\n    \"/Keharmenian\": \"\\u0554\",\n    \"/Kenarmenian\": \"\\u053F\",\n    \"/Khacyrillic\": \"\\u0425\",\n    \"/Kheicoptic\": \"\\u03E6\",\n    \"/Khook\": \"\\u0198\",\n    \"/Kjecyr\": \"\\u040C\",\n    \"/Kjecyrillic\": \"\\u040C\",\n    \"/Klinebelow\": \"\\u1E34\",\n    \"/Kmonospace\": \"\\uFF2B\",\n    \"/Kobliquestroke\": \"\\uA7A2\",\n    \"/Koppa\": \"\\u03DE\",\n    \"/Koppaarchaic\": \"\\u03D8\",\n    \"/Koppacyr\": \"\\u0480\",\n    \"/Koppacyrillic\": \"\\u0480\",\n    \"/Koppagreek\": \"\\u03DE\",\n    \"/Kparens\": \"\\u1F11A\",\n    \"/Ksicyr\": \"\\u046E\",\n    \"/Ksicyrillic\": \"\\u046E\",\n    \"/Ksmall\": \"\\uF76B\",\n    \"/Ksquare\": \"\\u1F13A\",\n    \"/Ksquareblack\": \"\\u1F17A\",\n    \"/Kstroke\": \"\\uA740\",\n    \"/Kstrokediagonalstroke\": \"\\uA744\",\n    \"/Kturned\": \"\\uA7B0\",\n    \"/L\": \"\\u004C\",\n    \"/LJ\": \"\\u01C7\",\n    \"/LL\": \"\\uF6BF\",\n    \"/LLwelsh\": \"\\u1EFA\",\n    \"/LTDfullwidth\": \"\\u32CF\",\n    \"/Lacute\": \"\\u0139\",\n    \"/Lambda\": \"\\u039B\",\n    \"/Lbar\": \"\\u023D\",\n    \"/Lbelt\": \"\\uA7AD\",\n    \"/Lbroken\": \"\\uA746\",\n    \"/Lcaron\": \"\\u013D\",\n    \"/Lcedilla\": \"\\u013B\",\n    \"/Lcircle\": \"\\u24C1\",\n    \"/Lcircleblack\": \"\\u1F15B\",\n    \"/Lcircumflexbelow\": \"\\u1E3C\",\n    \"/Lcommaaccent\": \"\\u013B\",\n    \"/Ldblbar\": \"\\u2C60\",\n    \"/Ldot\": \"\\u013F\",\n    \"/Ldotaccent\": \"\\u013F\",\n    \"/Ldotbelow\": \"\\u1E36\",\n    \"/Ldotbelowmacron\": \"\\u1E38\",\n    \"/Lhacyr\": \"\\u0514\",\n    \"/Liwnarmenian\": \"\\u053C\",\n    \"/Lj\": \"\\u01C8\",\n    \"/Ljecyr\": \"\\u0409\",\n    \"/Ljecyrillic\": \"\\u0409\",\n    \"/Ljekomicyr\": \"\\u0508\",\n    \"/Llinebelow\": \"\\u1E3A\",\n    \"/Lmacrondot\": \"\\u1E38\",\n    \"/Lmiddletilde\": \"\\u2C62\",\n    \"/Lmonospace\": \"\\uFF2C\",\n    \"/Lparens\": \"\\u1F11B\",\n    \"/Lreversedsans\": \"\\u2143\",\n    \"/Lscript\": \"\\u2112\",\n    \"/Lslash\": \"\\u0141\",\n    \"/Lslashsmall\": \"\\uF6F9\",\n    \"/Lsmall\": \"\\uF76C\",\n    \"/Lsquare\": \"\\u1F13B\",\n    \"/Lsquareblack\": \"\\u1F17B\",\n    \"/Lstroke\": \"\\uA748\",\n    \"/Lturned\": \"\\uA780\",\n    \"/Lturnedsans\": \"\\u2142\",\n    \"/M\": \"\\u004D\",\n    \"/MBsquare\": \"\\u3386\",\n    \"/MVsquare\": \"\\u1F14B\",\n    \"/Macron\": \"\\uF6D0\",\n    \"/Macronsmall\": \"\\uF7AF\",\n    \"/Macute\": \"\\u1E3E\",\n    \"/Mcircle\": \"\\u24C2\",\n    \"/Mcircleblack\": \"\\u1F15C\",\n    \"/Mdot\": \"\\u1E40\",\n    \"/Mdotaccent\": \"\\u1E40\",\n    \"/Mdotbelow\": \"\\u1E42\",\n    \"/Menarmenian\": \"\\u0544\",\n    \"/Mhook\": \"\\u2C6E\",\n    \"/Mmonospace\": \"\\uFF2D\",\n    \"/Mohammad\": \"\\uFDF4\",\n    \"/Mparens\": \"\\u1F11C\",\n    \"/Mscript\": \"\\u2133\",\n    \"/Msmall\": \"\\uF76D\",\n    \"/Msquare\": \"\\u1F13C\",\n    \"/Msquareblack\": \"\\u1F17C\",\n    \"/Mturned\": \"\\u019C\",\n    \"/Mturnedsmall\": \"\\uA7FA\",\n    \"/Mu\": \"\\u039C\",\n    \"/N\": \"\\u004E\",\n    \"/NJ\": \"\\u01CA\",\n    \"/Nacute\": \"\\u0143\",\n    \"/Ncaron\": \"\\u0147\",\n    \"/Ncedilla\": \"\\u0145\",\n    \"/Ncircle\": \"\\u24C3\",\n    \"/Ncircleblack\": \"\\u1F15D\",\n    \"/Ncircumflexbelow\": \"\\u1E4A\",\n    \"/Ncommaaccent\": \"\\u0145\",\n    \"/Ndblstruck\": \"\\u2115\",\n    \"/Ndescender\": \"\\uA790\",\n    \"/Ndot\": \"\\u1E44\",\n    \"/Ndotaccent\": \"\\u1E44\",\n    \"/Ndotbelow\": \"\\u1E46\",\n    \"/Ngrave\": \"\\u01F8\",\n    \"/Nhookleft\": \"\\u019D\",\n    \"/Nineroman\": \"\\u2168\",\n    \"/Nj\": \"\\u01CB\",\n    \"/Njecyr\": \"\\u040A\",\n    \"/Njecyrillic\": \"\\u040A\",\n    \"/Njekomicyr\": \"\\u050A\",\n    \"/Nlinebelow\": \"\\u1E48\",\n    \"/Nlongrightleg\": \"\\u0220\",\n    \"/Nmonospace\": \"\\uFF2E\",\n    \"/Nobliquestroke\": \"\\uA7A4\",\n    \"/Nowarmenian\": \"\\u0546\",\n    \"/Nparens\": \"\\u1F11D\",\n    \"/Nsmall\": \"\\uF76E\",\n    \"/Nsquare\": \"\\u1F13D\",\n    \"/Nsquareblack\": \"\\u1F17D\",\n    \"/Ntilde\": \"\\u00D1\",\n    \"/Ntildesmall\": \"\\uF7F1\",\n    \"/Nu\": \"\\u039D\",\n    \"/O\": \"\\u004F\",\n    \"/OE\": \"\\u0152\",\n    \"/OEsmall\": \"\\uF6FA\",\n    \"/OO\": \"\\uA74E\",\n    \"/Oacute\": \"\\u00D3\",\n    \"/Oacutesmall\": \"\\uF7F3\",\n    \"/Obar\": \"\\u019F\",\n    \"/Obarcyr\": \"\\u04E8\",\n    \"/Obardieresiscyr\": \"\\u04EA\",\n    \"/Obarredcyrillic\": \"\\u04E8\",\n    \"/Obarreddieresiscyrillic\": \"\\u04EA\",\n    \"/Obreve\": \"\\u014E\",\n    \"/Ocaron\": \"\\u01D1\",\n    \"/Ocenteredtilde\": \"\\u019F\",\n    \"/Ocircle\": \"\\u24C4\",\n    \"/Ocircleblack\": \"\\u1F15E\",\n    \"/Ocircumflex\": \"\\u00D4\",\n    \"/Ocircumflexacute\": \"\\u1ED0\",\n    \"/Ocircumflexdotbelow\": \"\\u1ED8\",\n    \"/Ocircumflexgrave\": \"\\u1ED2\",\n    \"/Ocircumflexhoi\": \"\\u1ED4\",\n    \"/Ocircumflexhookabove\": \"\\u1ED4\",\n    \"/Ocircumflexsmall\": \"\\uF7F4\",\n    \"/Ocircumflextilde\": \"\\u1ED6\",\n    \"/Ocyr\": \"\\u041E\",\n    \"/Ocyrillic\": \"\\u041E\",\n    \"/Odblacute\": \"\\u0150\",\n    \"/Odblgrave\": \"\\u020C\",\n    \"/Odieresis\": \"\\u00D6\",\n    \"/Odieresiscyr\": \"\\u04E6\",\n    \"/Odieresiscyrillic\": \"\\u04E6\",\n    \"/Odieresismacron\": \"\\u022A\",\n    \"/Odieresissmall\": \"\\uF7F6\",\n    \"/Odot\": \"\\u022E\",\n    \"/Odotbelow\": \"\\u1ECC\",\n    \"/Odotmacron\": \"\\u0230\",\n    \"/Ogoneksmall\": \"\\uF6FB\",\n    \"/Ograve\": \"\\u00D2\",\n    \"/Ogravedbl\": \"\\u020C\",\n    \"/Ogravesmall\": \"\\uF7F2\",\n    \"/Oharmenian\": \"\\u0555\",\n    \"/Ohm\": \"\\u2126\",\n    \"/Ohoi\": \"\\u1ECE\",\n    \"/Ohookabove\": \"\\u1ECE\",\n    \"/Ohorn\": \"\\u01A0\",\n    \"/Ohornacute\": \"\\u1EDA\",\n    \"/Ohorndotbelow\": \"\\u1EE2\",\n    \"/Ohorngrave\": \"\\u1EDC\",\n    \"/Ohornhoi\": \"\\u1EDE\",\n    \"/Ohornhookabove\": \"\\u1EDE\",\n    \"/Ohorntilde\": \"\\u1EE0\",\n    \"/Ohungarumlaut\": \"\\u0150\",\n    \"/Oi\": \"\\u01A2\",\n    \"/Oinvertedbreve\": \"\\u020E\",\n    \"/Oloop\": \"\\uA74C\",\n    \"/Omacron\": \"\\u014C\",\n    \"/Omacronacute\": \"\\u1E52\",\n    \"/Omacrongrave\": \"\\u1E50\",\n    \"/Omega\": \"\\u2126\",\n    \"/Omegaacute\": \"\\u1FFB\",\n    \"/Omegaasper\": \"\\u1F69\",\n    \"/Omegaasperacute\": \"\\u1F6D\",\n    \"/Omegaasperacuteiotasub\": \"\\u1FAD\",\n    \"/Omegaaspergrave\": \"\\u1F6B\",\n    \"/Omegaaspergraveiotasub\": \"\\u1FAB\",\n    \"/Omegaasperiotasub\": \"\\u1FA9\",\n    \"/Omegaaspertilde\": \"\\u1F6F\",\n    \"/Omegaaspertildeiotasub\": \"\\u1FAF\",\n    \"/Omegacyr\": \"\\u0460\",\n    \"/Omegacyrillic\": \"\\u0460\",\n    \"/Omegagrave\": \"\\u1FFA\",\n    \"/Omegagreek\": \"\\u03A9\",\n    \"/Omegaiotasub\": \"\\u1FFC\",\n    \"/Omegalenis\": \"\\u1F68\",\n    \"/Omegalenisacute\": \"\\u1F6C\",\n    \"/Omegalenisacuteiotasub\": \"\\u1FAC\",\n    \"/Omegalenisgrave\": \"\\u1F6A\",\n    \"/Omegalenisgraveiotasub\": \"\\u1FAA\",\n    \"/Omegalenisiotasub\": \"\\u1FA8\",\n    \"/Omegalenistilde\": \"\\u1F6E\",\n    \"/Omegalenistildeiotasub\": \"\\u1FAE\",\n    \"/Omegaroundcyr\": \"\\u047A\",\n    \"/Omegaroundcyrillic\": \"\\u047A\",\n    \"/Omegatitlocyr\": \"\\u047C\",\n    \"/Omegatitlocyrillic\": \"\\u047C\",\n    \"/Omegatonos\": \"\\u038F\",\n    \"/Omicron\": \"\\u039F\",\n    \"/Omicronacute\": \"\\u1FF9\",\n    \"/Omicronasper\": \"\\u1F49\",\n    \"/Omicronasperacute\": \"\\u1F4D\",\n    \"/Omicronaspergrave\": \"\\u1F4B\",\n    \"/Omicrongrave\": \"\\u1FF8\",\n    \"/Omicronlenis\": \"\\u1F48\",\n    \"/Omicronlenisacute\": \"\\u1F4C\",\n    \"/Omicronlenisgrave\": \"\\u1F4A\",\n    \"/Omicrontonos\": \"\\u038C\",\n    \"/Omonospace\": \"\\uFF2F\",\n    \"/Oneroman\": \"\\u2160\",\n    \"/Oogonek\": \"\\u01EA\",\n    \"/Oogonekmacron\": \"\\u01EC\",\n    \"/Oopen\": \"\\u0186\",\n    \"/Oparens\": \"\\u1F11E\",\n    \"/Oslash\": \"\\u00D8\",\n    \"/Oslashacute\": \"\\u01FE\",\n    \"/Oslashsmall\": \"\\uF7F8\",\n    \"/Osmall\": \"\\uF76F\",\n    \"/Osquare\": \"\\u1F13E\",\n    \"/Osquareblack\": \"\\u1F17E\",\n    \"/Ostroke\": \"\\uA74A\",\n    \"/Ostrokeacute\": \"\\u01FE\",\n    \"/Otcyr\": \"\\u047E\",\n    \"/Otcyrillic\": \"\\u047E\",\n    \"/Otilde\": \"\\u00D5\",\n    \"/Otildeacute\": \"\\u1E4C\",\n    \"/Otildedieresis\": \"\\u1E4E\",\n    \"/Otildemacron\": \"\\u022C\",\n    \"/Otildesmall\": \"\\uF7F5\",\n    \"/Ou\": \"\\u0222\",\n    \"/P\": \"\\u0050\",\n    \"/PAsquareblack\": \"\\u1F18C\",\n    \"/PPVsquare\": \"\\u1F14E\",\n    \"/Pacute\": \"\\u1E54\",\n    \"/Palochkacyr\": \"\\u04C0\",\n    \"/Pcircle\": \"\\u24C5\",\n    \"/Pcircleblack\": \"\\u1F15F\",\n    \"/Pcrosssquareblack\": \"\\u1F18A\",\n    \"/Pdblstruck\": \"\\u2119\",\n    \"/Pdot\": \"\\u1E56\",\n    \"/Pdotaccent\": \"\\u1E56\",\n    \"/Pecyr\": \"\\u041F\",\n    \"/Pecyrillic\": \"\\u041F\",\n    \"/Peharmenian\": \"\\u054A\",\n    \"/Pehookcyr\": \"\\u04A6\",\n    \"/Pemiddlehookcyrillic\": \"\\u04A6\",\n    \"/Petailcyr\": \"\\u0524\",\n    \"/Pflourish\": \"\\uA752\",\n    \"/Phi\": \"\\u03A6\",\n    \"/Phook\": \"\\u01A4\",\n    \"/Pi\": \"\\u03A0\",\n    \"/Pidblstruck\": \"\\u213F\",\n    \"/Piwrarmenian\": \"\\u0553\",\n    \"/Pmonospace\": \"\\uFF30\",\n    \"/Pparens\": \"\\u1F11F\",\n    \"/Psi\": \"\\u03A8\",\n    \"/Psicyr\": \"\\u0470\",\n    \"/Psicyrillic\": \"\\u0470\",\n    \"/Psmall\": \"\\uF770\",\n    \"/Psquare\": \"\\u1F13F\",\n    \"/Psquareblack\": \"\\u1F17F\",\n    \"/Pstroke\": \"\\u2C63\",\n    \"/Pstrokedescender\": \"\\uA750\",\n    \"/Ptail\": \"\\uA754\",\n    \"/Q\": \"\\u0051\",\n    \"/Qacyr\": \"\\u051A\",\n    \"/QalaUsedAsKoranicStopSign\": \"\\uFDF1\",\n    \"/Qcircle\": \"\\u24C6\",\n    \"/Qcircleblack\": \"\\u1F160\",\n    \"/Qdblstruck\": \"\\u211A\",\n    \"/Qdiagonalstroke\": \"\\uA758\",\n    \"/Qmonospace\": \"\\uFF31\",\n    \"/Qparens\": \"\\u1F120\",\n    \"/Qrotated\": \"\\u213A\",\n    \"/Qsmall\": \"\\uF771\",\n    \"/Qsmallhooktail\": \"\\u024A\",\n    \"/Qsquare\": \"\\u1F140\",\n    \"/Qsquareblack\": \"\\u1F180\",\n    \"/Qstrokedescender\": \"\\uA756\",\n    \"/R\": \"\\u0052\",\n    \"/Raarmenian\": \"\\u054C\",\n    \"/Racute\": \"\\u0154\",\n    \"/Rasoul\": \"\\uFDF6\",\n    \"/Rcaron\": \"\\u0158\",\n    \"/Rcedilla\": \"\\u0156\",\n    \"/Rcircle\": \"\\u24C7\",\n    \"/Rcircleblack\": \"\\u1F161\",\n    \"/Rcommaaccent\": \"\\u0156\",\n    \"/Rdblgrave\": \"\\u0210\",\n    \"/Rdblstruck\": \"\\u211D\",\n    \"/Rdot\": \"\\u1E58\",\n    \"/Rdotaccent\": \"\\u1E58\",\n    \"/Rdotbelow\": \"\\u1E5A\",\n    \"/Rdotbelowmacron\": \"\\u1E5C\",\n    \"/Reharmenian\": \"\\u0550\",\n    \"/Reverseddottedsigmalunatesymbol\": \"\\u03FF\",\n    \"/Reversedzecyr\": \"\\u0510\",\n    \"/Rfraktur\": \"\\u211C\",\n    \"/Rgravedbl\": \"\\u0210\",\n    \"/Rhacyr\": \"\\u0516\",\n    \"/Rho\": \"\\u03A1\",\n    \"/Rhoasper\": \"\\u1FEC\",\n    \"/Ringsmall\": \"\\uF6FC\",\n    \"/Rinsular\": \"\\uA782\",\n    \"/Rinvertedbreve\": \"\\u0212\",\n    \"/Rinvertedsmall\": \"\\u0281\",\n    \"/Ritaliccircle\": \"\\u1F12C\",\n    \"/Rlinebelow\": \"\\u1E5E\",\n    \"/Rmacrondot\": \"\\u1E5C\",\n    \"/Rmonospace\": \"\\uFF32\",\n    \"/Robliquestroke\": \"\\uA7A6\",\n    \"/Rparens\": \"\\u1F121\",\n    \"/Rrotunda\": \"\\uA75A\",\n    \"/Rscript\": \"\\u211B\",\n    \"/Rsmall\": \"\\uF772\",\n    \"/Rsmallinverted\": \"\\u0281\",\n    \"/Rsmallinvertedsuperior\": \"\\u02B6\",\n    \"/Rsquare\": \"\\u1F141\",\n    \"/Rsquareblack\": \"\\u1F181\",\n    \"/Rstroke\": \"\\u024C\",\n    \"/Rsupinvertedmod\": \"\\u02B6\",\n    \"/Rtail\": \"\\u2C64\",\n    \"/RubElHizbstart\": \"\\u06DE\",\n    \"/Rumrotunda\": \"\\uA75C\",\n    \"/Rumsmall\": \"\\uA776\",\n    \"/S\": \"\\u0053\",\n    \"/SAsquareblack\": \"\\u1F18D\",\n    \"/SDsquare\": \"\\u1F14C\",\n    \"/SF010000\": \"\\u250C\",\n    \"/SF020000\": \"\\u2514\",\n    \"/SF030000\": \"\\u2510\",\n    \"/SF040000\": \"\\u2518\",\n    \"/SF050000\": \"\\u253C\",\n    \"/SF060000\": \"\\u252C\",\n    \"/SF070000\": \"\\u2534\",\n    \"/SF080000\": \"\\u251C\",\n    \"/SF090000\": \"\\u2524\",\n    \"/SF100000\": \"\\u2500\",\n    \"/SF110000\": \"\\u2502\",\n    \"/SF190000\": \"\\u2561\",\n    \"/SF200000\": \"\\u2562\",\n    \"/SF210000\": \"\\u2556\",\n    \"/SF220000\": \"\\u2555\",\n    \"/SF230000\": \"\\u2563\",\n    \"/SF240000\": \"\\u2551\",\n    \"/SF250000\": \"\\u2557\",\n    \"/SF260000\": \"\\u255D\",\n    \"/SF270000\": \"\\u255C\",\n    \"/SF280000\": \"\\u255B\",\n    \"/SF360000\": \"\\u255E\",\n    \"/SF370000\": \"\\u255F\",\n    \"/SF380000\": \"\\u255A\",\n    \"/SF390000\": \"\\u2554\",\n    \"/SF400000\": \"\\u2569\",\n    \"/SF410000\": \"\\u2566\",\n    \"/SF420000\": \"\\u2560\",\n    \"/SF430000\": \"\\u2550\",\n    \"/SF440000\": \"\\u256C\",\n    \"/SF450000\": \"\\u2567\",\n    \"/SF460000\": \"\\u2568\",\n    \"/SF470000\": \"\\u2564\",\n    \"/SF480000\": \"\\u2565\",\n    \"/SF490000\": \"\\u2559\",\n    \"/SF500000\": \"\\u2558\",\n    \"/SF510000\": \"\\u2552\",\n    \"/SF520000\": \"\\u2553\",\n    \"/SF530000\": \"\\u256B\",\n    \"/SF540000\": \"\\u256A\",\n    \"/SSsquare\": \"\\u1F14D\",\n    \"/Sacute\": \"\\u015A\",\n    \"/Sacutedotaccent\": \"\\u1E64\",\n    \"/Safha\": \"\\u0603\",\n    \"/Sajdah\": \"\\u06E9\",\n    \"/Salam\": \"\\uFDF5\",\n    \"/Salla\": \"\\uFDF9\",\n    \"/SallaUsedAsKoranicStopSign\": \"\\uFDF0\",\n    \"/SallallahouAlayheWasallam\": \"\\uFDFA\",\n    \"/Saltillo\": \"\\uA78B\",\n    \"/Sampi\": \"\\u03E0\",\n    \"/Sampiarchaic\": \"\\u0372\",\n    \"/Sampigreek\": \"\\u03E0\",\n    \"/San\": \"\\u03FA\",\n    \"/Sanah\": \"\\u0601\",\n    \"/Scaron\": \"\\u0160\",\n    \"/Scarondot\": \"\\u1E66\",\n    \"/Scarondotaccent\": \"\\u1E66\",\n    \"/Scaronsmall\": \"\\uF6FD\",\n    \"/Scedilla\": \"\\u015E\",\n    \"/Schwa\": \"\\u018F\",\n    \"/Schwacyr\": \"\\u04D8\",\n    \"/Schwacyrillic\": \"\\u04D8\",\n    \"/Schwadieresiscyr\": \"\\u04DA\",\n    \"/Schwadieresiscyrillic\": \"\\u04DA\",\n    \"/Scircle\": \"\\u24C8\",\n    \"/Scircleblack\": \"\\u1F162\",\n    \"/Scircumflex\": \"\\u015C\",\n    \"/Scommaaccent\": \"\\u0218\",\n    \"/Scriptg\": \"\\uA7AC\",\n    \"/Sdot\": \"\\u1E60\",\n    \"/Sdotaccent\": \"\\u1E60\",\n    \"/Sdotbelow\": \"\\u1E62\",\n    \"/Sdotbelowdotabove\": \"\\u1E68\",\n    \"/Sdotbelowdotaccent\": \"\\u1E68\",\n    \"/Seharmenian\": \"\\u054D\",\n    \"/Semisoftcyr\": \"\\u048C\",\n    \"/Sevenroman\": \"\\u2166\",\n    \"/Shaarmenian\": \"\\u0547\",\n    \"/Shacyr\": \"\\u0428\",\n    \"/Shacyrillic\": \"\\u0428\",\n    \"/Shchacyr\": \"\\u0429\",\n    \"/Shchacyrillic\": \"\\u0429\",\n    \"/Sheicoptic\": \"\\u03E2\",\n    \"/SheneGerishin:hb\": \"\\u059E\",\n    \"/Shhacyr\": \"\\u04BA\",\n    \"/Shhacyrillic\": \"\\u04BA\",\n    \"/Shhatailcyr\": \"\\u0526\",\n    \"/Shimacoptic\": \"\\u03EC\",\n    \"/Sho\": \"\\u03F7\",\n    \"/Sigma\": \"\\u03A3\",\n    \"/Sigmalunatesymbol\": \"\\u03F9\",\n    \"/Sigmalunatesymboldotted\": \"\\u03FE\",\n    \"/Sigmareversedlunatesymbol\": \"\\u03FD\",\n    \"/Sinsular\": \"\\uA784\",\n    \"/Sixroman\": \"\\u2165\",\n    \"/Sjekomicyr\": \"\\u050C\",\n    \"/Smonospace\": \"\\uFF33\",\n    \"/Sobliquestroke\": \"\\uA7A8\",\n    \"/Softcyr\": \"\\u042C\",\n    \"/Softsigncyrillic\": \"\\u042C\",\n    \"/Sparens\": \"\\u1F122\",\n    \"/Sshell\": \"\\u1F12A\",\n    \"/Ssmall\": \"\\uF773\",\n    \"/Ssquare\": \"\\u1F142\",\n    \"/Ssquareblack\": \"\\u1F182\",\n    \"/Sswashtail\": \"\\u2C7E\",\n    \"/Stigma\": \"\\u03DA\",\n    \"/Stigmagreek\": \"\\u03DA\",\n    \"/T\": \"\\u0054\",\n    \"/Tau\": \"\\u03A4\",\n    \"/Tbar\": \"\\u0166\",\n    \"/Tcaron\": \"\\u0164\",\n    \"/Tcedilla\": \"\\u0162\",\n    \"/Tcircle\": \"\\u24C9\",\n    \"/Tcircleblack\": \"\\u1F163\",\n    \"/Tcircumflexbelow\": \"\\u1E70\",\n    \"/Tcommaaccent\": \"\\u0162\",\n    \"/Tdot\": \"\\u1E6A\",\n    \"/Tdotaccent\": \"\\u1E6A\",\n    \"/Tdotbelow\": \"\\u1E6C\",\n    \"/Tecyr\": \"\\u0422\",\n    \"/Tecyrillic\": \"\\u0422\",\n    \"/Tedescendercyrillic\": \"\\u04AC\",\n    \"/Tenroman\": \"\\u2169\",\n    \"/Tetailcyr\": \"\\u04AC\",\n    \"/Tetsecyr\": \"\\u04B4\",\n    \"/Tetsecyrillic\": \"\\u04B4\",\n    \"/Theta\": \"\\u0398\",\n    \"/Thetasymbol\": \"\\u03F4\",\n    \"/Thook\": \"\\u01AC\",\n    \"/Thorn\": \"\\u00DE\",\n    \"/Thornsmall\": \"\\uF7FE\",\n    \"/Thornstroke\": \"\\uA764\",\n    \"/Thornstrokedescender\": \"\\uA766\",\n    \"/Threeroman\": \"\\u2162\",\n    \"/Tildesmall\": \"\\uF6FE\",\n    \"/Tinsular\": \"\\uA786\",\n    \"/Tiwnarmenian\": \"\\u054F\",\n    \"/Tjekomicyr\": \"\\u050E\",\n    \"/Tlinebelow\": \"\\u1E6E\",\n    \"/Tmonospace\": \"\\uFF34\",\n    \"/Toarmenian\": \"\\u0539\",\n    \"/Tonefive\": \"\\u01BC\",\n    \"/Tonesix\": \"\\u0184\",\n    \"/Tonetwo\": \"\\u01A7\",\n    \"/Tparens\": \"\\u1F123\",\n    \"/Tresillo\": \"\\uA72A\",\n    \"/Tretroflexhook\": \"\\u01AE\",\n    \"/Tsecyr\": \"\\u0426\",\n    \"/Tsecyrillic\": \"\\u0426\",\n    \"/Tshecyr\": \"\\u040B\",\n    \"/Tshecyrillic\": \"\\u040B\",\n    \"/Tsmall\": \"\\uF774\",\n    \"/Tsquare\": \"\\u1F143\",\n    \"/Tsquareblack\": \"\\u1F183\",\n    \"/Tturned\": \"\\uA7B1\",\n    \"/Twelveroman\": \"\\u216B\",\n    \"/Twithdiagonalstroke\": \"\\u023E\",\n    \"/Tworoman\": \"\\u2161\",\n    \"/Tz\": \"\\uA728\",\n    \"/U\": \"\\u0055\",\n    \"/Uacute\": \"\\u00DA\",\n    \"/Uacutedblcyr\": \"\\u04F2\",\n    \"/Uacutesmall\": \"\\uF7FA\",\n    \"/Ubar\": \"\\u0244\",\n    \"/Ubreve\": \"\\u016C\",\n    \"/Ucaron\": \"\\u01D3\",\n    \"/Ucircle\": \"\\u24CA\",\n    \"/Ucircleblack\": \"\\u1F164\",\n    \"/Ucircumflex\": \"\\u00DB\",\n    \"/Ucircumflexbelow\": \"\\u1E76\",\n    \"/Ucircumflexsmall\": \"\\uF7FB\",\n    \"/Ucyr\": \"\\u0423\",\n    \"/Ucyrillic\": \"\\u0423\",\n    \"/Udblacute\": \"\\u0170\",\n    \"/Udblgrave\": \"\\u0214\",\n    \"/Udieresis\": \"\\u00DC\",\n    \"/Udieresisacute\": \"\\u01D7\",\n    \"/Udieresisbelow\": \"\\u1E72\",\n    \"/Udieresiscaron\": \"\\u01D9\",\n    \"/Udieresiscyr\": \"\\u04F0\",\n    \"/Udieresiscyrillic\": \"\\u04F0\",\n    \"/Udieresisgrave\": \"\\u01DB\",\n    \"/Udieresismacron\": \"\\u01D5\",\n    \"/Udieresissmall\": \"\\uF7FC\",\n    \"/Udotbelow\": \"\\u1EE4\",\n    \"/Ugrave\": \"\\u00D9\",\n    \"/Ugravedbl\": \"\\u0214\",\n    \"/Ugravesmall\": \"\\uF7F9\",\n    \"/Uhoi\": \"\\u1EE6\",\n    \"/Uhookabove\": \"\\u1EE6\",\n    \"/Uhorn\": \"\\u01AF\",\n    \"/Uhornacute\": \"\\u1EE8\",\n    \"/Uhorndotbelow\": \"\\u1EF0\",\n    \"/Uhorngrave\": \"\\u1EEA\",\n    \"/Uhornhoi\": \"\\u1EEC\",\n    \"/Uhornhookabove\": \"\\u1EEC\",\n    \"/Uhorntilde\": \"\\u1EEE\",\n    \"/Uhungarumlaut\": \"\\u0170\",\n    \"/Uhungarumlautcyrillic\": \"\\u04F2\",\n    \"/Uinvertedbreve\": \"\\u0216\",\n    \"/Ukcyr\": \"\\u0478\",\n    \"/Ukcyrillic\": \"\\u0478\",\n    \"/Umacron\": \"\\u016A\",\n    \"/Umacroncyr\": \"\\u04EE\",\n    \"/Umacroncyrillic\": \"\\u04EE\",\n    \"/Umacrondieresis\": \"\\u1E7A\",\n    \"/Umonospace\": \"\\uFF35\",\n    \"/Uogonek\": \"\\u0172\",\n    \"/Uparens\": \"\\u1F124\",\n    \"/Upsilon\": \"\\u03A5\",\n    \"/Upsilon1\": \"\\u03D2\",\n    \"/Upsilonacute\": \"\\u1FEB\",\n    \"/Upsilonacutehooksymbol\": \"\\u03D3\",\n    \"/Upsilonacutehooksymbolgreek\": \"\\u03D3\",\n    \"/Upsilonadieresishooksymbol\": \"\\u03D4\",\n    \"/Upsilonafrican\": \"\\u01B1\",\n    \"/Upsilonasper\": \"\\u1F59\",\n    \"/Upsilonasperacute\": \"\\u1F5D\",\n    \"/Upsilonaspergrave\": \"\\u1F5B\",\n    \"/Upsilonaspertilde\": \"\\u1F5F\",\n    \"/Upsilonbreve\": \"\\u1FE8\",\n    \"/Upsilondieresis\": \"\\u03AB\",\n    \"/Upsilondieresishooksymbolgreek\": \"\\u03D4\",\n    \"/Upsilongrave\": \"\\u1FEA\",\n    \"/Upsilonhooksymbol\": \"\\u03D2\",\n    \"/Upsilontonos\": \"\\u038E\",\n    \"/Upsilonwithmacron\": \"\\u1FE9\",\n    \"/Uring\": \"\\u016E\",\n    \"/Ushortcyr\": \"\\u040E\",\n    \"/Ushortcyrillic\": \"\\u040E\",\n    \"/Usmall\": \"\\uF775\",\n    \"/Usquare\": \"\\u1F144\",\n    \"/Usquareblack\": \"\\u1F184\",\n    \"/Ustraightcyr\": \"\\u04AE\",\n    \"/Ustraightcyrillic\": \"\\u04AE\",\n    \"/Ustraightstrokecyr\": \"\\u04B0\",\n    \"/Ustraightstrokecyrillic\": \"\\u04B0\",\n    \"/Utilde\": \"\\u0168\",\n    \"/Utildeacute\": \"\\u1E78\",\n    \"/Utildebelow\": \"\\u1E74\",\n    \"/V\": \"\\u0056\",\n    \"/Vcircle\": \"\\u24CB\",\n    \"/Vcircleblack\": \"\\u1F165\",\n    \"/Vdiagonalstroke\": \"\\uA75E\",\n    \"/Vdotbelow\": \"\\u1E7E\",\n    \"/Vecyr\": \"\\u0412\",\n    \"/Vecyrillic\": \"\\u0412\",\n    \"/Vend\": \"\\uA768\",\n    \"/Vewarmenian\": \"\\u054E\",\n    \"/Vhook\": \"\\u01B2\",\n    \"/Visigothicz\": \"\\uA762\",\n    \"/Vmod\": \"\\u2C7D\",\n    \"/Vmonospace\": \"\\uFF36\",\n    \"/Voarmenian\": \"\\u0548\",\n    \"/Volapukae\": \"\\uA79A\",\n    \"/Volapukoe\": \"\\uA79C\",\n    \"/Volapukue\": \"\\uA79E\",\n    \"/Vparens\": \"\\u1F125\",\n    \"/Vsmall\": \"\\uF776\",\n    \"/Vsquare\": \"\\u1F145\",\n    \"/Vsquareblack\": \"\\u1F185\",\n    \"/Vtilde\": \"\\u1E7C\",\n    \"/Vturned\": \"\\u0245\",\n    \"/Vwelsh\": \"\\u1EFC\",\n    \"/Vy\": \"\\uA760\",\n    \"/W\": \"\\u0057\",\n    \"/WZcircle\": \"\\u1F12E\",\n    \"/Wacute\": \"\\u1E82\",\n    \"/Wasallam\": \"\\uFDF8\",\n    \"/Wcircle\": \"\\u24CC\",\n    \"/Wcircleblack\": \"\\u1F166\",\n    \"/Wcircumflex\": \"\\u0174\",\n    \"/Wdieresis\": \"\\u1E84\",\n    \"/Wdot\": \"\\u1E86\",\n    \"/Wdotaccent\": \"\\u1E86\",\n    \"/Wdotbelow\": \"\\u1E88\",\n    \"/Wecyr\": \"\\u051C\",\n    \"/Wgrave\": \"\\u1E80\",\n    \"/Whook\": \"\\u2C72\",\n    \"/Wmonospace\": \"\\uFF37\",\n    \"/Wparens\": \"\\u1F126\",\n    \"/Wsmall\": \"\\uF777\",\n    \"/Wsquare\": \"\\u1F146\",\n    \"/Wsquareblack\": \"\\u1F186\",\n    \"/Wynn\": \"\\u01F7\",\n    \"/X\": \"\\u0058\",\n    \"/Xatailcyr\": \"\\u04B2\",\n    \"/Xcircle\": \"\\u24CD\",\n    \"/Xcircleblack\": \"\\u1F167\",\n    \"/Xdieresis\": \"\\u1E8C\",\n    \"/Xdot\": \"\\u1E8A\",\n    \"/Xdotaccent\": \"\\u1E8A\",\n    \"/Xeharmenian\": \"\\u053D\",\n    \"/Xi\": \"\\u039E\",\n    \"/Xmonospace\": \"\\uFF38\",\n    \"/Xparens\": \"\\u1F127\",\n    \"/Xsmall\": \"\\uF778\",\n    \"/Xsquare\": \"\\u1F147\",\n    \"/Xsquareblack\": \"\\u1F187\",\n    \"/Y\": \"\\u0059\",\n    \"/Yacute\": \"\\u00DD\",\n    \"/Yacutesmall\": \"\\uF7FD\",\n    \"/Yacyr\": \"\\u042F\",\n    \"/Yaecyr\": \"\\u0518\",\n    \"/Yatcyr\": \"\\u0462\",\n    \"/Yatcyrillic\": \"\\u0462\",\n    \"/Ycircle\": \"\\u24CE\",\n    \"/Ycircleblack\": \"\\u1F168\",\n    \"/Ycircumflex\": \"\\u0176\",\n    \"/Ydieresis\": \"\\u0178\",\n    \"/Ydieresissmall\": \"\\uF7FF\",\n    \"/Ydot\": \"\\u1E8E\",\n    \"/Ydotaccent\": \"\\u1E8E\",\n    \"/Ydotbelow\": \"\\u1EF4\",\n    \"/Yericyrillic\": \"\\u042B\",\n    \"/Yerudieresiscyrillic\": \"\\u04F8\",\n    \"/Ygrave\": \"\\u1EF2\",\n    \"/Yhoi\": \"\\u1EF6\",\n    \"/Yhook\": \"\\u01B3\",\n    \"/Yhookabove\": \"\\u1EF6\",\n    \"/Yiarmenian\": \"\\u0545\",\n    \"/Yicyrillic\": \"\\u0407\",\n    \"/Yiwnarmenian\": \"\\u0552\",\n    \"/Ylongcyr\": \"\\u042B\",\n    \"/Ylongdieresiscyr\": \"\\u04F8\",\n    \"/Yloop\": \"\\u1EFE\",\n    \"/Ymacron\": \"\\u0232\",\n    \"/Ymonospace\": \"\\uFF39\",\n    \"/Yogh\": \"\\u021C\",\n    \"/Yot\": \"\\u037F\",\n    \"/Yparens\": \"\\u1F128\",\n    \"/Ysmall\": \"\\uF779\",\n    \"/Ysquare\": \"\\u1F148\",\n    \"/Ysquareblack\": \"\\u1F188\",\n    \"/Ystroke\": \"\\u024E\",\n    \"/Ytilde\": \"\\u1EF8\",\n    \"/Yturnedsans\": \"\\u2144\",\n    \"/Yucyr\": \"\\u042E\",\n    \"/Yukrcyr\": \"\\u0407\",\n    \"/Yusbigcyr\": \"\\u046A\",\n    \"/Yusbigcyrillic\": \"\\u046A\",\n    \"/Yusbigiotifiedcyr\": \"\\u046C\",\n    \"/Yusbigiotifiedcyrillic\": \"\\u046C\",\n    \"/Yuslittlecyr\": \"\\u0466\",\n    \"/Yuslittlecyrillic\": \"\\u0466\",\n    \"/Yuslittleiotifiedcyr\": \"\\u0468\",\n    \"/Yuslittleiotifiedcyrillic\": \"\\u0468\",\n    \"/Z\": \"\\u005A\",\n    \"/Zaarmenian\": \"\\u0536\",\n    \"/Zacute\": \"\\u0179\",\n    \"/Zcaron\": \"\\u017D\",\n    \"/Zcaronsmall\": \"\\uF6FF\",\n    \"/Zcircle\": \"\\u24CF\",\n    \"/Zcircleblack\": \"\\u1F169\",\n    \"/Zcircumflex\": \"\\u1E90\",\n    \"/Zdblstruck\": \"\\u2124\",\n    \"/Zdescender\": \"\\u2C6B\",\n    \"/Zdot\": \"\\u017B\",\n    \"/Zdotaccent\": \"\\u017B\",\n    \"/Zdotbelow\": \"\\u1E92\",\n    \"/Zecyr\": \"\\u0417\",\n    \"/Zecyrillic\": \"\\u0417\",\n    \"/Zedescendercyrillic\": \"\\u0498\",\n    \"/Zedieresiscyr\": \"\\u04DE\",\n    \"/Zedieresiscyrillic\": \"\\u04DE\",\n    \"/Zeta\": \"\\u0396\",\n    \"/Zetailcyr\": \"\\u0498\",\n    \"/Zfraktur\": \"\\u2128\",\n    \"/Zhearmenian\": \"\\u053A\",\n    \"/Zhebrevecyr\": \"\\u04C1\",\n    \"/Zhebrevecyrillic\": \"\\u04C1\",\n    \"/Zhecyr\": \"\\u0416\",\n    \"/Zhecyrillic\": \"\\u0416\",\n    \"/Zhedescendercyrillic\": \"\\u0496\",\n    \"/Zhedieresiscyr\": \"\\u04DC\",\n    \"/Zhedieresiscyrillic\": \"\\u04DC\",\n    \"/Zhetailcyr\": \"\\u0496\",\n    \"/Zhook\": \"\\u0224\",\n    \"/Zjekomicyr\": \"\\u0504\",\n    \"/Zlinebelow\": \"\\u1E94\",\n    \"/Zmonospace\": \"\\uFF3A\",\n    \"/Zparens\": \"\\u1F129\",\n    \"/Zsmall\": \"\\uF77A\",\n    \"/Zsquare\": \"\\u1F149\",\n    \"/Zsquareblack\": \"\\u1F189\",\n    \"/Zstroke\": \"\\u01B5\",\n    \"/Zswashtail\": \"\\u2C7F\",\n    \"/a\": \"\\u0061\",\n    \"/a.inferior\": \"\\u2090\",\n    \"/aHonRAA\": \"\\u0613\",\n    \"/aa\": \"\\uA733\",\n    \"/aabengali\": \"\\u0986\",\n    \"/aacute\": \"\\u00E1\",\n    \"/aadeva\": \"\\u0906\",\n    \"/aagujarati\": \"\\u0A86\",\n    \"/aagurmukhi\": \"\\u0A06\",\n    \"/aamatragurmukhi\": \"\\u0A3E\",\n    \"/aarusquare\": \"\\u3303\",\n    \"/aavowelsignbengali\": \"\\u09BE\",\n    \"/aavowelsigndeva\": \"\\u093E\",\n    \"/aavowelsigngujarati\": \"\\u0ABE\",\n    \"/abbreviationmarkarmenian\": \"\\u055F\",\n    \"/abbreviationsigndeva\": \"\\u0970\",\n    \"/abengali\": \"\\u0985\",\n    \"/abopomofo\": \"\\u311A\",\n    \"/abreve\": \"\\u0103\",\n    \"/abreveacute\": \"\\u1EAF\",\n    \"/abrevecyr\": \"\\u04D1\",\n    \"/abrevecyrillic\": \"\\u04D1\",\n    \"/abrevedotbelow\": \"\\u1EB7\",\n    \"/abrevegrave\": \"\\u1EB1\",\n    \"/abrevehoi\": \"\\u1EB3\",\n    \"/abrevehookabove\": \"\\u1EB3\",\n    \"/abrevetilde\": \"\\u1EB5\",\n    \"/absquareblack\": \"\\u1F18E\",\n    \"/acaron\": \"\\u01CE\",\n    \"/accountof\": \"\\u2100\",\n    \"/accurrent\": \"\\u23E6\",\n    \"/acircle\": \"\\u24D0\",\n    \"/acirclekatakana\": \"\\u32D0\",\n    \"/acircumflex\": \"\\u00E2\",\n    \"/acircumflexacute\": \"\\u1EA5\",\n    \"/acircumflexdotbelow\": \"\\u1EAD\",\n    \"/acircumflexgrave\": \"\\u1EA7\",\n    \"/acircumflexhoi\": \"\\u1EA9\",\n    \"/acircumflexhookabove\": \"\\u1EA9\",\n    \"/acircumflextilde\": \"\\u1EAB\",\n    \"/activatearabicformshaping\": \"\\u206D\",\n    \"/activatesymmetricswapping\": \"\\u206B\",\n    \"/acute\": \"\\u00B4\",\n    \"/acutebelowcmb\": \"\\u0317\",\n    \"/acutecmb\": \"\\u0301\",\n    \"/acutecomb\": \"\\u0301\",\n    \"/acutedblmiddlemod\": \"\\u02F6\",\n    \"/acutedeva\": \"\\u0954\",\n    \"/acutelowmod\": \"\\u02CF\",\n    \"/acutemod\": \"\\u02CA\",\n    \"/acutetonecmb\": \"\\u0341\",\n    \"/acyr\": \"\\u0430\",\n    \"/acyrillic\": \"\\u0430\",\n    \"/adblgrave\": \"\\u0201\",\n    \"/addakgurmukhi\": \"\\u0A71\",\n    \"/addressedsubject\": \"\\u2101\",\n    \"/adegadegpada\": \"\\uA9CB\",\n    \"/adegpada\": \"\\uA9CA\",\n    \"/adeva\": \"\\u0905\",\n    \"/adieresis\": \"\\u00E4\",\n    \"/adieresiscyr\": \"\\u04D3\",\n    \"/adieresiscyrillic\": \"\\u04D3\",\n    \"/adieresismacron\": \"\\u01DF\",\n    \"/adishakti\": \"\\u262C\",\n    \"/admissionTickets\": \"\\u1F39F\",\n    \"/adot\": \"\\u0227\",\n    \"/adotbelow\": \"\\u1EA1\",\n    \"/adotmacron\": \"\\u01E1\",\n    \"/ae\": \"\\u00E6\",\n    \"/aeacute\": \"\\u01FD\",\n    \"/aekorean\": \"\\u3150\",\n    \"/aemacron\": \"\\u01E3\",\n    \"/aerialTramway\": \"\\u1F6A1\",\n    \"/afghani\": \"\\u060B\",\n    \"/afii00208\": \"\\u2015\",\n    \"/afii08941\": \"\\u20A4\",\n    \"/afii10017\": \"\\u0410\",\n    \"/afii10018\": \"\\u0411\",\n    \"/afii10019\": \"\\u0412\",\n    \"/afii10020\": \"\\u0413\",\n    \"/afii10021\": \"\\u0414\",\n    \"/afii10022\": \"\\u0415\",\n    \"/afii10023\": \"\\u0401\",\n    \"/afii10024\": \"\\u0416\",\n    \"/afii10025\": \"\\u0417\",\n    \"/afii10026\": \"\\u0418\",\n    \"/afii10027\": \"\\u0419\",\n    \"/afii10028\": \"\\u041A\",\n    \"/afii10029\": \"\\u041B\",\n    \"/afii10030\": \"\\u041C\",\n    \"/afii10031\": \"\\u041D\",\n    \"/afii10032\": \"\\u041E\",\n    \"/afii10033\": \"\\u041F\",\n    \"/afii10034\": \"\\u0420\",\n    \"/afii10035\": \"\\u0421\",\n    \"/afii10036\": \"\\u0422\",\n    \"/afii10037\": \"\\u0423\",\n    \"/afii10038\": \"\\u0424\",\n    \"/afii10039\": \"\\u0425\",\n    \"/afii10040\": \"\\u0426\",\n    \"/afii10041\": \"\\u0427\",\n    \"/afii10042\": \"\\u0428\",\n    \"/afii10043\": \"\\u0429\",\n    \"/afii10044\": \"\\u042A\",\n    \"/afii10045\": \"\\u042B\",\n    \"/afii10046\": \"\\u042C\",\n    \"/afii10047\": \"\\u042D\",\n    \"/afii10048\": \"\\u042E\",\n    \"/afii10049\": \"\\u042F\",\n    \"/afii10050\": \"\\u0490\",\n    \"/afii10051\": \"\\u0402\",\n    \"/afii10052\": \"\\u0403\",\n    \"/afii10053\": \"\\u0404\",\n    \"/afii10054\": \"\\u0405\",\n    \"/afii10055\": \"\\u0406\",\n    \"/afii10056\": \"\\u0407\",\n    \"/afii10057\": \"\\u0408\",\n    \"/afii10058\": \"\\u0409\",\n    \"/afii10059\": \"\\u040A\",\n    \"/afii10060\": \"\\u040B\",\n    \"/afii10061\": \"\\u040C\",\n    \"/afii10062\": \"\\u040E\",\n    \"/afii10063\": \"\\uF6C4\",\n    \"/afii10064\": \"\\uF6C5\",\n    \"/afii10065\": \"\\u0430\",\n    \"/afii10066\": \"\\u0431\",\n    \"/afii10067\": \"\\u0432\",\n    \"/afii10068\": \"\\u0433\",\n    \"/afii10069\": \"\\u0434\",\n    \"/afii10070\": \"\\u0435\",\n    \"/afii10071\": \"\\u0451\",\n    \"/afii10072\": \"\\u0436\",\n    \"/afii10073\": \"\\u0437\",\n    \"/afii10074\": \"\\u0438\",\n    \"/afii10075\": \"\\u0439\",\n    \"/afii10076\": \"\\u043A\",\n    \"/afii10077\": \"\\u043B\",\n    \"/afii10078\": \"\\u043C\",\n    \"/afii10079\": \"\\u043D\",\n    \"/afii10080\": \"\\u043E\",\n    \"/afii10081\": \"\\u043F\",\n    \"/afii10082\": \"\\u0440\",\n    \"/afii10083\": \"\\u0441\",\n    \"/afii10084\": \"\\u0442\",\n    \"/afii10085\": \"\\u0443\",\n    \"/afii10086\": \"\\u0444\",\n    \"/afii10087\": \"\\u0445\",\n    \"/afii10088\": \"\\u0446\",\n    \"/afii10089\": \"\\u0447\",\n    \"/afii10090\": \"\\u0448\",\n    \"/afii10091\": \"\\u0449\",\n    \"/afii10092\": \"\\u044A\",\n    \"/afii10093\": \"\\u044B\",\n    \"/afii10094\": \"\\u044C\",\n    \"/afii10095\": \"\\u044D\",\n    \"/afii10096\": \"\\u044E\",\n    \"/afii10097\": \"\\u044F\",\n    \"/afii10098\": \"\\u0491\",\n    \"/afii10099\": \"\\u0452\",\n    \"/afii10100\": \"\\u0453\",\n    \"/afii10101\": \"\\u0454\",\n    \"/afii10102\": \"\\u0455\",\n    \"/afii10103\": \"\\u0456\",\n    \"/afii10104\": \"\\u0457\",\n    \"/afii10105\": \"\\u0458\",\n    \"/afii10106\": \"\\u0459\",\n    \"/afii10107\": \"\\u045A\",\n    \"/afii10108\": \"\\u045B\",\n    \"/afii10109\": \"\\u045C\",\n    \"/afii10110\": \"\\u045E\",\n    \"/afii10145\": \"\\u040F\",\n    \"/afii10146\": \"\\u0462\",\n    \"/afii10147\": \"\\u0472\",\n    \"/afii10148\": \"\\u0474\",\n    \"/afii10192\": \"\\uF6C6\",\n    \"/afii10193\": \"\\u045F\",\n    \"/afii10194\": \"\\u0463\",\n    \"/afii10195\": \"\\u0473\",\n    \"/afii10196\": \"\\u0475\",\n    \"/afii10831\": \"\\uF6C7\",\n    \"/afii10832\": \"\\uF6C8\",\n    \"/afii10846\": \"\\u04D9\",\n    \"/afii299\": \"\\u200E\",\n    \"/afii300\": \"\\u200F\",\n    \"/afii301\": \"\\u200D\",\n    \"/afii57381\": \"\\u066A\",\n    \"/afii57388\": \"\\u060C\",\n    \"/afii57392\": \"\\u0660\",\n    \"/afii57393\": \"\\u0661\",\n    \"/afii57394\": \"\\u0662\",\n    \"/afii57395\": \"\\u0663\",\n    \"/afii57396\": \"\\u0664\",\n    \"/afii57397\": \"\\u0665\",\n    \"/afii57398\": \"\\u0666\",\n    \"/afii57399\": \"\\u0667\",\n    \"/afii57400\": \"\\u0668\",\n    \"/afii57401\": \"\\u0669\",\n    \"/afii57403\": \"\\u061B\",\n    \"/afii57407\": \"\\u061F\",\n    \"/afii57409\": \"\\u0621\",\n    \"/afii57410\": \"\\u0622\",\n    \"/afii57411\": \"\\u0623\",\n    \"/afii57412\": \"\\u0624\",\n    \"/afii57413\": \"\\u0625\",\n    \"/afii57414\": \"\\u0626\",\n    \"/afii57415\": \"\\u0627\",\n    \"/afii57416\": \"\\u0628\",\n    \"/afii57417\": \"\\u0629\",\n    \"/afii57418\": \"\\u062A\",\n    \"/afii57419\": \"\\u062B\",\n    \"/afii57420\": \"\\u062C\",\n    \"/afii57421\": \"\\u062D\",\n    \"/afii57422\": \"\\u062E\",\n    \"/afii57423\": \"\\u062F\",\n    \"/afii57424\": \"\\u0630\",\n    \"/afii57425\": \"\\u0631\",\n    \"/afii57426\": \"\\u0632\",\n    \"/afii57427\": \"\\u0633\",\n    \"/afii57428\": \"\\u0634\",\n    \"/afii57429\": \"\\u0635\",\n    \"/afii57430\": \"\\u0636\",\n    \"/afii57431\": \"\\u0637\",\n    \"/afii57432\": \"\\u0638\",\n    \"/afii57433\": \"\\u0639\",\n    \"/afii57434\": \"\\u063A\",\n    \"/afii57440\": \"\\u0640\",\n    \"/afii57441\": \"\\u0641\",\n    \"/afii57442\": \"\\u0642\",\n    \"/afii57443\": \"\\u0643\",\n    \"/afii57444\": \"\\u0644\",\n    \"/afii57445\": \"\\u0645\",\n    \"/afii57446\": \"\\u0646\",\n    \"/afii57448\": \"\\u0648\",\n    \"/afii57449\": \"\\u0649\",\n    \"/afii57450\": \"\\u064A\",\n    \"/afii57451\": \"\\u064B\",\n    \"/afii57452\": \"\\u064C\",\n    \"/afii57453\": \"\\u064D\",\n    \"/afii57454\": \"\\u064E\",\n    \"/afii57455\": \"\\u064F\",\n    \"/afii57456\": \"\\u0650\",\n    \"/afii57457\": \"\\u0651\",\n    \"/afii57458\": \"\\u0652\",\n    \"/afii57470\": \"\\u0647\",\n    \"/afii57505\": \"\\u06A4\",\n    \"/afii57506\": \"\\u067E\",\n    \"/afii57507\": \"\\u0686\",\n    \"/afii57508\": \"\\u0698\",\n    \"/afii57509\": \"\\u06AF\",\n    \"/afii57511\": \"\\u0679\",\n    \"/afii57512\": \"\\u0688\",\n    \"/afii57513\": \"\\u0691\",\n    \"/afii57514\": \"\\u06BA\",\n    \"/afii57519\": \"\\u06D2\",\n    \"/afii57534\": \"\\u06D5\",\n    \"/afii57636\": \"\\u20AA\",\n    \"/afii57645\": \"\\u05BE\",\n    \"/afii57658\": \"\\u05C3\",\n    \"/afii57664\": \"\\u05D0\",\n    \"/afii57665\": \"\\u05D1\",\n    \"/afii57666\": \"\\u05D2\",\n    \"/afii57667\": \"\\u05D3\",\n    \"/afii57668\": \"\\u05D4\",\n    \"/afii57669\": \"\\u05D5\",\n    \"/afii57670\": \"\\u05D6\",\n    \"/afii57671\": \"\\u05D7\",\n    \"/afii57672\": \"\\u05D8\",\n    \"/afii57673\": \"\\u05D9\",\n    \"/afii57674\": \"\\u05DA\",\n    \"/afii57675\": \"\\u05DB\",\n    \"/afii57676\": \"\\u05DC\",\n    \"/afii57677\": \"\\u05DD\",\n    \"/afii57678\": \"\\u05DE\",\n    \"/afii57679\": \"\\u05DF\",\n    \"/afii57680\": \"\\u05E0\",\n    \"/afii57681\": \"\\u05E1\",\n    \"/afii57682\": \"\\u05E2\",\n    \"/afii57683\": \"\\u05E3\",\n    \"/afii57684\": \"\\u05E4\",\n    \"/afii57685\": \"\\u05E5\",\n    \"/afii57686\": \"\\u05E6\",\n    \"/afii57687\": \"\\u05E7\",\n    \"/afii57688\": \"\\u05E8\",\n    \"/afii57689\": \"\\u05E9\",\n    \"/afii57690\": \"\\u05EA\",\n    \"/afii57694\": \"\\uFB2A\",\n    \"/afii57695\": \"\\uFB2B\",\n    \"/afii57700\": \"\\uFB4B\",\n    \"/afii57705\": \"\\uFB1F\",\n    \"/afii57716\": \"\\u05F0\",\n    \"/afii57717\": \"\\u05F1\",\n    \"/afii57718\": \"\\u05F2\",\n    \"/afii57723\": \"\\uFB35\",\n    \"/afii57793\": \"\\u05B4\",\n    \"/afii57794\": \"\\u05B5\",\n    \"/afii57795\": \"\\u05B6\",\n    \"/afii57796\": \"\\u05BB\",\n    \"/afii57797\": \"\\u05B8\",\n    \"/afii57798\": \"\\u05B7\",\n    \"/afii57799\": \"\\u05B0\",\n    \"/afii57800\": \"\\u05B2\",\n    \"/afii57801\": \"\\u05B1\",\n    \"/afii57802\": \"\\u05B3\",\n    \"/afii57803\": \"\\u05C2\",\n    \"/afii57804\": \"\\u05C1\",\n    \"/afii57806\": \"\\u05B9\",\n    \"/afii57807\": \"\\u05BC\",\n    \"/afii57839\": \"\\u05BD\",\n    \"/afii57841\": \"\\u05BF\",\n    \"/afii57842\": \"\\u05C0\",\n    \"/afii57929\": \"\\u02BC\",\n    \"/afii61248\": \"\\u2105\",\n    \"/afii61289\": \"\\u2113\",\n    \"/afii61352\": \"\\u2116\",\n    \"/afii61573\": \"\\u202C\",\n    \"/afii61574\": \"\\u202D\",\n    \"/afii61575\": \"\\u202E\",\n    \"/afii61664\": \"\\u200C\",\n    \"/afii63167\": \"\\u066D\",\n    \"/afii64937\": \"\\u02BD\",\n    \"/agrave\": \"\\u00E0\",\n    \"/agravedbl\": \"\\u0201\",\n    \"/agujarati\": \"\\u0A85\",\n    \"/agurmukhi\": \"\\u0A05\",\n    \"/ahiragana\": \"\\u3042\",\n    \"/ahoi\": \"\\u1EA3\",\n    \"/ahookabove\": \"\\u1EA3\",\n    \"/aibengali\": \"\\u0990\",\n    \"/aibopomofo\": \"\\u311E\",\n    \"/aideva\": \"\\u0910\",\n    \"/aiecyr\": \"\\u04D5\",\n    \"/aiecyrillic\": \"\\u04D5\",\n    \"/aigujarati\": \"\\u0A90\",\n    \"/aigurmukhi\": \"\\u0A10\",\n    \"/aimatragurmukhi\": \"\\u0A48\",\n    \"/ain.fina\": \"\\uFECA\",\n    \"/ain.init\": \"\\uFECB\",\n    \"/ain.init_alefmaksura.fina\": \"\\uFCF7\",\n    \"/ain.init_jeem.fina\": \"\\uFC29\",\n    \"/ain.init_jeem.medi\": \"\\uFCBA\",\n    \"/ain.init_jeem.medi_meem.medi\": \"\\uFDC4\",\n    \"/ain.init_meem.fina\": \"\\uFC2A\",\n    \"/ain.init_meem.medi\": \"\\uFCBB\",\n    \"/ain.init_meem.medi_meem.medi\": \"\\uFD77\",\n    \"/ain.init_yeh.fina\": \"\\uFCF8\",\n    \"/ain.isol\": \"\\uFEC9\",\n    \"/ain.medi\": \"\\uFECC\",\n    \"/ain.medi_alefmaksura.fina\": \"\\uFD13\",\n    \"/ain.medi_jeem.medi_meem.fina\": \"\\uFD75\",\n    \"/ain.medi_meem.medi_alefmaksura.fina\": \"\\uFD78\",\n    \"/ain.medi_meem.medi_meem.fina\": \"\\uFD76\",\n    \"/ain.medi_meem.medi_yeh.fina\": \"\\uFDB6\",\n    \"/ain.medi_yeh.fina\": \"\\uFD14\",\n    \"/ainThreeDotsDownAbove\": \"\\u075E\",\n    \"/ainTwoDotsAbove\": \"\\u075D\",\n    \"/ainTwoDotsVerticallyAbove\": \"\\u075F\",\n    \"/ainarabic\": \"\\u0639\",\n    \"/ainfinalarabic\": \"\\uFECA\",\n    \"/aininitialarabic\": \"\\uFECB\",\n    \"/ainmedialarabic\": \"\\uFECC\",\n    \"/ainthreedotsabove\": \"\\u06A0\",\n    \"/ainvertedbreve\": \"\\u0203\",\n    \"/airplaneArriving\": \"\\u1F6EC\",\n    \"/airplaneDeparture\": \"\\u1F6EB\",\n    \"/aivowelsignbengali\": \"\\u09C8\",\n    \"/aivowelsigndeva\": \"\\u0948\",\n    \"/aivowelsigngujarati\": \"\\u0AC8\",\n    \"/akatakana\": \"\\u30A2\",\n    \"/akatakanahalfwidth\": \"\\uFF71\",\n    \"/akorean\": \"\\u314F\",\n    \"/aktieselskab\": \"\\u214D\",\n    \"/alarmclock\": \"\\u23F0\",\n    \"/alef\": \"\\u05D0\",\n    \"/alef.fina\": \"\\uFE8E\",\n    \"/alef.init_fathatan.fina\": \"\\uFD3D\",\n    \"/alef.isol\": \"\\uFE8D\",\n    \"/alef.medi_fathatan.fina\": \"\\uFD3C\",\n    \"/alef:hb\": \"\\u05D0\",\n    \"/alefDigitThreeAbove\": \"\\u0774\",\n    \"/alefDigitTwoAbove\": \"\\u0773\",\n    \"/alefLamYehabove\": \"\\u0616\",\n    \"/alefabove\": \"\\u0670\",\n    \"/alefarabic\": \"\\u0627\",\n    \"/alefdageshhebrew\": \"\\uFB30\",\n    \"/aleffinalarabic\": \"\\uFE8E\",\n    \"/alefhamza\": \"\\u0623\",\n    \"/alefhamza.fina\": \"\\uFE84\",\n    \"/alefhamza.isol\": \"\\uFE83\",\n    \"/alefhamzaabovearabic\": \"\\u0623\",\n    \"/alefhamzaabovefinalarabic\": \"\\uFE84\",\n    \"/alefhamzabelow\": \"\\u0625\",\n    \"/alefhamzabelow.fina\": \"\\uFE88\",\n    \"/alefhamzabelow.isol\": \"\\uFE87\",\n    \"/alefhamzabelowarabic\": \"\\u0625\",\n    \"/alefhamzabelowfinalarabic\": \"\\uFE88\",\n    \"/alefhebrew\": \"\\u05D0\",\n    \"/alefhighhamza\": \"\\u0675\",\n    \"/aleflamedhebrew\": \"\\uFB4F\",\n    \"/alefmadda\": \"\\u0622\",\n    \"/alefmadda.fina\": \"\\uFE82\",\n    \"/alefmadda.isol\": \"\\uFE81\",\n    \"/alefmaddaabovearabic\": \"\\u0622\",\n    \"/alefmaddaabovefinalarabic\": \"\\uFE82\",\n    \"/alefmaksura\": \"\\u0649\",\n    \"/alefmaksura.fina\": \"\\uFEF0\",\n    \"/alefmaksura.init_superscriptalef.fina\": \"\\uFC5D\",\n    \"/alefmaksura.isol\": \"\\uFEEF\",\n    \"/alefmaksura.medi_superscriptalef.fina\": \"\\uFC90\",\n    \"/alefmaksuraarabic\": \"\\u0649\",\n    \"/alefmaksurafinalarabic\": \"\\uFEF0\",\n    \"/alefmaksurainitialarabic\": \"\\uFEF3\",\n    \"/alefmaksuramedialarabic\": \"\\uFEF4\",\n    \"/alefpatahhebrew\": \"\\uFB2E\",\n    \"/alefqamatshebrew\": \"\\uFB2F\",\n    \"/alefwasla\": \"\\u0671\",\n    \"/alefwasla.fina\": \"\\uFB51\",\n    \"/alefwasla.isol\": \"\\uFB50\",\n    \"/alefwavyhamza\": \"\\u0672\",\n    \"/alefwavyhamzabelow\": \"\\u0673\",\n    \"/alefwide:hb\": \"\\uFB21\",\n    \"/alefwithmapiq:hb\": \"\\uFB30\",\n    \"/alefwithpatah:hb\": \"\\uFB2E\",\n    \"/alefwithqamats:hb\": \"\\uFB2F\",\n    \"/alembic\": \"\\u2697\",\n    \"/aleph\": \"\\u2135\",\n    \"/alienMonster\": \"\\u1F47E\",\n    \"/allaroundprofile\": \"\\u232E\",\n    \"/allequal\": \"\\u224C\",\n    \"/allianceideographiccircled\": \"\\u32AF\",\n    \"/allianceideographicparen\": \"\\u323F\",\n    \"/almostequalorequal\": \"\\u224A\",\n    \"/alpha\": \"\\u03B1\",\n    \"/alphaacute\": \"\\u1F71\",\n    \"/alphaacuteiotasub\": \"\\u1FB4\",\n    \"/alphaasper\": \"\\u1F01\",\n    \"/alphaasperacute\": \"\\u1F05\",\n    \"/alphaasperacuteiotasub\": \"\\u1F85\",\n    \"/alphaaspergrave\": \"\\u1F03\",\n    \"/alphaaspergraveiotasub\": \"\\u1F83\",\n    \"/alphaasperiotasub\": \"\\u1F81\",\n    \"/alphaaspertilde\": \"\\u1F07\",\n    \"/alphaaspertildeiotasub\": \"\\u1F87\",\n    \"/alphabreve\": \"\\u1FB0\",\n    \"/alphafunc\": \"\\u237A\",\n    \"/alphagrave\": \"\\u1F70\",\n    \"/alphagraveiotasub\": \"\\u1FB2\",\n    \"/alphaiotasub\": \"\\u1FB3\",\n    \"/alphalenis\": \"\\u1F00\",\n    \"/alphalenisacute\": \"\\u1F04\",\n    \"/alphalenisacuteiotasub\": \"\\u1F84\",\n    \"/alphalenisgrave\": \"\\u1F02\",\n    \"/alphalenisgraveiotasub\": \"\\u1F82\",\n    \"/alphalenisiotasub\": \"\\u1F80\",\n    \"/alphalenistilde\": \"\\u1F06\",\n    \"/alphalenistildeiotasub\": \"\\u1F86\",\n    \"/alphatilde\": \"\\u1FB6\",\n    \"/alphatildeiotasub\": \"\\u1FB7\",\n    \"/alphatonos\": \"\\u03AC\",\n    \"/alphaturned\": \"\\u0252\",\n    \"/alphaunderlinefunc\": \"\\u2376\",\n    \"/alphawithmacron\": \"\\u1FB1\",\n    \"/alternateonewayleftwaytraffic\": \"\\u26D5\",\n    \"/alternative\": \"\\u2387\",\n    \"/amacron\": \"\\u0101\",\n    \"/ambulance\": \"\\u1F691\",\n    \"/americanFootball\": \"\\u1F3C8\",\n    \"/amfullwidth\": \"\\u33C2\",\n    \"/amonospace\": \"\\uFF41\",\n    \"/amountofcheck\": \"\\u2447\",\n    \"/ampersand\": \"\\u0026\",\n    \"/ampersandSindhi\": \"\\u06FD\",\n    \"/ampersandmonospace\": \"\\uFF06\",\n    \"/ampersandsmall\": \"\\uF726\",\n    \"/ampersandturned\": \"\\u214B\",\n    \"/amphora\": \"\\u1F3FA\",\n    \"/amsquare\": \"\\u33C2\",\n    \"/anbopomofo\": \"\\u3122\",\n    \"/anchor\": \"\\u2693\",\n    \"/ancoradown\": \"\\u2E14\",\n    \"/ancoraup\": \"\\u2E15\",\n    \"/andappada\": \"\\uA9C3\",\n    \"/angbopomofo\": \"\\u3124\",\n    \"/anger\": \"\\u1F4A2\",\n    \"/angkhankhuthai\": \"\\u0E5A\",\n    \"/angle\": \"\\u2220\",\n    \"/anglearcright\": \"\\u22BE\",\n    \"/anglebracketleft\": \"\\u3008\",\n    \"/anglebracketleftvertical\": \"\\uFE3F\",\n    \"/anglebracketright\": \"\\u3009\",\n    \"/anglebracketrightvertical\": \"\\uFE40\",\n    \"/angledottedright\": \"\\u2E16\",\n    \"/angleleft\": \"\\u2329\",\n    \"/anglemarkerdottedsubstitutionright\": \"\\u2E01\",\n    \"/anglemarkersubstitutionright\": \"\\u2E00\",\n    \"/angleright\": \"\\u232A\",\n    \"/anglezigzagarrowdownright\": \"\\u237C\",\n    \"/angryFace\": \"\\u1F620\",\n    \"/angstrom\": \"\\u212B\",\n    \"/anguishedFace\": \"\\u1F627\",\n    \"/ankh\": \"\\u2625\",\n    \"/anoteleia\": \"\\u0387\",\n    \"/anpeasquare\": \"\\u3302\",\n    \"/ant\": \"\\u1F41C\",\n    \"/antennaBars\": \"\\u1F4F6\",\n    \"/anticlockwiseDownwardsAndUpwardsOpenCircleArrows\": \"\\u1F504\",\n    \"/anudattadeva\": \"\\u0952\",\n    \"/anusvarabengali\": \"\\u0982\",\n    \"/anusvaradeva\": \"\\u0902\",\n    \"/anusvaragujarati\": \"\\u0A82\",\n    \"/ao\": \"\\uA735\",\n    \"/aogonek\": \"\\u0105\",\n    \"/aovermfullwidth\": \"\\u33DF\",\n    \"/apaatosquare\": \"\\u3300\",\n    \"/aparen\": \"\\u249C\",\n    \"/aparenthesized\": \"\\u249C\",\n    \"/apostrophearmenian\": \"\\u055A\",\n    \"/apostrophedblmod\": \"\\u02EE\",\n    \"/apostrophemod\": \"\\u02BC\",\n    \"/apple\": \"\\uF8FF\",\n    \"/approaches\": \"\\u2250\",\n    \"/approacheslimit\": \"\\u2250\",\n    \"/approxequal\": \"\\u2248\",\n    \"/approxequalorimage\": \"\\u2252\",\n    \"/approximatelybutnotactuallyequal\": \"\\u2246\",\n    \"/approximatelyequal\": \"\\u2245\",\n    \"/approximatelyequalorimage\": \"\\u2252\",\n    \"/apriltelegraph\": \"\\u32C3\",\n    \"/aquarius\": \"\\u2652\",\n    \"/ar:ae\": \"\\u06D5\",\n    \"/ar:ain\": \"\\u0639\",\n    \"/ar:alef\": \"\\u0627\",\n    \"/ar:comma\": \"\\u060C\",\n    \"/ar:cuberoot\": \"\\u0606\",\n    \"/ar:decimalseparator\": \"\\u066B\",\n    \"/ar:e\": \"\\u06D0\",\n    \"/ar:eight\": \"\\u0668\",\n    \"/ar:feh\": \"\\u0641\",\n    \"/ar:five\": \"\\u0665\",\n    \"/ar:four\": \"\\u0664\",\n    \"/ar:fourthroot\": \"\\u0607\",\n    \"/ar:kaf\": \"\\u0643\",\n    \"/ar:ng\": \"\\u06AD\",\n    \"/ar:nine\": \"\\u0669\",\n    \"/ar:numbersign\": \"\\u0600\",\n    \"/ar:oe\": \"\\u06C6\",\n    \"/ar:one\": \"\\u0661\",\n    \"/ar:peh\": \"\\u067E\",\n    \"/ar:percent\": \"\\u066A\",\n    \"/ar:perthousand\": \"\\u060A\",\n    \"/ar:question\": \"\\u061F\",\n    \"/ar:reh\": \"\\u0631\",\n    \"/ar:semicolon\": \"\\u061B\",\n    \"/ar:seven\": \"\\u0667\",\n    \"/ar:shadda\": \"\\u0651\",\n    \"/ar:six\": \"\\u0666\",\n    \"/ar:sukun\": \"\\u0652\",\n    \"/ar:three\": \"\\u0663\",\n    \"/ar:two\": \"\\u0662\",\n    \"/ar:u\": \"\\u06C7\",\n    \"/ar:ve\": \"\\u06CB\",\n    \"/ar:yu\": \"\\u06C8\",\n    \"/ar:zero\": \"\\u0660\",\n    \"/araeaekorean\": \"\\u318E\",\n    \"/araeakorean\": \"\\u318D\",\n    \"/arc\": \"\\u2312\",\n    \"/archaicmepigraphic\": \"\\uA7FF\",\n    \"/aries\": \"\\u2648\",\n    \"/arighthalfring\": \"\\u1E9A\",\n    \"/aring\": \"\\u00E5\",\n    \"/aringacute\": \"\\u01FB\",\n    \"/aringbelow\": \"\\u1E01\",\n    \"/armn:Ayb\": \"\\u0531\",\n    \"/armn:Ben\": \"\\u0532\",\n    \"/armn:Ca\": \"\\u053E\",\n    \"/armn:Cha\": \"\\u0549\",\n    \"/armn:Cheh\": \"\\u0543\",\n    \"/armn:Co\": \"\\u0551\",\n    \"/armn:DRAMSIGN\": \"\\u058F\",\n    \"/armn:Da\": \"\\u0534\",\n    \"/armn:Ech\": \"\\u0535\",\n    \"/armn:Eh\": \"\\u0537\",\n    \"/armn:Et\": \"\\u0538\",\n    \"/armn:Feh\": \"\\u0556\",\n    \"/armn:Ghad\": \"\\u0542\",\n    \"/armn:Gim\": \"\\u0533\",\n    \"/armn:Ho\": \"\\u0540\",\n    \"/armn:Ini\": \"\\u053B\",\n    \"/armn:Ja\": \"\\u0541\",\n    \"/armn:Jheh\": \"\\u054B\",\n    \"/armn:Keh\": \"\\u0554\",\n    \"/armn:Ken\": \"\\u053F\",\n    \"/armn:Liwn\": \"\\u053C\",\n    \"/armn:Men\": \"\\u0544\",\n    \"/armn:Now\": \"\\u0546\",\n    \"/armn:Oh\": \"\\u0555\",\n    \"/armn:Peh\": \"\\u054A\",\n    \"/armn:Piwr\": \"\\u0553\",\n    \"/armn:Ra\": \"\\u054C\",\n    \"/armn:Reh\": \"\\u0550\",\n    \"/armn:Seh\": \"\\u054D\",\n    \"/armn:Sha\": \"\\u0547\",\n    \"/armn:Tiwn\": \"\\u054F\",\n    \"/armn:To\": \"\\u0539\",\n    \"/armn:Vew\": \"\\u054E\",\n    \"/armn:Vo\": \"\\u0548\",\n    \"/armn:Xeh\": \"\\u053D\",\n    \"/armn:Yi\": \"\\u0545\",\n    \"/armn:Yiwn\": \"\\u0552\",\n    \"/armn:Za\": \"\\u0536\",\n    \"/armn:Zhe\": \"\\u053A\",\n    \"/armn:abbreviationmark\": \"\\u055F\",\n    \"/armn:apostrophe\": \"\\u055A\",\n    \"/armn:ayb\": \"\\u0561\",\n    \"/armn:ben\": \"\\u0562\",\n    \"/armn:ca\": \"\\u056E\",\n    \"/armn:cha\": \"\\u0579\",\n    \"/armn:cheh\": \"\\u0573\",\n    \"/armn:co\": \"\\u0581\",\n    \"/armn:comma\": \"\\u055D\",\n    \"/armn:da\": \"\\u0564\",\n    \"/armn:ech\": \"\\u0565\",\n    \"/armn:ech_yiwn\": \"\\u0587\",\n    \"/armn:eh\": \"\\u0567\",\n    \"/armn:emphasismark\": \"\\u055B\",\n    \"/armn:et\": \"\\u0568\",\n    \"/armn:exclam\": \"\\u055C\",\n    \"/armn:feh\": \"\\u0586\",\n    \"/armn:ghad\": \"\\u0572\",\n    \"/armn:gim\": \"\\u0563\",\n    \"/armn:ho\": \"\\u0570\",\n    \"/armn:hyphen\": \"\\u058A\",\n    \"/armn:ini\": \"\\u056B\",\n    \"/armn:ja\": \"\\u0571\",\n    \"/armn:jheh\": \"\\u057B\",\n    \"/armn:keh\": \"\\u0584\",\n    \"/armn:ken\": \"\\u056F\",\n    \"/armn:leftfacingeternitysign\": \"\\u058E\",\n    \"/armn:liwn\": \"\\u056C\",\n    \"/armn:men\": \"\\u0574\",\n    \"/armn:men_ech\": \"\\uFB14\",\n    \"/armn:men_ini\": \"\\uFB15\",\n    \"/armn:men_now\": \"\\uFB13\",\n    \"/armn:men_xeh\": \"\\uFB17\",\n    \"/armn:now\": \"\\u0576\",\n    \"/armn:oh\": \"\\u0585\",\n    \"/armn:peh\": \"\\u057A\",\n    \"/armn:period\": \"\\u0589\",\n    \"/armn:piwr\": \"\\u0583\",\n    \"/armn:question\": \"\\u055E\",\n    \"/armn:ra\": \"\\u057C\",\n    \"/armn:reh\": \"\\u0580\",\n    \"/armn:rightfacingeternitysign\": \"\\u058D\",\n    \"/armn:ringhalfleft\": \"\\u0559\",\n    \"/armn:seh\": \"\\u057D\",\n    \"/armn:sha\": \"\\u0577\",\n    \"/armn:tiwn\": \"\\u057F\",\n    \"/armn:to\": \"\\u0569\",\n    \"/armn:vew\": \"\\u057E\",\n    \"/armn:vew_now\": \"\\uFB16\",\n    \"/armn:vo\": \"\\u0578\",\n    \"/armn:xeh\": \"\\u056D\",\n    \"/armn:yi\": \"\\u0575\",\n    \"/armn:yiwn\": \"\\u0582\",\n    \"/armn:za\": \"\\u0566\",\n    \"/armn:zhe\": \"\\u056A\",\n    \"/arrowNE\": \"\\u2197\",\n    \"/arrowNW\": \"\\u2196\",\n    \"/arrowSE\": \"\\u2198\",\n    \"/arrowSW\": \"\\u2199\",\n    \"/arrowanticlockwiseopencircle\": \"\\u21BA\",\n    \"/arrowanticlockwisesemicircle\": \"\\u21B6\",\n    \"/arrowboth\": \"\\u2194\",\n    \"/arrowclockwiseopencircle\": \"\\u21BB\",\n    \"/arrowclockwisesemicircle\": \"\\u21B7\",\n    \"/arrowdashdown\": \"\\u21E3\",\n    \"/arrowdashleft\": \"\\u21E0\",\n    \"/arrowdashright\": \"\\u21E2\",\n    \"/arrowdashup\": \"\\u21E1\",\n    \"/arrowdblboth\": \"\\u21D4\",\n    \"/arrowdbldown\": \"\\u21D3\",\n    \"/arrowdblleft\": \"\\u21D0\",\n    \"/arrowdblright\": \"\\u21D2\",\n    \"/arrowdblup\": \"\\u21D1\",\n    \"/arrowdown\": \"\\u2193\",\n    \"/arrowdowndashed\": \"\\u21E3\",\n    \"/arrowdownfrombar\": \"\\u21A7\",\n    \"/arrowdownleft\": \"\\u2199\",\n    \"/arrowdownright\": \"\\u2198\",\n    \"/arrowdowntwoheaded\": \"\\u21A1\",\n    \"/arrowdownwhite\": \"\\u21E9\",\n    \"/arrowdownzigzag\": \"\\u21AF\",\n    \"/arrowheaddown\": \"\\u2304\",\n    \"/arrowheaddownlowmod\": \"\\u02EF\",\n    \"/arrowheaddownmod\": \"\\u02C5\",\n    \"/arrowheadleftlowmod\": \"\\u02F1\",\n    \"/arrowheadleftmod\": \"\\u02C2\",\n    \"/arrowheadrightlowmod\": \"\\u02F2\",\n    \"/arrowheadrightmod\": \"\\u02C3\",\n    \"/arrowheadtwobarsuphorizontal\": \"\\u2324\",\n    \"/arrowheadup\": \"\\u2303\",\n    \"/arrowheaduplowmod\": \"\\u02F0\",\n    \"/arrowheadupmod\": \"\\u02C4\",\n    \"/arrowhorizex\": \"\\uF8E7\",\n    \"/arrowleft\": \"\\u2190\",\n    \"/arrowleftdashed\": \"\\u21E0\",\n    \"/arrowleftdbl\": \"\\u21D0\",\n    \"/arrowleftdblstroke\": \"\\u21CD\",\n    \"/arrowleftdowncorner\": \"\\u21B5\",\n    \"/arrowleftdowntip\": \"\\u21B2\",\n    \"/arrowleftfrombar\": \"\\u21A4\",\n    \"/arrowlefthook\": \"\\u21A9\",\n    \"/arrowleftloop\": \"\\u21AB\",\n    \"/arrowleftlowmod\": \"\\u02FF\",\n    \"/arrowleftoverright\": \"\\u21C6\",\n    \"/arrowleftoverrighttobar\": \"\\u21B9\",\n    \"/arrowleftright\": \"\\u2194\",\n    \"/arrowleftrightstroke\": \"\\u21AE\",\n    \"/arrowleftrightwave\": \"\\u21AD\",\n    \"/arrowleftsquiggle\": \"\\u21DC\",\n    \"/arrowleftstroke\": \"\\u219A\",\n    \"/arrowlefttail\": \"\\u21A2\",\n    \"/arrowlefttobar\": \"\\u21E4\",\n    \"/arrowlefttwoheaded\": \"\\u219E\",\n    \"/arrowleftuptip\": \"\\u21B0\",\n    \"/arrowleftwave\": \"\\u219C\",\n    \"/arrowleftwhite\": \"\\u21E6\",\n    \"/arrowlongNWtobar\": \"\\u21B8\",\n    \"/arrowright\": \"\\u2192\",\n    \"/arrowrightdashed\": \"\\u21E2\",\n    \"/arrowrightdblstroke\": \"\\u21CF\",\n    \"/arrowrightdowncorner\": \"\\u21B4\",\n    \"/arrowrightdowntip\": \"\\u21B3\",\n    \"/arrowrightfrombar\": \"\\u21A6\",\n    \"/arrowrightheavy\": \"\\u279E\",\n    \"/arrowrighthook\": \"\\u21AA\",\n    \"/arrowrightloop\": \"\\u21AC\",\n    \"/arrowrightoverleft\": \"\\u21C4\",\n    \"/arrowrightsmallcircle\": \"\\u21F4\",\n    \"/arrowrightsquiggle\": \"\\u21DD\",\n    \"/arrowrightstroke\": \"\\u219B\",\n    \"/arrowrighttail\": \"\\u21A3\",\n    \"/arrowrighttobar\": \"\\u21E5\",\n    \"/arrowrighttwoheaded\": \"\\u21A0\",\n    \"/arrowrightwave\": \"\\u219D\",\n    \"/arrowrightwhite\": \"\\u21E8\",\n    \"/arrowspaireddown\": \"\\u21CA\",\n    \"/arrowspairedleft\": \"\\u21C7\",\n    \"/arrowspairedright\": \"\\u21C9\",\n    \"/arrowspairedup\": \"\\u21C8\",\n    \"/arrowtableft\": \"\\u21E4\",\n    \"/arrowtabright\": \"\\u21E5\",\n    \"/arrowup\": \"\\u2191\",\n    \"/arrowupdashed\": \"\\u21E1\",\n    \"/arrowupdn\": \"\\u2195\",\n    \"/arrowupdnbse\": \"\\u21A8\",\n    \"/arrowupdown\": \"\\u2195\",\n    \"/arrowupdownbase\": \"\\u21A8\",\n    \"/arrowupdownwithbase\": \"\\u21A8\",\n    \"/arrowupfrombar\": \"\\u21A5\",\n    \"/arrowupleft\": \"\\u2196\",\n    \"/arrowupleftofdown\": \"\\u21C5\",\n    \"/arrowupright\": \"\\u2197\",\n    \"/arrowuprighttip\": \"\\u21B1\",\n    \"/arrowuptwoheaded\": \"\\u219F\",\n    \"/arrowupwhite\": \"\\u21E7\",\n    \"/arrowvertex\": \"\\uF8E6\",\n    \"/articulatedLorry\": \"\\u1F69B\",\n    \"/artistPalette\": \"\\u1F3A8\",\n    \"/aruhuasquare\": \"\\u3301\",\n    \"/asciicircum\": \"\\u005E\",\n    \"/asciicircummonospace\": \"\\uFF3E\",\n    \"/asciitilde\": \"\\u007E\",\n    \"/asciitildemonospace\": \"\\uFF5E\",\n    \"/ascript\": \"\\u0251\",\n    \"/ascriptturned\": \"\\u0252\",\n    \"/asmallhiragana\": \"\\u3041\",\n    \"/asmallkatakana\": \"\\u30A1\",\n    \"/asmallkatakanahalfwidth\": \"\\uFF67\",\n    \"/asper\": \"\\u1FFE\",\n    \"/asperacute\": \"\\u1FDE\",\n    \"/aspergrave\": \"\\u1FDD\",\n    \"/aspertilde\": \"\\u1FDF\",\n    \"/assertion\": \"\\u22A6\",\n    \"/asterisk\": \"\\u002A\",\n    \"/asteriskaltonearabic\": \"\\u066D\",\n    \"/asteriskarabic\": \"\\u066D\",\n    \"/asteriskmath\": \"\\u2217\",\n    \"/asteriskmonospace\": \"\\uFF0A\",\n    \"/asterisksmall\": \"\\uFE61\",\n    \"/asterism\": \"\\u2042\",\n    \"/astonishedFace\": \"\\u1F632\",\n    \"/astroke\": \"\\u2C65\",\n    \"/astronomicaluranus\": \"\\u26E2\",\n    \"/asuperior\": \"\\uF6E9\",\n    \"/asympticallyequal\": \"\\u2243\",\n    \"/asymptoticallyequal\": \"\\u2243\",\n    \"/at\": \"\\u0040\",\n    \"/athleticShoe\": \"\\u1F45F\",\n    \"/atilde\": \"\\u00E3\",\n    \"/atmonospace\": \"\\uFF20\",\n    \"/atnachHafukh:hb\": \"\\u05A2\",\n    \"/atom\": \"\\u269B\",\n    \"/atsmall\": \"\\uFE6B\",\n    \"/attentionideographiccircled\": \"\\u329F\",\n    \"/aturned\": \"\\u0250\",\n    \"/au\": \"\\uA737\",\n    \"/aubengali\": \"\\u0994\",\n    \"/aubergine\": \"\\u1F346\",\n    \"/aubopomofo\": \"\\u3120\",\n    \"/audeva\": \"\\u0914\",\n    \"/aufullwidth\": \"\\u3373\",\n    \"/augujarati\": \"\\u0A94\",\n    \"/augurmukhi\": \"\\u0A14\",\n    \"/augusttelegraph\": \"\\u32C7\",\n    \"/aulengthmarkbengali\": \"\\u09D7\",\n    \"/aumatragurmukhi\": \"\\u0A4C\",\n    \"/austral\": \"\\u20B3\",\n    \"/automatedTellerMachine\": \"\\u1F3E7\",\n    \"/automobile\": \"\\u1F697\",\n    \"/auvowelsignbengali\": \"\\u09CC\",\n    \"/auvowelsigndeva\": \"\\u094C\",\n    \"/auvowelsigngujarati\": \"\\u0ACC\",\n    \"/av\": \"\\uA739\",\n    \"/avagrahadeva\": \"\\u093D\",\n    \"/avhorizontalbar\": \"\\uA73B\",\n    \"/ay\": \"\\uA73D\",\n    \"/aybarmenian\": \"\\u0561\",\n    \"/ayin\": \"\\u05E2\",\n    \"/ayin:hb\": \"\\u05E2\",\n    \"/ayinalt:hb\": \"\\uFB20\",\n    \"/ayinaltonehebrew\": \"\\uFB20\",\n    \"/ayinhebrew\": \"\\u05E2\",\n    \"/azla:hb\": \"\\u059C\",\n    \"/b\": \"\\u0062\",\n    \"/baarerusquare\": \"\\u332D\",\n    \"/babengali\": \"\\u09AC\",\n    \"/babyAngel\": \"\\u1F47C\",\n    \"/babyBottle\": \"\\u1F37C\",\n    \"/babyChick\": \"\\u1F424\",\n    \"/backLeftwardsArrowAbove\": \"\\u1F519\",\n    \"/backOfEnvelope\": \"\\u1F582\",\n    \"/backslash\": \"\\u005C\",\n    \"/backslashbarfunc\": \"\\u2340\",\n    \"/backslashdbl\": \"\\u244A\",\n    \"/backslashmonospace\": \"\\uFF3C\",\n    \"/bactrianCamel\": \"\\u1F42B\",\n    \"/badeva\": \"\\u092C\",\n    \"/badmintonRacquetAndShuttlecock\": \"\\u1F3F8\",\n    \"/bagdelimitersshapeleft\": \"\\u27C5\",\n    \"/bagdelimitersshaperight\": \"\\u27C6\",\n    \"/baggageClaim\": \"\\u1F6C4\",\n    \"/bagujarati\": \"\\u0AAC\",\n    \"/bagurmukhi\": \"\\u0A2C\",\n    \"/bahiragana\": \"\\u3070\",\n    \"/bahtthai\": \"\\u0E3F\",\n    \"/bakatakana\": \"\\u30D0\",\n    \"/balloon\": \"\\u1F388\",\n    \"/ballotBoldScriptX\": \"\\u1F5F6\",\n    \"/ballotBoxBallot\": \"\\u1F5F3\",\n    \"/ballotBoxBoldCheck\": \"\\u1F5F9\",\n    \"/ballotBoxBoldScriptX\": \"\\u1F5F7\",\n    \"/ballotBoxScriptX\": \"\\u1F5F5\",\n    \"/ballotScriptX\": \"\\u1F5F4\",\n    \"/bamurda\": \"\\uA9A8\",\n    \"/banana\": \"\\u1F34C\",\n    \"/bank\": \"\\u1F3E6\",\n    \"/banknoteDollarSign\": \"\\u1F4B5\",\n    \"/banknoteEuroSign\": \"\\u1F4B6\",\n    \"/banknotePoundSign\": \"\\u1F4B7\",\n    \"/banknoteYenSign\": \"\\u1F4B4\",\n    \"/bar\": \"\\u007C\",\n    \"/barChart\": \"\\u1F4CA\",\n    \"/barberPole\": \"\\u1F488\",\n    \"/barfullwidth\": \"\\u3374\",\n    \"/barmonospace\": \"\\uFF5C\",\n    \"/barquillverticalleft\": \"\\u2E20\",\n    \"/barquillverticalright\": \"\\u2E21\",\n    \"/baseball\": \"\\u26BE\",\n    \"/basketballAndHoop\": \"\\u1F3C0\",\n    \"/bath\": \"\\u1F6C0\",\n    \"/bathtub\": \"\\u1F6C1\",\n    \"/battery\": \"\\u1F50B\",\n    \"/bbopomofo\": \"\\u3105\",\n    \"/bcircle\": \"\\u24D1\",\n    \"/bdot\": \"\\u1E03\",\n    \"/bdotaccent\": \"\\u1E03\",\n    \"/bdotbelow\": \"\\u1E05\",\n    \"/beachUmbrella\": \"\\u1F3D6\",\n    \"/beamedAscendingMusicalNotes\": \"\\u1F39C\",\n    \"/beamedDescendingMusicalNotes\": \"\\u1F39D\",\n    \"/beamedeighthnotes\": \"\\u266B\",\n    \"/beamedsixteenthnotes\": \"\\u266C\",\n    \"/beamfunc\": \"\\u2336\",\n    \"/bearFace\": \"\\u1F43B\",\n    \"/beatingHeart\": \"\\u1F493\",\n    \"/because\": \"\\u2235\",\n    \"/becyr\": \"\\u0431\",\n    \"/becyrillic\": \"\\u0431\",\n    \"/bed\": \"\\u1F6CF\",\n    \"/beeh\": \"\\u067B\",\n    \"/beeh.fina\": \"\\uFB53\",\n    \"/beeh.init\": \"\\uFB54\",\n    \"/beeh.isol\": \"\\uFB52\",\n    \"/beeh.medi\": \"\\uFB55\",\n    \"/beerMug\": \"\\u1F37A\",\n    \"/beetasquare\": \"\\u333C\",\n    \"/beh\": \"\\u0628\",\n    \"/beh.fina\": \"\\uFE90\",\n    \"/beh.init\": \"\\uFE91\",\n    \"/beh.init_alefmaksura.fina\": \"\\uFC09\",\n    \"/beh.init_hah.fina\": \"\\uFC06\",\n    \"/beh.init_hah.medi\": \"\\uFC9D\",\n    \"/beh.init_heh.medi\": \"\\uFCA0\",\n    \"/beh.init_jeem.fina\": \"\\uFC05\",\n    \"/beh.init_jeem.medi\": \"\\uFC9C\",\n    \"/beh.init_khah.fina\": \"\\uFC07\",\n    \"/beh.init_khah.medi\": \"\\uFC9E\",\n    \"/beh.init_meem.fina\": \"\\uFC08\",\n    \"/beh.init_meem.medi\": \"\\uFC9F\",\n    \"/beh.init_yeh.fina\": \"\\uFC0A\",\n    \"/beh.isol\": \"\\uFE8F\",\n    \"/beh.medi\": \"\\uFE92\",\n    \"/beh.medi_alefmaksura.fina\": \"\\uFC6E\",\n    \"/beh.medi_hah.medi_yeh.fina\": \"\\uFDC2\",\n    \"/beh.medi_heh.medi\": \"\\uFCE2\",\n    \"/beh.medi_khah.medi_yeh.fina\": \"\\uFD9E\",\n    \"/beh.medi_meem.fina\": \"\\uFC6C\",\n    \"/beh.medi_meem.medi\": \"\\uFCE1\",\n    \"/beh.medi_noon.fina\": \"\\uFC6D\",\n    \"/beh.medi_reh.fina\": \"\\uFC6A\",\n    \"/beh.medi_yeh.fina\": \"\\uFC6F\",\n    \"/beh.medi_zain.fina\": \"\\uFC6B\",\n    \"/behDotBelowThreeDotsAbove\": \"\\u0751\",\n    \"/behInvertedSmallVBelow\": \"\\u0755\",\n    \"/behSmallV\": \"\\u0756\",\n    \"/behThreeDotsHorizontallyBelow\": \"\\u0750\",\n    \"/behThreeDotsUpBelow\": \"\\u0752\",\n    \"/behThreeDotsUpBelowTwoDotsAbove\": \"\\u0753\",\n    \"/behTwoDotsBelowDotAbove\": \"\\u0754\",\n    \"/beharabic\": \"\\u0628\",\n    \"/beheh\": \"\\u0680\",\n    \"/beheh.fina\": \"\\uFB5B\",\n    \"/beheh.init\": \"\\uFB5C\",\n    \"/beheh.isol\": \"\\uFB5A\",\n    \"/beheh.medi\": \"\\uFB5D\",\n    \"/behfinalarabic\": \"\\uFE90\",\n    \"/behinitialarabic\": \"\\uFE91\",\n    \"/behiragana\": \"\\u3079\",\n    \"/behmedialarabic\": \"\\uFE92\",\n    \"/behmeeminitialarabic\": \"\\uFC9F\",\n    \"/behmeemisolatedarabic\": \"\\uFC08\",\n    \"/behnoonfinalarabic\": \"\\uFC6D\",\n    \"/bekatakana\": \"\\u30D9\",\n    \"/bellCancellationStroke\": \"\\u1F515\",\n    \"/bellhopBell\": \"\\u1F6CE\",\n    \"/beltbuckle\": \"\\u2444\",\n    \"/benarmenian\": \"\\u0562\",\n    \"/beng:a\": \"\\u0985\",\n    \"/beng:aa\": \"\\u0986\",\n    \"/beng:aasign\": \"\\u09BE\",\n    \"/beng:abbreviationsign\": \"\\u09FD\",\n    \"/beng:ai\": \"\\u0990\",\n    \"/beng:aisign\": \"\\u09C8\",\n    \"/beng:anji\": \"\\u0980\",\n    \"/beng:anusvara\": \"\\u0982\",\n    \"/beng:au\": \"\\u0994\",\n    \"/beng:aulengthmark\": \"\\u09D7\",\n    \"/beng:ausign\": \"\\u09CC\",\n    \"/beng:avagraha\": \"\\u09BD\",\n    \"/beng:ba\": \"\\u09AC\",\n    \"/beng:bha\": \"\\u09AD\",\n    \"/beng:ca\": \"\\u099A\",\n    \"/beng:candrabindu\": \"\\u0981\",\n    \"/beng:cha\": \"\\u099B\",\n    \"/beng:currencyoneless\": \"\\u09F8\",\n    \"/beng:da\": \"\\u09A6\",\n    \"/beng:dda\": \"\\u09A1\",\n    \"/beng:ddha\": \"\\u09A2\",\n    \"/beng:dha\": \"\\u09A7\",\n    \"/beng:e\": \"\\u098F\",\n    \"/beng:eight\": \"\\u09EE\",\n    \"/beng:esign\": \"\\u09C7\",\n    \"/beng:five\": \"\\u09EB\",\n    \"/beng:four\": \"\\u09EA\",\n    \"/beng:fourcurrencynumerator\": \"\\u09F7\",\n    \"/beng:ga\": \"\\u0997\",\n    \"/beng:gandamark\": \"\\u09FB\",\n    \"/beng:gha\": \"\\u0998\",\n    \"/beng:ha\": \"\\u09B9\",\n    \"/beng:i\": \"\\u0987\",\n    \"/beng:ii\": \"\\u0988\",\n    \"/beng:iisign\": \"\\u09C0\",\n    \"/beng:isign\": \"\\u09BF\",\n    \"/beng:isshar\": \"\\u09FA\",\n    \"/beng:ja\": \"\\u099C\",\n    \"/beng:jha\": \"\\u099D\",\n    \"/beng:ka\": \"\\u0995\",\n    \"/beng:kha\": \"\\u0996\",\n    \"/beng:khandata\": \"\\u09CE\",\n    \"/beng:la\": \"\\u09B2\",\n    \"/beng:llvocal\": \"\\u09E1\",\n    \"/beng:llvocalsign\": \"\\u09E3\",\n    \"/beng:lvocal\": \"\\u098C\",\n    \"/beng:lvocalsign\": \"\\u09E2\",\n    \"/beng:ma\": \"\\u09AE\",\n    \"/beng:na\": \"\\u09A8\",\n    \"/beng:nga\": \"\\u0999\",\n    \"/beng:nine\": \"\\u09EF\",\n    \"/beng:nna\": \"\\u09A3\",\n    \"/beng:nukta\": \"\\u09BC\",\n    \"/beng:nya\": \"\\u099E\",\n    \"/beng:o\": \"\\u0993\",\n    \"/beng:one\": \"\\u09E7\",\n    \"/beng:onecurrencynumerator\": \"\\u09F4\",\n    \"/beng:osign\": \"\\u09CB\",\n    \"/beng:pa\": \"\\u09AA\",\n    \"/beng:pha\": \"\\u09AB\",\n    \"/beng:ra\": \"\\u09B0\",\n    \"/beng:ralowdiagonal\": \"\\u09F1\",\n    \"/beng:ramiddiagonal\": \"\\u09F0\",\n    \"/beng:rha\": \"\\u09DD\",\n    \"/beng:rra\": \"\\u09DC\",\n    \"/beng:rrvocal\": \"\\u09E0\",\n    \"/beng:rrvocalsign\": \"\\u09C4\",\n    \"/beng:rupee\": \"\\u09F3\",\n    \"/beng:rupeemark\": \"\\u09F2\",\n    \"/beng:rvocal\": \"\\u098B\",\n    \"/beng:rvocalsign\": \"\\u09C3\",\n    \"/beng:sa\": \"\\u09B8\",\n    \"/beng:seven\": \"\\u09ED\",\n    \"/beng:sha\": \"\\u09B6\",\n    \"/beng:six\": \"\\u09EC\",\n    \"/beng:sixteencurrencydenominator\": \"\\u09F9\",\n    \"/beng:ssa\": \"\\u09B7\",\n    \"/beng:ta\": \"\\u09A4\",\n    \"/beng:tha\": \"\\u09A5\",\n    \"/beng:three\": \"\\u09E9\",\n    \"/beng:threecurrencynumerator\": \"\\u09F6\",\n    \"/beng:tta\": \"\\u099F\",\n    \"/beng:ttha\": \"\\u09A0\",\n    \"/beng:two\": \"\\u09E8\",\n    \"/beng:twocurrencynumerator\": \"\\u09F5\",\n    \"/beng:u\": \"\\u0989\",\n    \"/beng:usign\": \"\\u09C1\",\n    \"/beng:uu\": \"\\u098A\",\n    \"/beng:uusign\": \"\\u09C2\",\n    \"/beng:vedicanusvara\": \"\\u09FC\",\n    \"/beng:virama\": \"\\u09CD\",\n    \"/beng:visarga\": \"\\u0983\",\n    \"/beng:ya\": \"\\u09AF\",\n    \"/beng:yya\": \"\\u09DF\",\n    \"/beng:zero\": \"\\u09E6\",\n    \"/bentoBox\": \"\\u1F371\",\n    \"/benzenering\": \"\\u232C\",\n    \"/benzeneringcircle\": \"\\u23E3\",\n    \"/bet\": \"\\u05D1\",\n    \"/bet:hb\": \"\\u05D1\",\n    \"/beta\": \"\\u03B2\",\n    \"/betasymbol\": \"\\u03D0\",\n    \"/betasymbolgreek\": \"\\u03D0\",\n    \"/betdagesh\": \"\\uFB31\",\n    \"/betdageshhebrew\": \"\\uFB31\",\n    \"/bethebrew\": \"\\u05D1\",\n    \"/betrafehebrew\": \"\\uFB4C\",\n    \"/between\": \"\\u226C\",\n    \"/betwithdagesh:hb\": \"\\uFB31\",\n    \"/betwithrafe:hb\": \"\\uFB4C\",\n    \"/bflourish\": \"\\uA797\",\n    \"/bhabengali\": \"\\u09AD\",\n    \"/bhadeva\": \"\\u092D\",\n    \"/bhagujarati\": \"\\u0AAD\",\n    \"/bhagurmukhi\": \"\\u0A2D\",\n    \"/bhook\": \"\\u0253\",\n    \"/bicycle\": \"\\u1F6B2\",\n    \"/bicyclist\": \"\\u1F6B4\",\n    \"/bihiragana\": \"\\u3073\",\n    \"/bikatakana\": \"\\u30D3\",\n    \"/bikini\": \"\\u1F459\",\n    \"/bilabialclick\": \"\\u0298\",\n    \"/billiards\": \"\\u1F3B1\",\n    \"/bindigurmukhi\": \"\\u0A02\",\n    \"/biohazard\": \"\\u2623\",\n    \"/bird\": \"\\u1F426\",\n    \"/birthdayCake\": \"\\u1F382\",\n    \"/birusquare\": \"\\u3331\",\n    \"/bishopblack\": \"\\u265D\",\n    \"/bishopwhite\": \"\\u2657\",\n    \"/bitcoin\": \"\\u20BF\",\n    \"/blackDownPointingBackhandIndex\": \"\\u1F5A3\",\n    \"/blackDroplet\": \"\\u1F322\",\n    \"/blackFolder\": \"\\u1F5BF\",\n    \"/blackHardShellFloppyDisk\": \"\\u1F5AA\",\n    \"/blackHeart\": \"\\u1F5A4\",\n    \"/blackLeftPointingBackhandIndex\": \"\\u1F59C\",\n    \"/blackPennant\": \"\\u1F3F2\",\n    \"/blackPushpin\": \"\\u1F588\",\n    \"/blackRightPointingBackhandIndex\": \"\\u1F59D\",\n    \"/blackRosette\": \"\\u1F3F6\",\n    \"/blackSkullAndCrossbones\": \"\\u1F571\",\n    \"/blackSquareButton\": \"\\u1F532\",\n    \"/blackTouchtoneTelephone\": \"\\u1F57F\",\n    \"/blackUpPointingBackhandIndex\": \"\\u1F5A2\",\n    \"/blackcircle\": \"\\u25CF\",\n    \"/blackcircleforrecord\": \"\\u23FA\",\n    \"/blackdiamond\": \"\\u25C6\",\n    \"/blackdownpointingtriangle\": \"\\u25BC\",\n    \"/blackforstopsquare\": \"\\u23F9\",\n    \"/blackleftpointingpointer\": \"\\u25C4\",\n    \"/blackleftpointingtriangle\": \"\\u25C0\",\n    \"/blacklenticularbracketleft\": \"\\u3010\",\n    \"/blacklenticularbracketleftvertical\": \"\\uFE3B\",\n    \"/blacklenticularbracketright\": \"\\u3011\",\n    \"/blacklenticularbracketrightvertical\": \"\\uFE3C\",\n    \"/blacklowerlefttriangle\": \"\\u25E3\",\n    \"/blacklowerrighttriangle\": \"\\u25E2\",\n    \"/blackmediumpointingtriangledown\": \"\\u23F7\",\n    \"/blackmediumpointingtriangleleft\": \"\\u23F4\",\n    \"/blackmediumpointingtriangleright\": \"\\u23F5\",\n    \"/blackmediumpointingtriangleup\": \"\\u23F6\",\n    \"/blackpointingdoubletrianglebarverticalleft\": \"\\u23EE\",\n    \"/blackpointingdoubletrianglebarverticalright\": \"\\u23ED\",\n    \"/blackpointingdoubletriangledown\": \"\\u23EC\",\n    \"/blackpointingdoubletriangleleft\": \"\\u23EA\",\n    \"/blackpointingdoubletriangleright\": \"\\u23E9\",\n    \"/blackpointingdoubletriangleup\": \"\\u23EB\",\n    \"/blackpointingtriangledoublebarverticalright\": \"\\u23EF\",\n    \"/blackrectangle\": \"\\u25AC\",\n    \"/blackrightpointingpointer\": \"\\u25BA\",\n    \"/blackrightpointingtriangle\": \"\\u25B6\",\n    \"/blacksmallsquare\": \"\\u25AA\",\n    \"/blacksmilingface\": \"\\u263B\",\n    \"/blacksquare\": \"\\u25A0\",\n    \"/blackstar\": \"\\u2605\",\n    \"/blackupperlefttriangle\": \"\\u25E4\",\n    \"/blackupperrighttriangle\": \"\\u25E5\",\n    \"/blackuppointingsmalltriangle\": \"\\u25B4\",\n    \"/blackuppointingtriangle\": \"\\u25B2\",\n    \"/blackwardsbulletleft\": \"\\u204C\",\n    \"/blackwardsbulletright\": \"\\u204D\",\n    \"/blank\": \"\\u2423\",\n    \"/blinebelow\": \"\\u1E07\",\n    \"/block\": \"\\u2588\",\n    \"/blossom\": \"\\u1F33C\",\n    \"/blowfish\": \"\\u1F421\",\n    \"/blueBook\": \"\\u1F4D8\",\n    \"/blueHeart\": \"\\u1F499\",\n    \"/bmonospace\": \"\\uFF42\",\n    \"/boar\": \"\\u1F417\",\n    \"/board\": \"\\u2328\",\n    \"/bobaimaithai\": \"\\u0E1A\",\n    \"/bohiragana\": \"\\u307C\",\n    \"/bokatakana\": \"\\u30DC\",\n    \"/bomb\": \"\\u1F4A3\",\n    \"/book\": \"\\u1F56E\",\n    \"/bookmark\": \"\\u1F516\",\n    \"/bookmarkTabs\": \"\\u1F4D1\",\n    \"/books\": \"\\u1F4DA\",\n    \"/bopo:a\": \"\\u311A\",\n    \"/bopo:ai\": \"\\u311E\",\n    \"/bopo:an\": \"\\u3122\",\n    \"/bopo:ang\": \"\\u3124\",\n    \"/bopo:au\": \"\\u3120\",\n    \"/bopo:b\": \"\\u3105\",\n    \"/bopo:c\": \"\\u3118\",\n    \"/bopo:ch\": \"\\u3114\",\n    \"/bopo:d\": \"\\u3109\",\n    \"/bopo:e\": \"\\u311C\",\n    \"/bopo:eh\": \"\\u311D\",\n    \"/bopo:ei\": \"\\u311F\",\n    \"/bopo:en\": \"\\u3123\",\n    \"/bopo:eng\": \"\\u3125\",\n    \"/bopo:er\": \"\\u3126\",\n    \"/bopo:f\": \"\\u3108\",\n    \"/bopo:g\": \"\\u310D\",\n    \"/bopo:gn\": \"\\u312C\",\n    \"/bopo:h\": \"\\u310F\",\n    \"/bopo:i\": \"\\u3127\",\n    \"/bopo:ih\": \"\\u312D\",\n    \"/bopo:iu\": \"\\u3129\",\n    \"/bopo:j\": \"\\u3110\",\n    \"/bopo:k\": \"\\u310E\",\n    \"/bopo:l\": \"\\u310C\",\n    \"/bopo:m\": \"\\u3107\",\n    \"/bopo:n\": \"\\u310B\",\n    \"/bopo:ng\": \"\\u312B\",\n    \"/bopo:o\": \"\\u311B\",\n    \"/bopo:ou\": \"\\u3121\",\n    \"/bopo:owithdotabove\": \"\\u312E\",\n    \"/bopo:p\": \"\\u3106\",\n    \"/bopo:q\": \"\\u3111\",\n    \"/bopo:r\": \"\\u3116\",\n    \"/bopo:s\": \"\\u3119\",\n    \"/bopo:sh\": \"\\u3115\",\n    \"/bopo:t\": \"\\u310A\",\n    \"/bopo:u\": \"\\u3128\",\n    \"/bopo:v\": \"\\u312A\",\n    \"/bopo:x\": \"\\u3112\",\n    \"/bopo:z\": \"\\u3117\",\n    \"/bopo:zh\": \"\\u3113\",\n    \"/borutosquare\": \"\\u333E\",\n    \"/bottlePoppingCork\": \"\\u1F37E\",\n    \"/bouquet\": \"\\u1F490\",\n    \"/bouquetOfFlowers\": \"\\u1F395\",\n    \"/bowAndArrow\": \"\\u1F3F9\",\n    \"/bowlOfHygieia\": \"\\u1F54F\",\n    \"/bowling\": \"\\u1F3B3\",\n    \"/boxlineverticalleft\": \"\\u23B8\",\n    \"/boxlineverticalright\": \"\\u23B9\",\n    \"/boy\": \"\\u1F466\",\n    \"/boys\": \"\\u1F6C9\",\n    \"/bparen\": \"\\u249D\",\n    \"/bparenthesized\": \"\\u249D\",\n    \"/bqfullwidth\": \"\\u33C3\",\n    \"/bqsquare\": \"\\u33C3\",\n    \"/braceex\": \"\\uF8F4\",\n    \"/braceleft\": \"\\u007B\",\n    \"/braceleftbt\": \"\\uF8F3\",\n    \"/braceleftmid\": \"\\uF8F2\",\n    \"/braceleftmonospace\": \"\\uFF5B\",\n    \"/braceleftsmall\": \"\\uFE5B\",\n    \"/bracelefttp\": \"\\uF8F1\",\n    \"/braceleftvertical\": \"\\uFE37\",\n    \"/braceright\": \"\\u007D\",\n    \"/bracerightbt\": \"\\uF8FE\",\n    \"/bracerightmid\": \"\\uF8FD\",\n    \"/bracerightmonospace\": \"\\uFF5D\",\n    \"/bracerightsmall\": \"\\uFE5C\",\n    \"/bracerighttp\": \"\\uF8FC\",\n    \"/bracerightvertical\": \"\\uFE38\",\n    \"/bracketangledblleft\": \"\\u27EA\",\n    \"/bracketangledblright\": \"\\u27EB\",\n    \"/bracketangleleft\": \"\\u27E8\",\n    \"/bracketangleright\": \"\\u27E9\",\n    \"/bracketbottomcurly\": \"\\u23DF\",\n    \"/bracketbottomsquare\": \"\\u23B5\",\n    \"/bracketcornerupleftsquare\": \"\\u23A1\",\n    \"/bracketcorneruprightsquare\": \"\\u23A4\",\n    \"/bracketdottedsubstitutionleft\": \"\\u2E04\",\n    \"/bracketdottedsubstitutionright\": \"\\u2E05\",\n    \"/bracketextensioncurly\": \"\\u23AA\",\n    \"/bracketextensionleftsquare\": \"\\u23A2\",\n    \"/bracketextensionrightsquare\": \"\\u23A5\",\n    \"/brackethalfbottomleft\": \"\\u2E24\",\n    \"/brackethalfbottomright\": \"\\u2E25\",\n    \"/brackethalftopleft\": \"\\u2E22\",\n    \"/brackethalftopright\": \"\\u2E23\",\n    \"/brackethookupleftcurly\": \"\\u23A7\",\n    \"/brackethookuprightcurly\": \"\\u23AB\",\n    \"/bracketleft\": \"\\u005B\",\n    \"/bracketleftbt\": \"\\uF8F0\",\n    \"/bracketleftex\": \"\\uF8EF\",\n    \"/bracketleftmonospace\": \"\\uFF3B\",\n    \"/bracketleftsquarequill\": \"\\u2045\",\n    \"/bracketlefttp\": \"\\uF8EE\",\n    \"/bracketlowercornerleftsquare\": \"\\u23A3\",\n    \"/bracketlowercornerrightsquare\": \"\\u23A6\",\n    \"/bracketlowerhookleftcurly\": \"\\u23A9\",\n    \"/bracketlowerhookrightcurly\": \"\\u23AD\",\n    \"/bracketmiddlepieceleftcurly\": \"\\u23A8\",\n    \"/bracketmiddlepiecerightcurly\": \"\\u23AC\",\n    \"/bracketoverbrackettopbottomsquare\": \"\\u23B6\",\n    \"/bracketparaphraselowleft\": \"\\u2E1C\",\n    \"/bracketparaphraselowright\": \"\\u2E1D\",\n    \"/bracketraisedleft\": \"\\u2E0C\",\n    \"/bracketraisedright\": \"\\u2E0D\",\n    \"/bracketright\": \"\\u005D\",\n    \"/bracketrightbt\": \"\\uF8FB\",\n    \"/bracketrightex\": \"\\uF8FA\",\n    \"/bracketrightmonospace\": \"\\uFF3D\",\n    \"/bracketrightsquarequill\": \"\\u2046\",\n    \"/bracketrighttp\": \"\\uF8F9\",\n    \"/bracketsectionupleftlowerrightcurly\": \"\\u23B0\",\n    \"/bracketsectionuprightlowerleftcurly\": \"\\u23B1\",\n    \"/bracketshellbottom\": \"\\u23E1\",\n    \"/bracketshelltop\": \"\\u23E0\",\n    \"/bracketshellwhiteleft\": \"\\u27EC\",\n    \"/bracketshellwhiteright\": \"\\u27ED\",\n    \"/bracketsubstitutionleft\": \"\\u2E02\",\n    \"/bracketsubstitutionright\": \"\\u2E03\",\n    \"/brackettopcurly\": \"\\u23DE\",\n    \"/brackettopsquare\": \"\\u23B4\",\n    \"/brackettranspositionleft\": \"\\u2E09\",\n    \"/brackettranspositionright\": \"\\u2E0A\",\n    \"/bracketwhitesquareleft\": \"\\u27E6\",\n    \"/bracketwhitesquareright\": \"\\u27E7\",\n    \"/branchbankidentification\": \"\\u2446\",\n    \"/bread\": \"\\u1F35E\",\n    \"/breve\": \"\\u02D8\",\n    \"/brevebelowcmb\": \"\\u032E\",\n    \"/brevecmb\": \"\\u0306\",\n    \"/breveinvertedbelowcmb\": \"\\u032F\",\n    \"/breveinvertedcmb\": \"\\u0311\",\n    \"/breveinverteddoublecmb\": \"\\u0361\",\n    \"/brevemetrical\": \"\\u23D1\",\n    \"/brideVeil\": \"\\u1F470\",\n    \"/bridgeAtNight\": \"\\u1F309\",\n    \"/bridgebelowcmb\": \"\\u032A\",\n    \"/bridgeinvertedbelowcmb\": \"\\u033A\",\n    \"/briefcase\": \"\\u1F4BC\",\n    \"/brll:blank\": \"\\u2800\",\n    \"/brokenHeart\": \"\\u1F494\",\n    \"/brokenbar\": \"\\u00A6\",\n    \"/brokencirclenorthwestarrow\": \"\\u238B\",\n    \"/bstroke\": \"\\u0180\",\n    \"/bsuperior\": \"\\uF6EA\",\n    \"/btopbar\": \"\\u0183\",\n    \"/bug\": \"\\u1F41B\",\n    \"/buhiragana\": \"\\u3076\",\n    \"/buildingConstruction\": \"\\u1F3D7\",\n    \"/bukatakana\": \"\\u30D6\",\n    \"/bullet\": \"\\u2022\",\n    \"/bulletinverse\": \"\\u25D8\",\n    \"/bulletoperator\": \"\\u2219\",\n    \"/bullhorn\": \"\\u1F56B\",\n    \"/bullhornSoundWaves\": \"\\u1F56C\",\n    \"/bullseye\": \"\\u25CE\",\n    \"/burrito\": \"\\u1F32F\",\n    \"/bus\": \"\\u1F68C\",\n    \"/busStop\": \"\\u1F68F\",\n    \"/bussyerusquare\": \"\\u3334\",\n    \"/bustInSilhouette\": \"\\u1F464\",\n    \"/bustsInSilhouette\": \"\\u1F465\",\n    \"/c\": \"\\u0063\",\n    \"/caarmenian\": \"\\u056E\",\n    \"/cabengali\": \"\\u099A\",\n    \"/cactus\": \"\\u1F335\",\n    \"/cacute\": \"\\u0107\",\n    \"/cadauna\": \"\\u2106\",\n    \"/cadeva\": \"\\u091A\",\n    \"/caduceus\": \"\\u2624\",\n    \"/cagujarati\": \"\\u0A9A\",\n    \"/cagurmukhi\": \"\\u0A1A\",\n    \"/cakraconsonant\": \"\\uA9BF\",\n    \"/calendar\": \"\\u1F4C5\",\n    \"/calfullwidth\": \"\\u3388\",\n    \"/callideographicparen\": \"\\u323A\",\n    \"/calsquare\": \"\\u3388\",\n    \"/camera\": \"\\u1F4F7\",\n    \"/cameraFlash\": \"\\u1F4F8\",\n    \"/camping\": \"\\u1F3D5\",\n    \"/camurda\": \"\\uA996\",\n    \"/cancellationX\": \"\\u1F5D9\",\n    \"/cancer\": \"\\u264B\",\n    \"/candle\": \"\\u1F56F\",\n    \"/candrabindubengali\": \"\\u0981\",\n    \"/candrabinducmb\": \"\\u0310\",\n    \"/candrabindudeva\": \"\\u0901\",\n    \"/candrabindugujarati\": \"\\u0A81\",\n    \"/candy\": \"\\u1F36C\",\n    \"/canoe\": \"\\u1F6F6\",\n    \"/capitulum\": \"\\u2E3F\",\n    \"/capricorn\": \"\\u2651\",\n    \"/capslock\": \"\\u21EA\",\n    \"/cardFileBox\": \"\\u1F5C3\",\n    \"/cardIndex\": \"\\u1F4C7\",\n    \"/cardIndexDividers\": \"\\u1F5C2\",\n    \"/careof\": \"\\u2105\",\n    \"/caret\": \"\\u2038\",\n    \"/caretinsertionpoint\": \"\\u2041\",\n    \"/carettildedownfunc\": \"\\u2371\",\n    \"/carettildeupfunc\": \"\\u2372\",\n    \"/caron\": \"\\u02C7\",\n    \"/caronbelowcmb\": \"\\u032C\",\n    \"/caroncmb\": \"\\u030C\",\n    \"/carouselHorse\": \"\\u1F3A0\",\n    \"/carpStreamer\": \"\\u1F38F\",\n    \"/carriagereturn\": \"\\u21B5\",\n    \"/carsliding\": \"\\u26D0\",\n    \"/castle\": \"\\u26EB\",\n    \"/cat\": \"\\u1F408\",\n    \"/catFace\": \"\\u1F431\",\n    \"/catFaceWithTearsOfJoy\": \"\\u1F639\",\n    \"/catFaceWithWrySmile\": \"\\u1F63C\",\n    \"/caution\": \"\\u2621\",\n    \"/cbar\": \"\\uA793\",\n    \"/cbopomofo\": \"\\u3118\",\n    \"/ccaron\": \"\\u010D\",\n    \"/ccedilla\": \"\\u00E7\",\n    \"/ccedillaacute\": \"\\u1E09\",\n    \"/ccfullwidth\": \"\\u33C4\",\n    \"/ccircle\": \"\\u24D2\",\n    \"/ccircumflex\": \"\\u0109\",\n    \"/ccurl\": \"\\u0255\",\n    \"/cdfullwidth\": \"\\u33C5\",\n    \"/cdot\": \"\\u010B\",\n    \"/cdotaccent\": \"\\u010B\",\n    \"/cdotreversed\": \"\\uA73F\",\n    \"/cdsquare\": \"\\u33C5\",\n    \"/cecak\": \"\\uA981\",\n    \"/cecaktelu\": \"\\uA9B3\",\n    \"/cedi\": \"\\u20B5\",\n    \"/cedilla\": \"\\u00B8\",\n    \"/cedillacmb\": \"\\u0327\",\n    \"/ceilingleft\": \"\\u2308\",\n    \"/ceilingright\": \"\\u2309\",\n    \"/celticCross\": \"\\u1F548\",\n    \"/cent\": \"\\u00A2\",\n    \"/centigrade\": \"\\u2103\",\n    \"/centinferior\": \"\\uF6DF\",\n    \"/centmonospace\": \"\\uFFE0\",\n    \"/centoldstyle\": \"\\uF7A2\",\n    \"/centreddotwhitediamond\": \"\\u27D0\",\n    \"/centreideographiccircled\": \"\\u32A5\",\n    \"/centreline\": \"\\u2104\",\n    \"/centrelineverticalsquarewhite\": \"\\u2385\",\n    \"/centsuperior\": \"\\uF6E0\",\n    \"/ceres\": \"\\u26B3\",\n    \"/chaarmenian\": \"\\u0579\",\n    \"/chabengali\": \"\\u099B\",\n    \"/chadeva\": \"\\u091B\",\n    \"/chagujarati\": \"\\u0A9B\",\n    \"/chagurmukhi\": \"\\u0A1B\",\n    \"/chains\": \"\\u26D3\",\n    \"/chair\": \"\\u2441\",\n    \"/chamkocircle\": \"\\u327C\",\n    \"/charactertie\": \"\\u2040\",\n    \"/chartDownwardsTrend\": \"\\u1F4C9\",\n    \"/chartUpwardsTrend\": \"\\u1F4C8\",\n    \"/chartUpwardsTrendAndYenSign\": \"\\u1F4B9\",\n    \"/chbopomofo\": \"\\u3114\",\n    \"/cheabkhasiancyrillic\": \"\\u04BD\",\n    \"/cheabkhcyr\": \"\\u04BD\",\n    \"/cheabkhtailcyr\": \"\\u04BF\",\n    \"/checkbox\": \"\\u2610\",\n    \"/checkboxchecked\": \"\\u2611\",\n    \"/checkboxx\": \"\\u2612\",\n    \"/checkmark\": \"\\u2713\",\n    \"/checyr\": \"\\u0447\",\n    \"/checyrillic\": \"\\u0447\",\n    \"/chedescenderabkhasiancyrillic\": \"\\u04BF\",\n    \"/chedescendercyrillic\": \"\\u04B7\",\n    \"/chedieresiscyr\": \"\\u04F5\",\n    \"/chedieresiscyrillic\": \"\\u04F5\",\n    \"/cheeringMegaphone\": \"\\u1F4E3\",\n    \"/cheharmenian\": \"\\u0573\",\n    \"/chekhakascyr\": \"\\u04CC\",\n    \"/chekhakassiancyrillic\": \"\\u04CC\",\n    \"/chequeredFlag\": \"\\u1F3C1\",\n    \"/cherries\": \"\\u1F352\",\n    \"/cherryBlossom\": \"\\u1F338\",\n    \"/chestnut\": \"\\u1F330\",\n    \"/chetailcyr\": \"\\u04B7\",\n    \"/chevertcyr\": \"\\u04B9\",\n    \"/cheverticalstrokecyrillic\": \"\\u04B9\",\n    \"/chi\": \"\\u03C7\",\n    \"/chicken\": \"\\u1F414\",\n    \"/chieuchacirclekorean\": \"\\u3277\",\n    \"/chieuchaparenkorean\": \"\\u3217\",\n    \"/chieuchcirclekorean\": \"\\u3269\",\n    \"/chieuchkorean\": \"\\u314A\",\n    \"/chieuchparenkorean\": \"\\u3209\",\n    \"/childrenCrossing\": \"\\u1F6B8\",\n    \"/chipmunk\": \"\\u1F43F\",\n    \"/chirho\": \"\\u2627\",\n    \"/chiron\": \"\\u26B7\",\n    \"/chochangthai\": \"\\u0E0A\",\n    \"/chochanthai\": \"\\u0E08\",\n    \"/chochingthai\": \"\\u0E09\",\n    \"/chochoethai\": \"\\u0E0C\",\n    \"/chocolateBar\": \"\\u1F36B\",\n    \"/chook\": \"\\u0188\",\n    \"/christmasTree\": \"\\u1F384\",\n    \"/church\": \"\\u26EA\",\n    \"/cieucacirclekorean\": \"\\u3276\",\n    \"/cieucaparenkorean\": \"\\u3216\",\n    \"/cieuccirclekorean\": \"\\u3268\",\n    \"/cieuckorean\": \"\\u3148\",\n    \"/cieucparenkorean\": \"\\u3208\",\n    \"/cieucuparenkorean\": \"\\u321C\",\n    \"/cinema\": \"\\u1F3A6\",\n    \"/circle\": \"\\u25CB\",\n    \"/circleallbutupperquadrantleftblack\": \"\\u25D5\",\n    \"/circlebackslashfunc\": \"\\u2349\",\n    \"/circleblack\": \"\\u25CF\",\n    \"/circledCrossPommee\": \"\\u1F540\",\n    \"/circledInformationSource\": \"\\u1F6C8\",\n    \"/circledasteriskoperator\": \"\\u229B\",\n    \"/circledbarnotchhorizontal\": \"\\u2389\",\n    \"/circledcrossinglanes\": \"\\u26D2\",\n    \"/circleddash\": \"\\u229D\",\n    \"/circleddivisionslash\": \"\\u2298\",\n    \"/circleddotoperator\": \"\\u2299\",\n    \"/circledequals\": \"\\u229C\",\n    \"/circlediaeresisfunc\": \"\\u2365\",\n    \"/circledminus\": \"\\u2296\",\n    \"/circledot\": \"\\u2299\",\n    \"/circledotrightwhite\": \"\\u2686\",\n    \"/circledotted\": \"\\u25CC\",\n    \"/circledringoperator\": \"\\u229A\",\n    \"/circledtriangledown\": \"\\u238A\",\n    \"/circlehalfleftblack\": \"\\u25D0\",\n    \"/circlehalfrightblack\": \"\\u25D1\",\n    \"/circleinversewhite\": \"\\u25D9\",\n    \"/circlejotfunc\": \"\\u233E\",\n    \"/circlelowerhalfblack\": \"\\u25D2\",\n    \"/circlelowerquadrantleftwhite\": \"\\u25F5\",\n    \"/circlelowerquadrantrightwhite\": \"\\u25F6\",\n    \"/circlemultiply\": \"\\u2297\",\n    \"/circleot\": \"\\u2299\",\n    \"/circleplus\": \"\\u2295\",\n    \"/circlepostalmark\": \"\\u3036\",\n    \"/circlestarfunc\": \"\\u235F\",\n    \"/circlestilefunc\": \"\\u233D\",\n    \"/circlestroketwodotsaboveheavy\": \"\\u26E3\",\n    \"/circletwodotsblackwhite\": \"\\u2689\",\n    \"/circletwodotswhite\": \"\\u2687\",\n    \"/circleunderlinefunc\": \"\\u235C\",\n    \"/circleupperhalfblack\": \"\\u25D3\",\n    \"/circleupperquadrantleftwhite\": \"\\u25F4\",\n    \"/circleupperquadrantrightblack\": \"\\u25D4\",\n    \"/circleupperquadrantrightwhite\": \"\\u25F7\",\n    \"/circleverticalfill\": \"\\u25CD\",\n    \"/circlewhite\": \"\\u25CB\",\n    \"/circlewhitedotrightblack\": \"\\u2688\",\n    \"/circlewithlefthalfblack\": \"\\u25D0\",\n    \"/circlewithrighthalfblack\": \"\\u25D1\",\n    \"/circumflex\": \"\\u02C6\",\n    \"/circumflexbelowcmb\": \"\\u032D\",\n    \"/circumflexcmb\": \"\\u0302\",\n    \"/circumflexlow\": \"\\uA788\",\n    \"/circusTent\": \"\\u1F3AA\",\n    \"/cityscape\": \"\\u1F3D9\",\n    \"/cityscapeAtDusk\": \"\\u1F306\",\n    \"/cjk:ideographiccomma\": \"\\u3001\",\n    \"/cjk:tortoiseshellbracketleft\": \"\\u3014\",\n    \"/cjk:tortoiseshellbracketright\": \"\\u3015\",\n    \"/clamshellMobilePhone\": \"\\u1F581\",\n    \"/clapperBoard\": \"\\u1F3AC\",\n    \"/clappingHandsSign\": \"\\u1F44F\",\n    \"/classicalBuilding\": \"\\u1F3DB\",\n    \"/clear\": \"\\u2327\",\n    \"/clearscreen\": \"\\u239A\",\n    \"/clickalveolar\": \"\\u01C2\",\n    \"/clickbilabial\": \"\\u0298\",\n    \"/clickdental\": \"\\u01C0\",\n    \"/clicklateral\": \"\\u01C1\",\n    \"/clickretroflex\": \"\\u01C3\",\n    \"/clinkingBeerMugs\": \"\\u1F37B\",\n    \"/clipboard\": \"\\u1F4CB\",\n    \"/clockFaceEight-thirty\": \"\\u1F563\",\n    \"/clockFaceEightOclock\": \"\\u1F557\",\n    \"/clockFaceEleven-thirty\": \"\\u1F566\",\n    \"/clockFaceElevenOclock\": \"\\u1F55A\",\n    \"/clockFaceFive-thirty\": \"\\u1F560\",\n    \"/clockFaceFiveOclock\": \"\\u1F554\",\n    \"/clockFaceFour-thirty\": \"\\u1F55F\",\n    \"/clockFaceFourOclock\": \"\\u1F553\",\n    \"/clockFaceNine-thirty\": \"\\u1F564\",\n    \"/clockFaceNineOclock\": \"\\u1F558\",\n    \"/clockFaceOne-thirty\": \"\\u1F55C\",\n    \"/clockFaceOneOclock\": \"\\u1F550\",\n    \"/clockFaceSeven-thirty\": \"\\u1F562\",\n    \"/clockFaceSevenOclock\": \"\\u1F556\",\n    \"/clockFaceSix-thirty\": \"\\u1F561\",\n    \"/clockFaceSixOclock\": \"\\u1F555\",\n    \"/clockFaceTen-thirty\": \"\\u1F565\",\n    \"/clockFaceTenOclock\": \"\\u1F559\",\n    \"/clockFaceThree-thirty\": \"\\u1F55E\",\n    \"/clockFaceThreeOclock\": \"\\u1F552\",\n    \"/clockFaceTwelve-thirty\": \"\\u1F567\",\n    \"/clockFaceTwelveOclock\": \"\\u1F55B\",\n    \"/clockFaceTwo-thirty\": \"\\u1F55D\",\n    \"/clockFaceTwoOclock\": \"\\u1F551\",\n    \"/clockwiseDownwardsAndUpwardsOpenCircleArrows\": \"\\u1F503\",\n    \"/clockwiseRightAndLeftSemicircleArrows\": \"\\u1F5D8\",\n    \"/clockwiseRightwardsAndLeftwardsOpenCircleArrows\": \"\\u1F501\",\n    \"/clockwiseRightwardsAndLeftwardsOpenCircleArrowsCircledOneOverlay\": \"\\u1F502\",\n    \"/closedBook\": \"\\u1F4D5\",\n    \"/closedLockKey\": \"\\u1F510\",\n    \"/closedMailboxLoweredFlag\": \"\\u1F4EA\",\n    \"/closedMailboxRaisedFlag\": \"\\u1F4EB\",\n    \"/closedUmbrella\": \"\\u1F302\",\n    \"/closedentryleft\": \"\\u26DC\",\n    \"/closeup\": \"\\u2050\",\n    \"/cloud\": \"\\u2601\",\n    \"/cloudLightning\": \"\\u1F329\",\n    \"/cloudRain\": \"\\u1F327\",\n    \"/cloudSnow\": \"\\u1F328\",\n    \"/cloudTornado\": \"\\u1F32A\",\n    \"/clsquare\": \"\\u1F191\",\n    \"/club\": \"\\u2663\",\n    \"/clubblack\": \"\\u2663\",\n    \"/clubsuitblack\": \"\\u2663\",\n    \"/clubsuitwhite\": \"\\u2667\",\n    \"/clubwhite\": \"\\u2667\",\n    \"/cm2fullwidth\": \"\\u33A0\",\n    \"/cm3fullwidth\": \"\\u33A4\",\n    \"/cmb:a\": \"\\u0363\",\n    \"/cmb:aaboveflat\": \"\\u1DD3\",\n    \"/cmb:aboveogonek\": \"\\u1DCE\",\n    \"/cmb:acute\": \"\\u0301\",\n    \"/cmb:acutebelow\": \"\\u0317\",\n    \"/cmb:acutegraveacute\": \"\\u1DC9\",\n    \"/cmb:acutemacron\": \"\\u1DC7\",\n    \"/cmb:acutetone\": \"\\u0341\",\n    \"/cmb:adieresis\": \"\\u1DF2\",\n    \"/cmb:ae\": \"\\u1DD4\",\n    \"/cmb:almostequalabove\": \"\\u034C\",\n    \"/cmb:almostequaltobelow\": \"\\u1DFD\",\n    \"/cmb:alpha\": \"\\u1DE7\",\n    \"/cmb:ao\": \"\\u1DD5\",\n    \"/cmb:arrowheadleftbelow\": \"\\u0354\",\n    \"/cmb:arrowheadrightabove\": \"\\u0350\",\n    \"/cmb:arrowheadrightarrowheadupbelow\": \"\\u0356\",\n    \"/cmb:arrowheadrightbelow\": \"\\u0355\",\n    \"/cmb:arrowleftrightbelow\": \"\\u034D\",\n    \"/cmb:arrowrightdoublebelow\": \"\\u0362\",\n    \"/cmb:arrowupbelow\": \"\\u034E\",\n    \"/cmb:asteriskbelow\": \"\\u0359\",\n    \"/cmb:av\": \"\\u1DD6\",\n    \"/cmb:b\": \"\\u1DE8\",\n    \"/cmb:belowbreve\": \"\\u032E\",\n    \"/cmb:beta\": \"\\u1DE9\",\n    \"/cmb:breve\": \"\\u0306\",\n    \"/cmb:brevemacron\": \"\\u1DCB\",\n    \"/cmb:bridgeabove\": \"\\u0346\",\n    \"/cmb:bridgebelow\": \"\\u032A\",\n    \"/cmb:c\": \"\\u0368\",\n    \"/cmb:candrabindu\": \"\\u0310\",\n    \"/cmb:caron\": \"\\u030C\",\n    \"/cmb:caronbelow\": \"\\u032C\",\n    \"/cmb:ccedilla\": \"\\u1DD7\",\n    \"/cmb:cedilla\": \"\\u0327\",\n    \"/cmb:circumflex\": \"\\u0302\",\n    \"/cmb:circumflexbelow\": \"\\u032D\",\n    \"/cmb:commaaccentbelow\": \"\\u0326\",\n    \"/cmb:commaturnedabove\": \"\\u0312\",\n    \"/cmb:d\": \"\\u0369\",\n    \"/cmb:dblarchinvertedbelow\": \"\\u032B\",\n    \"/cmb:dbloverline\": \"\\u033F\",\n    \"/cmb:dblverticallineabove\": \"\\u030E\",\n    \"/cmb:dblverticallinebelow\": \"\\u0348\",\n    \"/cmb:deletionmark\": \"\\u1DFB\",\n    \"/cmb:dialytikatonos\": \"\\u0344\",\n    \"/cmb:dieresis\": \"\\u0308\",\n    \"/cmb:dieresisbelow\": \"\\u0324\",\n    \"/cmb:dotaboveleft\": \"\\u1DF8\",\n    \"/cmb:dotaccent\": \"\\u0307\",\n    \"/cmb:dotbelowcomb\": \"\\u0323\",\n    \"/cmb:dotrightabove\": \"\\u0358\",\n    \"/cmb:dottedacute\": \"\\u1DC1\",\n    \"/cmb:dottedgrave\": \"\\u1DC0\",\n    \"/cmb:doubleabovecircumflex\": \"\\u1DCD\",\n    \"/cmb:doublebelowbreve\": \"\\u035C\",\n    \"/cmb:doublebreve\": \"\\u035D\",\n    \"/cmb:doubleinvertedbelowbreve\": \"\\u1DFC\",\n    \"/cmb:doubleringbelow\": \"\\u035A\",\n    \"/cmb:downtackbelow\": \"\\u031E\",\n    \"/cmb:e\": \"\\u0364\",\n    \"/cmb:equalbelow\": \"\\u0347\",\n    \"/cmb:esh\": \"\\u1DEF\",\n    \"/cmb:eth\": \"\\u1DD9\",\n    \"/cmb:f\": \"\\u1DEB\",\n    \"/cmb:fermata\": \"\\u0352\",\n    \"/cmb:g\": \"\\u1DDA\",\n    \"/cmb:graphemejoiner\": \"\\u034F\",\n    \"/cmb:grave\": \"\\u0300\",\n    \"/cmb:graveacutegrave\": \"\\u1DC8\",\n    \"/cmb:gravebelow\": \"\\u0316\",\n    \"/cmb:gravedouble\": \"\\u030F\",\n    \"/cmb:gravemacron\": \"\\u1DC5\",\n    \"/cmb:gravetone\": \"\\u0340\",\n    \"/cmb:gsmall\": \"\\u1DDB\",\n    \"/cmb:h\": \"\\u036A\",\n    \"/cmb:halfleftringabove\": \"\\u0351\",\n    \"/cmb:halfleftringbelow\": \"\\u031C\",\n    \"/cmb:halfrightringabove\": \"\\u0357\",\n    \"/cmb:halfrightringbelow\": \"\\u0339\",\n    \"/cmb:homotheticabove\": \"\\u034B\",\n    \"/cmb:hookabove\": \"\\u0309\",\n    \"/cmb:horn\": \"\\u031B\",\n    \"/cmb:hungarumlaut\": \"\\u030B\",\n    \"/cmb:i\": \"\\u0365\",\n    \"/cmb:insulard\": \"\\u1DD8\",\n    \"/cmb:invertedbelowbreve\": \"\\u032F\",\n    \"/cmb:invertedbreve\": \"\\u0311\",\n    \"/cmb:invertedbridgebelow\": \"\\u033A\",\n    \"/cmb:inverteddoublebreve\": \"\\u0361\",\n    \"/cmb:iotasub\": \"\\u0345\",\n    \"/cmb:isbelow\": \"\\u1DD0\",\n    \"/cmb:k\": \"\\u1DDC\",\n    \"/cmb:kavykaaboveleft\": \"\\u1DF7\",\n    \"/cmb:kavykaaboveright\": \"\\u1DF6\",\n    \"/cmb:koronis\": \"\\u0343\",\n    \"/cmb:l\": \"\\u1DDD\",\n    \"/cmb:leftangleabove\": \"\\u031A\",\n    \"/cmb:leftanglebelow\": \"\\u0349\",\n    \"/cmb:leftarrowheadabove\": \"\\u1DFE\",\n    \"/cmb:lefttackbelow\": \"\\u0318\",\n    \"/cmb:lineverticalabove\": \"\\u030D\",\n    \"/cmb:lineverticalbelow\": \"\\u0329\",\n    \"/cmb:longs\": \"\\u1DE5\",\n    \"/cmb:lowline\": \"\\u0332\",\n    \"/cmb:lowlinedouble\": \"\\u0333\",\n    \"/cmb:lsmall\": \"\\u1DDE\",\n    \"/cmb:lwithdoublemiddletilde\": \"\\u1DEC\",\n    \"/cmb:m\": \"\\u036B\",\n    \"/cmb:macron\": \"\\u0304\",\n    \"/cmb:macronacute\": \"\\u1DC4\",\n    \"/cmb:macronbelow\": \"\\u0331\",\n    \"/cmb:macronbreve\": \"\\u1DCC\",\n    \"/cmb:macrondouble\": \"\\u035E\",\n    \"/cmb:macrondoublebelow\": \"\\u035F\",\n    \"/cmb:macrongrave\": \"\\u1DC6\",\n    \"/cmb:minusbelow\": \"\\u0320\",\n    \"/cmb:msmall\": \"\\u1DDF\",\n    \"/cmb:n\": \"\\u1DE0\",\n    \"/cmb:nottildeabove\": \"\\u034A\",\n    \"/cmb:nsmall\": \"\\u1DE1\",\n    \"/cmb:o\": \"\\u0366\",\n    \"/cmb:odieresis\": \"\\u1DF3\",\n    \"/cmb:ogonek\": \"\\u0328\",\n    \"/cmb:overlaystrokelong\": \"\\u0336\",\n    \"/cmb:overlaystrokeshort\": \"\\u0335\",\n    \"/cmb:overline\": \"\\u0305\",\n    \"/cmb:owithlightcentralizationstroke\": \"\\u1DED\",\n    \"/cmb:p\": \"\\u1DEE\",\n    \"/cmb:palatalizedhookbelow\": \"\\u0321\",\n    \"/cmb:perispomeni\": \"\\u0342\",\n    \"/cmb:plusbelow\": \"\\u031F\",\n    \"/cmb:r\": \"\\u036C\",\n    \"/cmb:rbelow\": \"\\u1DCA\",\n    \"/cmb:retroflexhookbelow\": \"\\u0322\",\n    \"/cmb:reversedcommaabove\": \"\\u0314\",\n    \"/cmb:rightarrowheadanddownarrowheadbelow\": \"\\u1DFF\",\n    \"/cmb:righttackbelow\": \"\\u0319\",\n    \"/cmb:ringabove\": \"\\u030A\",\n    \"/cmb:ringbelow\": \"\\u0325\",\n    \"/cmb:rrotunda\": \"\\u1DE3\",\n    \"/cmb:rsmall\": \"\\u1DE2\",\n    \"/cmb:s\": \"\\u1DE4\",\n    \"/cmb:schwa\": \"\\u1DEA\",\n    \"/cmb:seagullbelow\": \"\\u033C\",\n    \"/cmb:snakebelow\": \"\\u1DC2\",\n    \"/cmb:soliduslongoverlay\": \"\\u0338\",\n    \"/cmb:solidusshortoverlay\": \"\\u0337\",\n    \"/cmb:squarebelow\": \"\\u033B\",\n    \"/cmb:suspensionmark\": \"\\u1DC3\",\n    \"/cmb:t\": \"\\u036D\",\n    \"/cmb:tilde\": \"\\u0303\",\n    \"/cmb:tildebelow\": \"\\u0330\",\n    \"/cmb:tildedouble\": \"\\u0360\",\n    \"/cmb:tildeoverlay\": \"\\u0334\",\n    \"/cmb:tildevertical\": \"\\u033E\",\n    \"/cmb:turnedabove\": \"\\u0313\",\n    \"/cmb:turnedcommaabove\": \"\\u0315\",\n    \"/cmb:u\": \"\\u0367\",\n    \"/cmb:udieresis\": \"\\u1DF4\",\n    \"/cmb:uptackabove\": \"\\u1DF5\",\n    \"/cmb:uptackbelow\": \"\\u031D\",\n    \"/cmb:urabove\": \"\\u1DD1\",\n    \"/cmb:usabove\": \"\\u1DD2\",\n    \"/cmb:uwithlightcentralizationstroke\": \"\\u1DF0\",\n    \"/cmb:v\": \"\\u036E\",\n    \"/cmb:w\": \"\\u1DF1\",\n    \"/cmb:wideinvertedbridgebelow\": \"\\u1DF9\",\n    \"/cmb:x\": \"\\u036F\",\n    \"/cmb:xabove\": \"\\u033D\",\n    \"/cmb:xbelow\": \"\\u0353\",\n    \"/cmb:z\": \"\\u1DE6\",\n    \"/cmb:zigzagabove\": \"\\u035B\",\n    \"/cmb:zigzagbelow\": \"\\u1DCF\",\n    \"/cmcubedsquare\": \"\\u33A4\",\n    \"/cmfullwidth\": \"\\u339D\",\n    \"/cmonospace\": \"\\uFF43\",\n    \"/cmsquaredsquare\": \"\\u33A0\",\n    \"/cntr:acknowledge\": \"\\u2406\",\n    \"/cntr:backspace\": \"\\u2408\",\n    \"/cntr:bell\": \"\\u2407\",\n    \"/cntr:blank\": \"\\u2422\",\n    \"/cntr:cancel\": \"\\u2418\",\n    \"/cntr:carriagereturn\": \"\\u240D\",\n    \"/cntr:datalinkescape\": \"\\u2410\",\n    \"/cntr:delete\": \"\\u2421\",\n    \"/cntr:deleteformtwo\": \"\\u2425\",\n    \"/cntr:devicecontrolfour\": \"\\u2414\",\n    \"/cntr:devicecontrolone\": \"\\u2411\",\n    \"/cntr:devicecontrolthree\": \"\\u2413\",\n    \"/cntr:devicecontroltwo\": \"\\u2412\",\n    \"/cntr:endofmedium\": \"\\u2419\",\n    \"/cntr:endoftext\": \"\\u2403\",\n    \"/cntr:endoftransmission\": \"\\u2404\",\n    \"/cntr:endoftransmissionblock\": \"\\u2417\",\n    \"/cntr:enquiry\": \"\\u2405\",\n    \"/cntr:escape\": \"\\u241B\",\n    \"/cntr:fileseparator\": \"\\u241C\",\n    \"/cntr:formfeed\": \"\\u240C\",\n    \"/cntr:groupseparator\": \"\\u241D\",\n    \"/cntr:horizontaltab\": \"\\u2409\",\n    \"/cntr:linefeed\": \"\\u240A\",\n    \"/cntr:negativeacknowledge\": \"\\u2415\",\n    \"/cntr:newline\": \"\\u2424\",\n    \"/cntr:null\": \"\\u2400\",\n    \"/cntr:openbox\": \"\\u2423\",\n    \"/cntr:recordseparator\": \"\\u241E\",\n    \"/cntr:shiftin\": \"\\u240F\",\n    \"/cntr:shiftout\": \"\\u240E\",\n    \"/cntr:space\": \"\\u2420\",\n    \"/cntr:startofheading\": \"\\u2401\",\n    \"/cntr:startoftext\": \"\\u2402\",\n    \"/cntr:substitute\": \"\\u241A\",\n    \"/cntr:substituteformtwo\": \"\\u2426\",\n    \"/cntr:synchronousidle\": \"\\u2416\",\n    \"/cntr:unitseparator\": \"\\u241F\",\n    \"/cntr:verticaltab\": \"\\u240B\",\n    \"/coarmenian\": \"\\u0581\",\n    \"/cocktailGlass\": \"\\u1F378\",\n    \"/coffin\": \"\\u26B0\",\n    \"/cofullwidth\": \"\\u33C7\",\n    \"/collision\": \"\\u1F4A5\",\n    \"/colon\": \"\\u003A\",\n    \"/colonequals\": \"\\u2254\",\n    \"/colonmod\": \"\\uA789\",\n    \"/colonmonetary\": \"\\u20A1\",\n    \"/colonmonospace\": \"\\uFF1A\",\n    \"/colonraisedmod\": \"\\u02F8\",\n    \"/colonsign\": \"\\u20A1\",\n    \"/colonsmall\": \"\\uFE55\",\n    \"/colontriangularhalfmod\": \"\\u02D1\",\n    \"/colontriangularmod\": \"\\u02D0\",\n    \"/comet\": \"\\u2604\",\n    \"/comma\": \"\\u002C\",\n    \"/commaabovecmb\": \"\\u0313\",\n    \"/commaaboverightcmb\": \"\\u0315\",\n    \"/commaaccent\": \"\\uF6C3\",\n    \"/commaarabic\": \"\\u060C\",\n    \"/commaarmenian\": \"\\u055D\",\n    \"/commabarfunc\": \"\\u236A\",\n    \"/commainferior\": \"\\uF6E1\",\n    \"/commamonospace\": \"\\uFF0C\",\n    \"/commaraised\": \"\\u2E34\",\n    \"/commareversed\": \"\\u2E41\",\n    \"/commareversedabovecmb\": \"\\u0314\",\n    \"/commareversedmod\": \"\\u02BD\",\n    \"/commasmall\": \"\\uFE50\",\n    \"/commasuperior\": \"\\uF6E2\",\n    \"/commaturnedabovecmb\": \"\\u0312\",\n    \"/commaturnedmod\": \"\\u02BB\",\n    \"/commercialat\": \"\\uFE6B\",\n    \"/commercialminussign\": \"\\u2052\",\n    \"/compass\": \"\\u263C\",\n    \"/complement\": \"\\u2201\",\n    \"/composition\": \"\\u2384\",\n    \"/compression\": \"\\u1F5DC\",\n    \"/con\": \"\\uA76F\",\n    \"/confettiBall\": \"\\u1F38A\",\n    \"/confoundedFace\": \"\\u1F616\",\n    \"/confusedFace\": \"\\u1F615\",\n    \"/congratulationideographiccircled\": \"\\u3297\",\n    \"/congratulationideographicparen\": \"\\u3237\",\n    \"/congruent\": \"\\u2245\",\n    \"/conicaltaper\": \"\\u2332\",\n    \"/conjunction\": \"\\u260C\",\n    \"/consquareupblack\": \"\\u26FE\",\n    \"/constructionSign\": \"\\u1F6A7\",\n    \"/constructionWorker\": \"\\u1F477\",\n    \"/containsasmembersmall\": \"\\u220D\",\n    \"/containsasnormalsubgroorequalup\": \"\\u22B5\",\n    \"/containsasnormalsubgroup\": \"\\u22B3\",\n    \"/containslonghorizontalstroke\": \"\\u22FA\",\n    \"/containsoverbar\": \"\\u22FD\",\n    \"/containsoverbarsmall\": \"\\u22FE\",\n    \"/containssmallverticalbarhorizontalstroke\": \"\\u22FC\",\n    \"/containsverticalbarhorizontalstroke\": \"\\u22FB\",\n    \"/continuousunderline\": \"\\u2381\",\n    \"/contourintegral\": \"\\u222E\",\n    \"/control\": \"\\u2303\",\n    \"/controlACK\": \"\\u0006\",\n    \"/controlBEL\": \"\\u0007\",\n    \"/controlBS\": \"\\u0008\",\n    \"/controlCAN\": \"\\u0018\",\n    \"/controlCR\": \"\\u000D\",\n    \"/controlDC1\": \"\\u0011\",\n    \"/controlDC2\": \"\\u0012\",\n    \"/controlDC3\": \"\\u0013\",\n    \"/controlDC4\": \"\\u0014\",\n    \"/controlDEL\": \"\\u007F\",\n    \"/controlDLE\": \"\\u0010\",\n    \"/controlEM\": \"\\u0019\",\n    \"/controlENQ\": \"\\u0005\",\n    \"/controlEOT\": \"\\u0004\",\n    \"/controlESC\": \"\\u001B\",\n    \"/controlETB\": \"\\u0017\",\n    \"/controlETX\": \"\\u0003\",\n    \"/controlFF\": \"\\u000C\",\n    \"/controlFS\": \"\\u001C\",\n    \"/controlGS\": \"\\u001D\",\n    \"/controlHT\": \"\\u0009\",\n    \"/controlKnobs\": \"\\u1F39B\",\n    \"/controlLF\": \"\\u000A\",\n    \"/controlNAK\": \"\\u0015\",\n    \"/controlRS\": \"\\u001E\",\n    \"/controlSI\": \"\\u000F\",\n    \"/controlSO\": \"\\u000E\",\n    \"/controlSOT\": \"\\u0002\",\n    \"/controlSTX\": \"\\u0001\",\n    \"/controlSUB\": \"\\u001A\",\n    \"/controlSYN\": \"\\u0016\",\n    \"/controlUS\": \"\\u001F\",\n    \"/controlVT\": \"\\u000B\",\n    \"/convavediamondwhite\": \"\\u27E1\",\n    \"/convenienceStore\": \"\\u1F3EA\",\n    \"/cookedRice\": \"\\u1F35A\",\n    \"/cookie\": \"\\u1F36A\",\n    \"/cooking\": \"\\u1F373\",\n    \"/coolsquare\": \"\\u1F192\",\n    \"/coproductarray\": \"\\u2210\",\n    \"/copyideographiccircled\": \"\\u32A2\",\n    \"/copyright\": \"\\u00A9\",\n    \"/copyrightsans\": \"\\uF8E9\",\n    \"/copyrightserif\": \"\\uF6D9\",\n    \"/cornerbottomleft\": \"\\u231E\",\n    \"/cornerbottomright\": \"\\u231F\",\n    \"/cornerbracketleft\": \"\\u300C\",\n    \"/cornerbracketlefthalfwidth\": \"\\uFF62\",\n    \"/cornerbracketleftvertical\": \"\\uFE41\",\n    \"/cornerbracketright\": \"\\u300D\",\n    \"/cornerbracketrighthalfwidth\": \"\\uFF63\",\n    \"/cornerbracketrightvertical\": \"\\uFE42\",\n    \"/cornerdotupleft\": \"\\u27D4\",\n    \"/cornertopleft\": \"\\u231C\",\n    \"/cornertopright\": \"\\u231D\",\n    \"/coroniseditorial\": \"\\u2E0E\",\n    \"/corporationsquare\": \"\\u337F\",\n    \"/correctideographiccircled\": \"\\u32A3\",\n    \"/corresponds\": \"\\u2258\",\n    \"/cosquare\": \"\\u33C7\",\n    \"/couchAndLamp\": \"\\u1F6CB\",\n    \"/counterbore\": \"\\u2334\",\n    \"/countersink\": \"\\u2335\",\n    \"/coupleHeart\": \"\\u1F491\",\n    \"/coverkgfullwidth\": \"\\u33C6\",\n    \"/coverkgsquare\": \"\\u33C6\",\n    \"/cow\": \"\\u1F404\",\n    \"/cowFace\": \"\\u1F42E\",\n    \"/cpalatalhook\": \"\\uA794\",\n    \"/cparen\": \"\\u249E\",\n    \"/cparenthesized\": \"\\u249E\",\n    \"/creditCard\": \"\\u1F4B3\",\n    \"/crescentMoon\": \"\\u1F319\",\n    \"/creversed\": \"\\u2184\",\n    \"/cricketBatAndBall\": \"\\u1F3CF\",\n    \"/crocodile\": \"\\u1F40A\",\n    \"/cropbottomleft\": \"\\u230D\",\n    \"/cropbottomright\": \"\\u230C\",\n    \"/croptopleft\": \"\\u230F\",\n    \"/croptopright\": \"\\u230E\",\n    \"/crossPommee\": \"\\u1F542\",\n    \"/crossPommeeHalf-circleBelow\": \"\\u1F541\",\n    \"/crossedFlags\": \"\\u1F38C\",\n    \"/crossedswords\": \"\\u2694\",\n    \"/crossinglanes\": \"\\u26CC\",\n    \"/crossmod\": \"\\u02DF\",\n    \"/crossofjerusalem\": \"\\u2629\",\n    \"/crossoflorraine\": \"\\u2628\",\n    \"/crossonshieldblack\": \"\\u26E8\",\n    \"/crown\": \"\\u1F451\",\n    \"/crrn:rupee\": \"\\u20A8\",\n    \"/cruzeiro\": \"\\u20A2\",\n    \"/cryingCatFace\": \"\\u1F63F\",\n    \"/cryingFace\": \"\\u1F622\",\n    \"/crystalBall\": \"\\u1F52E\",\n    \"/cstretched\": \"\\u0297\",\n    \"/cstroke\": \"\\u023C\",\n    \"/cuatrillo\": \"\\uA72D\",\n    \"/cuatrillocomma\": \"\\uA72F\",\n    \"/curlyand\": \"\\u22CF\",\n    \"/curlylogicaland\": \"\\u22CF\",\n    \"/curlylogicalor\": \"\\u22CE\",\n    \"/curlyor\": \"\\u22CE\",\n    \"/currency\": \"\\u00A4\",\n    \"/currencyExchange\": \"\\u1F4B1\",\n    \"/curryAndRice\": \"\\u1F35B\",\n    \"/custard\": \"\\u1F36E\",\n    \"/customeraccountnumber\": \"\\u2449\",\n    \"/customs\": \"\\u1F6C3\",\n    \"/cyclone\": \"\\u1F300\",\n    \"/cylindricity\": \"\\u232D\",\n    \"/cyrBreve\": \"\\uF6D1\",\n    \"/cyrFlex\": \"\\uF6D2\",\n    \"/cyrbreve\": \"\\uF6D4\",\n    \"/cyrflex\": \"\\uF6D5\",\n    \"/d\": \"\\u0064\",\n    \"/daarmenian\": \"\\u0564\",\n    \"/daasusquare\": \"\\u3324\",\n    \"/dabengali\": \"\\u09A6\",\n    \"/dad\": \"\\u0636\",\n    \"/dad.fina\": \"\\uFEBE\",\n    \"/dad.init\": \"\\uFEBF\",\n    \"/dad.init_alefmaksura.fina\": \"\\uFD07\",\n    \"/dad.init_hah.fina\": \"\\uFC23\",\n    \"/dad.init_hah.medi\": \"\\uFCB5\",\n    \"/dad.init_jeem.fina\": \"\\uFC22\",\n    \"/dad.init_jeem.medi\": \"\\uFCB4\",\n    \"/dad.init_khah.fina\": \"\\uFC24\",\n    \"/dad.init_khah.medi\": \"\\uFCB6\",\n    \"/dad.init_khah.medi_meem.medi\": \"\\uFD70\",\n    \"/dad.init_meem.fina\": \"\\uFC25\",\n    \"/dad.init_meem.medi\": \"\\uFCB7\",\n    \"/dad.init_reh.fina\": \"\\uFD10\",\n    \"/dad.init_yeh.fina\": \"\\uFD08\",\n    \"/dad.isol\": \"\\uFEBD\",\n    \"/dad.medi\": \"\\uFEC0\",\n    \"/dad.medi_alefmaksura.fina\": \"\\uFD23\",\n    \"/dad.medi_hah.medi_alefmaksura.fina\": \"\\uFD6E\",\n    \"/dad.medi_hah.medi_yeh.fina\": \"\\uFDAB\",\n    \"/dad.medi_khah.medi_meem.fina\": \"\\uFD6F\",\n    \"/dad.medi_reh.fina\": \"\\uFD2C\",\n    \"/dad.medi_yeh.fina\": \"\\uFD24\",\n    \"/dadarabic\": \"\\u0636\",\n    \"/daddotbelow\": \"\\u06FB\",\n    \"/dadeva\": \"\\u0926\",\n    \"/dadfinalarabic\": \"\\uFEBE\",\n    \"/dadinitialarabic\": \"\\uFEBF\",\n    \"/dadmedialarabic\": \"\\uFEC0\",\n    \"/dafullwidth\": \"\\u3372\",\n    \"/dagesh\": \"\\u05BC\",\n    \"/dagesh:hb\": \"\\u05BC\",\n    \"/dageshhebrew\": \"\\u05BC\",\n    \"/dagger\": \"\\u2020\",\n    \"/daggerKnife\": \"\\u1F5E1\",\n    \"/daggerdbl\": \"\\u2021\",\n    \"/daggerwithguardleft\": \"\\u2E36\",\n    \"/daggerwithguardright\": \"\\u2E37\",\n    \"/dagujarati\": \"\\u0AA6\",\n    \"/dagurmukhi\": \"\\u0A26\",\n    \"/dahal\": \"\\u068C\",\n    \"/dahal.fina\": \"\\uFB85\",\n    \"/dahal.isol\": \"\\uFB84\",\n    \"/dahiragana\": \"\\u3060\",\n    \"/dakatakana\": \"\\u30C0\",\n    \"/dal\": \"\\u062F\",\n    \"/dal.fina\": \"\\uFEAA\",\n    \"/dal.isol\": \"\\uFEA9\",\n    \"/dalInvertedSmallVBelow\": \"\\u075A\",\n    \"/dalTwoDotsVerticallyBelowSmallTah\": \"\\u0759\",\n    \"/dalarabic\": \"\\u062F\",\n    \"/daldotbelow\": \"\\u068A\",\n    \"/daldotbelowtahsmall\": \"\\u068B\",\n    \"/daldownthreedotsabove\": \"\\u068F\",\n    \"/dalet\": \"\\u05D3\",\n    \"/dalet:hb\": \"\\u05D3\",\n    \"/daletdagesh\": \"\\uFB33\",\n    \"/daletdageshhebrew\": \"\\uFB33\",\n    \"/dalethatafpatah\": \"\\u05D3\",\n    \"/dalethatafpatahhebrew\": \"\\u05D3\",\n    \"/dalethatafsegol\": \"\\u05D3\",\n    \"/dalethatafsegolhebrew\": \"\\u05D3\",\n    \"/dalethebrew\": \"\\u05D3\",\n    \"/dalethiriq\": \"\\u05D3\",\n    \"/dalethiriqhebrew\": \"\\u05D3\",\n    \"/daletholam\": \"\\u05D3\",\n    \"/daletholamhebrew\": \"\\u05D3\",\n    \"/daletpatah\": \"\\u05D3\",\n    \"/daletpatahhebrew\": \"\\u05D3\",\n    \"/daletqamats\": \"\\u05D3\",\n    \"/daletqamatshebrew\": \"\\u05D3\",\n    \"/daletqubuts\": \"\\u05D3\",\n    \"/daletqubutshebrew\": \"\\u05D3\",\n    \"/daletsegol\": \"\\u05D3\",\n    \"/daletsegolhebrew\": \"\\u05D3\",\n    \"/daletsheva\": \"\\u05D3\",\n    \"/daletshevahebrew\": \"\\u05D3\",\n    \"/dalettsere\": \"\\u05D3\",\n    \"/dalettserehebrew\": \"\\u05D3\",\n    \"/daletwide:hb\": \"\\uFB22\",\n    \"/daletwithdagesh:hb\": \"\\uFB33\",\n    \"/dalfinalarabic\": \"\\uFEAA\",\n    \"/dalfourdotsabove\": \"\\u0690\",\n    \"/dalinvertedV\": \"\\u06EE\",\n    \"/dalring\": \"\\u0689\",\n    \"/damahaprana\": \"\\uA9A3\",\n    \"/damma\": \"\\u064F\",\n    \"/dammaIsol\": \"\\uFE78\",\n    \"/dammaMedi\": \"\\uFE79\",\n    \"/dammaarabic\": \"\\u064F\",\n    \"/dammalowarabic\": \"\\u064F\",\n    \"/dammareversed\": \"\\u065D\",\n    \"/dammasmall\": \"\\u0619\",\n    \"/dammatan\": \"\\u064C\",\n    \"/dammatanIsol\": \"\\uFE72\",\n    \"/dammatanaltonearabic\": \"\\u064C\",\n    \"/dammatanarabic\": \"\\u064C\",\n    \"/dancer\": \"\\u1F483\",\n    \"/danda\": \"\\u0964\",\n    \"/dango\": \"\\u1F361\",\n    \"/darga:hb\": \"\\u05A7\",\n    \"/dargahebrew\": \"\\u05A7\",\n    \"/dargalefthebrew\": \"\\u05A7\",\n    \"/darkShade\": \"\\u2593\",\n    \"/darkSunglasses\": \"\\u1F576\",\n    \"/dashwithupturnleft\": \"\\u2E43\",\n    \"/dasiacmbcyr\": \"\\u0485\",\n    \"/dasiapneumatacyrilliccmb\": \"\\u0485\",\n    \"/dateseparator\": \"\\u060D\",\n    \"/dayeighteentelegraph\": \"\\u33F1\",\n    \"/dayeighttelegraph\": \"\\u33E7\",\n    \"/dayeleventelegraph\": \"\\u33EA\",\n    \"/dayfifteentelegraph\": \"\\u33EE\",\n    \"/dayfivetelegraph\": \"\\u33E4\",\n    \"/dayfourteentelegraph\": \"\\u33ED\",\n    \"/dayfourtelegraph\": \"\\u33E3\",\n    \"/daynineteentelegraph\": \"\\u33F2\",\n    \"/dayninetelegraph\": \"\\u33E8\",\n    \"/dayonetelegraph\": \"\\u33E0\",\n    \"/dayseventeentelegraph\": \"\\u33F0\",\n    \"/dayseventelegraph\": \"\\u33E6\",\n    \"/daysixteentelegraph\": \"\\u33EF\",\n    \"/daysixtelegraph\": \"\\u33E5\",\n    \"/daytentelegraph\": \"\\u33E9\",\n    \"/daythirteentelegraph\": \"\\u33EC\",\n    \"/daythirtyonetelegraph\": \"\\u33FE\",\n    \"/daythirtytelegraph\": \"\\u33FD\",\n    \"/daythreetelegraph\": \"\\u33E2\",\n    \"/daytwelvetelegraph\": \"\\u33EB\",\n    \"/daytwentyeighttelegraph\": \"\\u33FB\",\n    \"/daytwentyfivetelegraph\": \"\\u33F8\",\n    \"/daytwentyfourtelegraph\": \"\\u33F7\",\n    \"/daytwentyninetelegraph\": \"\\u33FC\",\n    \"/daytwentyonetelegraph\": \"\\u33F4\",\n    \"/daytwentyseventelegraph\": \"\\u33FA\",\n    \"/daytwentysixtelegraph\": \"\\u33F9\",\n    \"/daytwentytelegraph\": \"\\u33F3\",\n    \"/daytwentythreetelegraph\": \"\\u33F6\",\n    \"/daytwentytwotelegraph\": \"\\u33F5\",\n    \"/daytwotelegraph\": \"\\u33E1\",\n    \"/dbdigraph\": \"\\u0238\",\n    \"/dbfullwidth\": \"\\u33C8\",\n    \"/dblGrave\": \"\\uF6D3\",\n    \"/dblanglebracketleft\": \"\\u300A\",\n    \"/dblanglebracketleftvertical\": \"\\uFE3D\",\n    \"/dblanglebracketright\": \"\\u300B\",\n    \"/dblanglebracketrightvertical\": \"\\uFE3E\",\n    \"/dblarchinvertedbelowcmb\": \"\\u032B\",\n    \"/dblarrowNE\": \"\\u21D7\",\n    \"/dblarrowNW\": \"\\u21D6\",\n    \"/dblarrowSE\": \"\\u21D8\",\n    \"/dblarrowSW\": \"\\u21D9\",\n    \"/dblarrowdown\": \"\\u21D3\",\n    \"/dblarrowleft\": \"\\u21D4\",\n    \"/dblarrowleftright\": \"\\u21D4\",\n    \"/dblarrowleftrightstroke\": \"\\u21CE\",\n    \"/dblarrowleftstroke\": \"\\u21CD\",\n    \"/dblarrowright\": \"\\u21D2\",\n    \"/dblarrowrightstroke\": \"\\u21CF\",\n    \"/dblarrowup\": \"\\u21D1\",\n    \"/dblarrowupdown\": \"\\u21D5\",\n    \"/dbldanda\": \"\\u0965\",\n    \"/dbldnhorz\": \"\\u2566\",\n    \"/dbldnleft\": \"\\u2557\",\n    \"/dbldnright\": \"\\u2554\",\n    \"/dblgrave\": \"\\uF6D6\",\n    \"/dblgravecmb\": \"\\u030F\",\n    \"/dblhorz\": \"\\u2550\",\n    \"/dblintegral\": \"\\u222C\",\n    \"/dbllowline\": \"\\u2017\",\n    \"/dbllowlinecmb\": \"\\u0333\",\n    \"/dbloverlinecmb\": \"\\u033F\",\n    \"/dblprimemod\": \"\\u02BA\",\n    \"/dblstrokearrowdown\": \"\\u21DF\",\n    \"/dblstrokearrowup\": \"\\u21DE\",\n    \"/dbluphorz\": \"\\u2569\",\n    \"/dblupleft\": \"\\u255D\",\n    \"/dblupright\": \"\\u255A\",\n    \"/dblvert\": \"\\u2551\",\n    \"/dblverthorz\": \"\\u256C\",\n    \"/dblverticalbar\": \"\\u2016\",\n    \"/dblverticallineabovecmb\": \"\\u030E\",\n    \"/dblvertleft\": \"\\u2563\",\n    \"/dblvertright\": \"\\u2560\",\n    \"/dbopomofo\": \"\\u3109\",\n    \"/dbsquare\": \"\\u33C8\",\n    \"/dcaron\": \"\\u010F\",\n    \"/dcedilla\": \"\\u1E11\",\n    \"/dchecyr\": \"\\u052D\",\n    \"/dcircle\": \"\\u24D3\",\n    \"/dcircumflexbelow\": \"\\u1E13\",\n    \"/dcroat\": \"\\u0111\",\n    \"/dcurl\": \"\\u0221\",\n    \"/ddabengali\": \"\\u09A1\",\n    \"/ddadeva\": \"\\u0921\",\n    \"/ddagujarati\": \"\\u0AA1\",\n    \"/ddagurmukhi\": \"\\u0A21\",\n    \"/ddahal\": \"\\u068D\",\n    \"/ddahal.fina\": \"\\uFB83\",\n    \"/ddahal.isol\": \"\\uFB82\",\n    \"/ddal\": \"\\u0688\",\n    \"/ddal.fina\": \"\\uFB89\",\n    \"/ddal.isol\": \"\\uFB88\",\n    \"/ddalarabic\": \"\\u0688\",\n    \"/ddalfinalarabic\": \"\\uFB89\",\n    \"/ddamahaprana\": \"\\uA99E\",\n    \"/ddblstruckitalic\": \"\\u2146\",\n    \"/dddhadeva\": \"\\u095C\",\n    \"/ddhabengali\": \"\\u09A2\",\n    \"/ddhadeva\": \"\\u0922\",\n    \"/ddhagujarati\": \"\\u0AA2\",\n    \"/ddhagurmukhi\": \"\\u0A22\",\n    \"/ddot\": \"\\u1E0B\",\n    \"/ddotaccent\": \"\\u1E0B\",\n    \"/ddotbelow\": \"\\u1E0D\",\n    \"/decembertelegraph\": \"\\u32CB\",\n    \"/deciduousTree\": \"\\u1F333\",\n    \"/decimalexponent\": \"\\u23E8\",\n    \"/decimalseparatorarabic\": \"\\u066B\",\n    \"/decimalseparatorpersian\": \"\\u066B\",\n    \"/decreaseFontSize\": \"\\u1F5DB\",\n    \"/decyr\": \"\\u0434\",\n    \"/decyrillic\": \"\\u0434\",\n    \"/degree\": \"\\u00B0\",\n    \"/degreecelsius\": \"\\u2103\",\n    \"/degreefahrenheit\": \"\\u2109\",\n    \"/dehi:hb\": \"\\u05AD\",\n    \"/dehihebrew\": \"\\u05AD\",\n    \"/dehiragana\": \"\\u3067\",\n    \"/deicoptic\": \"\\u03EF\",\n    \"/dekatakana\": \"\\u30C7\",\n    \"/dekomicyr\": \"\\u0501\",\n    \"/deldiaeresisfunc\": \"\\u2362\",\n    \"/deleteleft\": \"\\u232B\",\n    \"/deleteright\": \"\\u2326\",\n    \"/deliveryTruck\": \"\\u1F69A\",\n    \"/delstilefunc\": \"\\u2352\",\n    \"/delta\": \"\\u03B4\",\n    \"/deltaequal\": \"\\u225C\",\n    \"/deltastilefunc\": \"\\u234B\",\n    \"/deltaturned\": \"\\u018D\",\n    \"/deltaunderlinefunc\": \"\\u2359\",\n    \"/deltildefunc\": \"\\u236B\",\n    \"/denominatorminusonenumeratorbengali\": \"\\u09F8\",\n    \"/dentistrybottomverticalleft\": \"\\u23CC\",\n    \"/dentistrybottomverticalright\": \"\\u23BF\",\n    \"/dentistrycircledownhorizontal\": \"\\u23C1\",\n    \"/dentistrycircleuphorizontal\": \"\\u23C2\",\n    \"/dentistrycirclevertical\": \"\\u23C0\",\n    \"/dentistrydownhorizontal\": \"\\u23C9\",\n    \"/dentistrytopverticalleft\": \"\\u23CB\",\n    \"/dentistrytopverticalright\": \"\\u23BE\",\n    \"/dentistrytriangledownhorizontal\": \"\\u23C4\",\n    \"/dentistrytriangleuphorizontal\": \"\\u23C5\",\n    \"/dentistrytrianglevertical\": \"\\u23C3\",\n    \"/dentistryuphorizontal\": \"\\u23CA\",\n    \"/dentistrywavedownhorizontal\": \"\\u23C7\",\n    \"/dentistrywaveuphorizontal\": \"\\u23C8\",\n    \"/dentistrywavevertical\": \"\\u23C6\",\n    \"/departmentStore\": \"\\u1F3EC\",\n    \"/derelictHouseBuilding\": \"\\u1F3DA\",\n    \"/desert\": \"\\u1F3DC\",\n    \"/desertIsland\": \"\\u1F3DD\",\n    \"/desisquare\": \"\\u3325\",\n    \"/desktopComputer\": \"\\u1F5A5\",\n    \"/desktopWindow\": \"\\u1F5D4\",\n    \"/deva:a\": \"\\u0905\",\n    \"/deva:aa\": \"\\u0906\",\n    \"/deva:aasign\": \"\\u093E\",\n    \"/deva:abbreviation\": \"\\u0970\",\n    \"/deva:acandra\": \"\\u0972\",\n    \"/deva:acute\": \"\\u0954\",\n    \"/deva:ai\": \"\\u0910\",\n    \"/deva:aisign\": \"\\u0948\",\n    \"/deva:anudatta\": \"\\u0952\",\n    \"/deva:anusvara\": \"\\u0902\",\n    \"/deva:ashort\": \"\\u0904\",\n    \"/deva:au\": \"\\u0914\",\n    \"/deva:ausign\": \"\\u094C\",\n    \"/deva:avagraha\": \"\\u093D\",\n    \"/deva:aw\": \"\\u0975\",\n    \"/deva:awsign\": \"\\u094F\",\n    \"/deva:ba\": \"\\u092C\",\n    \"/deva:bba\": \"\\u097F\",\n    \"/deva:bha\": \"\\u092D\",\n    \"/deva:ca\": \"\\u091A\",\n    \"/deva:candrabindu\": \"\\u0901\",\n    \"/deva:candrabinduinverted\": \"\\u0900\",\n    \"/deva:cha\": \"\\u091B\",\n    \"/deva:da\": \"\\u0926\",\n    \"/deva:danda\": \"\\u0964\",\n    \"/deva:dbldanda\": \"\\u0965\",\n    \"/deva:dda\": \"\\u0921\",\n    \"/deva:ddda\": \"\\u097E\",\n    \"/deva:dddha\": \"\\u095C\",\n    \"/deva:ddha\": \"\\u0922\",\n    \"/deva:dha\": \"\\u0927\",\n    \"/deva:dothigh\": \"\\u0971\",\n    \"/deva:e\": \"\\u090F\",\n    \"/deva:ecandra\": \"\\u090D\",\n    \"/deva:eight\": \"\\u096E\",\n    \"/deva:eshort\": \"\\u090E\",\n    \"/deva:esign\": \"\\u0947\",\n    \"/deva:esigncandra\": \"\\u0945\",\n    \"/deva:esignprishthamatra\": \"\\u094E\",\n    \"/deva:esignshort\": \"\\u0946\",\n    \"/deva:fa\": \"\\u095E\",\n    \"/deva:five\": \"\\u096B\",\n    \"/deva:four\": \"\\u096A\",\n    \"/deva:ga\": \"\\u0917\",\n    \"/deva:gga\": \"\\u097B\",\n    \"/deva:gha\": \"\\u0918\",\n    \"/deva:ghha\": \"\\u095A\",\n    \"/deva:glottalstop\": \"\\u097D\",\n    \"/deva:grave\": \"\\u0953\",\n    \"/deva:ha\": \"\\u0939\",\n    \"/deva:i\": \"\\u0907\",\n    \"/deva:ii\": \"\\u0908\",\n    \"/deva:iisign\": \"\\u0940\",\n    \"/deva:isign\": \"\\u093F\",\n    \"/deva:ja\": \"\\u091C\",\n    \"/deva:jha\": \"\\u091D\",\n    \"/deva:jja\": \"\\u097C\",\n    \"/deva:ka\": \"\\u0915\",\n    \"/deva:kha\": \"\\u0916\",\n    \"/deva:khha\": \"\\u0959\",\n    \"/deva:la\": \"\\u0932\",\n    \"/deva:lla\": \"\\u0933\",\n    \"/deva:llla\": \"\\u0934\",\n    \"/deva:llvocal\": \"\\u0961\",\n    \"/deva:llvocalsign\": \"\\u0963\",\n    \"/deva:lvocal\": \"\\u090C\",\n    \"/deva:lvocalsign\": \"\\u0962\",\n    \"/deva:ma\": \"\\u092E\",\n    \"/deva:marwaridda\": \"\\u0978\",\n    \"/deva:na\": \"\\u0928\",\n    \"/deva:nga\": \"\\u0919\",\n    \"/deva:nine\": \"\\u096F\",\n    \"/deva:nna\": \"\\u0923\",\n    \"/deva:nnna\": \"\\u0929\",\n    \"/deva:nukta\": \"\\u093C\",\n    \"/deva:nya\": \"\\u091E\",\n    \"/deva:o\": \"\\u0913\",\n    \"/deva:ocandra\": \"\\u0911\",\n    \"/deva:oe\": \"\\u0973\",\n    \"/deva:oesign\": \"\\u093A\",\n    \"/deva:om\": \"\\u0950\",\n    \"/deva:one\": \"\\u0967\",\n    \"/deva:ooe\": \"\\u0974\",\n    \"/deva:ooesign\": \"\\u093B\",\n    \"/deva:oshort\": \"\\u0912\",\n    \"/deva:osign\": \"\\u094B\",\n    \"/deva:osigncandra\": \"\\u0949\",\n    \"/deva:osignshort\": \"\\u094A\",\n    \"/deva:pa\": \"\\u092A\",\n    \"/deva:pha\": \"\\u092B\",\n    \"/deva:qa\": \"\\u0958\",\n    \"/deva:ra\": \"\\u0930\",\n    \"/deva:rha\": \"\\u095D\",\n    \"/deva:rra\": \"\\u0931\",\n    \"/deva:rrvocal\": \"\\u0960\",\n    \"/deva:rrvocalsign\": \"\\u0944\",\n    \"/deva:rvocal\": \"\\u090B\",\n    \"/deva:rvocalsign\": \"\\u0943\",\n    \"/deva:sa\": \"\\u0938\",\n    \"/deva:seven\": \"\\u096D\",\n    \"/deva:sha\": \"\\u0936\",\n    \"/deva:signelongcandra\": \"\\u0955\",\n    \"/deva:six\": \"\\u096C\",\n    \"/deva:ssa\": \"\\u0937\",\n    \"/deva:ta\": \"\\u0924\",\n    \"/deva:tha\": \"\\u0925\",\n    \"/deva:three\": \"\\u0969\",\n    \"/deva:tta\": \"\\u091F\",\n    \"/deva:ttha\": \"\\u0920\",\n    \"/deva:two\": \"\\u0968\",\n    \"/deva:u\": \"\\u0909\",\n    \"/deva:udatta\": \"\\u0951\",\n    \"/deva:ue\": \"\\u0976\",\n    \"/deva:uesign\": \"\\u0956\",\n    \"/deva:usign\": \"\\u0941\",\n    \"/deva:uu\": \"\\u090A\",\n    \"/deva:uue\": \"\\u0977\",\n    \"/deva:uuesign\": \"\\u0957\",\n    \"/deva:uusign\": \"\\u0942\",\n    \"/deva:va\": \"\\u0935\",\n    \"/deva:virama\": \"\\u094D\",\n    \"/deva:visarga\": \"\\u0903\",\n    \"/deva:ya\": \"\\u092F\",\n    \"/deva:yaheavy\": \"\\u097A\",\n    \"/deva:yya\": \"\\u095F\",\n    \"/deva:za\": \"\\u095B\",\n    \"/deva:zero\": \"\\u0966\",\n    \"/deva:zha\": \"\\u0979\",\n    \"/dezh\": \"\\u02A4\",\n    \"/dfemaledbl\": \"\\u26A2\",\n    \"/dhabengali\": \"\\u09A7\",\n    \"/dhadeva\": \"\\u0927\",\n    \"/dhagujarati\": \"\\u0AA7\",\n    \"/dhagurmukhi\": \"\\u0A27\",\n    \"/dhook\": \"\\u0257\",\n    \"/diaeresisgreaterfunc\": \"\\u2369\",\n    \"/dialytikatonos\": \"\\u0385\",\n    \"/dialytikatonoscmb\": \"\\u0344\",\n    \"/diametersign\": \"\\u2300\",\n    \"/diamond\": \"\\u2666\",\n    \"/diamondShapeADotInside\": \"\\u1F4A0\",\n    \"/diamondinsquarewhite\": \"\\u26CB\",\n    \"/diamondoperator\": \"\\u22C4\",\n    \"/diamondsuitwhite\": \"\\u2662\",\n    \"/diamondunderlinefunc\": \"\\u235A\",\n    \"/diamondwhitewithdiamondsmallblack\": \"\\u25C8\",\n    \"/diefive\": \"\\u2684\",\n    \"/diefour\": \"\\u2683\",\n    \"/dieone\": \"\\u2680\",\n    \"/dieresis\": \"\\u00A8\",\n    \"/dieresisacute\": \"\\uF6D7\",\n    \"/dieresisbelowcmb\": \"\\u0324\",\n    \"/dieresiscmb\": \"\\u0308\",\n    \"/dieresisgrave\": \"\\uF6D8\",\n    \"/dieresistilde\": \"\\u1FC1\",\n    \"/dieresistonos\": \"\\u0385\",\n    \"/dieselLocomotive\": \"\\u1F6F2\",\n    \"/diesix\": \"\\u2685\",\n    \"/diethree\": \"\\u2682\",\n    \"/dietwo\": \"\\u2681\",\n    \"/differencebetween\": \"\\u224F\",\n    \"/digamma\": \"\\u03DD\",\n    \"/digammapamphylian\": \"\\u0377\",\n    \"/digramgreateryang\": \"\\u268C\",\n    \"/digramgreateryin\": \"\\u268F\",\n    \"/digramlesseryang\": \"\\u268E\",\n    \"/digramlesseryin\": \"\\u268D\",\n    \"/dihiragana\": \"\\u3062\",\n    \"/dikatakana\": \"\\u30C2\",\n    \"/dimensionorigin\": \"\\u2331\",\n    \"/dingbatSAns-serifzerocircle\": \"\\u1F10B\",\n    \"/dingbatSAns-serifzerocircleblack\": \"\\u1F10C\",\n    \"/dinsular\": \"\\uA77A\",\n    \"/directHit\": \"\\u1F3AF\",\n    \"/directcurrentformtwo\": \"\\u2393\",\n    \"/dirgamurevowel\": \"\\uA9BB\",\n    \"/disabledcar\": \"\\u26CD\",\n    \"/disappointedButRelievedFace\": \"\\u1F625\",\n    \"/disappointedFace\": \"\\u1F61E\",\n    \"/discontinuousunderline\": \"\\u2382\",\n    \"/dittomark\": \"\\u3003\",\n    \"/divide\": \"\\u00F7\",\n    \"/divides\": \"\\u2223\",\n    \"/divisionslash\": \"\\u2215\",\n    \"/divisiontimes\": \"\\u22C7\",\n    \"/divorce\": \"\\u26AE\",\n    \"/dizzy\": \"\\u1F4AB\",\n    \"/dizzyFace\": \"\\u1F635\",\n    \"/djecyr\": \"\\u0452\",\n    \"/djecyrillic\": \"\\u0452\",\n    \"/djekomicyr\": \"\\u0503\",\n    \"/dkshade\": \"\\u2593\",\n    \"/dlfullwidth\": \"\\u3397\",\n    \"/dlinebelow\": \"\\u1E0F\",\n    \"/dlogicalorsquare\": \"\\u27CF\",\n    \"/dlogicalsquare\": \"\\u27CE\",\n    \"/dlsquare\": \"\\u3397\",\n    \"/dm2fullwidth\": \"\\u3378\",\n    \"/dm3fullwidth\": \"\\u3379\",\n    \"/dmacron\": \"\\u0111\",\n    \"/dmaledbl\": \"\\u26A3\",\n    \"/dmfullwidth\": \"\\u3377\",\n    \"/dmonospace\": \"\\uFF44\",\n    \"/dnblock\": \"\\u2584\",\n    \"/dndblhorzsng\": \"\\u2565\",\n    \"/dndblleftsng\": \"\\u2556\",\n    \"/dndblrightsng\": \"\\u2553\",\n    \"/dngb:airplane\": \"\\u2708\",\n    \"/dngb:arrowfeatheredblackNE\": \"\\u27B6\",\n    \"/dngb:arrowfeatheredblackSE\": \"\\u27B4\",\n    \"/dngb:arrowfeatheredblackheavyNE\": \"\\u27B9\",\n    \"/dngb:arrowfeatheredblackheavySE\": \"\\u27B7\",\n    \"/dngb:arrowheadrightblack\": \"\\u27A4\",\n    \"/dngb:arrowheadrightthreeDbottomlight\": \"\\u27A3\",\n    \"/dngb:arrowheadrightthreeDtoplight\": \"\\u27A2\",\n    \"/dngb:arrowheavyNE\": \"\\u279A\",\n    \"/dngb:arrowheavySE\": \"\\u2798\",\n    \"/dngb:arrowrightbacktiltedshadowedwhite\": \"\\u27AB\",\n    \"/dngb:arrowrightblack\": \"\\u27A1\",\n    \"/dngb:arrowrightcircledwhiteheavy\": \"\\u27B2\",\n    \"/dngb:arrowrightcurvedownblackheavy\": \"\\u27A5\",\n    \"/dngb:arrowrightcurveupblackheavy\": \"\\u27A6\",\n    \"/dngb:arrowrightfeatheredblack\": \"\\u27B5\",\n    \"/dngb:arrowrightfeatheredblackheavy\": \"\\u27B8\",\n    \"/dngb:arrowrightfeatheredwhite\": \"\\u27B3\",\n    \"/dngb:arrowrightfronttiltedshadowedwhite\": \"\\u27AC\",\n    \"/dngb:arrowrightheavy\": \"\\u2799\",\n    \"/dngb:arrowrightleftshadedwhite\": \"\\u27AA\",\n    \"/dngb:arrowrightoutlinedopen\": \"\\u27BE\",\n    \"/dngb:arrowrightpointed\": \"\\u279B\",\n    \"/dngb:arrowrightpointedblackheavy\": \"\\u27A8\",\n    \"/dngb:arrowrightrightshadedwhite\": \"\\u27A9\",\n    \"/dngb:arrowrightroundheavy\": \"\\u279C\",\n    \"/dngb:arrowrightsquatblack\": \"\\u27A7\",\n    \"/dngb:arrowrighttriangle\": \"\\u279D\",\n    \"/dngb:arrowrighttriangledashed\": \"\\u279F\",\n    \"/dngb:arrowrighttriangledashedheavy\": \"\\u27A0\",\n    \"/dngb:arrowrighttriangleheavy\": \"\\u279E\",\n    \"/dngb:arrowrightwedge\": \"\\u27BC\",\n    \"/dngb:arrowrightwedgeheavy\": \"\\u27BD\",\n    \"/dngb:arrowrightwideheavy\": \"\\u2794\",\n    \"/dngb:arrowshadowrightlowerwhiteheavy\": \"\\u27AD\",\n    \"/dngb:arrowshadowrightnotchedlowerwhite\": \"\\u27AF\",\n    \"/dngb:arrowshadowrightnotchedupperwhite\": \"\\u27B1\",\n    \"/dngb:arrowshadowrightupperwhiteheavy\": \"\\u27AE\",\n    \"/dngb:arrowteardropright\": \"\\u27BA\",\n    \"/dngb:arrowteardroprightheavy\": \"\\u27BB\",\n    \"/dngb:asteriskballoon\": \"\\u2749\",\n    \"/dngb:asteriskballoonfour\": \"\\u2723\",\n    \"/dngb:asteriskballoonheavyfour\": \"\\u2724\",\n    \"/dngb:asteriskcentreopen\": \"\\u2732\",\n    \"/dngb:asteriskclubfour\": \"\\u2725\",\n    \"/dngb:asteriskheavy\": \"\\u2731\",\n    \"/dngb:asteriskpointedsixteen\": \"\\u273A\",\n    \"/dngb:asteriskteardrop\": \"\\u273B\",\n    \"/dngb:asteriskteardropcentreopen\": \"\\u273C\",\n    \"/dngb:asteriskteardropfour\": \"\\u2722\",\n    \"/dngb:asteriskteardropheavy\": \"\\u273D\",\n    \"/dngb:asteriskteardroppinwheelheavy\": \"\\u2743\",\n    \"/dngb:asteriskteardroppropellereight\": \"\\u274A\",\n    \"/dngb:asteriskteardroppropellerheavyeight\": \"\\u274B\",\n    \"/dngb:ballotx\": \"\\u2717\",\n    \"/dngb:ballotxheavy\": \"\\u2718\",\n    \"/dngb:bracketleftpointedangleheavyornament\": \"\\u2770\",\n    \"/dngb:bracketleftpointedanglemediumornament\": \"\\u276C\",\n    \"/dngb:bracketrightpointedangleheavyornament\": \"\\u2771\",\n    \"/dngb:bracketrightpointedanglemediumornament\": \"\\u276D\",\n    \"/dngb:bracketshellleftlightornament\": \"\\u2772\",\n    \"/dngb:bracketshellrightlightornament\": \"\\u2773\",\n    \"/dngb:check\": \"\\u2713\",\n    \"/dngb:checkheavy\": \"\\u2714\",\n    \"/dngb:checkwhiteheavy\": \"\\u2705\",\n    \"/dngb:chevronsnowflakeheavy\": \"\\u2746\",\n    \"/dngb:circleshadowedwhite\": \"\\u274D\",\n    \"/dngb:commaheavydoubleornament\": \"\\u275E\",\n    \"/dngb:commaheavydoubleturnedornament\": \"\\u275D\",\n    \"/dngb:commaheavyornament\": \"\\u275C\",\n    \"/dngb:commaheavyturnedornament\": \"\\u275B\",\n    \"/dngb:compasstarpointedblackeight\": \"\\u2737\",\n    \"/dngb:compasstarpointedblackheavyeight\": \"\\u2738\",\n    \"/dngb:cross\": \"\\u274C\",\n    \"/dngb:crosscentreopen\": \"\\u271B\",\n    \"/dngb:crosscentreopenheavy\": \"\\u271C\",\n    \"/dngb:curlybracketleftmediumornament\": \"\\u2774\",\n    \"/dngb:curlybracketrightmediumornament\": \"\\u2775\",\n    \"/dngb:curlyloop\": \"\\u27B0\",\n    \"/dngb:curlyloopdouble\": \"\\u27BF\",\n    \"/dngb:curvedstemparagraphsignornament\": \"\\u2761\",\n    \"/dngb:diamondminusxblackwhite\": \"\\u2756\",\n    \"/dngb:divisionsignheavy\": \"\\u2797\",\n    \"/dngb:eightnegativecircled\": \"\\u277D\",\n    \"/dngb:eightsanscircled\": \"\\u2787\",\n    \"/dngb:eightsansnegativecircled\": \"\\u2791\",\n    \"/dngb:envelope\": \"\\u2709\",\n    \"/dngb:exclamationheavy\": \"\\u2757\",\n    \"/dngb:exclamationheavyornament\": \"\\u2762\",\n    \"/dngb:exclamationwhiteornament\": \"\\u2755\",\n    \"/dngb:fivenegativecircled\": \"\\u277A\",\n    \"/dngb:fivesanscircled\": \"\\u2784\",\n    \"/dngb:fivesansnegativecircled\": \"\\u278E\",\n    \"/dngb:floralheart\": \"\\u2766\",\n    \"/dngb:floralheartbulletrotated\": \"\\u2767\",\n    \"/dngb:floretteblack\": \"\\u273F\",\n    \"/dngb:floretteoutlinedpetalledblackeight\": \"\\u2741\",\n    \"/dngb:florettepetalledblackwhitesix\": \"\\u273E\",\n    \"/dngb:florettewhite\": \"\\u2740\",\n    \"/dngb:fournegativecircled\": \"\\u2779\",\n    \"/dngb:foursanscircled\": \"\\u2783\",\n    \"/dngb:foursansnegativecircled\": \"\\u278D\",\n    \"/dngb:greekcrossheavy\": \"\\u271A\",\n    \"/dngb:greekcrossoutlined\": \"\\u2719\",\n    \"/dngb:heartblackheavy\": \"\\u2764\",\n    \"/dngb:heartbulletrotatedblackheavy\": \"\\u2765\",\n    \"/dngb:heartexclamationheavyornament\": \"\\u2763\",\n    \"/dngb:hvictory\": \"\\u270C\",\n    \"/dngb:hwriting\": \"\\u270D\",\n    \"/dngb:latincross\": \"\\u271D\",\n    \"/dngb:latincrossoutlined\": \"\\u271F\",\n    \"/dngb:latincrossshadowedwhite\": \"\\u271E\",\n    \"/dngb:lowcommaheavydoubleornament\": \"\\u2760\",\n    \"/dngb:lowcommaheavyornament\": \"\\u275F\",\n    \"/dngb:maltesecross\": \"\\u2720\",\n    \"/dngb:minussignheavy\": \"\\u2796\",\n    \"/dngb:multiplicationx\": \"\\u2715\",\n    \"/dngb:multiplicationxheavy\": \"\\u2716\",\n    \"/dngb:nibblack\": \"\\u2712\",\n    \"/dngb:nibwhite\": \"\\u2711\",\n    \"/dngb:ninenegativecircled\": \"\\u277E\",\n    \"/dngb:ninesanscircled\": \"\\u2788\",\n    \"/dngb:ninesansnegativecircled\": \"\\u2792\",\n    \"/dngb:onenegativecircled\": \"\\u2776\",\n    \"/dngb:onesanscircled\": \"\\u2780\",\n    \"/dngb:onesansnegativecircled\": \"\\u278A\",\n    \"/dngb:parenthesisleftflattenedmediumornament\": \"\\u276A\",\n    \"/dngb:parenthesisleftmediumornament\": \"\\u2768\",\n    \"/dngb:parenthesisrightflattenedmediumornament\": \"\\u276B\",\n    \"/dngb:parenthesisrightmediumornament\": \"\\u2769\",\n    \"/dngb:pencil\": \"\\u270F\",\n    \"/dngb:pencillowerright\": \"\\u270E\",\n    \"/dngb:pencilupperright\": \"\\u2710\",\n    \"/dngb:plussignheavy\": \"\\u2795\",\n    \"/dngb:questionblackornament\": \"\\u2753\",\n    \"/dngb:questionwhiteornament\": \"\\u2754\",\n    \"/dngb:quotationleftpointedangleheavyornament\": \"\\u276E\",\n    \"/dngb:quotationrightpointedangleheavyornament\": \"\\u276F\",\n    \"/dngb:raisedfist\": \"\\u270A\",\n    \"/dngb:raisedh\": \"\\u270B\",\n    \"/dngb:safetyscissorsblack\": \"\\u2700\",\n    \"/dngb:scissorsblack\": \"\\u2702\",\n    \"/dngb:scissorslowerblade\": \"\\u2703\",\n    \"/dngb:scissorsupperblade\": \"\\u2701\",\n    \"/dngb:scissorswhite\": \"\\u2704\",\n    \"/dngb:sevennegativecircled\": \"\\u277C\",\n    \"/dngb:sevensanscircled\": \"\\u2786\",\n    \"/dngb:sevensansnegativecircled\": \"\\u2790\",\n    \"/dngb:sixnegativecircled\": \"\\u277B\",\n    \"/dngb:sixsanscircled\": \"\\u2785\",\n    \"/dngb:sixsansnegativecircled\": \"\\u278F\",\n    \"/dngb:snowflake\": \"\\u2744\",\n    \"/dngb:snowflaketight\": \"\\u2745\",\n    \"/dngb:sparkle\": \"\\u2747\",\n    \"/dngb:sparkleheavy\": \"\\u2748\",\n    \"/dngb:sparkles\": \"\\u2728\",\n    \"/dngb:spokedasteriskeight\": \"\\u2733\",\n    \"/dngb:squaredcrossnegative\": \"\\u274E\",\n    \"/dngb:squarelowerrightshadowedwhite\": \"\\u2751\",\n    \"/dngb:squareshadowlowerrightwhite\": \"\\u274F\",\n    \"/dngb:squareshadowupperrightwhite\": \"\\u2750\",\n    \"/dngb:squareupperrightshadowedwhite\": \"\\u2752\",\n    \"/dngb:starcentreblackwhite\": \"\\u272C\",\n    \"/dngb:starcentreopenblack\": \"\\u272B\",\n    \"/dngb:starcentreopenpointedcircledeight\": \"\\u2742\",\n    \"/dngb:starcircledwhite\": \"\\u272A\",\n    \"/dngb:starofdavid\": \"\\u2721\",\n    \"/dngb:staroutlinedblack\": \"\\u272D\",\n    \"/dngb:staroutlinedblackheavy\": \"\\u272E\",\n    \"/dngb:staroutlinedstresswhite\": \"\\u2729\",\n    \"/dngb:starpinwheel\": \"\\u272F\",\n    \"/dngb:starpointedblackeight\": \"\\u2734\",\n    \"/dngb:starpointedblackfour\": \"\\u2726\",\n    \"/dngb:starpointedblacksix\": \"\\u2736\",\n    \"/dngb:starpointedblacktwelve\": \"\\u2739\",\n    \"/dngb:starpointedpinwheeleight\": \"\\u2735\",\n    \"/dngb:starpointedwhitefour\": \"\\u2727\",\n    \"/dngb:starshadowedwhite\": \"\\u2730\",\n    \"/dngb:tapedrive\": \"\\u2707\",\n    \"/dngb:telephonelocationsign\": \"\\u2706\",\n    \"/dngb:tennegativecircled\": \"\\u277F\",\n    \"/dngb:tensanscircled\": \"\\u2789\",\n    \"/dngb:tensansnegativecircled\": \"\\u2793\",\n    \"/dngb:threenegativecircled\": \"\\u2778\",\n    \"/dngb:threesanscircled\": \"\\u2782\",\n    \"/dngb:threesansnegativecircled\": \"\\u278C\",\n    \"/dngb:twonegativecircled\": \"\\u2777\",\n    \"/dngb:twosanscircled\": \"\\u2781\",\n    \"/dngb:twosansnegativecircled\": \"\\u278B\",\n    \"/dngb:verticalbarheavy\": \"\\u275A\",\n    \"/dngb:verticalbarlight\": \"\\u2758\",\n    \"/dngb:verticalbarmedium\": \"\\u2759\",\n    \"/dnheavyhorzlight\": \"\\u2530\",\n    \"/dnheavyleftlight\": \"\\u2512\",\n    \"/dnheavyleftuplight\": \"\\u2527\",\n    \"/dnheavyrightlight\": \"\\u250E\",\n    \"/dnheavyrightuplight\": \"\\u251F\",\n    \"/dnheavyuphorzlight\": \"\\u2541\",\n    \"/dnlighthorzheavy\": \"\\u252F\",\n    \"/dnlightleftheavy\": \"\\u2511\",\n    \"/dnlightleftupheavy\": \"\\u2529\",\n    \"/dnlightrightheavy\": \"\\u250D\",\n    \"/dnlightrightupheavy\": \"\\u2521\",\n    \"/dnlightuphorzheavy\": \"\\u2547\",\n    \"/dnsnghorzdbl\": \"\\u2564\",\n    \"/dnsngleftdbl\": \"\\u2555\",\n    \"/dnsngrightdbl\": \"\\u2552\",\n    \"/doNotLitter\": \"\\u1F6AF\",\n    \"/dochadathai\": \"\\u0E0E\",\n    \"/document\": \"\\u1F5CE\",\n    \"/documentPicture\": \"\\u1F5BB\",\n    \"/documentText\": \"\\u1F5B9\",\n    \"/documentTextAndPicture\": \"\\u1F5BA\",\n    \"/dodekthai\": \"\\u0E14\",\n    \"/doesnotcontainasnormalsubgroorequalup\": \"\\u22ED\",\n    \"/doesnotcontainasnormalsubgroup\": \"\\u22EB\",\n    \"/doesnotdivide\": \"\\u2224\",\n    \"/doesnotforce\": \"\\u22AE\",\n    \"/doesnotprecede\": \"\\u2280\",\n    \"/doesnotprecedeorequal\": \"\\u22E0\",\n    \"/doesnotprove\": \"\\u22AC\",\n    \"/doesnotsucceed\": \"\\u2281\",\n    \"/doesnotsucceedorequal\": \"\\u22E1\",\n    \"/dog\": \"\\u1F415\",\n    \"/dogFace\": \"\\u1F436\",\n    \"/dohiragana\": \"\\u3069\",\n    \"/dokatakana\": \"\\u30C9\",\n    \"/dollar\": \"\\u0024\",\n    \"/dollarinferior\": \"\\uF6E3\",\n    \"/dollarmonospace\": \"\\uFF04\",\n    \"/dollaroldstyle\": \"\\uF724\",\n    \"/dollarsmall\": \"\\uFE69\",\n    \"/dollarsuperior\": \"\\uF6E4\",\n    \"/dolphin\": \"\\u1F42C\",\n    \"/dominohorizontal_00_00\": \"\\u1F031\",\n    \"/dominohorizontal_00_01\": \"\\u1F032\",\n    \"/dominohorizontal_00_02\": \"\\u1F033\",\n    \"/dominohorizontal_00_03\": \"\\u1F034\",\n    \"/dominohorizontal_00_04\": \"\\u1F035\",\n    \"/dominohorizontal_00_05\": \"\\u1F036\",\n    \"/dominohorizontal_00_06\": \"\\u1F037\",\n    \"/dominohorizontal_01_00\": \"\\u1F038\",\n    \"/dominohorizontal_01_01\": \"\\u1F039\",\n    \"/dominohorizontal_01_02\": \"\\u1F03A\",\n    \"/dominohorizontal_01_03\": \"\\u1F03B\",\n    \"/dominohorizontal_01_04\": \"\\u1F03C\",\n    \"/dominohorizontal_01_05\": \"\\u1F03D\",\n    \"/dominohorizontal_01_06\": \"\\u1F03E\",\n    \"/dominohorizontal_02_00\": \"\\u1F03F\",\n    \"/dominohorizontal_02_01\": \"\\u1F040\",\n    \"/dominohorizontal_02_02\": \"\\u1F041\",\n    \"/dominohorizontal_02_03\": \"\\u1F042\",\n    \"/dominohorizontal_02_04\": \"\\u1F043\",\n    \"/dominohorizontal_02_05\": \"\\u1F044\",\n    \"/dominohorizontal_02_06\": \"\\u1F045\",\n    \"/dominohorizontal_03_00\": \"\\u1F046\",\n    \"/dominohorizontal_03_01\": \"\\u1F047\",\n    \"/dominohorizontal_03_02\": \"\\u1F048\",\n    \"/dominohorizontal_03_03\": \"\\u1F049\",\n    \"/dominohorizontal_03_04\": \"\\u1F04A\",\n    \"/dominohorizontal_03_05\": \"\\u1F04B\",\n    \"/dominohorizontal_03_06\": \"\\u1F04C\",\n    \"/dominohorizontal_04_00\": \"\\u1F04D\",\n    \"/dominohorizontal_04_01\": \"\\u1F04E\",\n    \"/dominohorizontal_04_02\": \"\\u1F04F\",\n    \"/dominohorizontal_04_03\": \"\\u1F050\",\n    \"/dominohorizontal_04_04\": \"\\u1F051\",\n    \"/dominohorizontal_04_05\": \"\\u1F052\",\n    \"/dominohorizontal_04_06\": \"\\u1F053\",\n    \"/dominohorizontal_05_00\": \"\\u1F054\",\n    \"/dominohorizontal_05_01\": \"\\u1F055\",\n    \"/dominohorizontal_05_02\": \"\\u1F056\",\n    \"/dominohorizontal_05_03\": \"\\u1F057\",\n    \"/dominohorizontal_05_04\": \"\\u1F058\",\n    \"/dominohorizontal_05_05\": \"\\u1F059\",\n    \"/dominohorizontal_05_06\": \"\\u1F05A\",\n    \"/dominohorizontal_06_00\": \"\\u1F05B\",\n    \"/dominohorizontal_06_01\": \"\\u1F05C\",\n    \"/dominohorizontal_06_02\": \"\\u1F05D\",\n    \"/dominohorizontal_06_03\": \"\\u1F05E\",\n    \"/dominohorizontal_06_04\": \"\\u1F05F\",\n    \"/dominohorizontal_06_05\": \"\\u1F060\",\n    \"/dominohorizontal_06_06\": \"\\u1F061\",\n    \"/dominohorizontalback\": \"\\u1F030\",\n    \"/dominovertical_00_00\": \"\\u1F063\",\n    \"/dominovertical_00_01\": \"\\u1F064\",\n    \"/dominovertical_00_02\": \"\\u1F065\",\n    \"/dominovertical_00_03\": \"\\u1F066\",\n    \"/dominovertical_00_04\": \"\\u1F067\",\n    \"/dominovertical_00_05\": \"\\u1F068\",\n    \"/dominovertical_00_06\": \"\\u1F069\",\n    \"/dominovertical_01_00\": \"\\u1F06A\",\n    \"/dominovertical_01_01\": \"\\u1F06B\",\n    \"/dominovertical_01_02\": \"\\u1F06C\",\n    \"/dominovertical_01_03\": \"\\u1F06D\",\n    \"/dominovertical_01_04\": \"\\u1F06E\",\n    \"/dominovertical_01_05\": \"\\u1F06F\",\n    \"/dominovertical_01_06\": \"\\u1F070\",\n    \"/dominovertical_02_00\": \"\\u1F071\",\n    \"/dominovertical_02_01\": \"\\u1F072\",\n    \"/dominovertical_02_02\": \"\\u1F073\",\n    \"/dominovertical_02_03\": \"\\u1F074\",\n    \"/dominovertical_02_04\": \"\\u1F075\",\n    \"/dominovertical_02_05\": \"\\u1F076\",\n    \"/dominovertical_02_06\": \"\\u1F077\",\n    \"/dominovertical_03_00\": \"\\u1F078\",\n    \"/dominovertical_03_01\": \"\\u1F079\",\n    \"/dominovertical_03_02\": \"\\u1F07A\",\n    \"/dominovertical_03_03\": \"\\u1F07B\",\n    \"/dominovertical_03_04\": \"\\u1F07C\",\n    \"/dominovertical_03_05\": \"\\u1F07D\",\n    \"/dominovertical_03_06\": \"\\u1F07E\",\n    \"/dominovertical_04_00\": \"\\u1F07F\",\n    \"/dominovertical_04_01\": \"\\u1F080\",\n    \"/dominovertical_04_02\": \"\\u1F081\",\n    \"/dominovertical_04_03\": \"\\u1F082\",\n    \"/dominovertical_04_04\": \"\\u1F083\",\n    \"/dominovertical_04_05\": \"\\u1F084\",\n    \"/dominovertical_04_06\": \"\\u1F085\",\n    \"/dominovertical_05_00\": \"\\u1F086\",\n    \"/dominovertical_05_01\": \"\\u1F087\",\n    \"/dominovertical_05_02\": \"\\u1F088\",\n    \"/dominovertical_05_03\": \"\\u1F089\",\n    \"/dominovertical_05_04\": \"\\u1F08A\",\n    \"/dominovertical_05_05\": \"\\u1F08B\",\n    \"/dominovertical_05_06\": \"\\u1F08C\",\n    \"/dominovertical_06_00\": \"\\u1F08D\",\n    \"/dominovertical_06_01\": \"\\u1F08E\",\n    \"/dominovertical_06_02\": \"\\u1F08F\",\n    \"/dominovertical_06_03\": \"\\u1F090\",\n    \"/dominovertical_06_04\": \"\\u1F091\",\n    \"/dominovertical_06_05\": \"\\u1F092\",\n    \"/dominovertical_06_06\": \"\\u1F093\",\n    \"/dominoverticalback\": \"\\u1F062\",\n    \"/dong\": \"\\u20AB\",\n    \"/door\": \"\\u1F6AA\",\n    \"/dorusquare\": \"\\u3326\",\n    \"/dot\": \"\\u27D1\",\n    \"/dotaccent\": \"\\u02D9\",\n    \"/dotaccentcmb\": \"\\u0307\",\n    \"/dotbelowcmb\": \"\\u0323\",\n    \"/dotbelowcomb\": \"\\u0323\",\n    \"/dotkatakana\": \"\\u30FB\",\n    \"/dotlessbeh\": \"\\u066E\",\n    \"/dotlessfeh\": \"\\u06A1\",\n    \"/dotlessi\": \"\\u0131\",\n    \"/dotlessj\": \"\\uF6BE\",\n    \"/dotlessjstroke\": \"\\u025F\",\n    \"/dotlessjstrokehook\": \"\\u0284\",\n    \"/dotlesskhahabove\": \"\\u06E1\",\n    \"/dotlessqaf\": \"\\u066F\",\n    \"/dotlower:hb\": \"\\u05C5\",\n    \"/dotmath\": \"\\u22C5\",\n    \"/dotminus\": \"\\u2238\",\n    \"/dotplus\": \"\\u2214\",\n    \"/dotraised\": \"\\u2E33\",\n    \"/dots1\": \"\\u2801\",\n    \"/dots12\": \"\\u2803\",\n    \"/dots123\": \"\\u2807\",\n    \"/dots1234\": \"\\u280F\",\n    \"/dots12345\": \"\\u281F\",\n    \"/dots123456\": \"\\u283F\",\n    \"/dots1234567\": \"\\u287F\",\n    \"/dots12345678\": \"\\u28FF\",\n    \"/dots1234568\": \"\\u28BF\",\n    \"/dots123457\": \"\\u285F\",\n    \"/dots1234578\": \"\\u28DF\",\n    \"/dots123458\": \"\\u289F\",\n    \"/dots12346\": \"\\u282F\",\n    \"/dots123467\": \"\\u286F\",\n    \"/dots1234678\": \"\\u28EF\",\n    \"/dots123468\": \"\\u28AF\",\n    \"/dots12347\": \"\\u284F\",\n    \"/dots123478\": \"\\u28CF\",\n    \"/dots12348\": \"\\u288F\",\n    \"/dots1235\": \"\\u2817\",\n    \"/dots12356\": \"\\u2837\",\n    \"/dots123567\": \"\\u2877\",\n    \"/dots1235678\": \"\\u28F7\",\n    \"/dots123568\": \"\\u28B7\",\n    \"/dots12357\": \"\\u2857\",\n    \"/dots123578\": \"\\u28D7\",\n    \"/dots12358\": \"\\u2897\",\n    \"/dots1236\": \"\\u2827\",\n    \"/dots12367\": \"\\u2867\",\n    \"/dots123678\": \"\\u28E7\",\n    \"/dots12368\": \"\\u28A7\",\n    \"/dots1237\": \"\\u2847\",\n    \"/dots12378\": \"\\u28C7\",\n    \"/dots1238\": \"\\u2887\",\n    \"/dots124\": \"\\u280B\",\n    \"/dots1245\": \"\\u281B\",\n    \"/dots12456\": \"\\u283B\",\n    \"/dots124567\": \"\\u287B\",\n    \"/dots1245678\": \"\\u28FB\",\n    \"/dots124568\": \"\\u28BB\",\n    \"/dots12457\": \"\\u285B\",\n    \"/dots124578\": \"\\u28DB\",\n    \"/dots12458\": \"\\u289B\",\n    \"/dots1246\": \"\\u282B\",\n    \"/dots12467\": \"\\u286B\",\n    \"/dots124678\": \"\\u28EB\",\n    \"/dots12468\": \"\\u28AB\",\n    \"/dots1247\": \"\\u284B\",\n    \"/dots12478\": \"\\u28CB\",\n    \"/dots1248\": \"\\u288B\",\n    \"/dots125\": \"\\u2813\",\n    \"/dots1256\": \"\\u2833\",\n    \"/dots12567\": \"\\u2873\",\n    \"/dots125678\": \"\\u28F3\",\n    \"/dots12568\": \"\\u28B3\",\n    \"/dots1257\": \"\\u2853\",\n    \"/dots12578\": \"\\u28D3\",\n    \"/dots1258\": \"\\u2893\",\n    \"/dots126\": \"\\u2823\",\n    \"/dots1267\": \"\\u2863\",\n    \"/dots12678\": \"\\u28E3\",\n    \"/dots1268\": \"\\u28A3\",\n    \"/dots127\": \"\\u2843\",\n    \"/dots1278\": \"\\u28C3\",\n    \"/dots128\": \"\\u2883\",\n    \"/dots13\": \"\\u2805\",\n    \"/dots134\": \"\\u280D\",\n    \"/dots1345\": \"\\u281D\",\n    \"/dots13456\": \"\\u283D\",\n    \"/dots134567\": \"\\u287D\",\n    \"/dots1345678\": \"\\u28FD\",\n    \"/dots134568\": \"\\u28BD\",\n    \"/dots13457\": \"\\u285D\",\n    \"/dots134578\": \"\\u28DD\",\n    \"/dots13458\": \"\\u289D\",\n    \"/dots1346\": \"\\u282D\",\n    \"/dots13467\": \"\\u286D\",\n    \"/dots134678\": \"\\u28ED\",\n    \"/dots13468\": \"\\u28AD\",\n    \"/dots1347\": \"\\u284D\",\n    \"/dots13478\": \"\\u28CD\",\n    \"/dots1348\": \"\\u288D\",\n    \"/dots135\": \"\\u2815\",\n    \"/dots1356\": \"\\u2835\",\n    \"/dots13567\": \"\\u2875\",\n    \"/dots135678\": \"\\u28F5\",\n    \"/dots13568\": \"\\u28B5\",\n    \"/dots1357\": \"\\u2855\",\n    \"/dots13578\": \"\\u28D5\",\n    \"/dots1358\": \"\\u2895\",\n    \"/dots136\": \"\\u2825\",\n    \"/dots1367\": \"\\u2865\",\n    \"/dots13678\": \"\\u28E5\",\n    \"/dots1368\": \"\\u28A5\",\n    \"/dots137\": \"\\u2845\",\n    \"/dots1378\": \"\\u28C5\",\n    \"/dots138\": \"\\u2885\",\n    \"/dots14\": \"\\u2809\",\n    \"/dots145\": \"\\u2819\",\n    \"/dots1456\": \"\\u2839\",\n    \"/dots14567\": \"\\u2879\",\n    \"/dots145678\": \"\\u28F9\",\n    \"/dots14568\": \"\\u28B9\",\n    \"/dots1457\": \"\\u2859\",\n    \"/dots14578\": \"\\u28D9\",\n    \"/dots1458\": \"\\u2899\",\n    \"/dots146\": \"\\u2829\",\n    \"/dots1467\": \"\\u2869\",\n    \"/dots14678\": \"\\u28E9\",\n    \"/dots1468\": \"\\u28A9\",\n    \"/dots147\": \"\\u2849\",\n    \"/dots1478\": \"\\u28C9\",\n    \"/dots148\": \"\\u2889\",\n    \"/dots15\": \"\\u2811\",\n    \"/dots156\": \"\\u2831\",\n    \"/dots1567\": \"\\u2871\",\n    \"/dots15678\": \"\\u28F1\",\n    \"/dots1568\": \"\\u28B1\",\n    \"/dots157\": \"\\u2851\",\n    \"/dots1578\": \"\\u28D1\",\n    \"/dots158\": \"\\u2891\",\n    \"/dots16\": \"\\u2821\",\n    \"/dots167\": \"\\u2861\",\n    \"/dots1678\": \"\\u28E1\",\n    \"/dots168\": \"\\u28A1\",\n    \"/dots17\": \"\\u2841\",\n    \"/dots178\": \"\\u28C1\",\n    \"/dots18\": \"\\u2881\",\n    \"/dots2\": \"\\u2802\",\n    \"/dots23\": \"\\u2806\",\n    \"/dots234\": \"\\u280E\",\n    \"/dots2345\": \"\\u281E\",\n    \"/dots23456\": \"\\u283E\",\n    \"/dots234567\": \"\\u287E\",\n    \"/dots2345678\": \"\\u28FE\",\n    \"/dots234568\": \"\\u28BE\",\n    \"/dots23457\": \"\\u285E\",\n    \"/dots234578\": \"\\u28DE\",\n    \"/dots23458\": \"\\u289E\",\n    \"/dots2346\": \"\\u282E\",\n    \"/dots23467\": \"\\u286E\",\n    \"/dots234678\": \"\\u28EE\",\n    \"/dots23468\": \"\\u28AE\",\n    \"/dots2347\": \"\\u284E\",\n    \"/dots23478\": \"\\u28CE\",\n    \"/dots2348\": \"\\u288E\",\n    \"/dots235\": \"\\u2816\",\n    \"/dots2356\": \"\\u2836\",\n    \"/dots23567\": \"\\u2876\",\n    \"/dots235678\": \"\\u28F6\",\n    \"/dots23568\": \"\\u28B6\",\n    \"/dots2357\": \"\\u2856\",\n    \"/dots23578\": \"\\u28D6\",\n    \"/dots2358\": \"\\u2896\",\n    \"/dots236\": \"\\u2826\",\n    \"/dots2367\": \"\\u2866\",\n    \"/dots23678\": \"\\u28E6\",\n    \"/dots2368\": \"\\u28A6\",\n    \"/dots237\": \"\\u2846\",\n    \"/dots2378\": \"\\u28C6\",\n    \"/dots238\": \"\\u2886\",\n    \"/dots24\": \"\\u280A\",\n    \"/dots245\": \"\\u281A\",\n    \"/dots2456\": \"\\u283A\",\n    \"/dots24567\": \"\\u287A\",\n    \"/dots245678\": \"\\u28FA\",\n    \"/dots24568\": \"\\u28BA\",\n    \"/dots2457\": \"\\u285A\",\n    \"/dots24578\": \"\\u28DA\",\n    \"/dots2458\": \"\\u289A\",\n    \"/dots246\": \"\\u282A\",\n    \"/dots2467\": \"\\u286A\",\n    \"/dots24678\": \"\\u28EA\",\n    \"/dots2468\": \"\\u28AA\",\n    \"/dots247\": \"\\u284A\",\n    \"/dots2478\": \"\\u28CA\",\n    \"/dots248\": \"\\u288A\",\n    \"/dots25\": \"\\u2812\",\n    \"/dots256\": \"\\u2832\",\n    \"/dots2567\": \"\\u2872\",\n    \"/dots25678\": \"\\u28F2\",\n    \"/dots2568\": \"\\u28B2\",\n    \"/dots257\": \"\\u2852\",\n    \"/dots2578\": \"\\u28D2\",\n    \"/dots258\": \"\\u2892\",\n    \"/dots26\": \"\\u2822\",\n    \"/dots267\": \"\\u2862\",\n    \"/dots2678\": \"\\u28E2\",\n    \"/dots268\": \"\\u28A2\",\n    \"/dots27\": \"\\u2842\",\n    \"/dots278\": \"\\u28C2\",\n    \"/dots28\": \"\\u2882\",\n    \"/dots3\": \"\\u2804\",\n    \"/dots34\": \"\\u280C\",\n    \"/dots345\": \"\\u281C\",\n    \"/dots3456\": \"\\u283C\",\n    \"/dots34567\": \"\\u287C\",\n    \"/dots345678\": \"\\u28FC\",\n    \"/dots34568\": \"\\u28BC\",\n    \"/dots3457\": \"\\u285C\",\n    \"/dots34578\": \"\\u28DC\",\n    \"/dots3458\": \"\\u289C\",\n    \"/dots346\": \"\\u282C\",\n    \"/dots3467\": \"\\u286C\",\n    \"/dots34678\": \"\\u28EC\",\n    \"/dots3468\": \"\\u28AC\",\n    \"/dots347\": \"\\u284C\",\n    \"/dots3478\": \"\\u28CC\",\n    \"/dots348\": \"\\u288C\",\n    \"/dots35\": \"\\u2814\",\n    \"/dots356\": \"\\u2834\",\n    \"/dots3567\": \"\\u2874\",\n    \"/dots35678\": \"\\u28F4\",\n    \"/dots3568\": \"\\u28B4\",\n    \"/dots357\": \"\\u2854\",\n    \"/dots3578\": \"\\u28D4\",\n    \"/dots358\": \"\\u2894\",\n    \"/dots36\": \"\\u2824\",\n    \"/dots367\": \"\\u2864\",\n    \"/dots3678\": \"\\u28E4\",\n    \"/dots368\": \"\\u28A4\",\n    \"/dots37\": \"\\u2844\",\n    \"/dots378\": \"\\u28C4\",\n    \"/dots38\": \"\\u2884\",\n    \"/dots4\": \"\\u2808\",\n    \"/dots45\": \"\\u2818\",\n    \"/dots456\": \"\\u2838\",\n    \"/dots4567\": \"\\u2878\",\n    \"/dots45678\": \"\\u28F8\",\n    \"/dots4568\": \"\\u28B8\",\n    \"/dots457\": \"\\u2858\",\n    \"/dots4578\": \"\\u28D8\",\n    \"/dots458\": \"\\u2898\",\n    \"/dots46\": \"\\u2828\",\n    \"/dots467\": \"\\u2868\",\n    \"/dots4678\": \"\\u28E8\",\n    \"/dots468\": \"\\u28A8\",\n    \"/dots47\": \"\\u2848\",\n    \"/dots478\": \"\\u28C8\",\n    \"/dots48\": \"\\u2888\",\n    \"/dots5\": \"\\u2810\",\n    \"/dots56\": \"\\u2830\",\n    \"/dots567\": \"\\u2870\",\n    \"/dots5678\": \"\\u28F0\",\n    \"/dots568\": \"\\u28B0\",\n    \"/dots57\": \"\\u2850\",\n    \"/dots578\": \"\\u28D0\",\n    \"/dots58\": \"\\u2890\",\n    \"/dots6\": \"\\u2820\",\n    \"/dots67\": \"\\u2860\",\n    \"/dots678\": \"\\u28E0\",\n    \"/dots68\": \"\\u28A0\",\n    \"/dots7\": \"\\u2840\",\n    \"/dots78\": \"\\u28C0\",\n    \"/dots8\": \"\\u2880\",\n    \"/dotsquarefour\": \"\\u2E2C\",\n    \"/dottedcircle\": \"\\u25CC\",\n    \"/dottedcross\": \"\\u205C\",\n    \"/dotupper:hb\": \"\\u05C4\",\n    \"/doublebarvertical\": \"\\u23F8\",\n    \"/doubleyodpatah\": \"\\uFB1F\",\n    \"/doubleyodpatahhebrew\": \"\\uFB1F\",\n    \"/doughnut\": \"\\u1F369\",\n    \"/doveOfPeace\": \"\\u1F54A\",\n    \"/downtackbelowcmb\": \"\\u031E\",\n    \"/downtackmod\": \"\\u02D5\",\n    \"/downwarrowleftofuparrow\": \"\\u21F5\",\n    \"/dparen\": \"\\u249F\",\n    \"/dparenthesized\": \"\\u249F\",\n    \"/drachma\": \"\\u20AF\",\n    \"/dragon\": \"\\u1F409\",\n    \"/dragonFace\": \"\\u1F432\",\n    \"/draughtskingblack\": \"\\u26C3\",\n    \"/draughtskingwhite\": \"\\u26C1\",\n    \"/draughtsmanblack\": \"\\u26C2\",\n    \"/draughtsmanwhite\": \"\\u26C0\",\n    \"/dress\": \"\\u1F457\",\n    \"/driveslow\": \"\\u26DA\",\n    \"/dromedaryCamel\": \"\\u1F42A\",\n    \"/droplet\": \"\\u1F4A7\",\n    \"/dsquare\": \"\\u1F1A5\",\n    \"/dsuperior\": \"\\uF6EB\",\n    \"/dtail\": \"\\u0256\",\n    \"/dtopbar\": \"\\u018C\",\n    \"/duhiragana\": \"\\u3065\",\n    \"/dukatakana\": \"\\u30C5\",\n    \"/dul\": \"\\u068E\",\n    \"/dul.fina\": \"\\uFB87\",\n    \"/dul.isol\": \"\\uFB86\",\n    \"/dum\": \"\\uA771\",\n    \"/dvd\": \"\\u1F4C0\",\n    \"/dyeh\": \"\\u0684\",\n    \"/dyeh.fina\": \"\\uFB73\",\n    \"/dyeh.init\": \"\\uFB74\",\n    \"/dyeh.isol\": \"\\uFB72\",\n    \"/dyeh.medi\": \"\\uFB75\",\n    \"/dz\": \"\\u01F3\",\n    \"/dzaltone\": \"\\u02A3\",\n    \"/dzcaron\": \"\\u01C6\",\n    \"/dzcurl\": \"\\u02A5\",\n    \"/dzeabkhasiancyrillic\": \"\\u04E1\",\n    \"/dzeabkhcyr\": \"\\u04E1\",\n    \"/dzecyr\": \"\\u0455\",\n    \"/dzecyrillic\": \"\\u0455\",\n    \"/dzed\": \"\\u02A3\",\n    \"/dzedcurl\": \"\\u02A5\",\n    \"/dzhecyr\": \"\\u045F\",\n    \"/dzhecyrillic\": \"\\u045F\",\n    \"/dzjekomicyr\": \"\\u0507\",\n    \"/dzzhecyr\": \"\\u052B\",\n    \"/e\": \"\\u0065\",\n    \"/e-mail\": \"\\u1F4E7\",\n    \"/e.fina\": \"\\uFBE5\",\n    \"/e.inferior\": \"\\u2091\",\n    \"/e.init\": \"\\uFBE6\",\n    \"/e.isol\": \"\\uFBE4\",\n    \"/e.medi\": \"\\uFBE7\",\n    \"/eVfullwidth\": \"\\u32CE\",\n    \"/eacute\": \"\\u00E9\",\n    \"/earOfMaize\": \"\\u1F33D\",\n    \"/earOfRice\": \"\\u1F33E\",\n    \"/earth\": \"\\u2641\",\n    \"/earthGlobeAmericas\": \"\\u1F30E\",\n    \"/earthGlobeAsiaAustralia\": \"\\u1F30F\",\n    \"/earthGlobeEuropeAfrica\": \"\\u1F30D\",\n    \"/earthground\": \"\\u23DA\",\n    \"/earthideographiccircled\": \"\\u328F\",\n    \"/earthideographicparen\": \"\\u322F\",\n    \"/eastsyriaccross\": \"\\u2671\",\n    \"/ebengali\": \"\\u098F\",\n    \"/ebopomofo\": \"\\u311C\",\n    \"/ebreve\": \"\\u0115\",\n    \"/ecandradeva\": \"\\u090D\",\n    \"/ecandragujarati\": \"\\u0A8D\",\n    \"/ecandravowelsigndeva\": \"\\u0945\",\n    \"/ecandravowelsigngujarati\": \"\\u0AC5\",\n    \"/ecaron\": \"\\u011B\",\n    \"/ecedilla\": \"\\u0229\",\n    \"/ecedillabreve\": \"\\u1E1D\",\n    \"/echarmenian\": \"\\u0565\",\n    \"/echyiwnarmenian\": \"\\u0587\",\n    \"/ecircle\": \"\\u24D4\",\n    \"/ecirclekatakana\": \"\\u32D3\",\n    \"/ecircumflex\": \"\\u00EA\",\n    \"/ecircumflexacute\": \"\\u1EBF\",\n    \"/ecircumflexbelow\": \"\\u1E19\",\n    \"/ecircumflexdotbelow\": \"\\u1EC7\",\n    \"/ecircumflexgrave\": \"\\u1EC1\",\n    \"/ecircumflexhoi\": \"\\u1EC3\",\n    \"/ecircumflexhookabove\": \"\\u1EC3\",\n    \"/ecircumflextilde\": \"\\u1EC5\",\n    \"/ecyrillic\": \"\\u0454\",\n    \"/edblgrave\": \"\\u0205\",\n    \"/edblstruckitalic\": \"\\u2147\",\n    \"/edeva\": \"\\u090F\",\n    \"/edieresis\": \"\\u00EB\",\n    \"/edot\": \"\\u0117\",\n    \"/edotaccent\": \"\\u0117\",\n    \"/edotbelow\": \"\\u1EB9\",\n    \"/eegurmukhi\": \"\\u0A0F\",\n    \"/eekaasquare\": \"\\u3308\",\n    \"/eematragurmukhi\": \"\\u0A47\",\n    \"/efcyr\": \"\\u0444\",\n    \"/efcyrillic\": \"\\u0444\",\n    \"/egrave\": \"\\u00E8\",\n    \"/egravedbl\": \"\\u0205\",\n    \"/egujarati\": \"\\u0A8F\",\n    \"/egyptain\": \"\\uA725\",\n    \"/egyptalef\": \"\\uA723\",\n    \"/eharmenian\": \"\\u0567\",\n    \"/ehbopomofo\": \"\\u311D\",\n    \"/ehiragana\": \"\\u3048\",\n    \"/ehoi\": \"\\u1EBB\",\n    \"/ehookabove\": \"\\u1EBB\",\n    \"/eibopomofo\": \"\\u311F\",\n    \"/eight\": \"\\u0038\",\n    \"/eight.inferior\": \"\\u2088\",\n    \"/eight.roman\": \"\\u2167\",\n    \"/eight.romansmall\": \"\\u2177\",\n    \"/eight.superior\": \"\\u2078\",\n    \"/eightarabic\": \"\\u0668\",\n    \"/eightbengali\": \"\\u09EE\",\n    \"/eightcircle\": \"\\u2467\",\n    \"/eightcircledbl\": \"\\u24FC\",\n    \"/eightcircleinversesansserif\": \"\\u2791\",\n    \"/eightcomma\": \"\\u1F109\",\n    \"/eightdeva\": \"\\u096E\",\n    \"/eighteencircle\": \"\\u2471\",\n    \"/eighteencircleblack\": \"\\u24F2\",\n    \"/eighteenparen\": \"\\u2485\",\n    \"/eighteenparenthesized\": \"\\u2485\",\n    \"/eighteenperiod\": \"\\u2499\",\n    \"/eightfar\": \"\\u06F8\",\n    \"/eightgujarati\": \"\\u0AEE\",\n    \"/eightgurmukhi\": \"\\u0A6E\",\n    \"/eighthackarabic\": \"\\u0668\",\n    \"/eighthangzhou\": \"\\u3028\",\n    \"/eighthnote\": \"\\u266A\",\n    \"/eighthnotebeamed\": \"\\u266B\",\n    \"/eightideographiccircled\": \"\\u3287\",\n    \"/eightideographicparen\": \"\\u3227\",\n    \"/eightinferior\": \"\\u2088\",\n    \"/eightksquare\": \"\\u1F19F\",\n    \"/eightmonospace\": \"\\uFF18\",\n    \"/eightoldstyle\": \"\\uF738\",\n    \"/eightparen\": \"\\u247B\",\n    \"/eightparenthesized\": \"\\u247B\",\n    \"/eightperiod\": \"\\u248F\",\n    \"/eightpersian\": \"\\u06F8\",\n    \"/eightroman\": \"\\u2177\",\n    \"/eightsuperior\": \"\\u2078\",\n    \"/eightthai\": \"\\u0E58\",\n    \"/eightycirclesquare\": \"\\u324F\",\n    \"/einvertedbreve\": \"\\u0207\",\n    \"/eiotifiedcyr\": \"\\u0465\",\n    \"/eiotifiedcyrillic\": \"\\u0465\",\n    \"/eject\": \"\\u23CF\",\n    \"/ekatakana\": \"\\u30A8\",\n    \"/ekatakanahalfwidth\": \"\\uFF74\",\n    \"/ekonkargurmukhi\": \"\\u0A74\",\n    \"/ekorean\": \"\\u3154\",\n    \"/elcyr\": \"\\u043B\",\n    \"/elcyrillic\": \"\\u043B\",\n    \"/electricLightBulb\": \"\\u1F4A1\",\n    \"/electricPlug\": \"\\u1F50C\",\n    \"/electricTorch\": \"\\u1F526\",\n    \"/electricalintersection\": \"\\u23E7\",\n    \"/electricarrow\": \"\\u2301\",\n    \"/element\": \"\\u2208\",\n    \"/elementdotabove\": \"\\u22F5\",\n    \"/elementlonghorizontalstroke\": \"\\u22F2\",\n    \"/elementopeningup\": \"\\u27D2\",\n    \"/elementoverbar\": \"\\u22F6\",\n    \"/elementoverbarsmall\": \"\\u22F7\",\n    \"/elementsmall\": \"\\u220A\",\n    \"/elementsmallverticalbarhorizontalstroke\": \"\\u22F4\",\n    \"/elementtwoshorizontalstroke\": \"\\u22F9\",\n    \"/elementunderbar\": \"\\u22F8\",\n    \"/elementverticalbarhorizontalstroke\": \"\\u22F3\",\n    \"/elephant\": \"\\u1F418\",\n    \"/eleven.roman\": \"\\u216A\",\n    \"/eleven.romansmall\": \"\\u217A\",\n    \"/elevencircle\": \"\\u246A\",\n    \"/elevencircleblack\": \"\\u24EB\",\n    \"/elevenparen\": \"\\u247E\",\n    \"/elevenparenthesized\": \"\\u247E\",\n    \"/elevenperiod\": \"\\u2492\",\n    \"/elevenroman\": \"\\u217A\",\n    \"/elhookcyr\": \"\\u0513\",\n    \"/ellipsis\": \"\\u2026\",\n    \"/ellipsisdiagonaldownright\": \"\\u22F1\",\n    \"/ellipsisdiagonalupright\": \"\\u22F0\",\n    \"/ellipsismidhorizontal\": \"\\u22EF\",\n    \"/ellipsisvertical\": \"\\u22EE\",\n    \"/elmiddlehookcyr\": \"\\u0521\",\n    \"/elsharptailcyr\": \"\\u04C6\",\n    \"/eltailcyr\": \"\\u052F\",\n    \"/emacron\": \"\\u0113\",\n    \"/emacronacute\": \"\\u1E17\",\n    \"/emacrongrave\": \"\\u1E15\",\n    \"/emcyr\": \"\\u043C\",\n    \"/emcyrillic\": \"\\u043C\",\n    \"/emdash\": \"\\u2014\",\n    \"/emdashdbl\": \"\\u2E3A\",\n    \"/emdashtpl\": \"\\u2E3B\",\n    \"/emdashvertical\": \"\\uFE31\",\n    \"/emojiModifierFitzpatrickType-1-2\": \"\\u1F3FB\",\n    \"/emojiModifierFitzpatrickType-3\": \"\\u1F3FC\",\n    \"/emojiModifierFitzpatrickType-4\": \"\\u1F3FD\",\n    \"/emojiModifierFitzpatrickType-5\": \"\\u1F3FE\",\n    \"/emojiModifierFitzpatrickType-6\": \"\\u1F3FF\",\n    \"/emonospace\": \"\\uFF45\",\n    \"/emphasis\": \"\\u2383\",\n    \"/emphasismarkarmenian\": \"\\u055B\",\n    \"/emptyDocument\": \"\\u1F5CB\",\n    \"/emptyNote\": \"\\u1F5C5\",\n    \"/emptyNotePad\": \"\\u1F5C7\",\n    \"/emptyNotePage\": \"\\u1F5C6\",\n    \"/emptyPage\": \"\\u1F5CC\",\n    \"/emptyPages\": \"\\u1F5CD\",\n    \"/emptyset\": \"\\u2205\",\n    \"/emquad\": \"\\u2001\",\n    \"/emsharptailcyr\": \"\\u04CE\",\n    \"/emspace\": \"\\u2003\",\n    \"/enbopomofo\": \"\\u3123\",\n    \"/encyr\": \"\\u043D\",\n    \"/encyrillic\": \"\\u043D\",\n    \"/endLeftwardsArrowAbove\": \"\\u1F51A\",\n    \"/endash\": \"\\u2013\",\n    \"/endashvertical\": \"\\uFE32\",\n    \"/endescendercyrillic\": \"\\u04A3\",\n    \"/endpro\": \"\\u220E\",\n    \"/eng\": \"\\u014B\",\n    \"/engbopomofo\": \"\\u3125\",\n    \"/engecyr\": \"\\u04A5\",\n    \"/enghecyrillic\": \"\\u04A5\",\n    \"/enhookcyr\": \"\\u04C8\",\n    \"/enhookcyrillic\": \"\\u04C8\",\n    \"/enhookleftcyr\": \"\\u0529\",\n    \"/enmiddlehookcyr\": \"\\u0523\",\n    \"/enotch\": \"\\u2C78\",\n    \"/enquad\": \"\\u2000\",\n    \"/ensharptailcyr\": \"\\u04CA\",\n    \"/enspace\": \"\\u2002\",\n    \"/entailcyr\": \"\\u04A3\",\n    \"/enter\": \"\\u2386\",\n    \"/enterpriseideographiccircled\": \"\\u32AD\",\n    \"/enterpriseideographicparen\": \"\\u323D\",\n    \"/envelopeDownwardsArrowAbove\": \"\\u1F4E9\",\n    \"/envelopeLightning\": \"\\u1F584\",\n    \"/eogonek\": \"\\u0119\",\n    \"/eokorean\": \"\\u3153\",\n    \"/eopen\": \"\\u025B\",\n    \"/eopenclosed\": \"\\u029A\",\n    \"/eopenreversed\": \"\\u025C\",\n    \"/eopenreversedclosed\": \"\\u025E\",\n    \"/eopenreversedhook\": \"\\u025D\",\n    \"/eparen\": \"\\u24A0\",\n    \"/eparenthesized\": \"\\u24A0\",\n    \"/epsilon\": \"\\u03B5\",\n    \"/epsilonacute\": \"\\u1F73\",\n    \"/epsilonasper\": \"\\u1F11\",\n    \"/epsilonasperacute\": \"\\u1F15\",\n    \"/epsilonaspergrave\": \"\\u1F13\",\n    \"/epsilongrave\": \"\\u1F72\",\n    \"/epsilonlenis\": \"\\u1F10\",\n    \"/epsilonlenisacute\": \"\\u1F14\",\n    \"/epsilonlenisgrave\": \"\\u1F12\",\n    \"/epsilonlunatesymbol\": \"\\u03F5\",\n    \"/epsilonreversedlunatesymbol\": \"\\u03F6\",\n    \"/epsilontonos\": \"\\u03AD\",\n    \"/epsilonunderlinefunc\": \"\\u2377\",\n    \"/equal\": \"\\u003D\",\n    \"/equal.inferior\": \"\\u208C\",\n    \"/equal.superior\": \"\\u207C\",\n    \"/equalandparallel\": \"\\u22D5\",\n    \"/equalbydefinition\": \"\\u225D\",\n    \"/equalmonospace\": \"\\uFF1D\",\n    \"/equalorgreater\": \"\\u22DD\",\n    \"/equalorless\": \"\\u22DC\",\n    \"/equalorprecedes\": \"\\u22DE\",\n    \"/equalorsucceeds\": \"\\u22DF\",\n    \"/equalscolon\": \"\\u2255\",\n    \"/equalsmall\": \"\\uFE66\",\n    \"/equalsuperior\": \"\\u207C\",\n    \"/equiangular\": \"\\u225A\",\n    \"/equivalence\": \"\\u2261\",\n    \"/equivalent\": \"\\u224D\",\n    \"/eranameheiseisquare\": \"\\u337B\",\n    \"/eranamemeizisquare\": \"\\u337E\",\n    \"/eranamesyouwasquare\": \"\\u337C\",\n    \"/eranametaisyousquare\": \"\\u337D\",\n    \"/eraseleft\": \"\\u232B\",\n    \"/eraseright\": \"\\u2326\",\n    \"/erbopomofo\": \"\\u3126\",\n    \"/ercyr\": \"\\u0440\",\n    \"/ercyrillic\": \"\\u0440\",\n    \"/ereversed\": \"\\u0258\",\n    \"/ereversedcyr\": \"\\u044D\",\n    \"/ereversedcyrillic\": \"\\u044D\",\n    \"/ereverseddieresiscyr\": \"\\u04ED\",\n    \"/ergfullwidth\": \"\\u32CD\",\n    \"/ertickcyr\": \"\\u048F\",\n    \"/escript\": \"\\u212F\",\n    \"/escyr\": \"\\u0441\",\n    \"/escyrillic\": \"\\u0441\",\n    \"/esdescendercyrillic\": \"\\u04AB\",\n    \"/esh\": \"\\u0283\",\n    \"/eshcurl\": \"\\u0286\",\n    \"/eshortdeva\": \"\\u090E\",\n    \"/eshortvowelsigndeva\": \"\\u0946\",\n    \"/eshreversedloop\": \"\\u01AA\",\n    \"/eshsquatreversed\": \"\\u0285\",\n    \"/esmallhiragana\": \"\\u3047\",\n    \"/esmallkatakana\": \"\\u30A7\",\n    \"/esmallkatakanahalfwidth\": \"\\uFF6A\",\n    \"/estailcyr\": \"\\u04AB\",\n    \"/estimated\": \"\\u212E\",\n    \"/estimates\": \"\\u2259\",\n    \"/estroke\": \"\\u0247\",\n    \"/esukuudosquare\": \"\\u3307\",\n    \"/esuperior\": \"\\uF6EC\",\n    \"/et\": \"\\uA76B\",\n    \"/eta\": \"\\u03B7\",\n    \"/etaacute\": \"\\u1F75\",\n    \"/etaacuteiotasub\": \"\\u1FC4\",\n    \"/etaasper\": \"\\u1F21\",\n    \"/etaasperacute\": \"\\u1F25\",\n    \"/etaasperacuteiotasub\": \"\\u1F95\",\n    \"/etaaspergrave\": \"\\u1F23\",\n    \"/etaaspergraveiotasub\": \"\\u1F93\",\n    \"/etaasperiotasub\": \"\\u1F91\",\n    \"/etaaspertilde\": \"\\u1F27\",\n    \"/etaaspertildeiotasub\": \"\\u1F97\",\n    \"/etagrave\": \"\\u1F74\",\n    \"/etagraveiotasub\": \"\\u1FC2\",\n    \"/etaiotasub\": \"\\u1FC3\",\n    \"/etalenis\": \"\\u1F20\",\n    \"/etalenisacute\": \"\\u1F24\",\n    \"/etalenisacuteiotasub\": \"\\u1F94\",\n    \"/etalenisgrave\": \"\\u1F22\",\n    \"/etalenisgraveiotasub\": \"\\u1F92\",\n    \"/etalenisiotasub\": \"\\u1F90\",\n    \"/etalenistilde\": \"\\u1F26\",\n    \"/etalenistildeiotasub\": \"\\u1F96\",\n    \"/etarmenian\": \"\\u0568\",\n    \"/etatilde\": \"\\u1FC6\",\n    \"/etatildeiotasub\": \"\\u1FC7\",\n    \"/etatonos\": \"\\u03AE\",\n    \"/eth\": \"\\u00F0\",\n    \"/ethi:aaglottal\": \"\\u12A3\",\n    \"/ethi:aglottal\": \"\\u12A0\",\n    \"/ethi:ba\": \"\\u1260\",\n    \"/ethi:baa\": \"\\u1263\",\n    \"/ethi:be\": \"\\u1265\",\n    \"/ethi:bee\": \"\\u1264\",\n    \"/ethi:bi\": \"\\u1262\",\n    \"/ethi:bo\": \"\\u1266\",\n    \"/ethi:bu\": \"\\u1261\",\n    \"/ethi:bwa\": \"\\u1267\",\n    \"/ethi:ca\": \"\\u1278\",\n    \"/ethi:caa\": \"\\u127B\",\n    \"/ethi:ce\": \"\\u127D\",\n    \"/ethi:cee\": \"\\u127C\",\n    \"/ethi:cha\": \"\\u1328\",\n    \"/ethi:chaa\": \"\\u132B\",\n    \"/ethi:che\": \"\\u132D\",\n    \"/ethi:chee\": \"\\u132C\",\n    \"/ethi:chi\": \"\\u132A\",\n    \"/ethi:cho\": \"\\u132E\",\n    \"/ethi:chu\": \"\\u1329\",\n    \"/ethi:chwa\": \"\\u132F\",\n    \"/ethi:ci\": \"\\u127A\",\n    \"/ethi:co\": \"\\u127E\",\n    \"/ethi:colon\": \"\\u1365\",\n    \"/ethi:comma\": \"\\u1363\",\n    \"/ethi:cu\": \"\\u1279\",\n    \"/ethi:cwa\": \"\\u127F\",\n    \"/ethi:da\": \"\\u12F0\",\n    \"/ethi:daa\": \"\\u12F3\",\n    \"/ethi:dda\": \"\\u12F8\",\n    \"/ethi:ddaa\": \"\\u12FB\",\n    \"/ethi:dde\": \"\\u12FD\",\n    \"/ethi:ddee\": \"\\u12FC\",\n    \"/ethi:ddi\": \"\\u12FA\",\n    \"/ethi:ddo\": \"\\u12FE\",\n    \"/ethi:ddu\": \"\\u12F9\",\n    \"/ethi:ddwa\": \"\\u12FF\",\n    \"/ethi:de\": \"\\u12F5\",\n    \"/ethi:dee\": \"\\u12F4\",\n    \"/ethi:di\": \"\\u12F2\",\n    \"/ethi:do\": \"\\u12F6\",\n    \"/ethi:du\": \"\\u12F1\",\n    \"/ethi:dwa\": \"\\u12F7\",\n    \"/ethi:eeglottal\": \"\\u12A4\",\n    \"/ethi:eglottal\": \"\\u12A5\",\n    \"/ethi:eight\": \"\\u1370\",\n    \"/ethi:eighty\": \"\\u1379\",\n    \"/ethi:fa\": \"\\u1348\",\n    \"/ethi:faa\": \"\\u134B\",\n    \"/ethi:fe\": \"\\u134D\",\n    \"/ethi:fee\": \"\\u134C\",\n    \"/ethi:fi\": \"\\u134A\",\n    \"/ethi:fifty\": \"\\u1376\",\n    \"/ethi:five\": \"\\u136D\",\n    \"/ethi:fo\": \"\\u134E\",\n    \"/ethi:forty\": \"\\u1375\",\n    \"/ethi:four\": \"\\u136C\",\n    \"/ethi:fu\": \"\\u1349\",\n    \"/ethi:fullstop\": \"\\u1362\",\n    \"/ethi:fwa\": \"\\u134F\",\n    \"/ethi:fya\": \"\\u135A\",\n    \"/ethi:ga\": \"\\u1308\",\n    \"/ethi:gaa\": \"\\u130B\",\n    \"/ethi:ge\": \"\\u130D\",\n    \"/ethi:gee\": \"\\u130C\",\n    \"/ethi:geminationandvowellengthmarkcmb\": \"\\u135D\",\n    \"/ethi:geminationmarkcmb\": \"\\u135F\",\n    \"/ethi:gga\": \"\\u1318\",\n    \"/ethi:ggaa\": \"\\u131B\",\n    \"/ethi:gge\": \"\\u131D\",\n    \"/ethi:ggee\": \"\\u131C\",\n    \"/ethi:ggi\": \"\\u131A\",\n    \"/ethi:ggo\": \"\\u131E\",\n    \"/ethi:ggu\": \"\\u1319\",\n    \"/ethi:ggwaa\": \"\\u131F\",\n    \"/ethi:gi\": \"\\u130A\",\n    \"/ethi:go\": \"\\u130E\",\n    \"/ethi:goa\": \"\\u130F\",\n    \"/ethi:gu\": \"\\u1309\",\n    \"/ethi:gwa\": \"\\u1310\",\n    \"/ethi:gwaa\": \"\\u1313\",\n    \"/ethi:gwe\": \"\\u1315\",\n    \"/ethi:gwee\": \"\\u1314\",\n    \"/ethi:gwi\": \"\\u1312\",\n    \"/ethi:ha\": \"\\u1200\",\n    \"/ethi:haa\": \"\\u1203\",\n    \"/ethi:he\": \"\\u1205\",\n    \"/ethi:hee\": \"\\u1204\",\n    \"/ethi:hha\": \"\\u1210\",\n    \"/ethi:hhaa\": \"\\u1213\",\n    \"/ethi:hhe\": \"\\u1215\",\n    \"/ethi:hhee\": \"\\u1214\",\n    \"/ethi:hhi\": \"\\u1212\",\n    \"/ethi:hho\": \"\\u1216\",\n    \"/ethi:hhu\": \"\\u1211\",\n    \"/ethi:hhwa\": \"\\u1217\",\n    \"/ethi:hi\": \"\\u1202\",\n    \"/ethi:ho\": \"\\u1206\",\n    \"/ethi:hoa\": \"\\u1207\",\n    \"/ethi:hu\": \"\\u1201\",\n    \"/ethi:hundred\": \"\\u137B\",\n    \"/ethi:iglottal\": \"\\u12A2\",\n    \"/ethi:ja\": \"\\u1300\",\n    \"/ethi:jaa\": \"\\u1303\",\n    \"/ethi:je\": \"\\u1305\",\n    \"/ethi:jee\": \"\\u1304\",\n    \"/ethi:ji\": \"\\u1302\",\n    \"/ethi:jo\": \"\\u1306\",\n    \"/ethi:ju\": \"\\u1301\",\n    \"/ethi:jwa\": \"\\u1307\",\n    \"/ethi:ka\": \"\\u12A8\",\n    \"/ethi:kaa\": \"\\u12AB\",\n    \"/ethi:ke\": \"\\u12AD\",\n    \"/ethi:kee\": \"\\u12AC\",\n    \"/ethi:ki\": \"\\u12AA\",\n    \"/ethi:ko\": \"\\u12AE\",\n    \"/ethi:koa\": \"\\u12AF\",\n    \"/ethi:ku\": \"\\u12A9\",\n    \"/ethi:kwa\": \"\\u12B0\",\n    \"/ethi:kwaa\": \"\\u12B3\",\n    \"/ethi:kwe\": \"\\u12B5\",\n    \"/ethi:kwee\": \"\\u12B4\",\n    \"/ethi:kwi\": \"\\u12B2\",\n    \"/ethi:kxa\": \"\\u12B8\",\n    \"/ethi:kxaa\": \"\\u12BB\",\n    \"/ethi:kxe\": \"\\u12BD\",\n    \"/ethi:kxee\": \"\\u12BC\",\n    \"/ethi:kxi\": \"\\u12BA\",\n    \"/ethi:kxo\": \"\\u12BE\",\n    \"/ethi:kxu\": \"\\u12B9\",\n    \"/ethi:kxwa\": \"\\u12C0\",\n    \"/ethi:kxwaa\": \"\\u12C3\",\n    \"/ethi:kxwe\": \"\\u12C5\",\n    \"/ethi:kxwee\": \"\\u12C4\",\n    \"/ethi:kxwi\": \"\\u12C2\",\n    \"/ethi:la\": \"\\u1208\",\n    \"/ethi:laa\": \"\\u120B\",\n    \"/ethi:le\": \"\\u120D\",\n    \"/ethi:lee\": \"\\u120C\",\n    \"/ethi:li\": \"\\u120A\",\n    \"/ethi:lo\": \"\\u120E\",\n    \"/ethi:lu\": \"\\u1209\",\n    \"/ethi:lwa\": \"\\u120F\",\n    \"/ethi:ma\": \"\\u1218\",\n    \"/ethi:maa\": \"\\u121B\",\n    \"/ethi:me\": \"\\u121D\",\n    \"/ethi:mee\": \"\\u121C\",\n    \"/ethi:mi\": \"\\u121A\",\n    \"/ethi:mo\": \"\\u121E\",\n    \"/ethi:mu\": \"\\u1219\",\n    \"/ethi:mwa\": \"\\u121F\",\n    \"/ethi:mya\": \"\\u1359\",\n    \"/ethi:na\": \"\\u1290\",\n    \"/ethi:naa\": \"\\u1293\",\n    \"/ethi:ne\": \"\\u1295\",\n    \"/ethi:nee\": \"\\u1294\",\n    \"/ethi:ni\": \"\\u1292\",\n    \"/ethi:nine\": \"\\u1371\",\n    \"/ethi:ninety\": \"\\u137A\",\n    \"/ethi:no\": \"\\u1296\",\n    \"/ethi:nu\": \"\\u1291\",\n    \"/ethi:nwa\": \"\\u1297\",\n    \"/ethi:nya\": \"\\u1298\",\n    \"/ethi:nyaa\": \"\\u129B\",\n    \"/ethi:nye\": \"\\u129D\",\n    \"/ethi:nyee\": \"\\u129C\",\n    \"/ethi:nyi\": \"\\u129A\",\n    \"/ethi:nyo\": \"\\u129E\",\n    \"/ethi:nyu\": \"\\u1299\",\n    \"/ethi:nywa\": \"\\u129F\",\n    \"/ethi:oglottal\": \"\\u12A6\",\n    \"/ethi:one\": \"\\u1369\",\n    \"/ethi:pa\": \"\\u1350\",\n    \"/ethi:paa\": \"\\u1353\",\n    \"/ethi:paragraphseparator\": \"\\u1368\",\n    \"/ethi:pe\": \"\\u1355\",\n    \"/ethi:pee\": \"\\u1354\",\n    \"/ethi:pha\": \"\\u1330\",\n    \"/ethi:phaa\": \"\\u1333\",\n    \"/ethi:pharyngeala\": \"\\u12D0\",\n    \"/ethi:pharyngealaa\": \"\\u12D3\",\n    \"/ethi:pharyngeale\": \"\\u12D5\",\n    \"/ethi:pharyngealee\": \"\\u12D4\",\n    \"/ethi:pharyngeali\": \"\\u12D2\",\n    \"/ethi:pharyngealo\": \"\\u12D6\",\n    \"/ethi:pharyngealu\": \"\\u12D1\",\n    \"/ethi:phe\": \"\\u1335\",\n    \"/ethi:phee\": \"\\u1334\",\n    \"/ethi:phi\": \"\\u1332\",\n    \"/ethi:pho\": \"\\u1336\",\n    \"/ethi:phu\": \"\\u1331\",\n    \"/ethi:phwa\": \"\\u1337\",\n    \"/ethi:pi\": \"\\u1352\",\n    \"/ethi:po\": \"\\u1356\",\n    \"/ethi:prefacecolon\": \"\\u1366\",\n    \"/ethi:pu\": \"\\u1351\",\n    \"/ethi:pwa\": \"\\u1357\",\n    \"/ethi:qa\": \"\\u1240\",\n    \"/ethi:qaa\": \"\\u1243\",\n    \"/ethi:qe\": \"\\u1245\",\n    \"/ethi:qee\": \"\\u1244\",\n    \"/ethi:qha\": \"\\u1250\",\n    \"/ethi:qhaa\": \"\\u1253\",\n    \"/ethi:qhe\": \"\\u1255\",\n    \"/ethi:qhee\": \"\\u1254\",\n    \"/ethi:qhi\": \"\\u1252\",\n    \"/ethi:qho\": \"\\u1256\",\n    \"/ethi:qhu\": \"\\u1251\",\n    \"/ethi:qhwa\": \"\\u1258\",\n    \"/ethi:qhwaa\": \"\\u125B\",\n    \"/ethi:qhwe\": \"\\u125D\",\n    \"/ethi:qhwee\": \"\\u125C\",\n    \"/ethi:qhwi\": \"\\u125A\",\n    \"/ethi:qi\": \"\\u1242\",\n    \"/ethi:qo\": \"\\u1246\",\n    \"/ethi:qoa\": \"\\u1247\",\n    \"/ethi:qu\": \"\\u1241\",\n    \"/ethi:questionmark\": \"\\u1367\",\n    \"/ethi:qwa\": \"\\u1248\",\n    \"/ethi:qwaa\": \"\\u124B\",\n    \"/ethi:qwe\": \"\\u124D\",\n    \"/ethi:qwee\": \"\\u124C\",\n    \"/ethi:qwi\": \"\\u124A\",\n    \"/ethi:ra\": \"\\u1228\",\n    \"/ethi:raa\": \"\\u122B\",\n    \"/ethi:re\": \"\\u122D\",\n    \"/ethi:ree\": \"\\u122C\",\n    \"/ethi:ri\": \"\\u122A\",\n    \"/ethi:ro\": \"\\u122E\",\n    \"/ethi:ru\": \"\\u1229\",\n    \"/ethi:rwa\": \"\\u122F\",\n    \"/ethi:rya\": \"\\u1358\",\n    \"/ethi:sa\": \"\\u1230\",\n    \"/ethi:saa\": \"\\u1233\",\n    \"/ethi:se\": \"\\u1235\",\n    \"/ethi:sectionmark\": \"\\u1360\",\n    \"/ethi:see\": \"\\u1234\",\n    \"/ethi:semicolon\": \"\\u1364\",\n    \"/ethi:seven\": \"\\u136F\",\n    \"/ethi:seventy\": \"\\u1378\",\n    \"/ethi:sha\": \"\\u1238\",\n    \"/ethi:shaa\": \"\\u123B\",\n    \"/ethi:she\": \"\\u123D\",\n    \"/ethi:shee\": \"\\u123C\",\n    \"/ethi:shi\": \"\\u123A\",\n    \"/ethi:sho\": \"\\u123E\",\n    \"/ethi:shu\": \"\\u1239\",\n    \"/ethi:shwa\": \"\\u123F\",\n    \"/ethi:si\": \"\\u1232\",\n    \"/ethi:six\": \"\\u136E\",\n    \"/ethi:sixty\": \"\\u1377\",\n    \"/ethi:so\": \"\\u1236\",\n    \"/ethi:su\": \"\\u1231\",\n    \"/ethi:swa\": \"\\u1237\",\n    \"/ethi:sza\": \"\\u1220\",\n    \"/ethi:szaa\": \"\\u1223\",\n    \"/ethi:sze\": \"\\u1225\",\n    \"/ethi:szee\": \"\\u1224\",\n    \"/ethi:szi\": \"\\u1222\",\n    \"/ethi:szo\": \"\\u1226\",\n    \"/ethi:szu\": \"\\u1221\",\n    \"/ethi:szwa\": \"\\u1227\",\n    \"/ethi:ta\": \"\\u1270\",\n    \"/ethi:taa\": \"\\u1273\",\n    \"/ethi:te\": \"\\u1275\",\n    \"/ethi:tee\": \"\\u1274\",\n    \"/ethi:ten\": \"\\u1372\",\n    \"/ethi:tenthousand\": \"\\u137C\",\n    \"/ethi:tha\": \"\\u1320\",\n    \"/ethi:thaa\": \"\\u1323\",\n    \"/ethi:the\": \"\\u1325\",\n    \"/ethi:thee\": \"\\u1324\",\n    \"/ethi:thi\": \"\\u1322\",\n    \"/ethi:thirty\": \"\\u1374\",\n    \"/ethi:tho\": \"\\u1326\",\n    \"/ethi:three\": \"\\u136B\",\n    \"/ethi:thu\": \"\\u1321\",\n    \"/ethi:thwa\": \"\\u1327\",\n    \"/ethi:ti\": \"\\u1272\",\n    \"/ethi:to\": \"\\u1276\",\n    \"/ethi:tsa\": \"\\u1338\",\n    \"/ethi:tsaa\": \"\\u133B\",\n    \"/ethi:tse\": \"\\u133D\",\n    \"/ethi:tsee\": \"\\u133C\",\n    \"/ethi:tsi\": \"\\u133A\",\n    \"/ethi:tso\": \"\\u133E\",\n    \"/ethi:tsu\": \"\\u1339\",\n    \"/ethi:tswa\": \"\\u133F\",\n    \"/ethi:tu\": \"\\u1271\",\n    \"/ethi:twa\": \"\\u1277\",\n    \"/ethi:twenty\": \"\\u1373\",\n    \"/ethi:two\": \"\\u136A\",\n    \"/ethi:tza\": \"\\u1340\",\n    \"/ethi:tzaa\": \"\\u1343\",\n    \"/ethi:tze\": \"\\u1345\",\n    \"/ethi:tzee\": \"\\u1344\",\n    \"/ethi:tzi\": \"\\u1342\",\n    \"/ethi:tzo\": \"\\u1346\",\n    \"/ethi:tzoa\": \"\\u1347\",\n    \"/ethi:tzu\": \"\\u1341\",\n    \"/ethi:uglottal\": \"\\u12A1\",\n    \"/ethi:va\": \"\\u1268\",\n    \"/ethi:vaa\": \"\\u126B\",\n    \"/ethi:ve\": \"\\u126D\",\n    \"/ethi:vee\": \"\\u126C\",\n    \"/ethi:vi\": \"\\u126A\",\n    \"/ethi:vo\": \"\\u126E\",\n    \"/ethi:vowellengthmarkcmb\": \"\\u135E\",\n    \"/ethi:vu\": \"\\u1269\",\n    \"/ethi:vwa\": \"\\u126F\",\n    \"/ethi:wa\": \"\\u12C8\",\n    \"/ethi:waa\": \"\\u12CB\",\n    \"/ethi:waglottal\": \"\\u12A7\",\n    \"/ethi:we\": \"\\u12CD\",\n    \"/ethi:wee\": \"\\u12CC\",\n    \"/ethi:wi\": \"\\u12CA\",\n    \"/ethi:wo\": \"\\u12CE\",\n    \"/ethi:woa\": \"\\u12CF\",\n    \"/ethi:wordspace\": \"\\u1361\",\n    \"/ethi:wu\": \"\\u12C9\",\n    \"/ethi:xa\": \"\\u1280\",\n    \"/ethi:xaa\": \"\\u1283\",\n    \"/ethi:xe\": \"\\u1285\",\n    \"/ethi:xee\": \"\\u1284\",\n    \"/ethi:xi\": \"\\u1282\",\n    \"/ethi:xo\": \"\\u1286\",\n    \"/ethi:xoa\": \"\\u1287\",\n    \"/ethi:xu\": \"\\u1281\",\n    \"/ethi:xwa\": \"\\u1288\",\n    \"/ethi:xwaa\": \"\\u128B\",\n    \"/ethi:xwe\": \"\\u128D\",\n    \"/ethi:xwee\": \"\\u128C\",\n    \"/ethi:xwi\": \"\\u128A\",\n    \"/ethi:ya\": \"\\u12E8\",\n    \"/ethi:yaa\": \"\\u12EB\",\n    \"/ethi:ye\": \"\\u12ED\",\n    \"/ethi:yee\": \"\\u12EC\",\n    \"/ethi:yi\": \"\\u12EA\",\n    \"/ethi:yo\": \"\\u12EE\",\n    \"/ethi:yoa\": \"\\u12EF\",\n    \"/ethi:yu\": \"\\u12E9\",\n    \"/ethi:za\": \"\\u12D8\",\n    \"/ethi:zaa\": \"\\u12DB\",\n    \"/ethi:ze\": \"\\u12DD\",\n    \"/ethi:zee\": \"\\u12DC\",\n    \"/ethi:zha\": \"\\u12E0\",\n    \"/ethi:zhaa\": \"\\u12E3\",\n    \"/ethi:zhe\": \"\\u12E5\",\n    \"/ethi:zhee\": \"\\u12E4\",\n    \"/ethi:zhi\": \"\\u12E2\",\n    \"/ethi:zho\": \"\\u12E6\",\n    \"/ethi:zhu\": \"\\u12E1\",\n    \"/ethi:zhwa\": \"\\u12E7\",\n    \"/ethi:zi\": \"\\u12DA\",\n    \"/ethi:zo\": \"\\u12DE\",\n    \"/ethi:zu\": \"\\u12D9\",\n    \"/ethi:zwa\": \"\\u12DF\",\n    \"/etilde\": \"\\u1EBD\",\n    \"/etildebelow\": \"\\u1E1B\",\n    \"/etnahta:hb\": \"\\u0591\",\n    \"/etnahtafoukhhebrew\": \"\\u0591\",\n    \"/etnahtafoukhlefthebrew\": \"\\u0591\",\n    \"/etnahtahebrew\": \"\\u0591\",\n    \"/etnahtalefthebrew\": \"\\u0591\",\n    \"/eturned\": \"\\u01DD\",\n    \"/eukorean\": \"\\u3161\",\n    \"/eukrcyr\": \"\\u0454\",\n    \"/euler\": \"\\u2107\",\n    \"/euro\": \"\\u20AC\",\n    \"/euroarchaic\": \"\\u20A0\",\n    \"/europeanCastle\": \"\\u1F3F0\",\n    \"/europeanPostOffice\": \"\\u1F3E4\",\n    \"/evergreenTree\": \"\\u1F332\",\n    \"/evowelsignbengali\": \"\\u09C7\",\n    \"/evowelsigndeva\": \"\\u0947\",\n    \"/evowelsigngujarati\": \"\\u0AC7\",\n    \"/excellentideographiccircled\": \"\\u329D\",\n    \"/excess\": \"\\u2239\",\n    \"/exclam\": \"\\u0021\",\n    \"/exclamarmenian\": \"\\u055C\",\n    \"/exclamationquestion\": \"\\u2049\",\n    \"/exclamdbl\": \"\\u203C\",\n    \"/exclamdown\": \"\\u00A1\",\n    \"/exclamdownsmall\": \"\\uF7A1\",\n    \"/exclammonospace\": \"\\uFF01\",\n    \"/exclamsmall\": \"\\uF721\",\n    \"/existential\": \"\\u2203\",\n    \"/expressionlessFace\": \"\\u1F611\",\n    \"/extraterrestrialAlien\": \"\\u1F47D\",\n    \"/eye\": \"\\u1F441\",\n    \"/eyeglasses\": \"\\u1F453\",\n    \"/eyes\": \"\\u1F440\",\n    \"/ezh\": \"\\u0292\",\n    \"/ezhcaron\": \"\\u01EF\",\n    \"/ezhcurl\": \"\\u0293\",\n    \"/ezhreversed\": \"\\u01B9\",\n    \"/ezhtail\": \"\\u01BA\",\n    \"/f\": \"\\u0066\",\n    \"/f_f\": \"\\uFB00\",\n    \"/f_f_i\": \"\\uFB03\",\n    \"/f_f_l\": \"\\uFB04\",\n    \"/faceMassage\": \"\\u1F486\",\n    \"/faceSavouringDeliciousFood\": \"\\u1F60B\",\n    \"/faceScreamingInFear\": \"\\u1F631\",\n    \"/faceThrowingAKiss\": \"\\u1F618\",\n    \"/faceWithColdSweat\": \"\\u1F613\",\n    \"/faceWithLookOfTriumph\": \"\\u1F624\",\n    \"/faceWithMedicalMask\": \"\\u1F637\",\n    \"/faceWithNoGoodGesture\": \"\\u1F645\",\n    \"/faceWithOkGesture\": \"\\u1F646\",\n    \"/faceWithOpenMouth\": \"\\u1F62E\",\n    \"/faceWithOpenMouthAndColdSweat\": \"\\u1F630\",\n    \"/faceWithRollingEyes\": \"\\u1F644\",\n    \"/faceWithStuckOutTongue\": \"\\u1F61B\",\n    \"/faceWithStuckOutTongueAndTightlyClosedEyes\": \"\\u1F61D\",\n    \"/faceWithStuckOutTongueAndWinkingEye\": \"\\u1F61C\",\n    \"/faceWithTearsOfJoy\": \"\\u1F602\",\n    \"/faceWithoutMouth\": \"\\u1F636\",\n    \"/facsimile\": \"\\u213B\",\n    \"/factory\": \"\\u1F3ED\",\n    \"/fadeva\": \"\\u095E\",\n    \"/fagurmukhi\": \"\\u0A5E\",\n    \"/fahrenheit\": \"\\u2109\",\n    \"/fallenLeaf\": \"\\u1F342\",\n    \"/fallingdiagonal\": \"\\u27CD\",\n    \"/fallingdiagonalincircleinsquareblackwhite\": \"\\u26DE\",\n    \"/family\": \"\\u1F46A\",\n    \"/farsi\": \"\\u262B\",\n    \"/farsiYehDigitFourBelow\": \"\\u0777\",\n    \"/farsiYehDigitThreeAbove\": \"\\u0776\",\n    \"/farsiYehDigitTwoAbove\": \"\\u0775\",\n    \"/fatha\": \"\\u064E\",\n    \"/fathaIsol\": \"\\uFE76\",\n    \"/fathaMedi\": \"\\uFE77\",\n    \"/fathaarabic\": \"\\u064E\",\n    \"/fathalowarabic\": \"\\u064E\",\n    \"/fathasmall\": \"\\u0618\",\n    \"/fathatan\": \"\\u064B\",\n    \"/fathatanIsol\": \"\\uFE70\",\n    \"/fathatanarabic\": \"\\u064B\",\n    \"/fathatwodotsdots\": \"\\u065E\",\n    \"/fatherChristmas\": \"\\u1F385\",\n    \"/faxIcon\": \"\\u1F5B7\",\n    \"/faxMachine\": \"\\u1F4E0\",\n    \"/fbopomofo\": \"\\u3108\",\n    \"/fcircle\": \"\\u24D5\",\n    \"/fdot\": \"\\u1E1F\",\n    \"/fdotaccent\": \"\\u1E1F\",\n    \"/fearfulFace\": \"\\u1F628\",\n    \"/februarytelegraph\": \"\\u32C1\",\n    \"/feh.fina\": \"\\uFED2\",\n    \"/feh.init\": \"\\uFED3\",\n    \"/feh.init_alefmaksura.fina\": \"\\uFC31\",\n    \"/feh.init_hah.fina\": \"\\uFC2E\",\n    \"/feh.init_hah.medi\": \"\\uFCBF\",\n    \"/feh.init_jeem.fina\": \"\\uFC2D\",\n    \"/feh.init_jeem.medi\": \"\\uFCBE\",\n    \"/feh.init_khah.fina\": \"\\uFC2F\",\n    \"/feh.init_khah.medi\": \"\\uFCC0\",\n    \"/feh.init_khah.medi_meem.medi\": \"\\uFD7D\",\n    \"/feh.init_meem.fina\": \"\\uFC30\",\n    \"/feh.init_meem.medi\": \"\\uFCC1\",\n    \"/feh.init_yeh.fina\": \"\\uFC32\",\n    \"/feh.isol\": \"\\uFED1\",\n    \"/feh.medi\": \"\\uFED4\",\n    \"/feh.medi_alefmaksura.fina\": \"\\uFC7C\",\n    \"/feh.medi_khah.medi_meem.fina\": \"\\uFD7C\",\n    \"/feh.medi_meem.medi_yeh.fina\": \"\\uFDC1\",\n    \"/feh.medi_yeh.fina\": \"\\uFC7D\",\n    \"/fehThreeDotsUpBelow\": \"\\u0761\",\n    \"/fehTwoDotsBelow\": \"\\u0760\",\n    \"/feharabic\": \"\\u0641\",\n    \"/feharmenian\": \"\\u0586\",\n    \"/fehdotbelow\": \"\\u06A3\",\n    \"/fehdotbelowright\": \"\\u06A2\",\n    \"/fehfinalarabic\": \"\\uFED2\",\n    \"/fehinitialarabic\": \"\\uFED3\",\n    \"/fehmedialarabic\": \"\\uFED4\",\n    \"/fehthreedotsbelow\": \"\\u06A5\",\n    \"/feicoptic\": \"\\u03E5\",\n    \"/female\": \"\\u2640\",\n    \"/femaleideographiccircled\": \"\\u329B\",\n    \"/feng\": \"\\u02A9\",\n    \"/ferrisWheel\": \"\\u1F3A1\",\n    \"/ferry\": \"\\u26F4\",\n    \"/festivalideographicparen\": \"\\u3240\",\n    \"/ff\": \"\\uFB00\",\n    \"/ffi\": \"\\uFB03\",\n    \"/ffl\": \"\\uFB04\",\n    \"/fhook\": \"\\u0192\",\n    \"/fi\": \"\\uFB01\",  # ligature \"fi\"\n    \"/fieldHockeyStickAndBall\": \"\\u1F3D1\",\n    \"/fifteencircle\": \"\\u246E\",\n    \"/fifteencircleblack\": \"\\u24EF\",\n    \"/fifteenparen\": \"\\u2482\",\n    \"/fifteenparenthesized\": \"\\u2482\",\n    \"/fifteenperiod\": \"\\u2496\",\n    \"/fifty.roman\": \"\\u216C\",\n    \"/fifty.romansmall\": \"\\u217C\",\n    \"/fiftycircle\": \"\\u32BF\",\n    \"/fiftycirclesquare\": \"\\u324C\",\n    \"/fiftyearlyform.roman\": \"\\u2186\",\n    \"/fiftythousand.roman\": \"\\u2187\",\n    \"/figuredash\": \"\\u2012\",\n    \"/figurespace\": \"\\u2007\",\n    \"/fileCabinet\": \"\\u1F5C4\",\n    \"/fileFolder\": \"\\u1F4C1\",\n    \"/filledbox\": \"\\u25A0\",\n    \"/filledrect\": \"\\u25AC\",\n    \"/filledstopabove\": \"\\u06EC\",\n    \"/filmFrames\": \"\\u1F39E\",\n    \"/filmProjector\": \"\\u1F4FD\",\n    \"/finalkaf\": \"\\u05DA\",\n    \"/finalkaf:hb\": \"\\u05DA\",\n    \"/finalkafdagesh\": \"\\uFB3A\",\n    \"/finalkafdageshhebrew\": \"\\uFB3A\",\n    \"/finalkafhebrew\": \"\\u05DA\",\n    \"/finalkafqamats\": \"\\u05DA\",\n    \"/finalkafqamatshebrew\": \"\\u05DA\",\n    \"/finalkafsheva\": \"\\u05DA\",\n    \"/finalkafshevahebrew\": \"\\u05DA\",\n    \"/finalkafwithdagesh:hb\": \"\\uFB3A\",\n    \"/finalmem\": \"\\u05DD\",\n    \"/finalmem:hb\": \"\\u05DD\",\n    \"/finalmemhebrew\": \"\\u05DD\",\n    \"/finalmemwide:hb\": \"\\uFB26\",\n    \"/finalnun\": \"\\u05DF\",\n    \"/finalnun:hb\": \"\\u05DF\",\n    \"/finalnunhebrew\": \"\\u05DF\",\n    \"/finalpe\": \"\\u05E3\",\n    \"/finalpe:hb\": \"\\u05E3\",\n    \"/finalpehebrew\": \"\\u05E3\",\n    \"/finalpewithdagesh:hb\": \"\\uFB43\",\n    \"/finalsigma\": \"\\u03C2\",\n    \"/finaltsadi\": \"\\u05E5\",\n    \"/finaltsadi:hb\": \"\\u05E5\",\n    \"/finaltsadihebrew\": \"\\u05E5\",\n    \"/financialideographiccircled\": \"\\u3296\",\n    \"/financialideographicparen\": \"\\u3236\",\n    \"/finsular\": \"\\uA77C\",\n    \"/fire\": \"\\u1F525\",\n    \"/fireEngine\": \"\\u1F692\",\n    \"/fireideographiccircled\": \"\\u328B\",\n    \"/fireideographicparen\": \"\\u322B\",\n    \"/fireworkSparkler\": \"\\u1F387\",\n    \"/fireworks\": \"\\u1F386\",\n    \"/firstQuarterMoon\": \"\\u1F313\",\n    \"/firstQuarterMoonFace\": \"\\u1F31B\",\n    \"/firstquartermoon\": \"\\u263D\",\n    \"/firststrongisolate\": \"\\u2068\",\n    \"/firsttonechinese\": \"\\u02C9\",\n    \"/fish\": \"\\u1F41F\",\n    \"/fishCakeSwirlDesign\": \"\\u1F365\",\n    \"/fisheye\": \"\\u25C9\",\n    \"/fishingPoleAndFish\": \"\\u1F3A3\",\n    \"/fistedHandSign\": \"\\u1F44A\",\n    \"/fitacyr\": \"\\u0473\",\n    \"/fitacyrillic\": \"\\u0473\",\n    \"/five\": \"\\u0035\",\n    \"/five.inferior\": \"\\u2085\",\n    \"/five.roman\": \"\\u2164\",\n    \"/five.romansmall\": \"\\u2174\",\n    \"/five.superior\": \"\\u2075\",\n    \"/fivearabic\": \"\\u0665\",\n    \"/fivebengali\": \"\\u09EB\",\n    \"/fivecircle\": \"\\u2464\",\n    \"/fivecircledbl\": \"\\u24F9\",\n    \"/fivecircleinversesansserif\": \"\\u278E\",\n    \"/fivecomma\": \"\\u1F106\",\n    \"/fivedeva\": \"\\u096B\",\n    \"/fivedot\": \"\\u2E2D\",\n    \"/fivedotpunctuation\": \"\\u2059\",\n    \"/fiveeighths\": \"\\u215D\",\n    \"/fivefar\": \"\\u06F5\",\n    \"/fivegujarati\": \"\\u0AEB\",\n    \"/fivegurmukhi\": \"\\u0A6B\",\n    \"/fivehackarabic\": \"\\u0665\",\n    \"/fivehangzhou\": \"\\u3025\",\n    \"/fivehundred.roman\": \"\\u216E\",\n    \"/fivehundred.romansmall\": \"\\u217E\",\n    \"/fiveideographiccircled\": \"\\u3284\",\n    \"/fiveideographicparen\": \"\\u3224\",\n    \"/fiveinferior\": \"\\u2085\",\n    \"/fivemonospace\": \"\\uFF15\",\n    \"/fiveoldstyle\": \"\\uF735\",\n    \"/fiveparen\": \"\\u2478\",\n    \"/fiveparenthesized\": \"\\u2478\",\n    \"/fiveperiod\": \"\\u248C\",\n    \"/fivepersian\": \"\\u06F5\",\n    \"/fivepointedstar\": \"\\u066D\",\n    \"/fivepointonesquare\": \"\\u1F1A0\",\n    \"/fiveroman\": \"\\u2174\",\n    \"/fivesixths\": \"\\u215A\",\n    \"/fivesuperior\": \"\\u2075\",\n    \"/fivethai\": \"\\u0E55\",\n    \"/fivethousand.roman\": \"\\u2181\",\n    \"/fl\": \"\\uFB02\",\n    \"/flagblack\": \"\\u2691\",\n    \"/flaghorizontalmiddlestripeblackwhite\": \"\\u26FF\",\n    \"/flaginhole\": \"\\u26F3\",\n    \"/flagwhite\": \"\\u2690\",\n    \"/flatness\": \"\\u23E5\",\n    \"/fleurdelis\": \"\\u269C\",\n    \"/flexedBiceps\": \"\\u1F4AA\",\n    \"/floorleft\": \"\\u230A\",\n    \"/floorright\": \"\\u230B\",\n    \"/floppyDisk\": \"\\u1F4BE\",\n    \"/floralheartbulletreversedrotated\": \"\\u2619\",\n    \"/florin\": \"\\u0192\",\n    \"/flower\": \"\\u2698\",\n    \"/flowerPlayingCards\": \"\\u1F3B4\",\n    \"/flowerpunctuationmark\": \"\\u2055\",\n    \"/flushedFace\": \"\\u1F633\",\n    \"/flyingEnvelope\": \"\\u1F585\",\n    \"/flyingSaucer\": \"\\u1F6F8\",\n    \"/fmfullwidth\": \"\\u3399\",\n    \"/fmonospace\": \"\\uFF46\",\n    \"/fmsquare\": \"\\u3399\",\n    \"/fofanthai\": \"\\u0E1F\",\n    \"/fofathai\": \"\\u0E1D\",\n    \"/fog\": \"\\u1F32B\",\n    \"/foggy\": \"\\u1F301\",\n    \"/folder\": \"\\u1F5C0\",\n    \"/fongmanthai\": \"\\u0E4F\",\n    \"/footnote\": \"\\u0602\",\n    \"/footprints\": \"\\u1F463\",\n    \"/footsquare\": \"\\u23CD\",\n    \"/forall\": \"\\u2200\",\n    \"/forces\": \"\\u22A9\",\n    \"/fork\": \"\\u2442\",\n    \"/forkKnife\": \"\\u1F374\",\n    \"/forkKnifePlate\": \"\\u1F37D\",\n    \"/forsamaritan\": \"\\u214F\",\n    \"/fortycircle\": \"\\u32B5\",\n    \"/fortycirclesquare\": \"\\u324B\",\n    \"/fortyeightcircle\": \"\\u32BD\",\n    \"/fortyfivecircle\": \"\\u32BA\",\n    \"/fortyfourcircle\": \"\\u32B9\",\n    \"/fortyninecircle\": \"\\u32BE\",\n    \"/fortyonecircle\": \"\\u32B6\",\n    \"/fortysevencircle\": \"\\u32BC\",\n    \"/fortysixcircle\": \"\\u32BB\",\n    \"/fortythreecircle\": \"\\u32B8\",\n    \"/fortytwocircle\": \"\\u32B7\",\n    \"/fountain\": \"\\u26F2\",\n    \"/four\": \"\\u0034\",\n    \"/four.inferior\": \"\\u2084\",\n    \"/four.roman\": \"\\u2163\",\n    \"/four.romansmall\": \"\\u2173\",\n    \"/four.superior\": \"\\u2074\",\n    \"/fourLeafClover\": \"\\u1F340\",\n    \"/fourarabic\": \"\\u0664\",\n    \"/fourbengali\": \"\\u09EA\",\n    \"/fourcircle\": \"\\u2463\",\n    \"/fourcircledbl\": \"\\u24F8\",\n    \"/fourcircleinversesansserif\": \"\\u278D\",\n    \"/fourcomma\": \"\\u1F105\",\n    \"/fourdeva\": \"\\u096A\",\n    \"/fourdotmark\": \"\\u205B\",\n    \"/fourdotpunctuation\": \"\\u2058\",\n    \"/fourfar\": \"\\u06F4\",\n    \"/fourfifths\": \"\\u2158\",\n    \"/fourgujarati\": \"\\u0AEA\",\n    \"/fourgurmukhi\": \"\\u0A6A\",\n    \"/fourhackarabic\": \"\\u0664\",\n    \"/fourhangzhou\": \"\\u3024\",\n    \"/fourideographiccircled\": \"\\u3283\",\n    \"/fourideographicparen\": \"\\u3223\",\n    \"/fourinferior\": \"\\u2084\",\n    \"/fourksquare\": \"\\u1F19E\",\n    \"/fourmonospace\": \"\\uFF14\",\n    \"/fournumeratorbengali\": \"\\u09F7\",\n    \"/fouroldstyle\": \"\\uF734\",\n    \"/fourparen\": \"\\u2477\",\n    \"/fourparenthesized\": \"\\u2477\",\n    \"/fourperemspace\": \"\\u2005\",\n    \"/fourperiod\": \"\\u248B\",\n    \"/fourpersian\": \"\\u06F4\",\n    \"/fourroman\": \"\\u2173\",\n    \"/foursuperior\": \"\\u2074\",\n    \"/fourteencircle\": \"\\u246D\",\n    \"/fourteencircleblack\": \"\\u24EE\",\n    \"/fourteenparen\": \"\\u2481\",\n    \"/fourteenparenthesized\": \"\\u2481\",\n    \"/fourteenperiod\": \"\\u2495\",\n    \"/fourthai\": \"\\u0E54\",\n    \"/fourthtonechinese\": \"\\u02CB\",\n    \"/fparen\": \"\\u24A1\",\n    \"/fparenthesized\": \"\\u24A1\",\n    \"/fraction\": \"\\u2044\",\n    \"/frameAnX\": \"\\u1F5BE\",\n    \"/framePicture\": \"\\u1F5BC\",\n    \"/frameTiles\": \"\\u1F5BD\",\n    \"/franc\": \"\\u20A3\",\n    \"/freesquare\": \"\\u1F193\",\n    \"/frenchFries\": \"\\u1F35F\",\n    \"/freversedepigraphic\": \"\\uA7FB\",\n    \"/friedShrimp\": \"\\u1F364\",\n    \"/frogFace\": \"\\u1F438\",\n    \"/front-facingBabyChick\": \"\\u1F425\",\n    \"/frown\": \"\\u2322\",\n    \"/frowningFaceWithOpenMouth\": \"\\u1F626\",\n    \"/frowningfacewhite\": \"\\u2639\",\n    \"/fstroke\": \"\\uA799\",\n    \"/fturned\": \"\\u214E\",\n    \"/fuelpump\": \"\\u26FD\",\n    \"/fullBlock\": \"\\u2588\",\n    \"/fullMoon\": \"\\u1F315\",\n    \"/fullMoonFace\": \"\\u1F31D\",\n    \"/functionapplication\": \"\\u2061\",\n    \"/funeralurn\": \"\\u26B1\",\n    \"/fuse\": \"\\u23DB\",\n    \"/fwd:A\": \"\\uFF21\",\n    \"/fwd:B\": \"\\uFF22\",\n    \"/fwd:C\": \"\\uFF23\",\n    \"/fwd:D\": \"\\uFF24\",\n    \"/fwd:E\": \"\\uFF25\",\n    \"/fwd:F\": \"\\uFF26\",\n    \"/fwd:G\": \"\\uFF27\",\n    \"/fwd:H\": \"\\uFF28\",\n    \"/fwd:I\": \"\\uFF29\",\n    \"/fwd:J\": \"\\uFF2A\",\n    \"/fwd:K\": \"\\uFF2B\",\n    \"/fwd:L\": \"\\uFF2C\",\n    \"/fwd:M\": \"\\uFF2D\",\n    \"/fwd:N\": \"\\uFF2E\",\n    \"/fwd:O\": \"\\uFF2F\",\n    \"/fwd:P\": \"\\uFF30\",\n    \"/fwd:Q\": \"\\uFF31\",\n    \"/fwd:R\": \"\\uFF32\",\n    \"/fwd:S\": \"\\uFF33\",\n    \"/fwd:T\": \"\\uFF34\",\n    \"/fwd:U\": \"\\uFF35\",\n    \"/fwd:V\": \"\\uFF36\",\n    \"/fwd:W\": \"\\uFF37\",\n    \"/fwd:X\": \"\\uFF38\",\n    \"/fwd:Y\": \"\\uFF39\",\n    \"/fwd:Z\": \"\\uFF3A\",\n    \"/fwd:a\": \"\\uFF41\",\n    \"/fwd:ampersand\": \"\\uFF06\",\n    \"/fwd:asciicircum\": \"\\uFF3E\",\n    \"/fwd:asciitilde\": \"\\uFF5E\",\n    \"/fwd:asterisk\": \"\\uFF0A\",\n    \"/fwd:at\": \"\\uFF20\",\n    \"/fwd:b\": \"\\uFF42\",\n    \"/fwd:backslash\": \"\\uFF3C\",\n    \"/fwd:bar\": \"\\uFF5C\",\n    \"/fwd:braceleft\": \"\\uFF5B\",\n    \"/fwd:braceright\": \"\\uFF5D\",\n    \"/fwd:bracketleft\": \"\\uFF3B\",\n    \"/fwd:bracketright\": \"\\uFF3D\",\n    \"/fwd:brokenbar\": \"\\uFFE4\",\n    \"/fwd:c\": \"\\uFF43\",\n    \"/fwd:centsign\": \"\\uFFE0\",\n    \"/fwd:colon\": \"\\uFF1A\",\n    \"/fwd:comma\": \"\\uFF0C\",\n    \"/fwd:d\": \"\\uFF44\",\n    \"/fwd:dollar\": \"\\uFF04\",\n    \"/fwd:e\": \"\\uFF45\",\n    \"/fwd:eight\": \"\\uFF18\",\n    \"/fwd:equal\": \"\\uFF1D\",\n    \"/fwd:exclam\": \"\\uFF01\",\n    \"/fwd:f\": \"\\uFF46\",\n    \"/fwd:five\": \"\\uFF15\",\n    \"/fwd:four\": \"\\uFF14\",\n    \"/fwd:g\": \"\\uFF47\",\n    \"/fwd:grave\": \"\\uFF40\",\n    \"/fwd:greater\": \"\\uFF1E\",\n    \"/fwd:h\": \"\\uFF48\",\n    \"/fwd:hyphen\": \"\\uFF0D\",\n    \"/fwd:i\": \"\\uFF49\",\n    \"/fwd:j\": \"\\uFF4A\",\n    \"/fwd:k\": \"\\uFF4B\",\n    \"/fwd:l\": \"\\uFF4C\",\n    \"/fwd:leftwhiteparenthesis\": \"\\uFF5F\",\n    \"/fwd:less\": \"\\uFF1C\",\n    \"/fwd:m\": \"\\uFF4D\",\n    \"/fwd:macron\": \"\\uFFE3\",\n    \"/fwd:n\": \"\\uFF4E\",\n    \"/fwd:nine\": \"\\uFF19\",\n    \"/fwd:notsign\": \"\\uFFE2\",\n    \"/fwd:numbersign\": \"\\uFF03\",\n    \"/fwd:o\": \"\\uFF4F\",\n    \"/fwd:one\": \"\\uFF11\",\n    \"/fwd:p\": \"\\uFF50\",\n    \"/fwd:parenthesisleft\": \"\\uFF08\",\n    \"/fwd:parenthesisright\": \"\\uFF09\",\n    \"/fwd:percent\": \"\\uFF05\",\n    \"/fwd:period\": \"\\uFF0E\",\n    \"/fwd:plus\": \"\\uFF0B\",\n    \"/fwd:poundsign\": \"\\uFFE1\",\n    \"/fwd:q\": \"\\uFF51\",\n    \"/fwd:question\": \"\\uFF1F\",\n    \"/fwd:quotedbl\": \"\\uFF02\",\n    \"/fwd:quotesingle\": \"\\uFF07\",\n    \"/fwd:r\": \"\\uFF52\",\n    \"/fwd:rightwhiteparenthesis\": \"\\uFF60\",\n    \"/fwd:s\": \"\\uFF53\",\n    \"/fwd:semicolon\": \"\\uFF1B\",\n    \"/fwd:seven\": \"\\uFF17\",\n    \"/fwd:six\": \"\\uFF16\",\n    \"/fwd:slash\": \"\\uFF0F\",\n    \"/fwd:t\": \"\\uFF54\",\n    \"/fwd:three\": \"\\uFF13\",\n    \"/fwd:two\": \"\\uFF12\",\n    \"/fwd:u\": \"\\uFF55\",\n    \"/fwd:underscore\": \"\\uFF3F\",\n    \"/fwd:v\": \"\\uFF56\",\n    \"/fwd:w\": \"\\uFF57\",\n    \"/fwd:wonsign\": \"\\uFFE6\",\n    \"/fwd:x\": \"\\uFF58\",\n    \"/fwd:y\": \"\\uFF59\",\n    \"/fwd:yensign\": \"\\uFFE5\",\n    \"/fwd:z\": \"\\uFF5A\",\n    \"/fwd:zero\": \"\\uFF10\",\n    \"/g\": \"\\u0067\",\n    \"/gabengali\": \"\\u0997\",\n    \"/gacute\": \"\\u01F5\",\n    \"/gadeva\": \"\\u0917\",\n    \"/gaf\": \"\\u06AF\",\n    \"/gaf.fina\": \"\\uFB93\",\n    \"/gaf.init\": \"\\uFB94\",\n    \"/gaf.isol\": \"\\uFB92\",\n    \"/gaf.medi\": \"\\uFB95\",\n    \"/gafarabic\": \"\\u06AF\",\n    \"/gaffinalarabic\": \"\\uFB93\",\n    \"/gafinitialarabic\": \"\\uFB94\",\n    \"/gafmedialarabic\": \"\\uFB95\",\n    \"/gafring\": \"\\u06B0\",\n    \"/gafthreedotsabove\": \"\\u06B4\",\n    \"/gaftwodotsbelow\": \"\\u06B2\",\n    \"/gagujarati\": \"\\u0A97\",\n    \"/gagurmukhi\": \"\\u0A17\",\n    \"/gahiragana\": \"\\u304C\",\n    \"/gakatakana\": \"\\u30AC\",\n    \"/galsquare\": \"\\u33FF\",\n    \"/gameDie\": \"\\u1F3B2\",\n    \"/gamma\": \"\\u03B3\",\n    \"/gammadblstruck\": \"\\u213D\",\n    \"/gammalatinsmall\": \"\\u0263\",\n    \"/gammasuperior\": \"\\u02E0\",\n    \"/gammasupmod\": \"\\u02E0\",\n    \"/gamurda\": \"\\uA993\",\n    \"/gangiacoptic\": \"\\u03EB\",\n    \"/ganmasquare\": \"\\u330F\",\n    \"/garonsquare\": \"\\u330E\",\n    \"/gbfullwidth\": \"\\u3387\",\n    \"/gbopomofo\": \"\\u310D\",\n    \"/gbreve\": \"\\u011F\",\n    \"/gcaron\": \"\\u01E7\",\n    \"/gcedilla\": \"\\u0123\",\n    \"/gcircle\": \"\\u24D6\",\n    \"/gcircumflex\": \"\\u011D\",\n    \"/gcommaaccent\": \"\\u0123\",\n    \"/gdot\": \"\\u0121\",\n    \"/gdotaccent\": \"\\u0121\",\n    \"/gear\": \"\\u2699\",\n    \"/gearhles\": \"\\u26EE\",\n    \"/gearouthub\": \"\\u26ED\",\n    \"/gecyr\": \"\\u0433\",\n    \"/gecyrillic\": \"\\u0433\",\n    \"/gehiragana\": \"\\u3052\",\n    \"/gehookcyr\": \"\\u0495\",\n    \"/gehookstrokecyr\": \"\\u04FB\",\n    \"/gekatakana\": \"\\u30B2\",\n    \"/gemStone\": \"\\u1F48E\",\n    \"/gemini\": \"\\u264A\",\n    \"/geometricallyequal\": \"\\u2251\",\n    \"/geometricallyequivalent\": \"\\u224E\",\n    \"/geometricproportion\": \"\\u223A\",\n    \"/geresh:hb\": \"\\u05F3\",\n    \"/gereshMuqdam:hb\": \"\\u059D\",\n    \"/gereshaccenthebrew\": \"\\u059C\",\n    \"/gereshhebrew\": \"\\u05F3\",\n    \"/gereshmuqdamhebrew\": \"\\u059D\",\n    \"/germandbls\": \"\\u00DF\",\n    \"/germanpenny\": \"\\u20B0\",\n    \"/gershayim:hb\": \"\\u05F4\",\n    \"/gershayimaccenthebrew\": \"\\u059E\",\n    \"/gershayimhebrew\": \"\\u05F4\",\n    \"/gestrokecyr\": \"\\u0493\",\n    \"/getailcyr\": \"\\u04F7\",\n    \"/getamark\": \"\\u3013\",\n    \"/geupcyr\": \"\\u0491\",\n    \"/ghabengali\": \"\\u0998\",\n    \"/ghadarmenian\": \"\\u0572\",\n    \"/ghadeva\": \"\\u0918\",\n    \"/ghagujarati\": \"\\u0A98\",\n    \"/ghagurmukhi\": \"\\u0A18\",\n    \"/ghain\": \"\\u063A\",\n    \"/ghain.fina\": \"\\uFECE\",\n    \"/ghain.init\": \"\\uFECF\",\n    \"/ghain.init_alefmaksura.fina\": \"\\uFCF9\",\n    \"/ghain.init_jeem.fina\": \"\\uFC2B\",\n    \"/ghain.init_jeem.medi\": \"\\uFCBC\",\n    \"/ghain.init_meem.fina\": \"\\uFC2C\",\n    \"/ghain.init_meem.medi\": \"\\uFCBD\",\n    \"/ghain.init_yeh.fina\": \"\\uFCFA\",\n    \"/ghain.isol\": \"\\uFECD\",\n    \"/ghain.medi\": \"\\uFED0\",\n    \"/ghain.medi_alefmaksura.fina\": \"\\uFD15\",\n    \"/ghain.medi_meem.medi_alefmaksura.fina\": \"\\uFD7B\",\n    \"/ghain.medi_meem.medi_meem.fina\": \"\\uFD79\",\n    \"/ghain.medi_meem.medi_yeh.fina\": \"\\uFD7A\",\n    \"/ghain.medi_yeh.fina\": \"\\uFD16\",\n    \"/ghainarabic\": \"\\u063A\",\n    \"/ghaindotbelow\": \"\\u06FC\",\n    \"/ghainfinalarabic\": \"\\uFECE\",\n    \"/ghaininitialarabic\": \"\\uFECF\",\n    \"/ghainmedialarabic\": \"\\uFED0\",\n    \"/ghemiddlehookcyrillic\": \"\\u0495\",\n    \"/ghestrokecyrillic\": \"\\u0493\",\n    \"/gheupturncyrillic\": \"\\u0491\",\n    \"/ghhadeva\": \"\\u095A\",\n    \"/ghhagurmukhi\": \"\\u0A5A\",\n    \"/ghook\": \"\\u0260\",\n    \"/ghost\": \"\\u1F47B\",\n    \"/ghzfullwidth\": \"\\u3393\",\n    \"/ghzsquare\": \"\\u3393\",\n    \"/gigasquare\": \"\\u3310\",\n    \"/gihiragana\": \"\\u304E\",\n    \"/gikatakana\": \"\\u30AE\",\n    \"/gimarmenian\": \"\\u0563\",\n    \"/gimel\": \"\\u05D2\",\n    \"/gimel:hb\": \"\\u05D2\",\n    \"/gimeldagesh\": \"\\uFB32\",\n    \"/gimeldageshhebrew\": \"\\uFB32\",\n    \"/gimelhebrew\": \"\\u05D2\",\n    \"/gimelwithdagesh:hb\": \"\\uFB32\",\n    \"/giniisquare\": \"\\u3311\",\n    \"/ginsularturned\": \"\\uA77F\",\n    \"/girl\": \"\\u1F467\",\n    \"/girls\": \"\\u1F6CA\",\n    \"/girudaasquare\": \"\\u3313\",\n    \"/gjecyr\": \"\\u0453\",\n    \"/gjecyrillic\": \"\\u0453\",\n    \"/globeMeridians\": \"\\u1F310\",\n    \"/glottalinvertedstroke\": \"\\u01BE\",\n    \"/glottalstop\": \"\\u0294\",\n    \"/glottalstopinverted\": \"\\u0296\",\n    \"/glottalstopmod\": \"\\u02C0\",\n    \"/glottalstopreversed\": \"\\u0295\",\n    \"/glottalstopreversedmod\": \"\\u02C1\",\n    \"/glottalstopreversedsuperior\": \"\\u02E4\",\n    \"/glottalstopstroke\": \"\\u02A1\",\n    \"/glottalstopstrokereversed\": \"\\u02A2\",\n    \"/glottalstopsupreversedmod\": \"\\u02E4\",\n    \"/glowingStar\": \"\\u1F31F\",\n    \"/gmacron\": \"\\u1E21\",\n    \"/gmonospace\": \"\\uFF47\",\n    \"/gmtr:diamondblack\": \"\\u25C6\",\n    \"/gmtr:diamondwhite\": \"\\u25C7\",\n    \"/gnrl:hyphen\": \"\\u2010\",\n    \"/goat\": \"\\u1F410\",\n    \"/gobliquestroke\": \"\\uA7A1\",\n    \"/gohiragana\": \"\\u3054\",\n    \"/gokatakana\": \"\\u30B4\",\n    \"/golfer\": \"\\u1F3CC\",\n    \"/gpafullwidth\": \"\\u33AC\",\n    \"/gparen\": \"\\u24A2\",\n    \"/gparenthesized\": \"\\u24A2\",\n    \"/gpasquare\": \"\\u33AC\",\n    \"/gr:acute\": \"\\u1FFD\",\n    \"/gr:grave\": \"\\u1FEF\",\n    \"/gr:question\": \"\\u037E\",\n    \"/gr:tilde\": \"\\u1FC0\",\n    \"/gradient\": \"\\u2207\",\n    \"/graduationCap\": \"\\u1F393\",\n    \"/grapes\": \"\\u1F347\",\n    \"/grave\": \"\\u0060\",\n    \"/gravebelowcmb\": \"\\u0316\",\n    \"/gravecmb\": \"\\u0300\",\n    \"/gravecomb\": \"\\u0300\",\n    \"/gravedblmiddlemod\": \"\\u02F5\",\n    \"/gravedeva\": \"\\u0953\",\n    \"/gravelowmod\": \"\\u02CE\",\n    \"/gravemiddlemod\": \"\\u02F4\",\n    \"/gravemod\": \"\\u02CB\",\n    \"/gravemonospace\": \"\\uFF40\",\n    \"/gravetonecmb\": \"\\u0340\",\n    \"/greater\": \"\\u003E\",\n    \"/greaterbutnotequal\": \"\\u2269\",\n    \"/greaterbutnotequivalent\": \"\\u22E7\",\n    \"/greaterdot\": \"\\u22D7\",\n    \"/greaterequal\": \"\\u2265\",\n    \"/greaterequalorless\": \"\\u22DB\",\n    \"/greatermonospace\": \"\\uFF1E\",\n    \"/greaterorequivalent\": \"\\u2273\",\n    \"/greaterorless\": \"\\u2277\",\n    \"/greateroverequal\": \"\\u2267\",\n    \"/greatersmall\": \"\\uFE65\",\n    \"/greenApple\": \"\\u1F34F\",\n    \"/greenBook\": \"\\u1F4D7\",\n    \"/greenHeart\": \"\\u1F49A\",\n    \"/grimacingFace\": \"\\u1F62C\",\n    \"/grinningCatFaceWithSmilingEyes\": \"\\u1F638\",\n    \"/grinningFace\": \"\\u1F600\",\n    \"/grinningFaceWithSmilingEyes\": \"\\u1F601\",\n    \"/growingHeart\": \"\\u1F497\",\n    \"/gscript\": \"\\u0261\",\n    \"/gstroke\": \"\\u01E5\",\n    \"/guarani\": \"\\u20B2\",\n    \"/guardsman\": \"\\u1F482\",\n    \"/gueh\": \"\\u06B3\",\n    \"/gueh.fina\": \"\\uFB97\",\n    \"/gueh.init\": \"\\uFB98\",\n    \"/gueh.isol\": \"\\uFB96\",\n    \"/gueh.medi\": \"\\uFB99\",\n    \"/guhiragana\": \"\\u3050\",\n    \"/guillemetleft\": \"\\u00AB\",\n    \"/guillemetright\": \"\\u00BB\",\n    \"/guillemotleft\": \"\\u00AB\",\n    \"/guillemotright\": \"\\u00BB\",\n    \"/guilsinglleft\": \"\\u2039\",\n    \"/guilsinglright\": \"\\u203A\",\n    \"/guitar\": \"\\u1F3B8\",\n    \"/gujr:a\": \"\\u0A85\",\n    \"/gujr:aa\": \"\\u0A86\",\n    \"/gujr:aasign\": \"\\u0ABE\",\n    \"/gujr:abbreviation\": \"\\u0AF0\",\n    \"/gujr:ai\": \"\\u0A90\",\n    \"/gujr:aisign\": \"\\u0AC8\",\n    \"/gujr:anusvara\": \"\\u0A82\",\n    \"/gujr:au\": \"\\u0A94\",\n    \"/gujr:ausign\": \"\\u0ACC\",\n    \"/gujr:avagraha\": \"\\u0ABD\",\n    \"/gujr:ba\": \"\\u0AAC\",\n    \"/gujr:bha\": \"\\u0AAD\",\n    \"/gujr:binducandra\": \"\\u0A81\",\n    \"/gujr:ca\": \"\\u0A9A\",\n    \"/gujr:cha\": \"\\u0A9B\",\n    \"/gujr:circlenuktaabove\": \"\\u0AFE\",\n    \"/gujr:da\": \"\\u0AA6\",\n    \"/gujr:dda\": \"\\u0AA1\",\n    \"/gujr:ddha\": \"\\u0AA2\",\n    \"/gujr:dha\": \"\\u0AA7\",\n    \"/gujr:e\": \"\\u0A8F\",\n    \"/gujr:ecandra\": \"\\u0A8D\",\n    \"/gujr:eight\": \"\\u0AEE\",\n    \"/gujr:esign\": \"\\u0AC7\",\n    \"/gujr:esigncandra\": \"\\u0AC5\",\n    \"/gujr:five\": \"\\u0AEB\",\n    \"/gujr:four\": \"\\u0AEA\",\n    \"/gujr:ga\": \"\\u0A97\",\n    \"/gujr:gha\": \"\\u0A98\",\n    \"/gujr:ha\": \"\\u0AB9\",\n    \"/gujr:i\": \"\\u0A87\",\n    \"/gujr:ii\": \"\\u0A88\",\n    \"/gujr:iisign\": \"\\u0AC0\",\n    \"/gujr:isign\": \"\\u0ABF\",\n    \"/gujr:ja\": \"\\u0A9C\",\n    \"/gujr:jha\": \"\\u0A9D\",\n    \"/gujr:ka\": \"\\u0A95\",\n    \"/gujr:kha\": \"\\u0A96\",\n    \"/gujr:la\": \"\\u0AB2\",\n    \"/gujr:lla\": \"\\u0AB3\",\n    \"/gujr:llvocal\": \"\\u0AE1\",\n    \"/gujr:llvocalsign\": \"\\u0AE3\",\n    \"/gujr:lvocal\": \"\\u0A8C\",\n    \"/gujr:lvocalsign\": \"\\u0AE2\",\n    \"/gujr:ma\": \"\\u0AAE\",\n    \"/gujr:maddah\": \"\\u0AFC\",\n    \"/gujr:na\": \"\\u0AA8\",\n    \"/gujr:nga\": \"\\u0A99\",\n    \"/gujr:nine\": \"\\u0AEF\",\n    \"/gujr:nna\": \"\\u0AA3\",\n    \"/gujr:nukta\": \"\\u0ABC\",\n    \"/gujr:nya\": \"\\u0A9E\",\n    \"/gujr:o\": \"\\u0A93\",\n    \"/gujr:ocandra\": \"\\u0A91\",\n    \"/gujr:om\": \"\\u0AD0\",\n    \"/gujr:one\": \"\\u0AE7\",\n    \"/gujr:osign\": \"\\u0ACB\",\n    \"/gujr:osigncandra\": \"\\u0AC9\",\n    \"/gujr:pa\": \"\\u0AAA\",\n    \"/gujr:pha\": \"\\u0AAB\",\n    \"/gujr:ra\": \"\\u0AB0\",\n    \"/gujr:rrvocal\": \"\\u0AE0\",\n    \"/gujr:rrvocalsign\": \"\\u0AC4\",\n    \"/gujr:rupee\": \"\\u0AF1\",\n    \"/gujr:rvocal\": \"\\u0A8B\",\n    \"/gujr:rvocalsign\": \"\\u0AC3\",\n    \"/gujr:sa\": \"\\u0AB8\",\n    \"/gujr:seven\": \"\\u0AED\",\n    \"/gujr:sha\": \"\\u0AB6\",\n    \"/gujr:shadda\": \"\\u0AFB\",\n    \"/gujr:six\": \"\\u0AEC\",\n    \"/gujr:ssa\": \"\\u0AB7\",\n    \"/gujr:sukun\": \"\\u0AFA\",\n    \"/gujr:ta\": \"\\u0AA4\",\n    \"/gujr:tha\": \"\\u0AA5\",\n    \"/gujr:three\": \"\\u0AE9\",\n    \"/gujr:three-dotnuktaabove\": \"\\u0AFD\",\n    \"/gujr:tta\": \"\\u0A9F\",\n    \"/gujr:ttha\": \"\\u0AA0\",\n    \"/gujr:two\": \"\\u0AE8\",\n    \"/gujr:two-circlenuktaabove\": \"\\u0AFF\",\n    \"/gujr:u\": \"\\u0A89\",\n    \"/gujr:usign\": \"\\u0AC1\",\n    \"/gujr:uu\": \"\\u0A8A\",\n    \"/gujr:uusign\": \"\\u0AC2\",\n    \"/gujr:va\": \"\\u0AB5\",\n    \"/gujr:virama\": \"\\u0ACD\",\n    \"/gujr:visarga\": \"\\u0A83\",\n    \"/gujr:ya\": \"\\u0AAF\",\n    \"/gujr:zero\": \"\\u0AE6\",\n    \"/gujr:zha\": \"\\u0AF9\",\n    \"/gukatakana\": \"\\u30B0\",\n    \"/guramusquare\": \"\\u3318\",\n    \"/guramutonsquare\": \"\\u3319\",\n    \"/guru:a\": \"\\u0A05\",\n    \"/guru:aa\": \"\\u0A06\",\n    \"/guru:aasign\": \"\\u0A3E\",\n    \"/guru:adakbindisign\": \"\\u0A01\",\n    \"/guru:addak\": \"\\u0A71\",\n    \"/guru:ai\": \"\\u0A10\",\n    \"/guru:aisign\": \"\\u0A48\",\n    \"/guru:au\": \"\\u0A14\",\n    \"/guru:ausign\": \"\\u0A4C\",\n    \"/guru:ba\": \"\\u0A2C\",\n    \"/guru:bha\": \"\\u0A2D\",\n    \"/guru:bindisign\": \"\\u0A02\",\n    \"/guru:ca\": \"\\u0A1A\",\n    \"/guru:cha\": \"\\u0A1B\",\n    \"/guru:da\": \"\\u0A26\",\n    \"/guru:dda\": \"\\u0A21\",\n    \"/guru:ddha\": \"\\u0A22\",\n    \"/guru:dha\": \"\\u0A27\",\n    \"/guru:ee\": \"\\u0A0F\",\n    \"/guru:eesign\": \"\\u0A47\",\n    \"/guru:eight\": \"\\u0A6E\",\n    \"/guru:ekonkar\": \"\\u0A74\",\n    \"/guru:fa\": \"\\u0A5E\",\n    \"/guru:five\": \"\\u0A6B\",\n    \"/guru:four\": \"\\u0A6A\",\n    \"/guru:ga\": \"\\u0A17\",\n    \"/guru:gha\": \"\\u0A18\",\n    \"/guru:ghha\": \"\\u0A5A\",\n    \"/guru:ha\": \"\\u0A39\",\n    \"/guru:i\": \"\\u0A07\",\n    \"/guru:ii\": \"\\u0A08\",\n    \"/guru:iisign\": \"\\u0A40\",\n    \"/guru:iri\": \"\\u0A72\",\n    \"/guru:isign\": \"\\u0A3F\",\n    \"/guru:ja\": \"\\u0A1C\",\n    \"/guru:jha\": \"\\u0A1D\",\n    \"/guru:ka\": \"\\u0A15\",\n    \"/guru:kha\": \"\\u0A16\",\n    \"/guru:khha\": \"\\u0A59\",\n    \"/guru:la\": \"\\u0A32\",\n    \"/guru:lla\": \"\\u0A33\",\n    \"/guru:ma\": \"\\u0A2E\",\n    \"/guru:na\": \"\\u0A28\",\n    \"/guru:nga\": \"\\u0A19\",\n    \"/guru:nine\": \"\\u0A6F\",\n    \"/guru:nna\": \"\\u0A23\",\n    \"/guru:nukta\": \"\\u0A3C\",\n    \"/guru:nya\": \"\\u0A1E\",\n    \"/guru:one\": \"\\u0A67\",\n    \"/guru:oo\": \"\\u0A13\",\n    \"/guru:oosign\": \"\\u0A4B\",\n    \"/guru:pa\": \"\\u0A2A\",\n    \"/guru:pha\": \"\\u0A2B\",\n    \"/guru:ra\": \"\\u0A30\",\n    \"/guru:rra\": \"\\u0A5C\",\n    \"/guru:sa\": \"\\u0A38\",\n    \"/guru:seven\": \"\\u0A6D\",\n    \"/guru:sha\": \"\\u0A36\",\n    \"/guru:six\": \"\\u0A6C\",\n    \"/guru:ta\": \"\\u0A24\",\n    \"/guru:tha\": \"\\u0A25\",\n    \"/guru:three\": \"\\u0A69\",\n    \"/guru:tippi\": \"\\u0A70\",\n    \"/guru:tta\": \"\\u0A1F\",\n    \"/guru:ttha\": \"\\u0A20\",\n    \"/guru:two\": \"\\u0A68\",\n    \"/guru:u\": \"\\u0A09\",\n    \"/guru:udaatsign\": \"\\u0A51\",\n    \"/guru:ura\": \"\\u0A73\",\n    \"/guru:usign\": \"\\u0A41\",\n    \"/guru:uu\": \"\\u0A0A\",\n    \"/guru:uusign\": \"\\u0A42\",\n    \"/guru:va\": \"\\u0A35\",\n    \"/guru:virama\": \"\\u0A4D\",\n    \"/guru:visarga\": \"\\u0A03\",\n    \"/guru:ya\": \"\\u0A2F\",\n    \"/guru:yakashsign\": \"\\u0A75\",\n    \"/guru:za\": \"\\u0A5B\",\n    \"/guru:zero\": \"\\u0A66\",\n    \"/gyfullwidth\": \"\\u33C9\",\n    \"/gysquare\": \"\\u33C9\",\n    \"/h\": \"\\u0068\",\n    \"/h.inferior\": \"\\u2095\",\n    \"/haabkhasiancyrillic\": \"\\u04A9\",\n    \"/haabkhcyr\": \"\\u04A9\",\n    \"/haaltonearabic\": \"\\u06C1\",\n    \"/habengali\": \"\\u09B9\",\n    \"/hacirclekatakana\": \"\\u32E9\",\n    \"/hacyr\": \"\\u0445\",\n    \"/hadescendercyrillic\": \"\\u04B3\",\n    \"/hadeva\": \"\\u0939\",\n    \"/hafullwidth\": \"\\u33CA\",\n    \"/hagujarati\": \"\\u0AB9\",\n    \"/hagurmukhi\": \"\\u0A39\",\n    \"/hah\": \"\\u062D\",\n    \"/hah.fina\": \"\\uFEA2\",\n    \"/hah.init\": \"\\uFEA3\",\n    \"/hah.init_alefmaksura.fina\": \"\\uFCFF\",\n    \"/hah.init_jeem.fina\": \"\\uFC17\",\n    \"/hah.init_jeem.medi\": \"\\uFCA9\",\n    \"/hah.init_meem.fina\": \"\\uFC18\",\n    \"/hah.init_meem.medi\": \"\\uFCAA\",\n    \"/hah.init_yeh.fina\": \"\\uFD00\",\n    \"/hah.isol\": \"\\uFEA1\",\n    \"/hah.medi\": \"\\uFEA4\",\n    \"/hah.medi_alefmaksura.fina\": \"\\uFD1B\",\n    \"/hah.medi_jeem.medi_yeh.fina\": \"\\uFDBF\",\n    \"/hah.medi_meem.medi_alefmaksura.fina\": \"\\uFD5B\",\n    \"/hah.medi_meem.medi_yeh.fina\": \"\\uFD5A\",\n    \"/hah.medi_yeh.fina\": \"\\uFD1C\",\n    \"/hahDigitFourBelow\": \"\\u077C\",\n    \"/hahSmallTahAbove\": \"\\u0772\",\n    \"/hahSmallTahBelow\": \"\\u076E\",\n    \"/hahSmallTahTwoDots\": \"\\u076F\",\n    \"/hahThreeDotsUpBelow\": \"\\u0758\",\n    \"/hahTwoDotsAbove\": \"\\u0757\",\n    \"/haharabic\": \"\\u062D\",\n    \"/hahfinalarabic\": \"\\uFEA2\",\n    \"/hahhamza\": \"\\u0681\",\n    \"/hahinitialarabic\": \"\\uFEA3\",\n    \"/hahiragana\": \"\\u306F\",\n    \"/hahmedialarabic\": \"\\uFEA4\",\n    \"/hahookcyr\": \"\\u04FD\",\n    \"/hahthreedotsabove\": \"\\u0685\",\n    \"/hahtwodotsvertical\": \"\\u0682\",\n    \"/haircut\": \"\\u1F487\",\n    \"/hairspace\": \"\\u200A\",\n    \"/haitusquare\": \"\\u332A\",\n    \"/hakatakana\": \"\\u30CF\",\n    \"/hakatakanahalfwidth\": \"\\uFF8A\",\n    \"/halantgurmukhi\": \"\\u0A4D\",\n    \"/halfcircleleftblack\": \"\\u25D6\",\n    \"/halfcirclerightblack\": \"\\u25D7\",\n    \"/hamburger\": \"\\u1F354\",\n    \"/hammer\": \"\\u1F528\",\n    \"/hammerAndWrench\": \"\\u1F6E0\",\n    \"/hammerpick\": \"\\u2692\",\n    \"/hammersickle\": \"\\u262D\",\n    \"/hamsterFace\": \"\\u1F439\",\n    \"/hamza\": \"\\u0621\",\n    \"/hamzaIsol\": \"\\uFE80\",\n    \"/hamzaabove\": \"\\u0654\",\n    \"/hamzaarabic\": \"\\u0621\",\n    \"/hamzabelow\": \"\\u0655\",\n    \"/hamzadammaarabic\": \"\\u0621\",\n    \"/hamzadammatanarabic\": \"\\u0621\",\n    \"/hamzafathaarabic\": \"\\u0621\",\n    \"/hamzafathatanarabic\": \"\\u0621\",\n    \"/hamzalowarabic\": \"\\u0621\",\n    \"/hamzalowkasraarabic\": \"\\u0621\",\n    \"/hamzalowkasratanarabic\": \"\\u0621\",\n    \"/hamzasukunarabic\": \"\\u0621\",\n    \"/handbag\": \"\\u1F45C\",\n    \"/handtailfishhookturned\": \"\\u02AF\",\n    \"/hangulchieuchaparen\": \"\\u3217\",\n    \"/hangulchieuchparen\": \"\\u3209\",\n    \"/hangulcieucaparen\": \"\\u3216\",\n    \"/hangulcieucparen\": \"\\u3208\",\n    \"/hangulcieucuparen\": \"\\u321C\",\n    \"/hanguldottonemarkdbl\": \"\\u302F\",\n    \"/hangulfiller\": \"\\u3164\",\n    \"/hangulhieuhaparen\": \"\\u321B\",\n    \"/hangulhieuhparen\": \"\\u320D\",\n    \"/hangulieungaparen\": \"\\u3215\",\n    \"/hangulieungparen\": \"\\u3207\",\n    \"/hangulkhieukhaparen\": \"\\u3218\",\n    \"/hangulkhieukhparen\": \"\\u320A\",\n    \"/hangulkiyeokaparen\": \"\\u320E\",\n    \"/hangulkiyeokparen\": \"\\u3200\",\n    \"/hangulmieumaparen\": \"\\u3212\",\n    \"/hangulmieumparen\": \"\\u3204\",\n    \"/hangulnieunaparen\": \"\\u320F\",\n    \"/hangulnieunparen\": \"\\u3201\",\n    \"/hangulphieuphaparen\": \"\\u321A\",\n    \"/hangulphieuphparen\": \"\\u320C\",\n    \"/hangulpieupaparen\": \"\\u3213\",\n    \"/hangulpieupparen\": \"\\u3205\",\n    \"/hangulrieulaparen\": \"\\u3211\",\n    \"/hangulrieulparen\": \"\\u3203\",\n    \"/hangulsingledottonemark\": \"\\u302E\",\n    \"/hangulsiosaparen\": \"\\u3214\",\n    \"/hangulsiosparen\": \"\\u3206\",\n    \"/hangulthieuthaparen\": \"\\u3219\",\n    \"/hangulthieuthparen\": \"\\u320B\",\n    \"/hangultikeutaparen\": \"\\u3210\",\n    \"/hangultikeutparen\": \"\\u3202\",\n    \"/happyPersonRaisingOneHand\": \"\\u1F64B\",\n    \"/hardDisk\": \"\\u1F5B4\",\n    \"/hardcyr\": \"\\u044A\",\n    \"/hardsigncyrillic\": \"\\u044A\",\n    \"/harpoondownbarbleft\": \"\\u21C3\",\n    \"/harpoondownbarbright\": \"\\u21C2\",\n    \"/harpoonleftbarbdown\": \"\\u21BD\",\n    \"/harpoonleftbarbup\": \"\\u21BC\",\n    \"/harpoonrightbarbdown\": \"\\u21C1\",\n    \"/harpoonrightbarbup\": \"\\u21C0\",\n    \"/harpoonupbarbleft\": \"\\u21BF\",\n    \"/harpoonupbarbright\": \"\\u21BE\",\n    \"/hasquare\": \"\\u33CA\",\n    \"/hastrokecyr\": \"\\u04FF\",\n    \"/hatafPatah:hb\": \"\\u05B2\",\n    \"/hatafQamats:hb\": \"\\u05B3\",\n    \"/hatafSegol:hb\": \"\\u05B1\",\n    \"/hatafpatah\": \"\\u05B2\",\n    \"/hatafpatah16\": \"\\u05B2\",\n    \"/hatafpatah23\": \"\\u05B2\",\n    \"/hatafpatah2f\": \"\\u05B2\",\n    \"/hatafpatahhebrew\": \"\\u05B2\",\n    \"/hatafpatahnarrowhebrew\": \"\\u05B2\",\n    \"/hatafpatahquarterhebrew\": \"\\u05B2\",\n    \"/hatafpatahwidehebrew\": \"\\u05B2\",\n    \"/hatafqamats\": \"\\u05B3\",\n    \"/hatafqamats1b\": \"\\u05B3\",\n    \"/hatafqamats28\": \"\\u05B3\",\n    \"/hatafqamats34\": \"\\u05B3\",\n    \"/hatafqamatshebrew\": \"\\u05B3\",\n    \"/hatafqamatsnarrowhebrew\": \"\\u05B3\",\n    \"/hatafqamatsquarterhebrew\": \"\\u05B3\",\n    \"/hatafqamatswidehebrew\": \"\\u05B3\",\n    \"/hatafsegol\": \"\\u05B1\",\n    \"/hatafsegol17\": \"\\u05B1\",\n    \"/hatafsegol24\": \"\\u05B1\",\n    \"/hatafsegol30\": \"\\u05B1\",\n    \"/hatafsegolhebrew\": \"\\u05B1\",\n    \"/hatafsegolnarrowhebrew\": \"\\u05B1\",\n    \"/hatafsegolquarterhebrew\": \"\\u05B1\",\n    \"/hatafsegolwidehebrew\": \"\\u05B1\",\n    \"/hatchingChick\": \"\\u1F423\",\n    \"/haveideographiccircled\": \"\\u3292\",\n    \"/haveideographicparen\": \"\\u3232\",\n    \"/hbar\": \"\\u0127\",\n    \"/hbopomofo\": \"\\u310F\",\n    \"/hbrevebelow\": \"\\u1E2B\",\n    \"/hcaron\": \"\\u021F\",\n    \"/hcedilla\": \"\\u1E29\",\n    \"/hcircle\": \"\\u24D7\",\n    \"/hcircumflex\": \"\\u0125\",\n    \"/hcsquare\": \"\\u1F1A6\",\n    \"/hdescender\": \"\\u2C68\",\n    \"/hdieresis\": \"\\u1E27\",\n    \"/hdot\": \"\\u1E23\",\n    \"/hdotaccent\": \"\\u1E23\",\n    \"/hdotbelow\": \"\\u1E25\",\n    \"/hdrsquare\": \"\\u1F1A7\",\n    \"/he\": \"\\u05D4\",\n    \"/he:hb\": \"\\u05D4\",\n    \"/headphone\": \"\\u1F3A7\",\n    \"/headstonegraveyard\": \"\\u26FC\",\n    \"/hearNoEvilMonkey\": \"\\u1F649\",\n    \"/heart\": \"\\u2665\",\n    \"/heartArrow\": \"\\u1F498\",\n    \"/heartDecoration\": \"\\u1F49F\",\n    \"/heartRibbon\": \"\\u1F49D\",\n    \"/heartTipOnTheLeft\": \"\\u1F394\",\n    \"/heartblack\": \"\\u2665\",\n    \"/heartsuitblack\": \"\\u2665\",\n    \"/heartsuitwhite\": \"\\u2661\",\n    \"/heartwhite\": \"\\u2661\",\n    \"/heavyDollarSign\": \"\\u1F4B2\",\n    \"/heavyLatinCross\": \"\\u1F547\",\n    \"/heavydbldashhorz\": \"\\u254D\",\n    \"/heavydbldashvert\": \"\\u254F\",\n    \"/heavydn\": \"\\u257B\",\n    \"/heavydnhorz\": \"\\u2533\",\n    \"/heavydnleft\": \"\\u2513\",\n    \"/heavydnright\": \"\\u250F\",\n    \"/heavyhorz\": \"\\u2501\",\n    \"/heavyleft\": \"\\u2578\",\n    \"/heavyleftlightright\": \"\\u257E\",\n    \"/heavyquaddashhorz\": \"\\u2509\",\n    \"/heavyquaddashvert\": \"\\u250B\",\n    \"/heavyright\": \"\\u257A\",\n    \"/heavytrpldashhorz\": \"\\u2505\",\n    \"/heavytrpldashvert\": \"\\u2507\",\n    \"/heavyup\": \"\\u2579\",\n    \"/heavyuphorz\": \"\\u253B\",\n    \"/heavyupleft\": \"\\u251B\",\n    \"/heavyuplightdn\": \"\\u257F\",\n    \"/heavyupright\": \"\\u2517\",\n    \"/heavyvert\": \"\\u2503\",\n    \"/heavyverthorz\": \"\\u254B\",\n    \"/heavyvertleft\": \"\\u252B\",\n    \"/heavyvertright\": \"\\u2523\",\n    \"/hecirclekatakana\": \"\\u32EC\",\n    \"/hedagesh\": \"\\uFB34\",\n    \"/hedageshhebrew\": \"\\uFB34\",\n    \"/hedinterlacedpentagramleft\": \"\\u26E6\",\n    \"/hedinterlacedpentagramright\": \"\\u26E5\",\n    \"/heh\": \"\\u0647\",\n    \"/heh.fina\": \"\\uFEEA\",\n    \"/heh.init\": \"\\uFEEB\",\n    \"/heh.init_alefmaksura.fina\": \"\\uFC53\",\n    \"/heh.init_jeem.fina\": \"\\uFC51\",\n    \"/heh.init_jeem.medi\": \"\\uFCD7\",\n    \"/heh.init_meem.fina\": \"\\uFC52\",\n    \"/heh.init_meem.medi\": \"\\uFCD8\",\n    \"/heh.init_meem.medi_jeem.medi\": \"\\uFD93\",\n    \"/heh.init_meem.medi_meem.medi\": \"\\uFD94\",\n    \"/heh.init_superscriptalef.medi\": \"\\uFCD9\",\n    \"/heh.init_yeh.fina\": \"\\uFC54\",\n    \"/heh.isol\": \"\\uFEE9\",\n    \"/heh.medi\": \"\\uFEEC\",\n    \"/hehaltonearabic\": \"\\u06C1\",\n    \"/heharabic\": \"\\u0647\",\n    \"/hehdoachashmee\": \"\\u06BE\",\n    \"/hehdoachashmee.fina\": \"\\uFBAB\",\n    \"/hehdoachashmee.init\": \"\\uFBAC\",\n    \"/hehdoachashmee.isol\": \"\\uFBAA\",\n    \"/hehdoachashmee.medi\": \"\\uFBAD\",\n    \"/hehebrew\": \"\\u05D4\",\n    \"/hehfinalaltonearabic\": \"\\uFBA7\",\n    \"/hehfinalalttwoarabic\": \"\\uFEEA\",\n    \"/hehfinalarabic\": \"\\uFEEA\",\n    \"/hehgoal\": \"\\u06C1\",\n    \"/hehgoal.fina\": \"\\uFBA7\",\n    \"/hehgoal.init\": \"\\uFBA8\",\n    \"/hehgoal.isol\": \"\\uFBA6\",\n    \"/hehgoal.medi\": \"\\uFBA9\",\n    \"/hehgoalhamza\": \"\\u06C2\",\n    \"/hehhamzaabovefinalarabic\": \"\\uFBA5\",\n    \"/hehhamzaaboveisolatedarabic\": \"\\uFBA4\",\n    \"/hehinitialaltonearabic\": \"\\uFBA8\",\n    \"/hehinitialarabic\": \"\\uFEEB\",\n    \"/hehinvertedV\": \"\\u06FF\",\n    \"/hehiragana\": \"\\u3078\",\n    \"/hehmedialaltonearabic\": \"\\uFBA9\",\n    \"/hehmedialarabic\": \"\\uFEEC\",\n    \"/hehyeh\": \"\\u06C0\",\n    \"/hehyeh.fina\": \"\\uFBA5\",\n    \"/hehyeh.isol\": \"\\uFBA4\",\n    \"/heiseierasquare\": \"\\u337B\",\n    \"/hekatakana\": \"\\u30D8\",\n    \"/hekatakanahalfwidth\": \"\\uFF8D\",\n    \"/hekutaarusquare\": \"\\u3336\",\n    \"/helicopter\": \"\\u1F681\",\n    \"/helm\": \"\\u2388\",\n    \"/helmetcrosswhite\": \"\\u26D1\",\n    \"/heng\": \"\\uA727\",\n    \"/henghook\": \"\\u0267\",\n    \"/herb\": \"\\u1F33F\",\n    \"/hermitianconjugatematrix\": \"\\u22B9\",\n    \"/herutusquare\": \"\\u3339\",\n    \"/het\": \"\\u05D7\",\n    \"/het:hb\": \"\\u05D7\",\n    \"/heta\": \"\\u0371\",\n    \"/hethebrew\": \"\\u05D7\",\n    \"/hewide:hb\": \"\\uFB23\",\n    \"/hewithmapiq:hb\": \"\\uFB34\",\n    \"/hfishhookturned\": \"\\u02AE\",\n    \"/hhalf\": \"\\u2C76\",\n    \"/hhook\": \"\\u0266\",\n    \"/hhooksuperior\": \"\\u02B1\",\n    \"/hhooksupmod\": \"\\u02B1\",\n    \"/hi-ressquare\": \"\\u1F1A8\",\n    \"/hibiscus\": \"\\u1F33A\",\n    \"/hicirclekatakana\": \"\\u32EA\",\n    \"/hieuhacirclekorean\": \"\\u327B\",\n    \"/hieuhaparenkorean\": \"\\u321B\",\n    \"/hieuhcirclekorean\": \"\\u326D\",\n    \"/hieuhkorean\": \"\\u314E\",\n    \"/hieuhparenkorean\": \"\\u320D\",\n    \"/high-heeledShoe\": \"\\u1F460\",\n    \"/highBrightness\": \"\\u1F506\",\n    \"/highSpeedTrain\": \"\\u1F684\",\n    \"/highSpeedTrainWithBulletNose\": \"\\u1F685\",\n    \"/highhamza\": \"\\u0674\",\n    \"/highideographiccircled\": \"\\u32A4\",\n    \"/highvoltage\": \"\\u26A1\",\n    \"/hihiragana\": \"\\u3072\",\n    \"/hikatakana\": \"\\u30D2\",\n    \"/hikatakanahalfwidth\": \"\\uFF8B\",\n    \"/hira:a\": \"\\u3042\",\n    \"/hira:asmall\": \"\\u3041\",\n    \"/hira:ba\": \"\\u3070\",\n    \"/hira:be\": \"\\u3079\",\n    \"/hira:bi\": \"\\u3073\",\n    \"/hira:bo\": \"\\u307C\",\n    \"/hira:bu\": \"\\u3076\",\n    \"/hira:da\": \"\\u3060\",\n    \"/hira:de\": \"\\u3067\",\n    \"/hira:di\": \"\\u3062\",\n    \"/hira:digraphyori\": \"\\u309F\",\n    \"/hira:do\": \"\\u3069\",\n    \"/hira:du\": \"\\u3065\",\n    \"/hira:e\": \"\\u3048\",\n    \"/hira:esmall\": \"\\u3047\",\n    \"/hira:ga\": \"\\u304C\",\n    \"/hira:ge\": \"\\u3052\",\n    \"/hira:gi\": \"\\u304E\",\n    \"/hira:go\": \"\\u3054\",\n    \"/hira:gu\": \"\\u3050\",\n    \"/hira:ha\": \"\\u306F\",\n    \"/hira:he\": \"\\u3078\",\n    \"/hira:hi\": \"\\u3072\",\n    \"/hira:ho\": \"\\u307B\",\n    \"/hira:hu\": \"\\u3075\",\n    \"/hira:i\": \"\\u3044\",\n    \"/hira:ismall\": \"\\u3043\",\n    \"/hira:iterationhiragana\": \"\\u309D\",\n    \"/hira:ka\": \"\\u304B\",\n    \"/hira:kasmall\": \"\\u3095\",\n    \"/hira:ke\": \"\\u3051\",\n    \"/hira:kesmall\": \"\\u3096\",\n    \"/hira:ki\": \"\\u304D\",\n    \"/hira:ko\": \"\\u3053\",\n    \"/hira:ku\": \"\\u304F\",\n    \"/hira:ma\": \"\\u307E\",\n    \"/hira:me\": \"\\u3081\",\n    \"/hira:mi\": \"\\u307F\",\n    \"/hira:mo\": \"\\u3082\",\n    \"/hira:mu\": \"\\u3080\",\n    \"/hira:n\": \"\\u3093\",\n    \"/hira:na\": \"\\u306A\",\n    \"/hira:ne\": \"\\u306D\",\n    \"/hira:ni\": \"\\u306B\",\n    \"/hira:no\": \"\\u306E\",\n    \"/hira:nu\": \"\\u306C\",\n    \"/hira:o\": \"\\u304A\",\n    \"/hira:osmall\": \"\\u3049\",\n    \"/hira:pa\": \"\\u3071\",\n    \"/hira:pe\": \"\\u307A\",\n    \"/hira:pi\": \"\\u3074\",\n    \"/hira:po\": \"\\u307D\",\n    \"/hira:pu\": \"\\u3077\",\n    \"/hira:ra\": \"\\u3089\",\n    \"/hira:re\": \"\\u308C\",\n    \"/hira:ri\": \"\\u308A\",\n    \"/hira:ro\": \"\\u308D\",\n    \"/hira:ru\": \"\\u308B\",\n    \"/hira:sa\": \"\\u3055\",\n    \"/hira:se\": \"\\u305B\",\n    \"/hira:semivoicedmarkkana\": \"\\u309C\",\n    \"/hira:semivoicedmarkkanacmb\": \"\\u309A\",\n    \"/hira:si\": \"\\u3057\",\n    \"/hira:so\": \"\\u305D\",\n    \"/hira:su\": \"\\u3059\",\n    \"/hira:ta\": \"\\u305F\",\n    \"/hira:te\": \"\\u3066\",\n    \"/hira:ti\": \"\\u3061\",\n    \"/hira:to\": \"\\u3068\",\n    \"/hira:tu\": \"\\u3064\",\n    \"/hira:tusmall\": \"\\u3063\",\n    \"/hira:u\": \"\\u3046\",\n    \"/hira:usmall\": \"\\u3045\",\n    \"/hira:voicediterationhiragana\": \"\\u309E\",\n    \"/hira:voicedmarkkana\": \"\\u309B\",\n    \"/hira:voicedmarkkanacmb\": \"\\u3099\",\n    \"/hira:vu\": \"\\u3094\",\n    \"/hira:wa\": \"\\u308F\",\n    \"/hira:wasmall\": \"\\u308E\",\n    \"/hira:we\": \"\\u3091\",\n    \"/hira:wi\": \"\\u3090\",\n    \"/hira:wo\": \"\\u3092\",\n    \"/hira:ya\": \"\\u3084\",\n    \"/hira:yasmall\": \"\\u3083\",\n    \"/hira:yo\": \"\\u3088\",\n    \"/hira:yosmall\": \"\\u3087\",\n    \"/hira:yu\": \"\\u3086\",\n    \"/hira:yusmall\": \"\\u3085\",\n    \"/hira:za\": \"\\u3056\",\n    \"/hira:ze\": \"\\u305C\",\n    \"/hira:zi\": \"\\u3058\",\n    \"/hira:zo\": \"\\u305E\",\n    \"/hira:zu\": \"\\u305A\",\n    \"/hiriq\": \"\\u05B4\",\n    \"/hiriq14\": \"\\u05B4\",\n    \"/hiriq21\": \"\\u05B4\",\n    \"/hiriq2d\": \"\\u05B4\",\n    \"/hiriq:hb\": \"\\u05B4\",\n    \"/hiriqhebrew\": \"\\u05B4\",\n    \"/hiriqnarrowhebrew\": \"\\u05B4\",\n    \"/hiriqquarterhebrew\": \"\\u05B4\",\n    \"/hiriqwidehebrew\": \"\\u05B4\",\n    \"/historicsite\": \"\\u26EC\",\n    \"/hlinebelow\": \"\\u1E96\",\n    \"/hmonospace\": \"\\uFF48\",\n    \"/hoarmenian\": \"\\u0570\",\n    \"/hocho\": \"\\u1F52A\",\n    \"/hocirclekatakana\": \"\\u32ED\",\n    \"/hohipthai\": \"\\u0E2B\",\n    \"/hohiragana\": \"\\u307B\",\n    \"/hokatakana\": \"\\u30DB\",\n    \"/hokatakanahalfwidth\": \"\\uFF8E\",\n    \"/holam\": \"\\u05B9\",\n    \"/holam19\": \"\\u05B9\",\n    \"/holam26\": \"\\u05B9\",\n    \"/holam32\": \"\\u05B9\",\n    \"/holam:hb\": \"\\u05B9\",\n    \"/holamHaser:hb\": \"\\u05BA\",\n    \"/holamhebrew\": \"\\u05B9\",\n    \"/holamnarrowhebrew\": \"\\u05B9\",\n    \"/holamquarterhebrew\": \"\\u05B9\",\n    \"/holamwidehebrew\": \"\\u05B9\",\n    \"/hole\": \"\\u1F573\",\n    \"/homotic\": \"\\u223B\",\n    \"/honeyPot\": \"\\u1F36F\",\n    \"/honeybee\": \"\\u1F41D\",\n    \"/honokhukthai\": \"\\u0E2E\",\n    \"/honsquare\": \"\\u333F\",\n    \"/hook\": \"\\u2440\",\n    \"/hookabovecomb\": \"\\u0309\",\n    \"/hookcmb\": \"\\u0309\",\n    \"/hookpalatalizedbelowcmb\": \"\\u0321\",\n    \"/hookretroflexbelowcmb\": \"\\u0322\",\n    \"/hoonsquare\": \"\\u3342\",\n    \"/hoorusquare\": \"\\u3341\",\n    \"/horicoptic\": \"\\u03E9\",\n    \"/horizontalTrafficLight\": \"\\u1F6A5\",\n    \"/horizontalbar\": \"\\u2015\",\n    \"/horizontalbarwhitearrowonpedestalup\": \"\\u21EC\",\n    \"/horizontalmalestroke\": \"\\u26A9\",\n    \"/horncmb\": \"\\u031B\",\n    \"/horse\": \"\\u1F40E\",\n    \"/horseFace\": \"\\u1F434\",\n    \"/horseRacing\": \"\\u1F3C7\",\n    \"/hospital\": \"\\u1F3E5\",\n    \"/hotDog\": \"\\u1F32D\",\n    \"/hotPepper\": \"\\u1F336\",\n    \"/hotbeverage\": \"\\u2615\",\n    \"/hotel\": \"\\u1F3E8\",\n    \"/hotsprings\": \"\\u2668\",\n    \"/hourglass\": \"\\u231B\",\n    \"/hourglassflowings\": \"\\u23F3\",\n    \"/house\": \"\\u2302\",\n    \"/houseBuilding\": \"\\u1F3E0\",\n    \"/houseBuildings\": \"\\u1F3D8\",\n    \"/houseGarden\": \"\\u1F3E1\",\n    \"/hpafullwidth\": \"\\u3371\",\n    \"/hpalatalhook\": \"\\uA795\",\n    \"/hparen\": \"\\u24A3\",\n    \"/hparenthesized\": \"\\u24A3\",\n    \"/hpfullwidth\": \"\\u33CB\",\n    \"/hryvnia\": \"\\u20B4\",\n    \"/hsuperior\": \"\\u02B0\",\n    \"/hsupmod\": \"\\u02B0\",\n    \"/hturned\": \"\\u0265\",\n    \"/htypeopencircuit\": \"\\u238F\",\n    \"/huaraddosquare\": \"\\u3332\",\n    \"/hucirclekatakana\": \"\\u32EB\",\n    \"/huhiragana\": \"\\u3075\",\n    \"/huiitosquare\": \"\\u3333\",\n    \"/hukatakana\": \"\\u30D5\",\n    \"/hukatakanahalfwidth\": \"\\uFF8C\",\n    \"/hundredPoints\": \"\\u1F4AF\",\n    \"/hundredthousandscmbcyr\": \"\\u0488\",\n    \"/hungarumlaut\": \"\\u02DD\",\n    \"/hungarumlautcmb\": \"\\u030B\",\n    \"/huransquare\": \"\\u3335\",\n    \"/hushedFace\": \"\\u1F62F\",\n    \"/hv\": \"\\u0195\",\n    \"/hwd:a\": \"\\uFFC2\",\n    \"/hwd:ae\": \"\\uFFC3\",\n    \"/hwd:blacksquare\": \"\\uFFED\",\n    \"/hwd:chieuch\": \"\\uFFBA\",\n    \"/hwd:cieuc\": \"\\uFFB8\",\n    \"/hwd:downwardsarrow\": \"\\uFFEC\",\n    \"/hwd:e\": \"\\uFFC7\",\n    \"/hwd:eo\": \"\\uFFC6\",\n    \"/hwd:eu\": \"\\uFFDA\",\n    \"/hwd:formslightvertical\": \"\\uFFE8\",\n    \"/hwd:hangulfiller\": \"\\uFFA0\",\n    \"/hwd:hieuh\": \"\\uFFBE\",\n    \"/hwd:i\": \"\\uFFDC\",\n    \"/hwd:ideographiccomma\": \"\\uFF64\",\n    \"/hwd:ideographicfullstop\": \"\\uFF61\",\n    \"/hwd:ieung\": \"\\uFFB7\",\n    \"/hwd:kata:a\": \"\\uFF71\",\n    \"/hwd:kata:asmall\": \"\\uFF67\",\n    \"/hwd:kata:e\": \"\\uFF74\",\n    \"/hwd:kata:esmall\": \"\\uFF6A\",\n    \"/hwd:kata:ha\": \"\\uFF8A\",\n    \"/hwd:kata:he\": \"\\uFF8D\",\n    \"/hwd:kata:hi\": \"\\uFF8B\",\n    \"/hwd:kata:ho\": \"\\uFF8E\",\n    \"/hwd:kata:hu\": \"\\uFF8C\",\n    \"/hwd:kata:i\": \"\\uFF72\",\n    \"/hwd:kata:ismall\": \"\\uFF68\",\n    \"/hwd:kata:ka\": \"\\uFF76\",\n    \"/hwd:kata:ke\": \"\\uFF79\",\n    \"/hwd:kata:ki\": \"\\uFF77\",\n    \"/hwd:kata:ko\": \"\\uFF7A\",\n    \"/hwd:kata:ku\": \"\\uFF78\",\n    \"/hwd:kata:ma\": \"\\uFF8F\",\n    \"/hwd:kata:me\": \"\\uFF92\",\n    \"/hwd:kata:mi\": \"\\uFF90\",\n    \"/hwd:kata:middledot\": \"\\uFF65\",\n    \"/hwd:kata:mo\": \"\\uFF93\",\n    \"/hwd:kata:mu\": \"\\uFF91\",\n    \"/hwd:kata:n\": \"\\uFF9D\",\n    \"/hwd:kata:na\": \"\\uFF85\",\n    \"/hwd:kata:ne\": \"\\uFF88\",\n    \"/hwd:kata:ni\": \"\\uFF86\",\n    \"/hwd:kata:no\": \"\\uFF89\",\n    \"/hwd:kata:nu\": \"\\uFF87\",\n    \"/hwd:kata:o\": \"\\uFF75\",\n    \"/hwd:kata:osmall\": \"\\uFF6B\",\n    \"/hwd:kata:prolongedkana\": \"\\uFF70\",\n    \"/hwd:kata:ra\": \"\\uFF97\",\n    \"/hwd:kata:re\": \"\\uFF9A\",\n    \"/hwd:kata:ri\": \"\\uFF98\",\n    \"/hwd:kata:ro\": \"\\uFF9B\",\n    \"/hwd:kata:ru\": \"\\uFF99\",\n    \"/hwd:kata:sa\": \"\\uFF7B\",\n    \"/hwd:kata:se\": \"\\uFF7E\",\n    \"/hwd:kata:semi-voiced\": \"\\uFF9F\",\n    \"/hwd:kata:si\": \"\\uFF7C\",\n    \"/hwd:kata:so\": \"\\uFF7F\",\n    \"/hwd:kata:su\": \"\\uFF7D\",\n    \"/hwd:kata:ta\": \"\\uFF80\",\n    \"/hwd:kata:te\": \"\\uFF83\",\n    \"/hwd:kata:ti\": \"\\uFF81\",\n    \"/hwd:kata:to\": \"\\uFF84\",\n    \"/hwd:kata:tu\": \"\\uFF82\",\n    \"/hwd:kata:tusmall\": \"\\uFF6F\",\n    \"/hwd:kata:u\": \"\\uFF73\",\n    \"/hwd:kata:usmall\": \"\\uFF69\",\n    \"/hwd:kata:voiced\": \"\\uFF9E\",\n    \"/hwd:kata:wa\": \"\\uFF9C\",\n    \"/hwd:kata:wo\": \"\\uFF66\",\n    \"/hwd:kata:ya\": \"\\uFF94\",\n    \"/hwd:kata:yasmall\": \"\\uFF6C\",\n    \"/hwd:kata:yo\": \"\\uFF96\",\n    \"/hwd:kata:yosmall\": \"\\uFF6E\",\n    \"/hwd:kata:yu\": \"\\uFF95\",\n    \"/hwd:kata:yusmall\": \"\\uFF6D\",\n    \"/hwd:khieukh\": \"\\uFFBB\",\n    \"/hwd:kiyeok\": \"\\uFFA1\",\n    \"/hwd:kiyeoksios\": \"\\uFFA3\",\n    \"/hwd:leftcornerbracket\": \"\\uFF62\",\n    \"/hwd:leftwardsarrow\": \"\\uFFE9\",\n    \"/hwd:mieum\": \"\\uFFB1\",\n    \"/hwd:nieun\": \"\\uFFA4\",\n    \"/hwd:nieuncieuc\": \"\\uFFA5\",\n    \"/hwd:nieunhieuh\": \"\\uFFA6\",\n    \"/hwd:o\": \"\\uFFCC\",\n    \"/hwd:oe\": \"\\uFFCF\",\n    \"/hwd:phieuph\": \"\\uFFBD\",\n    \"/hwd:pieup\": \"\\uFFB2\",\n    \"/hwd:pieupsios\": \"\\uFFB4\",\n    \"/hwd:rieul\": \"\\uFFA9\",\n    \"/hwd:rieulhieuh\": \"\\uFFB0\",\n    \"/hwd:rieulkiyeok\": \"\\uFFAA\",\n    \"/hwd:rieulmieum\": \"\\uFFAB\",\n    \"/hwd:rieulphieuph\": \"\\uFFAF\",\n    \"/hwd:rieulpieup\": \"\\uFFAC\",\n    \"/hwd:rieulsios\": \"\\uFFAD\",\n    \"/hwd:rieulthieuth\": \"\\uFFAE\",\n    \"/hwd:rightcornerbracket\": \"\\uFF63\",\n    \"/hwd:rightwardsarrow\": \"\\uFFEB\",\n    \"/hwd:sios\": \"\\uFFB5\",\n    \"/hwd:ssangcieuc\": \"\\uFFB9\",\n    \"/hwd:ssangkiyeok\": \"\\uFFA2\",\n    \"/hwd:ssangpieup\": \"\\uFFB3\",\n    \"/hwd:ssangsios\": \"\\uFFB6\",\n    \"/hwd:ssangtikeut\": \"\\uFFA8\",\n    \"/hwd:thieuth\": \"\\uFFBC\",\n    \"/hwd:tikeut\": \"\\uFFA7\",\n    \"/hwd:u\": \"\\uFFD3\",\n    \"/hwd:upwardsarrow\": \"\\uFFEA\",\n    \"/hwd:wa\": \"\\uFFCD\",\n    \"/hwd:wae\": \"\\uFFCE\",\n    \"/hwd:we\": \"\\uFFD5\",\n    \"/hwd:weo\": \"\\uFFD4\",\n    \"/hwd:whitecircle\": \"\\uFFEE\",\n    \"/hwd:wi\": \"\\uFFD6\",\n    \"/hwd:ya\": \"\\uFFC4\",\n    \"/hwd:yae\": \"\\uFFC5\",\n    \"/hwd:ye\": \"\\uFFCB\",\n    \"/hwd:yeo\": \"\\uFFCA\",\n    \"/hwd:yi\": \"\\uFFDB\",\n    \"/hwd:yo\": \"\\uFFD2\",\n    \"/hwd:yu\": \"\\uFFD7\",\n    \"/hyphen\": \"\\u002D\",\n    \"/hyphenationpoint\": \"\\u2027\",\n    \"/hyphenbullet\": \"\\u2043\",\n    \"/hyphendbl\": \"\\u2E40\",\n    \"/hyphendbloblique\": \"\\u2E17\",\n    \"/hyphendieresis\": \"\\u2E1A\",\n    \"/hypheninferior\": \"\\uF6E5\",\n    \"/hyphenminus\": \"\\u002D\",\n    \"/hyphenmonospace\": \"\\uFF0D\",\n    \"/hyphensmall\": \"\\uFE63\",\n    \"/hyphensoft\": \"\\u00AD\",\n    \"/hyphensuperior\": \"\\uF6E6\",\n    \"/hyphentwo\": \"\\u2010\",\n    \"/hypodiastole\": \"\\u2E12\",\n    \"/hysteresis\": \"\\u238E\",\n    \"/hzfullwidth\": \"\\u3390\",\n    \"/i\": \"\\u0069\",\n    \"/i.superior\": \"\\u2071\",\n    \"/iacute\": \"\\u00ED\",\n    \"/iacyrillic\": \"\\u044F\",\n    \"/iaepigraphic\": \"\\uA7FE\",\n    \"/ibengali\": \"\\u0987\",\n    \"/ibopomofo\": \"\\u3127\",\n    \"/ibreve\": \"\\u012D\",\n    \"/icaron\": \"\\u01D0\",\n    \"/iceCream\": \"\\u1F368\",\n    \"/iceHockeyStickAndPuck\": \"\\u1F3D2\",\n    \"/iceskate\": \"\\u26F8\",\n    \"/icircle\": \"\\u24D8\",\n    \"/icirclekatakana\": \"\\u32D1\",\n    \"/icircumflex\": \"\\u00EE\",\n    \"/icyr\": \"\\u0438\",\n    \"/icyrillic\": \"\\u0456\",\n    \"/idblgrave\": \"\\u0209\",\n    \"/idblstruckitalic\": \"\\u2148\",\n    \"/ideographearthcircle\": \"\\u328F\",\n    \"/ideographfirecircle\": \"\\u328B\",\n    \"/ideographicallianceparen\": \"\\u323F\",\n    \"/ideographiccallparen\": \"\\u323A\",\n    \"/ideographiccentrecircle\": \"\\u32A5\",\n    \"/ideographicclose\": \"\\u3006\",\n    \"/ideographiccomma\": \"\\u3001\",\n    \"/ideographiccommaleft\": \"\\uFF64\",\n    \"/ideographiccongratulationparen\": \"\\u3237\",\n    \"/ideographiccorrectcircle\": \"\\u32A3\",\n    \"/ideographicdepartingtonemark\": \"\\u302C\",\n    \"/ideographicearthparen\": \"\\u322F\",\n    \"/ideographicenteringtonemark\": \"\\u302D\",\n    \"/ideographicenterpriseparen\": \"\\u323D\",\n    \"/ideographicexcellentcircle\": \"\\u329D\",\n    \"/ideographicfestivalparen\": \"\\u3240\",\n    \"/ideographicfinancialcircle\": \"\\u3296\",\n    \"/ideographicfinancialparen\": \"\\u3236\",\n    \"/ideographicfireparen\": \"\\u322B\",\n    \"/ideographichalffillspace\": \"\\u303F\",\n    \"/ideographichaveparen\": \"\\u3232\",\n    \"/ideographichighcircle\": \"\\u32A4\",\n    \"/ideographiciterationmark\": \"\\u3005\",\n    \"/ideographiclaborcircle\": \"\\u3298\",\n    \"/ideographiclaborparen\": \"\\u3238\",\n    \"/ideographicleftcircle\": \"\\u32A7\",\n    \"/ideographicleveltonemark\": \"\\u302A\",\n    \"/ideographiclowcircle\": \"\\u32A6\",\n    \"/ideographicmedicinecircle\": \"\\u32A9\",\n    \"/ideographicmetalparen\": \"\\u322E\",\n    \"/ideographicmoonparen\": \"\\u322A\",\n    \"/ideographicnameparen\": \"\\u3234\",\n    \"/ideographicperiod\": \"\\u3002\",\n    \"/ideographicprintcircle\": \"\\u329E\",\n    \"/ideographicreachparen\": \"\\u3243\",\n    \"/ideographicrepresentparen\": \"\\u3239\",\n    \"/ideographicresourceparen\": \"\\u323E\",\n    \"/ideographicrightcircle\": \"\\u32A8\",\n    \"/ideographicrisingtonemark\": \"\\u302B\",\n    \"/ideographicsecretcircle\": \"\\u3299\",\n    \"/ideographicselfparen\": \"\\u3242\",\n    \"/ideographicsocietyparen\": \"\\u3233\",\n    \"/ideographicspace\": \"\\u3000\",\n    \"/ideographicspecialparen\": \"\\u3235\",\n    \"/ideographicstockparen\": \"\\u3231\",\n    \"/ideographicstudyparen\": \"\\u323B\",\n    \"/ideographicsunparen\": \"\\u3230\",\n    \"/ideographicsuperviseparen\": \"\\u323C\",\n    \"/ideographictelegraphlinefeedseparatorsymbol\": \"\\u3037\",\n    \"/ideographictelegraphsymbolforhoureight\": \"\\u3360\",\n    \"/ideographictelegraphsymbolforhoureighteen\": \"\\u336A\",\n    \"/ideographictelegraphsymbolforhoureleven\": \"\\u3363\",\n    \"/ideographictelegraphsymbolforhourfifteen\": \"\\u3367\",\n    \"/ideographictelegraphsymbolforhourfive\": \"\\u335D\",\n    \"/ideographictelegraphsymbolforhourfour\": \"\\u335C\",\n    \"/ideographictelegraphsymbolforhourfourteen\": \"\\u3366\",\n    \"/ideographictelegraphsymbolforhournine\": \"\\u3361\",\n    \"/ideographictelegraphsymbolforhournineteen\": \"\\u336B\",\n    \"/ideographictelegraphsymbolforhourone\": \"\\u3359\",\n    \"/ideographictelegraphsymbolforhourseven\": \"\\u335F\",\n    \"/ideographictelegraphsymbolforhourseventeen\": \"\\u3369\",\n    \"/ideographictelegraphsymbolforhoursix\": \"\\u335E\",\n    \"/ideographictelegraphsymbolforhoursixteen\": \"\\u3368\",\n    \"/ideographictelegraphsymbolforhourten\": \"\\u3362\",\n    \"/ideographictelegraphsymbolforhourthirteen\": \"\\u3365\",\n    \"/ideographictelegraphsymbolforhourthree\": \"\\u335B\",\n    \"/ideographictelegraphsymbolforhourtwelve\": \"\\u3364\",\n    \"/ideographictelegraphsymbolforhourtwenty\": \"\\u336C\",\n    \"/ideographictelegraphsymbolforhourtwentyfour\": \"\\u3370\",\n    \"/ideographictelegraphsymbolforhourtwentyone\": \"\\u336D\",\n    \"/ideographictelegraphsymbolforhourtwentythree\": \"\\u336F\",\n    \"/ideographictelegraphsymbolforhourtwentytwo\": \"\\u336E\",\n    \"/ideographictelegraphsymbolforhourtwo\": \"\\u335A\",\n    \"/ideographictelegraphsymbolforhourzero\": \"\\u3358\",\n    \"/ideographicvariationindicator\": \"\\u303E\",\n    \"/ideographicwaterparen\": \"\\u322C\",\n    \"/ideographicwoodparen\": \"\\u322D\",\n    \"/ideographiczero\": \"\\u3007\",\n    \"/ideographmetalcircle\": \"\\u328E\",\n    \"/ideographmooncircle\": \"\\u328A\",\n    \"/ideographnamecircle\": \"\\u3294\",\n    \"/ideographsuncircle\": \"\\u3290\",\n    \"/ideographwatercircle\": \"\\u328C\",\n    \"/ideographwoodcircle\": \"\\u328D\",\n    \"/ideva\": \"\\u0907\",\n    \"/idieresis\": \"\\u00EF\",\n    \"/idieresisacute\": \"\\u1E2F\",\n    \"/idieresiscyr\": \"\\u04E5\",\n    \"/idieresiscyrillic\": \"\\u04E5\",\n    \"/idotbelow\": \"\\u1ECB\",\n    \"/idsquare\": \"\\u1F194\",\n    \"/iebrevecyr\": \"\\u04D7\",\n    \"/iebrevecyrillic\": \"\\u04D7\",\n    \"/iecyr\": \"\\u0435\",\n    \"/iecyrillic\": \"\\u0435\",\n    \"/iegravecyr\": \"\\u0450\",\n    \"/iepigraphicsideways\": \"\\uA7F7\",\n    \"/ieungacirclekorean\": \"\\u3275\",\n    \"/ieungaparenkorean\": \"\\u3215\",\n    \"/ieungcirclekorean\": \"\\u3267\",\n    \"/ieungkorean\": \"\\u3147\",\n    \"/ieungparenkorean\": \"\\u3207\",\n    \"/ieungucirclekorean\": \"\\u327E\",\n    \"/igrave\": \"\\u00EC\",\n    \"/igravecyr\": \"\\u045D\",\n    \"/igravedbl\": \"\\u0209\",\n    \"/igujarati\": \"\\u0A87\",\n    \"/igurmukhi\": \"\\u0A07\",\n    \"/ihiragana\": \"\\u3044\",\n    \"/ihoi\": \"\\u1EC9\",\n    \"/ihookabove\": \"\\u1EC9\",\n    \"/iibengali\": \"\\u0988\",\n    \"/iicyrillic\": \"\\u0438\",\n    \"/iideva\": \"\\u0908\",\n    \"/iigujarati\": \"\\u0A88\",\n    \"/iigurmukhi\": \"\\u0A08\",\n    \"/iimatragurmukhi\": \"\\u0A40\",\n    \"/iinvertedbreve\": \"\\u020B\",\n    \"/iishortcyrillic\": \"\\u0439\",\n    \"/iivowelsignbengali\": \"\\u09C0\",\n    \"/iivowelsigndeva\": \"\\u0940\",\n    \"/iivowelsigngujarati\": \"\\u0AC0\",\n    \"/ij\": \"\\u0133\",\n    \"/ikatakana\": \"\\u30A4\",\n    \"/ikatakanahalfwidth\": \"\\uFF72\",\n    \"/ikawi\": \"\\uA985\",\n    \"/ikorean\": \"\\u3163\",\n    \"/ilde\": \"\\u02DC\",\n    \"/iluy:hb\": \"\\u05AC\",\n    \"/iluyhebrew\": \"\\u05AC\",\n    \"/imacron\": \"\\u012B\",\n    \"/imacroncyr\": \"\\u04E3\",\n    \"/imacroncyrillic\": \"\\u04E3\",\n    \"/image\": \"\\u22B7\",\n    \"/imageorapproximatelyequal\": \"\\u2253\",\n    \"/imatragurmukhi\": \"\\u0A3F\",\n    \"/imonospace\": \"\\uFF49\",\n    \"/imp\": \"\\u1F47F\",\n    \"/inboxTray\": \"\\u1F4E5\",\n    \"/incomingEnvelope\": \"\\u1F4E8\",\n    \"/increaseFontSize\": \"\\u1F5DA\",\n    \"/increment\": \"\\u2206\",\n    \"/indianrupee\": \"\\u20B9\",\n    \"/infinity\": \"\\u221E\",\n    \"/information\": \"\\u2139\",\n    \"/infullwidth\": \"\\u33CC\",\n    \"/inhibitarabicformshaping\": \"\\u206C\",\n    \"/inhibitsymmetricswapping\": \"\\u206A\",\n    \"/iniarmenian\": \"\\u056B\",\n    \"/iningusquare\": \"\\u3304\",\n    \"/inmationDeskPerson\": \"\\u1F481\",\n    \"/inputLatinCapitalLetters\": \"\\u1F520\",\n    \"/inputLatinLetters\": \"\\u1F524\",\n    \"/inputLatinSmallLetters\": \"\\u1F521\",\n    \"/inputNumbers\": \"\\u1F522\",\n    \"/inputS\": \"\\u1F523\",\n    \"/insertion\": \"\\u2380\",\n    \"/integral\": \"\\u222B\",\n    \"/integralbottom\": \"\\u2321\",\n    \"/integralbt\": \"\\u2321\",\n    \"/integralclockwise\": \"\\u2231\",\n    \"/integralcontour\": \"\\u222E\",\n    \"/integralcontouranticlockwise\": \"\\u2233\",\n    \"/integralcontourclockwise\": \"\\u2232\",\n    \"/integraldbl\": \"\\u222C\",\n    \"/integralex\": \"\\uF8F5\",\n    \"/integralextension\": \"\\u23AE\",\n    \"/integralsurface\": \"\\u222F\",\n    \"/integraltop\": \"\\u2320\",\n    \"/integraltp\": \"\\u2320\",\n    \"/integraltpl\": \"\\u222D\",\n    \"/integralvolume\": \"\\u2230\",\n    \"/intercalate\": \"\\u22BA\",\n    \"/interlinearanchor\": \"\\uFFF9\",\n    \"/interlinearseparator\": \"\\uFFFA\",\n    \"/interlinearterminator\": \"\\uFFFB\",\n    \"/interlockedfemalemale\": \"\\u26A4\",\n    \"/interrobang\": \"\\u203D\",\n    \"/interrobanginverted\": \"\\u2E18\",\n    \"/intersection\": \"\\u2229\",\n    \"/intersectionarray\": \"\\u22C2\",\n    \"/intersectiondbl\": \"\\u22D2\",\n    \"/intisquare\": \"\\u3305\",\n    \"/invbullet\": \"\\u25D8\",\n    \"/invcircle\": \"\\u25D9\",\n    \"/inverteddamma\": \"\\u0657\",\n    \"/invertedfork\": \"\\u2443\",\n    \"/invertedpentagram\": \"\\u26E7\",\n    \"/invertedundertie\": \"\\u2054\",\n    \"/invisibleplus\": \"\\u2064\",\n    \"/invisibleseparator\": \"\\u2063\",\n    \"/invisibletimes\": \"\\u2062\",\n    \"/invsmileface\": \"\\u263B\",\n    \"/iocyr\": \"\\u0451\",\n    \"/iocyrillic\": \"\\u0451\",\n    \"/iogonek\": \"\\u012F\",\n    \"/iota\": \"\\u03B9\",\n    \"/iotaacute\": \"\\u1F77\",\n    \"/iotaadscript\": \"\\u1FBE\",\n    \"/iotaasper\": \"\\u1F31\",\n    \"/iotaasperacute\": \"\\u1F35\",\n    \"/iotaaspergrave\": \"\\u1F33\",\n    \"/iotaaspertilde\": \"\\u1F37\",\n    \"/iotabreve\": \"\\u1FD0\",\n    \"/iotadieresis\": \"\\u03CA\",\n    \"/iotadieresisacute\": \"\\u1FD3\",\n    \"/iotadieresisgrave\": \"\\u1FD2\",\n    \"/iotadieresistilde\": \"\\u1FD7\",\n    \"/iotadieresistonos\": \"\\u0390\",\n    \"/iotafunc\": \"\\u2373\",\n    \"/iotagrave\": \"\\u1F76\",\n    \"/iotalatin\": \"\\u0269\",\n    \"/iotalenis\": \"\\u1F30\",\n    \"/iotalenisacute\": \"\\u1F34\",\n    \"/iotalenisgrave\": \"\\u1F32\",\n    \"/iotalenistilde\": \"\\u1F36\",\n    \"/iotasub\": \"\\u037A\",\n    \"/iotatilde\": \"\\u1FD6\",\n    \"/iotatonos\": \"\\u03AF\",\n    \"/iotaturned\": \"\\u2129\",\n    \"/iotaunderlinefunc\": \"\\u2378\",\n    \"/iotawithmacron\": \"\\u1FD1\",\n    \"/ipa:Ismall\": \"\\u026A\",\n    \"/ipa:alpha\": \"\\u0251\",\n    \"/ipa:ereversed\": \"\\u0258\",\n    \"/ipa:esh\": \"\\u0283\",\n    \"/ipa:gamma\": \"\\u0263\",\n    \"/ipa:glottalstop\": \"\\u0294\",\n    \"/ipa:gscript\": \"\\u0261\",\n    \"/ipa:iota\": \"\\u0269\",\n    \"/ipa:phi\": \"\\u0278\",\n    \"/ipa:rtail\": \"\\u027D\",\n    \"/ipa:schwa\": \"\\u0259\",\n    \"/ipa:upsilon\": \"\\u028A\",\n    \"/iparen\": \"\\u24A4\",\n    \"/iparenthesized\": \"\\u24A4\",\n    \"/irigurmukhi\": \"\\u0A72\",\n    \"/is\": \"\\uA76D\",\n    \"/isen-isenpada\": \"\\uA9DF\",\n    \"/ishortcyr\": \"\\u0439\",\n    \"/ishortsharptailcyr\": \"\\u048B\",\n    \"/ismallhiragana\": \"\\u3043\",\n    \"/ismallkatakana\": \"\\u30A3\",\n    \"/ismallkatakanahalfwidth\": \"\\uFF68\",\n    \"/issharbengali\": \"\\u09FA\",\n    \"/istroke\": \"\\u0268\",\n    \"/isuperior\": \"\\uF6ED\",\n    \"/itemideographiccircled\": \"\\u32A0\",\n    \"/iterationhiragana\": \"\\u309D\",\n    \"/iterationkatakana\": \"\\u30FD\",\n    \"/itilde\": \"\\u0129\",\n    \"/itildebelow\": \"\\u1E2D\",\n    \"/iubopomofo\": \"\\u3129\",\n    \"/iucyrillic\": \"\\u044E\",\n    \"/iufullwidth\": \"\\u337A\",\n    \"/iukrcyr\": \"\\u0456\",\n    \"/ivowelsignbengali\": \"\\u09BF\",\n    \"/ivowelsigndeva\": \"\\u093F\",\n    \"/ivowelsigngujarati\": \"\\u0ABF\",\n    \"/izakayaLantern\": \"\\u1F3EE\",\n    \"/izhitsacyr\": \"\\u0475\",\n    \"/izhitsacyrillic\": \"\\u0475\",\n    \"/izhitsadblgravecyrillic\": \"\\u0477\",\n    \"/izhitsagravedblcyr\": \"\\u0477\",\n    \"/j\": \"\\u006A\",\n    \"/j.inferior\": \"\\u2C7C\",\n    \"/jaarmenian\": \"\\u0571\",\n    \"/jabengali\": \"\\u099C\",\n    \"/jackOLantern\": \"\\u1F383\",\n    \"/jadeva\": \"\\u091C\",\n    \"/jagujarati\": \"\\u0A9C\",\n    \"/jagurmukhi\": \"\\u0A1C\",\n    \"/jamahaprana\": \"\\uA999\",\n    \"/januarytelegraph\": \"\\u32C0\",\n    \"/japaneseBeginner\": \"\\u1F530\",\n    \"/japaneseCastle\": \"\\u1F3EF\",\n    \"/japaneseDolls\": \"\\u1F38E\",\n    \"/japaneseGoblin\": \"\\u1F47A\",\n    \"/japaneseOgre\": \"\\u1F479\",\n    \"/japanesePostOffice\": \"\\u1F3E3\",\n    \"/japanesebank\": \"\\u26FB\",\n    \"/java:a\": \"\\uA984\",\n    \"/java:ai\": \"\\uA98D\",\n    \"/java:ba\": \"\\uA9A7\",\n    \"/java:ca\": \"\\uA995\",\n    \"/java:da\": \"\\uA9A2\",\n    \"/java:dda\": \"\\uA99D\",\n    \"/java:e\": \"\\uA98C\",\n    \"/java:eight\": \"\\uA9D8\",\n    \"/java:five\": \"\\uA9D5\",\n    \"/java:four\": \"\\uA9D4\",\n    \"/java:ga\": \"\\uA992\",\n    \"/java:ha\": \"\\uA9B2\",\n    \"/java:i\": \"\\uA986\",\n    \"/java:ii\": \"\\uA987\",\n    \"/java:ja\": \"\\uA997\",\n    \"/java:ka\": \"\\uA98F\",\n    \"/java:la\": \"\\uA9AD\",\n    \"/java:ma\": \"\\uA9A9\",\n    \"/java:na\": \"\\uA9A4\",\n    \"/java:nga\": \"\\uA994\",\n    \"/java:nine\": \"\\uA9D9\",\n    \"/java:nya\": \"\\uA99A\",\n    \"/java:o\": \"\\uA98E\",\n    \"/java:one\": \"\\uA9D1\",\n    \"/java:pa\": \"\\uA9A5\",\n    \"/java:ra\": \"\\uA9AB\",\n    \"/java:sa\": \"\\uA9B1\",\n    \"/java:seven\": \"\\uA9D7\",\n    \"/java:six\": \"\\uA9D6\",\n    \"/java:ta\": \"\\uA9A0\",\n    \"/java:three\": \"\\uA9D3\",\n    \"/java:tta\": \"\\uA99B\",\n    \"/java:two\": \"\\uA9D2\",\n    \"/java:u\": \"\\uA988\",\n    \"/java:wa\": \"\\uA9AE\",\n    \"/java:ya\": \"\\uA9AA\",\n    \"/java:zero\": \"\\uA9D0\",\n    \"/jbopomofo\": \"\\u3110\",\n    \"/jcaron\": \"\\u01F0\",\n    \"/jcircle\": \"\\u24D9\",\n    \"/jcircumflex\": \"\\u0135\",\n    \"/jcrossedtail\": \"\\u029D\",\n    \"/jdblstruckitalic\": \"\\u2149\",\n    \"/jdotlessstroke\": \"\\u025F\",\n    \"/jeans\": \"\\u1F456\",\n    \"/jecyr\": \"\\u0458\",\n    \"/jecyrillic\": \"\\u0458\",\n    \"/jeem\": \"\\u062C\",\n    \"/jeem.fina\": \"\\uFE9E\",\n    \"/jeem.init\": \"\\uFE9F\",\n    \"/jeem.init_alefmaksura.fina\": \"\\uFD01\",\n    \"/jeem.init_hah.fina\": \"\\uFC15\",\n    \"/jeem.init_hah.medi\": \"\\uFCA7\",\n    \"/jeem.init_meem.fina\": \"\\uFC16\",\n    \"/jeem.init_meem.medi\": \"\\uFCA8\",\n    \"/jeem.init_meem.medi_hah.medi\": \"\\uFD59\",\n    \"/jeem.init_yeh.fina\": \"\\uFD02\",\n    \"/jeem.isol\": \"\\uFE9D\",\n    \"/jeem.medi\": \"\\uFEA0\",\n    \"/jeem.medi_alefmaksura.fina\": \"\\uFD1D\",\n    \"/jeem.medi_hah.medi_alefmaksura.fina\": \"\\uFDA6\",\n    \"/jeem.medi_hah.medi_yeh.fina\": \"\\uFDBE\",\n    \"/jeem.medi_meem.medi_alefmaksura.fina\": \"\\uFDA7\",\n    \"/jeem.medi_meem.medi_hah.fina\": \"\\uFD58\",\n    \"/jeem.medi_meem.medi_yeh.fina\": \"\\uFDA5\",\n    \"/jeem.medi_yeh.fina\": \"\\uFD1E\",\n    \"/jeemabove\": \"\\u06DA\",\n    \"/jeemarabic\": \"\\u062C\",\n    \"/jeemfinalarabic\": \"\\uFE9E\",\n    \"/jeeminitialarabic\": \"\\uFE9F\",\n    \"/jeemmedialarabic\": \"\\uFEA0\",\n    \"/jeh\": \"\\u0698\",\n    \"/jeh.fina\": \"\\uFB8B\",\n    \"/jeh.isol\": \"\\uFB8A\",\n    \"/jeharabic\": \"\\u0698\",\n    \"/jehfinalarabic\": \"\\uFB8B\",\n    \"/jhabengali\": \"\\u099D\",\n    \"/jhadeva\": \"\\u091D\",\n    \"/jhagujarati\": \"\\u0A9D\",\n    \"/jhagurmukhi\": \"\\u0A1D\",\n    \"/jheharmenian\": \"\\u057B\",\n    \"/jis\": \"\\u3004\",\n    \"/jiterup\": \"\\u2643\",\n    \"/jmonospace\": \"\\uFF4A\",\n    \"/jotdiaeresisfunc\": \"\\u2364\",\n    \"/jotunderlinefunc\": \"\\u235B\",\n    \"/joystick\": \"\\u1F579\",\n    \"/jparen\": \"\\u24A5\",\n    \"/jparenthesized\": \"\\u24A5\",\n    \"/jstroke\": \"\\u0249\",\n    \"/jsuperior\": \"\\u02B2\",\n    \"/jsupmod\": \"\\u02B2\",\n    \"/jueuicircle\": \"\\u327D\",\n    \"/julytelegraph\": \"\\u32C6\",\n    \"/junetelegraph\": \"\\u32C5\",\n    \"/juno\": \"\\u26B5\",\n    \"/k\": \"\\u006B\",\n    \"/k.inferior\": \"\\u2096\",\n    \"/kaaba\": \"\\u1F54B\",\n    \"/kaaleutcyr\": \"\\u051F\",\n    \"/kabashkcyr\": \"\\u04A1\",\n    \"/kabashkircyrillic\": \"\\u04A1\",\n    \"/kabengali\": \"\\u0995\",\n    \"/kacirclekatakana\": \"\\u32D5\",\n    \"/kacute\": \"\\u1E31\",\n    \"/kacyr\": \"\\u043A\",\n    \"/kacyrillic\": \"\\u043A\",\n    \"/kadescendercyrillic\": \"\\u049B\",\n    \"/kadeva\": \"\\u0915\",\n    \"/kaf\": \"\\u05DB\",\n    \"/kaf.fina\": \"\\uFEDA\",\n    \"/kaf.init\": \"\\uFEDB\",\n    \"/kaf.init_alef.fina\": \"\\uFC37\",\n    \"/kaf.init_alefmaksura.fina\": \"\\uFC3D\",\n    \"/kaf.init_hah.fina\": \"\\uFC39\",\n    \"/kaf.init_hah.medi\": \"\\uFCC5\",\n    \"/kaf.init_jeem.fina\": \"\\uFC38\",\n    \"/kaf.init_jeem.medi\": \"\\uFCC4\",\n    \"/kaf.init_khah.fina\": \"\\uFC3A\",\n    \"/kaf.init_khah.medi\": \"\\uFCC6\",\n    \"/kaf.init_lam.fina\": \"\\uFC3B\",\n    \"/kaf.init_lam.medi\": \"\\uFCC7\",\n    \"/kaf.init_meem.fina\": \"\\uFC3C\",\n    \"/kaf.init_meem.medi\": \"\\uFCC8\",\n    \"/kaf.init_meem.medi_meem.medi\": \"\\uFDC3\",\n    \"/kaf.init_yeh.fina\": \"\\uFC3E\",\n    \"/kaf.isol\": \"\\uFED9\",\n    \"/kaf.medi\": \"\\uFEDC\",\n    \"/kaf.medi_alef.fina\": \"\\uFC80\",\n    \"/kaf.medi_alefmaksura.fina\": \"\\uFC83\",\n    \"/kaf.medi_lam.fina\": \"\\uFC81\",\n    \"/kaf.medi_lam.medi\": \"\\uFCEB\",\n    \"/kaf.medi_meem.fina\": \"\\uFC82\",\n    \"/kaf.medi_meem.medi\": \"\\uFCEC\",\n    \"/kaf.medi_meem.medi_meem.fina\": \"\\uFDBB\",\n    \"/kaf.medi_meem.medi_yeh.fina\": \"\\uFDB7\",\n    \"/kaf.medi_yeh.fina\": \"\\uFC84\",\n    \"/kaf:hb\": \"\\u05DB\",\n    \"/kafTwoDotsAbove\": \"\\u077F\",\n    \"/kafarabic\": \"\\u0643\",\n    \"/kafdagesh\": \"\\uFB3B\",\n    \"/kafdageshhebrew\": \"\\uFB3B\",\n    \"/kafdotabove\": \"\\u06AC\",\n    \"/kaffinalarabic\": \"\\uFEDA\",\n    \"/kafhebrew\": \"\\u05DB\",\n    \"/kafinitialarabic\": \"\\uFEDB\",\n    \"/kafmedialarabic\": \"\\uFEDC\",\n    \"/kafrafehebrew\": \"\\uFB4D\",\n    \"/kafring\": \"\\u06AB\",\n    \"/kafswash\": \"\\u06AA\",\n    \"/kafthreedotsbelow\": \"\\u06AE\",\n    \"/kafullwidth\": \"\\u3384\",\n    \"/kafwide:hb\": \"\\uFB24\",\n    \"/kafwithdagesh:hb\": \"\\uFB3B\",\n    \"/kafwithrafe:hb\": \"\\uFB4D\",\n    \"/kagujarati\": \"\\u0A95\",\n    \"/kagurmukhi\": \"\\u0A15\",\n    \"/kahiragana\": \"\\u304B\",\n    \"/kahookcyr\": \"\\u04C4\",\n    \"/kahookcyrillic\": \"\\u04C4\",\n    \"/kairisquare\": \"\\u330B\",\n    \"/kaisymbol\": \"\\u03D7\",\n    \"/kakatakana\": \"\\u30AB\",\n    \"/kakatakanahalfwidth\": \"\\uFF76\",\n    \"/kamurda\": \"\\uA991\",\n    \"/kappa\": \"\\u03BA\",\n    \"/kappa.math\": \"\\u03F0\",\n    \"/kappasymbolgreek\": \"\\u03F0\",\n    \"/kapyeounmieumkorean\": \"\\u3171\",\n    \"/kapyeounphieuphkorean\": \"\\u3184\",\n    \"/kapyeounpieupkorean\": \"\\u3178\",\n    \"/kapyeounssangpieupkorean\": \"\\u3179\",\n    \"/karattosquare\": \"\\u330C\",\n    \"/karoriisquare\": \"\\u330D\",\n    \"/kasasak\": \"\\uA990\",\n    \"/kashida\": \"\\u0640\",\n    \"/kashidaFina\": \"\\uFE73\",\n    \"/kashidaautoarabic\": \"\\u0640\",\n    \"/kashidaautonosidebearingarabic\": \"\\u0640\",\n    \"/kashmiriyeh\": \"\\u0620\",\n    \"/kasmallkatakana\": \"\\u30F5\",\n    \"/kasquare\": \"\\u3384\",\n    \"/kasra\": \"\\u0650\",\n    \"/kasraIsol\": \"\\uFE7A\",\n    \"/kasraMedi\": \"\\uFE7B\",\n    \"/kasraarabic\": \"\\u0650\",\n    \"/kasrasmall\": \"\\u061A\",\n    \"/kasratan\": \"\\u064D\",\n    \"/kasratanIsol\": \"\\uFE74\",\n    \"/kasratanarabic\": \"\\u064D\",\n    \"/kastrokecyr\": \"\\u049F\",\n    \"/kastrokecyrillic\": \"\\u049F\",\n    \"/kata:a\": \"\\u30A2\",\n    \"/kata:asmall\": \"\\u30A1\",\n    \"/kata:ba\": \"\\u30D0\",\n    \"/kata:be\": \"\\u30D9\",\n    \"/kata:bi\": \"\\u30D3\",\n    \"/kata:bo\": \"\\u30DC\",\n    \"/kata:bu\": \"\\u30D6\",\n    \"/kata:da\": \"\\u30C0\",\n    \"/kata:de\": \"\\u30C7\",\n    \"/kata:di\": \"\\u30C2\",\n    \"/kata:digraphkoto\": \"\\u30FF\",\n    \"/kata:do\": \"\\u30C9\",\n    \"/kata:doublehyphenkana\": \"\\u30A0\",\n    \"/kata:du\": \"\\u30C5\",\n    \"/kata:e\": \"\\u30A8\",\n    \"/kata:esmall\": \"\\u30A7\",\n    \"/kata:ga\": \"\\u30AC\",\n    \"/kata:ge\": \"\\u30B2\",\n    \"/kata:gi\": \"\\u30AE\",\n    \"/kata:go\": \"\\u30B4\",\n    \"/kata:gu\": \"\\u30B0\",\n    \"/kata:ha\": \"\\u30CF\",\n    \"/kata:he\": \"\\u30D8\",\n    \"/kata:hi\": \"\\u30D2\",\n    \"/kata:ho\": \"\\u30DB\",\n    \"/kata:hu\": \"\\u30D5\",\n    \"/kata:i\": \"\\u30A4\",\n    \"/kata:ismall\": \"\\u30A3\",\n    \"/kata:iteration\": \"\\u30FD\",\n    \"/kata:ka\": \"\\u30AB\",\n    \"/kata:kasmall\": \"\\u30F5\",\n    \"/kata:ke\": \"\\u30B1\",\n    \"/kata:kesmall\": \"\\u30F6\",\n    \"/kata:ki\": \"\\u30AD\",\n    \"/kata:ko\": \"\\u30B3\",\n    \"/kata:ku\": \"\\u30AF\",\n    \"/kata:ma\": \"\\u30DE\",\n    \"/kata:me\": \"\\u30E1\",\n    \"/kata:mi\": \"\\u30DF\",\n    \"/kata:middledot\": \"\\u30FB\",\n    \"/kata:mo\": \"\\u30E2\",\n    \"/kata:mu\": \"\\u30E0\",\n    \"/kata:n\": \"\\u30F3\",\n    \"/kata:na\": \"\\u30CA\",\n    \"/kata:ne\": \"\\u30CD\",\n    \"/kata:ni\": \"\\u30CB\",\n    \"/kata:no\": \"\\u30CE\",\n    \"/kata:nu\": \"\\u30CC\",\n    \"/kata:o\": \"\\u30AA\",\n    \"/kata:osmall\": \"\\u30A9\",\n    \"/kata:pa\": \"\\u30D1\",\n    \"/kata:pe\": \"\\u30DA\",\n    \"/kata:pi\": \"\\u30D4\",\n    \"/kata:po\": \"\\u30DD\",\n    \"/kata:prolongedkana\": \"\\u30FC\",\n    \"/kata:pu\": \"\\u30D7\",\n    \"/kata:ra\": \"\\u30E9\",\n    \"/kata:re\": \"\\u30EC\",\n    \"/kata:ri\": \"\\u30EA\",\n    \"/kata:ro\": \"\\u30ED\",\n    \"/kata:ru\": \"\\u30EB\",\n    \"/kata:sa\": \"\\u30B5\",\n    \"/kata:se\": \"\\u30BB\",\n    \"/kata:si\": \"\\u30B7\",\n    \"/kata:so\": \"\\u30BD\",\n    \"/kata:su\": \"\\u30B9\",\n    \"/kata:ta\": \"\\u30BF\",\n    \"/kata:te\": \"\\u30C6\",\n    \"/kata:ti\": \"\\u30C1\",\n    \"/kata:to\": \"\\u30C8\",\n    \"/kata:tu\": \"\\u30C4\",\n    \"/kata:tusmall\": \"\\u30C3\",\n    \"/kata:u\": \"\\u30A6\",\n    \"/kata:usmall\": \"\\u30A5\",\n    \"/kata:va\": \"\\u30F7\",\n    \"/kata:ve\": \"\\u30F9\",\n    \"/kata:vi\": \"\\u30F8\",\n    \"/kata:vo\": \"\\u30FA\",\n    \"/kata:voicediteration\": \"\\u30FE\",\n    \"/kata:vu\": \"\\u30F4\",\n    \"/kata:wa\": \"\\u30EF\",\n    \"/kata:wasmall\": \"\\u30EE\",\n    \"/kata:we\": \"\\u30F1\",\n    \"/kata:wi\": \"\\u30F0\",\n    \"/kata:wo\": \"\\u30F2\",\n    \"/kata:ya\": \"\\u30E4\",\n    \"/kata:yasmall\": \"\\u30E3\",\n    \"/kata:yo\": \"\\u30E8\",\n    \"/kata:yosmall\": \"\\u30E7\",\n    \"/kata:yu\": \"\\u30E6\",\n    \"/kata:yusmall\": \"\\u30E5\",\n    \"/kata:za\": \"\\u30B6\",\n    \"/kata:ze\": \"\\u30BC\",\n    \"/kata:zi\": \"\\u30B8\",\n    \"/kata:zo\": \"\\u30BE\",\n    \"/kata:zu\": \"\\u30BA\",\n    \"/katahiraprolongmarkhalfwidth\": \"\\uFF70\",\n    \"/katailcyr\": \"\\u049B\",\n    \"/kaverticalstrokecyr\": \"\\u049D\",\n    \"/kaverticalstrokecyrillic\": \"\\u049D\",\n    \"/kavykainvertedlow\": \"\\u2E45\",\n    \"/kavykalow\": \"\\u2E47\",\n    \"/kavykawithdotlow\": \"\\u2E48\",\n    \"/kavykawithkavykaaboveinvertedlow\": \"\\u2E46\",\n    \"/kbfullwidth\": \"\\u3385\",\n    \"/kbopomofo\": \"\\u310E\",\n    \"/kcalfullwidth\": \"\\u3389\",\n    \"/kcalsquare\": \"\\u3389\",\n    \"/kcaron\": \"\\u01E9\",\n    \"/kcedilla\": \"\\u0137\",\n    \"/kcircle\": \"\\u24DA\",\n    \"/kcommaaccent\": \"\\u0137\",\n    \"/kdescender\": \"\\u2C6A\",\n    \"/kdiagonalstroke\": \"\\uA743\",\n    \"/kdotbelow\": \"\\u1E33\",\n    \"/kecirclekatakana\": \"\\u32D8\",\n    \"/keesusquare\": \"\\u331C\",\n    \"/keharmenian\": \"\\u0584\",\n    \"/keheh\": \"\\u06A9\",\n    \"/keheh.fina\": \"\\uFB8F\",\n    \"/keheh.init\": \"\\uFB90\",\n    \"/keheh.isol\": \"\\uFB8E\",\n    \"/keheh.medi\": \"\\uFB91\",\n    \"/kehehDotAbove\": \"\\u0762\",\n    \"/kehehThreeDotsAbove\": \"\\u0763\",\n    \"/kehehThreeDotsUpBelow\": \"\\u0764\",\n    \"/kehehthreedotsbelow\": \"\\u063C\",\n    \"/kehehtwodotsabove\": \"\\u063B\",\n    \"/kehiragana\": \"\\u3051\",\n    \"/kekatakana\": \"\\u30B1\",\n    \"/kekatakanahalfwidth\": \"\\uFF79\",\n    \"/kelvin\": \"\\u212A\",\n    \"/kenarmenian\": \"\\u056F\",\n    \"/keretconsonant\": \"\\uA9BD\",\n    \"/kesmallkatakana\": \"\\u30F6\",\n    \"/key\": \"\\u1F511\",\n    \"/keyboardAndMouse\": \"\\u1F5A6\",\n    \"/keycapTen\": \"\\u1F51F\",\n    \"/kgfullwidth\": \"\\u338F\",\n    \"/kgreenlandic\": \"\\u0138\",\n    \"/khabengali\": \"\\u0996\",\n    \"/khacyrillic\": \"\\u0445\",\n    \"/khadeva\": \"\\u0916\",\n    \"/khagujarati\": \"\\u0A96\",\n    \"/khagurmukhi\": \"\\u0A16\",\n    \"/khah\": \"\\u062E\",\n    \"/khah.fina\": \"\\uFEA6\",\n    \"/khah.init\": \"\\uFEA7\",\n    \"/khah.init_alefmaksura.fina\": \"\\uFD03\",\n    \"/khah.init_hah.fina\": \"\\uFC1A\",\n    \"/khah.init_jeem.fina\": \"\\uFC19\",\n    \"/khah.init_jeem.medi\": \"\\uFCAB\",\n    \"/khah.init_meem.fina\": \"\\uFC1B\",\n    \"/khah.init_meem.medi\": \"\\uFCAC\",\n    \"/khah.init_yeh.fina\": \"\\uFD04\",\n    \"/khah.isol\": \"\\uFEA5\",\n    \"/khah.medi\": \"\\uFEA8\",\n    \"/khah.medi_alefmaksura.fina\": \"\\uFD1F\",\n    \"/khah.medi_yeh.fina\": \"\\uFD20\",\n    \"/khaharabic\": \"\\u062E\",\n    \"/khahfinalarabic\": \"\\uFEA6\",\n    \"/khahinitialarabic\": \"\\uFEA7\",\n    \"/khahmedialarabic\": \"\\uFEA8\",\n    \"/kheicoptic\": \"\\u03E7\",\n    \"/khhadeva\": \"\\u0959\",\n    \"/khhagurmukhi\": \"\\u0A59\",\n    \"/khieukhacirclekorean\": \"\\u3278\",\n    \"/khieukhaparenkorean\": \"\\u3218\",\n    \"/khieukhcirclekorean\": \"\\u326A\",\n    \"/khieukhkorean\": \"\\u314B\",\n    \"/khieukhparenkorean\": \"\\u320A\",\n    \"/khokhaithai\": \"\\u0E02\",\n    \"/khokhonthai\": \"\\u0E05\",\n    \"/khokhuatthai\": \"\\u0E03\",\n    \"/khokhwaithai\": \"\\u0E04\",\n    \"/khomutthai\": \"\\u0E5B\",\n    \"/khook\": \"\\u0199\",\n    \"/khorakhangthai\": \"\\u0E06\",\n    \"/khzfullwidth\": \"\\u3391\",\n    \"/khzsquare\": \"\\u3391\",\n    \"/kicirclekatakana\": \"\\u32D6\",\n    \"/kihiragana\": \"\\u304D\",\n    \"/kikatakana\": \"\\u30AD\",\n    \"/kikatakanahalfwidth\": \"\\uFF77\",\n    \"/kimono\": \"\\u1F458\",\n    \"/kindergartenideographiccircled\": \"\\u3245\",\n    \"/kingblack\": \"\\u265A\",\n    \"/kingwhite\": \"\\u2654\",\n    \"/kip\": \"\\u20AD\",\n    \"/kiroguramusquare\": \"\\u3315\",\n    \"/kiromeetorusquare\": \"\\u3316\",\n    \"/kirosquare\": \"\\u3314\",\n    \"/kirowattosquare\": \"\\u3317\",\n    \"/kiss\": \"\\u1F48F\",\n    \"/kissMark\": \"\\u1F48B\",\n    \"/kissingCatFaceWithClosedEyes\": \"\\u1F63D\",\n    \"/kissingFace\": \"\\u1F617\",\n    \"/kissingFaceWithClosedEyes\": \"\\u1F61A\",\n    \"/kissingFaceWithSmilingEyes\": \"\\u1F619\",\n    \"/kiyeokacirclekorean\": \"\\u326E\",\n    \"/kiyeokaparenkorean\": \"\\u320E\",\n    \"/kiyeokcirclekorean\": \"\\u3260\",\n    \"/kiyeokkorean\": \"\\u3131\",\n    \"/kiyeokparenkorean\": \"\\u3200\",\n    \"/kiyeoksioskorean\": \"\\u3133\",\n    \"/kjecyr\": \"\\u045C\",\n    \"/kjecyrillic\": \"\\u045C\",\n    \"/kkfullwidth\": \"\\u33CD\",\n    \"/klfullwidth\": \"\\u3398\",\n    \"/klinebelow\": \"\\u1E35\",\n    \"/klsquare\": \"\\u3398\",\n    \"/km2fullwidth\": \"\\u33A2\",\n    \"/km3fullwidth\": \"\\u33A6\",\n    \"/kmcapitalfullwidth\": \"\\u33CE\",\n    \"/kmcubedsquare\": \"\\u33A6\",\n    \"/kmfullwidth\": \"\\u339E\",\n    \"/kmonospace\": \"\\uFF4B\",\n    \"/kmsquaredsquare\": \"\\u33A2\",\n    \"/knda:a\": \"\\u0C85\",\n    \"/knda:aa\": \"\\u0C86\",\n    \"/knda:aasign\": \"\\u0CBE\",\n    \"/knda:ai\": \"\\u0C90\",\n    \"/knda:ailength\": \"\\u0CD6\",\n    \"/knda:aisign\": \"\\u0CC8\",\n    \"/knda:anusvara\": \"\\u0C82\",\n    \"/knda:au\": \"\\u0C94\",\n    \"/knda:ausign\": \"\\u0CCC\",\n    \"/knda:avagraha\": \"\\u0CBD\",\n    \"/knda:ba\": \"\\u0CAC\",\n    \"/knda:bha\": \"\\u0CAD\",\n    \"/knda:ca\": \"\\u0C9A\",\n    \"/knda:cha\": \"\\u0C9B\",\n    \"/knda:da\": \"\\u0CA6\",\n    \"/knda:dda\": \"\\u0CA1\",\n    \"/knda:ddha\": \"\\u0CA2\",\n    \"/knda:dha\": \"\\u0CA7\",\n    \"/knda:e\": \"\\u0C8E\",\n    \"/knda:ee\": \"\\u0C8F\",\n    \"/knda:eesign\": \"\\u0CC7\",\n    \"/knda:eight\": \"\\u0CEE\",\n    \"/knda:esign\": \"\\u0CC6\",\n    \"/knda:fa\": \"\\u0CDE\",\n    \"/knda:five\": \"\\u0CEB\",\n    \"/knda:four\": \"\\u0CEA\",\n    \"/knda:ga\": \"\\u0C97\",\n    \"/knda:gha\": \"\\u0C98\",\n    \"/knda:ha\": \"\\u0CB9\",\n    \"/knda:i\": \"\\u0C87\",\n    \"/knda:ii\": \"\\u0C88\",\n    \"/knda:iisign\": \"\\u0CC0\",\n    \"/knda:isign\": \"\\u0CBF\",\n    \"/knda:ja\": \"\\u0C9C\",\n    \"/knda:jha\": \"\\u0C9D\",\n    \"/knda:jihvamuliya\": \"\\u0CF1\",\n    \"/knda:ka\": \"\\u0C95\",\n    \"/knda:kha\": \"\\u0C96\",\n    \"/knda:la\": \"\\u0CB2\",\n    \"/knda:length\": \"\\u0CD5\",\n    \"/knda:lla\": \"\\u0CB3\",\n    \"/knda:llvocal\": \"\\u0CE1\",\n    \"/knda:llvocalsign\": \"\\u0CE3\",\n    \"/knda:lvocal\": \"\\u0C8C\",\n    \"/knda:lvocalsign\": \"\\u0CE2\",\n    \"/knda:ma\": \"\\u0CAE\",\n    \"/knda:na\": \"\\u0CA8\",\n    \"/knda:nga\": \"\\u0C99\",\n    \"/knda:nine\": \"\\u0CEF\",\n    \"/knda:nna\": \"\\u0CA3\",\n    \"/knda:nukta\": \"\\u0CBC\",\n    \"/knda:nya\": \"\\u0C9E\",\n    \"/knda:o\": \"\\u0C92\",\n    \"/knda:one\": \"\\u0CE7\",\n    \"/knda:oo\": \"\\u0C93\",\n    \"/knda:oosign\": \"\\u0CCB\",\n    \"/knda:osign\": \"\\u0CCA\",\n    \"/knda:pa\": \"\\u0CAA\",\n    \"/knda:pha\": \"\\u0CAB\",\n    \"/knda:ra\": \"\\u0CB0\",\n    \"/knda:rra\": \"\\u0CB1\",\n    \"/knda:rrvocal\": \"\\u0CE0\",\n    \"/knda:rrvocalsign\": \"\\u0CC4\",\n    \"/knda:rvocal\": \"\\u0C8B\",\n    \"/knda:rvocalsign\": \"\\u0CC3\",\n    \"/knda:sa\": \"\\u0CB8\",\n    \"/knda:seven\": \"\\u0CED\",\n    \"/knda:sha\": \"\\u0CB6\",\n    \"/knda:signcandrabindu\": \"\\u0C81\",\n    \"/knda:signspacingcandrabindu\": \"\\u0C80\",\n    \"/knda:six\": \"\\u0CEC\",\n    \"/knda:ssa\": \"\\u0CB7\",\n    \"/knda:ta\": \"\\u0CA4\",\n    \"/knda:tha\": \"\\u0CA5\",\n    \"/knda:three\": \"\\u0CE9\",\n    \"/knda:tta\": \"\\u0C9F\",\n    \"/knda:ttha\": \"\\u0CA0\",\n    \"/knda:two\": \"\\u0CE8\",\n    \"/knda:u\": \"\\u0C89\",\n    \"/knda:upadhmaniya\": \"\\u0CF2\",\n    \"/knda:usign\": \"\\u0CC1\",\n    \"/knda:uu\": \"\\u0C8A\",\n    \"/knda:uusign\": \"\\u0CC2\",\n    \"/knda:va\": \"\\u0CB5\",\n    \"/knda:virama\": \"\\u0CCD\",\n    \"/knda:visarga\": \"\\u0C83\",\n    \"/knda:ya\": \"\\u0CAF\",\n    \"/knda:zero\": \"\\u0CE6\",\n    \"/knightblack\": \"\\u265E\",\n    \"/knightwhite\": \"\\u2658\",\n    \"/ko:a\": \"\\u314F\",\n    \"/ko:ae\": \"\\u3150\",\n    \"/ko:aejungseong\": \"\\u1162\",\n    \"/ko:aeujungseong\": \"\\u11A3\",\n    \"/ko:ajungseong\": \"\\u1161\",\n    \"/ko:aojungseong\": \"\\u1176\",\n    \"/ko:araea\": \"\\u318D\",\n    \"/ko:araeae\": \"\\u318E\",\n    \"/ko:araeaeojungseong\": \"\\u119F\",\n    \"/ko:araeaijungseong\": \"\\u11A1\",\n    \"/ko:araeajungseong\": \"\\u119E\",\n    \"/ko:araeaujungseong\": \"\\u11A0\",\n    \"/ko:aujungseong\": \"\\u1177\",\n    \"/ko:ceongchieumchieuchchoseong\": \"\\u1155\",\n    \"/ko:ceongchieumcieucchoseong\": \"\\u1150\",\n    \"/ko:ceongchieumsioschoseong\": \"\\u113E\",\n    \"/ko:ceongchieumssangcieucchoseong\": \"\\u1151\",\n    \"/ko:ceongchieumssangsioschoseong\": \"\\u113F\",\n    \"/ko:chieuch\": \"\\u314A\",\n    \"/ko:chieuchchoseong\": \"\\u110E\",\n    \"/ko:chieuchhieuhchoseong\": \"\\u1153\",\n    \"/ko:chieuchjongseong\": \"\\u11BE\",\n    \"/ko:chieuchkhieukhchoseong\": \"\\u1152\",\n    \"/ko:chitueumchieuchchoseong\": \"\\u1154\",\n    \"/ko:chitueumcieucchoseong\": \"\\u114E\",\n    \"/ko:chitueumsioschoseong\": \"\\u113C\",\n    \"/ko:chitueumssangcieucchoseong\": \"\\u114F\",\n    \"/ko:chitueumssangsioschoseong\": \"\\u113D\",\n    \"/ko:cieuc\": \"\\u3148\",\n    \"/ko:cieucchoseong\": \"\\u110C\",\n    \"/ko:cieucieungchoseong\": \"\\u114D\",\n    \"/ko:cieucjongseong\": \"\\u11BD\",\n    \"/ko:e\": \"\\u3154\",\n    \"/ko:ejungseong\": \"\\u1166\",\n    \"/ko:eo\": \"\\u3153\",\n    \"/ko:eo_eujungseong\": \"\\u117C\",\n    \"/ko:eojungseong\": \"\\u1165\",\n    \"/ko:eoojungseong\": \"\\u117A\",\n    \"/ko:eoujungseong\": \"\\u117B\",\n    \"/ko:eu\": \"\\u3161\",\n    \"/ko:eueujungseong\": \"\\u1196\",\n    \"/ko:eujungseong\": \"\\u1173\",\n    \"/ko:euujungseong\": \"\\u1195\",\n    \"/ko:filler\": \"\\u3164\",\n    \"/ko:fillerchoseong\": \"\\u115F\",\n    \"/ko:fillerjungseong\": \"\\u1160\",\n    \"/ko:hieuh\": \"\\u314E\",\n    \"/ko:hieuhchoseong\": \"\\u1112\",\n    \"/ko:hieuhjongseong\": \"\\u11C2\",\n    \"/ko:hieuhmieumjongseong\": \"\\u11F7\",\n    \"/ko:hieuhnieunjongseong\": \"\\u11F5\",\n    \"/ko:hieuhpieupjongseong\": \"\\u11F8\",\n    \"/ko:hieuhrieuljongseong\": \"\\u11F6\",\n    \"/ko:i\": \"\\u3163\",\n    \"/ko:iajungseong\": \"\\u1198\",\n    \"/ko:iaraeajungseong\": \"\\u119D\",\n    \"/ko:ieujungseong\": \"\\u119C\",\n    \"/ko:ieung\": \"\\u3147\",\n    \"/ko:ieungchieuchchoseong\": \"\\u1149\",\n    \"/ko:ieungchoseong\": \"\\u110B\",\n    \"/ko:ieungcieucchoseong\": \"\\u1148\",\n    \"/ko:ieungjongseong\": \"\\u11BC\",\n    \"/ko:ieungkhieukhjongseong\": \"\\u11EF\",\n    \"/ko:ieungkiyeokchoseong\": \"\\u1141\",\n    \"/ko:ieungkiyeokjongseong\": \"\\u11EC\",\n    \"/ko:ieungmieumchoseong\": \"\\u1143\",\n    \"/ko:ieungpansioschoseong\": \"\\u1146\",\n    \"/ko:ieungphieuphchoseong\": \"\\u114B\",\n    \"/ko:ieungpieupchoseong\": \"\\u1144\",\n    \"/ko:ieungsioschoseong\": \"\\u1145\",\n    \"/ko:ieungssangkiyeokjongseong\": \"\\u11ED\",\n    \"/ko:ieungthieuthchoseong\": \"\\u114A\",\n    \"/ko:ieungtikeutchoseong\": \"\\u1142\",\n    \"/ko:ijungseong\": \"\\u1175\",\n    \"/ko:iojungseong\": \"\\u119A\",\n    \"/ko:iujungseong\": \"\\u119B\",\n    \"/ko:iyajungseong\": \"\\u1199\",\n    \"/ko:kapyeounmieum\": \"\\u3171\",\n    \"/ko:kapyeounmieumchoseong\": \"\\u111D\",\n    \"/ko:kapyeounmieumjongseong\": \"\\u11E2\",\n    \"/ko:kapyeounphieuph\": \"\\u3184\",\n    \"/ko:kapyeounphieuphchoseong\": \"\\u1157\",\n    \"/ko:kapyeounphieuphjongseong\": \"\\u11F4\",\n    \"/ko:kapyeounpieup\": \"\\u3178\",\n    \"/ko:kapyeounpieupchoseong\": \"\\u112B\",\n    \"/ko:kapyeounpieupjongseong\": \"\\u11E6\",\n    \"/ko:kapyeounrieulchoseong\": \"\\u111B\",\n    \"/ko:kapyeounssangpieup\": \"\\u3179\",\n    \"/ko:kapyeounssangpieupchoseong\": \"\\u112C\",\n    \"/ko:khieukh\": \"\\u314B\",\n    \"/ko:khieukhchoseong\": \"\\u110F\",\n    \"/ko:khieukhjongseong\": \"\\u11BF\",\n    \"/ko:kiyeok\": \"\\u3131\",\n    \"/ko:kiyeokchieuchjongseong\": \"\\u11FC\",\n    \"/ko:kiyeokchoseong\": \"\\u1100\",\n    \"/ko:kiyeokhieuhjongseong\": \"\\u11FE\",\n    \"/ko:kiyeokjongseong\": \"\\u11A8\",\n    \"/ko:kiyeokkhieukhjongseong\": \"\\u11FD\",\n    \"/ko:kiyeoknieunjongseong\": \"\\u11FA\",\n    \"/ko:kiyeokpieupjongseong\": \"\\u11FB\",\n    \"/ko:kiyeokrieuljongseong\": \"\\u11C3\",\n    \"/ko:kiyeoksios\": \"\\u3133\",\n    \"/ko:kiyeoksiosjongseong\": \"\\u11AA\",\n    \"/ko:kiyeoksioskiyeokjongseong\": \"\\u11C4\",\n    \"/ko:kiyeoktikeutchoseong\": \"\\u115A\",\n    \"/ko:mieum\": \"\\u3141\",\n    \"/ko:mieumchieuchjongseong\": \"\\u11E0\",\n    \"/ko:mieumchoseong\": \"\\u1106\",\n    \"/ko:mieumhieuhjongseong\": \"\\u11E1\",\n    \"/ko:mieumjongseong\": \"\\u11B7\",\n    \"/ko:mieumkiyeokjongseong\": \"\\u11DA\",\n    \"/ko:mieumpansios\": \"\\u3170\",\n    \"/ko:mieumpansiosjongseong\": \"\\u11DF\",\n    \"/ko:mieumpieup\": \"\\u316E\",\n    \"/ko:mieumpieupchoseong\": \"\\u111C\",\n    \"/ko:mieumpieupjongseong\": \"\\u11DC\",\n    \"/ko:mieumrieuljongseong\": \"\\u11DB\",\n    \"/ko:mieumsios\": \"\\u316F\",\n    \"/ko:mieumsiosjongseong\": \"\\u11DD\",\n    \"/ko:mieumssangsiosjongseong\": \"\\u11DE\",\n    \"/ko:nieun\": \"\\u3134\",\n    \"/ko:nieunchoseong\": \"\\u1102\",\n    \"/ko:nieuncieuc\": \"\\u3135\",\n    \"/ko:nieuncieucchoseong\": \"\\u115C\",\n    \"/ko:nieuncieucjongseong\": \"\\u11AC\",\n    \"/ko:nieunhieuh\": \"\\u3136\",\n    \"/ko:nieunhieuhchoseong\": \"\\u115D\",\n    \"/ko:nieunhieuhjongseong\": \"\\u11AD\",\n    \"/ko:nieunjongseong\": \"\\u11AB\",\n    \"/ko:nieunkiyeokchoseong\": \"\\u1113\",\n    \"/ko:nieunkiyeokjongseong\": \"\\u11C5\",\n    \"/ko:nieunpansios\": \"\\u3168\",\n    \"/ko:nieunpansiosjongseong\": \"\\u11C8\",\n    \"/ko:nieunpieupchoseong\": \"\\u1116\",\n    \"/ko:nieunsios\": \"\\u3167\",\n    \"/ko:nieunsioschoseong\": \"\\u115B\",\n    \"/ko:nieunsiosjongseong\": \"\\u11C7\",\n    \"/ko:nieunthieuthjongseong\": \"\\u11C9\",\n    \"/ko:nieuntikeut\": \"\\u3166\",\n    \"/ko:nieuntikeutchoseong\": \"\\u1115\",\n    \"/ko:nieuntikeutjongseong\": \"\\u11C6\",\n    \"/ko:o\": \"\\u3157\",\n    \"/ko:o_ejungseong\": \"\\u1180\",\n    \"/ko:o_eojungseong\": \"\\u117F\",\n    \"/ko:oe\": \"\\u315A\",\n    \"/ko:oejungseong\": \"\\u116C\",\n    \"/ko:ojungseong\": \"\\u1169\",\n    \"/ko:oojungseong\": \"\\u1182\",\n    \"/ko:oujungseong\": \"\\u1183\",\n    \"/ko:oyaejungseong\": \"\\u11A7\",\n    \"/ko:oyajungseong\": \"\\u11A6\",\n    \"/ko:oyejungseong\": \"\\u1181\",\n    \"/ko:pansios\": \"\\u317F\",\n    \"/ko:pansioschoseong\": \"\\u1140\",\n    \"/ko:pansiosjongseong\": \"\\u11EB\",\n    \"/ko:phieuph\": \"\\u314D\",\n    \"/ko:phieuphchoseong\": \"\\u1111\",\n    \"/ko:phieuphjongseong\": \"\\u11C1\",\n    \"/ko:phieuphpieupchoseong\": \"\\u1156\",\n    \"/ko:phieuphpieupjongseong\": \"\\u11F3\",\n    \"/ko:pieup\": \"\\u3142\",\n    \"/ko:pieupchieuchchoseong\": \"\\u1128\",\n    \"/ko:pieupchoseong\": \"\\u1107\",\n    \"/ko:pieupcieuc\": \"\\u3176\",\n    \"/ko:pieupcieucchoseong\": \"\\u1127\",\n    \"/ko:pieuphieuhjongseong\": \"\\u11E5\",\n    \"/ko:pieupjongseong\": \"\\u11B8\",\n    \"/ko:pieupkiyeok\": \"\\u3172\",\n    \"/ko:pieupkiyeokchoseong\": \"\\u111E\",\n    \"/ko:pieupnieunchoseong\": \"\\u111F\",\n    \"/ko:pieupphieuphchoseong\": \"\\u112A\",\n    \"/ko:pieupphieuphjongseong\": \"\\u11E4\",\n    \"/ko:pieuprieuljongseong\": \"\\u11E3\",\n    \"/ko:pieupsios\": \"\\u3144\",\n    \"/ko:pieupsioschoseong\": \"\\u1121\",\n    \"/ko:pieupsioscieucchoseong\": \"\\u1126\",\n    \"/ko:pieupsiosjongseong\": \"\\u11B9\",\n    \"/ko:pieupsioskiyeok\": \"\\u3174\",\n    \"/ko:pieupsioskiyeokchoseong\": \"\\u1122\",\n    \"/ko:pieupsiospieupchoseong\": \"\\u1124\",\n    \"/ko:pieupsiostikeut\": \"\\u3175\",\n    \"/ko:pieupsiostikeutchoseong\": \"\\u1123\",\n    \"/ko:pieupssangsioschoseong\": \"\\u1125\",\n    \"/ko:pieupthieuth\": \"\\u3177\",\n    \"/ko:pieupthieuthchoseong\": \"\\u1129\",\n    \"/ko:pieuptikeut\": \"\\u3173\",\n    \"/ko:pieuptikeutchoseong\": \"\\u1120\",\n    \"/ko:rieul\": \"\\u3139\",\n    \"/ko:rieulchoseong\": \"\\u1105\",\n    \"/ko:rieulhieuh\": \"\\u3140\",\n    \"/ko:rieulhieuhchoseong\": \"\\u111A\",\n    \"/ko:rieulhieuhjongseong\": \"\\u11B6\",\n    \"/ko:rieuljongseong\": \"\\u11AF\",\n    \"/ko:rieulkapyeounpieupjongseong\": \"\\u11D5\",\n    \"/ko:rieulkhieukhjongseong\": \"\\u11D8\",\n    \"/ko:rieulkiyeok\": \"\\u313A\",\n    \"/ko:rieulkiyeokjongseong\": \"\\u11B0\",\n    \"/ko:rieulkiyeoksios\": \"\\u3169\",\n    \"/ko:rieulkiyeoksiosjongseong\": \"\\u11CC\",\n    \"/ko:rieulmieum\": \"\\u313B\",\n    \"/ko:rieulmieumjongseong\": \"\\u11B1\",\n    \"/ko:rieulmieumkiyeokjongseong\": \"\\u11D1\",\n    \"/ko:rieulmieumsiosjongseong\": \"\\u11D2\",\n    \"/ko:rieulnieunchoseong\": \"\\u1118\",\n    \"/ko:rieulnieunjongseong\": \"\\u11CD\",\n    \"/ko:rieulpansios\": \"\\u316C\",\n    \"/ko:rieulpansiosjongseong\": \"\\u11D7\",\n    \"/ko:rieulphieuph\": \"\\u313F\",\n    \"/ko:rieulphieuphjongseong\": \"\\u11B5\",\n    \"/ko:rieulpieup\": \"\\u313C\",\n    \"/ko:rieulpieuphieuhjongseong\": \"\\u11D4\",\n    \"/ko:rieulpieupjongseong\": \"\\u11B2\",\n    \"/ko:rieulpieupsios\": \"\\u316B\",\n    \"/ko:rieulpieupsiosjongseong\": \"\\u11D3\",\n    \"/ko:rieulsios\": \"\\u313D\",\n    \"/ko:rieulsiosjongseong\": \"\\u11B3\",\n    \"/ko:rieulssangsiosjongseong\": \"\\u11D6\",\n    \"/ko:rieulthieuth\": \"\\u313E\",\n    \"/ko:rieulthieuthjongseong\": \"\\u11B4\",\n    \"/ko:rieultikeut\": \"\\u316A\",\n    \"/ko:rieultikeuthieuhjongseong\": \"\\u11CF\",\n    \"/ko:rieultikeutjongseong\": \"\\u11CE\",\n    \"/ko:rieulyeorinhieuh\": \"\\u316D\",\n    \"/ko:rieulyeorinhieuhjongseong\": \"\\u11D9\",\n    \"/ko:sios\": \"\\u3145\",\n    \"/ko:sioschieuchchoseong\": \"\\u1137\",\n    \"/ko:sioschoseong\": \"\\u1109\",\n    \"/ko:sioscieuc\": \"\\u317E\",\n    \"/ko:sioscieucchoseong\": \"\\u1136\",\n    \"/ko:sioshieuhchoseong\": \"\\u113B\",\n    \"/ko:siosieungchoseong\": \"\\u1135\",\n    \"/ko:siosjongseong\": \"\\u11BA\",\n    \"/ko:sioskhieukhchoseong\": \"\\u1138\",\n    \"/ko:sioskiyeok\": \"\\u317A\",\n    \"/ko:sioskiyeokchoseong\": \"\\u112D\",\n    \"/ko:sioskiyeokjongseong\": \"\\u11E7\",\n    \"/ko:siosmieumchoseong\": \"\\u1131\",\n    \"/ko:siosnieun\": \"\\u317B\",\n    \"/ko:siosnieunchoseong\": \"\\u112E\",\n    \"/ko:siosphieuphchoseong\": \"\\u113A\",\n    \"/ko:siospieup\": \"\\u317D\",\n    \"/ko:siospieupchoseong\": \"\\u1132\",\n    \"/ko:siospieupjongseong\": \"\\u11EA\",\n    \"/ko:siospieupkiyeokchoseong\": \"\\u1133\",\n    \"/ko:siosrieulchoseong\": \"\\u1130\",\n    \"/ko:siosrieuljongseong\": \"\\u11E9\",\n    \"/ko:siosssangsioschoseong\": \"\\u1134\",\n    \"/ko:siosthieuthchoseong\": \"\\u1139\",\n    \"/ko:siostikeut\": \"\\u317C\",\n    \"/ko:siostikeutchoseong\": \"\\u112F\",\n    \"/ko:siostikeutjongseong\": \"\\u11E8\",\n    \"/ko:ssangaraeajungseong\": \"\\u11A2\",\n    \"/ko:ssangcieuc\": \"\\u3149\",\n    \"/ko:ssangcieucchoseong\": \"\\u110D\",\n    \"/ko:ssanghieuh\": \"\\u3185\",\n    \"/ko:ssanghieuhchoseong\": \"\\u1158\",\n    \"/ko:ssangieung\": \"\\u3180\",\n    \"/ko:ssangieungchoseong\": \"\\u1147\",\n    \"/ko:ssangieungjongseong\": \"\\u11EE\",\n    \"/ko:ssangkiyeok\": \"\\u3132\",\n    \"/ko:ssangkiyeokchoseong\": \"\\u1101\",\n    \"/ko:ssangkiyeokjongseong\": \"\\u11A9\",\n    \"/ko:ssangnieun\": \"\\u3165\",\n    \"/ko:ssangnieunchoseong\": \"\\u1114\",\n    \"/ko:ssangnieunjongseong\": \"\\u11FF\",\n    \"/ko:ssangpieup\": \"\\u3143\",\n    \"/ko:ssangpieupchoseong\": \"\\u1108\",\n    \"/ko:ssangrieulchoseong\": \"\\u1119\",\n    \"/ko:ssangrieuljongseong\": \"\\u11D0\",\n    \"/ko:ssangsios\": \"\\u3146\",\n    \"/ko:ssangsioschoseong\": \"\\u110A\",\n    \"/ko:ssangsiosjongseong\": \"\\u11BB\",\n    \"/ko:ssangtikeut\": \"\\u3138\",\n    \"/ko:ssangtikeutchoseong\": \"\\u1104\",\n    \"/ko:thieuth\": \"\\u314C\",\n    \"/ko:thieuthchoseong\": \"\\u1110\",\n    \"/ko:thieuthjongseong\": \"\\u11C0\",\n    \"/ko:tikeut\": \"\\u3137\",\n    \"/ko:tikeutchoseong\": \"\\u1103\",\n    \"/ko:tikeutjongseong\": \"\\u11AE\",\n    \"/ko:tikeutkiyeokchoseong\": \"\\u1117\",\n    \"/ko:tikeutkiyeokjongseong\": \"\\u11CA\",\n    \"/ko:tikeutrieulchoseong\": \"\\u115E\",\n    \"/ko:tikeutrieuljongseong\": \"\\u11CB\",\n    \"/ko:u\": \"\\u315C\",\n    \"/ko:uaejungseong\": \"\\u118A\",\n    \"/ko:uajungseong\": \"\\u1189\",\n    \"/ko:ueo_eujungseong\": \"\\u118B\",\n    \"/ko:ujungseong\": \"\\u116E\",\n    \"/ko:uujungseong\": \"\\u118D\",\n    \"/ko:uyejungseong\": \"\\u118C\",\n    \"/ko:wa\": \"\\u3158\",\n    \"/ko:wae\": \"\\u3159\",\n    \"/ko:waejungseong\": \"\\u116B\",\n    \"/ko:wajungseong\": \"\\u116A\",\n    \"/ko:we\": \"\\u315E\",\n    \"/ko:wejungseong\": \"\\u1170\",\n    \"/ko:weo\": \"\\u315D\",\n    \"/ko:weojungseong\": \"\\u116F\",\n    \"/ko:wi\": \"\\u315F\",\n    \"/ko:wijungseong\": \"\\u1171\",\n    \"/ko:ya\": \"\\u3151\",\n    \"/ko:yae\": \"\\u3152\",\n    \"/ko:yaejungseong\": \"\\u1164\",\n    \"/ko:yajungseong\": \"\\u1163\",\n    \"/ko:yaojungseong\": \"\\u1178\",\n    \"/ko:yaujungseong\": \"\\u11A4\",\n    \"/ko:yayojungseong\": \"\\u1179\",\n    \"/ko:ye\": \"\\u3156\",\n    \"/ko:yejungseong\": \"\\u1168\",\n    \"/ko:yeo\": \"\\u3155\",\n    \"/ko:yeojungseong\": \"\\u1167\",\n    \"/ko:yeoojungseong\": \"\\u117D\",\n    \"/ko:yeorinhieuh\": \"\\u3186\",\n    \"/ko:yeorinhieuhchoseong\": \"\\u1159\",\n    \"/ko:yeorinhieuhjongseong\": \"\\u11F9\",\n    \"/ko:yeoujungseong\": \"\\u117E\",\n    \"/ko:yeoyajungseong\": \"\\u11A5\",\n    \"/ko:yesieung\": \"\\u3181\",\n    \"/ko:yesieungchoseong\": \"\\u114C\",\n    \"/ko:yesieungjongseong\": \"\\u11F0\",\n    \"/ko:yesieungpansios\": \"\\u3183\",\n    \"/ko:yesieungpansiosjongseong\": \"\\u11F2\",\n    \"/ko:yesieungsios\": \"\\u3182\",\n    \"/ko:yesieungsiosjongseong\": \"\\u11F1\",\n    \"/ko:yi\": \"\\u3162\",\n    \"/ko:yijungseong\": \"\\u1174\",\n    \"/ko:yiujungseong\": \"\\u1197\",\n    \"/ko:yo\": \"\\u315B\",\n    \"/ko:yoi\": \"\\u3189\",\n    \"/ko:yoijungseong\": \"\\u1188\",\n    \"/ko:yojungseong\": \"\\u116D\",\n    \"/ko:yoojungseong\": \"\\u1187\",\n    \"/ko:yoya\": \"\\u3187\",\n    \"/ko:yoyae\": \"\\u3188\",\n    \"/ko:yoyaejungseong\": \"\\u1185\",\n    \"/ko:yoyajungseong\": \"\\u1184\",\n    \"/ko:yoyeojungseong\": \"\\u1186\",\n    \"/ko:yu\": \"\\u3160\",\n    \"/ko:yuajungseong\": \"\\u118E\",\n    \"/ko:yuejungseong\": \"\\u1190\",\n    \"/ko:yueojungseong\": \"\\u118F\",\n    \"/ko:yui\": \"\\u318C\",\n    \"/ko:yuijungseong\": \"\\u1194\",\n    \"/ko:yujungseong\": \"\\u1172\",\n    \"/ko:yuujungseong\": \"\\u1193\",\n    \"/ko:yuye\": \"\\u318B\",\n    \"/ko:yuyejungseong\": \"\\u1192\",\n    \"/ko:yuyeo\": \"\\u318A\",\n    \"/ko:yuyeojungseong\": \"\\u1191\",\n    \"/koala\": \"\\u1F428\",\n    \"/kobliquestroke\": \"\\uA7A3\",\n    \"/kocirclekatakana\": \"\\u32D9\",\n    \"/kohiragana\": \"\\u3053\",\n    \"/kohmfullwidth\": \"\\u33C0\",\n    \"/kohmsquare\": \"\\u33C0\",\n    \"/kokaithai\": \"\\u0E01\",\n    \"/kokatakana\": \"\\u30B3\",\n    \"/kokatakanahalfwidth\": \"\\uFF7A\",\n    \"/kooposquare\": \"\\u331E\",\n    \"/koppa\": \"\\u03DF\",\n    \"/koppaarchaic\": \"\\u03D9\",\n    \"/koppacyr\": \"\\u0481\",\n    \"/koppacyrillic\": \"\\u0481\",\n    \"/koreanstandardsymbol\": \"\\u327F\",\n    \"/koroniscmb\": \"\\u0343\",\n    \"/korunasquare\": \"\\u331D\",\n    \"/kotoideographiccircled\": \"\\u3247\",\n    \"/kpafullwidth\": \"\\u33AA\",\n    \"/kparen\": \"\\u24A6\",\n    \"/kparenthesized\": \"\\u24A6\",\n    \"/kpasquare\": \"\\u33AA\",\n    \"/kra\": \"\\u0138\",\n    \"/ksicyr\": \"\\u046F\",\n    \"/ksicyrillic\": \"\\u046F\",\n    \"/kstroke\": \"\\uA741\",\n    \"/kstrokediagonalstroke\": \"\\uA745\",\n    \"/ktfullwidth\": \"\\u33CF\",\n    \"/ktsquare\": \"\\u33CF\",\n    \"/kturned\": \"\\u029E\",\n    \"/kucirclekatakana\": \"\\u32D7\",\n    \"/kuhiragana\": \"\\u304F\",\n    \"/kukatakana\": \"\\u30AF\",\n    \"/kukatakanahalfwidth\": \"\\uFF78\",\n    \"/kuroonesquare\": \"\\u331B\",\n    \"/kuruzeirosquare\": \"\\u331A\",\n    \"/kvfullwidth\": \"\\u33B8\",\n    \"/kvsquare\": \"\\u33B8\",\n    \"/kwfullwidth\": \"\\u33BE\",\n    \"/kwsquare\": \"\\u33BE\",\n    \"/kyuriisquare\": \"\\u3312\",\n    \"/l\": \"\\u006C\",\n    \"/l.inferior\": \"\\u2097\",\n    \"/label\": \"\\u1F3F7\",\n    \"/labengali\": \"\\u09B2\",\n    \"/laborideographiccircled\": \"\\u3298\",\n    \"/laborideographicparen\": \"\\u3238\",\n    \"/lacute\": \"\\u013A\",\n    \"/ladeva\": \"\\u0932\",\n    \"/ladyBeetle\": \"\\u1F41E\",\n    \"/lagujarati\": \"\\u0AB2\",\n    \"/lagurmukhi\": \"\\u0A32\",\n    \"/lakkhangyaothai\": \"\\u0E45\",\n    \"/lam\": \"\\u0644\",\n    \"/lam.fina\": \"\\uFEDE\",\n    \"/lam.init\": \"\\uFEDF\",\n    \"/lam.init_alef.fina\": \"\\uFEFB\",\n    \"/lam.init_alef.medi_hamzaabove.fina\": \"\\uFEF7\",\n    \"/lam.init_alef.medi_hamzabelow.fina\": \"\\uFEF9\",\n    \"/lam.init_alef.medi_maddaabove.fina\": \"\\uFEF5\",\n    \"/lam.init_alefmaksura.fina\": \"\\uFC43\",\n    \"/lam.init_hah.fina\": \"\\uFC40\",\n    \"/lam.init_hah.medi\": \"\\uFCCA\",\n    \"/lam.init_hah.medi_meem.medi\": \"\\uFDB5\",\n    \"/lam.init_heh.medi\": \"\\uFCCD\",\n    \"/lam.init_jeem.fina\": \"\\uFC3F\",\n    \"/lam.init_jeem.medi\": \"\\uFCC9\",\n    \"/lam.init_jeem.medi_jeem.medi\": \"\\uFD83\",\n    \"/lam.init_jeem.medi_meem.medi\": \"\\uFDBA\",\n    \"/lam.init_khah.fina\": \"\\uFC41\",\n    \"/lam.init_khah.medi\": \"\\uFCCB\",\n    \"/lam.init_khah.medi_meem.medi\": \"\\uFD86\",\n    \"/lam.init_meem.fina\": \"\\uFC42\",\n    \"/lam.init_meem.medi\": \"\\uFCCC\",\n    \"/lam.init_meem.medi_hah.medi\": \"\\uFD88\",\n    \"/lam.init_yeh.fina\": \"\\uFC44\",\n    \"/lam.isol\": \"\\uFEDD\",\n    \"/lam.medi\": \"\\uFEE0\",\n    \"/lam.medi_alef.fina\": \"\\uFEFC\",\n    \"/lam.medi_alef.medi_hamzaabove.fina\": \"\\uFEF8\",\n    \"/lam.medi_alef.medi_hamzabelow.fina\": \"\\uFEFA\",\n    \"/lam.medi_alef.medi_maddaabove.fina\": \"\\uFEF6\",\n    \"/lam.medi_alefmaksura.fina\": \"\\uFC86\",\n    \"/lam.medi_hah.medi_alefmaksura.fina\": \"\\uFD82\",\n    \"/lam.medi_hah.medi_meem.fina\": \"\\uFD80\",\n    \"/lam.medi_hah.medi_yeh.fina\": \"\\uFD81\",\n    \"/lam.medi_jeem.medi_jeem.fina\": \"\\uFD84\",\n    \"/lam.medi_jeem.medi_meem.fina\": \"\\uFDBC\",\n    \"/lam.medi_jeem.medi_yeh.fina\": \"\\uFDAC\",\n    \"/lam.medi_khah.medi_meem.fina\": \"\\uFD85\",\n    \"/lam.medi_meem.fina\": \"\\uFC85\",\n    \"/lam.medi_meem.medi\": \"\\uFCED\",\n    \"/lam.medi_meem.medi_hah.fina\": \"\\uFD87\",\n    \"/lam.medi_meem.medi_yeh.fina\": \"\\uFDAD\",\n    \"/lam.medi_yeh.fina\": \"\\uFC87\",\n    \"/lamBar\": \"\\u076A\",\n    \"/lamVabove\": \"\\u06B5\",\n    \"/lamalefabove\": \"\\u06D9\",\n    \"/lamaleffinalarabic\": \"\\uFEFC\",\n    \"/lamalefhamzaabovefinalarabic\": \"\\uFEF8\",\n    \"/lamalefhamzaaboveisolatedarabic\": \"\\uFEF7\",\n    \"/lamalefhamzabelowfinalarabic\": \"\\uFEFA\",\n    \"/lamalefhamzabelowisolatedarabic\": \"\\uFEF9\",\n    \"/lamalefisolatedarabic\": \"\\uFEFB\",\n    \"/lamalefmaddaabovefinalarabic\": \"\\uFEF6\",\n    \"/lamalefmaddaaboveisolatedarabic\": \"\\uFEF5\",\n    \"/lamarabic\": \"\\u0644\",\n    \"/lambda\": \"\\u03BB\",\n    \"/lambdastroke\": \"\\u019B\",\n    \"/lamdotabove\": \"\\u06B6\",\n    \"/lamed\": \"\\u05DC\",\n    \"/lamed:hb\": \"\\u05DC\",\n    \"/lameddagesh\": \"\\uFB3C\",\n    \"/lameddageshhebrew\": \"\\uFB3C\",\n    \"/lamedhebrew\": \"\\u05DC\",\n    \"/lamedholam\": \"\\u05DC\",\n    \"/lamedholamdagesh\": \"\\u05DC\",\n    \"/lamedholamdageshhebrew\": \"\\u05DC\",\n    \"/lamedholamhebrew\": \"\\u05DC\",\n    \"/lamedwide:hb\": \"\\uFB25\",\n    \"/lamedwithdagesh:hb\": \"\\uFB3C\",\n    \"/lamfinalarabic\": \"\\uFEDE\",\n    \"/lamhahinitialarabic\": \"\\uFCCA\",\n    \"/laminitialarabic\": \"\\uFEDF\",\n    \"/lamjeeminitialarabic\": \"\\uFCC9\",\n    \"/lamkhahinitialarabic\": \"\\uFCCB\",\n    \"/lamlamhehisolatedarabic\": \"\\uFDF2\",\n    \"/lammedialarabic\": \"\\uFEE0\",\n    \"/lammeemhahinitialarabic\": \"\\uFD88\",\n    \"/lammeeminitialarabic\": \"\\uFCCC\",\n    \"/lammeemjeeminitialarabic\": \"\\uFEDF\",\n    \"/lammeemkhahinitialarabic\": \"\\uFEDF\",\n    \"/lamthreedotsabove\": \"\\u06B7\",\n    \"/lamthreedotsbelow\": \"\\u06B8\",\n    \"/lanemergeleftblack\": \"\\u26D8\",\n    \"/lanemergeleftwhite\": \"\\u26D9\",\n    \"/largeBlueCircle\": \"\\u1F535\",\n    \"/largeBlueDiamond\": \"\\u1F537\",\n    \"/largeOrangeDiamond\": \"\\u1F536\",\n    \"/largeRedCircle\": \"\\u1F534\",\n    \"/largecircle\": \"\\u25EF\",\n    \"/largetackdown\": \"\\u27D9\",\n    \"/largetackup\": \"\\u27D8\",\n    \"/lari\": \"\\u20BE\",\n    \"/lastQuarterMoon\": \"\\u1F317\",\n    \"/lastQuarterMoonFace\": \"\\u1F31C\",\n    \"/lastquartermoon\": \"\\u263E\",\n    \"/layar\": \"\\uA982\",\n    \"/lazysinverted\": \"\\u223E\",\n    \"/lbar\": \"\\u019A\",\n    \"/lbbar\": \"\\u2114\",\n    \"/lbelt\": \"\\u026C\",\n    \"/lbeltretroflex\": \"\\uA78E\",\n    \"/lbopomofo\": \"\\u310C\",\n    \"/lbroken\": \"\\uA747\",\n    \"/lcaron\": \"\\u013E\",\n    \"/lcedilla\": \"\\u013C\",\n    \"/lcircle\": \"\\u24DB\",\n    \"/lcircumflexbelow\": \"\\u1E3D\",\n    \"/lcommaaccent\": \"\\u013C\",\n    \"/lcurl\": \"\\u0234\",\n    \"/ldblbar\": \"\\u2C61\",\n    \"/ldot\": \"\\u0140\",\n    \"/ldotaccent\": \"\\u0140\",\n    \"/ldotbelow\": \"\\u1E37\",\n    \"/ldotbelowmacron\": \"\\u1E39\",\n    \"/leafFlutteringInWind\": \"\\u1F343\",\n    \"/ledger\": \"\\u1F4D2\",\n    \"/left-pointingMagnifyingGlass\": \"\\u1F50D\",\n    \"/leftAngerBubble\": \"\\u1F5EE\",\n    \"/leftFiveEighthsBlock\": \"\\u258B\",\n    \"/leftHalfBlock\": \"\\u258C\",\n    \"/leftHandTelephoneReceiver\": \"\\u1F57B\",\n    \"/leftLuggage\": \"\\u1F6C5\",\n    \"/leftOneEighthBlock\": \"\\u258F\",\n    \"/leftOneQuarterBlock\": \"\\u258E\",\n    \"/leftSevenEighthsBlock\": \"\\u2589\",\n    \"/leftSpeechBubble\": \"\\u1F5E8\",\n    \"/leftThoughtBubble\": \"\\u1F5EC\",\n    \"/leftThreeEighthsBlock\": \"\\u258D\",\n    \"/leftThreeQuartersBlock\": \"\\u258A\",\n    \"/leftWritingHand\": \"\\u1F58E\",\n    \"/leftangleabovecmb\": \"\\u031A\",\n    \"/leftarrowoverrightarrow\": \"\\u21C6\",\n    \"/leftdnheavyrightuplight\": \"\\u2545\",\n    \"/leftharpoonoverrightharpoon\": \"\\u21CB\",\n    \"/leftheavyrightdnlight\": \"\\u252D\",\n    \"/leftheavyrightuplight\": \"\\u2535\",\n    \"/leftheavyrightvertlight\": \"\\u253D\",\n    \"/leftideographiccircled\": \"\\u32A7\",\n    \"/leftlightrightdnheavy\": \"\\u2532\",\n    \"/leftlightrightupheavy\": \"\\u253A\",\n    \"/leftlightrightvertheavy\": \"\\u254A\",\n    \"/lefttackbelowcmb\": \"\\u0318\",\n    \"/lefttorightembed\": \"\\u202A\",\n    \"/lefttorightisolate\": \"\\u2066\",\n    \"/lefttorightmark\": \"\\u200E\",\n    \"/lefttorightoverride\": \"\\u202D\",\n    \"/leftupheavyrightdnlight\": \"\\u2543\",\n    \"/lemon\": \"\\u1F34B\",\n    \"/lenis\": \"\\u1FBF\",\n    \"/lenisacute\": \"\\u1FCE\",\n    \"/lenisgrave\": \"\\u1FCD\",\n    \"/lenistilde\": \"\\u1FCF\",\n    \"/leo\": \"\\u264C\",\n    \"/leopard\": \"\\u1F406\",\n    \"/less\": \"\\u003C\",\n    \"/lessbutnotequal\": \"\\u2268\",\n    \"/lessbutnotequivalent\": \"\\u22E6\",\n    \"/lessdot\": \"\\u22D6\",\n    \"/lessequal\": \"\\u2264\",\n    \"/lessequalorgreater\": \"\\u22DA\",\n    \"/lessmonospace\": \"\\uFF1C\",\n    \"/lessorequivalent\": \"\\u2272\",\n    \"/lessorgreater\": \"\\u2276\",\n    \"/lessoverequal\": \"\\u2266\",\n    \"/lesssmall\": \"\\uFE64\",\n    \"/levelSlider\": \"\\u1F39A\",\n    \"/lezh\": \"\\u026E\",\n    \"/lfblock\": \"\\u258C\",\n    \"/lhacyr\": \"\\u0515\",\n    \"/lhookretroflex\": \"\\u026D\",\n    \"/libra\": \"\\u264E\",\n    \"/ligaturealeflamed:hb\": \"\\uFB4F\",\n    \"/ligatureoemod\": \"\\uA7F9\",\n    \"/lightCheckMark\": \"\\u1F5F8\",\n    \"/lightRail\": \"\\u1F688\",\n    \"/lightShade\": \"\\u2591\",\n    \"/lightarcdnleft\": \"\\u256E\",\n    \"/lightarcdnright\": \"\\u256D\",\n    \"/lightarcupleft\": \"\\u256F\",\n    \"/lightarcupright\": \"\\u2570\",\n    \"/lightdbldashhorz\": \"\\u254C\",\n    \"/lightdbldashvert\": \"\\u254E\",\n    \"/lightdiagcross\": \"\\u2573\",\n    \"/lightdiagupleftdnright\": \"\\u2572\",\n    \"/lightdiaguprightdnleft\": \"\\u2571\",\n    \"/lightdn\": \"\\u2577\",\n    \"/lightdnhorz\": \"\\u252C\",\n    \"/lightdnleft\": \"\\u2510\",\n    \"/lightdnright\": \"\\u250C\",\n    \"/lighthorz\": \"\\u2500\",\n    \"/lightleft\": \"\\u2574\",\n    \"/lightleftheavyright\": \"\\u257C\",\n    \"/lightning\": \"\\u2607\",\n    \"/lightningMood\": \"\\u1F5F2\",\n    \"/lightningMoodBubble\": \"\\u1F5F1\",\n    \"/lightquaddashhorz\": \"\\u2508\",\n    \"/lightquaddashvert\": \"\\u250A\",\n    \"/lightright\": \"\\u2576\",\n    \"/lighttrpldashhorz\": \"\\u2504\",\n    \"/lighttrpldashvert\": \"\\u2506\",\n    \"/lightup\": \"\\u2575\",\n    \"/lightupheavydn\": \"\\u257D\",\n    \"/lightuphorz\": \"\\u2534\",\n    \"/lightupleft\": \"\\u2518\",\n    \"/lightupright\": \"\\u2514\",\n    \"/lightvert\": \"\\u2502\",\n    \"/lightverthorz\": \"\\u253C\",\n    \"/lightvertleft\": \"\\u2524\",\n    \"/lightvertright\": \"\\u251C\",\n    \"/lineextensionhorizontal\": \"\\u23AF\",\n    \"/lineextensionvertical\": \"\\u23D0\",\n    \"/linemiddledotvertical\": \"\\u237F\",\n    \"/lineseparator\": \"\\u2028\",\n    \"/lingsapada\": \"\\uA9C8\",\n    \"/link\": \"\\u1F517\",\n    \"/linkedPaperclips\": \"\\u1F587\",\n    \"/lips\": \"\\u1F5E2\",\n    \"/lipstick\": \"\\u1F484\",\n    \"/lira\": \"\\u20A4\",\n    \"/litre\": \"\\u2113\",\n    \"/livretournois\": \"\\u20B6\",\n    \"/liwnarmenian\": \"\\u056C\",\n    \"/lj\": \"\\u01C9\",\n    \"/ljecyr\": \"\\u0459\",\n    \"/ljecyrillic\": \"\\u0459\",\n    \"/ljekomicyr\": \"\\u0509\",\n    \"/ll\": \"\\uF6C0\",\n    \"/lladeva\": \"\\u0933\",\n    \"/llagujarati\": \"\\u0AB3\",\n    \"/llinebelow\": \"\\u1E3B\",\n    \"/llladeva\": \"\\u0934\",\n    \"/llvocalicbengali\": \"\\u09E1\",\n    \"/llvocalicdeva\": \"\\u0961\",\n    \"/llvocalicvowelsignbengali\": \"\\u09E3\",\n    \"/llvocalicvowelsigndeva\": \"\\u0963\",\n    \"/llwelsh\": \"\\u1EFB\",\n    \"/lmacrondot\": \"\\u1E39\",\n    \"/lmfullwidth\": \"\\u33D0\",\n    \"/lmiddletilde\": \"\\u026B\",\n    \"/lmonospace\": \"\\uFF4C\",\n    \"/lmsquare\": \"\\u33D0\",\n    \"/lnfullwidth\": \"\\u33D1\",\n    \"/lochulathai\": \"\\u0E2C\",\n    \"/lock\": \"\\u1F512\",\n    \"/lockInkPen\": \"\\u1F50F\",\n    \"/logfullwidth\": \"\\u33D2\",\n    \"/logicaland\": \"\\u2227\",\n    \"/logicalandarray\": \"\\u22C0\",\n    \"/logicalnot\": \"\\u00AC\",\n    \"/logicalnotreversed\": \"\\u2310\",\n    \"/logicalor\": \"\\u2228\",\n    \"/logicalorarray\": \"\\u22C1\",\n    \"/lolingthai\": \"\\u0E25\",\n    \"/lollipop\": \"\\u1F36D\",\n    \"/longdivision\": \"\\u27CC\",\n    \"/longovershortmetrical\": \"\\u23D2\",\n    \"/longovertwoshortsmetrical\": \"\\u23D4\",\n    \"/longs\": \"\\u017F\",\n    \"/longs_t\": \"\\uFB05\",\n    \"/longsdot\": \"\\u1E9B\",\n    \"/longswithdiagonalstroke\": \"\\u1E9C\",\n    \"/longswithhighstroke\": \"\\u1E9D\",\n    \"/longtackleft\": \"\\u27DE\",\n    \"/longtackright\": \"\\u27DD\",\n    \"/losslesssquare\": \"\\u1F1A9\",\n    \"/loudlyCryingFace\": \"\\u1F62D\",\n    \"/loveHotel\": \"\\u1F3E9\",\n    \"/loveLetter\": \"\\u1F48C\",\n    \"/lowBrightness\": \"\\u1F505\",\n    \"/lowasterisk\": \"\\u204E\",\n    \"/lowerFiveEighthsBlock\": \"\\u2585\",\n    \"/lowerHalfBlock\": \"\\u2584\",\n    \"/lowerLeftBallpointPen\": \"\\u1F58A\",\n    \"/lowerLeftCrayon\": \"\\u1F58D\",\n    \"/lowerLeftFountainPen\": \"\\u1F58B\",\n    \"/lowerLeftPaintbrush\": \"\\u1F58C\",\n    \"/lowerLeftPencil\": \"\\u1F589\",\n    \"/lowerOneEighthBlock\": \"\\u2581\",\n    \"/lowerOneQuarterBlock\": \"\\u2582\",\n    \"/lowerRightShadowedWhiteCircle\": \"\\u1F53E\",\n    \"/lowerSevenEighthsBlock\": \"\\u2587\",\n    \"/lowerThreeEighthsBlock\": \"\\u2583\",\n    \"/lowerThreeQuartersBlock\": \"\\u2586\",\n    \"/lowercornerdotright\": \"\\u27D3\",\n    \"/lowerhalfcircle\": \"\\u25E1\",\n    \"/lowerhalfcircleinversewhite\": \"\\u25DB\",\n    \"/lowerquadrantcirculararcleft\": \"\\u25DF\",\n    \"/lowerquadrantcirculararcright\": \"\\u25DE\",\n    \"/lowertriangleleft\": \"\\u25FA\",\n    \"/lowertriangleleftblack\": \"\\u25E3\",\n    \"/lowertriangleright\": \"\\u25FF\",\n    \"/lowertrianglerightblack\": \"\\u25E2\",\n    \"/lowideographiccircled\": \"\\u32A6\",\n    \"/lowlinecenterline\": \"\\uFE4E\",\n    \"/lowlinecmb\": \"\\u0332\",\n    \"/lowlinedashed\": \"\\uFE4D\",\n    \"/lownumeralsign\": \"\\u0375\",\n    \"/lowquotedblprime\": \"\\u301F\",\n    \"/lozenge\": \"\\u25CA\",\n    \"/lozengedividedbyrulehorizontal\": \"\\u27E0\",\n    \"/lozengesquare\": \"\\u2311\",\n    \"/lparen\": \"\\u24A7\",\n    \"/lparenthesized\": \"\\u24A7\",\n    \"/lretroflex\": \"\\u026D\",\n    \"/ls\": \"\\u02AA\",\n    \"/lslash\": \"\\u0142\",\n    \"/lsquare\": \"\\u2113\",\n    \"/lstroke\": \"\\uA749\",\n    \"/lsuperior\": \"\\uF6EE\",\n    \"/lsupmod\": \"\\u02E1\",\n    \"/lt:Alpha\": \"\\u2C6D\",\n    \"/lt:Alphaturned\": \"\\u2C70\",\n    \"/lt:Beta\": \"\\uA7B4\",\n    \"/lt:Chi\": \"\\uA7B3\",\n    \"/lt:Gamma\": \"\\u0194\",\n    \"/lt:Iota\": \"\\u0196\",\n    \"/lt:Omega\": \"\\uA7B6\",\n    \"/lt:Upsilon\": \"\\u01B1\",\n    \"/lt:beta\": \"\\uA7B5\",\n    \"/lt:delta\": \"\\u1E9F\",\n    \"/lt:omega\": \"\\uA7B7\",\n    \"/ltshade\": \"\\u2591\",\n    \"/lttr:bet\": \"\\u2136\",\n    \"/lttr:dalet\": \"\\u2138\",\n    \"/lttr:gimel\": \"\\u2137\",\n    \"/lttr:gscript\": \"\\u210A\",\n    \"/lturned\": \"\\uA781\",\n    \"/ltypeopencircuit\": \"\\u2390\",\n    \"/luhurpada\": \"\\uA9C5\",\n    \"/lum\": \"\\uA772\",\n    \"/lungsipada\": \"\\uA9C9\",\n    \"/luthai\": \"\\u0E26\",\n    \"/lvocalicbengali\": \"\\u098C\",\n    \"/lvocalicdeva\": \"\\u090C\",\n    \"/lvocalicvowelsignbengali\": \"\\u09E2\",\n    \"/lvocalicvowelsigndeva\": \"\\u0962\",\n    \"/lxfullwidth\": \"\\u33D3\",\n    \"/lxsquare\": \"\\u33D3\",\n    \"/lzed\": \"\\u02AB\",\n    \"/m\": \"\\u006D\",\n    \"/m.inferior\": \"\\u2098\",\n    \"/m2fullwidth\": \"\\u33A1\",\n    \"/m3fullwidth\": \"\\u33A5\",\n    \"/mabengali\": \"\\u09AE\",\n    \"/macirclekatakana\": \"\\u32EE\",\n    \"/macron\": \"\\u00AF\",\n    \"/macronbelowcmb\": \"\\u0331\",\n    \"/macroncmb\": \"\\u0304\",\n    \"/macronlowmod\": \"\\u02CD\",\n    \"/macronmod\": \"\\u02C9\",\n    \"/macronmonospace\": \"\\uFFE3\",\n    \"/macute\": \"\\u1E3F\",\n    \"/madda\": \"\\u0653\",\n    \"/maddaabove\": \"\\u06E4\",\n    \"/madeva\": \"\\u092E\",\n    \"/madyapada\": \"\\uA9C4\",\n    \"/mafullwidth\": \"\\u3383\",\n    \"/magujarati\": \"\\u0AAE\",\n    \"/magurmukhi\": \"\\u0A2E\",\n    \"/mahapakhhebrew\": \"\\u05A4\",\n    \"/mahapakhlefthebrew\": \"\\u05A4\",\n    \"/mahhasquare\": \"\\u3345\",\n    \"/mahiragana\": \"\\u307E\",\n    \"/mahpach:hb\": \"\\u05A4\",\n    \"/maichattawalowleftthai\": \"\\uF895\",\n    \"/maichattawalowrightthai\": \"\\uF894\",\n    \"/maichattawathai\": \"\\u0E4B\",\n    \"/maichattawaupperleftthai\": \"\\uF893\",\n    \"/maieklowleftthai\": \"\\uF88C\",\n    \"/maieklowrightthai\": \"\\uF88B\",\n    \"/maiekthai\": \"\\u0E48\",\n    \"/maiekupperleftthai\": \"\\uF88A\",\n    \"/maihanakatleftthai\": \"\\uF884\",\n    \"/maihanakatthai\": \"\\u0E31\",\n    \"/maikurosquare\": \"\\u3343\",\n    \"/mairusquare\": \"\\u3344\",\n    \"/maitaikhuleftthai\": \"\\uF889\",\n    \"/maitaikhuthai\": \"\\u0E47\",\n    \"/maitholowleftthai\": \"\\uF88F\",\n    \"/maitholowrightthai\": \"\\uF88E\",\n    \"/maithothai\": \"\\u0E49\",\n    \"/maithoupperleftthai\": \"\\uF88D\",\n    \"/maitrilowleftthai\": \"\\uF892\",\n    \"/maitrilowrightthai\": \"\\uF891\",\n    \"/maitrithai\": \"\\u0E4A\",\n    \"/maitriupperleftthai\": \"\\uF890\",\n    \"/maiyamokthai\": \"\\u0E46\",\n    \"/makatakana\": \"\\u30DE\",\n    \"/makatakanahalfwidth\": \"\\uFF8F\",\n    \"/male\": \"\\u2642\",\n    \"/malefemale\": \"\\u26A5\",\n    \"/maleideographiccircled\": \"\\u329A\",\n    \"/malestroke\": \"\\u26A6\",\n    \"/malestrokemalefemale\": \"\\u26A7\",\n    \"/man\": \"\\u1F468\",\n    \"/manAndWomanHoldingHands\": \"\\u1F46B\",\n    \"/manDancing\": \"\\u1F57A\",\n    \"/manGuaPiMao\": \"\\u1F472\",\n    \"/manInBusinessSuitLevitating\": \"\\u1F574\",\n    \"/manTurban\": \"\\u1F473\",\n    \"/manat\": \"\\u20BC\",\n    \"/mansShoe\": \"\\u1F45E\",\n    \"/mansyonsquare\": \"\\u3347\",\n    \"/mantelpieceClock\": \"\\u1F570\",\n    \"/mapleLeaf\": \"\\u1F341\",\n    \"/maplighthouse\": \"\\u26EF\",\n    \"/maqaf:hb\": \"\\u05BE\",\n    \"/maqafhebrew\": \"\\u05BE\",\n    \"/marchtelegraph\": \"\\u32C2\",\n    \"/mark\": \"\\u061C\",\n    \"/markerdottedraisedinterpolation\": \"\\u2E07\",\n    \"/markerdottedtransposition\": \"\\u2E08\",\n    \"/markerraisedinterpolation\": \"\\u2E06\",\n    \"/marknoonghunna\": \"\\u0658\",\n    \"/marksChapter\": \"\\u1F545\",\n    \"/marriage\": \"\\u26AD\",\n    \"/mars\": \"\\u2642\",\n    \"/marukusquare\": \"\\u3346\",\n    \"/masoraCircle:hb\": \"\\u05AF\",\n    \"/masoracirclehebrew\": \"\\u05AF\",\n    \"/masquare\": \"\\u3383\",\n    \"/masumark\": \"\\u303C\",\n    \"/math:bowtie\": \"\\u22C8\",\n    \"/math:cuberoot\": \"\\u221B\",\n    \"/math:fourthroot\": \"\\u221C\",\n    \"/maximize\": \"\\u1F5D6\",\n    \"/maytelegraph\": \"\\u32C4\",\n    \"/mbfullwidth\": \"\\u3386\",\n    \"/mbopomofo\": \"\\u3107\",\n    \"/mbsmallfullwidth\": \"\\u33D4\",\n    \"/mbsquare\": \"\\u33D4\",\n    \"/mcircle\": \"\\u24DC\",\n    \"/mcubedsquare\": \"\\u33A5\",\n    \"/mdot\": \"\\u1E41\",\n    \"/mdotaccent\": \"\\u1E41\",\n    \"/mdotbelow\": \"\\u1E43\",\n    \"/measuredangle\": \"\\u2221\",\n    \"/measuredby\": \"\\u225E\",\n    \"/meatOnBone\": \"\\u1F356\",\n    \"/mecirclekatakana\": \"\\u32F1\",\n    \"/medicineideographiccircled\": \"\\u32A9\",\n    \"/mediumShade\": \"\\u2592\",\n    \"/mediumcircleblack\": \"\\u26AB\",\n    \"/mediumcirclewhite\": \"\\u26AA\",\n    \"/mediummathematicalspace\": \"\\u205F\",\n    \"/mediumsmallcirclewhite\": \"\\u26AC\",\n    \"/meem\": \"\\u0645\",\n    \"/meem.fina\": \"\\uFEE2\",\n    \"/meem.init\": \"\\uFEE3\",\n    \"/meem.init_alefmaksura.fina\": \"\\uFC49\",\n    \"/meem.init_hah.fina\": \"\\uFC46\",\n    \"/meem.init_hah.medi\": \"\\uFCCF\",\n    \"/meem.init_hah.medi_jeem.medi\": \"\\uFD89\",\n    \"/meem.init_hah.medi_meem.medi\": \"\\uFD8A\",\n    \"/meem.init_jeem.fina\": \"\\uFC45\",\n    \"/meem.init_jeem.medi\": \"\\uFCCE\",\n    \"/meem.init_jeem.medi_hah.medi\": \"\\uFD8C\",\n    \"/meem.init_jeem.medi_khah.medi\": \"\\uFD92\",\n    \"/meem.init_jeem.medi_meem.medi\": \"\\uFD8D\",\n    \"/meem.init_khah.fina\": \"\\uFC47\",\n    \"/meem.init_khah.medi\": \"\\uFCD0\",\n    \"/meem.init_khah.medi_jeem.medi\": \"\\uFD8E\",\n    \"/meem.init_khah.medi_meem.medi\": \"\\uFD8F\",\n    \"/meem.init_meem.fina\": \"\\uFC48\",\n    \"/meem.init_meem.medi\": \"\\uFCD1\",\n    \"/meem.init_yeh.fina\": \"\\uFC4A\",\n    \"/meem.isol\": \"\\uFEE1\",\n    \"/meem.medi\": \"\\uFEE4\",\n    \"/meem.medi_alef.fina\": \"\\uFC88\",\n    \"/meem.medi_hah.medi_yeh.fina\": \"\\uFD8B\",\n    \"/meem.medi_jeem.medi_yeh.fina\": \"\\uFDC0\",\n    \"/meem.medi_khah.medi_yeh.fina\": \"\\uFDB9\",\n    \"/meem.medi_meem.fina\": \"\\uFC89\",\n    \"/meem.medi_meem.medi_yeh.fina\": \"\\uFDB1\",\n    \"/meemDotAbove\": \"\\u0765\",\n    \"/meemDotBelow\": \"\\u0766\",\n    \"/meemabove\": \"\\u06E2\",\n    \"/meemabove.init\": \"\\u06D8\",\n    \"/meemarabic\": \"\\u0645\",\n    \"/meembelow\": \"\\u06ED\",\n    \"/meemfinalarabic\": \"\\uFEE2\",\n    \"/meeminitialarabic\": \"\\uFEE3\",\n    \"/meemmedialarabic\": \"\\uFEE4\",\n    \"/meemmeeminitialarabic\": \"\\uFCD1\",\n    \"/meemmeemisolatedarabic\": \"\\uFC48\",\n    \"/meetorusquare\": \"\\u334D\",\n    \"/megasquare\": \"\\u334B\",\n    \"/megatonsquare\": \"\\u334C\",\n    \"/mehiragana\": \"\\u3081\",\n    \"/meizierasquare\": \"\\u337E\",\n    \"/mekatakana\": \"\\u30E1\",\n    \"/mekatakanahalfwidth\": \"\\uFF92\",\n    \"/melon\": \"\\u1F348\",\n    \"/mem\": \"\\u05DE\",\n    \"/mem:hb\": \"\\u05DE\",\n    \"/memdagesh\": \"\\uFB3E\",\n    \"/memdageshhebrew\": \"\\uFB3E\",\n    \"/memhebrew\": \"\\u05DE\",\n    \"/memo\": \"\\u1F4DD\",\n    \"/memwithdagesh:hb\": \"\\uFB3E\",\n    \"/menarmenian\": \"\\u0574\",\n    \"/menorahNineBranches\": \"\\u1F54E\",\n    \"/menpostSindhi\": \"\\u06FE\",\n    \"/mens\": \"\\u1F6B9\",\n    \"/mepigraphicinverted\": \"\\uA7FD\",\n    \"/mercha:hb\": \"\\u05A5\",\n    \"/merchaKefulah:hb\": \"\\u05A6\",\n    \"/mercury\": \"\\u263F\",\n    \"/merkhahebrew\": \"\\u05A5\",\n    \"/merkhakefulahebrew\": \"\\u05A6\",\n    \"/merkhakefulalefthebrew\": \"\\u05A6\",\n    \"/merkhalefthebrew\": \"\\u05A5\",\n    \"/metalideographiccircled\": \"\\u328E\",\n    \"/metalideographicparen\": \"\\u322E\",\n    \"/meteg:hb\": \"\\u05BD\",\n    \"/metro\": \"\\u1F687\",\n    \"/mgfullwidth\": \"\\u338E\",\n    \"/mhook\": \"\\u0271\",\n    \"/mhzfullwidth\": \"\\u3392\",\n    \"/mhzsquare\": \"\\u3392\",\n    \"/micirclekatakana\": \"\\u32EF\",\n    \"/microphone\": \"\\u1F3A4\",\n    \"/microscope\": \"\\u1F52C\",\n    \"/middledotkatakanahalfwidth\": \"\\uFF65\",\n    \"/middot\": \"\\u00B7\",\n    \"/mieumacirclekorean\": \"\\u3272\",\n    \"/mieumaparenkorean\": \"\\u3212\",\n    \"/mieumcirclekorean\": \"\\u3264\",\n    \"/mieumkorean\": \"\\u3141\",\n    \"/mieumpansioskorean\": \"\\u3170\",\n    \"/mieumparenkorean\": \"\\u3204\",\n    \"/mieumpieupkorean\": \"\\u316E\",\n    \"/mieumsioskorean\": \"\\u316F\",\n    \"/mihiragana\": \"\\u307F\",\n    \"/mikatakana\": \"\\u30DF\",\n    \"/mikatakanahalfwidth\": \"\\uFF90\",\n    \"/mikuronsquare\": \"\\u3348\",\n    \"/milfullwidth\": \"\\u33D5\",\n    \"/militaryMedal\": \"\\u1F396\",\n    \"/milkyWay\": \"\\u1F30C\",\n    \"/mill\": \"\\u20A5\",\n    \"/millionscmbcyr\": \"\\u0489\",\n    \"/millisecond\": \"\\u2034\",\n    \"/millisecondreversed\": \"\\u2037\",\n    \"/minibus\": \"\\u1F690\",\n    \"/minidisc\": \"\\u1F4BD\",\n    \"/minimize\": \"\\u1F5D5\",\n    \"/minus\": \"\\u2212\",\n    \"/minus.inferior\": \"\\u208B\",\n    \"/minus.superior\": \"\\u207B\",\n    \"/minusbelowcmb\": \"\\u0320\",\n    \"/minuscircle\": \"\\u2296\",\n    \"/minusmod\": \"\\u02D7\",\n    \"/minusplus\": \"\\u2213\",\n    \"/minussignmod\": \"\\u02D7\",\n    \"/minustilde\": \"\\u2242\",\n    \"/minute\": \"\\u2032\",\n    \"/minutereversed\": \"\\u2035\",\n    \"/miribaarusquare\": \"\\u334A\",\n    \"/mirisquare\": \"\\u3349\",\n    \"/misc:baby\": \"\\u1F476\",\n    \"/misc:bell\": \"\\u1F514\",\n    \"/misc:dash\": \"\\u1F4A8\",\n    \"/misc:decimalseparator\": \"\\u2396\",\n    \"/misc:diamondblack\": \"\\u2666\",\n    \"/misc:diamondwhite\": \"\\u2662\",\n    \"/misc:ear\": \"\\u1F442\",\n    \"/misc:om\": \"\\u1F549\",\n    \"/misc:ring\": \"\\u1F48D\",\n    \"/misra\": \"\\u060F\",\n    \"/mlfullwidth\": \"\\u3396\",\n    \"/mlonglegturned\": \"\\u0270\",\n    \"/mlsquare\": \"\\u3396\",\n    \"/mlym:a\": \"\\u0D05\",\n    \"/mlym:aa\": \"\\u0D06\",\n    \"/mlym:aasign\": \"\\u0D3E\",\n    \"/mlym:ai\": \"\\u0D10\",\n    \"/mlym:aisign\": \"\\u0D48\",\n    \"/mlym:anusvarasign\": \"\\u0D02\",\n    \"/mlym:archaicii\": \"\\u0D5F\",\n    \"/mlym:au\": \"\\u0D14\",\n    \"/mlym:aulength\": \"\\u0D57\",\n    \"/mlym:ausign\": \"\\u0D4C\",\n    \"/mlym:avagrahasign\": \"\\u0D3D\",\n    \"/mlym:ba\": \"\\u0D2C\",\n    \"/mlym:bha\": \"\\u0D2D\",\n    \"/mlym:ca\": \"\\u0D1A\",\n    \"/mlym:candrabindusign\": \"\\u0D01\",\n    \"/mlym:cha\": \"\\u0D1B\",\n    \"/mlym:circularviramasign\": \"\\u0D3C\",\n    \"/mlym:combininganusvaraabovesign\": \"\\u0D00\",\n    \"/mlym:da\": \"\\u0D26\",\n    \"/mlym:date\": \"\\u0D79\",\n    \"/mlym:dda\": \"\\u0D21\",\n    \"/mlym:ddha\": \"\\u0D22\",\n    \"/mlym:dha\": \"\\u0D27\",\n    \"/mlym:dotreph\": \"\\u0D4E\",\n    \"/mlym:e\": \"\\u0D0E\",\n    \"/mlym:ee\": \"\\u0D0F\",\n    \"/mlym:eesign\": \"\\u0D47\",\n    \"/mlym:eight\": \"\\u0D6E\",\n    \"/mlym:esign\": \"\\u0D46\",\n    \"/mlym:five\": \"\\u0D6B\",\n    \"/mlym:four\": \"\\u0D6A\",\n    \"/mlym:ga\": \"\\u0D17\",\n    \"/mlym:gha\": \"\\u0D18\",\n    \"/mlym:ha\": \"\\u0D39\",\n    \"/mlym:i\": \"\\u0D07\",\n    \"/mlym:ii\": \"\\u0D08\",\n    \"/mlym:iisign\": \"\\u0D40\",\n    \"/mlym:isign\": \"\\u0D3F\",\n    \"/mlym:ja\": \"\\u0D1C\",\n    \"/mlym:jha\": \"\\u0D1D\",\n    \"/mlym:ka\": \"\\u0D15\",\n    \"/mlym:kchillu\": \"\\u0D7F\",\n    \"/mlym:kha\": \"\\u0D16\",\n    \"/mlym:la\": \"\\u0D32\",\n    \"/mlym:lchillu\": \"\\u0D7D\",\n    \"/mlym:lla\": \"\\u0D33\",\n    \"/mlym:llchillu\": \"\\u0D7E\",\n    \"/mlym:llla\": \"\\u0D34\",\n    \"/mlym:lllchillu\": \"\\u0D56\",\n    \"/mlym:llvocal\": \"\\u0D61\",\n    \"/mlym:llvocalsign\": \"\\u0D63\",\n    \"/mlym:lvocal\": \"\\u0D0C\",\n    \"/mlym:lvocalsign\": \"\\u0D62\",\n    \"/mlym:ma\": \"\\u0D2E\",\n    \"/mlym:mchillu\": \"\\u0D54\",\n    \"/mlym:na\": \"\\u0D28\",\n    \"/mlym:nchillu\": \"\\u0D7B\",\n    \"/mlym:nga\": \"\\u0D19\",\n    \"/mlym:nine\": \"\\u0D6F\",\n    \"/mlym:nna\": \"\\u0D23\",\n    \"/mlym:nnchillu\": \"\\u0D7A\",\n    \"/mlym:nnna\": \"\\u0D29\",\n    \"/mlym:nya\": \"\\u0D1E\",\n    \"/mlym:o\": \"\\u0D12\",\n    \"/mlym:one\": \"\\u0D67\",\n    \"/mlym:oneeighth\": \"\\u0D77\",\n    \"/mlym:onefifth\": \"\\u0D5E\",\n    \"/mlym:onefortieth\": \"\\u0D59\",\n    \"/mlym:onehalf\": \"\\u0D74\",\n    \"/mlym:onehundred\": \"\\u0D71\",\n    \"/mlym:oneone-hundred-and-sixtieth\": \"\\u0D58\",\n    \"/mlym:onequarter\": \"\\u0D73\",\n    \"/mlym:onesixteenth\": \"\\u0D76\",\n    \"/mlym:onetenth\": \"\\u0D5C\",\n    \"/mlym:onethousand\": \"\\u0D72\",\n    \"/mlym:onetwentieth\": \"\\u0D5B\",\n    \"/mlym:oo\": \"\\u0D13\",\n    \"/mlym:oosign\": \"\\u0D4B\",\n    \"/mlym:osign\": \"\\u0D4A\",\n    \"/mlym:pa\": \"\\u0D2A\",\n    \"/mlym:parasign\": \"\\u0D4F\",\n    \"/mlym:pha\": \"\\u0D2B\",\n    \"/mlym:ra\": \"\\u0D30\",\n    \"/mlym:rra\": \"\\u0D31\",\n    \"/mlym:rrchillu\": \"\\u0D7C\",\n    \"/mlym:rrvocal\": \"\\u0D60\",\n    \"/mlym:rrvocalsign\": \"\\u0D44\",\n    \"/mlym:rvocal\": \"\\u0D0B\",\n    \"/mlym:rvocalsign\": \"\\u0D43\",\n    \"/mlym:sa\": \"\\u0D38\",\n    \"/mlym:seven\": \"\\u0D6D\",\n    \"/mlym:sha\": \"\\u0D36\",\n    \"/mlym:six\": \"\\u0D6C\",\n    \"/mlym:ssa\": \"\\u0D37\",\n    \"/mlym:ta\": \"\\u0D24\",\n    \"/mlym:ten\": \"\\u0D70\",\n    \"/mlym:tha\": \"\\u0D25\",\n    \"/mlym:three\": \"\\u0D69\",\n    \"/mlym:threeeightieths\": \"\\u0D5A\",\n    \"/mlym:threequarters\": \"\\u0D75\",\n    \"/mlym:threesixteenths\": \"\\u0D78\",\n    \"/mlym:threetwentieths\": \"\\u0D5D\",\n    \"/mlym:tta\": \"\\u0D1F\",\n    \"/mlym:ttha\": \"\\u0D20\",\n    \"/mlym:ttta\": \"\\u0D3A\",\n    \"/mlym:two\": \"\\u0D68\",\n    \"/mlym:u\": \"\\u0D09\",\n    \"/mlym:usign\": \"\\u0D41\",\n    \"/mlym:uu\": \"\\u0D0A\",\n    \"/mlym:uusign\": \"\\u0D42\",\n    \"/mlym:va\": \"\\u0D35\",\n    \"/mlym:verticalbarviramasign\": \"\\u0D3B\",\n    \"/mlym:viramasign\": \"\\u0D4D\",\n    \"/mlym:visargasign\": \"\\u0D03\",\n    \"/mlym:ya\": \"\\u0D2F\",\n    \"/mlym:ychillu\": \"\\u0D55\",\n    \"/mlym:zero\": \"\\u0D66\",\n    \"/mm2fullwidth\": \"\\u339F\",\n    \"/mm3fullwidth\": \"\\u33A3\",\n    \"/mmcubedsquare\": \"\\u33A3\",\n    \"/mmfullwidth\": \"\\u339C\",\n    \"/mmonospace\": \"\\uFF4D\",\n    \"/mmsquaredsquare\": \"\\u339F\",\n    \"/mobilePhone\": \"\\u1F4F1\",\n    \"/mobilePhoneOff\": \"\\u1F4F4\",\n    \"/mobilePhoneRightwardsArrowAtLeft\": \"\\u1F4F2\",\n    \"/mocirclekatakana\": \"\\u32F2\",\n    \"/models\": \"\\u22A7\",\n    \"/mohiragana\": \"\\u3082\",\n    \"/mohmfullwidth\": \"\\u33C1\",\n    \"/mohmsquare\": \"\\u33C1\",\n    \"/mokatakana\": \"\\u30E2\",\n    \"/mokatakanahalfwidth\": \"\\uFF93\",\n    \"/molfullwidth\": \"\\u33D6\",\n    \"/molsquare\": \"\\u33D6\",\n    \"/momathai\": \"\\u0E21\",\n    \"/moneyBag\": \"\\u1F4B0\",\n    \"/moneyWings\": \"\\u1F4B8\",\n    \"/mong:a\": \"\\u1820\",\n    \"/mong:aaligali\": \"\\u1887\",\n    \"/mong:ahaligali\": \"\\u1897\",\n    \"/mong:ang\": \"\\u1829\",\n    \"/mong:angsibe\": \"\\u1862\",\n    \"/mong:angtodo\": \"\\u184A\",\n    \"/mong:anusvaraonealigali\": \"\\u1880\",\n    \"/mong:ba\": \"\\u182A\",\n    \"/mong:baludaaligali\": \"\\u1885\",\n    \"/mong:baludaaligalithree\": \"\\u1886\",\n    \"/mong:batodo\": \"\\u184B\",\n    \"/mong:bhamanchualigali\": \"\\u18A8\",\n    \"/mong:birga\": \"\\u1800\",\n    \"/mong:caaligali\": \"\\u188B\",\n    \"/mong:camanchualigali\": \"\\u189C\",\n    \"/mong:cha\": \"\\u1834\",\n    \"/mong:chasibe\": \"\\u1871\",\n    \"/mong:chatodo\": \"\\u1852\",\n    \"/mong:chi\": \"\\u1842\",\n    \"/mong:colon\": \"\\u1804\",\n    \"/mong:comma\": \"\\u1802\",\n    \"/mong:commamanchu\": \"\\u1808\",\n    \"/mong:cyamanchualigali\": \"\\u18A3\",\n    \"/mong:da\": \"\\u1833\",\n    \"/mong:daaligali\": \"\\u1891\",\n    \"/mong:dagalgaaligali\": \"\\u18A9\",\n    \"/mong:damarualigali\": \"\\u1882\",\n    \"/mong:dasibe\": \"\\u1869\",\n    \"/mong:datodo\": \"\\u1851\",\n    \"/mong:ddaaligali\": \"\\u188E\",\n    \"/mong:ddhamanchualigali\": \"\\u189F\",\n    \"/mong:dhamanchualigali\": \"\\u18A1\",\n    \"/mong:dzatodo\": \"\\u185C\",\n    \"/mong:e\": \"\\u1821\",\n    \"/mong:ee\": \"\\u1827\",\n    \"/mong:eight\": \"\\u1818\",\n    \"/mong:ellipsis\": \"\\u1801\",\n    \"/mong:esibe\": \"\\u185D\",\n    \"/mong:etodo\": \"\\u1844\",\n    \"/mong:fa\": \"\\u1839\",\n    \"/mong:famanchu\": \"\\u1876\",\n    \"/mong:fasibe\": \"\\u186B\",\n    \"/mong:five\": \"\\u1815\",\n    \"/mong:four\": \"\\u1814\",\n    \"/mong:fourdots\": \"\\u1805\",\n    \"/mong:freevariationselectorone\": \"\\u180B\",\n    \"/mong:freevariationselectorthree\": \"\\u180D\",\n    \"/mong:freevariationselectortwo\": \"\\u180C\",\n    \"/mong:ga\": \"\\u182D\",\n    \"/mong:gaasibe\": \"\\u186C\",\n    \"/mong:gaatodo\": \"\\u1858\",\n    \"/mong:gasibe\": \"\\u1864\",\n    \"/mong:gatodo\": \"\\u184E\",\n    \"/mong:ghamanchualigali\": \"\\u189A\",\n    \"/mong:haa\": \"\\u183E\",\n    \"/mong:haasibe\": \"\\u186D\",\n    \"/mong:haatodo\": \"\\u1859\",\n    \"/mong:hasibe\": \"\\u1865\",\n    \"/mong:i\": \"\\u1822\",\n    \"/mong:ialigali\": \"\\u1888\",\n    \"/mong:imanchu\": \"\\u1873\",\n    \"/mong:isibe\": \"\\u185E\",\n    \"/mong:itodo\": \"\\u1845\",\n    \"/mong:iysibe\": \"\\u185F\",\n    \"/mong:ja\": \"\\u1835\",\n    \"/mong:jasibe\": \"\\u186A\",\n    \"/mong:jatodo\": \"\\u1853\",\n    \"/mong:jhamanchualigali\": \"\\u189D\",\n    \"/mong:jiatodo\": \"\\u185A\",\n    \"/mong:ka\": \"\\u183A\",\n    \"/mong:kaaligali\": \"\\u1889\",\n    \"/mong:kamanchu\": \"\\u1874\",\n    \"/mong:kasibe\": \"\\u1863\",\n    \"/mong:katodo\": \"\\u1857\",\n    \"/mong:kha\": \"\\u183B\",\n    \"/mong:la\": \"\\u182F\",\n    \"/mong:lha\": \"\\u1840\",\n    \"/mong:lhamanchualigali\": \"\\u18AA\",\n    \"/mong:longvowelsigntodo\": \"\\u1843\",\n    \"/mong:ma\": \"\\u182E\",\n    \"/mong:matodo\": \"\\u184F\",\n    \"/mong:na\": \"\\u1828\",\n    \"/mong:ngaaligali\": \"\\u188A\",\n    \"/mong:ngamanchualigali\": \"\\u189B\",\n    \"/mong:niatodo\": \"\\u185B\",\n    \"/mong:nine\": \"\\u1819\",\n    \"/mong:nirugu\": \"\\u180A\",\n    \"/mong:nnaaligali\": \"\\u188F\",\n    \"/mong:o\": \"\\u1823\",\n    \"/mong:oe\": \"\\u1825\",\n    \"/mong:oetodo\": \"\\u1848\",\n    \"/mong:one\": \"\\u1811\",\n    \"/mong:otodo\": \"\\u1846\",\n    \"/mong:pa\": \"\\u182B\",\n    \"/mong:paaligali\": \"\\u1892\",\n    \"/mong:pasibe\": \"\\u1866\",\n    \"/mong:patodo\": \"\\u184C\",\n    \"/mong:period\": \"\\u1803\",\n    \"/mong:periodmanchu\": \"\\u1809\",\n    \"/mong:phaaligali\": \"\\u1893\",\n    \"/mong:qa\": \"\\u182C\",\n    \"/mong:qatodo\": \"\\u184D\",\n    \"/mong:ra\": \"\\u1837\",\n    \"/mong:raasibe\": \"\\u1870\",\n    \"/mong:ramanchu\": \"\\u1875\",\n    \"/mong:sa\": \"\\u1830\",\n    \"/mong:seven\": \"\\u1817\",\n    \"/mong:sha\": \"\\u1831\",\n    \"/mong:shasibe\": \"\\u1867\",\n    \"/mong:six\": \"\\u1816\",\n    \"/mong:softhyphentodo\": \"\\u1806\",\n    \"/mong:ssaaligali\": \"\\u1894\",\n    \"/mong:ssamanchualigali\": \"\\u18A2\",\n    \"/mong:syllableboundarymarkersibe\": \"\\u1807\",\n    \"/mong:ta\": \"\\u1832\",\n    \"/mong:taaligali\": \"\\u1890\",\n    \"/mong:tamanchualigali\": \"\\u18A0\",\n    \"/mong:tasibe\": \"\\u1868\",\n    \"/mong:tatodo\": \"\\u1850\",\n    \"/mong:tatodoaligali\": \"\\u1898\",\n    \"/mong:three\": \"\\u1813\",\n    \"/mong:tsa\": \"\\u183C\",\n    \"/mong:tsasibe\": \"\\u186E\",\n    \"/mong:tsatodo\": \"\\u1854\",\n    \"/mong:ttaaligali\": \"\\u188C\",\n    \"/mong:ttamanchualigali\": \"\\u189E\",\n    \"/mong:tthaaligali\": \"\\u188D\",\n    \"/mong:two\": \"\\u1812\",\n    \"/mong:u\": \"\\u1824\",\n    \"/mong:ualigalihalf\": \"\\u18A6\",\n    \"/mong:ubadamaaligali\": \"\\u1883\",\n    \"/mong:ubadamaaligaliinverted\": \"\\u1884\",\n    \"/mong:ue\": \"\\u1826\",\n    \"/mong:uesibe\": \"\\u1860\",\n    \"/mong:uetodo\": \"\\u1849\",\n    \"/mong:usibe\": \"\\u1861\",\n    \"/mong:utodo\": \"\\u1847\",\n    \"/mong:visargaonealigali\": \"\\u1881\",\n    \"/mong:vowelseparator\": \"\\u180E\",\n    \"/mong:wa\": \"\\u1838\",\n    \"/mong:watodo\": \"\\u1856\",\n    \"/mong:ya\": \"\\u1836\",\n    \"/mong:yaaligalihalf\": \"\\u18A7\",\n    \"/mong:yatodo\": \"\\u1855\",\n    \"/mong:za\": \"\\u183D\",\n    \"/mong:zaaligali\": \"\\u1896\",\n    \"/mong:zamanchualigali\": \"\\u18A5\",\n    \"/mong:zasibe\": \"\\u186F\",\n    \"/mong:zero\": \"\\u1810\",\n    \"/mong:zhaaligali\": \"\\u1895\",\n    \"/mong:zhamanchu\": \"\\u1877\",\n    \"/mong:zhamanchualigali\": \"\\u18A4\",\n    \"/mong:zhasibe\": \"\\u1872\",\n    \"/mong:zhatodoaligali\": \"\\u1899\",\n    \"/mong:zhi\": \"\\u1841\",\n    \"/mong:zra\": \"\\u183F\",\n    \"/monkey\": \"\\u1F412\",\n    \"/monkeyFace\": \"\\u1F435\",\n    \"/monogramyang\": \"\\u268A\",\n    \"/monogramyin\": \"\\u268B\",\n    \"/monorail\": \"\\u1F69D\",\n    \"/monostable\": \"\\u238D\",\n    \"/moodBubble\": \"\\u1F5F0\",\n    \"/moonViewingCeremony\": \"\\u1F391\",\n    \"/moonideographiccircled\": \"\\u328A\",\n    \"/moonideographicparen\": \"\\u322A\",\n    \"/moonlilithblack\": \"\\u26B8\",\n    \"/mosque\": \"\\u1F54C\",\n    \"/motorBoat\": \"\\u1F6E5\",\n    \"/motorScooter\": \"\\u1F6F5\",\n    \"/motorway\": \"\\u1F6E3\",\n    \"/mountFuji\": \"\\u1F5FB\",\n    \"/mountain\": \"\\u26F0\",\n    \"/mountainBicyclist\": \"\\u1F6B5\",\n    \"/mountainCableway\": \"\\u1F6A0\",\n    \"/mountainRailway\": \"\\u1F69E\",\n    \"/mouse\": \"\\u1F401\",\n    \"/mouseFace\": \"\\u1F42D\",\n    \"/mouth\": \"\\u1F444\",\n    \"/movers2fullwidth\": \"\\u33A8\",\n    \"/moversfullwidth\": \"\\u33A7\",\n    \"/moverssquare\": \"\\u33A7\",\n    \"/moverssquaredsquare\": \"\\u33A8\",\n    \"/movieCamera\": \"\\u1F3A5\",\n    \"/moyai\": \"\\u1F5FF\",\n    \"/mpafullwidth\": \"\\u33AB\",\n    \"/mparen\": \"\\u24A8\",\n    \"/mparenthesized\": \"\\u24A8\",\n    \"/mpasquare\": \"\\u33AB\",\n    \"/msfullwidth\": \"\\u33B3\",\n    \"/mssquare\": \"\\u33B3\",\n    \"/msuperior\": \"\\uF6EF\",\n    \"/mturned\": \"\\u026F\",\n    \"/mu\": \"\\u00B5\",\n    \"/mu.math\": \"\\u00B5\",\n    \"/mu1\": \"\\u00B5\",\n    \"/muafullwidth\": \"\\u3382\",\n    \"/muasquare\": \"\\u3382\",\n    \"/muchgreater\": \"\\u226B\",\n    \"/muchless\": \"\\u226A\",\n    \"/mucirclekatakana\": \"\\u32F0\",\n    \"/muffullwidth\": \"\\u338C\",\n    \"/mufsquare\": \"\\u338C\",\n    \"/mugfullwidth\": \"\\u338D\",\n    \"/mugreek\": \"\\u03BC\",\n    \"/mugsquare\": \"\\u338D\",\n    \"/muhiragana\": \"\\u3080\",\n    \"/mukatakana\": \"\\u30E0\",\n    \"/mukatakanahalfwidth\": \"\\uFF91\",\n    \"/mulfullwidth\": \"\\u3395\",\n    \"/mulsquare\": \"\\u3395\",\n    \"/multimap\": \"\\u22B8\",\n    \"/multimapleft\": \"\\u27DC\",\n    \"/multipleMusicalNotes\": \"\\u1F3B6\",\n    \"/multiply\": \"\\u00D7\",\n    \"/multiset\": \"\\u228C\",\n    \"/multisetmultiplication\": \"\\u228D\",\n    \"/multisetunion\": \"\\u228E\",\n    \"/mum\": \"\\uA773\",\n    \"/mumfullwidth\": \"\\u339B\",\n    \"/mumsquare\": \"\\u339B\",\n    \"/munach:hb\": \"\\u05A3\",\n    \"/munahhebrew\": \"\\u05A3\",\n    \"/munahlefthebrew\": \"\\u05A3\",\n    \"/musfullwidth\": \"\\u33B2\",\n    \"/mushroom\": \"\\u1F344\",\n    \"/musicalKeyboard\": \"\\u1F3B9\",\n    \"/musicalKeyboardJacks\": \"\\u1F398\",\n    \"/musicalNote\": \"\\u1F3B5\",\n    \"/musicalScore\": \"\\u1F3BC\",\n    \"/musicalnote\": \"\\u266A\",\n    \"/musicalnotedbl\": \"\\u266B\",\n    \"/musicflat\": \"\\u266D\",\n    \"/musicflatsign\": \"\\u266D\",\n    \"/musicnatural\": \"\\u266E\",\n    \"/musicsharp\": \"\\u266F\",\n    \"/musicsharpsign\": \"\\u266F\",\n    \"/mussquare\": \"\\u33B2\",\n    \"/muvfullwidth\": \"\\u33B6\",\n    \"/muvsquare\": \"\\u33B6\",\n    \"/muwfullwidth\": \"\\u33BC\",\n    \"/muwsquare\": \"\\u33BC\",\n    \"/mvfullwidth\": \"\\u33B7\",\n    \"/mvmegafullwidth\": \"\\u33B9\",\n    \"/mvmegasquare\": \"\\u33B9\",\n    \"/mvsquare\": \"\\u33B7\",\n    \"/mwfullwidth\": \"\\u33BD\",\n    \"/mwmegafullwidth\": \"\\u33BF\",\n    \"/mwmegasquare\": \"\\u33BF\",\n    \"/mwsquare\": \"\\u33BD\",\n    \"/n\": \"\\u006E\",\n    \"/n.inferior\": \"\\u2099\",\n    \"/n.superior\": \"\\u207F\",\n    \"/nabengali\": \"\\u09A8\",\n    \"/nabla\": \"\\u2207\",\n    \"/nacirclekatakana\": \"\\u32E4\",\n    \"/nacute\": \"\\u0144\",\n    \"/nadeva\": \"\\u0928\",\n    \"/nafullwidth\": \"\\u3381\",\n    \"/nagujarati\": \"\\u0AA8\",\n    \"/nagurmukhi\": \"\\u0A28\",\n    \"/nahiragana\": \"\\u306A\",\n    \"/nailPolish\": \"\\u1F485\",\n    \"/naira\": \"\\u20A6\",\n    \"/nakatakana\": \"\\u30CA\",\n    \"/nakatakanahalfwidth\": \"\\uFF85\",\n    \"/nameBadge\": \"\\u1F4DB\",\n    \"/nameideographiccircled\": \"\\u3294\",\n    \"/nameideographicparen\": \"\\u3234\",\n    \"/namurda\": \"\\uA99F\",\n    \"/nand\": \"\\u22BC\",\n    \"/nanosquare\": \"\\u3328\",\n    \"/napostrophe\": \"\\u0149\",\n    \"/narrownobreakspace\": \"\\u202F\",\n    \"/nasquare\": \"\\u3381\",\n    \"/nationalPark\": \"\\u1F3DE\",\n    \"/nationaldigitshapes\": \"\\u206E\",\n    \"/nbopomofo\": \"\\u310B\",\n    \"/nbspace\": \"\\u00A0\",\n    \"/ncaron\": \"\\u0148\",\n    \"/ncedilla\": \"\\u0146\",\n    \"/ncircle\": \"\\u24DD\",\n    \"/ncircumflexbelow\": \"\\u1E4B\",\n    \"/ncommaaccent\": \"\\u0146\",\n    \"/ncurl\": \"\\u0235\",\n    \"/ndescender\": \"\\uA791\",\n    \"/ndot\": \"\\u1E45\",\n    \"/ndotaccent\": \"\\u1E45\",\n    \"/ndotbelow\": \"\\u1E47\",\n    \"/necirclekatakana\": \"\\u32E7\",\n    \"/necktie\": \"\\u1F454\",\n    \"/negatedturnstiledblverticalbarright\": \"\\u22AF\",\n    \"/nehiragana\": \"\\u306D\",\n    \"/neirapproximatelynoractuallyequal\": \"\\u2247\",\n    \"/neirasersetnorequalup\": \"\\u2289\",\n    \"/neirasubsetnorequal\": \"\\u2288\",\n    \"/neirgreaternorequal\": \"\\u2271\",\n    \"/neirgreaternorequivalent\": \"\\u2275\",\n    \"/neirgreaternorless\": \"\\u2279\",\n    \"/neirlessnorequal\": \"\\u2270\",\n    \"/neirlessnorequivalent\": \"\\u2274\",\n    \"/neirlessnorgreater\": \"\\u2278\",\n    \"/nekatakana\": \"\\u30CD\",\n    \"/nekatakanahalfwidth\": \"\\uFF88\",\n    \"/neptune\": \"\\u2646\",\n    \"/neuter\": \"\\u26B2\",\n    \"/neutralFace\": \"\\u1F610\",\n    \"/newMoon\": \"\\u1F311\",\n    \"/newMoonFace\": \"\\u1F31A\",\n    \"/newsheqel\": \"\\u20AA\",\n    \"/newsheqelsign\": \"\\u20AA\",\n    \"/newspaper\": \"\\u1F4F0\",\n    \"/newsquare\": \"\\u1F195\",\n    \"/nextpage\": \"\\u2398\",\n    \"/nffullwidth\": \"\\u338B\",\n    \"/nfsquare\": \"\\u338B\",\n    \"/ng.fina\": \"\\uFBD4\",\n    \"/ng.init\": \"\\uFBD5\",\n    \"/ng.isol\": \"\\uFBD3\",\n    \"/ng.medi\": \"\\uFBD6\",\n    \"/ngabengali\": \"\\u0999\",\n    \"/ngadeva\": \"\\u0919\",\n    \"/ngagujarati\": \"\\u0A99\",\n    \"/ngagurmukhi\": \"\\u0A19\",\n    \"/ngalelet\": \"\\uA98A\",\n    \"/ngaleletraswadi\": \"\\uA98B\",\n    \"/ngoeh\": \"\\u06B1\",\n    \"/ngoeh.fina\": \"\\uFB9B\",\n    \"/ngoeh.init\": \"\\uFB9C\",\n    \"/ngoeh.isol\": \"\\uFB9A\",\n    \"/ngoeh.medi\": \"\\uFB9D\",\n    \"/ngonguthai\": \"\\u0E07\",\n    \"/ngrave\": \"\\u01F9\",\n    \"/ngsquare\": \"\\u1F196\",\n    \"/nhiragana\": \"\\u3093\",\n    \"/nhookleft\": \"\\u0272\",\n    \"/nhookretroflex\": \"\\u0273\",\n    \"/nicirclekatakana\": \"\\u32E5\",\n    \"/nieunacirclekorean\": \"\\u326F\",\n    \"/nieunaparenkorean\": \"\\u320F\",\n    \"/nieuncieuckorean\": \"\\u3135\",\n    \"/nieuncirclekorean\": \"\\u3261\",\n    \"/nieunhieuhkorean\": \"\\u3136\",\n    \"/nieunkorean\": \"\\u3134\",\n    \"/nieunpansioskorean\": \"\\u3168\",\n    \"/nieunparenkorean\": \"\\u3201\",\n    \"/nieunsioskorean\": \"\\u3167\",\n    \"/nieuntikeutkorean\": \"\\u3166\",\n    \"/nightStars\": \"\\u1F303\",\n    \"/nightideographiccircled\": \"\\u32B0\",\n    \"/nihiragana\": \"\\u306B\",\n    \"/nikatakana\": \"\\u30CB\",\n    \"/nikatakanahalfwidth\": \"\\uFF86\",\n    \"/nikhahitleftthai\": \"\\uF899\",\n    \"/nikhahitthai\": \"\\u0E4D\",\n    \"/nine\": \"\\u0039\",\n    \"/nine.inferior\": \"\\u2089\",\n    \"/nine.roman\": \"\\u2168\",\n    \"/nine.romansmall\": \"\\u2178\",\n    \"/nine.superior\": \"\\u2079\",\n    \"/ninearabic\": \"\\u0669\",\n    \"/ninebengali\": \"\\u09EF\",\n    \"/ninecircle\": \"\\u2468\",\n    \"/ninecircledbl\": \"\\u24FD\",\n    \"/ninecircleinversesansserif\": \"\\u2792\",\n    \"/ninecomma\": \"\\u1F10A\",\n    \"/ninedeva\": \"\\u096F\",\n    \"/ninefar\": \"\\u06F9\",\n    \"/ninegujarati\": \"\\u0AEF\",\n    \"/ninegurmukhi\": \"\\u0A6F\",\n    \"/ninehackarabic\": \"\\u0669\",\n    \"/ninehangzhou\": \"\\u3029\",\n    \"/nineideographiccircled\": \"\\u3288\",\n    \"/nineideographicparen\": \"\\u3228\",\n    \"/nineinferior\": \"\\u2089\",\n    \"/ninemonospace\": \"\\uFF19\",\n    \"/nineoldstyle\": \"\\uF739\",\n    \"/nineparen\": \"\\u247C\",\n    \"/nineparenthesized\": \"\\u247C\",\n    \"/nineperiod\": \"\\u2490\",\n    \"/ninepersian\": \"\\u06F9\",\n    \"/nineroman\": \"\\u2178\",\n    \"/ninesuperior\": \"\\u2079\",\n    \"/nineteencircle\": \"\\u2472\",\n    \"/nineteencircleblack\": \"\\u24F3\",\n    \"/nineteenparen\": \"\\u2486\",\n    \"/nineteenparenthesized\": \"\\u2486\",\n    \"/nineteenperiod\": \"\\u249A\",\n    \"/ninethai\": \"\\u0E59\",\n    \"/nj\": \"\\u01CC\",\n    \"/njecyr\": \"\\u045A\",\n    \"/njecyrillic\": \"\\u045A\",\n    \"/njekomicyr\": \"\\u050B\",\n    \"/nkatakana\": \"\\u30F3\",\n    \"/nkatakanahalfwidth\": \"\\uFF9D\",\n    \"/nlegrightlong\": \"\\u019E\",\n    \"/nlinebelow\": \"\\u1E49\",\n    \"/nlongrightleg\": \"\\u019E\",\n    \"/nmbr:oneeighth\": \"\\u215B\",\n    \"/nmbr:onefifth\": \"\\u2155\",\n    \"/nmbr:onetenth\": \"\\u2152\",\n    \"/nmfullwidth\": \"\\u339A\",\n    \"/nmonospace\": \"\\uFF4E\",\n    \"/nmsquare\": \"\\u339A\",\n    \"/nnabengali\": \"\\u09A3\",\n    \"/nnadeva\": \"\\u0923\",\n    \"/nnagujarati\": \"\\u0AA3\",\n    \"/nnagurmukhi\": \"\\u0A23\",\n    \"/nnnadeva\": \"\\u0929\",\n    \"/noBicycles\": \"\\u1F6B3\",\n    \"/noEntrySign\": \"\\u1F6AB\",\n    \"/noMobilePhones\": \"\\u1F4F5\",\n    \"/noOneUnderEighteen\": \"\\u1F51E\",\n    \"/noPedestrians\": \"\\u1F6B7\",\n    \"/noPiracy\": \"\\u1F572\",\n    \"/noSmoking\": \"\\u1F6AD\",\n    \"/nobliquestroke\": \"\\uA7A5\",\n    \"/nocirclekatakana\": \"\\u32E8\",\n    \"/nodeascending\": \"\\u260A\",\n    \"/nodedescending\": \"\\u260B\",\n    \"/noentry\": \"\\u26D4\",\n    \"/nohiragana\": \"\\u306E\",\n    \"/nokatakana\": \"\\u30CE\",\n    \"/nokatakanahalfwidth\": \"\\uFF89\",\n    \"/nominaldigitshapes\": \"\\u206F\",\n    \"/nonPotableWater\": \"\\u1F6B1\",\n    \"/nonbreakinghyphen\": \"\\u2011\",\n    \"/nonbreakingspace\": \"\\u00A0\",\n    \"/nonenthai\": \"\\u0E13\",\n    \"/nonuthai\": \"\\u0E19\",\n    \"/noon\": \"\\u0646\",\n    \"/noon.fina\": \"\\uFEE6\",\n    \"/noon.init\": \"\\uFEE7\",\n    \"/noon.init_alefmaksura.fina\": \"\\uFC4F\",\n    \"/noon.init_hah.fina\": \"\\uFC4C\",\n    \"/noon.init_hah.medi\": \"\\uFCD3\",\n    \"/noon.init_hah.medi_meem.medi\": \"\\uFD95\",\n    \"/noon.init_heh.medi\": \"\\uFCD6\",\n    \"/noon.init_jeem.fina\": \"\\uFC4B\",\n    \"/noon.init_jeem.medi\": \"\\uFCD2\",\n    \"/noon.init_jeem.medi_hah.medi\": \"\\uFDB8\",\n    \"/noon.init_jeem.medi_meem.medi\": \"\\uFD98\",\n    \"/noon.init_khah.fina\": \"\\uFC4D\",\n    \"/noon.init_khah.medi\": \"\\uFCD4\",\n    \"/noon.init_meem.fina\": \"\\uFC4E\",\n    \"/noon.init_meem.medi\": \"\\uFCD5\",\n    \"/noon.init_yeh.fina\": \"\\uFC50\",\n    \"/noon.isol\": \"\\uFEE5\",\n    \"/noon.medi\": \"\\uFEE8\",\n    \"/noon.medi_alefmaksura.fina\": \"\\uFC8E\",\n    \"/noon.medi_hah.medi_alefmaksura.fina\": \"\\uFD96\",\n    \"/noon.medi_hah.medi_yeh.fina\": \"\\uFDB3\",\n    \"/noon.medi_heh.medi\": \"\\uFCEF\",\n    \"/noon.medi_jeem.medi_alefmaksura.fina\": \"\\uFD99\",\n    \"/noon.medi_jeem.medi_hah.fina\": \"\\uFDBD\",\n    \"/noon.medi_jeem.medi_meem.fina\": \"\\uFD97\",\n    \"/noon.medi_jeem.medi_yeh.fina\": \"\\uFDC7\",\n    \"/noon.medi_meem.fina\": \"\\uFC8C\",\n    \"/noon.medi_meem.medi\": \"\\uFCEE\",\n    \"/noon.medi_meem.medi_alefmaksura.fina\": \"\\uFD9B\",\n    \"/noon.medi_meem.medi_yeh.fina\": \"\\uFD9A\",\n    \"/noon.medi_noon.fina\": \"\\uFC8D\",\n    \"/noon.medi_reh.fina\": \"\\uFC8A\",\n    \"/noon.medi_yeh.fina\": \"\\uFC8F\",\n    \"/noon.medi_zain.fina\": \"\\uFC8B\",\n    \"/noonSmallTah\": \"\\u0768\",\n    \"/noonSmallV\": \"\\u0769\",\n    \"/noonTwoDotsBelow\": \"\\u0767\",\n    \"/noonabove\": \"\\u06E8\",\n    \"/noonarabic\": \"\\u0646\",\n    \"/noondotbelow\": \"\\u06B9\",\n    \"/noonfinalarabic\": \"\\uFEE6\",\n    \"/noonghunna\": \"\\u06BA\",\n    \"/noonghunna.fina\": \"\\uFB9F\",\n    \"/noonghunna.isol\": \"\\uFB9E\",\n    \"/noonghunnaarabic\": \"\\u06BA\",\n    \"/noonghunnafinalarabic\": \"\\uFB9F\",\n    \"/noonhehinitialarabic\": \"\\uFEE7\",\n    \"/nooninitialarabic\": \"\\uFEE7\",\n    \"/noonjeeminitialarabic\": \"\\uFCD2\",\n    \"/noonjeemisolatedarabic\": \"\\uFC4B\",\n    \"/noonmedialarabic\": \"\\uFEE8\",\n    \"/noonmeeminitialarabic\": \"\\uFCD5\",\n    \"/noonmeemisolatedarabic\": \"\\uFC4E\",\n    \"/noonnoonfinalarabic\": \"\\uFC8D\",\n    \"/noonring\": \"\\u06BC\",\n    \"/noonthreedotsabove\": \"\\u06BD\",\n    \"/nor\": \"\\u22BD\",\n    \"/nordicmark\": \"\\u20BB\",\n    \"/normalfacrsemidirectproductleft\": \"\\u22C9\",\n    \"/normalfacrsemidirectproductright\": \"\\u22CA\",\n    \"/normalsubgroorequalup\": \"\\u22B4\",\n    \"/normalsubgroup\": \"\\u22B2\",\n    \"/northeastPointingAirplane\": \"\\u1F6EA\",\n    \"/nose\": \"\\u1F443\",\n    \"/notalmostequal\": \"\\u2249\",\n    \"/notasersetup\": \"\\u2285\",\n    \"/notasympticallyequal\": \"\\u2244\",\n    \"/notcheckmark\": \"\\u237B\",\n    \"/notchedLeftSemicircleThreeDots\": \"\\u1F543\",\n    \"/notchedRightSemicircleThreeDots\": \"\\u1F544\",\n    \"/notcontains\": \"\\u220C\",\n    \"/note\": \"\\u1F5C8\",\n    \"/notePad\": \"\\u1F5CA\",\n    \"/notePage\": \"\\u1F5C9\",\n    \"/notebook\": \"\\u1F4D3\",\n    \"/notebookDecorativeCover\": \"\\u1F4D4\",\n    \"/notelement\": \"\\u2209\",\n    \"/notelementof\": \"\\u2209\",\n    \"/notequal\": \"\\u2260\",\n    \"/notequivalent\": \"\\u226D\",\n    \"/notexistential\": \"\\u2204\",\n    \"/notgreater\": \"\\u226F\",\n    \"/notgreaternorequal\": \"\\u2271\",\n    \"/notgreaternorless\": \"\\u2279\",\n    \"/notidentical\": \"\\u2262\",\n    \"/notless\": \"\\u226E\",\n    \"/notlessnorequal\": \"\\u2270\",\n    \"/notnormalsubgroorequalup\": \"\\u22EC\",\n    \"/notnormalsubgroup\": \"\\u22EA\",\n    \"/notparallel\": \"\\u2226\",\n    \"/notprecedes\": \"\\u2280\",\n    \"/notsignturned\": \"\\u2319\",\n    \"/notsquareimageorequal\": \"\\u22E2\",\n    \"/notsquareoriginalorequal\": \"\\u22E3\",\n    \"/notsubset\": \"\\u2284\",\n    \"/notsucceeds\": \"\\u2281\",\n    \"/notsuperset\": \"\\u2285\",\n    \"/nottilde\": \"\\u2241\",\n    \"/nottosquare\": \"\\u3329\",\n    \"/nottrue\": \"\\u22AD\",\n    \"/novembertelegraph\": \"\\u32CA\",\n    \"/nowarmenian\": \"\\u0576\",\n    \"/nparen\": \"\\u24A9\",\n    \"/nparenthesized\": \"\\u24A9\",\n    \"/nretroflex\": \"\\u0273\",\n    \"/nsfullwidth\": \"\\u33B1\",\n    \"/nssquare\": \"\\u33B1\",\n    \"/nsuperior\": \"\\u207F\",\n    \"/ntilde\": \"\\u00F1\",\n    \"/nu\": \"\\u03BD\",\n    \"/nucirclekatakana\": \"\\u32E6\",\n    \"/nuhiragana\": \"\\u306C\",\n    \"/nukatakana\": \"\\u30CC\",\n    \"/nukatakanahalfwidth\": \"\\uFF87\",\n    \"/nuktabengali\": \"\\u09BC\",\n    \"/nuktadeva\": \"\\u093C\",\n    \"/nuktagujarati\": \"\\u0ABC\",\n    \"/nuktagurmukhi\": \"\\u0A3C\",\n    \"/num\": \"\\uA774\",\n    \"/numbermarkabove\": \"\\u0605\",\n    \"/numbersign\": \"\\u0023\",\n    \"/numbersignmonospace\": \"\\uFF03\",\n    \"/numbersignsmall\": \"\\uFE5F\",\n    \"/numeralsign\": \"\\u0374\",\n    \"/numeralsigngreek\": \"\\u0374\",\n    \"/numeralsignlowergreek\": \"\\u0375\",\n    \"/numero\": \"\\u2116\",\n    \"/nun\": \"\\u05E0\",\n    \"/nun:hb\": \"\\u05E0\",\n    \"/nunHafukha:hb\": \"\\u05C6\",\n    \"/nundagesh\": \"\\uFB40\",\n    \"/nundageshhebrew\": \"\\uFB40\",\n    \"/nunhebrew\": \"\\u05E0\",\n    \"/nunwithdagesh:hb\": \"\\uFB40\",\n    \"/nutAndBolt\": \"\\u1F529\",\n    \"/nvfullwidth\": \"\\u33B5\",\n    \"/nvsquare\": \"\\u33B5\",\n    \"/nwfullwidth\": \"\\u33BB\",\n    \"/nwsquare\": \"\\u33BB\",\n    \"/nyabengali\": \"\\u099E\",\n    \"/nyadeva\": \"\\u091E\",\n    \"/nyagujarati\": \"\\u0A9E\",\n    \"/nyagurmukhi\": \"\\u0A1E\",\n    \"/nyamurda\": \"\\uA998\",\n    \"/nyeh\": \"\\u0683\",\n    \"/nyeh.fina\": \"\\uFB77\",\n    \"/nyeh.init\": \"\\uFB78\",\n    \"/nyeh.isol\": \"\\uFB76\",\n    \"/nyeh.medi\": \"\\uFB79\",\n    \"/o\": \"\\u006F\",\n    \"/o.inferior\": \"\\u2092\",\n    \"/oacute\": \"\\u00F3\",\n    \"/oangthai\": \"\\u0E2D\",\n    \"/obarcyr\": \"\\u04E9\",\n    \"/obardieresiscyr\": \"\\u04EB\",\n    \"/obarred\": \"\\u0275\",\n    \"/obarredcyrillic\": \"\\u04E9\",\n    \"/obarreddieresiscyrillic\": \"\\u04EB\",\n    \"/obelosdotted\": \"\\u2E13\",\n    \"/obengali\": \"\\u0993\",\n    \"/obopomofo\": \"\\u311B\",\n    \"/obreve\": \"\\u014F\",\n    \"/observereye\": \"\\u23FF\",\n    \"/ocandradeva\": \"\\u0911\",\n    \"/ocandragujarati\": \"\\u0A91\",\n    \"/ocandravowelsigndeva\": \"\\u0949\",\n    \"/ocandravowelsigngujarati\": \"\\u0AC9\",\n    \"/ocaron\": \"\\u01D2\",\n    \"/ocircle\": \"\\u24DE\",\n    \"/ocirclekatakana\": \"\\u32D4\",\n    \"/ocircumflex\": \"\\u00F4\",\n    \"/ocircumflexacute\": \"\\u1ED1\",\n    \"/ocircumflexdotbelow\": \"\\u1ED9\",\n    \"/ocircumflexgrave\": \"\\u1ED3\",\n    \"/ocircumflexhoi\": \"\\u1ED5\",\n    \"/ocircumflexhookabove\": \"\\u1ED5\",\n    \"/ocircumflextilde\": \"\\u1ED7\",\n    \"/ocr:bowtie\": \"\\u2445\",\n    \"/ocr:dash\": \"\\u2448\",\n    \"/octagonalSign\": \"\\u1F6D1\",\n    \"/octobertelegraph\": \"\\u32C9\",\n    \"/octopus\": \"\\u1F419\",\n    \"/ocyr\": \"\\u043E\",\n    \"/ocyrillic\": \"\\u043E\",\n    \"/odblacute\": \"\\u0151\",\n    \"/odblgrave\": \"\\u020D\",\n    \"/oden\": \"\\u1F362\",\n    \"/odeva\": \"\\u0913\",\n    \"/odieresis\": \"\\u00F6\",\n    \"/odieresiscyr\": \"\\u04E7\",\n    \"/odieresiscyrillic\": \"\\u04E7\",\n    \"/odieresismacron\": \"\\u022B\",\n    \"/odot\": \"\\u022F\",\n    \"/odotbelow\": \"\\u1ECD\",\n    \"/odotmacron\": \"\\u0231\",\n    \"/oe\": \"\\u0153\",\n    \"/oe.fina\": \"\\uFBDA\",\n    \"/oe.isol\": \"\\uFBD9\",\n    \"/oekirghiz\": \"\\u06C5\",\n    \"/oekirghiz.fina\": \"\\uFBE1\",\n    \"/oekirghiz.isol\": \"\\uFBE0\",\n    \"/oekorean\": \"\\u315A\",\n    \"/officeBuilding\": \"\\u1F3E2\",\n    \"/ogonek\": \"\\u02DB\",\n    \"/ogonekcmb\": \"\\u0328\",\n    \"/ograve\": \"\\u00F2\",\n    \"/ogravedbl\": \"\\u020D\",\n    \"/ogujarati\": \"\\u0A93\",\n    \"/oharmenian\": \"\\u0585\",\n    \"/ohiragana\": \"\\u304A\",\n    \"/ohm\": \"\\u2126\",\n    \"/ohminverted\": \"\\u2127\",\n    \"/ohoi\": \"\\u1ECF\",\n    \"/ohookabove\": \"\\u1ECF\",\n    \"/ohorn\": \"\\u01A1\",\n    \"/ohornacute\": \"\\u1EDB\",\n    \"/ohorndotbelow\": \"\\u1EE3\",\n    \"/ohorngrave\": \"\\u1EDD\",\n    \"/ohornhoi\": \"\\u1EDF\",\n    \"/ohornhookabove\": \"\\u1EDF\",\n    \"/ohorntilde\": \"\\u1EE1\",\n    \"/ohungarumlaut\": \"\\u0151\",\n    \"/ohuparen\": \"\\u321E\",\n    \"/oi\": \"\\u01A3\",\n    \"/oilDrum\": \"\\u1F6E2\",\n    \"/oinvertedbreve\": \"\\u020F\",\n    \"/ojeonparen\": \"\\u321D\",\n    \"/okHandSign\": \"\\u1F44C\",\n    \"/okatakana\": \"\\u30AA\",\n    \"/okatakanahalfwidth\": \"\\uFF75\",\n    \"/okorean\": \"\\u3157\",\n    \"/oksquare\": \"\\u1F197\",\n    \"/oldKey\": \"\\u1F5DD\",\n    \"/oldPersonalComputer\": \"\\u1F5B3\",\n    \"/olderMan\": \"\\u1F474\",\n    \"/olderWoman\": \"\\u1F475\",\n    \"/ole:hb\": \"\\u05AB\",\n    \"/olehebrew\": \"\\u05AB\",\n    \"/oloop\": \"\\uA74D\",\n    \"/olowringinside\": \"\\u2C7A\",\n    \"/omacron\": \"\\u014D\",\n    \"/omacronacute\": \"\\u1E53\",\n    \"/omacrongrave\": \"\\u1E51\",\n    \"/omdeva\": \"\\u0950\",\n    \"/omega\": \"\\u03C9\",\n    \"/omega1\": \"\\u03D6\",\n    \"/omegaacute\": \"\\u1F7D\",\n    \"/omegaacuteiotasub\": \"\\u1FF4\",\n    \"/omegaasper\": \"\\u1F61\",\n    \"/omegaasperacute\": \"\\u1F65\",\n    \"/omegaasperacuteiotasub\": \"\\u1FA5\",\n    \"/omegaaspergrave\": \"\\u1F63\",\n    \"/omegaaspergraveiotasub\": \"\\u1FA3\",\n    \"/omegaasperiotasub\": \"\\u1FA1\",\n    \"/omegaaspertilde\": \"\\u1F67\",\n    \"/omegaaspertildeiotasub\": \"\\u1FA7\",\n    \"/omegaclosed\": \"\\u0277\",\n    \"/omegacyr\": \"\\u0461\",\n    \"/omegacyrillic\": \"\\u0461\",\n    \"/omegafunc\": \"\\u2375\",\n    \"/omegagrave\": \"\\u1F7C\",\n    \"/omegagraveiotasub\": \"\\u1FF2\",\n    \"/omegaiotasub\": \"\\u1FF3\",\n    \"/omegalatinclosed\": \"\\u0277\",\n    \"/omegalenis\": \"\\u1F60\",\n    \"/omegalenisacute\": \"\\u1F64\",\n    \"/omegalenisacuteiotasub\": \"\\u1FA4\",\n    \"/omegalenisgrave\": \"\\u1F62\",\n    \"/omegalenisgraveiotasub\": \"\\u1FA2\",\n    \"/omegalenisiotasub\": \"\\u1FA0\",\n    \"/omegalenistilde\": \"\\u1F66\",\n    \"/omegalenistildeiotasub\": \"\\u1FA6\",\n    \"/omegaroundcyr\": \"\\u047B\",\n    \"/omegaroundcyrillic\": \"\\u047B\",\n    \"/omegatilde\": \"\\u1FF6\",\n    \"/omegatildeiotasub\": \"\\u1FF7\",\n    \"/omegatitlocyr\": \"\\u047D\",\n    \"/omegatitlocyrillic\": \"\\u047D\",\n    \"/omegatonos\": \"\\u03CE\",\n    \"/omegaunderlinefunc\": \"\\u2379\",\n    \"/omgujarati\": \"\\u0AD0\",\n    \"/omicron\": \"\\u03BF\",\n    \"/omicronacute\": \"\\u1F79\",\n    \"/omicronasper\": \"\\u1F41\",\n    \"/omicronasperacute\": \"\\u1F45\",\n    \"/omicronaspergrave\": \"\\u1F43\",\n    \"/omicrongrave\": \"\\u1F78\",\n    \"/omicronlenis\": \"\\u1F40\",\n    \"/omicronlenisacute\": \"\\u1F44\",\n    \"/omicronlenisgrave\": \"\\u1F42\",\n    \"/omicrontonos\": \"\\u03CC\",\n    \"/omonospace\": \"\\uFF4F\",\n    \"/onExclamationMarkLeftRightArrowAbove\": \"\\u1F51B\",\n    \"/oncomingAutomobile\": \"\\u1F698\",\n    \"/oncomingBus\": \"\\u1F68D\",\n    \"/oncomingFireEngine\": \"\\u1F6F1\",\n    \"/oncomingPoliceCar\": \"\\u1F694\",\n    \"/oncomingTaxi\": \"\\u1F696\",\n    \"/one\": \"\\u0031\",\n    \"/one.inferior\": \"\\u2081\",\n    \"/one.roman\": \"\\u2160\",\n    \"/one.romansmall\": \"\\u2170\",\n    \"/oneButtonMouse\": \"\\u1F5AF\",\n    \"/onearabic\": \"\\u0661\",\n    \"/onebengali\": \"\\u09E7\",\n    \"/onecircle\": \"\\u2460\",\n    \"/onecircledbl\": \"\\u24F5\",\n    \"/onecircleinversesansserif\": \"\\u278A\",\n    \"/onecomma\": \"\\u1F102\",\n    \"/onedeva\": \"\\u0967\",\n    \"/onedotenleader\": \"\\u2024\",\n    \"/onedotovertwodots\": \"\\u2E2B\",\n    \"/oneeighth\": \"\\u215B\",\n    \"/onefar\": \"\\u06F1\",\n    \"/onefitted\": \"\\uF6DC\",\n    \"/onefraction\": \"\\u215F\",\n    \"/onegujarati\": \"\\u0AE7\",\n    \"/onegurmukhi\": \"\\u0A67\",\n    \"/onehackarabic\": \"\\u0661\",\n    \"/onehalf\": \"\\u00BD\",\n    \"/onehangzhou\": \"\\u3021\",\n    \"/onehundred.roman\": \"\\u216D\",\n    \"/onehundred.romansmall\": \"\\u217D\",\n    \"/onehundredthousand.roman\": \"\\u2188\",\n    \"/onehundredtwentypsquare\": \"\\u1F1A4\",\n    \"/oneideographiccircled\": \"\\u3280\",\n    \"/oneideographicparen\": \"\\u3220\",\n    \"/oneinferior\": \"\\u2081\",\n    \"/onemonospace\": \"\\uFF11\",\n    \"/oneninth\": \"\\u2151\",\n    \"/onenumeratorbengali\": \"\\u09F4\",\n    \"/oneoldstyle\": \"\\uF731\",\n    \"/oneparen\": \"\\u2474\",\n    \"/oneparenthesized\": \"\\u2474\",\n    \"/oneperiod\": \"\\u2488\",\n    \"/onepersian\": \"\\u06F1\",\n    \"/onequarter\": \"\\u00BC\",\n    \"/oneroman\": \"\\u2170\",\n    \"/oneseventh\": \"\\u2150\",\n    \"/onesixth\": \"\\u2159\",\n    \"/onesuperior\": \"\\u00B9\",\n    \"/onethai\": \"\\u0E51\",\n    \"/onethird\": \"\\u2153\",\n    \"/onethousand.roman\": \"\\u216F\",\n    \"/onethousand.romansmall\": \"\\u217F\",\n    \"/onethousandcd.roman\": \"\\u2180\",\n    \"/onsusquare\": \"\\u3309\",\n    \"/oo\": \"\\uA74F\",\n    \"/oogonek\": \"\\u01EB\",\n    \"/oogonekmacron\": \"\\u01ED\",\n    \"/oogurmukhi\": \"\\u0A13\",\n    \"/oomatragurmukhi\": \"\\u0A4B\",\n    \"/oomusquare\": \"\\u330A\",\n    \"/oopen\": \"\\u0254\",\n    \"/oparen\": \"\\u24AA\",\n    \"/oparenthesized\": \"\\u24AA\",\n    \"/openBook\": \"\\u1F4D6\",\n    \"/openFileFolder\": \"\\u1F4C2\",\n    \"/openFolder\": \"\\u1F5C1\",\n    \"/openHandsSign\": \"\\u1F450\",\n    \"/openLock\": \"\\u1F513\",\n    \"/openMailboxLoweredFlag\": \"\\u1F4ED\",\n    \"/openMailboxRaisedFlag\": \"\\u1F4EC\",\n    \"/openbullet\": \"\\u25E6\",\n    \"/openheadarrowleft\": \"\\u21FD\",\n    \"/openheadarrowleftright\": \"\\u21FF\",\n    \"/openheadarrowright\": \"\\u21FE\",\n    \"/opensubset\": \"\\u27C3\",\n    \"/opensuperset\": \"\\u27C4\",\n    \"/ophiuchus\": \"\\u26CE\",\n    \"/opposition\": \"\\u260D\",\n    \"/opticalDisc\": \"\\u1F4BF\",\n    \"/opticalDiscIcon\": \"\\u1F5B8\",\n    \"/option\": \"\\u2325\",\n    \"/orangeBook\": \"\\u1F4D9\",\n    \"/ordfeminine\": \"\\u00AA\",\n    \"/ordmasculine\": \"\\u00BA\",\n    \"/ordotinside\": \"\\u27C7\",\n    \"/original\": \"\\u22B6\",\n    \"/ornateleftparenthesis\": \"\\uFD3E\",\n    \"/ornaterightparenthesis\": \"\\uFD3F\",\n    \"/orthodoxcross\": \"\\u2626\",\n    \"/orthogonal\": \"\\u221F\",\n    \"/orya:a\": \"\\u0B05\",\n    \"/orya:aa\": \"\\u0B06\",\n    \"/orya:aasign\": \"\\u0B3E\",\n    \"/orya:ai\": \"\\u0B10\",\n    \"/orya:ailengthmark\": \"\\u0B56\",\n    \"/orya:aisign\": \"\\u0B48\",\n    \"/orya:anusvara\": \"\\u0B02\",\n    \"/orya:au\": \"\\u0B14\",\n    \"/orya:aulengthmark\": \"\\u0B57\",\n    \"/orya:ausign\": \"\\u0B4C\",\n    \"/orya:avagraha\": \"\\u0B3D\",\n    \"/orya:ba\": \"\\u0B2C\",\n    \"/orya:bha\": \"\\u0B2D\",\n    \"/orya:ca\": \"\\u0B1A\",\n    \"/orya:candrabindu\": \"\\u0B01\",\n    \"/orya:cha\": \"\\u0B1B\",\n    \"/orya:da\": \"\\u0B26\",\n    \"/orya:dda\": \"\\u0B21\",\n    \"/orya:ddha\": \"\\u0B22\",\n    \"/orya:dha\": \"\\u0B27\",\n    \"/orya:e\": \"\\u0B0F\",\n    \"/orya:eight\": \"\\u0B6E\",\n    \"/orya:esign\": \"\\u0B47\",\n    \"/orya:five\": \"\\u0B6B\",\n    \"/orya:four\": \"\\u0B6A\",\n    \"/orya:fractiononeeighth\": \"\\u0B76\",\n    \"/orya:fractiononehalf\": \"\\u0B73\",\n    \"/orya:fractiononequarter\": \"\\u0B72\",\n    \"/orya:fractiononesixteenth\": \"\\u0B75\",\n    \"/orya:fractionthreequarters\": \"\\u0B74\",\n    \"/orya:fractionthreesixteenths\": \"\\u0B77\",\n    \"/orya:ga\": \"\\u0B17\",\n    \"/orya:gha\": \"\\u0B18\",\n    \"/orya:ha\": \"\\u0B39\",\n    \"/orya:i\": \"\\u0B07\",\n    \"/orya:ii\": \"\\u0B08\",\n    \"/orya:iisign\": \"\\u0B40\",\n    \"/orya:isign\": \"\\u0B3F\",\n    \"/orya:isshar\": \"\\u0B70\",\n    \"/orya:ja\": \"\\u0B1C\",\n    \"/orya:jha\": \"\\u0B1D\",\n    \"/orya:ka\": \"\\u0B15\",\n    \"/orya:kha\": \"\\u0B16\",\n    \"/orya:la\": \"\\u0B32\",\n    \"/orya:lla\": \"\\u0B33\",\n    \"/orya:llvocal\": \"\\u0B61\",\n    \"/orya:llvocalsign\": \"\\u0B63\",\n    \"/orya:lvocal\": \"\\u0B0C\",\n    \"/orya:lvocalsign\": \"\\u0B62\",\n    \"/orya:ma\": \"\\u0B2E\",\n    \"/orya:na\": \"\\u0B28\",\n    \"/orya:nga\": \"\\u0B19\",\n    \"/orya:nine\": \"\\u0B6F\",\n    \"/orya:nna\": \"\\u0B23\",\n    \"/orya:nukta\": \"\\u0B3C\",\n    \"/orya:nya\": \"\\u0B1E\",\n    \"/orya:o\": \"\\u0B13\",\n    \"/orya:one\": \"\\u0B67\",\n    \"/orya:osign\": \"\\u0B4B\",\n    \"/orya:pa\": \"\\u0B2A\",\n    \"/orya:pha\": \"\\u0B2B\",\n    \"/orya:ra\": \"\\u0B30\",\n    \"/orya:rha\": \"\\u0B5D\",\n    \"/orya:rra\": \"\\u0B5C\",\n    \"/orya:rrvocal\": \"\\u0B60\",\n    \"/orya:rrvocalsign\": \"\\u0B44\",\n    \"/orya:rvocal\": \"\\u0B0B\",\n    \"/orya:rvocalsign\": \"\\u0B43\",\n    \"/orya:sa\": \"\\u0B38\",\n    \"/orya:seven\": \"\\u0B6D\",\n    \"/orya:sha\": \"\\u0B36\",\n    \"/orya:six\": \"\\u0B6C\",\n    \"/orya:ssa\": \"\\u0B37\",\n    \"/orya:ta\": \"\\u0B24\",\n    \"/orya:tha\": \"\\u0B25\",\n    \"/orya:three\": \"\\u0B69\",\n    \"/orya:tta\": \"\\u0B1F\",\n    \"/orya:ttha\": \"\\u0B20\",\n    \"/orya:two\": \"\\u0B68\",\n    \"/orya:u\": \"\\u0B09\",\n    \"/orya:usign\": \"\\u0B41\",\n    \"/orya:uu\": \"\\u0B0A\",\n    \"/orya:uusign\": \"\\u0B42\",\n    \"/orya:va\": \"\\u0B35\",\n    \"/orya:virama\": \"\\u0B4D\",\n    \"/orya:visarga\": \"\\u0B03\",\n    \"/orya:wa\": \"\\u0B71\",\n    \"/orya:ya\": \"\\u0B2F\",\n    \"/orya:yya\": \"\\u0B5F\",\n    \"/orya:zero\": \"\\u0B66\",\n    \"/oscript\": \"\\u2134\",\n    \"/oshortdeva\": \"\\u0912\",\n    \"/oshortvowelsigndeva\": \"\\u094A\",\n    \"/oslash\": \"\\u00F8\",\n    \"/oslashacute\": \"\\u01FF\",\n    \"/osmallhiragana\": \"\\u3049\",\n    \"/osmallkatakana\": \"\\u30A9\",\n    \"/osmallkatakanahalfwidth\": \"\\uFF6B\",\n    \"/ostroke\": \"\\uA74B\",\n    \"/ostrokeacute\": \"\\u01FF\",\n    \"/osuperior\": \"\\uF6F0\",\n    \"/otcyr\": \"\\u047F\",\n    \"/otcyrillic\": \"\\u047F\",\n    \"/otilde\": \"\\u00F5\",\n    \"/otildeacute\": \"\\u1E4D\",\n    \"/otildedieresis\": \"\\u1E4F\",\n    \"/otildemacron\": \"\\u022D\",\n    \"/ou\": \"\\u0223\",\n    \"/oubopomofo\": \"\\u3121\",\n    \"/ounce\": \"\\u2125\",\n    \"/outboxTray\": \"\\u1F4E4\",\n    \"/outerjoinfull\": \"\\u27D7\",\n    \"/outerjoinleft\": \"\\u27D5\",\n    \"/outerjoinright\": \"\\u27D6\",\n    \"/outputpassiveup\": \"\\u2392\",\n    \"/overlap\": \"\\u1F5D7\",\n    \"/overline\": \"\\u203E\",\n    \"/overlinecenterline\": \"\\uFE4A\",\n    \"/overlinecmb\": \"\\u0305\",\n    \"/overlinedashed\": \"\\uFE49\",\n    \"/overlinedblwavy\": \"\\uFE4C\",\n    \"/overlinewavy\": \"\\uFE4B\",\n    \"/overscore\": \"\\u00AF\",\n    \"/ovfullwidth\": \"\\u3375\",\n    \"/ovowelsignbengali\": \"\\u09CB\",\n    \"/ovowelsigndeva\": \"\\u094B\",\n    \"/ovowelsigngujarati\": \"\\u0ACB\",\n    \"/ox\": \"\\u1F402\",\n    \"/p\": \"\\u0070\",\n    \"/p.inferior\": \"\\u209A\",\n    \"/paampsfullwidth\": \"\\u3380\",\n    \"/paampssquare\": \"\\u3380\",\n    \"/paasentosquare\": \"\\u332B\",\n    \"/paatusquare\": \"\\u332C\",\n    \"/pabengali\": \"\\u09AA\",\n    \"/pacerek\": \"\\uA989\",\n    \"/package\": \"\\u1F4E6\",\n    \"/pacute\": \"\\u1E55\",\n    \"/padeva\": \"\\u092A\",\n    \"/pafullwidth\": \"\\u33A9\",\n    \"/page\": \"\\u1F5CF\",\n    \"/pageCircledText\": \"\\u1F5DF\",\n    \"/pageCurl\": \"\\u1F4C3\",\n    \"/pageFacingUp\": \"\\u1F4C4\",\n    \"/pagedown\": \"\\u21DF\",\n    \"/pager\": \"\\u1F4DF\",\n    \"/pages\": \"\\u1F5D0\",\n    \"/pageup\": \"\\u21DE\",\n    \"/pagoda\": \"\\u1F6D4\",\n    \"/pagujarati\": \"\\u0AAA\",\n    \"/pagurmukhi\": \"\\u0A2A\",\n    \"/pahiragana\": \"\\u3071\",\n    \"/paiyannoithai\": \"\\u0E2F\",\n    \"/pakatakana\": \"\\u30D1\",\n    \"/palatalizationcyrilliccmb\": \"\\u0484\",\n    \"/palatcmbcyr\": \"\\u0484\",\n    \"/pallas\": \"\\u26B4\",\n    \"/palmTree\": \"\\u1F334\",\n    \"/palmbranch\": \"\\u2E19\",\n    \"/palochkacyr\": \"\\u04CF\",\n    \"/palochkacyrillic\": \"\\u04C0\",\n    \"/pamurda\": \"\\uA9A6\",\n    \"/pandaFace\": \"\\u1F43C\",\n    \"/pangkatpada\": \"\\uA9C7\",\n    \"/pangkon\": \"\\uA9C0\",\n    \"/pangrangkep\": \"\\uA9CF\",\n    \"/pansioskorean\": \"\\u317F\",\n    \"/panyangga\": \"\\uA980\",\n    \"/paperclip\": \"\\u1F4CE\",\n    \"/paragraph\": \"\\u00B6\",\n    \"/paragraphos\": \"\\u2E0F\",\n    \"/paragraphosforked\": \"\\u2E10\",\n    \"/paragraphosforkedreversed\": \"\\u2E11\",\n    \"/paragraphseparator\": \"\\u2029\",\n    \"/parallel\": \"\\u2225\",\n    \"/parallelogramblack\": \"\\u25B0\",\n    \"/parallelogramwhite\": \"\\u25B1\",\n    \"/parenbottom\": \"\\u23DD\",\n    \"/parendblleft\": \"\\u2E28\",\n    \"/parendblright\": \"\\u2E29\",\n    \"/parenextensionleft\": \"\\u239C\",\n    \"/parenextensionright\": \"\\u239F\",\n    \"/parenflatleft\": \"\\u27EE\",\n    \"/parenflatright\": \"\\u27EF\",\n    \"/parenhookupleft\": \"\\u239B\",\n    \"/parenhookupright\": \"\\u239E\",\n    \"/parenleft\": \"\\u0028\",\n    \"/parenleft.inferior\": \"\\u208D\",\n    \"/parenleft.superior\": \"\\u207D\",\n    \"/parenleftaltonearabic\": \"\\uFD3E\",\n    \"/parenleftbt\": \"\\uF8ED\",\n    \"/parenleftex\": \"\\uF8EC\",\n    \"/parenleftinferior\": \"\\u208D\",\n    \"/parenleftmonospace\": \"\\uFF08\",\n    \"/parenleftsmall\": \"\\uFE59\",\n    \"/parenleftsuperior\": \"\\u207D\",\n    \"/parenlefttp\": \"\\uF8EB\",\n    \"/parenleftvertical\": \"\\uFE35\",\n    \"/parenlowerhookleft\": \"\\u239D\",\n    \"/parenlowerhookright\": \"\\u23A0\",\n    \"/parenright\": \"\\u0029\",\n    \"/parenright.inferior\": \"\\u208E\",\n    \"/parenright.superior\": \"\\u207E\",\n    \"/parenrightaltonearabic\": \"\\uFD3F\",\n    \"/parenrightbt\": \"\\uF8F8\",\n    \"/parenrightex\": \"\\uF8F7\",\n    \"/parenrightinferior\": \"\\u208E\",\n    \"/parenrightmonospace\": \"\\uFF09\",\n    \"/parenrightsmall\": \"\\uFE5A\",\n    \"/parenrightsuperior\": \"\\u207E\",\n    \"/parenrighttp\": \"\\uF8F6\",\n    \"/parenrightvertical\": \"\\uFE36\",\n    \"/parentop\": \"\\u23DC\",\n    \"/partalternationmark\": \"\\u303D\",\n    \"/partialdiff\": \"\\u2202\",\n    \"/partnership\": \"\\u3250\",\n    \"/partyPopper\": \"\\u1F389\",\n    \"/paseq:hb\": \"\\u05C0\",\n    \"/paseqhebrew\": \"\\u05C0\",\n    \"/pashta:hb\": \"\\u0599\",\n    \"/pashtahebrew\": \"\\u0599\",\n    \"/pasquare\": \"\\u33A9\",\n    \"/passengerShip\": \"\\u1F6F3\",\n    \"/passivedown\": \"\\u2391\",\n    \"/passportControl\": \"\\u1F6C2\",\n    \"/patah\": \"\\u05B7\",\n    \"/patah11\": \"\\u05B7\",\n    \"/patah1d\": \"\\u05B7\",\n    \"/patah2a\": \"\\u05B7\",\n    \"/patah:hb\": \"\\u05B7\",\n    \"/patahhebrew\": \"\\u05B7\",\n    \"/patahnarrowhebrew\": \"\\u05B7\",\n    \"/patahquarterhebrew\": \"\\u05B7\",\n    \"/patahwidehebrew\": \"\\u05B7\",\n    \"/pawPrints\": \"\\u1F43E\",\n    \"/pawnblack\": \"\\u265F\",\n    \"/pawnwhite\": \"\\u2659\",\n    \"/pazer:hb\": \"\\u05A1\",\n    \"/pazerhebrew\": \"\\u05A1\",\n    \"/pbopomofo\": \"\\u3106\",\n    \"/pcfullwidth\": \"\\u3376\",\n    \"/pcircle\": \"\\u24DF\",\n    \"/pdot\": \"\\u1E57\",\n    \"/pdotaccent\": \"\\u1E57\",\n    \"/pe\": \"\\u05E4\",\n    \"/pe:hb\": \"\\u05E4\",\n    \"/peace\": \"\\u262E\",\n    \"/peach\": \"\\u1F351\",\n    \"/pear\": \"\\u1F350\",\n    \"/pecyr\": \"\\u043F\",\n    \"/pecyrillic\": \"\\u043F\",\n    \"/pedagesh\": \"\\uFB44\",\n    \"/pedageshhebrew\": \"\\uFB44\",\n    \"/pedestrian\": \"\\u1F6B6\",\n    \"/peezisquare\": \"\\u333B\",\n    \"/pefinaldageshhebrew\": \"\\uFB43\",\n    \"/peh.fina\": \"\\uFB57\",\n    \"/peh.init\": \"\\uFB58\",\n    \"/peh.isol\": \"\\uFB56\",\n    \"/peh.medi\": \"\\uFB59\",\n    \"/peharabic\": \"\\u067E\",\n    \"/peharmenian\": \"\\u057A\",\n    \"/pehebrew\": \"\\u05E4\",\n    \"/peheh\": \"\\u06A6\",\n    \"/peheh.fina\": \"\\uFB6F\",\n    \"/peheh.init\": \"\\uFB70\",\n    \"/peheh.isol\": \"\\uFB6E\",\n    \"/peheh.medi\": \"\\uFB71\",\n    \"/pehfinalarabic\": \"\\uFB57\",\n    \"/pehinitialarabic\": \"\\uFB58\",\n    \"/pehiragana\": \"\\u307A\",\n    \"/pehmedialarabic\": \"\\uFB59\",\n    \"/pehookcyr\": \"\\u04A7\",\n    \"/pekatakana\": \"\\u30DA\",\n    \"/pemiddlehookcyrillic\": \"\\u04A7\",\n    \"/penOverStampedEnvelope\": \"\\u1F586\",\n    \"/pengkalconsonant\": \"\\uA9BE\",\n    \"/penguin\": \"\\u1F427\",\n    \"/penihisquare\": \"\\u3338\",\n    \"/pensiveFace\": \"\\u1F614\",\n    \"/pensusquare\": \"\\u333A\",\n    \"/pentagram\": \"\\u26E4\",\n    \"/pentasememetrical\": \"\\u23D9\",\n    \"/pepetvowel\": \"\\uA9BC\",\n    \"/per\": \"\\u214C\",\n    \"/perafehebrew\": \"\\uFB4E\",\n    \"/percent\": \"\\u0025\",\n    \"/percentarabic\": \"\\u066A\",\n    \"/percentmonospace\": \"\\uFF05\",\n    \"/percentsmall\": \"\\uFE6A\",\n    \"/percussivebidental\": \"\\u02AD\",\n    \"/percussivebilabial\": \"\\u02AC\",\n    \"/performingArts\": \"\\u1F3AD\",\n    \"/period\": \"\\u002E\",\n    \"/periodarmenian\": \"\\u0589\",\n    \"/periodcentered\": \"\\u00B7\",\n    \"/periodhalfwidth\": \"\\uFF61\",\n    \"/periodinferior\": \"\\uF6E7\",\n    \"/periodmonospace\": \"\\uFF0E\",\n    \"/periodsmall\": \"\\uFE52\",\n    \"/periodsuperior\": \"\\uF6E8\",\n    \"/periodurdu\": \"\\u06D4\",\n    \"/perispomenigreekcmb\": \"\\u0342\",\n    \"/permanentpaper\": \"\\u267E\",\n    \"/permille\": \"\\u0609\",\n    \"/perpendicular\": \"\\u22A5\",\n    \"/perseveringFace\": \"\\u1F623\",\n    \"/personBlondHair\": \"\\u1F471\",\n    \"/personBowingDeeply\": \"\\u1F647\",\n    \"/personFrowning\": \"\\u1F64D\",\n    \"/personRaisingBothHandsInCelebration\": \"\\u1F64C\",\n    \"/personWithFoldedHands\": \"\\u1F64F\",\n    \"/personWithPoutingFace\": \"\\u1F64E\",\n    \"/personalComputer\": \"\\u1F4BB\",\n    \"/personball\": \"\\u26F9\",\n    \"/perspective\": \"\\u2306\",\n    \"/pertenthousandsign\": \"\\u2031\",\n    \"/perthousand\": \"\\u2030\",\n    \"/peseta\": \"\\u20A7\",\n    \"/peso\": \"\\u20B1\",\n    \"/pesosquare\": \"\\u3337\",\n    \"/petailcyr\": \"\\u0525\",\n    \"/pewithdagesh:hb\": \"\\uFB44\",\n    \"/pewithrafe:hb\": \"\\uFB4E\",\n    \"/pffullwidth\": \"\\u338A\",\n    \"/pflourish\": \"\\uA753\",\n    \"/pfsquare\": \"\\u338A\",\n    \"/phabengali\": \"\\u09AB\",\n    \"/phadeva\": \"\\u092B\",\n    \"/phagujarati\": \"\\u0AAB\",\n    \"/phagurmukhi\": \"\\u0A2B\",\n    \"/pharyngealvoicedfricative\": \"\\u0295\",\n    \"/phfullwidth\": \"\\u33D7\",\n    \"/phi\": \"\\u03C6\",\n    \"/phi.math\": \"\\u03D5\",\n    \"/phi1\": \"\\u03D5\",\n    \"/phieuphacirclekorean\": \"\\u327A\",\n    \"/phieuphaparenkorean\": \"\\u321A\",\n    \"/phieuphcirclekorean\": \"\\u326C\",\n    \"/phieuphkorean\": \"\\u314D\",\n    \"/phieuphparenkorean\": \"\\u320C\",\n    \"/philatin\": \"\\u0278\",\n    \"/phinthuthai\": \"\\u0E3A\",\n    \"/phisymbolgreek\": \"\\u03D5\",\n    \"/phitailless\": \"\\u2C77\",\n    \"/phon:AEsmall\": \"\\u1D01\",\n    \"/phon:Aemod\": \"\\u1D2D\",\n    \"/phon:Amod\": \"\\u1D2C\",\n    \"/phon:Asmall\": \"\\u1D00\",\n    \"/phon:Bbarmod\": \"\\u1D2F\",\n    \"/phon:Bbarsmall\": \"\\u1D03\",\n    \"/phon:Bmod\": \"\\u1D2E\",\n    \"/phon:Csmall\": \"\\u1D04\",\n    \"/phon:Dmod\": \"\\u1D30\",\n    \"/phon:Dsmall\": \"\\u1D05\",\n    \"/phon:ENcyrmod\": \"\\u1D78\",\n    \"/phon:Elsmallcyr\": \"\\u1D2B\",\n    \"/phon:Emod\": \"\\u1D31\",\n    \"/phon:Ereversedmod\": \"\\u1D32\",\n    \"/phon:Esmall\": \"\\u1D07\",\n    \"/phon:Ethsmall\": \"\\u1D06\",\n    \"/phon:Ezhsmall\": \"\\u1D23\",\n    \"/phon:Gmod\": \"\\u1D33\",\n    \"/phon:Hmod\": \"\\u1D34\",\n    \"/phon:Imod\": \"\\u1D35\",\n    \"/phon:Ismallmod\": \"\\u1DA6\",\n    \"/phon:Ismallstroke\": \"\\u1D7B\",\n    \"/phon:Istrokesmallmod\": \"\\u1DA7\",\n    \"/phon:Jmod\": \"\\u1D36\",\n    \"/phon:Jsmall\": \"\\u1D0A\",\n    \"/phon:Kmod\": \"\\u1D37\",\n    \"/phon:Ksmall\": \"\\u1D0B\",\n    \"/phon:Lmod\": \"\\u1D38\",\n    \"/phon:Lsmallmod\": \"\\u1DAB\",\n    \"/phon:Lsmallstroke\": \"\\u1D0C\",\n    \"/phon:Mmod\": \"\\u1D39\",\n    \"/phon:Msmall\": \"\\u1D0D\",\n    \"/phon:Nmod\": \"\\u1D3A\",\n    \"/phon:Nreversedmod\": \"\\u1D3B\",\n    \"/phon:Nsmallmod\": \"\\u1DB0\",\n    \"/phon:Nsmallreversed\": \"\\u1D0E\",\n    \"/phon:OUsmall\": \"\\u1D15\",\n    \"/phon:Omod\": \"\\u1D3C\",\n    \"/phon:Oopensmall\": \"\\u1D10\",\n    \"/phon:Osmall\": \"\\u1D0F\",\n    \"/phon:Oumod\": \"\\u1D3D\",\n    \"/phon:Pmod\": \"\\u1D3E\",\n    \"/phon:Psmall\": \"\\u1D18\",\n    \"/phon:Rmod\": \"\\u1D3F\",\n    \"/phon:Rsmallreversed\": \"\\u1D19\",\n    \"/phon:Rsmallturned\": \"\\u1D1A\",\n    \"/phon:Tmod\": \"\\u1D40\",\n    \"/phon:Tsmall\": \"\\u1D1B\",\n    \"/phon:Umod\": \"\\u1D41\",\n    \"/phon:Usmall\": \"\\u1D1C\",\n    \"/phon:Usmallmod\": \"\\u1DB8\",\n    \"/phon:Usmallstroke\": \"\\u1D7E\",\n    \"/phon:Vsmall\": \"\\u1D20\",\n    \"/phon:Wmod\": \"\\u1D42\",\n    \"/phon:Wsmall\": \"\\u1D21\",\n    \"/phon:Zsmall\": \"\\u1D22\",\n    \"/phon:aeturned\": \"\\u1D02\",\n    \"/phon:aeturnedmod\": \"\\u1D46\",\n    \"/phon:ain\": \"\\u1D25\",\n    \"/phon:ainmod\": \"\\u1D5C\",\n    \"/phon:alphamod\": \"\\u1D45\",\n    \"/phon:alpharetroflexhook\": \"\\u1D90\",\n    \"/phon:alphaturnedmod\": \"\\u1D9B\",\n    \"/phon:amod\": \"\\u1D43\",\n    \"/phon:aretroflexhook\": \"\\u1D8F\",\n    \"/phon:aturnedmod\": \"\\u1D44\",\n    \"/phon:betamod\": \"\\u1D5D\",\n    \"/phon:bmiddletilde\": \"\\u1D6C\",\n    \"/phon:bmod\": \"\\u1D47\",\n    \"/phon:bpalatalhook\": \"\\u1D80\",\n    \"/phon:ccurlmod\": \"\\u1D9D\",\n    \"/phon:chimod\": \"\\u1D61\",\n    \"/phon:cmod\": \"\\u1D9C\",\n    \"/phon:deltamod\": \"\\u1D5F\",\n    \"/phon:dhooktail\": \"\\u1D91\",\n    \"/phon:dmiddletilde\": \"\\u1D6D\",\n    \"/phon:dmod\": \"\\u1D48\",\n    \"/phon:dotlessjstrokemod\": \"\\u1DA1\",\n    \"/phon:dpalatalhook\": \"\\u1D81\",\n    \"/phon:emod\": \"\\u1D49\",\n    \"/phon:engmod\": \"\\u1D51\",\n    \"/phon:eopenmod\": \"\\u1D4B\",\n    \"/phon:eopenretroflexhook\": \"\\u1D93\",\n    \"/phon:eopenreversedmod\": \"\\u1D9F\",\n    \"/phon:eopenreversedretroflexhook\": \"\\u1D94\",\n    \"/phon:eopenturned\": \"\\u1D08\",\n    \"/phon:eopenturnedmod\": \"\\u1D4C\",\n    \"/phon:eretroflexhook\": \"\\u1D92\",\n    \"/phon:eshmod\": \"\\u1DB4\",\n    \"/phon:eshpalatalhook\": \"\\u1D8B\",\n    \"/phon:eshretroflexhook\": \"\\u1D98\",\n    \"/phon:ethmod\": \"\\u1D9E\",\n    \"/phon:ezhmod\": \"\\u1DBE\",\n    \"/phon:ezhretroflexhook\": \"\\u1D9A\",\n    \"/phon:fmiddletilde\": \"\\u1D6E\",\n    \"/phon:fmod\": \"\\u1DA0\",\n    \"/phon:fpalatalhook\": \"\\u1D82\",\n    \"/phon:ginsular\": \"\\u1D79\",\n    \"/phon:gmod\": \"\\u1D4D\",\n    \"/phon:gpalatalhook\": \"\\u1D83\",\n    \"/phon:gr:Gammasmall\": \"\\u1D26\",\n    \"/phon:gr:Lambdasmall\": \"\\u1D27\",\n    \"/phon:gr:Pismall\": \"\\u1D28\",\n    \"/phon:gr:Psismall\": \"\\u1D2A\",\n    \"/phon:gr:RsmallHO\": \"\\u1D29\",\n    \"/phon:gr:betasubscript\": \"\\u1D66\",\n    \"/phon:gr:chisubscript\": \"\\u1D6A\",\n    \"/phon:gr:gammamod\": \"\\u1D5E\",\n    \"/phon:gr:gammasubscript\": \"\\u1D67\",\n    \"/phon:gr:phimod\": \"\\u1D60\",\n    \"/phon:gr:phisubscript\": \"\\u1D69\",\n    \"/phon:gr:rhosubscript\": \"\\u1D68\",\n    \"/phon:gscriptmod\": \"\\u1DA2\",\n    \"/phon:gturned\": \"\\u1D77\",\n    \"/phon:hturnedmod\": \"\\u1DA3\",\n    \"/phon:iotamod\": \"\\u1DA5\",\n    \"/phon:iotastroke\": \"\\u1D7C\",\n    \"/phon:iretroflexhook\": \"\\u1D96\",\n    \"/phon:istrokemod\": \"\\u1DA4\",\n    \"/phon:isubscript\": \"\\u1D62\",\n    \"/phon:iturned\": \"\\u1D09\",\n    \"/phon:iturnedmod\": \"\\u1D4E\",\n    \"/phon:jcrossedtailmod\": \"\\u1DA8\",\n    \"/phon:kmod\": \"\\u1D4F\",\n    \"/phon:kpalatalhook\": \"\\u1D84\",\n    \"/phon:lpalatalhook\": \"\\u1D85\",\n    \"/phon:lpalatalhookmod\": \"\\u1DAA\",\n    \"/phon:lretroflexhookmod\": \"\\u1DA9\",\n    \"/phon:mhookmod\": \"\\u1DAC\",\n    \"/phon:mlonglegturnedmod\": \"\\u1DAD\",\n    \"/phon:mmiddletilde\": \"\\u1D6F\",\n    \"/phon:mmod\": \"\\u1D50\",\n    \"/phon:mpalatalhook\": \"\\u1D86\",\n    \"/phon:mturnedmod\": \"\\u1D5A\",\n    \"/phon:mturnedsideways\": \"\\u1D1F\",\n    \"/phon:nlefthookmod\": \"\\u1DAE\",\n    \"/phon:nmiddletilde\": \"\\u1D70\",\n    \"/phon:npalatalhook\": \"\\u1D87\",\n    \"/phon:nretroflexhookmod\": \"\\u1DAF\",\n    \"/phon:obarmod\": \"\\u1DB1\",\n    \"/phon:obottomhalf\": \"\\u1D17\",\n    \"/phon:obottomhalfmod\": \"\\u1D55\",\n    \"/phon:oeturned\": \"\\u1D14\",\n    \"/phon:omod\": \"\\u1D52\",\n    \"/phon:oopenmod\": \"\\u1D53\",\n    \"/phon:oopenretroflexhook\": \"\\u1D97\",\n    \"/phon:oopensideways\": \"\\u1D12\",\n    \"/phon:osideways\": \"\\u1D11\",\n    \"/phon:ostrokesideways\": \"\\u1D13\",\n    \"/phon:otophalf\": \"\\u1D16\",\n    \"/phon:otophalfmod\": \"\\u1D54\",\n    \"/phon:phimod\": \"\\u1DB2\",\n    \"/phon:pmiddletilde\": \"\\u1D71\",\n    \"/phon:pmod\": \"\\u1D56\",\n    \"/phon:ppalatalhook\": \"\\u1D88\",\n    \"/phon:pstroke\": \"\\u1D7D\",\n    \"/phon:rfishmiddletilde\": \"\\u1D73\",\n    \"/phon:rmiddletilde\": \"\\u1D72\",\n    \"/phon:rpalatalhook\": \"\\u1D89\",\n    \"/phon:rsubscript\": \"\\u1D63\",\n    \"/phon:schwamod\": \"\\u1D4A\",\n    \"/phon:schwaretroflexhook\": \"\\u1D95\",\n    \"/phon:shookmod\": \"\\u1DB3\",\n    \"/phon:smiddletilde\": \"\\u1D74\",\n    \"/phon:spalatalhook\": \"\\u1D8A\",\n    \"/phon:spirantvoicedlaryngeal\": \"\\u1D24\",\n    \"/phon:thetamod\": \"\\u1DBF\",\n    \"/phon:thstrike\": \"\\u1D7A\",\n    \"/phon:tmiddletilde\": \"\\u1D75\",\n    \"/phon:tmod\": \"\\u1D57\",\n    \"/phon:tpalatalhookmod\": \"\\u1DB5\",\n    \"/phon:ubarmod\": \"\\u1DB6\",\n    \"/phon:ue\": \"\\u1D6B\",\n    \"/phon:umod\": \"\\u1D58\",\n    \"/phon:upsilonmod\": \"\\u1DB7\",\n    \"/phon:upsilonstroke\": \"\\u1D7F\",\n    \"/phon:uretroflexhook\": \"\\u1D99\",\n    \"/phon:usideways\": \"\\u1D1D\",\n    \"/phon:usidewaysdieresised\": \"\\u1D1E\",\n    \"/phon:usidewaysmod\": \"\\u1D59\",\n    \"/phon:usubscript\": \"\\u1D64\",\n    \"/phon:vhookmod\": \"\\u1DB9\",\n    \"/phon:vmod\": \"\\u1D5B\",\n    \"/phon:vpalatalhook\": \"\\u1D8C\",\n    \"/phon:vsubscript\": \"\\u1D65\",\n    \"/phon:vturnedmod\": \"\\u1DBA\",\n    \"/phon:xpalatalhook\": \"\\u1D8D\",\n    \"/phon:zcurlmod\": \"\\u1DBD\",\n    \"/phon:zmiddletilde\": \"\\u1D76\",\n    \"/phon:zmod\": \"\\u1DBB\",\n    \"/phon:zpalatalhook\": \"\\u1D8E\",\n    \"/phon:zretroflexhookmod\": \"\\u1DBC\",\n    \"/phook\": \"\\u01A5\",\n    \"/phophanthai\": \"\\u0E1E\",\n    \"/phophungthai\": \"\\u0E1C\",\n    \"/phosamphaothai\": \"\\u0E20\",\n    \"/pi\": \"\\u03C0\",\n    \"/pi.math\": \"\\u03D6\",\n    \"/piasutorusquare\": \"\\u332E\",\n    \"/pick\": \"\\u26CF\",\n    \"/pidblstruck\": \"\\u213C\",\n    \"/pieupacirclekorean\": \"\\u3273\",\n    \"/pieupaparenkorean\": \"\\u3213\",\n    \"/pieupcieuckorean\": \"\\u3176\",\n    \"/pieupcirclekorean\": \"\\u3265\",\n    \"/pieupkiyeokkorean\": \"\\u3172\",\n    \"/pieupkorean\": \"\\u3142\",\n    \"/pieupparenkorean\": \"\\u3205\",\n    \"/pieupsioskiyeokkorean\": \"\\u3174\",\n    \"/pieupsioskorean\": \"\\u3144\",\n    \"/pieupsiostikeutkorean\": \"\\u3175\",\n    \"/pieupthieuthkorean\": \"\\u3177\",\n    \"/pieuptikeutkorean\": \"\\u3173\",\n    \"/pig\": \"\\u1F416\",\n    \"/pigFace\": \"\\u1F437\",\n    \"/pigNose\": \"\\u1F43D\",\n    \"/pihiragana\": \"\\u3074\",\n    \"/pikatakana\": \"\\u30D4\",\n    \"/pikosquare\": \"\\u3330\",\n    \"/pikurusquare\": \"\\u332F\",\n    \"/pilcrowsignreversed\": \"\\u204B\",\n    \"/pileOfPoo\": \"\\u1F4A9\",\n    \"/pill\": \"\\u1F48A\",\n    \"/pineDecoration\": \"\\u1F38D\",\n    \"/pineapple\": \"\\u1F34D\",\n    \"/pisces\": \"\\u2653\",\n    \"/piselehpada\": \"\\uA9CC\",\n    \"/pistol\": \"\\u1F52B\",\n    \"/pisymbolgreek\": \"\\u03D6\",\n    \"/pitchfork\": \"\\u22D4\",\n    \"/piwrarmenian\": \"\\u0583\",\n    \"/placeOfWorship\": \"\\u1F6D0\",\n    \"/placeofinterestsign\": \"\\u2318\",\n    \"/planck\": \"\\u210E\",\n    \"/plancktwopi\": \"\\u210F\",\n    \"/plus\": \"\\u002B\",\n    \"/plus.inferior\": \"\\u208A\",\n    \"/plus.superior\": \"\\u207A\",\n    \"/plusbelowcmb\": \"\\u031F\",\n    \"/pluscircle\": \"\\u2295\",\n    \"/plusminus\": \"\\u00B1\",\n    \"/plusmod\": \"\\u02D6\",\n    \"/plusmonospace\": \"\\uFF0B\",\n    \"/plussignalt:hb\": \"\\uFB29\",\n    \"/plussignmod\": \"\\u02D6\",\n    \"/plussmall\": \"\\uFE62\",\n    \"/plussuperior\": \"\\u207A\",\n    \"/pluto\": \"\\u2647\",\n    \"/pmfullwidth\": \"\\u33D8\",\n    \"/pmonospace\": \"\\uFF50\",\n    \"/pmsquare\": \"\\u33D8\",\n    \"/pocketCalculator\": \"\\u1F5A9\",\n    \"/poeticverse\": \"\\u060E\",\n    \"/pohiragana\": \"\\u307D\",\n    \"/pointerleftblack\": \"\\u25C4\",\n    \"/pointerleftwhite\": \"\\u25C5\",\n    \"/pointerrightblack\": \"\\u25BA\",\n    \"/pointerrightwhite\": \"\\u25BB\",\n    \"/pointingindexdownwhite\": \"\\u261F\",\n    \"/pointingindexleftblack\": \"\\u261A\",\n    \"/pointingindexleftwhite\": \"\\u261C\",\n    \"/pointingindexrightblack\": \"\\u261B\",\n    \"/pointingindexrightwhite\": \"\\u261E\",\n    \"/pointingindexupwhite\": \"\\u261D\",\n    \"/pointingtriangledownheavywhite\": \"\\u26DB\",\n    \"/pointosquare\": \"\\u333D\",\n    \"/pointring\": \"\\u2E30\",\n    \"/pokatakana\": \"\\u30DD\",\n    \"/pokrytiecmbcyr\": \"\\u0487\",\n    \"/policeCar\": \"\\u1F693\",\n    \"/policeCarsRevolvingLight\": \"\\u1F6A8\",\n    \"/policeOfficer\": \"\\u1F46E\",\n    \"/pondosquare\": \"\\u3340\",\n    \"/poodle\": \"\\u1F429\",\n    \"/popcorn\": \"\\u1F37F\",\n    \"/popdirectionalformatting\": \"\\u202C\",\n    \"/popdirectionalisolate\": \"\\u2069\",\n    \"/poplathai\": \"\\u0E1B\",\n    \"/portableStereo\": \"\\u1F4FE\",\n    \"/positionindicator\": \"\\u2316\",\n    \"/postalHorn\": \"\\u1F4EF\",\n    \"/postalmark\": \"\\u3012\",\n    \"/postalmarkface\": \"\\u3020\",\n    \"/postbox\": \"\\u1F4EE\",\n    \"/potOfFood\": \"\\u1F372\",\n    \"/potableWater\": \"\\u1F6B0\",\n    \"/pouch\": \"\\u1F45D\",\n    \"/poultryLeg\": \"\\u1F357\",\n    \"/poutingCatFace\": \"\\u1F63E\",\n    \"/poutingFace\": \"\\u1F621\",\n    \"/power\": \"\\u23FB\",\n    \"/poweron\": \"\\u23FD\",\n    \"/poweronoff\": \"\\u23FC\",\n    \"/powersleep\": \"\\u23FE\",\n    \"/pparen\": \"\\u24AB\",\n    \"/pparenthesized\": \"\\u24AB\",\n    \"/ppmfullwidth\": \"\\u33D9\",\n    \"/prayerBeads\": \"\\u1F4FF\",\n    \"/precedes\": \"\\u227A\",\n    \"/precedesbutnotequivalent\": \"\\u22E8\",\n    \"/precedesorequal\": \"\\u227C\",\n    \"/precedesorequivalent\": \"\\u227E\",\n    \"/precedesunderrelation\": \"\\u22B0\",\n    \"/prescription\": \"\\u211E\",\n    \"/preversedepigraphic\": \"\\uA7FC\",\n    \"/previouspage\": \"\\u2397\",\n    \"/prfullwidth\": \"\\u33DA\",\n    \"/primedblmod\": \"\\u02BA\",\n    \"/primemod\": \"\\u02B9\",\n    \"/primereversed\": \"\\u2035\",\n    \"/princess\": \"\\u1F478\",\n    \"/printer\": \"\\u1F5A8\",\n    \"/printerIcon\": \"\\u1F5B6\",\n    \"/printideographiccircled\": \"\\u329E\",\n    \"/printscreen\": \"\\u2399\",\n    \"/product\": \"\\u220F\",\n    \"/prohibitedSign\": \"\\u1F6C7\",\n    \"/projective\": \"\\u2305\",\n    \"/prolongedkana\": \"\\u30FC\",\n    \"/propellor\": \"\\u2318\",\n    \"/propersubset\": \"\\u2282\",\n    \"/propersuperset\": \"\\u2283\",\n    \"/propertyline\": \"\\u214A\",\n    \"/proportion\": \"\\u2237\",\n    \"/proportional\": \"\\u221D\",\n    \"/psfullwidth\": \"\\u33B0\",\n    \"/psi\": \"\\u03C8\",\n    \"/psicyr\": \"\\u0471\",\n    \"/psicyrillic\": \"\\u0471\",\n    \"/psilicmbcyr\": \"\\u0486\",\n    \"/psilipneumatacyrilliccmb\": \"\\u0486\",\n    \"/pssquare\": \"\\u33B0\",\n    \"/pstrokedescender\": \"\\uA751\",\n    \"/ptail\": \"\\uA755\",\n    \"/publicAddressLoudspeaker\": \"\\u1F4E2\",\n    \"/puhiragana\": \"\\u3077\",\n    \"/pukatakana\": \"\\u30D7\",\n    \"/punctuationspace\": \"\\u2008\",\n    \"/purpleHeart\": \"\\u1F49C\",\n    \"/purse\": \"\\u1F45B\",\n    \"/pushpin\": \"\\u1F4CC\",\n    \"/putLitterInItsPlace\": \"\\u1F6AE\",\n    \"/pvfullwidth\": \"\\u33B4\",\n    \"/pvsquare\": \"\\u33B4\",\n    \"/pwfullwidth\": \"\\u33BA\",\n    \"/pwsquare\": \"\\u33BA\",\n    \"/q\": \"\\u0071\",\n    \"/qacyr\": \"\\u051B\",\n    \"/qadeva\": \"\\u0958\",\n    \"/qadma:hb\": \"\\u05A8\",\n    \"/qadmahebrew\": \"\\u05A8\",\n    \"/qaf\": \"\\u0642\",\n    \"/qaf.fina\": \"\\uFED6\",\n    \"/qaf.init\": \"\\uFED7\",\n    \"/qaf.init_alefmaksura.fina\": \"\\uFC35\",\n    \"/qaf.init_hah.fina\": \"\\uFC33\",\n    \"/qaf.init_hah.medi\": \"\\uFCC2\",\n    \"/qaf.init_meem.fina\": \"\\uFC34\",\n    \"/qaf.init_meem.medi\": \"\\uFCC3\",\n    \"/qaf.init_meem.medi_hah.medi\": \"\\uFDB4\",\n    \"/qaf.init_yeh.fina\": \"\\uFC36\",\n    \"/qaf.isol\": \"\\uFED5\",\n    \"/qaf.medi\": \"\\uFED8\",\n    \"/qaf.medi_alefmaksura.fina\": \"\\uFC7E\",\n    \"/qaf.medi_meem.medi_hah.fina\": \"\\uFD7E\",\n    \"/qaf.medi_meem.medi_meem.fina\": \"\\uFD7F\",\n    \"/qaf.medi_meem.medi_yeh.fina\": \"\\uFDB2\",\n    \"/qaf.medi_yeh.fina\": \"\\uFC7F\",\n    \"/qaf_lam_alefmaksuraabove\": \"\\u06D7\",\n    \"/qafarabic\": \"\\u0642\",\n    \"/qafdotabove\": \"\\u06A7\",\n    \"/qaffinalarabic\": \"\\uFED6\",\n    \"/qafinitialarabic\": \"\\uFED7\",\n    \"/qafmedialarabic\": \"\\uFED8\",\n    \"/qafthreedotsabove\": \"\\u06A8\",\n    \"/qamats\": \"\\u05B8\",\n    \"/qamats10\": \"\\u05B8\",\n    \"/qamats1a\": \"\\u05B8\",\n    \"/qamats1c\": \"\\u05B8\",\n    \"/qamats27\": \"\\u05B8\",\n    \"/qamats29\": \"\\u05B8\",\n    \"/qamats33\": \"\\u05B8\",\n    \"/qamats:hb\": \"\\u05B8\",\n    \"/qamatsQatan:hb\": \"\\u05C7\",\n    \"/qamatsde\": \"\\u05B8\",\n    \"/qamatshebrew\": \"\\u05B8\",\n    \"/qamatsnarrowhebrew\": \"\\u05B8\",\n    \"/qamatsqatanhebrew\": \"\\u05B8\",\n    \"/qamatsqatannarrowhebrew\": \"\\u05B8\",\n    \"/qamatsqatanquarterhebrew\": \"\\u05B8\",\n    \"/qamatsqatanwidehebrew\": \"\\u05B8\",\n    \"/qamatsquarterhebrew\": \"\\u05B8\",\n    \"/qamatswidehebrew\": \"\\u05B8\",\n    \"/qarneFarah:hb\": \"\\u059F\",\n    \"/qarneyparahebrew\": \"\\u059F\",\n    \"/qbopomofo\": \"\\u3111\",\n    \"/qcircle\": \"\\u24E0\",\n    \"/qdiagonalstroke\": \"\\uA759\",\n    \"/qhook\": \"\\u02A0\",\n    \"/qhooktail\": \"\\u024B\",\n    \"/qmonospace\": \"\\uFF51\",\n    \"/qof\": \"\\u05E7\",\n    \"/qof:hb\": \"\\u05E7\",\n    \"/qofdagesh\": \"\\uFB47\",\n    \"/qofdageshhebrew\": \"\\uFB47\",\n    \"/qofhatafpatah\": \"\\u05E7\",\n    \"/qofhatafpatahhebrew\": \"\\u05E7\",\n    \"/qofhatafsegol\": \"\\u05E7\",\n    \"/qofhatafsegolhebrew\": \"\\u05E7\",\n    \"/qofhebrew\": \"\\u05E7\",\n    \"/qofhiriq\": \"\\u05E7\",\n    \"/qofhiriqhebrew\": \"\\u05E7\",\n    \"/qofholam\": \"\\u05E7\",\n    \"/qofholamhebrew\": \"\\u05E7\",\n    \"/qofpatah\": \"\\u05E7\",\n    \"/qofpatahhebrew\": \"\\u05E7\",\n    \"/qofqamats\": \"\\u05E7\",\n    \"/qofqamatshebrew\": \"\\u05E7\",\n    \"/qofqubuts\": \"\\u05E7\",\n    \"/qofqubutshebrew\": \"\\u05E7\",\n    \"/qofsegol\": \"\\u05E7\",\n    \"/qofsegolhebrew\": \"\\u05E7\",\n    \"/qofsheva\": \"\\u05E7\",\n    \"/qofshevahebrew\": \"\\u05E7\",\n    \"/qoftsere\": \"\\u05E7\",\n    \"/qoftserehebrew\": \"\\u05E7\",\n    \"/qofwithdagesh:hb\": \"\\uFB47\",\n    \"/qparen\": \"\\u24AC\",\n    \"/qparenthesized\": \"\\u24AC\",\n    \"/qpdigraph\": \"\\u0239\",\n    \"/qstrokedescender\": \"\\uA757\",\n    \"/quadarrowdownfunc\": \"\\u2357\",\n    \"/quadarrowleftfunc\": \"\\u2347\",\n    \"/quadarrowrightfunc\": \"\\u2348\",\n    \"/quadarrowupfunc\": \"\\u2350\",\n    \"/quadbackslashfunc\": \"\\u2342\",\n    \"/quadcaretdownfunc\": \"\\u234C\",\n    \"/quadcaretupfunc\": \"\\u2353\",\n    \"/quadcirclefunc\": \"\\u233C\",\n    \"/quadcolonfunc\": \"\\u2360\",\n    \"/quaddelfunc\": \"\\u2354\",\n    \"/quaddeltafunc\": \"\\u234D\",\n    \"/quaddiamondfunc\": \"\\u233A\",\n    \"/quaddividefunc\": \"\\u2339\",\n    \"/quadequalfunc\": \"\\u2338\",\n    \"/quadfunc\": \"\\u2395\",\n    \"/quadgreaterfunc\": \"\\u2344\",\n    \"/quadjotfunc\": \"\\u233B\",\n    \"/quadlessfunc\": \"\\u2343\",\n    \"/quadnotequalfunc\": \"\\u236F\",\n    \"/quadquestionfunc\": \"\\u2370\",\n    \"/quadrantLowerLeft\": \"\\u2596\",\n    \"/quadrantLowerRight\": \"\\u2597\",\n    \"/quadrantUpperLeft\": \"\\u2598\",\n    \"/quadrantUpperLeftAndLowerLeftAndLowerRight\": \"\\u2599\",\n    \"/quadrantUpperLeftAndLowerRight\": \"\\u259A\",\n    \"/quadrantUpperLeftAndUpperRightAndLowerLeft\": \"\\u259B\",\n    \"/quadrantUpperLeftAndUpperRightAndLowerRight\": \"\\u259C\",\n    \"/quadrantUpperRight\": \"\\u259D\",\n    \"/quadrantUpperRightAndLowerLeft\": \"\\u259E\",\n    \"/quadrantUpperRightAndLowerLeftAndLowerRight\": \"\\u259F\",\n    \"/quadrupleminute\": \"\\u2057\",\n    \"/quadslashfunc\": \"\\u2341\",\n    \"/quarternote\": \"\\u2669\",\n    \"/qubuts\": \"\\u05BB\",\n    \"/qubuts18\": \"\\u05BB\",\n    \"/qubuts25\": \"\\u05BB\",\n    \"/qubuts31\": \"\\u05BB\",\n    \"/qubuts:hb\": \"\\u05BB\",\n    \"/qubutshebrew\": \"\\u05BB\",\n    \"/qubutsnarrowhebrew\": \"\\u05BB\",\n    \"/qubutsquarterhebrew\": \"\\u05BB\",\n    \"/qubutswidehebrew\": \"\\u05BB\",\n    \"/queenblack\": \"\\u265B\",\n    \"/queenwhite\": \"\\u2655\",\n    \"/question\": \"\\u003F\",\n    \"/questionarabic\": \"\\u061F\",\n    \"/questionarmenian\": \"\\u055E\",\n    \"/questiondbl\": \"\\u2047\",\n    \"/questiondown\": \"\\u00BF\",\n    \"/questiondownsmall\": \"\\uF7BF\",\n    \"/questionedequal\": \"\\u225F\",\n    \"/questionexclamationmark\": \"\\u2048\",\n    \"/questiongreek\": \"\\u037E\",\n    \"/questionideographiccircled\": \"\\u3244\",\n    \"/questionmonospace\": \"\\uFF1F\",\n    \"/questionreversed\": \"\\u2E2E\",\n    \"/questionsmall\": \"\\uF73F\",\n    \"/quincunx\": \"\\u26BB\",\n    \"/quotedbl\": \"\\u0022\",\n    \"/quotedblbase\": \"\\u201E\",\n    \"/quotedblleft\": \"\\u201C\",\n    \"/quotedbllowreversed\": \"\\u2E42\",\n    \"/quotedblmonospace\": \"\\uFF02\",\n    \"/quotedblprime\": \"\\u301E\",\n    \"/quotedblprimereversed\": \"\\u301D\",\n    \"/quotedblreversed\": \"\\u201F\",\n    \"/quotedblright\": \"\\u201D\",\n    \"/quoteleft\": \"\\u2018\",\n    \"/quoteleftreversed\": \"\\u201B\",\n    \"/quotequadfunc\": \"\\u235E\",\n    \"/quotereversed\": \"\\u201B\",\n    \"/quoteright\": \"\\u2019\",\n    \"/quoterightn\": \"\\u0149\",\n    \"/quotesinglbase\": \"\\u201A\",\n    \"/quotesingle\": \"\\u0027\",\n    \"/quotesinglemonospace\": \"\\uFF07\",\n    \"/quoteunderlinefunc\": \"\\u2358\",\n    \"/r\": \"\\u0072\",\n    \"/raagung\": \"\\uA9AC\",\n    \"/raarmenian\": \"\\u057C\",\n    \"/rabbit\": \"\\u1F407\",\n    \"/rabbitFace\": \"\\u1F430\",\n    \"/rabengali\": \"\\u09B0\",\n    \"/racingCar\": \"\\u1F3CE\",\n    \"/racingMotorcycle\": \"\\u1F3CD\",\n    \"/racirclekatakana\": \"\\u32F6\",\n    \"/racute\": \"\\u0155\",\n    \"/radeva\": \"\\u0930\",\n    \"/radfullwidth\": \"\\u33AD\",\n    \"/radical\": \"\\u221A\",\n    \"/radicalbottom\": \"\\u23B7\",\n    \"/radicalex\": \"\\uF8E5\",\n    \"/radio\": \"\\u1F4FB\",\n    \"/radioButton\": \"\\u1F518\",\n    \"/radioactive\": \"\\u2622\",\n    \"/radovers2fullwidth\": \"\\u33AF\",\n    \"/radoversfullwidth\": \"\\u33AE\",\n    \"/radoverssquare\": \"\\u33AE\",\n    \"/radoverssquaredsquare\": \"\\u33AF\",\n    \"/radsquare\": \"\\u33AD\",\n    \"/rafe\": \"\\u05BF\",\n    \"/rafe:hb\": \"\\u05BF\",\n    \"/rafehebrew\": \"\\u05BF\",\n    \"/ragujarati\": \"\\u0AB0\",\n    \"/ragurmukhi\": \"\\u0A30\",\n    \"/rahiragana\": \"\\u3089\",\n    \"/railwayCar\": \"\\u1F683\",\n    \"/railwayTrack\": \"\\u1F6E4\",\n    \"/rain\": \"\\u26C6\",\n    \"/rainbow\": \"\\u1F308\",\n    \"/raisedHandFingersSplayed\": \"\\u1F590\",\n    \"/raisedHandPartBetweenMiddleAndRingFingers\": \"\\u1F596\",\n    \"/raisedmcsign\": \"\\u1F16A\",\n    \"/raisedmdsign\": \"\\u1F16B\",\n    \"/rakatakana\": \"\\u30E9\",\n    \"/rakatakanahalfwidth\": \"\\uFF97\",\n    \"/ralowerdiagonalbengali\": \"\\u09F1\",\n    \"/ram\": \"\\u1F40F\",\n    \"/ramiddlediagonalbengali\": \"\\u09F0\",\n    \"/ramshorn\": \"\\u0264\",\n    \"/rat\": \"\\u1F400\",\n    \"/ratio\": \"\\u2236\",\n    \"/ray\": \"\\u0608\",\n    \"/rbopomofo\": \"\\u3116\",\n    \"/rcaron\": \"\\u0159\",\n    \"/rcedilla\": \"\\u0157\",\n    \"/rcircle\": \"\\u24E1\",\n    \"/rcommaaccent\": \"\\u0157\",\n    \"/rdblgrave\": \"\\u0211\",\n    \"/rdot\": \"\\u1E59\",\n    \"/rdotaccent\": \"\\u1E59\",\n    \"/rdotbelow\": \"\\u1E5B\",\n    \"/rdotbelowmacron\": \"\\u1E5D\",\n    \"/reachideographicparen\": \"\\u3243\",\n    \"/recirclekatakana\": \"\\u32F9\",\n    \"/recreationalVehicle\": \"\\u1F699\",\n    \"/rectangleblack\": \"\\u25AC\",\n    \"/rectangleverticalblack\": \"\\u25AE\",\n    \"/rectangleverticalwhite\": \"\\u25AF\",\n    \"/rectanglewhite\": \"\\u25AD\",\n    \"/recycledpaper\": \"\\u267C\",\n    \"/recyclefiveplastics\": \"\\u2677\",\n    \"/recyclefourplastics\": \"\\u2676\",\n    \"/recyclegeneric\": \"\\u267A\",\n    \"/recycleoneplastics\": \"\\u2673\",\n    \"/recyclepartiallypaper\": \"\\u267D\",\n    \"/recyclesevenplastics\": \"\\u2679\",\n    \"/recyclesixplastics\": \"\\u2678\",\n    \"/recyclethreeplastics\": \"\\u2675\",\n    \"/recycletwoplastics\": \"\\u2674\",\n    \"/recycleuniversal\": \"\\u2672\",\n    \"/recycleuniversalblack\": \"\\u267B\",\n    \"/redApple\": \"\\u1F34E\",\n    \"/redTriangleDOwn\": \"\\u1F53B\",\n    \"/redTriangleUp\": \"\\u1F53A\",\n    \"/referencemark\": \"\\u203B\",\n    \"/reflexsubset\": \"\\u2286\",\n    \"/reflexsuperset\": \"\\u2287\",\n    \"/regionalindicatorsymbollettera\": \"\\u1F1E6\",\n    \"/regionalindicatorsymbolletterb\": \"\\u1F1E7\",\n    \"/regionalindicatorsymbolletterc\": \"\\u1F1E8\",\n    \"/regionalindicatorsymbolletterd\": \"\\u1F1E9\",\n    \"/regionalindicatorsymbollettere\": \"\\u1F1EA\",\n    \"/regionalindicatorsymbolletterf\": \"\\u1F1EB\",\n    \"/regionalindicatorsymbolletterg\": \"\\u1F1EC\",\n    \"/regionalindicatorsymbolletterh\": \"\\u1F1ED\",\n    \"/regionalindicatorsymbolletteri\": \"\\u1F1EE\",\n    \"/regionalindicatorsymbolletterj\": \"\\u1F1EF\",\n    \"/regionalindicatorsymbolletterk\": \"\\u1F1F0\",\n    \"/regionalindicatorsymbolletterl\": \"\\u1F1F1\",\n    \"/regionalindicatorsymbolletterm\": \"\\u1F1F2\",\n    \"/regionalindicatorsymbollettern\": \"\\u1F1F3\",\n    \"/regionalindicatorsymbollettero\": \"\\u1F1F4\",\n    \"/regionalindicatorsymbolletterp\": \"\\u1F1F5\",\n    \"/regionalindicatorsymbolletterq\": \"\\u1F1F6\",\n    \"/regionalindicatorsymbolletterr\": \"\\u1F1F7\",\n    \"/regionalindicatorsymbolletters\": \"\\u1F1F8\",\n    \"/regionalindicatorsymbollettert\": \"\\u1F1F9\",\n    \"/regionalindicatorsymbolletteru\": \"\\u1F1FA\",\n    \"/regionalindicatorsymbolletterv\": \"\\u1F1FB\",\n    \"/regionalindicatorsymbolletterw\": \"\\u1F1FC\",\n    \"/regionalindicatorsymbolletterx\": \"\\u1F1FD\",\n    \"/regionalindicatorsymbollettery\": \"\\u1F1FE\",\n    \"/regionalindicatorsymbolletterz\": \"\\u1F1FF\",\n    \"/registered\": \"\\u00AE\",\n    \"/registersans\": \"\\uF8E8\",\n    \"/registerserif\": \"\\uF6DA\",\n    \"/reh.fina\": \"\\uFEAE\",\n    \"/reh.init_superscriptalef.fina\": \"\\uFC5C\",\n    \"/reh.isol\": \"\\uFEAD\",\n    \"/rehHamzaAbove\": \"\\u076C\",\n    \"/rehSmallTahTwoDots\": \"\\u0771\",\n    \"/rehStroke\": \"\\u075B\",\n    \"/rehTwoDotsVerticallyAbove\": \"\\u076B\",\n    \"/rehVabove\": \"\\u0692\",\n    \"/rehVbelow\": \"\\u0695\",\n    \"/reharabic\": \"\\u0631\",\n    \"/reharmenian\": \"\\u0580\",\n    \"/rehdotbelow\": \"\\u0694\",\n    \"/rehdotbelowdotabove\": \"\\u0696\",\n    \"/rehfinalarabic\": \"\\uFEAE\",\n    \"/rehfourdotsabove\": \"\\u0699\",\n    \"/rehinvertedV\": \"\\u06EF\",\n    \"/rehiragana\": \"\\u308C\",\n    \"/rehring\": \"\\u0693\",\n    \"/rehtwodotsabove\": \"\\u0697\",\n    \"/rehyehaleflamarabic\": \"\\u0631\",\n    \"/rekatakana\": \"\\u30EC\",\n    \"/rekatakanahalfwidth\": \"\\uFF9A\",\n    \"/relievedFace\": \"\\u1F60C\",\n    \"/religionideographiccircled\": \"\\u32AA\",\n    \"/reminderRibbon\": \"\\u1F397\",\n    \"/remusquare\": \"\\u3355\",\n    \"/rentogensquare\": \"\\u3356\",\n    \"/replacementchar\": \"\\uFFFD\",\n    \"/replacementcharobj\": \"\\uFFFC\",\n    \"/representideographicparen\": \"\\u3239\",\n    \"/rerengganleft\": \"\\uA9C1\",\n    \"/rerengganright\": \"\\uA9C2\",\n    \"/resh\": \"\\u05E8\",\n    \"/resh:hb\": \"\\u05E8\",\n    \"/reshdageshhebrew\": \"\\uFB48\",\n    \"/reshhatafpatah\": \"\\u05E8\",\n    \"/reshhatafpatahhebrew\": \"\\u05E8\",\n    \"/reshhatafsegol\": \"\\u05E8\",\n    \"/reshhatafsegolhebrew\": \"\\u05E8\",\n    \"/reshhebrew\": \"\\u05E8\",\n    \"/reshhiriq\": \"\\u05E8\",\n    \"/reshhiriqhebrew\": \"\\u05E8\",\n    \"/reshholam\": \"\\u05E8\",\n    \"/reshholamhebrew\": \"\\u05E8\",\n    \"/reshpatah\": \"\\u05E8\",\n    \"/reshpatahhebrew\": \"\\u05E8\",\n    \"/reshqamats\": \"\\u05E8\",\n    \"/reshqamatshebrew\": \"\\u05E8\",\n    \"/reshqubuts\": \"\\u05E8\",\n    \"/reshqubutshebrew\": \"\\u05E8\",\n    \"/reshsegol\": \"\\u05E8\",\n    \"/reshsegolhebrew\": \"\\u05E8\",\n    \"/reshsheva\": \"\\u05E8\",\n    \"/reshshevahebrew\": \"\\u05E8\",\n    \"/reshtsere\": \"\\u05E8\",\n    \"/reshtserehebrew\": \"\\u05E8\",\n    \"/reshwide:hb\": \"\\uFB27\",\n    \"/reshwithdagesh:hb\": \"\\uFB48\",\n    \"/resourceideographiccircled\": \"\\u32AE\",\n    \"/resourceideographicparen\": \"\\u323E\",\n    \"/response\": \"\\u211F\",\n    \"/restideographiccircled\": \"\\u32A1\",\n    \"/restideographicparen\": \"\\u3241\",\n    \"/restrictedentryoneleft\": \"\\u26E0\",\n    \"/restrictedentrytwoleft\": \"\\u26E1\",\n    \"/restroom\": \"\\u1F6BB\",\n    \"/return\": \"\\u23CE\",\n    \"/reversedHandMiddleFingerExtended\": \"\\u1F595\",\n    \"/reversedRaisedHandFingersSplayed\": \"\\u1F591\",\n    \"/reversedThumbsDownSign\": \"\\u1F593\",\n    \"/reversedThumbsUpSign\": \"\\u1F592\",\n    \"/reversedVictoryHand\": \"\\u1F594\",\n    \"/reversedonehundred.roman\": \"\\u2183\",\n    \"/reversedtilde\": \"\\u223D\",\n    \"/reversedzecyr\": \"\\u0511\",\n    \"/revia:hb\": \"\\u0597\",\n    \"/reviahebrew\": \"\\u0597\",\n    \"/reviamugrashhebrew\": \"\\u0597\",\n    \"/revlogicalnot\": \"\\u2310\",\n    \"/revolvingHearts\": \"\\u1F49E\",\n    \"/rfishhook\": \"\\u027E\",\n    \"/rfishhookreversed\": \"\\u027F\",\n    \"/rgravedbl\": \"\\u0211\",\n    \"/rhabengali\": \"\\u09DD\",\n    \"/rhacyr\": \"\\u0517\",\n    \"/rhadeva\": \"\\u095D\",\n    \"/rho\": \"\\u03C1\",\n    \"/rhoasper\": \"\\u1FE5\",\n    \"/rhofunc\": \"\\u2374\",\n    \"/rholenis\": \"\\u1FE4\",\n    \"/rhook\": \"\\u027D\",\n    \"/rhookturned\": \"\\u027B\",\n    \"/rhookturnedsuperior\": \"\\u02B5\",\n    \"/rhookturnedsupmod\": \"\\u02B5\",\n    \"/rhostrokesymbol\": \"\\u03FC\",\n    \"/rhosymbol\": \"\\u03F1\",\n    \"/rhosymbolgreek\": \"\\u03F1\",\n    \"/rhotichookmod\": \"\\u02DE\",\n    \"/rial\": \"\\uFDFC\",\n    \"/ribbon\": \"\\u1F380\",\n    \"/riceBall\": \"\\u1F359\",\n    \"/riceCracker\": \"\\u1F358\",\n    \"/ricirclekatakana\": \"\\u32F7\",\n    \"/rieulacirclekorean\": \"\\u3271\",\n    \"/rieulaparenkorean\": \"\\u3211\",\n    \"/rieulcirclekorean\": \"\\u3263\",\n    \"/rieulhieuhkorean\": \"\\u3140\",\n    \"/rieulkiyeokkorean\": \"\\u313A\",\n    \"/rieulkiyeoksioskorean\": \"\\u3169\",\n    \"/rieulkorean\": \"\\u3139\",\n    \"/rieulmieumkorean\": \"\\u313B\",\n    \"/rieulpansioskorean\": \"\\u316C\",\n    \"/rieulparenkorean\": \"\\u3203\",\n    \"/rieulphieuphkorean\": \"\\u313F\",\n    \"/rieulpieupkorean\": \"\\u313C\",\n    \"/rieulpieupsioskorean\": \"\\u316B\",\n    \"/rieulsioskorean\": \"\\u313D\",\n    \"/rieulthieuthkorean\": \"\\u313E\",\n    \"/rieultikeutkorean\": \"\\u316A\",\n    \"/rieulyeorinhieuhkorean\": \"\\u316D\",\n    \"/right-pointingMagnifyingGlass\": \"\\u1F50E\",\n    \"/rightAngerBubble\": \"\\u1F5EF\",\n    \"/rightHalfBlock\": \"\\u2590\",\n    \"/rightHandTelephoneReceiver\": \"\\u1F57D\",\n    \"/rightOneEighthBlock\": \"\\u2595\",\n    \"/rightSpeaker\": \"\\u1F568\",\n    \"/rightSpeakerOneSoundWave\": \"\\u1F569\",\n    \"/rightSpeakerThreeSoundWaves\": \"\\u1F56A\",\n    \"/rightSpeechBubble\": \"\\u1F5E9\",\n    \"/rightThoughtBubble\": \"\\u1F5ED\",\n    \"/rightangle\": \"\\u221F\",\n    \"/rightarrowoverleftarrow\": \"\\u21C4\",\n    \"/rightdnheavyleftuplight\": \"\\u2546\",\n    \"/rightharpoonoverleftharpoon\": \"\\u21CC\",\n    \"/rightheavyleftdnlight\": \"\\u252E\",\n    \"/rightheavyleftuplight\": \"\\u2536\",\n    \"/rightheavyleftvertlight\": \"\\u253E\",\n    \"/rightideographiccircled\": \"\\u32A8\",\n    \"/rightlightleftdnheavy\": \"\\u2531\",\n    \"/rightlightleftupheavy\": \"\\u2539\",\n    \"/rightlightleftvertheavy\": \"\\u2549\",\n    \"/righttackbelowcmb\": \"\\u0319\",\n    \"/righttoleftembed\": \"\\u202B\",\n    \"/righttoleftisolate\": \"\\u2067\",\n    \"/righttoleftmark\": \"\\u200F\",\n    \"/righttoleftoverride\": \"\\u202E\",\n    \"/righttriangle\": \"\\u22BF\",\n    \"/rightupheavyleftdnlight\": \"\\u2544\",\n    \"/rihiragana\": \"\\u308A\",\n    \"/rikatakana\": \"\\u30EA\",\n    \"/rikatakanahalfwidth\": \"\\uFF98\",\n    \"/ring\": \"\\u02DA\",\n    \"/ringbelowcmb\": \"\\u0325\",\n    \"/ringcmb\": \"\\u030A\",\n    \"/ringequal\": \"\\u2257\",\n    \"/ringhalfleft\": \"\\u02BF\",\n    \"/ringhalfleftarmenian\": \"\\u0559\",\n    \"/ringhalfleftbelowcmb\": \"\\u031C\",\n    \"/ringhalfleftcentered\": \"\\u02D3\",\n    \"/ringhalfleftcentredmod\": \"\\u02D3\",\n    \"/ringhalfleftmod\": \"\\u02BF\",\n    \"/ringhalfright\": \"\\u02BE\",\n    \"/ringhalfrightbelowcmb\": \"\\u0339\",\n    \"/ringhalfrightcentered\": \"\\u02D2\",\n    \"/ringhalfrightcentredmod\": \"\\u02D2\",\n    \"/ringhalfrightmod\": \"\\u02BE\",\n    \"/ringinequal\": \"\\u2256\",\n    \"/ringingBell\": \"\\u1F56D\",\n    \"/ringlowmod\": \"\\u02F3\",\n    \"/ringoperator\": \"\\u2218\",\n    \"/rinsular\": \"\\uA783\",\n    \"/rinvertedbreve\": \"\\u0213\",\n    \"/rirasquare\": \"\\u3352\",\n    \"/risingdiagonal\": \"\\u27CB\",\n    \"/rittorusquare\": \"\\u3351\",\n    \"/rlinebelow\": \"\\u1E5F\",\n    \"/rlongleg\": \"\\u027C\",\n    \"/rlonglegturned\": \"\\u027A\",\n    \"/rmacrondot\": \"\\u1E5D\",\n    \"/rmonospace\": \"\\uFF52\",\n    \"/rnoon\": \"\\u06BB\",\n    \"/rnoon.fina\": \"\\uFBA1\",\n    \"/rnoon.init\": \"\\uFBA2\",\n    \"/rnoon.isol\": \"\\uFBA0\",\n    \"/rnoon.medi\": \"\\uFBA3\",\n    \"/roastedSweetPotato\": \"\\u1F360\",\n    \"/robliquestroke\": \"\\uA7A7\",\n    \"/rocirclekatakana\": \"\\u32FA\",\n    \"/rocket\": \"\\u1F680\",\n    \"/rohiragana\": \"\\u308D\",\n    \"/rokatakana\": \"\\u30ED\",\n    \"/rokatakanahalfwidth\": \"\\uFF9B\",\n    \"/rolled-upNewspaper\": \"\\u1F5DE\",\n    \"/rollerCoaster\": \"\\u1F3A2\",\n    \"/rookblack\": \"\\u265C\",\n    \"/rookwhite\": \"\\u2656\",\n    \"/rooster\": \"\\u1F413\",\n    \"/roruathai\": \"\\u0E23\",\n    \"/rose\": \"\\u1F339\",\n    \"/rosette\": \"\\u1F3F5\",\n    \"/roundPushpin\": \"\\u1F4CD\",\n    \"/roundedzeroabove\": \"\\u06DF\",\n    \"/rowboat\": \"\\u1F6A3\",\n    \"/rparen\": \"\\u24AD\",\n    \"/rparenthesized\": \"\\u24AD\",\n    \"/rrabengali\": \"\\u09DC\",\n    \"/rradeva\": \"\\u0931\",\n    \"/rragurmukhi\": \"\\u0A5C\",\n    \"/rreh\": \"\\u0691\",\n    \"/rreh.fina\": \"\\uFB8D\",\n    \"/rreh.isol\": \"\\uFB8C\",\n    \"/rreharabic\": \"\\u0691\",\n    \"/rrehfinalarabic\": \"\\uFB8D\",\n    \"/rrotunda\": \"\\uA75B\",\n    \"/rrvocalicbengali\": \"\\u09E0\",\n    \"/rrvocalicdeva\": \"\\u0960\",\n    \"/rrvocalicgujarati\": \"\\u0AE0\",\n    \"/rrvocalicvowelsignbengali\": \"\\u09C4\",\n    \"/rrvocalicvowelsigndeva\": \"\\u0944\",\n    \"/rrvocalicvowelsigngujarati\": \"\\u0AC4\",\n    \"/rstroke\": \"\\u024D\",\n    \"/rsuperior\": \"\\uF6F1\",\n    \"/rsupmod\": \"\\u02B3\",\n    \"/rtailturned\": \"\\u2C79\",\n    \"/rtblock\": \"\\u2590\",\n    \"/rturned\": \"\\u0279\",\n    \"/rturnedsuperior\": \"\\u02B4\",\n    \"/rturnedsupmod\": \"\\u02B4\",\n    \"/ruble\": \"\\u20BD\",\n    \"/rucirclekatakana\": \"\\u32F8\",\n    \"/rugbyFootball\": \"\\u1F3C9\",\n    \"/ruhiragana\": \"\\u308B\",\n    \"/rukatakana\": \"\\u30EB\",\n    \"/rukatakanahalfwidth\": \"\\uFF99\",\n    \"/rum\": \"\\uA775\",\n    \"/rumrotunda\": \"\\uA75D\",\n    \"/runner\": \"\\u1F3C3\",\n    \"/runningShirtSash\": \"\\u1F3BD\",\n    \"/rupeemarkbengali\": \"\\u09F2\",\n    \"/rupeesignbengali\": \"\\u09F3\",\n    \"/rupiah\": \"\\uF6DD\",\n    \"/rupiisquare\": \"\\u3353\",\n    \"/ruthai\": \"\\u0E24\",\n    \"/ruuburusquare\": \"\\u3354\",\n    \"/rvocalicbengali\": \"\\u098B\",\n    \"/rvocalicdeva\": \"\\u090B\",\n    \"/rvocalicgujarati\": \"\\u0A8B\",\n    \"/rvocalicvowelsignbengali\": \"\\u09C3\",\n    \"/rvocalicvowelsigndeva\": \"\\u0943\",\n    \"/rvocalicvowelsigngujarati\": \"\\u0AC3\",\n    \"/s\": \"\\u0073\",\n    \"/s.inferior\": \"\\u209B\",\n    \"/s_t\": \"\\uFB06\",\n    \"/sabengali\": \"\\u09B8\",\n    \"/sacirclekatakana\": \"\\u32DA\",\n    \"/sacute\": \"\\u015B\",\n    \"/sacutedotaccent\": \"\\u1E65\",\n    \"/sad\": \"\\u0635\",\n    \"/sad.fina\": \"\\uFEBA\",\n    \"/sad.init\": \"\\uFEBB\",\n    \"/sad.init_alefmaksura.fina\": \"\\uFD05\",\n    \"/sad.init_hah.fina\": \"\\uFC20\",\n    \"/sad.init_hah.medi\": \"\\uFCB1\",\n    \"/sad.init_hah.medi_hah.medi\": \"\\uFD65\",\n    \"/sad.init_khah.medi\": \"\\uFCB2\",\n    \"/sad.init_meem.fina\": \"\\uFC21\",\n    \"/sad.init_meem.medi\": \"\\uFCB3\",\n    \"/sad.init_meem.medi_meem.medi\": \"\\uFDC5\",\n    \"/sad.init_reh.fina\": \"\\uFD0F\",\n    \"/sad.init_yeh.fina\": \"\\uFD06\",\n    \"/sad.isol\": \"\\uFEB9\",\n    \"/sad.medi\": \"\\uFEBC\",\n    \"/sad.medi_alefmaksura.fina\": \"\\uFD21\",\n    \"/sad.medi_hah.medi_hah.fina\": \"\\uFD64\",\n    \"/sad.medi_hah.medi_yeh.fina\": \"\\uFDA9\",\n    \"/sad.medi_meem.medi_meem.fina\": \"\\uFD66\",\n    \"/sad.medi_reh.fina\": \"\\uFD2B\",\n    \"/sad.medi_yeh.fina\": \"\\uFD22\",\n    \"/sad_lam_alefmaksuraabove\": \"\\u06D6\",\n    \"/sadarabic\": \"\\u0635\",\n    \"/sadeva\": \"\\u0938\",\n    \"/sadfinalarabic\": \"\\uFEBA\",\n    \"/sadinitialarabic\": \"\\uFEBB\",\n    \"/sadmedialarabic\": \"\\uFEBC\",\n    \"/sadthreedotsabove\": \"\\u069E\",\n    \"/sadtwodotsbelow\": \"\\u069D\",\n    \"/sagittarius\": \"\\u2650\",\n    \"/sagujarati\": \"\\u0AB8\",\n    \"/sagurmukhi\": \"\\u0A38\",\n    \"/sahiragana\": \"\\u3055\",\n    \"/saikurusquare\": \"\\u331F\",\n    \"/sailboat\": \"\\u26F5\",\n    \"/sakatakana\": \"\\u30B5\",\n    \"/sakatakanahalfwidth\": \"\\uFF7B\",\n    \"/sakeBottleAndCup\": \"\\u1F376\",\n    \"/sallallahoualayhewasallamarabic\": \"\\uFDFA\",\n    \"/saltillo\": \"\\uA78C\",\n    \"/saltire\": \"\\u2613\",\n    \"/samahaprana\": \"\\uA9B0\",\n    \"/samekh\": \"\\u05E1\",\n    \"/samekh:hb\": \"\\u05E1\",\n    \"/samekhdagesh\": \"\\uFB41\",\n    \"/samekhdageshhebrew\": \"\\uFB41\",\n    \"/samekhhebrew\": \"\\u05E1\",\n    \"/samekhwithdagesh:hb\": \"\\uFB41\",\n    \"/sampi\": \"\\u03E1\",\n    \"/sampiarchaic\": \"\\u0373\",\n    \"/samurda\": \"\\uA9AF\",\n    \"/samvat\": \"\\u0604\",\n    \"/san\": \"\\u03FB\",\n    \"/santiimusquare\": \"\\u3320\",\n    \"/saraaathai\": \"\\u0E32\",\n    \"/saraaethai\": \"\\u0E41\",\n    \"/saraaimaimalaithai\": \"\\u0E44\",\n    \"/saraaimaimuanthai\": \"\\u0E43\",\n    \"/saraamthai\": \"\\u0E33\",\n    \"/saraathai\": \"\\u0E30\",\n    \"/saraethai\": \"\\u0E40\",\n    \"/saraiileftthai\": \"\\uF886\",\n    \"/saraiithai\": \"\\u0E35\",\n    \"/saraileftthai\": \"\\uF885\",\n    \"/saraithai\": \"\\u0E34\",\n    \"/saraothai\": \"\\u0E42\",\n    \"/saraueeleftthai\": \"\\uF888\",\n    \"/saraueethai\": \"\\u0E37\",\n    \"/saraueleftthai\": \"\\uF887\",\n    \"/sarauethai\": \"\\u0E36\",\n    \"/sarauthai\": \"\\u0E38\",\n    \"/sarauuthai\": \"\\u0E39\",\n    \"/satellite\": \"\\u1F6F0\",\n    \"/satelliteAntenna\": \"\\u1F4E1\",\n    \"/saturn\": \"\\u2644\",\n    \"/saxophone\": \"\\u1F3B7\",\n    \"/sbopomofo\": \"\\u3119\",\n    \"/scales\": \"\\u2696\",\n    \"/scanninehorizontal\": \"\\u23BD\",\n    \"/scanonehorizontal\": \"\\u23BA\",\n    \"/scansevenhorizontal\": \"\\u23BC\",\n    \"/scanthreehorizontal\": \"\\u23BB\",\n    \"/scaron\": \"\\u0161\",\n    \"/scarondot\": \"\\u1E67\",\n    \"/scarondotaccent\": \"\\u1E67\",\n    \"/scedilla\": \"\\u015F\",\n    \"/school\": \"\\u1F3EB\",\n    \"/schoolSatchel\": \"\\u1F392\",\n    \"/schoolideographiccircled\": \"\\u3246\",\n    \"/schwa\": \"\\u0259\",\n    \"/schwa.inferior\": \"\\u2094\",\n    \"/schwacyr\": \"\\u04D9\",\n    \"/schwacyrillic\": \"\\u04D9\",\n    \"/schwadieresiscyr\": \"\\u04DB\",\n    \"/schwadieresiscyrillic\": \"\\u04DB\",\n    \"/schwahook\": \"\\u025A\",\n    \"/scircle\": \"\\u24E2\",\n    \"/scircumflex\": \"\\u015D\",\n    \"/scommaaccent\": \"\\u0219\",\n    \"/scooter\": \"\\u1F6F4\",\n    \"/scorpius\": \"\\u264F\",\n    \"/screen\": \"\\u1F5B5\",\n    \"/scroll\": \"\\u1F4DC\",\n    \"/scruple\": \"\\u2108\",\n    \"/sdot\": \"\\u1E61\",\n    \"/sdotaccent\": \"\\u1E61\",\n    \"/sdotbelow\": \"\\u1E63\",\n    \"/sdotbelowdotabove\": \"\\u1E69\",\n    \"/sdotbelowdotaccent\": \"\\u1E69\",\n    \"/seagullbelowcmb\": \"\\u033C\",\n    \"/seat\": \"\\u1F4BA\",\n    \"/secirclekatakana\": \"\\u32DD\",\n    \"/second\": \"\\u2033\",\n    \"/secondreversed\": \"\\u2036\",\n    \"/secondscreensquare\": \"\\u1F19C\",\n    \"/secondtonechinese\": \"\\u02CA\",\n    \"/secretideographiccircled\": \"\\u3299\",\n    \"/section\": \"\\u00A7\",\n    \"/sectionsignhalftop\": \"\\u2E39\",\n    \"/sector\": \"\\u2314\",\n    \"/seeNoEvilMonkey\": \"\\u1F648\",\n    \"/seedling\": \"\\u1F331\",\n    \"/seen\": \"\\u0633\",\n    \"/seen.fina\": \"\\uFEB2\",\n    \"/seen.init\": \"\\uFEB3\",\n    \"/seen.init_alefmaksura.fina\": \"\\uFCFB\",\n    \"/seen.init_hah.fina\": \"\\uFC1D\",\n    \"/seen.init_hah.medi\": \"\\uFCAE\",\n    \"/seen.init_hah.medi_jeem.medi\": \"\\uFD5C\",\n    \"/seen.init_heh.medi\": \"\\uFD31\",\n    \"/seen.init_jeem.fina\": \"\\uFC1C\",\n    \"/seen.init_jeem.medi\": \"\\uFCAD\",\n    \"/seen.init_jeem.medi_hah.medi\": \"\\uFD5D\",\n    \"/seen.init_khah.fina\": \"\\uFC1E\",\n    \"/seen.init_khah.medi\": \"\\uFCAF\",\n    \"/seen.init_meem.fina\": \"\\uFC1F\",\n    \"/seen.init_meem.medi\": \"\\uFCB0\",\n    \"/seen.init_meem.medi_hah.medi\": \"\\uFD60\",\n    \"/seen.init_meem.medi_jeem.medi\": \"\\uFD61\",\n    \"/seen.init_meem.medi_meem.medi\": \"\\uFD63\",\n    \"/seen.init_reh.fina\": \"\\uFD0E\",\n    \"/seen.init_yeh.fina\": \"\\uFCFC\",\n    \"/seen.isol\": \"\\uFEB1\",\n    \"/seen.medi\": \"\\uFEB4\",\n    \"/seen.medi_alefmaksura.fina\": \"\\uFD17\",\n    \"/seen.medi_hah.medi\": \"\\uFD35\",\n    \"/seen.medi_heh.medi\": \"\\uFCE8\",\n    \"/seen.medi_jeem.medi\": \"\\uFD34\",\n    \"/seen.medi_jeem.medi_alefmaksura.fina\": \"\\uFD5E\",\n    \"/seen.medi_khah.medi\": \"\\uFD36\",\n    \"/seen.medi_khah.medi_alefmaksura.fina\": \"\\uFDA8\",\n    \"/seen.medi_khah.medi_yeh.fina\": \"\\uFDC6\",\n    \"/seen.medi_meem.medi\": \"\\uFCE7\",\n    \"/seen.medi_meem.medi_hah.fina\": \"\\uFD5F\",\n    \"/seen.medi_meem.medi_meem.fina\": \"\\uFD62\",\n    \"/seen.medi_reh.fina\": \"\\uFD2A\",\n    \"/seen.medi_yeh.fina\": \"\\uFD18\",\n    \"/seenDigitFourAbove\": \"\\u077D\",\n    \"/seenFourDotsAbove\": \"\\u075C\",\n    \"/seenInvertedV\": \"\\u077E\",\n    \"/seenSmallTahTwoDots\": \"\\u0770\",\n    \"/seenTwoDotsVerticallyAbove\": \"\\u076D\",\n    \"/seenabove\": \"\\u06DC\",\n    \"/seenarabic\": \"\\u0633\",\n    \"/seendotbelowdotabove\": \"\\u069A\",\n    \"/seenfinalarabic\": \"\\uFEB2\",\n    \"/seeninitialarabic\": \"\\uFEB3\",\n    \"/seenlow\": \"\\u06E3\",\n    \"/seenmedialarabic\": \"\\uFEB4\",\n    \"/seenthreedotsbelow\": \"\\u069B\",\n    \"/seenthreedotsbelowthreedotsabove\": \"\\u069C\",\n    \"/segment\": \"\\u2313\",\n    \"/segol\": \"\\u05B6\",\n    \"/segol13\": \"\\u05B6\",\n    \"/segol1f\": \"\\u05B6\",\n    \"/segol2c\": \"\\u05B6\",\n    \"/segol:hb\": \"\\u05B6\",\n    \"/segolhebrew\": \"\\u05B6\",\n    \"/segolnarrowhebrew\": \"\\u05B6\",\n    \"/segolquarterhebrew\": \"\\u05B6\",\n    \"/segolta:hb\": \"\\u0592\",\n    \"/segoltahebrew\": \"\\u0592\",\n    \"/segolwidehebrew\": \"\\u05B6\",\n    \"/seharmenian\": \"\\u057D\",\n    \"/sehiragana\": \"\\u305B\",\n    \"/sekatakana\": \"\\u30BB\",\n    \"/sekatakanahalfwidth\": \"\\uFF7E\",\n    \"/selfideographicparen\": \"\\u3242\",\n    \"/semicolon\": \"\\u003B\",\n    \"/semicolonarabic\": \"\\u061B\",\n    \"/semicolonmonospace\": \"\\uFF1B\",\n    \"/semicolonreversed\": \"\\u204F\",\n    \"/semicolonsmall\": \"\\uFE54\",\n    \"/semicolonunderlinefunc\": \"\\u236E\",\n    \"/semidirectproductleft\": \"\\u22CB\",\n    \"/semidirectproductright\": \"\\u22CC\",\n    \"/semisextile\": \"\\u26BA\",\n    \"/semisoftcyr\": \"\\u048D\",\n    \"/semivoicedmarkkana\": \"\\u309C\",\n    \"/semivoicedmarkkanahalfwidth\": \"\\uFF9F\",\n    \"/sentisquare\": \"\\u3322\",\n    \"/sentosquare\": \"\\u3323\",\n    \"/septembertelegraph\": \"\\u32C8\",\n    \"/sersetdblup\": \"\\u22D1\",\n    \"/sersetnotequalup\": \"\\u228B\",\n    \"/servicemark\": \"\\u2120\",\n    \"/sesamedot\": \"\\uFE45\",\n    \"/sesquiquadrate\": \"\\u26BC\",\n    \"/setminus\": \"\\u2216\",\n    \"/seven\": \"\\u0037\",\n    \"/seven.inferior\": \"\\u2087\",\n    \"/seven.roman\": \"\\u2166\",\n    \"/seven.romansmall\": \"\\u2176\",\n    \"/seven.superior\": \"\\u2077\",\n    \"/sevenarabic\": \"\\u0667\",\n    \"/sevenbengali\": \"\\u09ED\",\n    \"/sevencircle\": \"\\u2466\",\n    \"/sevencircledbl\": \"\\u24FB\",\n    \"/sevencircleinversesansserif\": \"\\u2790\",\n    \"/sevencomma\": \"\\u1F108\",\n    \"/sevendeva\": \"\\u096D\",\n    \"/seveneighths\": \"\\u215E\",\n    \"/sevenfar\": \"\\u06F7\",\n    \"/sevengujarati\": \"\\u0AED\",\n    \"/sevengurmukhi\": \"\\u0A6D\",\n    \"/sevenhackarabic\": \"\\u0667\",\n    \"/sevenhangzhou\": \"\\u3027\",\n    \"/sevenideographiccircled\": \"\\u3286\",\n    \"/sevenideographicparen\": \"\\u3226\",\n    \"/seveninferior\": \"\\u2087\",\n    \"/sevenmonospace\": \"\\uFF17\",\n    \"/sevenoldstyle\": \"\\uF737\",\n    \"/sevenparen\": \"\\u247A\",\n    \"/sevenparenthesized\": \"\\u247A\",\n    \"/sevenperiod\": \"\\u248E\",\n    \"/sevenpersian\": \"\\u06F7\",\n    \"/sevenpointonesquare\": \"\\u1F1A1\",\n    \"/sevenroman\": \"\\u2176\",\n    \"/sevensuperior\": \"\\u2077\",\n    \"/seventeencircle\": \"\\u2470\",\n    \"/seventeencircleblack\": \"\\u24F1\",\n    \"/seventeenparen\": \"\\u2484\",\n    \"/seventeenparenthesized\": \"\\u2484\",\n    \"/seventeenperiod\": \"\\u2498\",\n    \"/seventhai\": \"\\u0E57\",\n    \"/seventycirclesquare\": \"\\u324E\",\n    \"/sextile\": \"\\u26B9\",\n    \"/sfthyphen\": \"\\u00AD\",\n    \"/shaarmenian\": \"\\u0577\",\n    \"/shabengali\": \"\\u09B6\",\n    \"/shacyr\": \"\\u0448\",\n    \"/shacyrillic\": \"\\u0448\",\n    \"/shaddaAlefIsol\": \"\\uFC63\",\n    \"/shaddaDammaIsol\": \"\\uFC61\",\n    \"/shaddaDammaMedi\": \"\\uFCF3\",\n    \"/shaddaDammatanIsol\": \"\\uFC5E\",\n    \"/shaddaFathaIsol\": \"\\uFC60\",\n    \"/shaddaFathaMedi\": \"\\uFCF2\",\n    \"/shaddaIsol\": \"\\uFE7C\",\n    \"/shaddaKasraIsol\": \"\\uFC62\",\n    \"/shaddaKasraMedi\": \"\\uFCF4\",\n    \"/shaddaKasratanIsol\": \"\\uFC5F\",\n    \"/shaddaMedi\": \"\\uFE7D\",\n    \"/shaddaarabic\": \"\\u0651\",\n    \"/shaddadammaarabic\": \"\\uFC61\",\n    \"/shaddadammatanarabic\": \"\\uFC5E\",\n    \"/shaddafathaarabic\": \"\\uFC60\",\n    \"/shaddafathatanarabic\": \"\\u0651\",\n    \"/shaddakasraarabic\": \"\\uFC62\",\n    \"/shaddakasratanarabic\": \"\\uFC5F\",\n    \"/shade\": \"\\u2592\",\n    \"/shadedark\": \"\\u2593\",\n    \"/shadelight\": \"\\u2591\",\n    \"/shademedium\": \"\\u2592\",\n    \"/shadeva\": \"\\u0936\",\n    \"/shagujarati\": \"\\u0AB6\",\n    \"/shagurmukhi\": \"\\u0A36\",\n    \"/shalshelet:hb\": \"\\u0593\",\n    \"/shalshelethebrew\": \"\\u0593\",\n    \"/shamrock\": \"\\u2618\",\n    \"/shavedIce\": \"\\u1F367\",\n    \"/shbopomofo\": \"\\u3115\",\n    \"/shchacyr\": \"\\u0449\",\n    \"/shchacyrillic\": \"\\u0449\",\n    \"/sheen\": \"\\u0634\",\n    \"/sheen.fina\": \"\\uFEB6\",\n    \"/sheen.init\": \"\\uFEB7\",\n    \"/sheen.init_alefmaksura.fina\": \"\\uFCFD\",\n    \"/sheen.init_hah.fina\": \"\\uFD0A\",\n    \"/sheen.init_hah.medi\": \"\\uFD2E\",\n    \"/sheen.init_hah.medi_meem.medi\": \"\\uFD68\",\n    \"/sheen.init_heh.medi\": \"\\uFD32\",\n    \"/sheen.init_jeem.fina\": \"\\uFD09\",\n    \"/sheen.init_jeem.medi\": \"\\uFD2D\",\n    \"/sheen.init_khah.fina\": \"\\uFD0B\",\n    \"/sheen.init_khah.medi\": \"\\uFD2F\",\n    \"/sheen.init_meem.fina\": \"\\uFD0C\",\n    \"/sheen.init_meem.medi\": \"\\uFD30\",\n    \"/sheen.init_meem.medi_khah.medi\": \"\\uFD6B\",\n    \"/sheen.init_meem.medi_meem.medi\": \"\\uFD6D\",\n    \"/sheen.init_reh.fina\": \"\\uFD0D\",\n    \"/sheen.init_yeh.fina\": \"\\uFCFE\",\n    \"/sheen.isol\": \"\\uFEB5\",\n    \"/sheen.medi\": \"\\uFEB8\",\n    \"/sheen.medi_alefmaksura.fina\": \"\\uFD19\",\n    \"/sheen.medi_hah.fina\": \"\\uFD26\",\n    \"/sheen.medi_hah.medi\": \"\\uFD38\",\n    \"/sheen.medi_hah.medi_meem.fina\": \"\\uFD67\",\n    \"/sheen.medi_hah.medi_yeh.fina\": \"\\uFDAA\",\n    \"/sheen.medi_heh.medi\": \"\\uFCEA\",\n    \"/sheen.medi_jeem.fina\": \"\\uFD25\",\n    \"/sheen.medi_jeem.medi\": \"\\uFD37\",\n    \"/sheen.medi_jeem.medi_yeh.fina\": \"\\uFD69\",\n    \"/sheen.medi_khah.fina\": \"\\uFD27\",\n    \"/sheen.medi_khah.medi\": \"\\uFD39\",\n    \"/sheen.medi_meem.fina\": \"\\uFD28\",\n    \"/sheen.medi_meem.medi\": \"\\uFCE9\",\n    \"/sheen.medi_meem.medi_khah.fina\": \"\\uFD6A\",\n    \"/sheen.medi_meem.medi_meem.fina\": \"\\uFD6C\",\n    \"/sheen.medi_reh.fina\": \"\\uFD29\",\n    \"/sheen.medi_yeh.fina\": \"\\uFD1A\",\n    \"/sheenarabic\": \"\\u0634\",\n    \"/sheendotbelow\": \"\\u06FA\",\n    \"/sheenfinalarabic\": \"\\uFEB6\",\n    \"/sheeninitialarabic\": \"\\uFEB7\",\n    \"/sheenmedialarabic\": \"\\uFEB8\",\n    \"/sheep\": \"\\u1F411\",\n    \"/sheicoptic\": \"\\u03E3\",\n    \"/shelfmod\": \"\\u02FD\",\n    \"/shelfopenmod\": \"\\u02FE\",\n    \"/sheqel\": \"\\u20AA\",\n    \"/sheqelhebrew\": \"\\u20AA\",\n    \"/sheva\": \"\\u05B0\",\n    \"/sheva115\": \"\\u05B0\",\n    \"/sheva15\": \"\\u05B0\",\n    \"/sheva22\": \"\\u05B0\",\n    \"/sheva2e\": \"\\u05B0\",\n    \"/sheva:hb\": \"\\u05B0\",\n    \"/shevahebrew\": \"\\u05B0\",\n    \"/shevanarrowhebrew\": \"\\u05B0\",\n    \"/shevaquarterhebrew\": \"\\u05B0\",\n    \"/shevawidehebrew\": \"\\u05B0\",\n    \"/shhacyr\": \"\\u04BB\",\n    \"/shhacyrillic\": \"\\u04BB\",\n    \"/shhatailcyr\": \"\\u0527\",\n    \"/shield\": \"\\u1F6E1\",\n    \"/shimacoptic\": \"\\u03ED\",\n    \"/shin\": \"\\u05E9\",\n    \"/shin:hb\": \"\\u05E9\",\n    \"/shinDot:hb\": \"\\u05C1\",\n    \"/shindagesh\": \"\\uFB49\",\n    \"/shindageshhebrew\": \"\\uFB49\",\n    \"/shindageshshindot\": \"\\uFB2C\",\n    \"/shindageshshindothebrew\": \"\\uFB2C\",\n    \"/shindageshsindot\": \"\\uFB2D\",\n    \"/shindageshsindothebrew\": \"\\uFB2D\",\n    \"/shindothebrew\": \"\\u05C1\",\n    \"/shinhebrew\": \"\\u05E9\",\n    \"/shinshindot\": \"\\uFB2A\",\n    \"/shinshindothebrew\": \"\\uFB2A\",\n    \"/shinsindot\": \"\\uFB2B\",\n    \"/shinsindothebrew\": \"\\uFB2B\",\n    \"/shintoshrine\": \"\\u26E9\",\n    \"/shinwithdagesh:hb\": \"\\uFB49\",\n    \"/shinwithdageshandshinDot:hb\": \"\\uFB2C\",\n    \"/shinwithdageshandsinDot:hb\": \"\\uFB2D\",\n    \"/shinwithshinDot:hb\": \"\\uFB2A\",\n    \"/shinwithsinDot:hb\": \"\\uFB2B\",\n    \"/ship\": \"\\u1F6A2\",\n    \"/sho\": \"\\u03F8\",\n    \"/shoejotupfunc\": \"\\u235D\",\n    \"/shoestiledownfunc\": \"\\u2366\",\n    \"/shoestileleftfunc\": \"\\u2367\",\n    \"/shogipieceblack\": \"\\u2617\",\n    \"/shogipiecewhite\": \"\\u2616\",\n    \"/shook\": \"\\u0282\",\n    \"/shootingStar\": \"\\u1F320\",\n    \"/shoppingBags\": \"\\u1F6CD\",\n    \"/shoppingTrolley\": \"\\u1F6D2\",\n    \"/shortcake\": \"\\u1F370\",\n    \"/shortequalsmod\": \"\\uA78A\",\n    \"/shortoverlongmetrical\": \"\\u23D3\",\n    \"/shoulderedopenbox\": \"\\u237D\",\n    \"/shower\": \"\\u1F6BF\",\n    \"/shvsquare\": \"\\u1F1AA\",\n    \"/sicirclekatakana\": \"\\u32DB\",\n    \"/sidewaysBlackDownPointingIndex\": \"\\u1F5A1\",\n    \"/sidewaysBlackLeftPointingIndex\": \"\\u1F59A\",\n    \"/sidewaysBlackRightPointingIndex\": \"\\u1F59B\",\n    \"/sidewaysBlackUpPointingIndex\": \"\\u1F5A0\",\n    \"/sidewaysWhiteDownPointingIndex\": \"\\u1F59F\",\n    \"/sidewaysWhiteLeftPointingIndex\": \"\\u1F598\",\n    \"/sidewaysWhiteRightPointingIndex\": \"\\u1F599\",\n    \"/sidewaysWhiteUpPointingIndex\": \"\\u1F59E\",\n    \"/sigma\": \"\\u03C3\",\n    \"/sigma1\": \"\\u03C2\",\n    \"/sigmafinal\": \"\\u03C2\",\n    \"/sigmalunatedottedreversedsymbol\": \"\\u037D\",\n    \"/sigmalunatedottedsymbol\": \"\\u037C\",\n    \"/sigmalunatereversedsymbol\": \"\\u037B\",\n    \"/sigmalunatesymbol\": \"\\u03F2\",\n    \"/sigmalunatesymbolgreek\": \"\\u03F2\",\n    \"/sihiragana\": \"\\u3057\",\n    \"/sikatakana\": \"\\u30B7\",\n    \"/sikatakanahalfwidth\": \"\\uFF7C\",\n    \"/silhouetteOfJapan\": \"\\u1F5FE\",\n    \"/siluqhebrew\": \"\\u05BD\",\n    \"/siluqlefthebrew\": \"\\u05BD\",\n    \"/similar\": \"\\u223C\",\n    \"/sinDot:hb\": \"\\u05C2\",\n    \"/sindothebrew\": \"\\u05C2\",\n    \"/sinewave\": \"\\u223F\",\n    \"/sinh:a\": \"\\u0D85\",\n    \"/sinh:aa\": \"\\u0D86\",\n    \"/sinh:aae\": \"\\u0D88\",\n    \"/sinh:aaesign\": \"\\u0DD1\",\n    \"/sinh:aasign\": \"\\u0DCF\",\n    \"/sinh:ae\": \"\\u0D87\",\n    \"/sinh:aesign\": \"\\u0DD0\",\n    \"/sinh:ai\": \"\\u0D93\",\n    \"/sinh:aisign\": \"\\u0DDB\",\n    \"/sinh:anusvara\": \"\\u0D82\",\n    \"/sinh:au\": \"\\u0D96\",\n    \"/sinh:ausign\": \"\\u0DDE\",\n    \"/sinh:ba\": \"\\u0DB6\",\n    \"/sinh:bha\": \"\\u0DB7\",\n    \"/sinh:ca\": \"\\u0DA0\",\n    \"/sinh:cha\": \"\\u0DA1\",\n    \"/sinh:da\": \"\\u0DAF\",\n    \"/sinh:dda\": \"\\u0DA9\",\n    \"/sinh:ddha\": \"\\u0DAA\",\n    \"/sinh:dha\": \"\\u0DB0\",\n    \"/sinh:e\": \"\\u0D91\",\n    \"/sinh:ee\": \"\\u0D92\",\n    \"/sinh:eesign\": \"\\u0DDA\",\n    \"/sinh:esign\": \"\\u0DD9\",\n    \"/sinh:fa\": \"\\u0DC6\",\n    \"/sinh:ga\": \"\\u0D9C\",\n    \"/sinh:gha\": \"\\u0D9D\",\n    \"/sinh:ha\": \"\\u0DC4\",\n    \"/sinh:i\": \"\\u0D89\",\n    \"/sinh:ii\": \"\\u0D8A\",\n    \"/sinh:iisign\": \"\\u0DD3\",\n    \"/sinh:isign\": \"\\u0DD2\",\n    \"/sinh:ja\": \"\\u0DA2\",\n    \"/sinh:jha\": \"\\u0DA3\",\n    \"/sinh:jnya\": \"\\u0DA5\",\n    \"/sinh:ka\": \"\\u0D9A\",\n    \"/sinh:kha\": \"\\u0D9B\",\n    \"/sinh:kunddaliya\": \"\\u0DF4\",\n    \"/sinh:la\": \"\\u0DBD\",\n    \"/sinh:litheight\": \"\\u0DEE\",\n    \"/sinh:lithfive\": \"\\u0DEB\",\n    \"/sinh:lithfour\": \"\\u0DEA\",\n    \"/sinh:lithnine\": \"\\u0DEF\",\n    \"/sinh:lithone\": \"\\u0DE7\",\n    \"/sinh:lithseven\": \"\\u0DED\",\n    \"/sinh:lithsix\": \"\\u0DEC\",\n    \"/sinh:liththree\": \"\\u0DE9\",\n    \"/sinh:lithtwo\": \"\\u0DE8\",\n    \"/sinh:lithzero\": \"\\u0DE6\",\n    \"/sinh:lla\": \"\\u0DC5\",\n    \"/sinh:llvocal\": \"\\u0D90\",\n    \"/sinh:llvocalsign\": \"\\u0DF3\",\n    \"/sinh:lvocal\": \"\\u0D8F\",\n    \"/sinh:lvocalsign\": \"\\u0DDF\",\n    \"/sinh:ma\": \"\\u0DB8\",\n    \"/sinh:mba\": \"\\u0DB9\",\n    \"/sinh:na\": \"\\u0DB1\",\n    \"/sinh:nda\": \"\\u0DB3\",\n    \"/sinh:nga\": \"\\u0D9E\",\n    \"/sinh:nna\": \"\\u0DAB\",\n    \"/sinh:nndda\": \"\\u0DAC\",\n    \"/sinh:nnga\": \"\\u0D9F\",\n    \"/sinh:nya\": \"\\u0DA4\",\n    \"/sinh:nyja\": \"\\u0DA6\",\n    \"/sinh:o\": \"\\u0D94\",\n    \"/sinh:oo\": \"\\u0D95\",\n    \"/sinh:oosign\": \"\\u0DDD\",\n    \"/sinh:osign\": \"\\u0DDC\",\n    \"/sinh:pa\": \"\\u0DB4\",\n    \"/sinh:pha\": \"\\u0DB5\",\n    \"/sinh:ra\": \"\\u0DBB\",\n    \"/sinh:rrvocal\": \"\\u0D8E\",\n    \"/sinh:rrvocalsign\": \"\\u0DF2\",\n    \"/sinh:rvocal\": \"\\u0D8D\",\n    \"/sinh:rvocalsign\": \"\\u0DD8\",\n    \"/sinh:sa\": \"\\u0DC3\",\n    \"/sinh:sha\": \"\\u0DC1\",\n    \"/sinh:ssa\": \"\\u0DC2\",\n    \"/sinh:ta\": \"\\u0DAD\",\n    \"/sinh:tha\": \"\\u0DAE\",\n    \"/sinh:tta\": \"\\u0DA7\",\n    \"/sinh:ttha\": \"\\u0DA8\",\n    \"/sinh:u\": \"\\u0D8B\",\n    \"/sinh:usign\": \"\\u0DD4\",\n    \"/sinh:uu\": \"\\u0D8C\",\n    \"/sinh:uusign\": \"\\u0DD6\",\n    \"/sinh:va\": \"\\u0DC0\",\n    \"/sinh:virama\": \"\\u0DCA\",\n    \"/sinh:visarga\": \"\\u0D83\",\n    \"/sinh:ya\": \"\\u0DBA\",\n    \"/sinologicaldot\": \"\\uA78F\",\n    \"/sinsular\": \"\\uA785\",\n    \"/siosacirclekorean\": \"\\u3274\",\n    \"/siosaparenkorean\": \"\\u3214\",\n    \"/sioscieuckorean\": \"\\u317E\",\n    \"/sioscirclekorean\": \"\\u3266\",\n    \"/sioskiyeokkorean\": \"\\u317A\",\n    \"/sioskorean\": \"\\u3145\",\n    \"/siosnieunkorean\": \"\\u317B\",\n    \"/siosparenkorean\": \"\\u3206\",\n    \"/siospieupkorean\": \"\\u317D\",\n    \"/siostikeutkorean\": \"\\u317C\",\n    \"/siringusquare\": \"\\u3321\",\n    \"/six\": \"\\u0036\",\n    \"/six.inferior\": \"\\u2086\",\n    \"/six.roman\": \"\\u2165\",\n    \"/six.romansmall\": \"\\u2175\",\n    \"/six.superior\": \"\\u2076\",\n    \"/sixPointedStarMiddleDot\": \"\\u1F52F\",\n    \"/sixarabic\": \"\\u0666\",\n    \"/sixbengali\": \"\\u09EC\",\n    \"/sixcircle\": \"\\u2465\",\n    \"/sixcircledbl\": \"\\u24FA\",\n    \"/sixcircleinversesansserif\": \"\\u278F\",\n    \"/sixcomma\": \"\\u1F107\",\n    \"/sixdeva\": \"\\u096C\",\n    \"/sixdotsvertical\": \"\\u2E3D\",\n    \"/sixfar\": \"\\u06F6\",\n    \"/sixgujarati\": \"\\u0AEC\",\n    \"/sixgurmukhi\": \"\\u0A6C\",\n    \"/sixhackarabic\": \"\\u0666\",\n    \"/sixhangzhou\": \"\\u3026\",\n    \"/sixideographiccircled\": \"\\u3285\",\n    \"/sixideographicparen\": \"\\u3225\",\n    \"/sixinferior\": \"\\u2086\",\n    \"/sixlateform.roman\": \"\\u2185\",\n    \"/sixmonospace\": \"\\uFF16\",\n    \"/sixoldstyle\": \"\\uF736\",\n    \"/sixparen\": \"\\u2479\",\n    \"/sixparenthesized\": \"\\u2479\",\n    \"/sixperemspace\": \"\\u2006\",\n    \"/sixperiod\": \"\\u248D\",\n    \"/sixpersian\": \"\\u06F6\",\n    \"/sixroman\": \"\\u2175\",\n    \"/sixsuperior\": \"\\u2076\",\n    \"/sixteencircle\": \"\\u246F\",\n    \"/sixteencircleblack\": \"\\u24F0\",\n    \"/sixteencurrencydenominatorbengali\": \"\\u09F9\",\n    \"/sixteenparen\": \"\\u2483\",\n    \"/sixteenparenthesized\": \"\\u2483\",\n    \"/sixteenperiod\": \"\\u2497\",\n    \"/sixthai\": \"\\u0E56\",\n    \"/sixtycirclesquare\": \"\\u324D\",\n    \"/sixtypsquare\": \"\\u1F1A3\",\n    \"/sjekomicyr\": \"\\u050D\",\n    \"/skiAndSkiBoot\": \"\\u1F3BF\",\n    \"/skier\": \"\\u26F7\",\n    \"/skull\": \"\\u1F480\",\n    \"/skullcrossbones\": \"\\u2620\",\n    \"/slash\": \"\\u002F\",\n    \"/slashbarfunc\": \"\\u233F\",\n    \"/slashmonospace\": \"\\uFF0F\",\n    \"/sled\": \"\\u1F6F7\",\n    \"/sleeping\": \"\\u1F4A4\",\n    \"/sleepingAccommodation\": \"\\u1F6CC\",\n    \"/sleepingFace\": \"\\u1F634\",\n    \"/sleepyFace\": \"\\u1F62A\",\n    \"/sleuthOrSpy\": \"\\u1F575\",\n    \"/sliceOfPizza\": \"\\u1F355\",\n    \"/slightlyFrowningFace\": \"\\u1F641\",\n    \"/slightlySmilingFace\": \"\\u1F642\",\n    \"/slong\": \"\\u017F\",\n    \"/slongdotaccent\": \"\\u1E9B\",\n    \"/slope\": \"\\u2333\",\n    \"/slotMachine\": \"\\u1F3B0\",\n    \"/smallAirplane\": \"\\u1F6E9\",\n    \"/smallBlueDiamond\": \"\\u1F539\",\n    \"/smallOrangeDiamond\": \"\\u1F538\",\n    \"/smallRedTriangleDOwn\": \"\\u1F53D\",\n    \"/smallRedTriangleUp\": \"\\u1F53C\",\n    \"/smile\": \"\\u2323\",\n    \"/smileface\": \"\\u263A\",\n    \"/smilingCatFaceWithHeartShapedEyes\": \"\\u1F63B\",\n    \"/smilingCatFaceWithOpenMouth\": \"\\u1F63A\",\n    \"/smilingFaceWithHalo\": \"\\u1F607\",\n    \"/smilingFaceWithHeartShapedEyes\": \"\\u1F60D\",\n    \"/smilingFaceWithHorns\": \"\\u1F608\",\n    \"/smilingFaceWithOpenMouth\": \"\\u1F603\",\n    \"/smilingFaceWithOpenMouthAndColdSweat\": \"\\u1F605\",\n    \"/smilingFaceWithOpenMouthAndSmilingEyes\": \"\\u1F604\",\n    \"/smilingFaceWithOpenMouthAndTightlyClosedEyes\": \"\\u1F606\",\n    \"/smilingFaceWithSmilingEyes\": \"\\u1F60A\",\n    \"/smilingFaceWithSunglasses\": \"\\u1F60E\",\n    \"/smilingfaceblack\": \"\\u263B\",\n    \"/smilingfacewhite\": \"\\u263A\",\n    \"/smirkingFace\": \"\\u1F60F\",\n    \"/smll:ampersand\": \"\\uFE60\",\n    \"/smll:asterisk\": \"\\uFE61\",\n    \"/smll:backslash\": \"\\uFE68\",\n    \"/smll:braceleft\": \"\\uFE5B\",\n    \"/smll:braceright\": \"\\uFE5C\",\n    \"/smll:colon\": \"\\uFE55\",\n    \"/smll:comma\": \"\\uFE50\",\n    \"/smll:dollar\": \"\\uFE69\",\n    \"/smll:emdash\": \"\\uFE58\",\n    \"/smll:equal\": \"\\uFE66\",\n    \"/smll:exclam\": \"\\uFE57\",\n    \"/smll:greater\": \"\\uFE65\",\n    \"/smll:hyphen\": \"\\uFE63\",\n    \"/smll:ideographiccomma\": \"\\uFE51\",\n    \"/smll:less\": \"\\uFE64\",\n    \"/smll:numbersign\": \"\\uFE5F\",\n    \"/smll:parenthesisleft\": \"\\uFE59\",\n    \"/smll:parenthesisright\": \"\\uFE5A\",\n    \"/smll:percent\": \"\\uFE6A\",\n    \"/smll:period\": \"\\uFE52\",\n    \"/smll:plus\": \"\\uFE62\",\n    \"/smll:question\": \"\\uFE56\",\n    \"/smll:semicolon\": \"\\uFE54\",\n    \"/smll:tortoiseshellbracketleft\": \"\\uFE5D\",\n    \"/smll:tortoiseshellbracketright\": \"\\uFE5E\",\n    \"/smoking\": \"\\u1F6AC\",\n    \"/smonospace\": \"\\uFF53\",\n    \"/snail\": \"\\u1F40C\",\n    \"/snake\": \"\\u1F40D\",\n    \"/snowboarder\": \"\\u1F3C2\",\n    \"/snowcappedMountain\": \"\\u1F3D4\",\n    \"/snowman\": \"\\u2603\",\n    \"/snowmanblack\": \"\\u26C7\",\n    \"/snowmanoutsnow\": \"\\u26C4\",\n    \"/sobliquestroke\": \"\\uA7A9\",\n    \"/soccerball\": \"\\u26BD\",\n    \"/societyideographiccircled\": \"\\u3293\",\n    \"/societyideographicparen\": \"\\u3233\",\n    \"/socirclekatakana\": \"\\u32DE\",\n    \"/sofPasuq:hb\": \"\\u05C3\",\n    \"/sofpasuqhebrew\": \"\\u05C3\",\n    \"/softIceCream\": \"\\u1F366\",\n    \"/softShellFloppyDisk\": \"\\u1F5AC\",\n    \"/softcyr\": \"\\u044C\",\n    \"/softhyphen\": \"\\u00AD\",\n    \"/softsigncyrillic\": \"\\u044C\",\n    \"/softwarefunction\": \"\\u2394\",\n    \"/sohiragana\": \"\\u305D\",\n    \"/sokatakana\": \"\\u30BD\",\n    \"/sokatakanahalfwidth\": \"\\uFF7F\",\n    \"/soliduslongoverlaycmb\": \"\\u0338\",\n    \"/solidusshortoverlaycmb\": \"\\u0337\",\n    \"/solidussubsetreversepreceding\": \"\\u27C8\",\n    \"/solidussupersetpreceding\": \"\\u27C9\",\n    \"/soonRightwardsArrowAbove\": \"\\u1F51C\",\n    \"/sorusithai\": \"\\u0E29\",\n    \"/sosalathai\": \"\\u0E28\",\n    \"/sosothai\": \"\\u0E0B\",\n    \"/sossquare\": \"\\u1F198\",\n    \"/sosuathai\": \"\\u0E2A\",\n    \"/soundcopyright\": \"\\u2117\",\n    \"/space\": \"\\u0020\",\n    \"/spacehackarabic\": \"\\u0020\",\n    \"/spade\": \"\\u2660\",\n    \"/spadeblack\": \"\\u2660\",\n    \"/spadesuitblack\": \"\\u2660\",\n    \"/spadesuitwhite\": \"\\u2664\",\n    \"/spadewhite\": \"\\u2664\",\n    \"/spaghetti\": \"\\u1F35D\",\n    \"/sparen\": \"\\u24AE\",\n    \"/sparenthesized\": \"\\u24AE\",\n    \"/sparklingHeart\": \"\\u1F496\",\n    \"/speakNoEvilMonkey\": \"\\u1F64A\",\n    \"/speaker\": \"\\u1F508\",\n    \"/speakerCancellationStroke\": \"\\u1F507\",\n    \"/speakerOneSoundWave\": \"\\u1F509\",\n    \"/speakerThreeSoundWaves\": \"\\u1F50A\",\n    \"/speakingHeadInSilhouette\": \"\\u1F5E3\",\n    \"/specialideographiccircled\": \"\\u3295\",\n    \"/specialideographicparen\": \"\\u3235\",\n    \"/speechBalloon\": \"\\u1F4AC\",\n    \"/speedboat\": \"\\u1F6A4\",\n    \"/spesmilo\": \"\\u20B7\",\n    \"/sphericalangle\": \"\\u2222\",\n    \"/spider\": \"\\u1F577\",\n    \"/spiderWeb\": \"\\u1F578\",\n    \"/spiralCalendarPad\": \"\\u1F5D3\",\n    \"/spiralNotePad\": \"\\u1F5D2\",\n    \"/spiralShell\": \"\\u1F41A\",\n    \"/splashingSweat\": \"\\u1F4A6\",\n    \"/sportsMedal\": \"\\u1F3C5\",\n    \"/spoutingWhale\": \"\\u1F433\",\n    \"/sppl:tildevertical\": \"\\u2E2F\",\n    \"/squarebelowcmb\": \"\\u033B\",\n    \"/squareblack\": \"\\u25A0\",\n    \"/squarebracketleftvertical\": \"\\uFE47\",\n    \"/squarebracketrightvertical\": \"\\uFE48\",\n    \"/squarecap\": \"\\u2293\",\n    \"/squarecc\": \"\\u33C4\",\n    \"/squarecm\": \"\\u339D\",\n    \"/squarecup\": \"\\u2294\",\n    \"/squareddotoperator\": \"\\u22A1\",\n    \"/squarediagonalcrosshatchfill\": \"\\u25A9\",\n    \"/squaredj\": \"\\u1F190\",\n    \"/squaredkey\": \"\\u26BF\",\n    \"/squaredminus\": \"\\u229F\",\n    \"/squaredplus\": \"\\u229E\",\n    \"/squaredsaltire\": \"\\u26DD\",\n    \"/squaredtimes\": \"\\u22A0\",\n    \"/squarefourcorners\": \"\\u26F6\",\n    \"/squarehalfleftblack\": \"\\u25E7\",\n    \"/squarehalfrightblack\": \"\\u25E8\",\n    \"/squarehorizontalfill\": \"\\u25A4\",\n    \"/squareimage\": \"\\u228F\",\n    \"/squareimageorequal\": \"\\u2291\",\n    \"/squareimageornotequal\": \"\\u22E4\",\n    \"/squarekg\": \"\\u338F\",\n    \"/squarekm\": \"\\u339E\",\n    \"/squarekmcapital\": \"\\u33CE\",\n    \"/squareln\": \"\\u33D1\",\n    \"/squarelog\": \"\\u33D2\",\n    \"/squarelowerdiagonalhalfrightblack\": \"\\u25EA\",\n    \"/squaremediumblack\": \"\\u25FC\",\n    \"/squaremediumwhite\": \"\\u25FB\",\n    \"/squaremg\": \"\\u338E\",\n    \"/squaremil\": \"\\u33D5\",\n    \"/squaremm\": \"\\u339C\",\n    \"/squaremsquared\": \"\\u33A1\",\n    \"/squareoriginal\": \"\\u2290\",\n    \"/squareoriginalorequal\": \"\\u2292\",\n    \"/squareoriginalornotequal\": \"\\u22E5\",\n    \"/squareorthogonalcrosshatchfill\": \"\\u25A6\",\n    \"/squareraised\": \"\\u2E0B\",\n    \"/squaresmallblack\": \"\\u25AA\",\n    \"/squaresmallmediumblack\": \"\\u25FE\",\n    \"/squaresmallmediumwhite\": \"\\u25FD\",\n    \"/squaresmallwhite\": \"\\u25AB\",\n    \"/squareupperdiagonalhalfleftblack\": \"\\u25E9\",\n    \"/squareupperlefttolowerrightfill\": \"\\u25A7\",\n    \"/squareupperrighttolowerleftfill\": \"\\u25A8\",\n    \"/squareverticalfill\": \"\\u25A5\",\n    \"/squarewhite\": \"\\u25A1\",\n    \"/squarewhitebisectinglinevertical\": \"\\u25EB\",\n    \"/squarewhitelowerquadrantleft\": \"\\u25F1\",\n    \"/squarewhitelowerquadrantright\": \"\\u25F2\",\n    \"/squarewhiteround\": \"\\u25A2\",\n    \"/squarewhiteupperquadrantleft\": \"\\u25F0\",\n    \"/squarewhiteupperquadrantright\": \"\\u25F3\",\n    \"/squarewhitewithsmallblack\": \"\\u25A3\",\n    \"/squarewhitewithsquaresmallblack\": \"\\u25A3\",\n    \"/squishquadfunc\": \"\\u2337\",\n    \"/srfullwidth\": \"\\u33DB\",\n    \"/srsquare\": \"\\u33DB\",\n    \"/ssabengali\": \"\\u09B7\",\n    \"/ssadeva\": \"\\u0937\",\n    \"/ssagujarati\": \"\\u0AB7\",\n    \"/ssangcieuckorean\": \"\\u3149\",\n    \"/ssanghieuhkorean\": \"\\u3185\",\n    \"/ssangieungkorean\": \"\\u3180\",\n    \"/ssangkiyeokkorean\": \"\\u3132\",\n    \"/ssangnieunkorean\": \"\\u3165\",\n    \"/ssangpieupkorean\": \"\\u3143\",\n    \"/ssangsioskorean\": \"\\u3146\",\n    \"/ssangtikeutkorean\": \"\\u3138\",\n    \"/ssuperior\": \"\\uF6F2\",\n    \"/ssupmod\": \"\\u02E2\",\n    \"/sswashtail\": \"\\u023F\",\n    \"/stackedcommadbl\": \"\\u2E49\",\n    \"/stadium\": \"\\u1F3DF\",\n    \"/staffofaesculapius\": \"\\u2695\",\n    \"/staffofhermes\": \"\\u269A\",\n    \"/stampedEnvelope\": \"\\u1F583\",\n    \"/star\": \"\\u22C6\",\n    \"/starblack\": \"\\u2605\",\n    \"/starcrescent\": \"\\u262A\",\n    \"/stardiaeresisfunc\": \"\\u2363\",\n    \"/starequals\": \"\\u225B\",\n    \"/staroperator\": \"\\u22C6\",\n    \"/staroutlinedwhite\": \"\\u269D\",\n    \"/starwhite\": \"\\u2606\",\n    \"/station\": \"\\u1F689\",\n    \"/statueOfLiberty\": \"\\u1F5FD\",\n    \"/steamLocomotive\": \"\\u1F682\",\n    \"/steamingBowl\": \"\\u1F35C\",\n    \"/stenographicfullstop\": \"\\u2E3C\",\n    \"/sterling\": \"\\u00A3\",\n    \"/sterlingmonospace\": \"\\uFFE1\",\n    \"/stigma\": \"\\u03DB\",\n    \"/stiletildefunc\": \"\\u236D\",\n    \"/stockChart\": \"\\u1F5E0\",\n    \"/stockideographiccircled\": \"\\u3291\",\n    \"/stockideographicparen\": \"\\u3231\",\n    \"/stopabove\": \"\\u06EB\",\n    \"/stopbelow\": \"\\u06EA\",\n    \"/straightRuler\": \"\\u1F4CF\",\n    \"/straightness\": \"\\u23E4\",\n    \"/strawberry\": \"\\u1F353\",\n    \"/stresslowtonemod\": \"\\uA721\",\n    \"/stresstonemod\": \"\\uA720\",\n    \"/strictlyequivalent\": \"\\u2263\",\n    \"/strokelongoverlaycmb\": \"\\u0336\",\n    \"/strokeshortoverlaycmb\": \"\\u0335\",\n    \"/studioMicrophone\": \"\\u1F399\",\n    \"/studyideographiccircled\": \"\\u32AB\",\n    \"/studyideographicparen\": \"\\u323B\",\n    \"/stupa\": \"\\u1F6D3\",\n    \"/subscriptalef\": \"\\u0656\",\n    \"/subset\": \"\\u2282\",\n    \"/subsetdbl\": \"\\u22D0\",\n    \"/subsetnotequal\": \"\\u228A\",\n    \"/subsetorequal\": \"\\u2286\",\n    \"/succeeds\": \"\\u227B\",\n    \"/succeedsbutnotequivalent\": \"\\u22E9\",\n    \"/succeedsorequal\": \"\\u227D\",\n    \"/succeedsorequivalent\": \"\\u227F\",\n    \"/succeedsunderrelation\": \"\\u22B1\",\n    \"/suchthat\": \"\\u220B\",\n    \"/sucirclekatakana\": \"\\u32DC\",\n    \"/suhiragana\": \"\\u3059\",\n    \"/suitableideographiccircled\": \"\\u329C\",\n    \"/sukatakana\": \"\\u30B9\",\n    \"/sukatakanahalfwidth\": \"\\uFF7D\",\n    \"/sukumendutvowel\": \"\\uA9B9\",\n    \"/sukunIsol\": \"\\uFE7E\",\n    \"/sukunMedi\": \"\\uFE7F\",\n    \"/sukunarabic\": \"\\u0652\",\n    \"/sukuvowel\": \"\\uA9B8\",\n    \"/summation\": \"\\u2211\",\n    \"/summationbottom\": \"\\u23B3\",\n    \"/summationdblstruck\": \"\\u2140\",\n    \"/summationtop\": \"\\u23B2\",\n    \"/sun\": \"\\u263C\",\n    \"/sunFace\": \"\\u1F31E\",\n    \"/sunbehindcloud\": \"\\u26C5\",\n    \"/sunflower\": \"\\u1F33B\",\n    \"/sunideographiccircled\": \"\\u3290\",\n    \"/sunideographicparen\": \"\\u3230\",\n    \"/sunraysblack\": \"\\u2600\",\n    \"/sunrayswhite\": \"\\u263C\",\n    \"/sunrise\": \"\\u1F305\",\n    \"/sunriseOverMountains\": \"\\u1F304\",\n    \"/sunsetOverBuildings\": \"\\u1F307\",\n    \"/superset\": \"\\u2283\",\n    \"/supersetnotequal\": \"\\u228B\",\n    \"/supersetorequal\": \"\\u2287\",\n    \"/superviseideographiccircled\": \"\\u32AC\",\n    \"/superviseideographicparen\": \"\\u323C\",\n    \"/surfer\": \"\\u1F3C4\",\n    \"/sushi\": \"\\u1F363\",\n    \"/suspensionRailway\": \"\\u1F69F\",\n    \"/suspensiondbl\": \"\\u2E44\",\n    \"/svfullwidth\": \"\\u33DC\",\n    \"/svsquare\": \"\\u33DC\",\n    \"/swatchtop\": \"\\u23F1\",\n    \"/swimmer\": \"\\u1F3CA\",\n    \"/swungdash\": \"\\u2053\",\n    \"/symbolabovethreedotsabove\": \"\\uFBB6\",\n    \"/symbolbelowthreedotsabove\": \"\\uFBB7\",\n    \"/symboldotabove\": \"\\uFBB2\",\n    \"/symboldotbelow\": \"\\uFBB3\",\n    \"/symboldoubleverticalbarbelow\": \"\\uFBBC\",\n    \"/symbolfourdotsabove\": \"\\uFBBA\",\n    \"/symbolfourdotsbelow\": \"\\uFBBB\",\n    \"/symbolpointingabovedownthreedotsabove\": \"\\uFBB8\",\n    \"/symbolpointingbelowdownthreedotsabove\": \"\\uFBB9\",\n    \"/symbolring\": \"\\uFBBF\",\n    \"/symboltahabovesmall\": \"\\uFBC0\",\n    \"/symboltahbelowsmall\": \"\\uFBC1\",\n    \"/symboltwodotsabove\": \"\\uFBB4\",\n    \"/symboltwodotsbelow\": \"\\uFBB5\",\n    \"/symboltwodotsverticallyabove\": \"\\uFBBD\",\n    \"/symboltwodotsverticallybelow\": \"\\uFBBE\",\n    \"/symmetry\": \"\\u232F\",\n    \"/synagogue\": \"\\u1F54D\",\n    \"/syouwaerasquare\": \"\\u337C\",\n    \"/syringe\": \"\\u1F489\",\n    \"/t\": \"\\u0074\",\n    \"/t-shirt\": \"\\u1F455\",\n    \"/t.inferior\": \"\\u209C\",\n    \"/tabengali\": \"\\u09A4\",\n    \"/tableTennisPaddleAndBall\": \"\\u1F3D3\",\n    \"/tacirclekatakana\": \"\\u32DF\",\n    \"/tackcircleaboveup\": \"\\u27DF\",\n    \"/tackdiaeresisupfunc\": \"\\u2361\",\n    \"/tackdown\": \"\\u22A4\",\n    \"/tackdownmod\": \"\\u02D5\",\n    \"/tackjotdownfunc\": \"\\u234E\",\n    \"/tackjotupfunc\": \"\\u2355\",\n    \"/tackleft\": \"\\u22A3\",\n    \"/tackleftright\": \"\\u27DB\",\n    \"/tackoverbarupfunc\": \"\\u2351\",\n    \"/tackright\": \"\\u22A2\",\n    \"/tackunderlinedownfunc\": \"\\u234A\",\n    \"/tackup\": \"\\u22A5\",\n    \"/tackupmod\": \"\\u02D4\",\n    \"/taco\": \"\\u1F32E\",\n    \"/tadeva\": \"\\u0924\",\n    \"/tagujarati\": \"\\u0AA4\",\n    \"/tagurmukhi\": \"\\u0A24\",\n    \"/tah\": \"\\u0637\",\n    \"/tah.fina\": \"\\uFEC2\",\n    \"/tah.init\": \"\\uFEC3\",\n    \"/tah.init_alefmaksura.fina\": \"\\uFCF5\",\n    \"/tah.init_hah.fina\": \"\\uFC26\",\n    \"/tah.init_hah.medi\": \"\\uFCB8\",\n    \"/tah.init_meem.fina\": \"\\uFC27\",\n    \"/tah.init_meem.medi\": \"\\uFD33\",\n    \"/tah.init_meem.medi_hah.medi\": \"\\uFD72\",\n    \"/tah.init_meem.medi_meem.medi\": \"\\uFD73\",\n    \"/tah.init_yeh.fina\": \"\\uFCF6\",\n    \"/tah.isol\": \"\\uFEC1\",\n    \"/tah.medi\": \"\\uFEC4\",\n    \"/tah.medi_alefmaksura.fina\": \"\\uFD11\",\n    \"/tah.medi_meem.medi\": \"\\uFD3A\",\n    \"/tah.medi_meem.medi_hah.fina\": \"\\uFD71\",\n    \"/tah.medi_meem.medi_yeh.fina\": \"\\uFD74\",\n    \"/tah.medi_yeh.fina\": \"\\uFD12\",\n    \"/tahabove\": \"\\u0615\",\n    \"/taharabic\": \"\\u0637\",\n    \"/tahfinalarabic\": \"\\uFEC2\",\n    \"/tahinitialarabic\": \"\\uFEC3\",\n    \"/tahiragana\": \"\\u305F\",\n    \"/tahmedialarabic\": \"\\uFEC4\",\n    \"/tahthreedotsabove\": \"\\u069F\",\n    \"/taisyouerasquare\": \"\\u337D\",\n    \"/takatakana\": \"\\u30BF\",\n    \"/takatakanahalfwidth\": \"\\uFF80\",\n    \"/takhallus\": \"\\u0614\",\n    \"/talingvowel\": \"\\uA9BA\",\n    \"/taml:a\": \"\\u0B85\",\n    \"/taml:aa\": \"\\u0B86\",\n    \"/taml:aasign\": \"\\u0BBE\",\n    \"/taml:ai\": \"\\u0B90\",\n    \"/taml:aisign\": \"\\u0BC8\",\n    \"/taml:anusvarasign\": \"\\u0B82\",\n    \"/taml:asabovesign\": \"\\u0BF8\",\n    \"/taml:au\": \"\\u0B94\",\n    \"/taml:aulengthmark\": \"\\u0BD7\",\n    \"/taml:ausign\": \"\\u0BCC\",\n    \"/taml:ca\": \"\\u0B9A\",\n    \"/taml:creditsign\": \"\\u0BF7\",\n    \"/taml:daysign\": \"\\u0BF3\",\n    \"/taml:debitsign\": \"\\u0BF6\",\n    \"/taml:e\": \"\\u0B8E\",\n    \"/taml:ee\": \"\\u0B8F\",\n    \"/taml:eesign\": \"\\u0BC7\",\n    \"/taml:eight\": \"\\u0BEE\",\n    \"/taml:esign\": \"\\u0BC6\",\n    \"/taml:five\": \"\\u0BEB\",\n    \"/taml:four\": \"\\u0BEA\",\n    \"/taml:ha\": \"\\u0BB9\",\n    \"/taml:i\": \"\\u0B87\",\n    \"/taml:ii\": \"\\u0B88\",\n    \"/taml:iisign\": \"\\u0BC0\",\n    \"/taml:isign\": \"\\u0BBF\",\n    \"/taml:ja\": \"\\u0B9C\",\n    \"/taml:ka\": \"\\u0B95\",\n    \"/taml:la\": \"\\u0BB2\",\n    \"/taml:lla\": \"\\u0BB3\",\n    \"/taml:llla\": \"\\u0BB4\",\n    \"/taml:ma\": \"\\u0BAE\",\n    \"/taml:monthsign\": \"\\u0BF4\",\n    \"/taml:na\": \"\\u0BA8\",\n    \"/taml:nga\": \"\\u0B99\",\n    \"/taml:nine\": \"\\u0BEF\",\n    \"/taml:nna\": \"\\u0BA3\",\n    \"/taml:nnna\": \"\\u0BA9\",\n    \"/taml:nya\": \"\\u0B9E\",\n    \"/taml:o\": \"\\u0B92\",\n    \"/taml:om\": \"\\u0BD0\",\n    \"/taml:one\": \"\\u0BE7\",\n    \"/taml:onehundred\": \"\\u0BF1\",\n    \"/taml:onethousand\": \"\\u0BF2\",\n    \"/taml:oo\": \"\\u0B93\",\n    \"/taml:oosign\": \"\\u0BCB\",\n    \"/taml:osign\": \"\\u0BCA\",\n    \"/taml:pa\": \"\\u0BAA\",\n    \"/taml:ra\": \"\\u0BB0\",\n    \"/taml:rra\": \"\\u0BB1\",\n    \"/taml:rupeesign\": \"\\u0BF9\",\n    \"/taml:sa\": \"\\u0BB8\",\n    \"/taml:seven\": \"\\u0BED\",\n    \"/taml:sha\": \"\\u0BB6\",\n    \"/taml:sign\": \"\\u0BFA\",\n    \"/taml:six\": \"\\u0BEC\",\n    \"/taml:ssa\": \"\\u0BB7\",\n    \"/taml:ta\": \"\\u0BA4\",\n    \"/taml:ten\": \"\\u0BF0\",\n    \"/taml:three\": \"\\u0BE9\",\n    \"/taml:tta\": \"\\u0B9F\",\n    \"/taml:two\": \"\\u0BE8\",\n    \"/taml:u\": \"\\u0B89\",\n    \"/taml:usign\": \"\\u0BC1\",\n    \"/taml:uu\": \"\\u0B8A\",\n    \"/taml:uusign\": \"\\u0BC2\",\n    \"/taml:va\": \"\\u0BB5\",\n    \"/taml:viramasign\": \"\\u0BCD\",\n    \"/taml:visargasign\": \"\\u0B83\",\n    \"/taml:ya\": \"\\u0BAF\",\n    \"/taml:yearsign\": \"\\u0BF5\",\n    \"/taml:zero\": \"\\u0BE6\",\n    \"/tamurda\": \"\\uA9A1\",\n    \"/tanabataTree\": \"\\u1F38B\",\n    \"/tangerine\": \"\\u1F34A\",\n    \"/tapeCartridge\": \"\\u1F5AD\",\n    \"/tarungvowel\": \"\\uA9B4\",\n    \"/tatweelFathatanAbove\": \"\\uFE71\",\n    \"/tatweelarabic\": \"\\u0640\",\n    \"/tau\": \"\\u03C4\",\n    \"/taurus\": \"\\u2649\",\n    \"/tav\": \"\\u05EA\",\n    \"/tav:hb\": \"\\u05EA\",\n    \"/tavdages\": \"\\uFB4A\",\n    \"/tavdagesh\": \"\\uFB4A\",\n    \"/tavdageshhebrew\": \"\\uFB4A\",\n    \"/tavhebrew\": \"\\u05EA\",\n    \"/tavwide:hb\": \"\\uFB28\",\n    \"/tavwithdagesh:hb\": \"\\uFB4A\",\n    \"/taxi\": \"\\u1F695\",\n    \"/tbar\": \"\\u0167\",\n    \"/tbopomofo\": \"\\u310A\",\n    \"/tcaron\": \"\\u0165\",\n    \"/tccurl\": \"\\u02A8\",\n    \"/tcedilla\": \"\\u0163\",\n    \"/tcheh\": \"\\u0686\",\n    \"/tcheh.fina\": \"\\uFB7B\",\n    \"/tcheh.init\": \"\\uFB7C\",\n    \"/tcheh.isol\": \"\\uFB7A\",\n    \"/tcheh.medi\": \"\\uFB7D\",\n    \"/tcheharabic\": \"\\u0686\",\n    \"/tchehdotabove\": \"\\u06BF\",\n    \"/tcheheh\": \"\\u0687\",\n    \"/tcheheh.fina\": \"\\uFB7F\",\n    \"/tcheheh.init\": \"\\uFB80\",\n    \"/tcheheh.isol\": \"\\uFB7E\",\n    \"/tcheheh.medi\": \"\\uFB81\",\n    \"/tchehfinalarabic\": \"\\uFB7B\",\n    \"/tchehinitialarabic\": \"\\uFB7C\",\n    \"/tchehmedialarabic\": \"\\uFB7D\",\n    \"/tchehmeeminitialarabic\": \"\\uFB7C\",\n    \"/tcircle\": \"\\u24E3\",\n    \"/tcircumflexbelow\": \"\\u1E71\",\n    \"/tcommaaccent\": \"\\u0163\",\n    \"/tcurl\": \"\\u0236\",\n    \"/tdieresis\": \"\\u1E97\",\n    \"/tdot\": \"\\u1E6B\",\n    \"/tdotaccent\": \"\\u1E6B\",\n    \"/tdotbelow\": \"\\u1E6D\",\n    \"/teacupOutHandle\": \"\\u1F375\",\n    \"/tear-offCalendar\": \"\\u1F4C6\",\n    \"/tecirclekatakana\": \"\\u32E2\",\n    \"/tecyr\": \"\\u0442\",\n    \"/tecyrillic\": \"\\u0442\",\n    \"/tedescendercyrillic\": \"\\u04AD\",\n    \"/teh\": \"\\u062A\",\n    \"/teh.fina\": \"\\uFE96\",\n    \"/teh.init\": \"\\uFE97\",\n    \"/teh.init_alefmaksura.fina\": \"\\uFC0F\",\n    \"/teh.init_hah.fina\": \"\\uFC0C\",\n    \"/teh.init_hah.medi\": \"\\uFCA2\",\n    \"/teh.init_hah.medi_jeem.medi\": \"\\uFD52\",\n    \"/teh.init_hah.medi_meem.medi\": \"\\uFD53\",\n    \"/teh.init_heh.medi\": \"\\uFCA5\",\n    \"/teh.init_jeem.fina\": \"\\uFC0B\",\n    \"/teh.init_jeem.medi\": \"\\uFCA1\",\n    \"/teh.init_jeem.medi_meem.medi\": \"\\uFD50\",\n    \"/teh.init_khah.fina\": \"\\uFC0D\",\n    \"/teh.init_khah.medi\": \"\\uFCA3\",\n    \"/teh.init_khah.medi_meem.medi\": \"\\uFD54\",\n    \"/teh.init_meem.fina\": \"\\uFC0E\",\n    \"/teh.init_meem.medi\": \"\\uFCA4\",\n    \"/teh.init_meem.medi_hah.medi\": \"\\uFD56\",\n    \"/teh.init_meem.medi_jeem.medi\": \"\\uFD55\",\n    \"/teh.init_meem.medi_khah.medi\": \"\\uFD57\",\n    \"/teh.init_yeh.fina\": \"\\uFC10\",\n    \"/teh.isol\": \"\\uFE95\",\n    \"/teh.medi\": \"\\uFE98\",\n    \"/teh.medi_alefmaksura.fina\": \"\\uFC74\",\n    \"/teh.medi_hah.medi_jeem.fina\": \"\\uFD51\",\n    \"/teh.medi_heh.medi\": \"\\uFCE4\",\n    \"/teh.medi_jeem.medi_alefmaksura.fina\": \"\\uFDA0\",\n    \"/teh.medi_jeem.medi_yeh.fina\": \"\\uFD9F\",\n    \"/teh.medi_khah.medi_alefmaksura.fina\": \"\\uFDA2\",\n    \"/teh.medi_khah.medi_yeh.fina\": \"\\uFDA1\",\n    \"/teh.medi_meem.fina\": \"\\uFC72\",\n    \"/teh.medi_meem.medi\": \"\\uFCE3\",\n    \"/teh.medi_meem.medi_alefmaksura.fina\": \"\\uFDA4\",\n    \"/teh.medi_meem.medi_yeh.fina\": \"\\uFDA3\",\n    \"/teh.medi_noon.fina\": \"\\uFC73\",\n    \"/teh.medi_reh.fina\": \"\\uFC70\",\n    \"/teh.medi_yeh.fina\": \"\\uFC75\",\n    \"/teh.medi_zain.fina\": \"\\uFC71\",\n    \"/teharabic\": \"\\u062A\",\n    \"/tehdownthreedotsabove\": \"\\u067D\",\n    \"/teheh\": \"\\u067F\",\n    \"/teheh.fina\": \"\\uFB63\",\n    \"/teheh.init\": \"\\uFB64\",\n    \"/teheh.isol\": \"\\uFB62\",\n    \"/teheh.medi\": \"\\uFB65\",\n    \"/tehfinalarabic\": \"\\uFE96\",\n    \"/tehhahinitialarabic\": \"\\uFCA2\",\n    \"/tehhahisolatedarabic\": \"\\uFC0C\",\n    \"/tehinitialarabic\": \"\\uFE97\",\n    \"/tehiragana\": \"\\u3066\",\n    \"/tehjeeminitialarabic\": \"\\uFCA1\",\n    \"/tehjeemisolatedarabic\": \"\\uFC0B\",\n    \"/tehmarbuta\": \"\\u0629\",\n    \"/tehmarbuta.fina\": \"\\uFE94\",\n    \"/tehmarbuta.isol\": \"\\uFE93\",\n    \"/tehmarbutaarabic\": \"\\u0629\",\n    \"/tehmarbutafinalarabic\": \"\\uFE94\",\n    \"/tehmarbutagoal\": \"\\u06C3\",\n    \"/tehmedialarabic\": \"\\uFE98\",\n    \"/tehmeeminitialarabic\": \"\\uFCA4\",\n    \"/tehmeemisolatedarabic\": \"\\uFC0E\",\n    \"/tehnoonfinalarabic\": \"\\uFC73\",\n    \"/tehring\": \"\\u067C\",\n    \"/tekatakana\": \"\\u30C6\",\n    \"/tekatakanahalfwidth\": \"\\uFF83\",\n    \"/telephone\": \"\\u2121\",\n    \"/telephoneOnTopOfModem\": \"\\u1F580\",\n    \"/telephoneReceiver\": \"\\u1F4DE\",\n    \"/telephoneReceiverPage\": \"\\u1F57C\",\n    \"/telephoneblack\": \"\\u260E\",\n    \"/telephonerecorder\": \"\\u2315\",\n    \"/telephonewhite\": \"\\u260F\",\n    \"/telescope\": \"\\u1F52D\",\n    \"/television\": \"\\u1F4FA\",\n    \"/telishaGedolah:hb\": \"\\u05A0\",\n    \"/telishaQetannah:hb\": \"\\u05A9\",\n    \"/telishagedolahebrew\": \"\\u05A0\",\n    \"/telishaqetanahebrew\": \"\\u05A9\",\n    \"/telu:a\": \"\\u0C05\",\n    \"/telu:aa\": \"\\u0C06\",\n    \"/telu:aasign\": \"\\u0C3E\",\n    \"/telu:ai\": \"\\u0C10\",\n    \"/telu:ailengthmark\": \"\\u0C56\",\n    \"/telu:aisign\": \"\\u0C48\",\n    \"/telu:anusvarasign\": \"\\u0C02\",\n    \"/telu:au\": \"\\u0C14\",\n    \"/telu:ausign\": \"\\u0C4C\",\n    \"/telu:avagrahasign\": \"\\u0C3D\",\n    \"/telu:ba\": \"\\u0C2C\",\n    \"/telu:bha\": \"\\u0C2D\",\n    \"/telu:bindusigncandra\": \"\\u0C01\",\n    \"/telu:ca\": \"\\u0C1A\",\n    \"/telu:cha\": \"\\u0C1B\",\n    \"/telu:combiningbinduabovesigncandra\": \"\\u0C00\",\n    \"/telu:da\": \"\\u0C26\",\n    \"/telu:dda\": \"\\u0C21\",\n    \"/telu:ddha\": \"\\u0C22\",\n    \"/telu:dha\": \"\\u0C27\",\n    \"/telu:dza\": \"\\u0C59\",\n    \"/telu:e\": \"\\u0C0E\",\n    \"/telu:ee\": \"\\u0C0F\",\n    \"/telu:eesign\": \"\\u0C47\",\n    \"/telu:eight\": \"\\u0C6E\",\n    \"/telu:esign\": \"\\u0C46\",\n    \"/telu:five\": \"\\u0C6B\",\n    \"/telu:four\": \"\\u0C6A\",\n    \"/telu:fractiononeforevenpowersoffour\": \"\\u0C7C\",\n    \"/telu:fractiononeforoddpowersoffour\": \"\\u0C79\",\n    \"/telu:fractionthreeforevenpowersoffour\": \"\\u0C7E\",\n    \"/telu:fractionthreeforoddpowersoffour\": \"\\u0C7B\",\n    \"/telu:fractiontwoforevenpowersoffour\": \"\\u0C7D\",\n    \"/telu:fractiontwoforoddpowersoffour\": \"\\u0C7A\",\n    \"/telu:fractionzeroforoddpowersoffour\": \"\\u0C78\",\n    \"/telu:ga\": \"\\u0C17\",\n    \"/telu:gha\": \"\\u0C18\",\n    \"/telu:ha\": \"\\u0C39\",\n    \"/telu:i\": \"\\u0C07\",\n    \"/telu:ii\": \"\\u0C08\",\n    \"/telu:iisign\": \"\\u0C40\",\n    \"/telu:isign\": \"\\u0C3F\",\n    \"/telu:ja\": \"\\u0C1C\",\n    \"/telu:jha\": \"\\u0C1D\",\n    \"/telu:ka\": \"\\u0C15\",\n    \"/telu:kha\": \"\\u0C16\",\n    \"/telu:la\": \"\\u0C32\",\n    \"/telu:lengthmark\": \"\\u0C55\",\n    \"/telu:lla\": \"\\u0C33\",\n    \"/telu:llla\": \"\\u0C34\",\n    \"/telu:llsignvocal\": \"\\u0C63\",\n    \"/telu:llvocal\": \"\\u0C61\",\n    \"/telu:lsignvocal\": \"\\u0C62\",\n    \"/telu:lvocal\": \"\\u0C0C\",\n    \"/telu:ma\": \"\\u0C2E\",\n    \"/telu:na\": \"\\u0C28\",\n    \"/telu:nga\": \"\\u0C19\",\n    \"/telu:nine\": \"\\u0C6F\",\n    \"/telu:nna\": \"\\u0C23\",\n    \"/telu:nya\": \"\\u0C1E\",\n    \"/telu:o\": \"\\u0C12\",\n    \"/telu:one\": \"\\u0C67\",\n    \"/telu:oo\": \"\\u0C13\",\n    \"/telu:oosign\": \"\\u0C4B\",\n    \"/telu:osign\": \"\\u0C4A\",\n    \"/telu:pa\": \"\\u0C2A\",\n    \"/telu:pha\": \"\\u0C2B\",\n    \"/telu:ra\": \"\\u0C30\",\n    \"/telu:rra\": \"\\u0C31\",\n    \"/telu:rrra\": \"\\u0C5A\",\n    \"/telu:rrsignvocal\": \"\\u0C44\",\n    \"/telu:rrvocal\": \"\\u0C60\",\n    \"/telu:rsignvocal\": \"\\u0C43\",\n    \"/telu:rvocal\": \"\\u0C0B\",\n    \"/telu:sa\": \"\\u0C38\",\n    \"/telu:seven\": \"\\u0C6D\",\n    \"/telu:sha\": \"\\u0C36\",\n    \"/telu:six\": \"\\u0C6C\",\n    \"/telu:ssa\": \"\\u0C37\",\n    \"/telu:ta\": \"\\u0C24\",\n    \"/telu:tha\": \"\\u0C25\",\n    \"/telu:three\": \"\\u0C69\",\n    \"/telu:tsa\": \"\\u0C58\",\n    \"/telu:tta\": \"\\u0C1F\",\n    \"/telu:ttha\": \"\\u0C20\",\n    \"/telu:tuumusign\": \"\\u0C7F\",\n    \"/telu:two\": \"\\u0C68\",\n    \"/telu:u\": \"\\u0C09\",\n    \"/telu:usign\": \"\\u0C41\",\n    \"/telu:uu\": \"\\u0C0A\",\n    \"/telu:uusign\": \"\\u0C42\",\n    \"/telu:va\": \"\\u0C35\",\n    \"/telu:viramasign\": \"\\u0C4D\",\n    \"/telu:visargasign\": \"\\u0C03\",\n    \"/telu:ya\": \"\\u0C2F\",\n    \"/telu:zero\": \"\\u0C66\",\n    \"/ten.roman\": \"\\u2169\",\n    \"/ten.romansmall\": \"\\u2179\",\n    \"/tencircle\": \"\\u2469\",\n    \"/tencircledbl\": \"\\u24FE\",\n    \"/tencirclesquare\": \"\\u3248\",\n    \"/tenge\": \"\\u20B8\",\n    \"/tenhangzhou\": \"\\u3038\",\n    \"/tenideographiccircled\": \"\\u3289\",\n    \"/tenideographicparen\": \"\\u3229\",\n    \"/tennisRacquetAndBall\": \"\\u1F3BE\",\n    \"/tenparen\": \"\\u247D\",\n    \"/tenparenthesized\": \"\\u247D\",\n    \"/tenperiod\": \"\\u2491\",\n    \"/tenroman\": \"\\u2179\",\n    \"/tent\": \"\\u26FA\",\n    \"/tenthousand.roman\": \"\\u2182\",\n    \"/tesh\": \"\\u02A7\",\n    \"/tet\": \"\\u05D8\",\n    \"/tet:hb\": \"\\u05D8\",\n    \"/tetailcyr\": \"\\u04AD\",\n    \"/tetdagesh\": \"\\uFB38\",\n    \"/tetdageshhebrew\": \"\\uFB38\",\n    \"/tethebrew\": \"\\u05D8\",\n    \"/tetrasememetrical\": \"\\u23D8\",\n    \"/tetsecyr\": \"\\u04B5\",\n    \"/tetsecyrillic\": \"\\u04B5\",\n    \"/tetwithdagesh:hb\": \"\\uFB38\",\n    \"/tevir:hb\": \"\\u059B\",\n    \"/tevirhebrew\": \"\\u059B\",\n    \"/tevirlefthebrew\": \"\\u059B\",\n    \"/thabengali\": \"\\u09A5\",\n    \"/thadeva\": \"\\u0925\",\n    \"/thagujarati\": \"\\u0AA5\",\n    \"/thagurmukhi\": \"\\u0A25\",\n    \"/thai:angkhankhu\": \"\\u0E5A\",\n    \"/thai:baht\": \"\\u0E3F\",\n    \"/thai:bobaimai\": \"\\u0E1A\",\n    \"/thai:chochan\": \"\\u0E08\",\n    \"/thai:chochang\": \"\\u0E0A\",\n    \"/thai:choching\": \"\\u0E09\",\n    \"/thai:chochoe\": \"\\u0E0C\",\n    \"/thai:dochada\": \"\\u0E0E\",\n    \"/thai:dodek\": \"\\u0E14\",\n    \"/thai:eight\": \"\\u0E58\",\n    \"/thai:five\": \"\\u0E55\",\n    \"/thai:fofa\": \"\\u0E1D\",\n    \"/thai:fofan\": \"\\u0E1F\",\n    \"/thai:fongman\": \"\\u0E4F\",\n    \"/thai:four\": \"\\u0E54\",\n    \"/thai:hohip\": \"\\u0E2B\",\n    \"/thai:honokhuk\": \"\\u0E2E\",\n    \"/thai:khokhai\": \"\\u0E02\",\n    \"/thai:khokhon\": \"\\u0E05\",\n    \"/thai:khokhuat\": \"\\u0E03\",\n    \"/thai:khokhwai\": \"\\u0E04\",\n    \"/thai:khomut\": \"\\u0E5B\",\n    \"/thai:khorakhang\": \"\\u0E06\",\n    \"/thai:kokai\": \"\\u0E01\",\n    \"/thai:lakkhangyao\": \"\\u0E45\",\n    \"/thai:lochula\": \"\\u0E2C\",\n    \"/thai:loling\": \"\\u0E25\",\n    \"/thai:lu\": \"\\u0E26\",\n    \"/thai:maichattawa\": \"\\u0E4B\",\n    \"/thai:maiek\": \"\\u0E48\",\n    \"/thai:maihan-akat\": \"\\u0E31\",\n    \"/thai:maitaikhu\": \"\\u0E47\",\n    \"/thai:maitho\": \"\\u0E49\",\n    \"/thai:maitri\": \"\\u0E4A\",\n    \"/thai:maiyamok\": \"\\u0E46\",\n    \"/thai:moma\": \"\\u0E21\",\n    \"/thai:ngongu\": \"\\u0E07\",\n    \"/thai:nikhahit\": \"\\u0E4D\",\n    \"/thai:nine\": \"\\u0E59\",\n    \"/thai:nonen\": \"\\u0E13\",\n    \"/thai:nonu\": \"\\u0E19\",\n    \"/thai:oang\": \"\\u0E2D\",\n    \"/thai:one\": \"\\u0E51\",\n    \"/thai:paiyannoi\": \"\\u0E2F\",\n    \"/thai:phinthu\": \"\\u0E3A\",\n    \"/thai:phophan\": \"\\u0E1E\",\n    \"/thai:phophung\": \"\\u0E1C\",\n    \"/thai:phosamphao\": \"\\u0E20\",\n    \"/thai:popla\": \"\\u0E1B\",\n    \"/thai:rorua\": \"\\u0E23\",\n    \"/thai:ru\": \"\\u0E24\",\n    \"/thai:saraa\": \"\\u0E30\",\n    \"/thai:saraaa\": \"\\u0E32\",\n    \"/thai:saraae\": \"\\u0E41\",\n    \"/thai:saraaimaimalai\": \"\\u0E44\",\n    \"/thai:saraaimaimuan\": \"\\u0E43\",\n    \"/thai:saraam\": \"\\u0E33\",\n    \"/thai:sarae\": \"\\u0E40\",\n    \"/thai:sarai\": \"\\u0E34\",\n    \"/thai:saraii\": \"\\u0E35\",\n    \"/thai:sarao\": \"\\u0E42\",\n    \"/thai:sarau\": \"\\u0E38\",\n    \"/thai:saraue\": \"\\u0E36\",\n    \"/thai:sarauee\": \"\\u0E37\",\n    \"/thai:sarauu\": \"\\u0E39\",\n    \"/thai:seven\": \"\\u0E57\",\n    \"/thai:six\": \"\\u0E56\",\n    \"/thai:sorusi\": \"\\u0E29\",\n    \"/thai:sosala\": \"\\u0E28\",\n    \"/thai:soso\": \"\\u0E0B\",\n    \"/thai:sosua\": \"\\u0E2A\",\n    \"/thai:thanthakhat\": \"\\u0E4C\",\n    \"/thai:thonangmontho\": \"\\u0E11\",\n    \"/thai:thophuthao\": \"\\u0E12\",\n    \"/thai:thothahan\": \"\\u0E17\",\n    \"/thai:thothan\": \"\\u0E10\",\n    \"/thai:thothong\": \"\\u0E18\",\n    \"/thai:thothung\": \"\\u0E16\",\n    \"/thai:three\": \"\\u0E53\",\n    \"/thai:topatak\": \"\\u0E0F\",\n    \"/thai:totao\": \"\\u0E15\",\n    \"/thai:two\": \"\\u0E52\",\n    \"/thai:wowaen\": \"\\u0E27\",\n    \"/thai:yamakkan\": \"\\u0E4E\",\n    \"/thai:yoyak\": \"\\u0E22\",\n    \"/thai:yoying\": \"\\u0E0D\",\n    \"/thai:zero\": \"\\u0E50\",\n    \"/thal\": \"\\u0630\",\n    \"/thal.fina\": \"\\uFEAC\",\n    \"/thal.init_superscriptalef.fina\": \"\\uFC5B\",\n    \"/thal.isol\": \"\\uFEAB\",\n    \"/thalarabic\": \"\\u0630\",\n    \"/thalfinalarabic\": \"\\uFEAC\",\n    \"/thanthakhatlowleftthai\": \"\\uF898\",\n    \"/thanthakhatlowrightthai\": \"\\uF897\",\n    \"/thanthakhatthai\": \"\\u0E4C\",\n    \"/thanthakhatupperleftthai\": \"\\uF896\",\n    \"/theh\": \"\\u062B\",\n    \"/theh.fina\": \"\\uFE9A\",\n    \"/theh.init\": \"\\uFE9B\",\n    \"/theh.init_alefmaksura.fina\": \"\\uFC13\",\n    \"/theh.init_jeem.fina\": \"\\uFC11\",\n    \"/theh.init_meem.fina\": \"\\uFC12\",\n    \"/theh.init_meem.medi\": \"\\uFCA6\",\n    \"/theh.init_yeh.fina\": \"\\uFC14\",\n    \"/theh.isol\": \"\\uFE99\",\n    \"/theh.medi\": \"\\uFE9C\",\n    \"/theh.medi_alefmaksura.fina\": \"\\uFC7A\",\n    \"/theh.medi_heh.medi\": \"\\uFCE6\",\n    \"/theh.medi_meem.fina\": \"\\uFC78\",\n    \"/theh.medi_meem.medi\": \"\\uFCE5\",\n    \"/theh.medi_noon.fina\": \"\\uFC79\",\n    \"/theh.medi_reh.fina\": \"\\uFC76\",\n    \"/theh.medi_yeh.fina\": \"\\uFC7B\",\n    \"/theh.medi_zain.fina\": \"\\uFC77\",\n    \"/theharabic\": \"\\u062B\",\n    \"/thehfinalarabic\": \"\\uFE9A\",\n    \"/thehinitialarabic\": \"\\uFE9B\",\n    \"/thehmedialarabic\": \"\\uFE9C\",\n    \"/thereexists\": \"\\u2203\",\n    \"/therefore\": \"\\u2234\",\n    \"/thermometer\": \"\\u1F321\",\n    \"/theta\": \"\\u03B8\",\n    \"/theta.math\": \"\\u03D1\",\n    \"/theta1\": \"\\u03D1\",\n    \"/thetasymbolgreek\": \"\\u03D1\",\n    \"/thieuthacirclekorean\": \"\\u3279\",\n    \"/thieuthaparenkorean\": \"\\u3219\",\n    \"/thieuthcirclekorean\": \"\\u326B\",\n    \"/thieuthkorean\": \"\\u314C\",\n    \"/thieuthparenkorean\": \"\\u320B\",\n    \"/thinspace\": \"\\u2009\",\n    \"/thirteencircle\": \"\\u246C\",\n    \"/thirteencircleblack\": \"\\u24ED\",\n    \"/thirteenparen\": \"\\u2480\",\n    \"/thirteenparenthesized\": \"\\u2480\",\n    \"/thirteenperiod\": \"\\u2494\",\n    \"/thirtycircle\": \"\\u325A\",\n    \"/thirtycirclesquare\": \"\\u324A\",\n    \"/thirtyeightcircle\": \"\\u32B3\",\n    \"/thirtyfivecircle\": \"\\u325F\",\n    \"/thirtyfourcircle\": \"\\u325E\",\n    \"/thirtyhangzhou\": \"\\u303A\",\n    \"/thirtyninecircle\": \"\\u32B4\",\n    \"/thirtyonecircle\": \"\\u325B\",\n    \"/thirtysevencircle\": \"\\u32B2\",\n    \"/thirtysixcircle\": \"\\u32B1\",\n    \"/thirtythreecircle\": \"\\u325D\",\n    \"/thirtytwocircle\": \"\\u325C\",\n    \"/thonangmonthothai\": \"\\u0E11\",\n    \"/thook\": \"\\u01AD\",\n    \"/thophuthaothai\": \"\\u0E12\",\n    \"/thorn\": \"\\u00FE\",\n    \"/thornstroke\": \"\\uA765\",\n    \"/thornstrokedescender\": \"\\uA767\",\n    \"/thothahanthai\": \"\\u0E17\",\n    \"/thothanthai\": \"\\u0E10\",\n    \"/thothongthai\": \"\\u0E18\",\n    \"/thothungthai\": \"\\u0E16\",\n    \"/thoughtBalloon\": \"\\u1F4AD\",\n    \"/thousandcyrillic\": \"\\u0482\",\n    \"/thousandscyr\": \"\\u0482\",\n    \"/thousandsseparator\": \"\\u066C\",\n    \"/thousandsseparatorarabic\": \"\\u066C\",\n    \"/thousandsseparatorpersian\": \"\\u066C\",\n    \"/three\": \"\\u0033\",\n    \"/three.inferior\": \"\\u2083\",\n    \"/three.roman\": \"\\u2162\",\n    \"/three.romansmall\": \"\\u2172\",\n    \"/threeButtonMouse\": \"\\u1F5B1\",\n    \"/threeNetworkedComputers\": \"\\u1F5A7\",\n    \"/threeRaysAbove\": \"\\u1F5E4\",\n    \"/threeRaysBelow\": \"\\u1F5E5\",\n    \"/threeRaysLeft\": \"\\u1F5E6\",\n    \"/threeRaysRight\": \"\\u1F5E7\",\n    \"/threeSpeechBubbles\": \"\\u1F5EB\",\n    \"/threearabic\": \"\\u0663\",\n    \"/threebengali\": \"\\u09E9\",\n    \"/threecircle\": \"\\u2462\",\n    \"/threecircledbl\": \"\\u24F7\",\n    \"/threecircleinversesansserif\": \"\\u278C\",\n    \"/threecomma\": \"\\u1F104\",\n    \"/threedeva\": \"\\u0969\",\n    \"/threedimensionalangle\": \"\\u27C0\",\n    \"/threedotpunctuation\": \"\\u2056\",\n    \"/threedotsaboveabove\": \"\\u06DB\",\n    \"/threedsquare\": \"\\u1F19B\",\n    \"/threeeighths\": \"\\u215C\",\n    \"/threefar\": \"\\u06F3\",\n    \"/threefifths\": \"\\u2157\",\n    \"/threegujarati\": \"\\u0AE9\",\n    \"/threegurmukhi\": \"\\u0A69\",\n    \"/threehackarabic\": \"\\u0663\",\n    \"/threehangzhou\": \"\\u3023\",\n    \"/threeideographiccircled\": \"\\u3282\",\n    \"/threeideographicparen\": \"\\u3222\",\n    \"/threeinferior\": \"\\u2083\",\n    \"/threelinesconvergingleft\": \"\\u269F\",\n    \"/threelinesconvergingright\": \"\\u269E\",\n    \"/threemonospace\": \"\\uFF13\",\n    \"/threenumeratorbengali\": \"\\u09F6\",\n    \"/threeoldstyle\": \"\\uF733\",\n    \"/threeparen\": \"\\u2476\",\n    \"/threeparenthesized\": \"\\u2476\",\n    \"/threeperemspace\": \"\\u2004\",\n    \"/threeperiod\": \"\\u248A\",\n    \"/threepersian\": \"\\u06F3\",\n    \"/threequarters\": \"\\u00BE\",\n    \"/threequartersemdash\": \"\\uF6DE\",\n    \"/threerightarrows\": \"\\u21F6\",\n    \"/threeroman\": \"\\u2172\",\n    \"/threesuperior\": \"\\u00B3\",\n    \"/threethai\": \"\\u0E53\",\n    \"/thumbsDownSign\": \"\\u1F44E\",\n    \"/thumbsUpSign\": \"\\u1F44D\",\n    \"/thundercloudrain\": \"\\u26C8\",\n    \"/thunderstorm\": \"\\u2608\",\n    \"/thzfullwidth\": \"\\u3394\",\n    \"/thzsquare\": \"\\u3394\",\n    \"/tibt:AA\": \"\\u0F60\",\n    \"/tibt:a\": \"\\u0F68\",\n    \"/tibt:aavowelsign\": \"\\u0F71\",\n    \"/tibt:angkhanggyasmark\": \"\\u0F3D\",\n    \"/tibt:angkhanggyonmark\": \"\\u0F3C\",\n    \"/tibt:astrologicalkhyudpasign\": \"\\u0F18\",\n    \"/tibt:astrologicalsdongtshugssign\": \"\\u0F19\",\n    \"/tibt:astrologicalsgragcancharrtagssign\": \"\\u0F17\",\n    \"/tibt:asubjoined\": \"\\u0FB8\",\n    \"/tibt:ba\": \"\\u0F56\",\n    \"/tibt:basubjoined\": \"\\u0FA6\",\n    \"/tibt:bha\": \"\\u0F57\",\n    \"/tibt:bhasubjoined\": \"\\u0FA7\",\n    \"/tibt:bkashogyigmgomark\": \"\\u0F0A\",\n    \"/tibt:brdarnyingyigmgomdunmainitialmark\": \"\\u0FD3\",\n    \"/tibt:brdarnyingyigmgosgabmaclosingmark\": \"\\u0FD4\",\n    \"/tibt:bsdusrtagsmark\": \"\\u0F34\",\n    \"/tibt:bskashoggimgorgyanmark\": \"\\u0FD0\",\n    \"/tibt:bskuryigmgomark\": \"\\u0F09\",\n    \"/tibt:ca\": \"\\u0F45\",\n    \"/tibt:cangteucantillationsign\": \"\\u0FC2\",\n    \"/tibt:caretdzudrtagsbzhimigcanmark\": \"\\u0F36\",\n    \"/tibt:caretdzudrtagsmelongcanmark\": \"\\u0F13\",\n    \"/tibt:caretyigmgophurshadmamark\": \"\\u0F06\",\n    \"/tibt:casubjoined\": \"\\u0F95\",\n    \"/tibt:cha\": \"\\u0F46\",\n    \"/tibt:chadrtagslogotypesign\": \"\\u0F15\",\n    \"/tibt:chasubjoined\": \"\\u0F96\",\n    \"/tibt:chemgomark\": \"\\u0F38\",\n    \"/tibt:da\": \"\\u0F51\",\n    \"/tibt:dasubjoined\": \"\\u0FA1\",\n    \"/tibt:dda\": \"\\u0F4C\",\n    \"/tibt:ddasubjoined\": \"\\u0F9C\",\n    \"/tibt:ddha\": \"\\u0F4D\",\n    \"/tibt:ddhasubjoined\": \"\\u0F9D\",\n    \"/tibt:delimitertshegbstarmark\": \"\\u0F0C\",\n    \"/tibt:dha\": \"\\u0F52\",\n    \"/tibt:dhasubjoined\": \"\\u0FA2\",\n    \"/tibt:drilbusymbol\": \"\\u0FC4\",\n    \"/tibt:dza\": \"\\u0F5B\",\n    \"/tibt:dzasubjoined\": \"\\u0FAB\",\n    \"/tibt:dzha\": \"\\u0F5C\",\n    \"/tibt:dzhasubjoined\": \"\\u0FAC\",\n    \"/tibt:eevowelsign\": \"\\u0F7B\",\n    \"/tibt:eight\": \"\\u0F28\",\n    \"/tibt:evowelsign\": \"\\u0F7A\",\n    \"/tibt:five\": \"\\u0F25\",\n    \"/tibt:four\": \"\\u0F24\",\n    \"/tibt:ga\": \"\\u0F42\",\n    \"/tibt:gasubjoined\": \"\\u0F92\",\n    \"/tibt:gha\": \"\\u0F43\",\n    \"/tibt:ghasubjoined\": \"\\u0F93\",\n    \"/tibt:grucanrgyingssign\": \"\\u0F8A\",\n    \"/tibt:grumedrgyingssign\": \"\\u0F8B\",\n    \"/tibt:gtertshegmark\": \"\\u0F14\",\n    \"/tibt:gteryigmgotruncatedamark\": \"\\u0F01\",\n    \"/tibt:gteryigmgoumgtertshegmamark\": \"\\u0F03\",\n    \"/tibt:gteryigmgoumrnambcadmamark\": \"\\u0F02\",\n    \"/tibt:gugrtagsgyasmark\": \"\\u0F3B\",\n    \"/tibt:gugrtagsgyonmark\": \"\\u0F3A\",\n    \"/tibt:ha\": \"\\u0F67\",\n    \"/tibt:halantamark\": \"\\u0F84\",\n    \"/tibt:halfeight\": \"\\u0F31\",\n    \"/tibt:halffive\": \"\\u0F2E\",\n    \"/tibt:halffour\": \"\\u0F2D\",\n    \"/tibt:halfnine\": \"\\u0F32\",\n    \"/tibt:halfone\": \"\\u0F2A\",\n    \"/tibt:halfseven\": \"\\u0F30\",\n    \"/tibt:halfsix\": \"\\u0F2F\",\n    \"/tibt:halfthree\": \"\\u0F2C\",\n    \"/tibt:halftwo\": \"\\u0F2B\",\n    \"/tibt:halfzero\": \"\\u0F33\",\n    \"/tibt:hasubjoined\": \"\\u0FB7\",\n    \"/tibt:heavybeatcantillationsign\": \"\\u0FC0\",\n    \"/tibt:iivowelsign\": \"\\u0F73\",\n    \"/tibt:intersyllabictshegmark\": \"\\u0F0B\",\n    \"/tibt:invertedmchucansign\": \"\\u0F8C\",\n    \"/tibt:invertedmchucansubjoinedsign\": \"\\u0F8F\",\n    \"/tibt:ivowelsign\": \"\\u0F72\",\n    \"/tibt:ja\": \"\\u0F47\",\n    \"/tibt:jasubjoined\": \"\\u0F97\",\n    \"/tibt:ka\": \"\\u0F40\",\n    \"/tibt:kasubjoined\": \"\\u0F90\",\n    \"/tibt:kha\": \"\\u0F41\",\n    \"/tibt:khasubjoined\": \"\\u0F91\",\n    \"/tibt:kka\": \"\\u0F6B\",\n    \"/tibt:kssa\": \"\\u0F69\",\n    \"/tibt:kssasubjoined\": \"\\u0FB9\",\n    \"/tibt:kurukha\": \"\\u0FBE\",\n    \"/tibt:kurukhabzhimigcan\": \"\\u0FBF\",\n    \"/tibt:la\": \"\\u0F63\",\n    \"/tibt:lasubjoined\": \"\\u0FB3\",\n    \"/tibt:lcetsacansign\": \"\\u0F88\",\n    \"/tibt:lcetsacansubjoinedsign\": \"\\u0F8D\",\n    \"/tibt:lcirtagssign\": \"\\u0F86\",\n    \"/tibt:leadingmchanrtagsmark\": \"\\u0FD9\",\n    \"/tibt:lhagrtagslogotypesign\": \"\\u0F16\",\n    \"/tibt:lightbeatcantillationsign\": \"\\u0FC1\",\n    \"/tibt:llvocalicvowelsign\": \"\\u0F79\",\n    \"/tibt:lvocalicvowelsign\": \"\\u0F78\",\n    \"/tibt:ma\": \"\\u0F58\",\n    \"/tibt:martshessign\": \"\\u0F3F\",\n    \"/tibt:masubjoined\": \"\\u0FA8\",\n    \"/tibt:mchucansign\": \"\\u0F89\",\n    \"/tibt:mchucansubjoinedsign\": \"\\u0F8E\",\n    \"/tibt:mnyamyiggimgorgyanmark\": \"\\u0FD1\",\n    \"/tibt:na\": \"\\u0F53\",\n    \"/tibt:nasubjoined\": \"\\u0FA3\",\n    \"/tibt:nga\": \"\\u0F44\",\n    \"/tibt:ngasbzungnyizlamark\": \"\\u0F35\",\n    \"/tibt:ngasbzungsgorrtagsmark\": \"\\u0F37\",\n    \"/tibt:ngasubjoined\": \"\\u0F94\",\n    \"/tibt:nine\": \"\\u0F29\",\n    \"/tibt:nna\": \"\\u0F4E\",\n    \"/tibt:nnasubjoined\": \"\\u0F9E\",\n    \"/tibt:norbubzhikhyilsymbol\": \"\\u0FCC\",\n    \"/tibt:norbugsumkhyilsymbol\": \"\\u0FCB\",\n    \"/tibt:norbunyiskhyilsymbol\": \"\\u0FCA\",\n    \"/tibt:norbusymbol\": \"\\u0FC9\",\n    \"/tibt:nya\": \"\\u0F49\",\n    \"/tibt:nyasubjoined\": \"\\u0F99\",\n    \"/tibt:nyisshadmark\": \"\\u0F0E\",\n    \"/tibt:nyistshegmark\": \"\\u0FD2\",\n    \"/tibt:nyistshegshadmark\": \"\\u0F10\",\n    \"/tibt:nyizlanaadasign\": \"\\u0F82\",\n    \"/tibt:omsyllable\": \"\\u0F00\",\n    \"/tibt:one\": \"\\u0F21\",\n    \"/tibt:oovowelsign\": \"\\u0F7D\",\n    \"/tibt:ovowelsign\": \"\\u0F7C\",\n    \"/tibt:pa\": \"\\u0F54\",\n    \"/tibt:padmagdansymbol\": \"\\u0FC6\",\n    \"/tibt:palutamark\": \"\\u0F85\",\n    \"/tibt:pasubjoined\": \"\\u0FA4\",\n    \"/tibt:pha\": \"\\u0F55\",\n    \"/tibt:phasubjoined\": \"\\u0FA5\",\n    \"/tibt:phurpasymbol\": \"\\u0FC8\",\n    \"/tibt:ra\": \"\\u0F62\",\n    \"/tibt:rafixed\": \"\\u0F6A\",\n    \"/tibt:rasubjoined\": \"\\u0FB2\",\n    \"/tibt:rasubjoinedfixed\": \"\\u0FBC\",\n    \"/tibt:rdeldkargcigsign\": \"\\u0F1A\",\n    \"/tibt:rdeldkargnyissign\": \"\\u0F1B\",\n    \"/tibt:rdeldkargsumsign\": \"\\u0F1C\",\n    \"/tibt:rdeldkarrdelnagsign\": \"\\u0F1F\",\n    \"/tibt:rdelnaggcigsign\": \"\\u0F1D\",\n    \"/tibt:rdelnaggnyissign\": \"\\u0F1E\",\n    \"/tibt:rdelnaggsumsign\": \"\\u0FCF\",\n    \"/tibt:rdelnagrdeldkarsign\": \"\\u0FCE\",\n    \"/tibt:rdorjergyagramsymbol\": \"\\u0FC7\",\n    \"/tibt:rdorjesymbol\": \"\\u0FC5\",\n    \"/tibt:reversediivowelsign\": \"\\u0F81\",\n    \"/tibt:reversedivowelsign\": \"\\u0F80\",\n    \"/tibt:rgyagramshadmark\": \"\\u0F12\",\n    \"/tibt:rinchenspungsshadmark\": \"\\u0F11\",\n    \"/tibt:rjessungarosign\": \"\\u0F7E\",\n    \"/tibt:rnambcadsign\": \"\\u0F7F\",\n    \"/tibt:rra\": \"\\u0F6C\",\n    \"/tibt:rrvocalicvowelsign\": \"\\u0F77\",\n    \"/tibt:rvocalicvowelsign\": \"\\u0F76\",\n    \"/tibt:sa\": \"\\u0F66\",\n    \"/tibt:sasubjoined\": \"\\u0FB6\",\n    \"/tibt:sbrulshadmark\": \"\\u0F08\",\n    \"/tibt:sbubchalcantillationsign\": \"\\u0FC3\",\n    \"/tibt:seven\": \"\\u0F27\",\n    \"/tibt:sha\": \"\\u0F64\",\n    \"/tibt:shadmark\": \"\\u0F0D\",\n    \"/tibt:shasubjoined\": \"\\u0FB4\",\n    \"/tibt:six\": \"\\u0F26\",\n    \"/tibt:snaldansign\": \"\\u0F83\",\n    \"/tibt:ssa\": \"\\u0F65\",\n    \"/tibt:ssasubjoined\": \"\\u0FB5\",\n    \"/tibt:subjoinedAA\": \"\\u0FB0\",\n    \"/tibt:svastileft\": \"\\u0FD6\",\n    \"/tibt:svastileftdot\": \"\\u0FD8\",\n    \"/tibt:svastiright\": \"\\u0FD5\",\n    \"/tibt:svastirightdot\": \"\\u0FD7\",\n    \"/tibt:ta\": \"\\u0F4F\",\n    \"/tibt:tasubjoined\": \"\\u0F9F\",\n    \"/tibt:tha\": \"\\u0F50\",\n    \"/tibt:thasubjoined\": \"\\u0FA0\",\n    \"/tibt:three\": \"\\u0F23\",\n    \"/tibt:trailingmchanrtagsmark\": \"\\u0FDA\",\n    \"/tibt:tsa\": \"\\u0F59\",\n    \"/tibt:tsaphrumark\": \"\\u0F39\",\n    \"/tibt:tsasubjoined\": \"\\u0FA9\",\n    \"/tibt:tsha\": \"\\u0F5A\",\n    \"/tibt:tshasubjoined\": \"\\u0FAA\",\n    \"/tibt:tshegshadmark\": \"\\u0F0F\",\n    \"/tibt:tta\": \"\\u0F4A\",\n    \"/tibt:ttasubjoined\": \"\\u0F9A\",\n    \"/tibt:ttha\": \"\\u0F4B\",\n    \"/tibt:tthasubjoined\": \"\\u0F9B\",\n    \"/tibt:two\": \"\\u0F22\",\n    \"/tibt:uuvowelsign\": \"\\u0F75\",\n    \"/tibt:uvowelsign\": \"\\u0F74\",\n    \"/tibt:wa\": \"\\u0F5D\",\n    \"/tibt:wasubjoined\": \"\\u0FAD\",\n    \"/tibt:wasubjoinedfixed\": \"\\u0FBA\",\n    \"/tibt:ya\": \"\\u0F61\",\n    \"/tibt:yangrtagssign\": \"\\u0F87\",\n    \"/tibt:yartshessign\": \"\\u0F3E\",\n    \"/tibt:yasubjoined\": \"\\u0FB1\",\n    \"/tibt:yasubjoinedfixed\": \"\\u0FBB\",\n    \"/tibt:yigmgomdunmainitialmark\": \"\\u0F04\",\n    \"/tibt:yigmgosgabmaclosingmark\": \"\\u0F05\",\n    \"/tibt:yigmgotshegshadmamark\": \"\\u0F07\",\n    \"/tibt:za\": \"\\u0F5F\",\n    \"/tibt:zasubjoined\": \"\\u0FAF\",\n    \"/tibt:zero\": \"\\u0F20\",\n    \"/tibt:zha\": \"\\u0F5E\",\n    \"/tibt:zhasubjoined\": \"\\u0FAE\",\n    \"/ticirclekatakana\": \"\\u32E0\",\n    \"/tickconvavediamondleftwhite\": \"\\u27E2\",\n    \"/tickconvavediamondrightwhite\": \"\\u27E3\",\n    \"/ticket\": \"\\u1F3AB\",\n    \"/tickleftwhitesquare\": \"\\u27E4\",\n    \"/tickrightwhitesquare\": \"\\u27E5\",\n    \"/tifcha:hb\": \"\\u0596\",\n    \"/tiger\": \"\\u1F405\",\n    \"/tigerFace\": \"\\u1F42F\",\n    \"/tihiragana\": \"\\u3061\",\n    \"/tikatakana\": \"\\u30C1\",\n    \"/tikatakanahalfwidth\": \"\\uFF81\",\n    \"/tikeutacirclekorean\": \"\\u3270\",\n    \"/tikeutaparenkorean\": \"\\u3210\",\n    \"/tikeutcirclekorean\": \"\\u3262\",\n    \"/tikeutkorean\": \"\\u3137\",\n    \"/tikeutparenkorean\": \"\\u3202\",\n    \"/tilde\": \"\\u02DC\",\n    \"/tildebelowcmb\": \"\\u0330\",\n    \"/tildecmb\": \"\\u0303\",\n    \"/tildecomb\": \"\\u0303\",\n    \"/tildediaeresisfunc\": \"\\u2368\",\n    \"/tildedotaccent\": \"\\u2E1E\",\n    \"/tildedotbelow\": \"\\u2E1F\",\n    \"/tildedoublecmb\": \"\\u0360\",\n    \"/tildeequalsreversed\": \"\\u22CD\",\n    \"/tildelowmod\": \"\\u02F7\",\n    \"/tildeoperator\": \"\\u223C\",\n    \"/tildeoverlaycmb\": \"\\u0334\",\n    \"/tildereversed\": \"\\u223D\",\n    \"/tildering\": \"\\u2E1B\",\n    \"/tildetpl\": \"\\u224B\",\n    \"/tildeverticalcmb\": \"\\u033E\",\n    \"/timerclock\": \"\\u23F2\",\n    \"/timescircle\": \"\\u2297\",\n    \"/tinsular\": \"\\uA787\",\n    \"/tipehahebrew\": \"\\u0596\",\n    \"/tipehalefthebrew\": \"\\u0596\",\n    \"/tippigurmukhi\": \"\\u0A70\",\n    \"/tiredFace\": \"\\u1F62B\",\n    \"/tironiansignet\": \"\\u204A\",\n    \"/tirtatumetespada\": \"\\uA9DE\",\n    \"/titlocmbcyr\": \"\\u0483\",\n    \"/titlocyrilliccmb\": \"\\u0483\",\n    \"/tiwnarmenian\": \"\\u057F\",\n    \"/tjekomicyr\": \"\\u050F\",\n    \"/tlinebelow\": \"\\u1E6F\",\n    \"/tmonospace\": \"\\uFF54\",\n    \"/toarmenian\": \"\\u0569\",\n    \"/tocirclekatakana\": \"\\u32E3\",\n    \"/tocornerarrowNW\": \"\\u21F1\",\n    \"/tocornerarrowSE\": \"\\u21F2\",\n    \"/tohiragana\": \"\\u3068\",\n    \"/toilet\": \"\\u1F6BD\",\n    \"/tokatakana\": \"\\u30C8\",\n    \"/tokatakanahalfwidth\": \"\\uFF84\",\n    \"/tokyoTower\": \"\\u1F5FC\",\n    \"/tolongvowel\": \"\\uA9B5\",\n    \"/tomato\": \"\\u1F345\",\n    \"/tonebarextrahighmod\": \"\\u02E5\",\n    \"/tonebarextralowmod\": \"\\u02E9\",\n    \"/tonebarhighmod\": \"\\u02E6\",\n    \"/tonebarlowmod\": \"\\u02E8\",\n    \"/tonebarmidmod\": \"\\u02E7\",\n    \"/tonefive\": \"\\u01BD\",\n    \"/tonehighbeginmod\": \"\\u02F9\",\n    \"/tonehighendmod\": \"\\u02FA\",\n    \"/tonelowbeginmod\": \"\\u02FB\",\n    \"/tonelowendmod\": \"\\u02FC\",\n    \"/tonesix\": \"\\u0185\",\n    \"/tonetwo\": \"\\u01A8\",\n    \"/tongue\": \"\\u1F445\",\n    \"/tonos\": \"\\u0384\",\n    \"/tonsquare\": \"\\u3327\",\n    \"/topHat\": \"\\u1F3A9\",\n    \"/topUpwardsArrowAbove\": \"\\u1F51D\",\n    \"/topatakthai\": \"\\u0E0F\",\n    \"/tortoiseshellbracketleft\": \"\\u3014\",\n    \"/tortoiseshellbracketleftsmall\": \"\\uFE5D\",\n    \"/tortoiseshellbracketleftvertical\": \"\\uFE39\",\n    \"/tortoiseshellbracketright\": \"\\u3015\",\n    \"/tortoiseshellbracketrightsmall\": \"\\uFE5E\",\n    \"/tortoiseshellbracketrightvertical\": \"\\uFE3A\",\n    \"/totalrunout\": \"\\u2330\",\n    \"/totaothai\": \"\\u0E15\",\n    \"/tpalatalhook\": \"\\u01AB\",\n    \"/tparen\": \"\\u24AF\",\n    \"/tparenthesized\": \"\\u24AF\",\n    \"/trackball\": \"\\u1F5B2\",\n    \"/tractor\": \"\\u1F69C\",\n    \"/trademark\": \"\\u2122\",\n    \"/trademarksans\": \"\\uF8EA\",\n    \"/trademarkserif\": \"\\uF6DB\",\n    \"/train\": \"\\u1F686\",\n    \"/tram\": \"\\u1F68A\",\n    \"/tramCar\": \"\\u1F68B\",\n    \"/trapeziumwhite\": \"\\u23E2\",\n    \"/tresillo\": \"\\uA72B\",\n    \"/tretroflex\": \"\\u0288\",\n    \"/tretroflexhook\": \"\\u0288\",\n    \"/triagdn\": \"\\u25BC\",\n    \"/triaglf\": \"\\u25C4\",\n    \"/triagrt\": \"\\u25BA\",\n    \"/triagup\": \"\\u25B2\",\n    \"/triangleWithRoundedCorners\": \"\\u1F6C6\",\n    \"/triangledotupwhite\": \"\\u25EC\",\n    \"/triangledownblack\": \"\\u25BC\",\n    \"/triangledownsmallblack\": \"\\u25BE\",\n    \"/triangledownsmallwhite\": \"\\u25BF\",\n    \"/triangledownwhite\": \"\\u25BD\",\n    \"/trianglehalfupleftblack\": \"\\u25ED\",\n    \"/trianglehalfuprightblack\": \"\\u25EE\",\n    \"/triangleleftblack\": \"\\u25C0\",\n    \"/triangleleftsmallblack\": \"\\u25C2\",\n    \"/triangleleftsmallwhite\": \"\\u25C3\",\n    \"/triangleleftwhite\": \"\\u25C1\",\n    \"/triangleright\": \"\\u22BF\",\n    \"/trianglerightblack\": \"\\u25B6\",\n    \"/trianglerightsmallblack\": \"\\u25B8\",\n    \"/trianglerightsmallwhite\": \"\\u25B9\",\n    \"/trianglerightwhite\": \"\\u25B7\",\n    \"/triangleupblack\": \"\\u25B2\",\n    \"/triangleupsmallblack\": \"\\u25B4\",\n    \"/triangleupsmallwhite\": \"\\u25B5\",\n    \"/triangleupwhite\": \"\\u25B3\",\n    \"/triangularFlagOnPost\": \"\\u1F6A9\",\n    \"/triangularRuler\": \"\\u1F4D0\",\n    \"/triangularbullet\": \"\\u2023\",\n    \"/tricolon\": \"\\u205D\",\n    \"/tricontainingtriwhiteanglesmall\": \"\\u27C1\",\n    \"/tridentEmblem\": \"\\u1F531\",\n    \"/trigramearth\": \"\\u2637\",\n    \"/trigramfire\": \"\\u2632\",\n    \"/trigramheaven\": \"\\u2630\",\n    \"/trigramlake\": \"\\u2631\",\n    \"/trigrammountain\": \"\\u2636\",\n    \"/trigramthunder\": \"\\u2633\",\n    \"/trigramwater\": \"\\u2635\",\n    \"/trigramwind\": \"\\u2634\",\n    \"/triplearrowleft\": \"\\u21DA\",\n    \"/triplearrowright\": \"\\u21DB\",\n    \"/tripledot\": \"\\u061E\",\n    \"/trisememetrical\": \"\\u23D7\",\n    \"/trns:baby\": \"\\u1F6BC\",\n    \"/trolleybus\": \"\\u1F68E\",\n    \"/trophy\": \"\\u1F3C6\",\n    \"/tropicalDrink\": \"\\u1F379\",\n    \"/tropicalFish\": \"\\u1F420\",\n    \"/truckblack\": \"\\u26DF\",\n    \"/true\": \"\\u22A8\",\n    \"/trumpet\": \"\\u1F3BA\",\n    \"/ts\": \"\\u02A6\",\n    \"/tsadi\": \"\\u05E6\",\n    \"/tsadi:hb\": \"\\u05E6\",\n    \"/tsadidagesh\": \"\\uFB46\",\n    \"/tsadidageshhebrew\": \"\\uFB46\",\n    \"/tsadihebrew\": \"\\u05E6\",\n    \"/tsadiwithdagesh:hb\": \"\\uFB46\",\n    \"/tsecyr\": \"\\u0446\",\n    \"/tsecyrillic\": \"\\u0446\",\n    \"/tsere\": \"\\u05B5\",\n    \"/tsere12\": \"\\u05B5\",\n    \"/tsere1e\": \"\\u05B5\",\n    \"/tsere2b\": \"\\u05B5\",\n    \"/tsere:hb\": \"\\u05B5\",\n    \"/tserehebrew\": \"\\u05B5\",\n    \"/tserenarrowhebrew\": \"\\u05B5\",\n    \"/tserequarterhebrew\": \"\\u05B5\",\n    \"/tserewidehebrew\": \"\\u05B5\",\n    \"/tshecyr\": \"\\u045B\",\n    \"/tshecyrillic\": \"\\u045B\",\n    \"/tsinnorit:hb\": \"\\u05AE\",\n    \"/tstroke\": \"\\u2C66\",\n    \"/tsuperior\": \"\\uF6F3\",\n    \"/ttabengali\": \"\\u099F\",\n    \"/ttadeva\": \"\\u091F\",\n    \"/ttagujarati\": \"\\u0A9F\",\n    \"/ttagurmukhi\": \"\\u0A1F\",\n    \"/ttamahaprana\": \"\\uA99C\",\n    \"/tteh\": \"\\u0679\",\n    \"/tteh.fina\": \"\\uFB67\",\n    \"/tteh.init\": \"\\uFB68\",\n    \"/tteh.isol\": \"\\uFB66\",\n    \"/tteh.medi\": \"\\uFB69\",\n    \"/tteharabic\": \"\\u0679\",\n    \"/tteheh\": \"\\u067A\",\n    \"/tteheh.fina\": \"\\uFB5F\",\n    \"/tteheh.init\": \"\\uFB60\",\n    \"/tteheh.isol\": \"\\uFB5E\",\n    \"/tteheh.medi\": \"\\uFB61\",\n    \"/ttehfinalarabic\": \"\\uFB67\",\n    \"/ttehinitialarabic\": \"\\uFB68\",\n    \"/ttehmedialarabic\": \"\\uFB69\",\n    \"/tthabengali\": \"\\u09A0\",\n    \"/tthadeva\": \"\\u0920\",\n    \"/tthagujarati\": \"\\u0AA0\",\n    \"/tthagurmukhi\": \"\\u0A20\",\n    \"/tturned\": \"\\u0287\",\n    \"/tucirclekatakana\": \"\\u32E1\",\n    \"/tugrik\": \"\\u20AE\",\n    \"/tuhiragana\": \"\\u3064\",\n    \"/tukatakana\": \"\\u30C4\",\n    \"/tukatakanahalfwidth\": \"\\uFF82\",\n    \"/tulip\": \"\\u1F337\",\n    \"/tum\": \"\\uA777\",\n    \"/turkishlira\": \"\\u20BA\",\n    \"/turnedOkHandSign\": \"\\u1F58F\",\n    \"/turnedcomma\": \"\\u2E32\",\n    \"/turneddagger\": \"\\u2E38\",\n    \"/turneddigitthree\": \"\\u218B\",\n    \"/turneddigittwo\": \"\\u218A\",\n    \"/turnedpiselehpada\": \"\\uA9CD\",\n    \"/turnedsemicolon\": \"\\u2E35\",\n    \"/turnedshogipieceblack\": \"\\u26CA\",\n    \"/turnedshogipiecewhite\": \"\\u26C9\",\n    \"/turnstiledblverticalbarright\": \"\\u22AB\",\n    \"/turnstileleftrightdbl\": \"\\u27DA\",\n    \"/turnstiletplverticalbarright\": \"\\u22AA\",\n    \"/turtle\": \"\\u1F422\",\n    \"/tusmallhiragana\": \"\\u3063\",\n    \"/tusmallkatakana\": \"\\u30C3\",\n    \"/tusmallkatakanahalfwidth\": \"\\uFF6F\",\n    \"/twelve.roman\": \"\\u216B\",\n    \"/twelve.romansmall\": \"\\u217B\",\n    \"/twelvecircle\": \"\\u246B\",\n    \"/twelvecircleblack\": \"\\u24EC\",\n    \"/twelveparen\": \"\\u247F\",\n    \"/twelveparenthesized\": \"\\u247F\",\n    \"/twelveperiod\": \"\\u2493\",\n    \"/twelveroman\": \"\\u217B\",\n    \"/twenty-twopointtwosquare\": \"\\u1F1A2\",\n    \"/twentycircle\": \"\\u2473\",\n    \"/twentycircleblack\": \"\\u24F4\",\n    \"/twentycirclesquare\": \"\\u3249\",\n    \"/twentyeightcircle\": \"\\u3258\",\n    \"/twentyfivecircle\": \"\\u3255\",\n    \"/twentyfourcircle\": \"\\u3254\",\n    \"/twentyhangzhou\": \"\\u5344\",\n    \"/twentyninecircle\": \"\\u3259\",\n    \"/twentyonecircle\": \"\\u3251\",\n    \"/twentyparen\": \"\\u2487\",\n    \"/twentyparenthesized\": \"\\u2487\",\n    \"/twentyperiod\": \"\\u249B\",\n    \"/twentysevencircle\": \"\\u3257\",\n    \"/twentysixcircle\": \"\\u3256\",\n    \"/twentythreecircle\": \"\\u3253\",\n    \"/twentytwocircle\": \"\\u3252\",\n    \"/twistedRightwardsArrows\": \"\\u1F500\",\n    \"/two\": \"\\u0032\",\n    \"/two.inferior\": \"\\u2082\",\n    \"/two.roman\": \"\\u2161\",\n    \"/two.romansmall\": \"\\u2171\",\n    \"/twoButtonMouse\": \"\\u1F5B0\",\n    \"/twoHearts\": \"\\u1F495\",\n    \"/twoMenHoldingHands\": \"\\u1F46C\",\n    \"/twoSpeechBubbles\": \"\\u1F5EA\",\n    \"/twoWomenHoldingHands\": \"\\u1F46D\",\n    \"/twoarabic\": \"\\u0662\",\n    \"/twoasterisksalignedvertically\": \"\\u2051\",\n    \"/twobengali\": \"\\u09E8\",\n    \"/twocircle\": \"\\u2461\",\n    \"/twocircledbl\": \"\\u24F6\",\n    \"/twocircleinversesansserif\": \"\\u278B\",\n    \"/twocomma\": \"\\u1F103\",\n    \"/twodeva\": \"\\u0968\",\n    \"/twodotenleader\": \"\\u2025\",\n    \"/twodotleader\": \"\\u2025\",\n    \"/twodotleadervertical\": \"\\uFE30\",\n    \"/twodotpunctuation\": \"\\u205A\",\n    \"/twodotsoveronedot\": \"\\u2E2A\",\n    \"/twofar\": \"\\u06F2\",\n    \"/twofifths\": \"\\u2156\",\n    \"/twogujarati\": \"\\u0AE8\",\n    \"/twogurmukhi\": \"\\u0A68\",\n    \"/twohackarabic\": \"\\u0662\",\n    \"/twohangzhou\": \"\\u3022\",\n    \"/twoideographiccircled\": \"\\u3281\",\n    \"/twoideographicparen\": \"\\u3221\",\n    \"/twoinferior\": \"\\u2082\",\n    \"/twoksquare\": \"\\u1F19D\",\n    \"/twomonospace\": \"\\uFF12\",\n    \"/twonumeratorbengali\": \"\\u09F5\",\n    \"/twooldstyle\": \"\\uF732\",\n    \"/twoparen\": \"\\u2475\",\n    \"/twoparenthesized\": \"\\u2475\",\n    \"/twoperiod\": \"\\u2489\",\n    \"/twopersian\": \"\\u06F2\",\n    \"/tworoman\": \"\\u2171\",\n    \"/twoshortsjoinedmetrical\": \"\\u23D6\",\n    \"/twoshortsoverlongmetrical\": \"\\u23D5\",\n    \"/twostroke\": \"\\u01BB\",\n    \"/twosuperior\": \"\\u00B2\",\n    \"/twothai\": \"\\u0E52\",\n    \"/twothirds\": \"\\u2154\",\n    \"/twowayleftwaytrafficblack\": \"\\u26D6\",\n    \"/twowayleftwaytrafficwhite\": \"\\u26D7\",\n    \"/tz\": \"\\uA729\",\n    \"/u\": \"\\u0075\",\n    \"/u.fina\": \"\\uFBD8\",\n    \"/u.isol\": \"\\uFBD7\",\n    \"/uacute\": \"\\u00FA\",\n    \"/uacutedblcyr\": \"\\u04F3\",\n    \"/ubar\": \"\\u0289\",\n    \"/ubengali\": \"\\u0989\",\n    \"/ubopomofo\": \"\\u3128\",\n    \"/ubracketleft\": \"\\u2E26\",\n    \"/ubracketright\": \"\\u2E27\",\n    \"/ubreve\": \"\\u016D\",\n    \"/ucaron\": \"\\u01D4\",\n    \"/ucircle\": \"\\u24E4\",\n    \"/ucirclekatakana\": \"\\u32D2\",\n    \"/ucircumflex\": \"\\u00FB\",\n    \"/ucircumflexbelow\": \"\\u1E77\",\n    \"/ucyr\": \"\\u0443\",\n    \"/ucyrillic\": \"\\u0443\",\n    \"/udattadeva\": \"\\u0951\",\n    \"/udblacute\": \"\\u0171\",\n    \"/udblgrave\": \"\\u0215\",\n    \"/udeva\": \"\\u0909\",\n    \"/udieresis\": \"\\u00FC\",\n    \"/udieresisacute\": \"\\u01D8\",\n    \"/udieresisbelow\": \"\\u1E73\",\n    \"/udieresiscaron\": \"\\u01DA\",\n    \"/udieresiscyr\": \"\\u04F1\",\n    \"/udieresiscyrillic\": \"\\u04F1\",\n    \"/udieresisgrave\": \"\\u01DC\",\n    \"/udieresismacron\": \"\\u01D6\",\n    \"/udotbelow\": \"\\u1EE5\",\n    \"/ugrave\": \"\\u00F9\",\n    \"/ugravedbl\": \"\\u0215\",\n    \"/ugujarati\": \"\\u0A89\",\n    \"/ugurmukhi\": \"\\u0A09\",\n    \"/uhamza\": \"\\u0677\",\n    \"/uhamza.isol\": \"\\uFBDD\",\n    \"/uhdsquare\": \"\\u1F1AB\",\n    \"/uhiragana\": \"\\u3046\",\n    \"/uhoi\": \"\\u1EE7\",\n    \"/uhookabove\": \"\\u1EE7\",\n    \"/uhorn\": \"\\u01B0\",\n    \"/uhornacute\": \"\\u1EE9\",\n    \"/uhorndotbelow\": \"\\u1EF1\",\n    \"/uhorngrave\": \"\\u1EEB\",\n    \"/uhornhoi\": \"\\u1EED\",\n    \"/uhornhookabove\": \"\\u1EED\",\n    \"/uhorntilde\": \"\\u1EEF\",\n    \"/uhungarumlaut\": \"\\u0171\",\n    \"/uhungarumlautcyrillic\": \"\\u04F3\",\n    \"/uighurkazakhkirghizalefmaksura.init\": \"\\uFBE8\",\n    \"/uighurkazakhkirghizalefmaksura.medi\": \"\\uFBE9\",\n    \"/uighurkirghizyeh.init_hamzaabove.medi_alefmaksura.fina\": \"\\uFBF9\",\n    \"/uighurkirghizyeh.init_hamzaabove.medi_alefmaksura.medi\": \"\\uFBFB\",\n    \"/uighurkirghizyeh.medi_hamzaabove.medi_alefmaksura.fina\": \"\\uFBFA\",\n    \"/uinvertedbreve\": \"\\u0217\",\n    \"/ukatakana\": \"\\u30A6\",\n    \"/ukatakanahalfwidth\": \"\\uFF73\",\n    \"/ukcyr\": \"\\u0479\",\n    \"/ukcyrillic\": \"\\u0479\",\n    \"/ukorean\": \"\\u315C\",\n    \"/um\": \"\\uA778\",\n    \"/umacron\": \"\\u016B\",\n    \"/umacroncyr\": \"\\u04EF\",\n    \"/umacroncyrillic\": \"\\u04EF\",\n    \"/umacrondieresis\": \"\\u1E7B\",\n    \"/umatragurmukhi\": \"\\u0A41\",\n    \"/umbrella\": \"\\u2602\",\n    \"/umbrellaonground\": \"\\u26F1\",\n    \"/umbrellaraindrops\": \"\\u2614\",\n    \"/umonospace\": \"\\uFF55\",\n    \"/unamusedFace\": \"\\u1F612\",\n    \"/unaspiratedmod\": \"\\u02ED\",\n    \"/underscore\": \"\\u005F\",\n    \"/underscorecenterline\": \"\\uFE4E\",\n    \"/underscoredashed\": \"\\uFE4D\",\n    \"/underscoredbl\": \"\\u2017\",\n    \"/underscoremonospace\": \"\\uFF3F\",\n    \"/underscorevertical\": \"\\uFE33\",\n    \"/underscorewavy\": \"\\uFE4F\",\n    \"/underscorewavyvertical\": \"\\uFE34\",\n    \"/undertie\": \"\\u203F\",\n    \"/undo\": \"\\u238C\",\n    \"/union\": \"\\u222A\",\n    \"/unionarray\": \"\\u22C3\",\n    \"/uniondbl\": \"\\u22D3\",\n    \"/universal\": \"\\u2200\",\n    \"/unmarriedpartnership\": \"\\u26AF\",\n    \"/uogonek\": \"\\u0173\",\n    \"/uonsquare\": \"\\u3306\",\n    \"/upPointingAirplane\": \"\\u1F6E7\",\n    \"/upPointingMilitaryAirplane\": \"\\u1F6E6\",\n    \"/upPointingSmallAirplane\": \"\\u1F6E8\",\n    \"/uparen\": \"\\u24B0\",\n    \"/uparenthesized\": \"\\u24B0\",\n    \"/uparrowleftofdownarrow\": \"\\u21C5\",\n    \"/upblock\": \"\\u2580\",\n    \"/updblhorzsng\": \"\\u2568\",\n    \"/updblleftsng\": \"\\u255C\",\n    \"/updblrightsng\": \"\\u2559\",\n    \"/upheavydnhorzlight\": \"\\u2540\",\n    \"/upheavyhorzlight\": \"\\u2538\",\n    \"/upheavyleftdnlight\": \"\\u2526\",\n    \"/upheavyleftlight\": \"\\u251A\",\n    \"/upheavyrightdnlight\": \"\\u251E\",\n    \"/upheavyrightlight\": \"\\u2516\",\n    \"/uplightdnhorzheavy\": \"\\u2548\",\n    \"/uplighthorzheavy\": \"\\u2537\",\n    \"/uplightleftdnheavy\": \"\\u252A\",\n    \"/uplightleftheavy\": \"\\u2519\",\n    \"/uplightrightdnheavy\": \"\\u2522\",\n    \"/uplightrightheavy\": \"\\u2515\",\n    \"/upperHalfBlock\": \"\\u2580\",\n    \"/upperOneEighthBlock\": \"\\u2594\",\n    \"/upperRightShadowedWhiteCircle\": \"\\u1F53F\",\n    \"/upperdothebrew\": \"\\u05C4\",\n    \"/upperhalfcircle\": \"\\u25E0\",\n    \"/upperhalfcircleinversewhite\": \"\\u25DA\",\n    \"/upperquadrantcirculararcleft\": \"\\u25DC\",\n    \"/upperquadrantcirculararcright\": \"\\u25DD\",\n    \"/uppertriangleleft\": \"\\u25F8\",\n    \"/uppertriangleleftblack\": \"\\u25E4\",\n    \"/uppertriangleright\": \"\\u25F9\",\n    \"/uppertrianglerightblack\": \"\\u25E5\",\n    \"/upsideDownFace\": \"\\u1F643\",\n    \"/upsilon\": \"\\u03C5\",\n    \"/upsilonacute\": \"\\u1F7B\",\n    \"/upsilonasper\": \"\\u1F51\",\n    \"/upsilonasperacute\": \"\\u1F55\",\n    \"/upsilonaspergrave\": \"\\u1F53\",\n    \"/upsilonaspertilde\": \"\\u1F57\",\n    \"/upsilonbreve\": \"\\u1FE0\",\n    \"/upsilondieresis\": \"\\u03CB\",\n    \"/upsilondieresisacute\": \"\\u1FE3\",\n    \"/upsilondieresisgrave\": \"\\u1FE2\",\n    \"/upsilondieresistilde\": \"\\u1FE7\",\n    \"/upsilondieresistonos\": \"\\u03B0\",\n    \"/upsilongrave\": \"\\u1F7A\",\n    \"/upsilonlatin\": \"\\u028A\",\n    \"/upsilonlenis\": \"\\u1F50\",\n    \"/upsilonlenisacute\": \"\\u1F54\",\n    \"/upsilonlenisgrave\": \"\\u1F52\",\n    \"/upsilonlenistilde\": \"\\u1F56\",\n    \"/upsilontilde\": \"\\u1FE6\",\n    \"/upsilontonos\": \"\\u03CD\",\n    \"/upsilonwithmacron\": \"\\u1FE1\",\n    \"/upsnghorzdbl\": \"\\u2567\",\n    \"/upsngleftdbl\": \"\\u255B\",\n    \"/upsngrightdbl\": \"\\u2558\",\n    \"/uptackbelowcmb\": \"\\u031D\",\n    \"/uptackmod\": \"\\u02D4\",\n    \"/upwithexclamationmarksquare\": \"\\u1F199\",\n    \"/uragurmukhi\": \"\\u0A73\",\n    \"/uranus\": \"\\u2645\",\n    \"/uring\": \"\\u016F\",\n    \"/ushortcyr\": \"\\u045E\",\n    \"/ushortcyrillic\": \"\\u045E\",\n    \"/usmallhiragana\": \"\\u3045\",\n    \"/usmallkatakana\": \"\\u30A5\",\n    \"/usmallkatakanahalfwidth\": \"\\uFF69\",\n    \"/usmod\": \"\\uA770\",\n    \"/ustraightcyr\": \"\\u04AF\",\n    \"/ustraightcyrillic\": \"\\u04AF\",\n    \"/ustraightstrokecyr\": \"\\u04B1\",\n    \"/ustraightstrokecyrillic\": \"\\u04B1\",\n    \"/utilde\": \"\\u0169\",\n    \"/utildeacute\": \"\\u1E79\",\n    \"/utildebelow\": \"\\u1E75\",\n    \"/uubengali\": \"\\u098A\",\n    \"/uudeva\": \"\\u090A\",\n    \"/uugujarati\": \"\\u0A8A\",\n    \"/uugurmukhi\": \"\\u0A0A\",\n    \"/uumatragurmukhi\": \"\\u0A42\",\n    \"/uuvowelsignbengali\": \"\\u09C2\",\n    \"/uuvowelsigndeva\": \"\\u0942\",\n    \"/uuvowelsigngujarati\": \"\\u0AC2\",\n    \"/uvowelsignbengali\": \"\\u09C1\",\n    \"/uvowelsigndeva\": \"\\u0941\",\n    \"/uvowelsigngujarati\": \"\\u0AC1\",\n    \"/v\": \"\\u0076\",\n    \"/vadeva\": \"\\u0935\",\n    \"/vagujarati\": \"\\u0AB5\",\n    \"/vagurmukhi\": \"\\u0A35\",\n    \"/vakatakana\": \"\\u30F7\",\n    \"/vanedownfunc\": \"\\u2356\",\n    \"/vaneleftfunc\": \"\\u2345\",\n    \"/vanerightfunc\": \"\\u2346\",\n    \"/vaneupfunc\": \"\\u234F\",\n    \"/varikajudeospanish:hb\": \"\\uFB1E\",\n    \"/vav\": \"\\u05D5\",\n    \"/vav:hb\": \"\\u05D5\",\n    \"/vav_vav:hb\": \"\\u05F0\",\n    \"/vav_yod:hb\": \"\\u05F1\",\n    \"/vavdagesh\": \"\\uFB35\",\n    \"/vavdagesh65\": \"\\uFB35\",\n    \"/vavdageshhebrew\": \"\\uFB35\",\n    \"/vavhebrew\": \"\\u05D5\",\n    \"/vavholam\": \"\\uFB4B\",\n    \"/vavholamhebrew\": \"\\uFB4B\",\n    \"/vavvavhebrew\": \"\\u05F0\",\n    \"/vavwithdagesh:hb\": \"\\uFB35\",\n    \"/vavwithholam:hb\": \"\\uFB4B\",\n    \"/vavyodhebrew\": \"\\u05F1\",\n    \"/vcircle\": \"\\u24E5\",\n    \"/vcurl\": \"\\u2C74\",\n    \"/vdiagonalstroke\": \"\\uA75F\",\n    \"/vdotbelow\": \"\\u1E7F\",\n    \"/ve.fina\": \"\\uFBDF\",\n    \"/ve.isol\": \"\\uFBDE\",\n    \"/ve:abovetonecandra\": \"\\u1CF4\",\n    \"/ve:anusvaraantargomukhasign\": \"\\u1CE9\",\n    \"/ve:anusvarabahirgomukhasign\": \"\\u1CEA\",\n    \"/ve:anusvarasignlong\": \"\\u1CEF\",\n    \"/ve:anusvaraubhayatomukhasign\": \"\\u1CF1\",\n    \"/ve:anusvaravamagomukhasign\": \"\\u1CEB\",\n    \"/ve:anusvaravamagomukhawithtailsign\": \"\\u1CEC\",\n    \"/ve:ardhavisargasign\": \"\\u1CF2\",\n    \"/ve:atharvaindependentsvaritatone\": \"\\u1CE1\",\n    \"/ve:atikramasign\": \"\\u1CF7\",\n    \"/ve:belowtonecandra\": \"\\u1CD8\",\n    \"/ve:dotbelowtone\": \"\\u1CDD\",\n    \"/ve:hexiformanusvarasignlong\": \"\\u1CEE\",\n    \"/ve:jihvamuliyasign\": \"\\u1CF5\",\n    \"/ve:karshanatone\": \"\\u1CD0\",\n    \"/ve:kathakaanudattatone\": \"\\u1CDC\",\n    \"/ve:nihshvasasign\": \"\\u1CD3\",\n    \"/ve:prenkhatone\": \"\\u1CD2\",\n    \"/ve:rigkashmiriindependentsvaritatone\": \"\\u1CE0\",\n    \"/ve:ringabovetone\": \"\\u1CF8\",\n    \"/ve:ringabovetonedbl\": \"\\u1CF9\",\n    \"/ve:rotatedardhavisargasign\": \"\\u1CF3\",\n    \"/ve:rthanganusvarasignlong\": \"\\u1CF0\",\n    \"/ve:sharatone\": \"\\u1CD1\",\n    \"/ve:svaritatonedbl\": \"\\u1CDA\",\n    \"/ve:svaritatonetpl\": \"\\u1CDB\",\n    \"/ve:threedotsbelowtone\": \"\\u1CDF\",\n    \"/ve:tiryaksign\": \"\\u1CED\",\n    \"/ve:twodotsbelowtone\": \"\\u1CDE\",\n    \"/ve:upadhmaniyasign\": \"\\u1CF6\",\n    \"/ve:visargaanudattasign\": \"\\u1CE5\",\n    \"/ve:visargaanudattasignreversed\": \"\\u1CE6\",\n    \"/ve:visargaanudattawithtailsign\": \"\\u1CE8\",\n    \"/ve:visargasvaritasign\": \"\\u1CE2\",\n    \"/ve:visargaudattasign\": \"\\u1CE3\",\n    \"/ve:visargaudattasignreversed\": \"\\u1CE4\",\n    \"/ve:visargaudattawithtailsign\": \"\\u1CE7\",\n    \"/ve:yajuraggravatedindependentsvaritatone\": \"\\u1CD5\",\n    \"/ve:yajurindependentsvaritatone\": \"\\u1CD6\",\n    \"/ve:yajurkathakaindependentsvaritaschroedertone\": \"\\u1CD9\",\n    \"/ve:yajurkathakaindependentsvaritatone\": \"\\u1CD7\",\n    \"/ve:yajurmidlinesvaritasign\": \"\\u1CD4\",\n    \"/vecyr\": \"\\u0432\",\n    \"/vecyrillic\": \"\\u0432\",\n    \"/veh\": \"\\u06A4\",\n    \"/veh.fina\": \"\\uFB6B\",\n    \"/veh.init\": \"\\uFB6C\",\n    \"/veh.isol\": \"\\uFB6A\",\n    \"/veh.medi\": \"\\uFB6D\",\n    \"/veharabic\": \"\\u06A4\",\n    \"/vehfinalarabic\": \"\\uFB6B\",\n    \"/vehinitialarabic\": \"\\uFB6C\",\n    \"/vehmedialarabic\": \"\\uFB6D\",\n    \"/vekatakana\": \"\\u30F9\",\n    \"/vend\": \"\\uA769\",\n    \"/venus\": \"\\u2640\",\n    \"/versicle\": \"\\u2123\",\n    \"/vert:bracketwhiteleft\": \"\\uFE17\",\n    \"/vert:brakcetwhiteright\": \"\\uFE18\",\n    \"/vert:colon\": \"\\uFE13\",\n    \"/vert:comma\": \"\\uFE10\",\n    \"/vert:ellipsishor\": \"\\uFE19\",\n    \"/vert:exclam\": \"\\uFE15\",\n    \"/vert:ideographiccomma\": \"\\uFE11\",\n    \"/vert:ideographicfullstop\": \"\\uFE12\",\n    \"/vert:question\": \"\\uFE16\",\n    \"/vert:semicolon\": \"\\uFE14\",\n    \"/vertdblhorzsng\": \"\\u256B\",\n    \"/vertdblleftsng\": \"\\u2562\",\n    \"/vertdblrightsng\": \"\\u255F\",\n    \"/vertheavyhorzlight\": \"\\u2542\",\n    \"/vertheavyleftlight\": \"\\u2528\",\n    \"/vertheavyrightlight\": \"\\u2520\",\n    \"/verticalTrafficLight\": \"\\u1F6A6\",\n    \"/verticalbar\": \"\\u007C\",\n    \"/verticalbardbl\": \"\\u2016\",\n    \"/verticalbarhorizontalstroke\": \"\\u27CA\",\n    \"/verticalbarwhitearrowonpedestalup\": \"\\u21ED\",\n    \"/verticalfourdots\": \"\\u205E\",\n    \"/verticalideographiciterationmark\": \"\\u303B\",\n    \"/verticalkanarepeatmark\": \"\\u3031\",\n    \"/verticalkanarepeatmarklowerhalf\": \"\\u3035\",\n    \"/verticalkanarepeatmarkupperhalf\": \"\\u3033\",\n    \"/verticalkanarepeatwithvoicedsoundmark\": \"\\u3032\",\n    \"/verticalkanarepeatwithvoicedsoundmarkupperhalf\": \"\\u3034\",\n    \"/verticallineabovecmb\": \"\\u030D\",\n    \"/verticallinebelowcmb\": \"\\u0329\",\n    \"/verticallinelowmod\": \"\\u02CC\",\n    \"/verticallinemod\": \"\\u02C8\",\n    \"/verticalmalestroke\": \"\\u26A8\",\n    \"/verticalsdbltrokearrowleft\": \"\\u21FA\",\n    \"/verticalsdbltrokearrowleftright\": \"\\u21FC\",\n    \"/verticalsdbltrokearrowright\": \"\\u21FB\",\n    \"/verticalstrokearrowleft\": \"\\u21F7\",\n    \"/verticalstrokearrowleftright\": \"\\u21F9\",\n    \"/verticalstrokearrowright\": \"\\u21F8\",\n    \"/vertlighthorzheavy\": \"\\u253F\",\n    \"/vertlightleftheavy\": \"\\u2525\",\n    \"/vertlightrightheavy\": \"\\u251D\",\n    \"/vertsnghorzdbl\": \"\\u256A\",\n    \"/vertsngleftdbl\": \"\\u2561\",\n    \"/vertsngrightdbl\": \"\\u255E\",\n    \"/verymuchgreater\": \"\\u22D9\",\n    \"/verymuchless\": \"\\u22D8\",\n    \"/vesta\": \"\\u26B6\",\n    \"/vewarmenian\": \"\\u057E\",\n    \"/vhook\": \"\\u028B\",\n    \"/vibrationMode\": \"\\u1F4F3\",\n    \"/videoCamera\": \"\\u1F4F9\",\n    \"/videoGame\": \"\\u1F3AE\",\n    \"/videocassette\": \"\\u1F4FC\",\n    \"/viewdatasquare\": \"\\u2317\",\n    \"/vikatakana\": \"\\u30F8\",\n    \"/violin\": \"\\u1F3BB\",\n    \"/viramabengali\": \"\\u09CD\",\n    \"/viramadeva\": \"\\u094D\",\n    \"/viramagujarati\": \"\\u0ACD\",\n    \"/virgo\": \"\\u264D\",\n    \"/visargabengali\": \"\\u0983\",\n    \"/visargadeva\": \"\\u0903\",\n    \"/visargagujarati\": \"\\u0A83\",\n    \"/visigothicz\": \"\\uA763\",\n    \"/vmonospace\": \"\\uFF56\",\n    \"/voarmenian\": \"\\u0578\",\n    \"/vodsquare\": \"\\u1F1AC\",\n    \"/voicediterationhiragana\": \"\\u309E\",\n    \"/voicediterationkatakana\": \"\\u30FE\",\n    \"/voicedmarkkana\": \"\\u309B\",\n    \"/voicedmarkkanahalfwidth\": \"\\uFF9E\",\n    \"/voicingmod\": \"\\u02EC\",\n    \"/vokatakana\": \"\\u30FA\",\n    \"/volapukae\": \"\\uA79B\",\n    \"/volapukoe\": \"\\uA79D\",\n    \"/volapukue\": \"\\uA79F\",\n    \"/volcano\": \"\\u1F30B\",\n    \"/volleyball\": \"\\u1F3D0\",\n    \"/vovermfullwidth\": \"\\u33DE\",\n    \"/vowelVabove\": \"\\u065A\",\n    \"/voweldotbelow\": \"\\u065C\",\n    \"/vowelinvertedVabove\": \"\\u065B\",\n    \"/vparen\": \"\\u24B1\",\n    \"/vparenthesized\": \"\\u24B1\",\n    \"/vrighthook\": \"\\u2C71\",\n    \"/vssquare\": \"\\u1F19A\",\n    \"/vtilde\": \"\\u1E7D\",\n    \"/vturned\": \"\\u028C\",\n    \"/vuhiragana\": \"\\u3094\",\n    \"/vukatakana\": \"\\u30F4\",\n    \"/vwelsh\": \"\\u1EFD\",\n    \"/vy\": \"\\uA761\",\n    \"/w\": \"\\u0077\",\n    \"/wacirclekatakana\": \"\\u32FB\",\n    \"/wacute\": \"\\u1E83\",\n    \"/waekorean\": \"\\u3159\",\n    \"/wahiragana\": \"\\u308F\",\n    \"/wakatakana\": \"\\u30EF\",\n    \"/wakatakanahalfwidth\": \"\\uFF9C\",\n    \"/wakorean\": \"\\u3158\",\n    \"/waningCrescentMoon\": \"\\u1F318\",\n    \"/waningGibbousMoon\": \"\\u1F316\",\n    \"/warning\": \"\\u26A0\",\n    \"/wasmallhiragana\": \"\\u308E\",\n    \"/wasmallkatakana\": \"\\u30EE\",\n    \"/wastebasket\": \"\\u1F5D1\",\n    \"/watch\": \"\\u231A\",\n    \"/waterBuffalo\": \"\\u1F403\",\n    \"/waterCloset\": \"\\u1F6BE\",\n    \"/waterWave\": \"\\u1F30A\",\n    \"/waterideographiccircled\": \"\\u328C\",\n    \"/waterideographicparen\": \"\\u322C\",\n    \"/watermelon\": \"\\u1F349\",\n    \"/wattosquare\": \"\\u3357\",\n    \"/wavedash\": \"\\u301C\",\n    \"/wavingBlackFlag\": \"\\u1F3F4\",\n    \"/wavingHandSign\": \"\\u1F44B\",\n    \"/wavingWhiteFlag\": \"\\u1F3F3\",\n    \"/wavydash\": \"\\u3030\",\n    \"/wavyhamzabelow\": \"\\u065F\",\n    \"/wavyline\": \"\\u2307\",\n    \"/wavyunderscorevertical\": \"\\uFE34\",\n    \"/waw\": \"\\u0648\",\n    \"/waw.fina\": \"\\uFEEE\",\n    \"/waw.isol\": \"\\uFEED\",\n    \"/wawDigitThreeAbove\": \"\\u0779\",\n    \"/wawDigitTwoAbove\": \"\\u0778\",\n    \"/wawarabic\": \"\\u0648\",\n    \"/wawdotabove\": \"\\u06CF\",\n    \"/wawfinalarabic\": \"\\uFEEE\",\n    \"/wawhamza\": \"\\u0624\",\n    \"/wawhamza.fina\": \"\\uFE86\",\n    \"/wawhamza.isol\": \"\\uFE85\",\n    \"/wawhamzaabovearabic\": \"\\u0624\",\n    \"/wawhamzaabovefinalarabic\": \"\\uFE86\",\n    \"/wawhighhamza\": \"\\u0676\",\n    \"/wawring\": \"\\u06C4\",\n    \"/wawsmall\": \"\\u06E5\",\n    \"/wawtwodotsabove\": \"\\u06CA\",\n    \"/waxingCrescentMoon\": \"\\u1F312\",\n    \"/waxingGibbousMoon\": \"\\u1F314\",\n    \"/wbfullwidth\": \"\\u33DD\",\n    \"/wbsquare\": \"\\u33DD\",\n    \"/wcircle\": \"\\u24E6\",\n    \"/wcircumflex\": \"\\u0175\",\n    \"/wcsquare\": \"\\u1F14F\",\n    \"/wcsquareblack\": \"\\u1F18F\",\n    \"/wdieresis\": \"\\u1E85\",\n    \"/wdot\": \"\\u1E87\",\n    \"/wdotaccent\": \"\\u1E87\",\n    \"/wdotbelow\": \"\\u1E89\",\n    \"/wearyCatFace\": \"\\u1F640\",\n    \"/wearyFace\": \"\\u1F629\",\n    \"/wecirclekatakana\": \"\\u32FD\",\n    \"/wecyr\": \"\\u051D\",\n    \"/wedding\": \"\\u1F492\",\n    \"/wehiragana\": \"\\u3091\",\n    \"/weierstrass\": \"\\u2118\",\n    \"/weightLifter\": \"\\u1F3CB\",\n    \"/wekatakana\": \"\\u30F1\",\n    \"/wekorean\": \"\\u315E\",\n    \"/weokorean\": \"\\u315D\",\n    \"/westsyriaccross\": \"\\u2670\",\n    \"/wgrave\": \"\\u1E81\",\n    \"/whale\": \"\\u1F40B\",\n    \"/wheelchair\": \"\\u267F\",\n    \"/wheelofdharma\": \"\\u2638\",\n    \"/whiteDownPointingBackhandIndex\": \"\\u1F447\",\n    \"/whiteDownPointingLeftHandIndex\": \"\\u1F597\",\n    \"/whiteFlower\": \"\\u1F4AE\",\n    \"/whiteHardShellFloppyDisk\": \"\\u1F5AB\",\n    \"/whiteLatinCross\": \"\\u1F546\",\n    \"/whiteLeftPointingBackhandIndex\": \"\\u1F448\",\n    \"/whitePennant\": \"\\u1F3F1\",\n    \"/whiteRightPointingBackhandIndex\": \"\\u1F449\",\n    \"/whiteSquareButton\": \"\\u1F533\",\n    \"/whiteSun\": \"\\u1F323\",\n    \"/whiteSunBehindCloud\": \"\\u1F325\",\n    \"/whiteSunBehindCloudRain\": \"\\u1F326\",\n    \"/whiteSunSmallCloud\": \"\\u1F324\",\n    \"/whiteTouchtoneTelephone\": \"\\u1F57E\",\n    \"/whiteUpPointingBackhandIndex\": \"\\u1F446\",\n    \"/whitearrowdown\": \"\\u21E9\",\n    \"/whitearrowfromwallright\": \"\\u21F0\",\n    \"/whitearrowleft\": \"\\u21E6\",\n    \"/whitearrowonpedestalup\": \"\\u21EB\",\n    \"/whitearrowright\": \"\\u21E8\",\n    \"/whitearrowup\": \"\\u21E7\",\n    \"/whitearrowupdown\": \"\\u21F3\",\n    \"/whitearrowupfrombar\": \"\\u21EA\",\n    \"/whitebullet\": \"\\u25E6\",\n    \"/whitecircle\": \"\\u25CB\",\n    \"/whitecircleinverse\": \"\\u25D9\",\n    \"/whitecornerbracketleft\": \"\\u300E\",\n    \"/whitecornerbracketleftvertical\": \"\\uFE43\",\n    \"/whitecornerbracketright\": \"\\u300F\",\n    \"/whitecornerbracketrightvertical\": \"\\uFE44\",\n    \"/whitedblarrowonpedestalup\": \"\\u21EF\",\n    \"/whitedblarrowup\": \"\\u21EE\",\n    \"/whitediamond\": \"\\u25C7\",\n    \"/whitediamondcontainingblacksmalldiamond\": \"\\u25C8\",\n    \"/whitedownpointingsmalltriangle\": \"\\u25BF\",\n    \"/whitedownpointingtriangle\": \"\\u25BD\",\n    \"/whiteleftpointingsmalltriangle\": \"\\u25C3\",\n    \"/whiteleftpointingtriangle\": \"\\u25C1\",\n    \"/whitelenticularbracketleft\": \"\\u3016\",\n    \"/whitelenticularbracketright\": \"\\u3017\",\n    \"/whiterightpointingsmalltriangle\": \"\\u25B9\",\n    \"/whiterightpointingtriangle\": \"\\u25B7\",\n    \"/whitesesamedot\": \"\\uFE46\",\n    \"/whitesmallsquare\": \"\\u25AB\",\n    \"/whitesmilingface\": \"\\u263A\",\n    \"/whitesquare\": \"\\u25A1\",\n    \"/whitesquarebracketleft\": \"\\u301A\",\n    \"/whitesquarebracketright\": \"\\u301B\",\n    \"/whitestar\": \"\\u2606\",\n    \"/whitetelephone\": \"\\u260F\",\n    \"/whitetortoiseshellbracketleft\": \"\\u3018\",\n    \"/whitetortoiseshellbracketright\": \"\\u3019\",\n    \"/whiteuppointingsmalltriangle\": \"\\u25B5\",\n    \"/whiteuppointingtriangle\": \"\\u25B3\",\n    \"/whook\": \"\\u2C73\",\n    \"/wicirclekatakana\": \"\\u32FC\",\n    \"/wigglylinevertical\": \"\\u2E3E\",\n    \"/wignyan\": \"\\uA983\",\n    \"/wihiragana\": \"\\u3090\",\n    \"/wikatakana\": \"\\u30F0\",\n    \"/wikorean\": \"\\u315F\",\n    \"/windBlowingFace\": \"\\u1F32C\",\n    \"/windChime\": \"\\u1F390\",\n    \"/windupada\": \"\\uA9C6\",\n    \"/wineGlass\": \"\\u1F377\",\n    \"/winkingFace\": \"\\u1F609\",\n    \"/wiredKeyboard\": \"\\u1F5AE\",\n    \"/wmonospace\": \"\\uFF57\",\n    \"/wocirclekatakana\": \"\\u32FE\",\n    \"/wohiragana\": \"\\u3092\",\n    \"/wokatakana\": \"\\u30F2\",\n    \"/wokatakanahalfwidth\": \"\\uFF66\",\n    \"/wolfFace\": \"\\u1F43A\",\n    \"/woman\": \"\\u1F469\",\n    \"/womanBunnyEars\": \"\\u1F46F\",\n    \"/womansBoots\": \"\\u1F462\",\n    \"/womansClothes\": \"\\u1F45A\",\n    \"/womansHat\": \"\\u1F452\",\n    \"/womansSandal\": \"\\u1F461\",\n    \"/womens\": \"\\u1F6BA\",\n    \"/won\": \"\\u20A9\",\n    \"/wonmonospace\": \"\\uFFE6\",\n    \"/woodideographiccircled\": \"\\u328D\",\n    \"/woodideographicparen\": \"\\u322D\",\n    \"/wordjoiner\": \"\\u2060\",\n    \"/wordseparatormiddledot\": \"\\u2E31\",\n    \"/worldMap\": \"\\u1F5FA\",\n    \"/worriedFace\": \"\\u1F61F\",\n    \"/wowaenthai\": \"\\u0E27\",\n    \"/wparen\": \"\\u24B2\",\n    \"/wparenthesized\": \"\\u24B2\",\n    \"/wrappedPresent\": \"\\u1F381\",\n    \"/wreathproduct\": \"\\u2240\",\n    \"/wrench\": \"\\u1F527\",\n    \"/wring\": \"\\u1E98\",\n    \"/wsuperior\": \"\\u02B7\",\n    \"/wsupmod\": \"\\u02B7\",\n    \"/wturned\": \"\\u028D\",\n    \"/wulumelikvowel\": \"\\uA9B7\",\n    \"/wuluvowel\": \"\\uA9B6\",\n    \"/wynn\": \"\\u01BF\",\n    \"/x\": \"\\u0078\",\n    \"/x.inferior\": \"\\u2093\",\n    \"/xabovecmb\": \"\\u033D\",\n    \"/xatailcyr\": \"\\u04B3\",\n    \"/xbopomofo\": \"\\u3112\",\n    \"/xcircle\": \"\\u24E7\",\n    \"/xdieresis\": \"\\u1E8D\",\n    \"/xdot\": \"\\u1E8B\",\n    \"/xdotaccent\": \"\\u1E8B\",\n    \"/xeharmenian\": \"\\u056D\",\n    \"/xi\": \"\\u03BE\",\n    \"/xmonospace\": \"\\uFF58\",\n    \"/xor\": \"\\u22BB\",\n    \"/xparen\": \"\\u24B3\",\n    \"/xparenthesized\": \"\\u24B3\",\n    \"/xsuperior\": \"\\u02E3\",\n    \"/xsupmod\": \"\\u02E3\",\n    \"/y\": \"\\u0079\",\n    \"/yaadosquare\": \"\\u334E\",\n    \"/yaarusquare\": \"\\u334F\",\n    \"/yabengali\": \"\\u09AF\",\n    \"/yacirclekatakana\": \"\\u32F3\",\n    \"/yacute\": \"\\u00FD\",\n    \"/yacyr\": \"\\u044F\",\n    \"/yadeva\": \"\\u092F\",\n    \"/yaecyr\": \"\\u0519\",\n    \"/yaekorean\": \"\\u3152\",\n    \"/yagujarati\": \"\\u0AAF\",\n    \"/yagurmukhi\": \"\\u0A2F\",\n    \"/yahiragana\": \"\\u3084\",\n    \"/yakatakana\": \"\\u30E4\",\n    \"/yakatakanahalfwidth\": \"\\uFF94\",\n    \"/yakorean\": \"\\u3151\",\n    \"/yamakkanthai\": \"\\u0E4E\",\n    \"/yangtonemod\": \"\\u02EB\",\n    \"/yasmallhiragana\": \"\\u3083\",\n    \"/yasmallkatakana\": \"\\u30E3\",\n    \"/yasmallkatakanahalfwidth\": \"\\uFF6C\",\n    \"/yatcyr\": \"\\u0463\",\n    \"/yatcyrillic\": \"\\u0463\",\n    \"/ycircle\": \"\\u24E8\",\n    \"/ycircumflex\": \"\\u0177\",\n    \"/ydieresis\": \"\\u00FF\",\n    \"/ydot\": \"\\u1E8F\",\n    \"/ydotaccent\": \"\\u1E8F\",\n    \"/ydotbelow\": \"\\u1EF5\",\n    \"/yeh\": \"\\u064A\",\n    \"/yeh.fina\": \"\\uFEF2\",\n    \"/yeh.init\": \"\\uFEF3\",\n    \"/yeh.init_alefmaksura.fina\": \"\\uFC59\",\n    \"/yeh.init_hah.fina\": \"\\uFC56\",\n    \"/yeh.init_hah.medi\": \"\\uFCDB\",\n    \"/yeh.init_hamzaabove.medi_ae.fina\": \"\\uFBEC\",\n    \"/yeh.init_hamzaabove.medi_alef.fina\": \"\\uFBEA\",\n    \"/yeh.init_hamzaabove.medi_alefmaksura.fina\": \"\\uFC03\",\n    \"/yeh.init_hamzaabove.medi_e.fina\": \"\\uFBF6\",\n    \"/yeh.init_hamzaabove.medi_e.medi\": \"\\uFBF8\",\n    \"/yeh.init_hamzaabove.medi_hah.fina\": \"\\uFC01\",\n    \"/yeh.init_hamzaabove.medi_hah.medi\": \"\\uFC98\",\n    \"/yeh.init_hamzaabove.medi_heh.medi\": \"\\uFC9B\",\n    \"/yeh.init_hamzaabove.medi_jeem.fina\": \"\\uFC00\",\n    \"/yeh.init_hamzaabove.medi_jeem.medi\": \"\\uFC97\",\n    \"/yeh.init_hamzaabove.medi_khah.medi\": \"\\uFC99\",\n    \"/yeh.init_hamzaabove.medi_meem.fina\": \"\\uFC02\",\n    \"/yeh.init_hamzaabove.medi_meem.medi\": \"\\uFC9A\",\n    \"/yeh.init_hamzaabove.medi_oe.fina\": \"\\uFBF2\",\n    \"/yeh.init_hamzaabove.medi_u.fina\": \"\\uFBF0\",\n    \"/yeh.init_hamzaabove.medi_waw.fina\": \"\\uFBEE\",\n    \"/yeh.init_hamzaabove.medi_yeh.fina\": \"\\uFC04\",\n    \"/yeh.init_hamzaabove.medi_yu.fina\": \"\\uFBF4\",\n    \"/yeh.init_heh.medi\": \"\\uFCDE\",\n    \"/yeh.init_jeem.fina\": \"\\uFC55\",\n    \"/yeh.init_jeem.medi\": \"\\uFCDA\",\n    \"/yeh.init_khah.fina\": \"\\uFC57\",\n    \"/yeh.init_khah.medi\": \"\\uFCDC\",\n    \"/yeh.init_meem.fina\": \"\\uFC58\",\n    \"/yeh.init_meem.medi\": \"\\uFCDD\",\n    \"/yeh.init_meem.medi_meem.medi\": \"\\uFD9D\",\n    \"/yeh.init_yeh.fina\": \"\\uFC5A\",\n    \"/yeh.isol\": \"\\uFEF1\",\n    \"/yeh.medi\": \"\\uFEF4\",\n    \"/yeh.medi_alefmaksura.fina\": \"\\uFC95\",\n    \"/yeh.medi_hah.medi_yeh.fina\": \"\\uFDAE\",\n    \"/yeh.medi_hamzaabove.medi_ae.fina\": \"\\uFBED\",\n    \"/yeh.medi_hamzaabove.medi_alef.fina\": \"\\uFBEB\",\n    \"/yeh.medi_hamzaabove.medi_alefmaksura.fina\": \"\\uFC68\",\n    \"/yeh.medi_hamzaabove.medi_e.fina\": \"\\uFBF7\",\n    \"/yeh.medi_hamzaabove.medi_heh.medi\": \"\\uFCE0\",\n    \"/yeh.medi_hamzaabove.medi_meem.fina\": \"\\uFC66\",\n    \"/yeh.medi_hamzaabove.medi_meem.medi\": \"\\uFCDF\",\n    \"/yeh.medi_hamzaabove.medi_noon.fina\": \"\\uFC67\",\n    \"/yeh.medi_hamzaabove.medi_oe.fina\": \"\\uFBF3\",\n    \"/yeh.medi_hamzaabove.medi_reh.fina\": \"\\uFC64\",\n    \"/yeh.medi_hamzaabove.medi_u.fina\": \"\\uFBF1\",\n    \"/yeh.medi_hamzaabove.medi_waw.fina\": \"\\uFBEF\",\n    \"/yeh.medi_hamzaabove.medi_yeh.fina\": \"\\uFC69\",\n    \"/yeh.medi_hamzaabove.medi_yu.fina\": \"\\uFBF5\",\n    \"/yeh.medi_hamzaabove.medi_zain.fina\": \"\\uFC65\",\n    \"/yeh.medi_heh.medi\": \"\\uFCF1\",\n    \"/yeh.medi_jeem.medi_yeh.fina\": \"\\uFDAF\",\n    \"/yeh.medi_meem.fina\": \"\\uFC93\",\n    \"/yeh.medi_meem.medi\": \"\\uFCF0\",\n    \"/yeh.medi_meem.medi_meem.fina\": \"\\uFD9C\",\n    \"/yeh.medi_meem.medi_yeh.fina\": \"\\uFDB0\",\n    \"/yeh.medi_noon.fina\": \"\\uFC94\",\n    \"/yeh.medi_reh.fina\": \"\\uFC91\",\n    \"/yeh.medi_yeh.fina\": \"\\uFC96\",\n    \"/yeh.medi_zain.fina\": \"\\uFC92\",\n    \"/yehBarreeDigitThreeAbove\": \"\\u077B\",\n    \"/yehBarreeDigitTwoAbove\": \"\\u077A\",\n    \"/yehVabove\": \"\\u06CE\",\n    \"/yehabove\": \"\\u06E7\",\n    \"/yeharabic\": \"\\u064A\",\n    \"/yehbarree\": \"\\u06D2\",\n    \"/yehbarree.fina\": \"\\uFBAF\",\n    \"/yehbarree.isol\": \"\\uFBAE\",\n    \"/yehbarreearabic\": \"\\u06D2\",\n    \"/yehbarreefinalarabic\": \"\\uFBAF\",\n    \"/yehbarreehamza\": \"\\u06D3\",\n    \"/yehbarreehamza.fina\": \"\\uFBB1\",\n    \"/yehbarreehamza.isol\": \"\\uFBB0\",\n    \"/yehfarsi\": \"\\u06CC\",\n    \"/yehfarsi.fina\": \"\\uFBFD\",\n    \"/yehfarsi.init\": \"\\uFBFE\",\n    \"/yehfarsi.isol\": \"\\uFBFC\",\n    \"/yehfarsi.medi\": \"\\uFBFF\",\n    \"/yehfarsiinvertedV\": \"\\u063D\",\n    \"/yehfarsithreedotsabove\": \"\\u063F\",\n    \"/yehfarsitwodotsabove\": \"\\u063E\",\n    \"/yehfinalarabic\": \"\\uFEF2\",\n    \"/yehhamza\": \"\\u0626\",\n    \"/yehhamza.fina\": \"\\uFE8A\",\n    \"/yehhamza.init\": \"\\uFE8B\",\n    \"/yehhamza.isol\": \"\\uFE89\",\n    \"/yehhamza.medi\": \"\\uFE8C\",\n    \"/yehhamzaabovearabic\": \"\\u0626\",\n    \"/yehhamzaabovefinalarabic\": \"\\uFE8A\",\n    \"/yehhamzaaboveinitialarabic\": \"\\uFE8B\",\n    \"/yehhamzaabovemedialarabic\": \"\\uFE8C\",\n    \"/yehhighhamza\": \"\\u0678\",\n    \"/yehinitialarabic\": \"\\uFEF3\",\n    \"/yehmedialarabic\": \"\\uFEF4\",\n    \"/yehmeeminitialarabic\": \"\\uFCDD\",\n    \"/yehmeemisolatedarabic\": \"\\uFC58\",\n    \"/yehnoonfinalarabic\": \"\\uFC94\",\n    \"/yehsmall\": \"\\u06E6\",\n    \"/yehtail\": \"\\u06CD\",\n    \"/yehthreedotsbelow\": \"\\u06D1\",\n    \"/yehthreedotsbelowarabic\": \"\\u06D1\",\n    \"/yekorean\": \"\\u3156\",\n    \"/yellowHeart\": \"\\u1F49B\",\n    \"/yen\": \"\\u00A5\",\n    \"/yenmonospace\": \"\\uFFE5\",\n    \"/yeokorean\": \"\\u3155\",\n    \"/yeorinhieuhkorean\": \"\\u3186\",\n    \"/yerachBenYomo:hb\": \"\\u05AA\",\n    \"/yerahbenyomohebrew\": \"\\u05AA\",\n    \"/yerahbenyomolefthebrew\": \"\\u05AA\",\n    \"/yericyrillic\": \"\\u044B\",\n    \"/yerudieresiscyrillic\": \"\\u04F9\",\n    \"/yesieungkorean\": \"\\u3181\",\n    \"/yesieungpansioskorean\": \"\\u3183\",\n    \"/yesieungsioskorean\": \"\\u3182\",\n    \"/yetiv:hb\": \"\\u059A\",\n    \"/yetivhebrew\": \"\\u059A\",\n    \"/ygrave\": \"\\u1EF3\",\n    \"/yhoi\": \"\\u1EF7\",\n    \"/yhook\": \"\\u01B4\",\n    \"/yhookabove\": \"\\u1EF7\",\n    \"/yiarmenian\": \"\\u0575\",\n    \"/yicyrillic\": \"\\u0457\",\n    \"/yikorean\": \"\\u3162\",\n    \"/yintonemod\": \"\\u02EA\",\n    \"/yinyang\": \"\\u262F\",\n    \"/yiwnarmenian\": \"\\u0582\",\n    \"/ylongcyr\": \"\\u044B\",\n    \"/ylongdieresiscyr\": \"\\u04F9\",\n    \"/yloop\": \"\\u1EFF\",\n    \"/ymacron\": \"\\u0233\",\n    \"/ymonospace\": \"\\uFF59\",\n    \"/yocirclekatakana\": \"\\u32F5\",\n    \"/yod\": \"\\u05D9\",\n    \"/yod:hb\": \"\\u05D9\",\n    \"/yod_yod:hb\": \"\\u05F2\",\n    \"/yod_yod_patah:hb\": \"\\uFB1F\",\n    \"/yoddagesh\": \"\\uFB39\",\n    \"/yoddageshhebrew\": \"\\uFB39\",\n    \"/yodhebrew\": \"\\u05D9\",\n    \"/yodwithdagesh:hb\": \"\\uFB39\",\n    \"/yodwithhiriq:hb\": \"\\uFB1D\",\n    \"/yodyodhebrew\": \"\\u05F2\",\n    \"/yodyodpatahhebrew\": \"\\uFB1F\",\n    \"/yogh\": \"\\u021D\",\n    \"/yohiragana\": \"\\u3088\",\n    \"/yoikorean\": \"\\u3189\",\n    \"/yokatakana\": \"\\u30E8\",\n    \"/yokatakanahalfwidth\": \"\\uFF96\",\n    \"/yokorean\": \"\\u315B\",\n    \"/yosmallhiragana\": \"\\u3087\",\n    \"/yosmallkatakana\": \"\\u30E7\",\n    \"/yosmallkatakanahalfwidth\": \"\\uFF6E\",\n    \"/yot\": \"\\u03F3\",\n    \"/yotgreek\": \"\\u03F3\",\n    \"/yoyaekorean\": \"\\u3188\",\n    \"/yoyakorean\": \"\\u3187\",\n    \"/yoyakthai\": \"\\u0E22\",\n    \"/yoyingthai\": \"\\u0E0D\",\n    \"/yparen\": \"\\u24B4\",\n    \"/yparenthesized\": \"\\u24B4\",\n    \"/ypogegrammeni\": \"\\u037A\",\n    \"/ypogegrammenigreekcmb\": \"\\u0345\",\n    \"/yr\": \"\\u01A6\",\n    \"/yring\": \"\\u1E99\",\n    \"/ystroke\": \"\\u024F\",\n    \"/ysuperior\": \"\\u02B8\",\n    \"/ysupmod\": \"\\u02B8\",\n    \"/ytilde\": \"\\u1EF9\",\n    \"/yturned\": \"\\u028E\",\n    \"/yu.fina\": \"\\uFBDC\",\n    \"/yu.isol\": \"\\uFBDB\",\n    \"/yuansquare\": \"\\u3350\",\n    \"/yucirclekatakana\": \"\\u32F4\",\n    \"/yucyr\": \"\\u044E\",\n    \"/yuhiragana\": \"\\u3086\",\n    \"/yuikorean\": \"\\u318C\",\n    \"/yukatakana\": \"\\u30E6\",\n    \"/yukatakanahalfwidth\": \"\\uFF95\",\n    \"/yukirghiz\": \"\\u06C9\",\n    \"/yukirghiz.fina\": \"\\uFBE3\",\n    \"/yukirghiz.isol\": \"\\uFBE2\",\n    \"/yukorean\": \"\\u3160\",\n    \"/yukrcyr\": \"\\u0457\",\n    \"/yusbigcyr\": \"\\u046B\",\n    \"/yusbigcyrillic\": \"\\u046B\",\n    \"/yusbigiotifiedcyr\": \"\\u046D\",\n    \"/yusbigiotifiedcyrillic\": \"\\u046D\",\n    \"/yuslittlecyr\": \"\\u0467\",\n    \"/yuslittlecyrillic\": \"\\u0467\",\n    \"/yuslittleiotifiedcyr\": \"\\u0469\",\n    \"/yuslittleiotifiedcyrillic\": \"\\u0469\",\n    \"/yusmallhiragana\": \"\\u3085\",\n    \"/yusmallkatakana\": \"\\u30E5\",\n    \"/yusmallkatakanahalfwidth\": \"\\uFF6D\",\n    \"/yuyekorean\": \"\\u318B\",\n    \"/yuyeokorean\": \"\\u318A\",\n    \"/yyabengali\": \"\\u09DF\",\n    \"/yyadeva\": \"\\u095F\",\n    \"/z\": \"\\u007A\",\n    \"/zaarmenian\": \"\\u0566\",\n    \"/zacute\": \"\\u017A\",\n    \"/zadeva\": \"\\u095B\",\n    \"/zagurmukhi\": \"\\u0A5B\",\n    \"/zah\": \"\\u0638\",\n    \"/zah.fina\": \"\\uFEC6\",\n    \"/zah.init\": \"\\uFEC7\",\n    \"/zah.init_meem.fina\": \"\\uFC28\",\n    \"/zah.init_meem.medi\": \"\\uFCB9\",\n    \"/zah.isol\": \"\\uFEC5\",\n    \"/zah.medi\": \"\\uFEC8\",\n    \"/zah.medi_meem.medi\": \"\\uFD3B\",\n    \"/zaharabic\": \"\\u0638\",\n    \"/zahfinalarabic\": \"\\uFEC6\",\n    \"/zahinitialarabic\": \"\\uFEC7\",\n    \"/zahiragana\": \"\\u3056\",\n    \"/zahmedialarabic\": \"\\uFEC8\",\n    \"/zain\": \"\\u0632\",\n    \"/zain.fina\": \"\\uFEB0\",\n    \"/zain.isol\": \"\\uFEAF\",\n    \"/zainabove\": \"\\u0617\",\n    \"/zainarabic\": \"\\u0632\",\n    \"/zainfinalarabic\": \"\\uFEB0\",\n    \"/zakatakana\": \"\\u30B6\",\n    \"/zaqefGadol:hb\": \"\\u0595\",\n    \"/zaqefQatan:hb\": \"\\u0594\",\n    \"/zaqefgadolhebrew\": \"\\u0595\",\n    \"/zaqefqatanhebrew\": \"\\u0594\",\n    \"/zarqa:hb\": \"\\u0598\",\n    \"/zarqahebrew\": \"\\u0598\",\n    \"/zayin\": \"\\u05D6\",\n    \"/zayin:hb\": \"\\u05D6\",\n    \"/zayindagesh\": \"\\uFB36\",\n    \"/zayindageshhebrew\": \"\\uFB36\",\n    \"/zayinhebrew\": \"\\u05D6\",\n    \"/zayinwithdagesh:hb\": \"\\uFB36\",\n    \"/zbopomofo\": \"\\u3117\",\n    \"/zcaron\": \"\\u017E\",\n    \"/zcircle\": \"\\u24E9\",\n    \"/zcircumflex\": \"\\u1E91\",\n    \"/zcurl\": \"\\u0291\",\n    \"/zdescender\": \"\\u2C6C\",\n    \"/zdot\": \"\\u017C\",\n    \"/zdotaccent\": \"\\u017C\",\n    \"/zdotbelow\": \"\\u1E93\",\n    \"/zecyr\": \"\\u0437\",\n    \"/zecyrillic\": \"\\u0437\",\n    \"/zedescendercyrillic\": \"\\u0499\",\n    \"/zedieresiscyr\": \"\\u04DF\",\n    \"/zedieresiscyrillic\": \"\\u04DF\",\n    \"/zehiragana\": \"\\u305C\",\n    \"/zekatakana\": \"\\u30BC\",\n    \"/zero\": \"\\u0030\",\n    \"/zero.inferior\": \"\\u2080\",\n    \"/zero.superior\": \"\\u2070\",\n    \"/zeroarabic\": \"\\u0660\",\n    \"/zerobengali\": \"\\u09E6\",\n    \"/zerocircle\": \"\\u24EA\",\n    \"/zerocircleblack\": \"\\u24FF\",\n    \"/zerocomma\": \"\\u1F101\",\n    \"/zerodeva\": \"\\u0966\",\n    \"/zerofar\": \"\\u06F0\",\n    \"/zerofullstop\": \"\\u1F100\",\n    \"/zerogujarati\": \"\\u0AE6\",\n    \"/zerogurmukhi\": \"\\u0A66\",\n    \"/zerohackarabic\": \"\\u0660\",\n    \"/zeroinferior\": \"\\u2080\",\n    \"/zeromonospace\": \"\\uFF10\",\n    \"/zerooldstyle\": \"\\uF730\",\n    \"/zeropersian\": \"\\u06F0\",\n    \"/zerosquareabove\": \"\\u06E0\",\n    \"/zerosuperior\": \"\\u2070\",\n    \"/zerothai\": \"\\u0E50\",\n    \"/zerothirds\": \"\\u2189\",\n    \"/zerowidthjoiner\": \"\\uFEFF\",\n    \"/zerowidthnobreakspace\": \"\\uFEFF\",\n    \"/zerowidthnonjoiner\": \"\\u200C\",\n    \"/zerowidthspace\": \"\\u200B\",\n    \"/zeta\": \"\\u03B6\",\n    \"/zetailcyr\": \"\\u0499\",\n    \"/zhbopomofo\": \"\\u3113\",\n    \"/zhearmenian\": \"\\u056A\",\n    \"/zhebrevecyr\": \"\\u04C2\",\n    \"/zhebrevecyrillic\": \"\\u04C2\",\n    \"/zhecyr\": \"\\u0436\",\n    \"/zhecyrillic\": \"\\u0436\",\n    \"/zhedescendercyrillic\": \"\\u0497\",\n    \"/zhedieresiscyr\": \"\\u04DD\",\n    \"/zhedieresiscyrillic\": \"\\u04DD\",\n    \"/zhetailcyr\": \"\\u0497\",\n    \"/zhook\": \"\\u0225\",\n    \"/zihiragana\": \"\\u3058\",\n    \"/zikatakana\": \"\\u30B8\",\n    \"/zildefunc\": \"\\u236C\",\n    \"/zinorhebrew\": \"\\u05AE\",\n    \"/zjekomicyr\": \"\\u0505\",\n    \"/zlinebelow\": \"\\u1E95\",\n    \"/zmonospace\": \"\\uFF5A\",\n    \"/znotationbagmembership\": \"\\u22FF\",\n    \"/zohiragana\": \"\\u305E\",\n    \"/zokatakana\": \"\\u30BE\",\n    \"/zparen\": \"\\u24B5\",\n    \"/zparenthesized\": \"\\u24B5\",\n    \"/zretroflex\": \"\\u0290\",\n    \"/zretroflexhook\": \"\\u0290\",\n    \"/zstroke\": \"\\u01B6\",\n    \"/zswashtail\": \"\\u0240\",\n    \"/zuhiragana\": \"\\u305A\",\n    \"/zukatakana\": \"\\u30BA\",\n    \"/zwarakay\": \"\\u0659\",\n    # manually added from\n    # https://github.com/serviceprototypinglab/latex-pdfa/blob/master/glyphtounicode-cmr.tex\n    \"/angbracketleftBig\": \"\\u28E8\",\n    \"/angbracketleftBigg\": \"\\u27E8\",\n    \"/angbracketleftbig\": \"\\u27E8\",\n    \"/angbracketleftbigg\": \"\\u27E8\",\n    \"/angbracketrightBig\": \"\\u27E9\",\n    \"/angbracketrightBigg\": \"\\u27E9\",\n    \"/angbracketrightbig\": \"\\u27E9\",\n    \"/angbracketrightbigg\": \"\\u27E9\",\n    \"/arrowbt\": \"\\u2193\",\n    \"/arrowdblbt\": \"\\u21D3\",\n    \"/arrowdbltp\": \"\\u21D1\",\n    \"/arrowhookleft\": \"\\u21AA\",\n    \"/arrowhookright\": \"\\u21A9\",\n    \"/arrowtp\": \"\\u2191\",\n    # diff : \"/arrowvertex\": \"\\u23D0\",\n    \"/arrowvertexdbl\": \"\\uED12\",\n    \"/backslashBig\": \"\\u005C\",\n    \"/backslashBigg\": \"\\u005C\",\n    \"/backslashbig\": \"\\u005C\",\n    \"/backslashbigg\": \"\\u005C\",\n    # diff : \"/braceex\": \"\\u23AA\",\n    \"/bracehtipdownleft\": \"\\uED17\",\n    \"/bracehtipdownright\": \"\\uED18\",\n    \"/bracehtipupleft\": \"\\uED19\",\n    \"/bracehtipupright\": \"\\uED1A\",\n    \"/braceleftBig\": \"\\u007B\",\n    \"/braceleftBigg\": \"\\u007B\",\n    \"/braceleftbig\": \"\\u007B\",\n    \"/braceleftbigg\": \"\\u007B\",\n    # diff : \"/braceleftbt\": \"\\u23A9\",\n    # diff : \"/braceleftmid\": \"\\u23A8\",\n    # diff : \"/bracelefttp\": \"\\u23A7\",\n    \"/bracerightBig\": \"\\u007D\",\n    \"/bracerightBigg\": \"\\u007D\",\n    \"/bracerightbig\": \"\\u007D\",\n    \"/bracerightbigg\": \"\\u007D\",\n    # diff : \"/bracerightbt\": \"\\u23AD\",\n    # diff : \"/bracerightmid\": \"\\u23AC\",\n    # diff : \"/bracerighttp\": \"\\u23AB\",\n    \"/bracketleftBig\": \"\\u005B\",\n    \"/bracketleftBigg\": \"\\u005B\",\n    \"/bracketleftbig\": \"\\u005B\",\n    \"/bracketleftbigg\": \"\\u005B\",\n    # diff : \"/bracketleftbt\": \"\\u23A3\",\n    # diff : \"/bracketleftex\": \"\\u23A2\",\n    # diff : \"/bracketlefttp\": \"\\u23A1\",\n    \"/bracketrightBig\": \"\\u005D\",\n    \"/bracketrightBigg\": \"\\u005D\",\n    \"/bracketrightbig\": \"\\u005D\",\n    \"/bracketrightbigg\": \"\\u005D\",\n    # diff : \"/bracketrightbt\": \"\\u23A6\",\n    # diff : \"/bracketrightex\": \"\\u23A5\",\n    # diff : \"/bracketrighttp\": \"\\u23A4\",\n    \"/ceilingleftBig\": \"\\u2308\",\n    \"/ceilingleftBigg\": \"\\u2308\",\n    \"/ceilingleftbig\": \"\\u2308\",\n    \"/ceilingleftbigg\": \"\\u2308\",\n    \"/ceilingrightBig\": \"\\u2309\",\n    \"/ceilingrightBigg\": \"\\u2309\",\n    \"/ceilingrightbig\": \"\\u2309\",\n    \"/ceilingrightbigg\": \"\\u2309\",\n    \"/circledotdisplay\": \"\\u2A00\",\n    \"/circledottext\": \"\\u2A00\",\n    \"/circlemultiplydisplay\": \"\\u2A02\",\n    \"/circlemultiplytext\": \"\\u2A02\",\n    \"/circleplusdisplay\": \"\\u2A01\",\n    \"/circleplustext\": \"\\u2A01\",\n    \"/contintegraldisplay\": \"\\u222E\",\n    \"/contintegraltext\": \"\\u222E\",\n    \"/coproductdisplay\": \"\\u2210\",\n    \"/coproducttext\": \"\\u2210\",\n    \"/floorleftBig\": \"\\u230A\",\n    \"/floorleftBigg\": \"\\u230A\",\n    \"/floorleftbig\": \"\\u230A\",\n    \"/floorleftbigg\": \"\\u230A\",\n    \"/floorrightBig\": \"\\u230B\",\n    \"/floorrightBigg\": \"\\u230B\",\n    \"/floorrightbig\": \"\\u230B\",\n    \"/floorrightbigg\": \"\\u230B\",\n    \"/hatwide\": \"\\u02C6\",\n    \"/hatwider\": \"\\u02C6\",\n    \"/hatwidest\": \"\\u02C6\",\n    \"/integraldisplay\": \"\\u222B\",\n    \"/integraltext\": \"\\u222B\",\n    \"/intersectiondisplay\": \"\\u22C2\",\n    \"/intersectiontext\": \"\\u22C2\",\n    \"/logicalanddisplay\": \"\\u22C0\",\n    \"/logicalandtext\": \"\\u22C0\",\n    \"/logicalordisplay\": \"\\u22C1\",\n    \"/logicalortext\": \"\\u22C1\",\n    \"/mapsto\": \"\\u21A6\",\n    \"/parenleftBig\": \"\\u0028\",\n    \"/parenleftBigg\": \"\\u0028\",\n    \"/parenleftbig\": \"\\u0028\",\n    \"/parenleftbigg\": \"\\u0028\",\n    # diff : \"/parenleftbt\": \"\\u239D\",\n    # diff : \"/parenleftex\": \"\\u239C\",\n    # diff : \"/parenlefttp\": \"\\u239B\",\n    \"/parenrightBig\": \"\\u0029\",\n    \"/parenrightBigg\": \"\\u0029\",\n    \"/parenrightbig\": \"\\u0029\",\n    \"/parenrightbigg\": \"\\u0029\",\n    # diff : \"/parenrightbt\": \"\\u23A0\",\n    # diff : \"/parenrightex\": \"\\u239F\",\n    # diff : \"/parenrighttp\": \"\\u239E\",\n    \"/productdisplay\": \"\\u220F\",\n    \"/producttext\": \"\\u220F\",\n    \"/radicalBig\": \"\\u221A\",\n    \"/radicalBigg\": \"\\u221A\",\n    \"/radicalbig\": \"\\u221A\",\n    \"/radicalbigg\": \"\\u221A\",\n    \"/radicalbt\": \"\\u221A\",\n    \"/radicaltp\": \"\\uED6A\",\n    \"/radicalvertex\": \"\\uED6B\",\n    \"/slashBig\": \"\\u002F\",\n    \"/slashBigg\": \"\\u002F\",\n    \"/slashbig\": \"\\u002F\",\n    \"/slashbigg\": \"\\u002F\",\n    \"/summationdisplay\": \"\\u2211\",\n    \"/summationtext\": \"\\u2211\",\n    \"/tie\": \"\\u2040\",\n    \"/tildewide\": \"\\u02DC\",\n    \"/tildewider\": \"\\u02DC\",\n    \"/tildewidest\": \"\\u02DC\",\n    \"/uniondisplay\": \"\\u22C3\",\n    \"/unionmultidisplay\": \"\\u2A04\",\n    \"/unionmultitext\": \"\\u2A04\",\n    \"/unionsqdisplay\": \"\\u2A06\",\n    \"/unionsqtext\": \"\\u2A06\",\n    \"/uniontext\": \"\\u22C3\",\n    \"/vextenddouble\": \"\\uED79\",\n    \"/vextendsingle\": \"\\u23D0\",\n    \"/a1\": \"\\u25C1\",\n    \"/a2\": \"\\u22B4\",\n    \"/a3\": \"\\u25B7\",\n    \"/a4\": \"\\u22B5\",\n    \"/a40\": \"\\u02C2\",\n    \"/a41\": \"\\u02C3\",\n    \"/a42\": \"\\u2303\",\n    \"/a43\": \"\\u2304\",\n    \"/a48\": \"\\u2127\",\n    \"/a49\": \"\\u22C8\",\n    \"/a50\": \"\\u25A1\",\n    \"/a51\": \"\\u25C7\",\n    \"/a58\": \"\\u2053\",\n    \"/a59\": \"\\u219D\",\n    \"/a60\": \"\\u228F\",\n    \"/a61\": \"\\u2290\",\n    \"/d0\": \"\\u2199\",\n    \"/d1\": \"\\u2199\",\n    \"/d2\": \"\\u2199\",\n    \"/d3\": \"\\u2199\",\n    \"/d4\": \"\\u2199\",\n    \"/d5\": \"\\u2199\",\n    \"/d6\": \"\\u2199\",\n    \"/d7\": \"\\u2193\",\n    \"/d8\": \"\\u2193\",\n    \"/d9\": \"\\u2193\",\n    \"/d10\": \"\\u2193\",\n    \"/d11\": \"\\u2193\",\n    \"/d12\": \"\\u2193\",\n    \"/d13\": \"\\u2193\",\n    \"/d14\": \"\\u2193\",\n    \"/d15\": \"\\u2193\",\n    \"/d16\": \"\\u2193\",\n    \"/d17\": \"\\u2193\",\n    \"/d18\": \"\\u2193\",\n    \"/d19\": \"\\u2193\",\n    \"/d20\": \"\\u2193\",\n    \"/d21\": \"\\u2193\",\n    \"/d22\": \"\\u2193\",\n    \"/d23\": \"\\u2193\",\n    \"/d24\": \"\\u2198\",\n    \"/d25\": \"\\u2198\",\n    \"/d26\": \"\\u2198\",\n    \"/d27\": \"\\u2198\",\n    \"/d28\": \"\\u2198\",\n    \"/d29\": \"\\u2198\",\n    \"/d30\": \"\\u2198\",\n    \"/d31\": \"\\u2198\",\n    \"/d32\": \"\\u2198\",\n    \"/d33\": \"\\u2198\",\n    \"/d34\": \"\\u2198\",\n    \"/d35\": \"\\u2198\",\n    \"/d36\": \"\\u2198\",\n    \"/d37\": \"\\u2198\",\n    \"/d38\": \"\\u2198\",\n    \"/d39\": \"\\u2192\",\n    \"/d40\": \"\\u2192\",\n    \"/d41\": \"\\u2192\",\n    \"/d42\": \"\\u2192\",\n    \"/d43\": \"\\u2192\",\n    \"/d44\": \"\\u2192\",\n    \"/d45\": \"\\u2192\",\n    \"/d46\": \"\\u2192\",\n    \"/d47\": \"\\u2192\",\n    \"/d48\": \"\\u2192\",\n    \"/d49\": \"\\u2192\",\n    \"/d50\": \"\\u2192\",\n    \"/d51\": \"\\u2192\",\n    \"/d52\": \"\\u2192\",\n    \"/d53\": \"\\u2192\",\n    \"/d54\": \"\\u2192\",\n    \"/d55\": \"\\u2192\",\n    \"/d56\": \"\\u2197\",\n    \"/d57\": \"\\u2197\",\n    \"/d58\": \"\\u2197\",\n    \"/d59\": \"\\u2197\",\n    \"/d60\": \"\\u2197\",\n    \"/d61\": \"\\u2197\",\n    \"/d62\": \"\\u2197\",\n    \"/d63\": \"\\u2197\",\n    \"/d64\": \"\\u2197\",\n    \"/d65\": \"\\u2197\",\n    \"/d66\": \"\\u2197\",\n    \"/d67\": \"\\u2197\",\n    \"/d68\": \"\\u2197\",\n    \"/d69\": \"\\u2197\",\n    \"/d70\": \"\\u2197\",\n    \"/d71\": \"\\u2191\",\n    \"/d72\": \"\\u2191\",\n    \"/d73\": \"\\u2191\",\n    \"/d74\": \"\\u2191\",\n    \"/d75\": \"\\u2191\",\n    \"/d76\": \"\\u2191\",\n    \"/d77\": \"\\u2191\",\n    \"/d78\": \"\\u2191\",\n    \"/d79\": \"\\u2191\",\n    \"/d80\": \"\\u2191\",\n    \"/d81\": \"\\u2191\",\n    \"/d82\": \"\\u2191\",\n    \"/d83\": \"\\u2191\",\n    \"/d84\": \"\\u2191\",\n    \"/d85\": \"\\u2191\",\n    \"/d86\": \"\\u2191\",\n    \"/d87\": \"\\u2191\",\n    \"/d88\": \"\\u2196\",\n    \"/d89\": \"\\u2196\",\n    \"/d90\": \"\\u2196\",\n    \"/d91\": \"\\u2196\",\n    \"/d92\": \"\\u2196\",\n    \"/d93\": \"\\u2196\",\n    \"/d94\": \"\\u2196\",\n    \"/d95\": \"\\u2196\",\n    \"/d96\": \"\\u2196\",\n    \"/d97\": \"\\u2196\",\n    \"/d98\": \"\\u2196\",\n    \"/d99\": \"\\u2196\",\n    \"/d100\": \"\\u2196\",\n    \"/d101\": \"\\u2196\",\n    \"/d102\": \"\\u2196\",\n    \"/d103\": \"\\u2190\",\n    \"/d104\": \"\\u2190\",\n    \"/d105\": \"\\u2190\",\n    \"/d106\": \"\\u2190\",\n    \"/d107\": \"\\u2190\",\n    \"/d108\": \"\\u2190\",\n    \"/d109\": \"\\u2190\",\n    \"/d110\": \"\\u2190\",\n    \"/d111\": \"\\u2190\",\n    \"/d112\": \"\\u2190\",\n    \"/d113\": \"\\u2190\",\n    \"/d114\": \"\\u2190\",\n    \"/d115\": \"\\u2190\",\n    \"/d116\": \"\\u2190\",\n    \"/d117\": \"\\u2190\",\n    \"/d118\": \"\\u2190\",\n    \"/d119\": \"\\u2190\",\n    \"/d120\": \"\\u2199\",\n    \"/d121\": \"\\u2199\",\n    \"/d122\": \"\\u2199\",\n    \"/d123\": \"\\u2199\",\n    \"/d124\": \"\\u2199\",\n    \"/d125\": \"\\u2199\",\n    \"/d126\": \"\\u2199\",\n    \"/d127\": \"\\u2199\",\n    # manually added from\n    # https://github.com/kohler/lcdf-typetools/blob/master/texglyphlist.txt\n    \"/Ifractur\": \"\\u2111\",\n    \"/FFsmall\": \"\\uF766\",\n    \"/FFIsmall\": \"\\uF766\",\n    \"/FFLsmall\": \"\\uF766\",\n    \"/FIsmall\": \"\\uF766\",\n    \"/FLsmall\": \"\\uF766\",\n    # diff : \"/Germandbls\": \"\\u0053\",\n    \"/Germandblssmall\": \"\\uF773\",\n    \"/Ng\": \"\\u014A\",\n    \"/Rfractur\": \"\\u211C\",\n    \"/SS\": \"\\u0053\",\n    \"/SSsmall\": \"\\uF773\",\n    \"/altselector\": \"\\uD802\",\n    \"/angbracketleft\": \"\\u27E8\",\n    \"/angbracketright\": \"\\u27E9\",\n    \"/arrowbothv\": \"\\u2195\",\n    \"/arrowdblbothv\": \"\\u21D5\",\n    \"/arrowleftbothalf\": \"\\u21BD\",\n    \"/arrowlefttophalf\": \"\\u21BC\",\n    \"/arrownortheast\": \"\\u2197\",\n    \"/arrownorthwest\": \"\\u2196\",\n    \"/arrowrightbothalf\": \"\\u21C1\",\n    \"/arrowrighttophalf\": \"\\u21C0\",\n    \"/arrowsoutheast\": \"\\u2198\",\n    \"/arrowsouthwest\": \"\\u2199\",\n    \"/ascendercompwordmark\": \"\\uD80A\",\n    \"/asteriskcentered\": \"\\u2217\",\n    \"/bardbl\": \"\\u2225\",\n    \"/capitalcompwordmark\": \"\\uD809\",\n    \"/circlecopyrt\": \"\\u20DD\",\n    \"/circledivide\": \"\\u2298\",\n    \"/circleminus\": \"\\u2296\",\n    \"/coproduct\": \"\\u2A3F\",\n    \"/ct\": \"\\u0063\",\n    \"/cwm\": \"\\u200C\",\n    \"/dblbracketleft\": \"\\u27E6\",\n    \"/dblbracketright\": \"\\u27E7\",\n    # diff : \"/diamond\": \"\\u2662\",\n    \"/diamondmath\": \"\\u22C4\",\n    # diff : \"/dotlessj\": \"\\u0237\",\n    \"/emptyslot\": \"\\uD801\",\n    \"/epsilon1\": \"\\u03F5\",\n    \"/epsiloninv\": \"\\u03F6\",\n    \"/equivasymptotic\": \"\\u224D\",\n    \"/flat\": \"\\u266D\",\n    \"/follows\": \"\\u227B\",\n    \"/followsequal\": \"\\u2AB0\",\n    \"/followsorcurly\": \"\\u227D\",\n    \"/greatermuch\": \"\\u226B\",\n    # diff : \"/heart\": \"\\u2661\",\n    \"/interrobangdown\": \"\\u2E18\",\n    \"/intersectionsq\": \"\\u2293\",\n    \"/latticetop\": \"\\u22A4\",\n    \"/lessmuch\": \"\\u226A\",\n    \"/longdbls\": \"\\u017F\",\n    \"/longsh\": \"\\u017F\",\n    \"/longsi\": \"\\u017F\",\n    \"/longsl\": \"\\u017F\",\n    \"/longst\": \"\\uFB05\",\n    \"/lscript\": \"\\u2113\",\n    \"/natural\": \"\\u266E\",\n    \"/negationslash\": \"\\u0338\",\n    \"/ng\": \"\\u014B\",\n    \"/owner\": \"\\u220B\",\n    \"/pertenthousand\": \"\\u2031\",\n    # diff : \"/phi\": \"\\u03D5\",\n    # diff : \"/phi1\": \"\\u03C6\",\n    \"/pi1\": \"\\u03D6\",\n    \"/precedesequal\": \"\\u2AAF\",\n    \"/precedesorcurly\": \"\\u227C\",\n    \"/prime\": \"\\u2032\",\n    \"/rho1\": \"\\u03F1\",\n    \"/ringfitted\": \"\\uD80D\",\n    \"/sharp\": \"\\u266F\",\n    \"/similarequal\": \"\\u2243\",\n    \"/slurabove\": \"\\u2322\",\n    \"/slurbelow\": \"\\u2323\",\n    \"/st\": \"\\uFB06\",\n    \"/subsetsqequal\": \"\\u2291\",\n    \"/supersetsqequal\": \"\\u2292\",\n    \"/triangle\": \"\\u25B3\",\n    \"/triangleinv\": \"\\u25BD\",\n    \"/triangleleft\": \"\\u25C1\",\n    # diff : \"/triangleright\": \"\\u25B7\",\n    \"/turnstileleft\": \"\\u22A2\",\n    \"/turnstileright\": \"\\u22A3\",\n    \"/twelveudash\": \"\\uD80C\",\n    \"/unionmulti\": \"\\u228E\",\n    \"/unionsq\": \"\\u2294\",\n    \"/vector\": \"\\u20D7\",\n    \"/visualspace\": \"\\u2423\",\n    \"/Dbar\": \"\\u0110\",\n    \"/compwordmark\": \"\\u200C\",\n    \"/dbar\": \"\\u0111\",\n    \"/rangedash\": \"\\u2013\",\n    \"/hyphenchar\": \"\\u002D\",\n    \"/punctdash\": \"\\u2014\",\n    \"/visiblespace\": \"\\u2423\",\n    \"/Yen\": \"\\u00A5\",\n    \"/anticlockwise\": \"\\u27F2\",\n    \"/arrowparrleftright\": \"\\u21C6\",\n    \"/arrowparrrightleft\": \"\\u21C4\",\n    \"/arrowtailleft\": \"\\u21A2\",\n    \"/arrowtailright\": \"\\u21A3\",\n    \"/arrowtripleleft\": \"\\u21DA\",\n    \"/arrowtripleright\": \"\\u21DB\",\n    \"/check\": \"\\u2713\",\n    \"/circleR\": \"\\u00AE\",\n    \"/circleS\": \"\\u24C8\",\n    \"/circleasterisk\": \"\\u229B\",\n    \"/circleequal\": \"\\u229C\",\n    \"/circlering\": \"\\u229A\",\n    \"/clockwise\": \"\\u27F3\",\n    \"/curlyleft\": \"\\u21AB\",\n    \"/curlyright\": \"\\u21AC\",\n    \"/dblarrowdwn\": \"\\u21CA\",\n    \"/dblarrowheadleft\": \"\\u219E\",\n    \"/dblarrowheadright\": \"\\u21A0\",\n    # diff : \"/dblarrowup\": \"\\u21C8\",\n    \"/defines\": \"\\u225C\",\n    \"/diamondsolid\": \"\\u2666\",\n    \"/difference\": \"\\u224F\",\n    \"/downfall\": \"\\u22CE\",\n    \"/equaldotleftright\": \"\\u2252\",\n    \"/equaldotrightleft\": \"\\u2253\",\n    \"/equalorfollows\": \"\\u22DF\",\n    # diff : \"/equalorgreater\": \"\\u2A96\",\n    # diff : \"/equalorless\": \"\\u2A95\",\n    \"/equalsdots\": \"\\u2251\",\n    \"/followsorequal\": \"\\u227F\",\n    \"/forcesbar\": \"\\u22AA\",\n    # diff : \"/fork\": \"\\u22D4\",\n    \"/geomequivalent\": \"\\u224E\",\n    \"/greaterdbleqlless\": \"\\u2A8C\",\n    \"/greaterdblequal\": \"\\u2267\",\n    \"/greaterlessequal\": \"\\u22DB\",\n    \"/greaterorapproxeql\": \"\\u2A86\",\n    \"/greaterorequalslant\": \"\\u2A7E\",\n    \"/greaterorsimilar\": \"\\u2273\",\n    \"/harpoondownleft\": \"\\u21C3\",\n    \"/harpoondownright\": \"\\u21C2\",\n    \"/harpoonleftright\": \"\\u21CC\",\n    \"/harpoonrightleft\": \"\\u21CB\",\n    \"/harpoonupleft\": \"\\u21BF\",\n    \"/harpoonupright\": \"\\u21BE\",\n    \"/intercal\": \"\\u22BA\",\n    \"/lessdbleqlgreater\": \"\\u2A8B\",\n    \"/lessdblequal\": \"\\u2266\",\n    \"/lessequalgreater\": \"\\u22DA\",\n    \"/lessorapproxeql\": \"\\u2A85\",\n    \"/lessorequalslant\": \"\\u2A7D\",\n    \"/lessorsimilar\": \"\\u2272\",\n    \"/maltesecross\": \"\\u2720\",\n    \"/multiopenleft\": \"\\u22CB\",\n    \"/multiopenright\": \"\\u22CC\",\n    \"/orunderscore\": \"\\u22BB\",\n    \"/perpcorrespond\": \"\\u2A5E\",\n    # diff : \"/precedesorequal\": \"\\u227E\",\n    \"/primereverse\": \"\\u2035\",\n    \"/revasymptequal\": \"\\u22CD\",\n    \"/revsimilar\": \"\\u223D\",\n    \"/rightanglene\": \"\\u231D\",\n    \"/rightanglenw\": \"\\u231C\",\n    \"/rightanglese\": \"\\u231F\",\n    \"/rightanglesw\": \"\\u231E\",\n    \"/satisfies\": \"\\u22A8\",\n    \"/shiftleft\": \"\\u21B0\",\n    \"/shiftright\": \"\\u21B1\",\n    \"/square\": \"\\u25A1\",\n    \"/squaredot\": \"\\u22A1\",\n    \"/squareminus\": \"\\u229F\",\n    \"/squaremultiply\": \"\\u22A0\",\n    \"/squareplus\": \"\\u229E\",\n    \"/squaresolid\": \"\\u25A0\",\n    \"/squiggleleftright\": \"\\u21AD\",\n    \"/squiggleright\": \"\\u21DD\",\n    \"/subsetdblequal\": \"\\u2AC5\",\n    \"/supersetdbl\": \"\\u22D1\",\n    \"/supersetdblequal\": \"\\u2AC6\",\n    \"/triangledownsld\": \"\\u25BC\",\n    \"/triangleleftequal\": \"\\u22B4\",\n    \"/triangleleftsld\": \"\\u25C0\",\n    \"/trianglerightequal\": \"\\u22B5\",\n    \"/trianglerightsld\": \"\\u25B6\",\n    \"/trianglesolid\": \"\\u25B2\",\n    \"/uprise\": \"\\u22CF\",\n    # diff : \"/Digamma\": \"\\u1D7C\",\n    \"/Finv\": \"\\u2132\",\n    \"/Gmir\": \"\\u2141\",\n    \"/Omegainv\": \"\\u2127\",\n    \"/approxorequal\": \"\\u224A\",\n    \"/archleftdown\": \"\\u21B6\",\n    \"/archrightdown\": \"\\u21B7\",\n    \"/beth\": \"\\u2136\",\n    \"/daleth\": \"\\u2138\",\n    \"/dividemultiply\": \"\\u22C7\",\n    \"/downslope\": \"\\u29F9\",\n    \"/equalorsimilar\": \"\\u2242\",\n    \"/follownotdbleqv\": \"\\u2ABA\",\n    \"/follownotslnteql\": \"\\u2AB6\",\n    \"/followornoteqvlnt\": \"\\u22E9\",\n    \"/greaternotdblequal\": \"\\u2A8A\",\n    \"/greaternotequal\": \"\\u2A88\",\n    \"/greaterornotdbleql\": \"\\u2269\",\n    \"/greaterornotequal\": \"\\u2269\",\n    \"/integerdivide\": \"\\u2216\",\n    \"/lessnotdblequal\": \"\\u2A89\",\n    \"/lessnotequal\": \"\\u2A87\",\n    \"/lessornotdbleql\": \"\\u2268\",\n    \"/lessornotequal\": \"\\u2268\",\n    \"/multicloseleft\": \"\\u22C9\",\n    \"/multicloseright\": \"\\u22CA\",\n    \"/notapproxequal\": \"\\u2247\",\n    \"/notarrowboth\": \"\\u21AE\",\n    \"/notarrowleft\": \"\\u219A\",\n    \"/notarrowright\": \"\\u219B\",\n    \"/notbar\": \"\\u2224\",\n    \"/notdblarrowboth\": \"\\u21CE\",\n    \"/notdblarrowleft\": \"\\u21CD\",\n    \"/notdblarrowright\": \"\\u21CF\",\n    \"/notfollows\": \"\\u2281\",\n    \"/notfollowsoreql\": \"\\u2AB0\",\n    \"/notforces\": \"\\u22AE\",\n    \"/notforcesextra\": \"\\u22AF\",\n    \"/notgreaterdblequal\": \"\\u2267\",\n    \"/notgreaterequal\": \"\\u2271\",\n    \"/notgreaterorslnteql\": \"\\u2A7E\",\n    \"/notlessdblequal\": \"\\u2266\",\n    \"/notlessequal\": \"\\u2270\",\n    \"/notlessorslnteql\": \"\\u2A7D\",\n    \"/notprecedesoreql\": \"\\u2AAF\",\n    \"/notsatisfies\": \"\\u22AD\",\n    \"/notsimilar\": \"\\u2241\",\n    \"/notsubseteql\": \"\\u2288\",\n    \"/notsubsetordbleql\": \"\\u2AC5\",\n    \"/notsubsetoreql\": \"\\u228A\",\n    \"/notsuperseteql\": \"\\u2289\",\n    \"/notsupersetordbleql\": \"\\u2AC6\",\n    \"/notsupersetoreql\": \"\\u228B\",\n    \"/nottriangeqlleft\": \"\\u22EC\",\n    \"/nottriangeqlright\": \"\\u22ED\",\n    \"/nottriangleleft\": \"\\u22EA\",\n    \"/nottriangleright\": \"\\u22EB\",\n    \"/notturnstile\": \"\\u22AC\",\n    \"/planckover2pi\": \"\\u210F\",\n    \"/planckover2pi1\": \"\\u210F\",\n    \"/precedenotdbleqv\": \"\\u2AB9\",\n    \"/precedenotslnteql\": \"\\u2AB5\",\n    \"/precedeornoteqvlnt\": \"\\u22E8\",\n    \"/subsetnoteql\": \"\\u228A\",\n    \"/subsetornotdbleql\": \"\\u2ACB\",\n    \"/supersetnoteql\": \"\\u228B\",\n    \"/supersetornotdbleql\": \"\\u2ACC\",\n    \"/upslope\": \"\\u29F8\",\n}\n\n\ndef _complete() -> None:\n    for i in range(256):\n        adobe_glyphs[f\"/a{i}\"] = chr(i)\n    adobe_glyphs[\"/.notdef\"] = \"□\"\n\n\n_complete()\n"
  },
  {
    "path": "pypdf/_codecs/core_font_metrics.py",
    "content": "# This file is based upon the 14 core AFM files provided by Adobe/Macromedia at\n# https://download.macromedia.com/pub/developer/opentype/tech-notes/Core14_AFMs.zip\n# The original copyright follows:\n#\n# -----------------------------------------------------------------------------------------------\n# Core 14 AFM Files - ReadMe\n#\n# This file and the 14 PostScript(R) AFM files it accompanies may be used, copied, and\n# distributed for any purpose and without charge, with or without modification, provided that all\n# copyright notices are retained; that the AFM files are not distributed without this file; that\n# all modifications to this file or any of the AFM files are prominently noted in the modified\n# file(s); and that this paragraph is not modified. Adobe Systems has no responsibility or\n# obligation to support the use of the AFM files.\n# -----------------------------------------------------------------------------------------------\n\n\nfrom pypdf._font import CoreFontMetrics, FontDescriptor\n\nCORE_FONT_METRICS: dict[str, CoreFontMetrics] = {\n    # Generated from Courier.afm\n    # Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated.  All Rights\n    # Reserved.\n    \"Courier\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Courier\",\n            family=\"Courier\",\n            weight=\"Medium\",\n            ascent=629,\n            descent=-157,\n            cap_height=562,\n            x_height=426,\n            italic_angle=0,\n            flags=33,\n            bbox=(-23.0, -250.0, 715.0, 805.0),\n        ),\n        character_widths={\n            \" \": 600,\n            \"default\": 600,\n            \"!\": 600,\n            '\"': 600,\n            \"#\": 600,\n            \"$\": 600,\n            \"%\": 600,\n            \"&\": 600,\n            \"\\u2019\": 600,\n            \"(\": 600,\n            \")\": 600,\n            \"*\": 600,\n            \"+\": 600,\n            \",\": 600,\n            \"-\": 600,\n            \".\": 600,\n            \"/\": 600,\n            \"0\": 600,\n            \"1\": 600,\n            \"2\": 600,\n            \"3\": 600,\n            \"4\": 600,\n            \"5\": 600,\n            \"6\": 600,\n            \"7\": 600,\n            \"8\": 600,\n            \"9\": 600,\n            \":\": 600,\n            \";\": 600,\n            \"<\": 600,\n            \"=\": 600,\n            \">\": 600,\n            \"?\": 600,\n            \"@\": 600,\n            \"A\": 600,\n            \"B\": 600,\n            \"C\": 600,\n            \"D\": 600,\n            \"E\": 600,\n            \"F\": 600,\n            \"G\": 600,\n            \"H\": 600,\n            \"I\": 600,\n            \"J\": 600,\n            \"K\": 600,\n            \"L\": 600,\n            \"M\": 600,\n            \"N\": 600,\n            \"O\": 600,\n            \"P\": 600,\n            \"Q\": 600,\n            \"R\": 600,\n            \"S\": 600,\n            \"T\": 600,\n            \"U\": 600,\n            \"V\": 600,\n            \"W\": 600,\n            \"X\": 600,\n            \"Y\": 600,\n            \"Z\": 600,\n            \"[\": 600,\n            \"\\\\\": 600,\n            \"]\": 600,\n            \"^\": 600,\n            \"_\": 600,\n            \"\\u2018\": 600,\n            \"a\": 600,\n            \"b\": 600,\n            \"c\": 600,\n            \"d\": 600,\n            \"e\": 600,\n            \"f\": 600,\n            \"g\": 600,\n            \"h\": 600,\n            \"i\": 600,\n            \"j\": 600,\n            \"k\": 600,\n            \"l\": 600,\n            \"m\": 600,\n            \"n\": 600,\n            \"o\": 600,\n            \"p\": 600,\n            \"q\": 600,\n            \"r\": 600,\n            \"s\": 600,\n            \"t\": 600,\n            \"u\": 600,\n            \"v\": 600,\n            \"w\": 600,\n            \"x\": 600,\n            \"y\": 600,\n            \"z\": 600,\n            \"{\": 600,\n            \"|\": 600,\n            \"}\": 600,\n            \"~\": 600,\n            \"\\xa1\": 600,\n            \"\\xa2\": 600,\n            \"\\xa3\": 600,\n            \"\\u2044\": 600,\n            \"\\xa5\": 600,\n            \"\\u0192\": 600,\n            \"\\xa7\": 600,\n            \"\\xa4\": 600,\n            \"'\": 600,\n            \"\\u201c\": 600,\n            \"\\xab\": 600,\n            \"\\u2039\": 600,\n            \"\\u203a\": 600,\n            \"\\ufb01\": 600,\n            \"\\ufb02\": 600,\n            \"\\u2013\": 600,\n            \"\\u2020\": 600,\n            \"\\u2021\": 600,\n            \"\\xb7\": 600,\n            \"\\xb6\": 600,\n            \"\\u2022\": 600,\n            \"\\u201a\": 600,\n            \"\\u201e\": 600,\n            \"\\u201d\": 600,\n            \"\\xbb\": 600,\n            \"\\u2026\": 600,\n            \"\\u2030\": 600,\n            \"\\xbf\": 600,\n            \"`\": 600,\n            \"\\xb4\": 600,\n            \"\\u02c6\": 600,\n            \"\\u02dc\": 600,\n            \"\\xaf\": 600,\n            \"\\u02d8\": 600,\n            \"\\u02d9\": 600,\n            \"\\xa8\": 600,\n            \"\\u02da\": 600,\n            \"\\xb8\": 600,\n            \"\\u02dd\": 600,\n            \"\\u02db\": 600,\n            \"\\u02c7\": 600,\n            \"\\u2014\": 600,\n            \"\\xc6\": 600,\n            \"\\xaa\": 600,\n            \"\\u0141\": 600,\n            \"\\xd8\": 600,\n            \"\\u0152\": 600,\n            \"\\xba\": 600,\n            \"\\xe6\": 600,\n            \"\\u0131\": 600,\n            \"\\u0142\": 600,\n            \"\\xf8\": 600,\n            \"\\u0153\": 600,\n            \"\\xdf\": 600,\n            \"\\xcf\": 600,\n            \"\\xe9\": 600,\n            \"\\u0103\": 600,\n            \"\\u0171\": 600,\n            \"\\u011b\": 600,\n            \"\\u0178\": 600,\n            \"\\xf7\": 600,\n            \"\\xdd\": 600,\n            \"\\xc2\": 600,\n            \"\\xe1\": 600,\n            \"\\xdb\": 600,\n            \"\\xfd\": 600,\n            \"\\u0219\": 600,\n            \"\\xea\": 600,\n            \"\\u016e\": 600,\n            \"\\xdc\": 600,\n            \"\\u0105\": 600,\n            \"\\xda\": 600,\n            \"\\u0173\": 600,\n            \"\\xcb\": 600,\n            \"\\u0110\": 600,\n            \"\\uf6c3\": 600,\n            \"\\xa9\": 600,\n            \"\\u0112\": 600,\n            \"\\u010d\": 600,\n            \"\\xe5\": 600,\n            \"\\u0145\": 600,\n            \"\\u013a\": 600,\n            \"\\xe0\": 600,\n            \"\\u0162\": 600,\n            \"\\u0106\": 600,\n            \"\\xe3\": 600,\n            \"\\u0116\": 600,\n            \"\\u0161\": 600,\n            \"\\u015f\": 600,\n            \"\\xed\": 600,\n            \"\\u25ca\": 600,\n            \"\\u0158\": 600,\n            \"\\u0122\": 600,\n            \"\\xfb\": 600,\n            \"\\xe2\": 600,\n            \"\\u0100\": 600,\n            \"\\u0159\": 600,\n            \"\\xe7\": 600,\n            \"\\u017b\": 600,\n            \"\\xde\": 600,\n            \"\\u014c\": 600,\n            \"\\u0154\": 600,\n            \"\\u015a\": 600,\n            \"\\u010f\": 600,\n            \"\\u016a\": 600,\n            \"\\u016f\": 600,\n            \"\\xb3\": 600,\n            \"\\xd2\": 600,\n            \"\\xc0\": 600,\n            \"\\u0102\": 600,\n            \"\\xd7\": 600,\n            \"\\xfa\": 600,\n            \"\\u0164\": 600,\n            \"\\u2202\": 600,\n            \"\\xff\": 600,\n            \"\\u0143\": 600,\n            \"\\xee\": 600,\n            \"\\xca\": 600,\n            \"\\xe4\": 600,\n            \"\\xeb\": 600,\n            \"\\u0107\": 600,\n            \"\\u0144\": 600,\n            \"\\u016b\": 600,\n            \"\\u0147\": 600,\n            \"\\xcd\": 600,\n            \"\\xb1\": 600,\n            \"\\xa6\": 600,\n            \"\\xae\": 600,\n            \"\\u011e\": 600,\n            \"\\u0130\": 600,\n            \"\\u2211\": 600,\n            \"\\xc8\": 600,\n            \"\\u0155\": 600,\n            \"\\u014d\": 600,\n            \"\\u0179\": 600,\n            \"\\u017d\": 600,\n            \"\\u2265\": 600,\n            \"\\xd0\": 600,\n            \"\\xc7\": 600,\n            \"\\u013c\": 600,\n            \"\\u0165\": 600,\n            \"\\u0119\": 600,\n            \"\\u0172\": 600,\n            \"\\xc1\": 600,\n            \"\\xc4\": 600,\n            \"\\xe8\": 600,\n            \"\\u017a\": 600,\n            \"\\u012f\": 600,\n            \"\\xd3\": 600,\n            \"\\xf3\": 600,\n            \"\\u0101\": 600,\n            \"\\u015b\": 600,\n            \"\\xef\": 600,\n            \"\\xd4\": 600,\n            \"\\xd9\": 600,\n            \"\\u2206\": 600,\n            \"\\xfe\": 600,\n            \"\\xb2\": 600,\n            \"\\xd6\": 600,\n            \"\\xb5\": 600,\n            \"\\xec\": 600,\n            \"\\u0151\": 600,\n            \"\\u0118\": 600,\n            \"\\u0111\": 600,\n            \"\\xbe\": 600,\n            \"\\u015e\": 600,\n            \"\\u013e\": 600,\n            \"\\u0136\": 600,\n            \"\\u0139\": 600,\n            \"\\u2122\": 600,\n            \"\\u0117\": 600,\n            \"\\xcc\": 600,\n            \"\\u012a\": 600,\n            \"\\u013d\": 600,\n            \"\\xbd\": 600,\n            \"\\u2264\": 600,\n            \"\\xf4\": 600,\n            \"\\xf1\": 600,\n            \"\\u0170\": 600,\n            \"\\xc9\": 600,\n            \"\\u0113\": 600,\n            \"\\u011f\": 600,\n            \"\\xbc\": 600,\n            \"\\u0160\": 600,\n            \"\\u0218\": 600,\n            \"\\u0150\": 600,\n            \"\\xb0\": 600,\n            \"\\xf2\": 600,\n            \"\\u010c\": 600,\n            \"\\xf9\": 600,\n            \"\\u221a\": 600,\n            \"\\u010e\": 600,\n            \"\\u0157\": 600,\n            \"\\xd1\": 600,\n            \"\\xf5\": 600,\n            \"\\u0156\": 600,\n            \"\\u013b\": 600,\n            \"\\xc3\": 600,\n            \"\\u0104\": 600,\n            \"\\xc5\": 600,\n            \"\\xd5\": 600,\n            \"\\u017c\": 600,\n            \"\\u011a\": 600,\n            \"\\u012e\": 600,\n            \"\\u0137\": 600,\n            \"\\u2212\": 600,\n            \"\\xce\": 600,\n            \"\\u0148\": 600,\n            \"\\u0163\": 600,\n            \"\\xac\": 600,\n            \"\\xf6\": 600,\n            \"\\xfc\": 600,\n            \"\\u2260\": 600,\n            \"\\u0123\": 600,\n            \"\\xf0\": 600,\n            \"\\u017e\": 600,\n            \"\\u0146\": 600,\n            \"\\xb9\": 600,\n            \"\\u012b\": 600,\n            \"\\u20ac\": 600,\n        },\n    ),\n    # Generated from Courier-Bold.afm\n    # Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated.  All Rights Reserved.\n    \"Courier-Bold\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Courier-Bold\",\n            family=\"Courier\",\n            weight=\"Bold\",\n            ascent=629,\n            descent=-157,\n            cap_height=562,\n            x_height=439,\n            italic_angle=0,\n            flags=33,\n            bbox=(-113.0, -250.0, 749.0, 801.0),\n        ),\n        character_widths={\n            \" \": 600,\n            \"default\": 600,\n            \"!\": 600,\n            '\"': 600,\n            \"#\": 600,\n            \"$\": 600,\n            \"%\": 600,\n            \"&\": 600,\n            \"\\u2019\": 600,\n            \"(\": 600,\n            \")\": 600,\n            \"*\": 600,\n            \"+\": 600,\n            \",\": 600,\n            \"-\": 600,\n            \".\": 600,\n            \"/\": 600,\n            \"0\": 600,\n            \"1\": 600,\n            \"2\": 600,\n            \"3\": 600,\n            \"4\": 600,\n            \"5\": 600,\n            \"6\": 600,\n            \"7\": 600,\n            \"8\": 600,\n            \"9\": 600,\n            \":\": 600,\n            \";\": 600,\n            \"<\": 600,\n            \"=\": 600,\n            \">\": 600,\n            \"?\": 600,\n            \"@\": 600,\n            \"A\": 600,\n            \"B\": 600,\n            \"C\": 600,\n            \"D\": 600,\n            \"E\": 600,\n            \"F\": 600,\n            \"G\": 600,\n            \"H\": 600,\n            \"I\": 600,\n            \"J\": 600,\n            \"K\": 600,\n            \"L\": 600,\n            \"M\": 600,\n            \"N\": 600,\n            \"O\": 600,\n            \"P\": 600,\n            \"Q\": 600,\n            \"R\": 600,\n            \"S\": 600,\n            \"T\": 600,\n            \"U\": 600,\n            \"V\": 600,\n            \"W\": 600,\n            \"X\": 600,\n            \"Y\": 600,\n            \"Z\": 600,\n            \"[\": 600,\n            \"\\\\\": 600,\n            \"]\": 600,\n            \"^\": 600,\n            \"_\": 600,\n            \"\\u2018\": 600,\n            \"a\": 600,\n            \"b\": 600,\n            \"c\": 600,\n            \"d\": 600,\n            \"e\": 600,\n            \"f\": 600,\n            \"g\": 600,\n            \"h\": 600,\n            \"i\": 600,\n            \"j\": 600,\n            \"k\": 600,\n            \"l\": 600,\n            \"m\": 600,\n            \"n\": 600,\n            \"o\": 600,\n            \"p\": 600,\n            \"q\": 600,\n            \"r\": 600,\n            \"s\": 600,\n            \"t\": 600,\n            \"u\": 600,\n            \"v\": 600,\n            \"w\": 600,\n            \"x\": 600,\n            \"y\": 600,\n            \"z\": 600,\n            \"{\": 600,\n            \"|\": 600,\n            \"}\": 600,\n            \"~\": 600,\n            \"\\xa1\": 600,\n            \"\\xa2\": 600,\n            \"\\xa3\": 600,\n            \"\\u2044\": 600,\n            \"\\xa5\": 600,\n            \"\\u0192\": 600,\n            \"\\xa7\": 600,\n            \"\\xa4\": 600,\n            \"'\": 600,\n            \"\\u201c\": 600,\n            \"\\xab\": 600,\n            \"\\u2039\": 600,\n            \"\\u203a\": 600,\n            \"\\ufb01\": 600,\n            \"\\ufb02\": 600,\n            \"\\u2013\": 600,\n            \"\\u2020\": 600,\n            \"\\u2021\": 600,\n            \"\\xb7\": 600,\n            \"\\xb6\": 600,\n            \"\\u2022\": 600,\n            \"\\u201a\": 600,\n            \"\\u201e\": 600,\n            \"\\u201d\": 600,\n            \"\\xbb\": 600,\n            \"\\u2026\": 600,\n            \"\\u2030\": 600,\n            \"\\xbf\": 600,\n            \"`\": 600,\n            \"\\xb4\": 600,\n            \"\\u02c6\": 600,\n            \"\\u02dc\": 600,\n            \"\\xaf\": 600,\n            \"\\u02d8\": 600,\n            \"\\u02d9\": 600,\n            \"\\xa8\": 600,\n            \"\\u02da\": 600,\n            \"\\xb8\": 600,\n            \"\\u02dd\": 600,\n            \"\\u02db\": 600,\n            \"\\u02c7\": 600,\n            \"\\u2014\": 600,\n            \"\\xc6\": 600,\n            \"\\xaa\": 600,\n            \"\\u0141\": 600,\n            \"\\xd8\": 600,\n            \"\\u0152\": 600,\n            \"\\xba\": 600,\n            \"\\xe6\": 600,\n            \"\\u0131\": 600,\n            \"\\u0142\": 600,\n            \"\\xf8\": 600,\n            \"\\u0153\": 600,\n            \"\\xdf\": 600,\n            \"\\xcf\": 600,\n            \"\\xe9\": 600,\n            \"\\u0103\": 600,\n            \"\\u0171\": 600,\n            \"\\u011b\": 600,\n            \"\\u0178\": 600,\n            \"\\xf7\": 600,\n            \"\\xdd\": 600,\n            \"\\xc2\": 600,\n            \"\\xe1\": 600,\n            \"\\xdb\": 600,\n            \"\\xfd\": 600,\n            \"\\u0219\": 600,\n            \"\\xea\": 600,\n            \"\\u016e\": 600,\n            \"\\xdc\": 600,\n            \"\\u0105\": 600,\n            \"\\xda\": 600,\n            \"\\u0173\": 600,\n            \"\\xcb\": 600,\n            \"\\u0110\": 600,\n            \"\\uf6c3\": 600,\n            \"\\xa9\": 600,\n            \"\\u0112\": 600,\n            \"\\u010d\": 600,\n            \"\\xe5\": 600,\n            \"\\u0145\": 600,\n            \"\\u013a\": 600,\n            \"\\xe0\": 600,\n            \"\\u0162\": 600,\n            \"\\u0106\": 600,\n            \"\\xe3\": 600,\n            \"\\u0116\": 600,\n            \"\\u0161\": 600,\n            \"\\u015f\": 600,\n            \"\\xed\": 600,\n            \"\\u25ca\": 600,\n            \"\\u0158\": 600,\n            \"\\u0122\": 600,\n            \"\\xfb\": 600,\n            \"\\xe2\": 600,\n            \"\\u0100\": 600,\n            \"\\u0159\": 600,\n            \"\\xe7\": 600,\n            \"\\u017b\": 600,\n            \"\\xde\": 600,\n            \"\\u014c\": 600,\n            \"\\u0154\": 600,\n            \"\\u015a\": 600,\n            \"\\u010f\": 600,\n            \"\\u016a\": 600,\n            \"\\u016f\": 600,\n            \"\\xb3\": 600,\n            \"\\xd2\": 600,\n            \"\\xc0\": 600,\n            \"\\u0102\": 600,\n            \"\\xd7\": 600,\n            \"\\xfa\": 600,\n            \"\\u0164\": 600,\n            \"\\u2202\": 600,\n            \"\\xff\": 600,\n            \"\\u0143\": 600,\n            \"\\xee\": 600,\n            \"\\xca\": 600,\n            \"\\xe4\": 600,\n            \"\\xeb\": 600,\n            \"\\u0107\": 600,\n            \"\\u0144\": 600,\n            \"\\u016b\": 600,\n            \"\\u0147\": 600,\n            \"\\xcd\": 600,\n            \"\\xb1\": 600,\n            \"\\xa6\": 600,\n            \"\\xae\": 600,\n            \"\\u011e\": 600,\n            \"\\u0130\": 600,\n            \"\\u2211\": 600,\n            \"\\xc8\": 600,\n            \"\\u0155\": 600,\n            \"\\u014d\": 600,\n            \"\\u0179\": 600,\n            \"\\u017d\": 600,\n            \"\\u2265\": 600,\n            \"\\xd0\": 600,\n            \"\\xc7\": 600,\n            \"\\u013c\": 600,\n            \"\\u0165\": 600,\n            \"\\u0119\": 600,\n            \"\\u0172\": 600,\n            \"\\xc1\": 600,\n            \"\\xc4\": 600,\n            \"\\xe8\": 600,\n            \"\\u017a\": 600,\n            \"\\u012f\": 600,\n            \"\\xd3\": 600,\n            \"\\xf3\": 600,\n            \"\\u0101\": 600,\n            \"\\u015b\": 600,\n            \"\\xef\": 600,\n            \"\\xd4\": 600,\n            \"\\xd9\": 600,\n            \"\\u2206\": 600,\n            \"\\xfe\": 600,\n            \"\\xb2\": 600,\n            \"\\xd6\": 600,\n            \"\\xb5\": 600,\n            \"\\xec\": 600,\n            \"\\u0151\": 600,\n            \"\\u0118\": 600,\n            \"\\u0111\": 600,\n            \"\\xbe\": 600,\n            \"\\u015e\": 600,\n            \"\\u013e\": 600,\n            \"\\u0136\": 600,\n            \"\\u0139\": 600,\n            \"\\u2122\": 600,\n            \"\\u0117\": 600,\n            \"\\xcc\": 600,\n            \"\\u012a\": 600,\n            \"\\u013d\": 600,\n            \"\\xbd\": 600,\n            \"\\u2264\": 600,\n            \"\\xf4\": 600,\n            \"\\xf1\": 600,\n            \"\\u0170\": 600,\n            \"\\xc9\": 600,\n            \"\\u0113\": 600,\n            \"\\u011f\": 600,\n            \"\\xbc\": 600,\n            \"\\u0160\": 600,\n            \"\\u0218\": 600,\n            \"\\u0150\": 600,\n            \"\\xb0\": 600,\n            \"\\xf2\": 600,\n            \"\\u010c\": 600,\n            \"\\xf9\": 600,\n            \"\\u221a\": 600,\n            \"\\u010e\": 600,\n            \"\\u0157\": 600,\n            \"\\xd1\": 600,\n            \"\\xf5\": 600,\n            \"\\u0156\": 600,\n            \"\\u013b\": 600,\n            \"\\xc3\": 600,\n            \"\\u0104\": 600,\n            \"\\xc5\": 600,\n            \"\\xd5\": 600,\n            \"\\u017c\": 600,\n            \"\\u011a\": 600,\n            \"\\u012e\": 600,\n            \"\\u0137\": 600,\n            \"\\u2212\": 600,\n            \"\\xce\": 600,\n            \"\\u0148\": 600,\n            \"\\u0163\": 600,\n            \"\\xac\": 600,\n            \"\\xf6\": 600,\n            \"\\xfc\": 600,\n            \"\\u2260\": 600,\n            \"\\u0123\": 600,\n            \"\\xf0\": 600,\n            \"\\u017e\": 600,\n            \"\\u0146\": 600,\n            \"\\xb9\": 600,\n            \"\\u012b\": 600,\n            \"\\u20ac\": 600,\n        },\n    ),\n    # Generated from Courier-BoldOblique.afm\n    # Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated.  All Rights Reserved.\n    \"Courier-BoldOblique\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Courier-BoldOblique\",\n            family=\"Courier\",\n            weight=\"Bold\",\n            ascent=629,\n            descent=-157,\n            cap_height=562,\n            x_height=439,\n            italic_angle=-12,\n            flags=97,\n            bbox=(-57.0, -250.0, 869.0, 801.0),\n        ),\n        character_widths={\n            \" \": 600,\n            \"default\": 600,\n            \"!\": 600,\n            '\"': 600,\n            \"#\": 600,\n            \"$\": 600,\n            \"%\": 600,\n            \"&\": 600,\n            \"\\u2019\": 600,\n            \"(\": 600,\n            \")\": 600,\n            \"*\": 600,\n            \"+\": 600,\n            \",\": 600,\n            \"-\": 600,\n            \".\": 600,\n            \"/\": 600,\n            \"0\": 600,\n            \"1\": 600,\n            \"2\": 600,\n            \"3\": 600,\n            \"4\": 600,\n            \"5\": 600,\n            \"6\": 600,\n            \"7\": 600,\n            \"8\": 600,\n            \"9\": 600,\n            \":\": 600,\n            \";\": 600,\n            \"<\": 600,\n            \"=\": 600,\n            \">\": 600,\n            \"?\": 600,\n            \"@\": 600,\n            \"A\": 600,\n            \"B\": 600,\n            \"C\": 600,\n            \"D\": 600,\n            \"E\": 600,\n            \"F\": 600,\n            \"G\": 600,\n            \"H\": 600,\n            \"I\": 600,\n            \"J\": 600,\n            \"K\": 600,\n            \"L\": 600,\n            \"M\": 600,\n            \"N\": 600,\n            \"O\": 600,\n            \"P\": 600,\n            \"Q\": 600,\n            \"R\": 600,\n            \"S\": 600,\n            \"T\": 600,\n            \"U\": 600,\n            \"V\": 600,\n            \"W\": 600,\n            \"X\": 600,\n            \"Y\": 600,\n            \"Z\": 600,\n            \"[\": 600,\n            \"\\\\\": 600,\n            \"]\": 600,\n            \"^\": 600,\n            \"_\": 600,\n            \"\\u2018\": 600,\n            \"a\": 600,\n            \"b\": 600,\n            \"c\": 600,\n            \"d\": 600,\n            \"e\": 600,\n            \"f\": 600,\n            \"g\": 600,\n            \"h\": 600,\n            \"i\": 600,\n            \"j\": 600,\n            \"k\": 600,\n            \"l\": 600,\n            \"m\": 600,\n            \"n\": 600,\n            \"o\": 600,\n            \"p\": 600,\n            \"q\": 600,\n            \"r\": 600,\n            \"s\": 600,\n            \"t\": 600,\n            \"u\": 600,\n            \"v\": 600,\n            \"w\": 600,\n            \"x\": 600,\n            \"y\": 600,\n            \"z\": 600,\n            \"{\": 600,\n            \"|\": 600,\n            \"}\": 600,\n            \"~\": 600,\n            \"\\xa1\": 600,\n            \"\\xa2\": 600,\n            \"\\xa3\": 600,\n            \"\\u2044\": 600,\n            \"\\xa5\": 600,\n            \"\\u0192\": 600,\n            \"\\xa7\": 600,\n            \"\\xa4\": 600,\n            \"'\": 600,\n            \"\\u201c\": 600,\n            \"\\xab\": 600,\n            \"\\u2039\": 600,\n            \"\\u203a\": 600,\n            \"\\ufb01\": 600,\n            \"\\ufb02\": 600,\n            \"\\u2013\": 600,\n            \"\\u2020\": 600,\n            \"\\u2021\": 600,\n            \"\\xb7\": 600,\n            \"\\xb6\": 600,\n            \"\\u2022\": 600,\n            \"\\u201a\": 600,\n            \"\\u201e\": 600,\n            \"\\u201d\": 600,\n            \"\\xbb\": 600,\n            \"\\u2026\": 600,\n            \"\\u2030\": 600,\n            \"\\xbf\": 600,\n            \"`\": 600,\n            \"\\xb4\": 600,\n            \"\\u02c6\": 600,\n            \"\\u02dc\": 600,\n            \"\\xaf\": 600,\n            \"\\u02d8\": 600,\n            \"\\u02d9\": 600,\n            \"\\xa8\": 600,\n            \"\\u02da\": 600,\n            \"\\xb8\": 600,\n            \"\\u02dd\": 600,\n            \"\\u02db\": 600,\n            \"\\u02c7\": 600,\n            \"\\u2014\": 600,\n            \"\\xc6\": 600,\n            \"\\xaa\": 600,\n            \"\\u0141\": 600,\n            \"\\xd8\": 600,\n            \"\\u0152\": 600,\n            \"\\xba\": 600,\n            \"\\xe6\": 600,\n            \"\\u0131\": 600,\n            \"\\u0142\": 600,\n            \"\\xf8\": 600,\n            \"\\u0153\": 600,\n            \"\\xdf\": 600,\n            \"\\xcf\": 600,\n            \"\\xe9\": 600,\n            \"\\u0103\": 600,\n            \"\\u0171\": 600,\n            \"\\u011b\": 600,\n            \"\\u0178\": 600,\n            \"\\xf7\": 600,\n            \"\\xdd\": 600,\n            \"\\xc2\": 600,\n            \"\\xe1\": 600,\n            \"\\xdb\": 600,\n            \"\\xfd\": 600,\n            \"\\u0219\": 600,\n            \"\\xea\": 600,\n            \"\\u016e\": 600,\n            \"\\xdc\": 600,\n            \"\\u0105\": 600,\n            \"\\xda\": 600,\n            \"\\u0173\": 600,\n            \"\\xcb\": 600,\n            \"\\u0110\": 600,\n            \"\\uf6c3\": 600,\n            \"\\xa9\": 600,\n            \"\\u0112\": 600,\n            \"\\u010d\": 600,\n            \"\\xe5\": 600,\n            \"\\u0145\": 600,\n            \"\\u013a\": 600,\n            \"\\xe0\": 600,\n            \"\\u0162\": 600,\n            \"\\u0106\": 600,\n            \"\\xe3\": 600,\n            \"\\u0116\": 600,\n            \"\\u0161\": 600,\n            \"\\u015f\": 600,\n            \"\\xed\": 600,\n            \"\\u25ca\": 600,\n            \"\\u0158\": 600,\n            \"\\u0122\": 600,\n            \"\\xfb\": 600,\n            \"\\xe2\": 600,\n            \"\\u0100\": 600,\n            \"\\u0159\": 600,\n            \"\\xe7\": 600,\n            \"\\u017b\": 600,\n            \"\\xde\": 600,\n            \"\\u014c\": 600,\n            \"\\u0154\": 600,\n            \"\\u015a\": 600,\n            \"\\u010f\": 600,\n            \"\\u016a\": 600,\n            \"\\u016f\": 600,\n            \"\\xb3\": 600,\n            \"\\xd2\": 600,\n            \"\\xc0\": 600,\n            \"\\u0102\": 600,\n            \"\\xd7\": 600,\n            \"\\xfa\": 600,\n            \"\\u0164\": 600,\n            \"\\u2202\": 600,\n            \"\\xff\": 600,\n            \"\\u0143\": 600,\n            \"\\xee\": 600,\n            \"\\xca\": 600,\n            \"\\xe4\": 600,\n            \"\\xeb\": 600,\n            \"\\u0107\": 600,\n            \"\\u0144\": 600,\n            \"\\u016b\": 600,\n            \"\\u0147\": 600,\n            \"\\xcd\": 600,\n            \"\\xb1\": 600,\n            \"\\xa6\": 600,\n            \"\\xae\": 600,\n            \"\\u011e\": 600,\n            \"\\u0130\": 600,\n            \"\\u2211\": 600,\n            \"\\xc8\": 600,\n            \"\\u0155\": 600,\n            \"\\u014d\": 600,\n            \"\\u0179\": 600,\n            \"\\u017d\": 600,\n            \"\\u2265\": 600,\n            \"\\xd0\": 600,\n            \"\\xc7\": 600,\n            \"\\u013c\": 600,\n            \"\\u0165\": 600,\n            \"\\u0119\": 600,\n            \"\\u0172\": 600,\n            \"\\xc1\": 600,\n            \"\\xc4\": 600,\n            \"\\xe8\": 600,\n            \"\\u017a\": 600,\n            \"\\u012f\": 600,\n            \"\\xd3\": 600,\n            \"\\xf3\": 600,\n            \"\\u0101\": 600,\n            \"\\u015b\": 600,\n            \"\\xef\": 600,\n            \"\\xd4\": 600,\n            \"\\xd9\": 600,\n            \"\\u2206\": 600,\n            \"\\xfe\": 600,\n            \"\\xb2\": 600,\n            \"\\xd6\": 600,\n            \"\\xb5\": 600,\n            \"\\xec\": 600,\n            \"\\u0151\": 600,\n            \"\\u0118\": 600,\n            \"\\u0111\": 600,\n            \"\\xbe\": 600,\n            \"\\u015e\": 600,\n            \"\\u013e\": 600,\n            \"\\u0136\": 600,\n            \"\\u0139\": 600,\n            \"\\u2122\": 600,\n            \"\\u0117\": 600,\n            \"\\xcc\": 600,\n            \"\\u012a\": 600,\n            \"\\u013d\": 600,\n            \"\\xbd\": 600,\n            \"\\u2264\": 600,\n            \"\\xf4\": 600,\n            \"\\xf1\": 600,\n            \"\\u0170\": 600,\n            \"\\xc9\": 600,\n            \"\\u0113\": 600,\n            \"\\u011f\": 600,\n            \"\\xbc\": 600,\n            \"\\u0160\": 600,\n            \"\\u0218\": 600,\n            \"\\u0150\": 600,\n            \"\\xb0\": 600,\n            \"\\xf2\": 600,\n            \"\\u010c\": 600,\n            \"\\xf9\": 600,\n            \"\\u221a\": 600,\n            \"\\u010e\": 600,\n            \"\\u0157\": 600,\n            \"\\xd1\": 600,\n            \"\\xf5\": 600,\n            \"\\u0156\": 600,\n            \"\\u013b\": 600,\n            \"\\xc3\": 600,\n            \"\\u0104\": 600,\n            \"\\xc5\": 600,\n            \"\\xd5\": 600,\n            \"\\u017c\": 600,\n            \"\\u011a\": 600,\n            \"\\u012e\": 600,\n            \"\\u0137\": 600,\n            \"\\u2212\": 600,\n            \"\\xce\": 600,\n            \"\\u0148\": 600,\n            \"\\u0163\": 600,\n            \"\\xac\": 600,\n            \"\\xf6\": 600,\n            \"\\xfc\": 600,\n            \"\\u2260\": 600,\n            \"\\u0123\": 600,\n            \"\\xf0\": 600,\n            \"\\u017e\": 600,\n            \"\\u0146\": 600,\n            \"\\xb9\": 600,\n            \"\\u012b\": 600,\n            \"\\u20ac\": 600,\n        },\n    ),\n    # Generated from Courier-Oblique.afm\n    # Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated.  All Rights\n    # Reserved.\n    \"Courier-Oblique\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Courier-Oblique\",\n            family=\"Courier\",\n            weight=\"Medium\",\n            ascent=629,\n            descent=-157,\n            cap_height=562,\n            x_height=426,\n            italic_angle=-12,\n            flags=97,\n            bbox=(-27.0, -250.0, 849.0, 805.0),\n        ),\n        character_widths={\n            \" \": 600,\n            \"default\": 600,\n            \"!\": 600,\n            '\"': 600,\n            \"#\": 600,\n            \"$\": 600,\n            \"%\": 600,\n            \"&\": 600,\n            \"\\u2019\": 600,\n            \"(\": 600,\n            \")\": 600,\n            \"*\": 600,\n            \"+\": 600,\n            \",\": 600,\n            \"-\": 600,\n            \".\": 600,\n            \"/\": 600,\n            \"0\": 600,\n            \"1\": 600,\n            \"2\": 600,\n            \"3\": 600,\n            \"4\": 600,\n            \"5\": 600,\n            \"6\": 600,\n            \"7\": 600,\n            \"8\": 600,\n            \"9\": 600,\n            \":\": 600,\n            \";\": 600,\n            \"<\": 600,\n            \"=\": 600,\n            \">\": 600,\n            \"?\": 600,\n            \"@\": 600,\n            \"A\": 600,\n            \"B\": 600,\n            \"C\": 600,\n            \"D\": 600,\n            \"E\": 600,\n            \"F\": 600,\n            \"G\": 600,\n            \"H\": 600,\n            \"I\": 600,\n            \"J\": 600,\n            \"K\": 600,\n            \"L\": 600,\n            \"M\": 600,\n            \"N\": 600,\n            \"O\": 600,\n            \"P\": 600,\n            \"Q\": 600,\n            \"R\": 600,\n            \"S\": 600,\n            \"T\": 600,\n            \"U\": 600,\n            \"V\": 600,\n            \"W\": 600,\n            \"X\": 600,\n            \"Y\": 600,\n            \"Z\": 600,\n            \"[\": 600,\n            \"\\\\\": 600,\n            \"]\": 600,\n            \"^\": 600,\n            \"_\": 600,\n            \"\\u2018\": 600,\n            \"a\": 600,\n            \"b\": 600,\n            \"c\": 600,\n            \"d\": 600,\n            \"e\": 600,\n            \"f\": 600,\n            \"g\": 600,\n            \"h\": 600,\n            \"i\": 600,\n            \"j\": 600,\n            \"k\": 600,\n            \"l\": 600,\n            \"m\": 600,\n            \"n\": 600,\n            \"o\": 600,\n            \"p\": 600,\n            \"q\": 600,\n            \"r\": 600,\n            \"s\": 600,\n            \"t\": 600,\n            \"u\": 600,\n            \"v\": 600,\n            \"w\": 600,\n            \"x\": 600,\n            \"y\": 600,\n            \"z\": 600,\n            \"{\": 600,\n            \"|\": 600,\n            \"}\": 600,\n            \"~\": 600,\n            \"\\xa1\": 600,\n            \"\\xa2\": 600,\n            \"\\xa3\": 600,\n            \"\\u2044\": 600,\n            \"\\xa5\": 600,\n            \"\\u0192\": 600,\n            \"\\xa7\": 600,\n            \"\\xa4\": 600,\n            \"'\": 600,\n            \"\\u201c\": 600,\n            \"\\xab\": 600,\n            \"\\u2039\": 600,\n            \"\\u203a\": 600,\n            \"\\ufb01\": 600,\n            \"\\ufb02\": 600,\n            \"\\u2013\": 600,\n            \"\\u2020\": 600,\n            \"\\u2021\": 600,\n            \"\\xb7\": 600,\n            \"\\xb6\": 600,\n            \"\\u2022\": 600,\n            \"\\u201a\": 600,\n            \"\\u201e\": 600,\n            \"\\u201d\": 600,\n            \"\\xbb\": 600,\n            \"\\u2026\": 600,\n            \"\\u2030\": 600,\n            \"\\xbf\": 600,\n            \"`\": 600,\n            \"\\xb4\": 600,\n            \"\\u02c6\": 600,\n            \"\\u02dc\": 600,\n            \"\\xaf\": 600,\n            \"\\u02d8\": 600,\n            \"\\u02d9\": 600,\n            \"\\xa8\": 600,\n            \"\\u02da\": 600,\n            \"\\xb8\": 600,\n            \"\\u02dd\": 600,\n            \"\\u02db\": 600,\n            \"\\u02c7\": 600,\n            \"\\u2014\": 600,\n            \"\\xc6\": 600,\n            \"\\xaa\": 600,\n            \"\\u0141\": 600,\n            \"\\xd8\": 600,\n            \"\\u0152\": 600,\n            \"\\xba\": 600,\n            \"\\xe6\": 600,\n            \"\\u0131\": 600,\n            \"\\u0142\": 600,\n            \"\\xf8\": 600,\n            \"\\u0153\": 600,\n            \"\\xdf\": 600,\n            \"\\xcf\": 600,\n            \"\\xe9\": 600,\n            \"\\u0103\": 600,\n            \"\\u0171\": 600,\n            \"\\u011b\": 600,\n            \"\\u0178\": 600,\n            \"\\xf7\": 600,\n            \"\\xdd\": 600,\n            \"\\xc2\": 600,\n            \"\\xe1\": 600,\n            \"\\xdb\": 600,\n            \"\\xfd\": 600,\n            \"\\u0219\": 600,\n            \"\\xea\": 600,\n            \"\\u016e\": 600,\n            \"\\xdc\": 600,\n            \"\\u0105\": 600,\n            \"\\xda\": 600,\n            \"\\u0173\": 600,\n            \"\\xcb\": 600,\n            \"\\u0110\": 600,\n            \"\\uf6c3\": 600,\n            \"\\xa9\": 600,\n            \"\\u0112\": 600,\n            \"\\u010d\": 600,\n            \"\\xe5\": 600,\n            \"\\u0145\": 600,\n            \"\\u013a\": 600,\n            \"\\xe0\": 600,\n            \"\\u0162\": 600,\n            \"\\u0106\": 600,\n            \"\\xe3\": 600,\n            \"\\u0116\": 600,\n            \"\\u0161\": 600,\n            \"\\u015f\": 600,\n            \"\\xed\": 600,\n            \"\\u25ca\": 600,\n            \"\\u0158\": 600,\n            \"\\u0122\": 600,\n            \"\\xfb\": 600,\n            \"\\xe2\": 600,\n            \"\\u0100\": 600,\n            \"\\u0159\": 600,\n            \"\\xe7\": 600,\n            \"\\u017b\": 600,\n            \"\\xde\": 600,\n            \"\\u014c\": 600,\n            \"\\u0154\": 600,\n            \"\\u015a\": 600,\n            \"\\u010f\": 600,\n            \"\\u016a\": 600,\n            \"\\u016f\": 600,\n            \"\\xb3\": 600,\n            \"\\xd2\": 600,\n            \"\\xc0\": 600,\n            \"\\u0102\": 600,\n            \"\\xd7\": 600,\n            \"\\xfa\": 600,\n            \"\\u0164\": 600,\n            \"\\u2202\": 600,\n            \"\\xff\": 600,\n            \"\\u0143\": 600,\n            \"\\xee\": 600,\n            \"\\xca\": 600,\n            \"\\xe4\": 600,\n            \"\\xeb\": 600,\n            \"\\u0107\": 600,\n            \"\\u0144\": 600,\n            \"\\u016b\": 600,\n            \"\\u0147\": 600,\n            \"\\xcd\": 600,\n            \"\\xb1\": 600,\n            \"\\xa6\": 600,\n            \"\\xae\": 600,\n            \"\\u011e\": 600,\n            \"\\u0130\": 600,\n            \"\\u2211\": 600,\n            \"\\xc8\": 600,\n            \"\\u0155\": 600,\n            \"\\u014d\": 600,\n            \"\\u0179\": 600,\n            \"\\u017d\": 600,\n            \"\\u2265\": 600,\n            \"\\xd0\": 600,\n            \"\\xc7\": 600,\n            \"\\u013c\": 600,\n            \"\\u0165\": 600,\n            \"\\u0119\": 600,\n            \"\\u0172\": 600,\n            \"\\xc1\": 600,\n            \"\\xc4\": 600,\n            \"\\xe8\": 600,\n            \"\\u017a\": 600,\n            \"\\u012f\": 600,\n            \"\\xd3\": 600,\n            \"\\xf3\": 600,\n            \"\\u0101\": 600,\n            \"\\u015b\": 600,\n            \"\\xef\": 600,\n            \"\\xd4\": 600,\n            \"\\xd9\": 600,\n            \"\\u2206\": 600,\n            \"\\xfe\": 600,\n            \"\\xb2\": 600,\n            \"\\xd6\": 600,\n            \"\\xb5\": 600,\n            \"\\xec\": 600,\n            \"\\u0151\": 600,\n            \"\\u0118\": 600,\n            \"\\u0111\": 600,\n            \"\\xbe\": 600,\n            \"\\u015e\": 600,\n            \"\\u013e\": 600,\n            \"\\u0136\": 600,\n            \"\\u0139\": 600,\n            \"\\u2122\": 600,\n            \"\\u0117\": 600,\n            \"\\xcc\": 600,\n            \"\\u012a\": 600,\n            \"\\u013d\": 600,\n            \"\\xbd\": 600,\n            \"\\u2264\": 600,\n            \"\\xf4\": 600,\n            \"\\xf1\": 600,\n            \"\\u0170\": 600,\n            \"\\xc9\": 600,\n            \"\\u0113\": 600,\n            \"\\u011f\": 600,\n            \"\\xbc\": 600,\n            \"\\u0160\": 600,\n            \"\\u0218\": 600,\n            \"\\u0150\": 600,\n            \"\\xb0\": 600,\n            \"\\xf2\": 600,\n            \"\\u010c\": 600,\n            \"\\xf9\": 600,\n            \"\\u221a\": 600,\n            \"\\u010e\": 600,\n            \"\\u0157\": 600,\n            \"\\xd1\": 600,\n            \"\\xf5\": 600,\n            \"\\u0156\": 600,\n            \"\\u013b\": 600,\n            \"\\xc3\": 600,\n            \"\\u0104\": 600,\n            \"\\xc5\": 600,\n            \"\\xd5\": 600,\n            \"\\u017c\": 600,\n            \"\\u011a\": 600,\n            \"\\u012e\": 600,\n            \"\\u0137\": 600,\n            \"\\u2212\": 600,\n            \"\\xce\": 600,\n            \"\\u0148\": 600,\n            \"\\u0163\": 600,\n            \"\\xac\": 600,\n            \"\\xf6\": 600,\n            \"\\xfc\": 600,\n            \"\\u2260\": 600,\n            \"\\u0123\": 600,\n            \"\\xf0\": 600,\n            \"\\u017e\": 600,\n            \"\\u0146\": 600,\n            \"\\xb9\": 600,\n            \"\\u012b\": 600,\n            \"\\u20ac\": 600,\n        },\n    ),\n    # Generated from Helvetica.afm\n    # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated.  All Rights Reserved.\n    # Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries.\n    \"Helvetica\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Helvetica\",\n            family=\"Helvetica\",\n            weight=\"Medium\",\n            ascent=718,\n            descent=-207,\n            cap_height=718,\n            x_height=523,\n            italic_angle=0,\n            flags=32,\n            bbox=(-166.0, -225.0, 1000.0, 931.0),\n        ),\n        character_widths={\n            \" \": 278,\n            \"default\": 556,\n            \"!\": 278,\n            '\"': 355,\n            \"#\": 556,\n            \"$\": 556,\n            \"%\": 889,\n            \"&\": 667,\n            \"\\u2019\": 222,\n            \"(\": 333,\n            \")\": 333,\n            \"*\": 389,\n            \"+\": 584,\n            \",\": 278,\n            \"-\": 333,\n            \".\": 278,\n            \"/\": 278,\n            \"0\": 556,\n            \"1\": 556,\n            \"2\": 556,\n            \"3\": 556,\n            \"4\": 556,\n            \"5\": 556,\n            \"6\": 556,\n            \"7\": 556,\n            \"8\": 556,\n            \"9\": 556,\n            \":\": 278,\n            \";\": 278,\n            \"<\": 584,\n            \"=\": 584,\n            \">\": 584,\n            \"?\": 556,\n            \"@\": 1015,\n            \"A\": 667,\n            \"B\": 667,\n            \"C\": 722,\n            \"D\": 722,\n            \"E\": 667,\n            \"F\": 611,\n            \"G\": 778,\n            \"H\": 722,\n            \"I\": 278,\n            \"J\": 500,\n            \"K\": 667,\n            \"L\": 556,\n            \"M\": 833,\n            \"N\": 722,\n            \"O\": 778,\n            \"P\": 667,\n            \"Q\": 778,\n            \"R\": 722,\n            \"S\": 667,\n            \"T\": 611,\n            \"U\": 722,\n            \"V\": 667,\n            \"W\": 944,\n            \"X\": 667,\n            \"Y\": 667,\n            \"Z\": 611,\n            \"[\": 278,\n            \"\\\\\": 278,\n            \"]\": 278,\n            \"^\": 469,\n            \"_\": 556,\n            \"\\u2018\": 222,\n            \"a\": 556,\n            \"b\": 556,\n            \"c\": 500,\n            \"d\": 556,\n            \"e\": 556,\n            \"f\": 278,\n            \"g\": 556,\n            \"h\": 556,\n            \"i\": 222,\n            \"j\": 222,\n            \"k\": 500,\n            \"l\": 222,\n            \"m\": 833,\n            \"n\": 556,\n            \"o\": 556,\n            \"p\": 556,\n            \"q\": 556,\n            \"r\": 333,\n            \"s\": 500,\n            \"t\": 278,\n            \"u\": 556,\n            \"v\": 500,\n            \"w\": 722,\n            \"x\": 500,\n            \"y\": 500,\n            \"z\": 500,\n            \"{\": 334,\n            \"|\": 260,\n            \"}\": 334,\n            \"~\": 584,\n            \"\\xa1\": 333,\n            \"\\xa2\": 556,\n            \"\\xa3\": 556,\n            \"\\u2044\": 167,\n            \"\\xa5\": 556,\n            \"\\u0192\": 556,\n            \"\\xa7\": 556,\n            \"\\xa4\": 556,\n            \"'\": 191,\n            \"\\u201c\": 333,\n            \"\\xab\": 556,\n            \"\\u2039\": 333,\n            \"\\u203a\": 333,\n            \"\\ufb01\": 500,\n            \"\\ufb02\": 500,\n            \"\\u2013\": 556,\n            \"\\u2020\": 556,\n            \"\\u2021\": 556,\n            \"\\xb7\": 278,\n            \"\\xb6\": 537,\n            \"\\u2022\": 350,\n            \"\\u201a\": 222,\n            \"\\u201e\": 333,\n            \"\\u201d\": 333,\n            \"\\xbb\": 556,\n            \"\\u2026\": 1000,\n            \"\\u2030\": 1000,\n            \"\\xbf\": 611,\n            \"`\": 333,\n            \"\\xb4\": 333,\n            \"\\u02c6\": 333,\n            \"\\u02dc\": 333,\n            \"\\xaf\": 333,\n            \"\\u02d8\": 333,\n            \"\\u02d9\": 333,\n            \"\\xa8\": 333,\n            \"\\u02da\": 333,\n            \"\\xb8\": 333,\n            \"\\u02dd\": 333,\n            \"\\u02db\": 333,\n            \"\\u02c7\": 333,\n            \"\\u2014\": 1000,\n            \"\\xc6\": 1000,\n            \"\\xaa\": 370,\n            \"\\u0141\": 556,\n            \"\\xd8\": 778,\n            \"\\u0152\": 1000,\n            \"\\xba\": 365,\n            \"\\xe6\": 889,\n            \"\\u0131\": 278,\n            \"\\u0142\": 222,\n            \"\\xf8\": 611,\n            \"\\u0153\": 944,\n            \"\\xdf\": 611,\n            \"\\xcf\": 278,\n            \"\\xe9\": 556,\n            \"\\u0103\": 556,\n            \"\\u0171\": 556,\n            \"\\u011b\": 556,\n            \"\\u0178\": 667,\n            \"\\xf7\": 584,\n            \"\\xdd\": 667,\n            \"\\xc2\": 667,\n            \"\\xe1\": 556,\n            \"\\xdb\": 722,\n            \"\\xfd\": 500,\n            \"\\u0219\": 500,\n            \"\\xea\": 556,\n            \"\\u016e\": 722,\n            \"\\xdc\": 722,\n            \"\\u0105\": 556,\n            \"\\xda\": 722,\n            \"\\u0173\": 556,\n            \"\\xcb\": 667,\n            \"\\u0110\": 722,\n            \"\\uf6c3\": 250,\n            \"\\xa9\": 737,\n            \"\\u0112\": 667,\n            \"\\u010d\": 500,\n            \"\\xe5\": 556,\n            \"\\u0145\": 722,\n            \"\\u013a\": 222,\n            \"\\xe0\": 556,\n            \"\\u0162\": 611,\n            \"\\u0106\": 722,\n            \"\\xe3\": 556,\n            \"\\u0116\": 667,\n            \"\\u0161\": 500,\n            \"\\u015f\": 500,\n            \"\\xed\": 278,\n            \"\\u25ca\": 471,\n            \"\\u0158\": 722,\n            \"\\u0122\": 778,\n            \"\\xfb\": 556,\n            \"\\xe2\": 556,\n            \"\\u0100\": 667,\n            \"\\u0159\": 333,\n            \"\\xe7\": 500,\n            \"\\u017b\": 611,\n            \"\\xde\": 667,\n            \"\\u014c\": 778,\n            \"\\u0154\": 722,\n            \"\\u015a\": 667,\n            \"\\u010f\": 643,\n            \"\\u016a\": 722,\n            \"\\u016f\": 556,\n            \"\\xb3\": 333,\n            \"\\xd2\": 778,\n            \"\\xc0\": 667,\n            \"\\u0102\": 667,\n            \"\\xd7\": 584,\n            \"\\xfa\": 556,\n            \"\\u0164\": 611,\n            \"\\u2202\": 476,\n            \"\\xff\": 500,\n            \"\\u0143\": 722,\n            \"\\xee\": 278,\n            \"\\xca\": 667,\n            \"\\xe4\": 556,\n            \"\\xeb\": 556,\n            \"\\u0107\": 500,\n            \"\\u0144\": 556,\n            \"\\u016b\": 556,\n            \"\\u0147\": 722,\n            \"\\xcd\": 278,\n            \"\\xb1\": 584,\n            \"\\xa6\": 260,\n            \"\\xae\": 737,\n            \"\\u011e\": 778,\n            \"\\u0130\": 278,\n            \"\\u2211\": 600,\n            \"\\xc8\": 667,\n            \"\\u0155\": 333,\n            \"\\u014d\": 556,\n            \"\\u0179\": 611,\n            \"\\u017d\": 611,\n            \"\\u2265\": 549,\n            \"\\xd0\": 722,\n            \"\\xc7\": 722,\n            \"\\u013c\": 222,\n            \"\\u0165\": 317,\n            \"\\u0119\": 556,\n            \"\\u0172\": 722,\n            \"\\xc1\": 667,\n            \"\\xc4\": 667,\n            \"\\xe8\": 556,\n            \"\\u017a\": 500,\n            \"\\u012f\": 222,\n            \"\\xd3\": 778,\n            \"\\xf3\": 556,\n            \"\\u0101\": 556,\n            \"\\u015b\": 500,\n            \"\\xef\": 278,\n            \"\\xd4\": 778,\n            \"\\xd9\": 722,\n            \"\\u2206\": 612,\n            \"\\xfe\": 556,\n            \"\\xb2\": 333,\n            \"\\xd6\": 778,\n            \"\\xb5\": 556,\n            \"\\xec\": 278,\n            \"\\u0151\": 556,\n            \"\\u0118\": 667,\n            \"\\u0111\": 556,\n            \"\\xbe\": 834,\n            \"\\u015e\": 667,\n            \"\\u013e\": 299,\n            \"\\u0136\": 667,\n            \"\\u0139\": 556,\n            \"\\u2122\": 1000,\n            \"\\u0117\": 556,\n            \"\\xcc\": 278,\n            \"\\u012a\": 278,\n            \"\\u013d\": 556,\n            \"\\xbd\": 834,\n            \"\\u2264\": 549,\n            \"\\xf4\": 556,\n            \"\\xf1\": 556,\n            \"\\u0170\": 722,\n            \"\\xc9\": 667,\n            \"\\u0113\": 556,\n            \"\\u011f\": 556,\n            \"\\xbc\": 834,\n            \"\\u0160\": 667,\n            \"\\u0218\": 667,\n            \"\\u0150\": 778,\n            \"\\xb0\": 400,\n            \"\\xf2\": 556,\n            \"\\u010c\": 722,\n            \"\\xf9\": 556,\n            \"\\u221a\": 453,\n            \"\\u010e\": 722,\n            \"\\u0157\": 333,\n            \"\\xd1\": 722,\n            \"\\xf5\": 556,\n            \"\\u0156\": 722,\n            \"\\u013b\": 556,\n            \"\\xc3\": 667,\n            \"\\u0104\": 667,\n            \"\\xc5\": 667,\n            \"\\xd5\": 778,\n            \"\\u017c\": 500,\n            \"\\u011a\": 667,\n            \"\\u012e\": 278,\n            \"\\u0137\": 500,\n            \"\\u2212\": 584,\n            \"\\xce\": 278,\n            \"\\u0148\": 556,\n            \"\\u0163\": 278,\n            \"\\xac\": 584,\n            \"\\xf6\": 556,\n            \"\\xfc\": 556,\n            \"\\u2260\": 549,\n            \"\\u0123\": 556,\n            \"\\xf0\": 556,\n            \"\\u017e\": 500,\n            \"\\u0146\": 556,\n            \"\\xb9\": 333,\n            \"\\u012b\": 278,\n            \"\\u20ac\": 556,\n        },\n    ),\n    # Generated from Helvetica-Bold.afm\n    # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated.  All Rights Reserved.\n    # Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries.\n    \"Helvetica-Bold\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Helvetica-Bold\",\n            family=\"Helvetica\",\n            weight=\"Bold\",\n            ascent=718,\n            descent=-207,\n            cap_height=718,\n            x_height=532,\n            italic_angle=0,\n            flags=32,\n            bbox=(-170.0, -228.0, 1003.0, 962.0),\n        ),\n        character_widths={\n            \" \": 278,\n            \"default\": 556,\n            \"!\": 333,\n            '\"': 474,\n            \"#\": 556,\n            \"$\": 556,\n            \"%\": 889,\n            \"&\": 722,\n            \"\\u2019\": 278,\n            \"(\": 333,\n            \")\": 333,\n            \"*\": 389,\n            \"+\": 584,\n            \",\": 278,\n            \"-\": 333,\n            \".\": 278,\n            \"/\": 278,\n            \"0\": 556,\n            \"1\": 556,\n            \"2\": 556,\n            \"3\": 556,\n            \"4\": 556,\n            \"5\": 556,\n            \"6\": 556,\n            \"7\": 556,\n            \"8\": 556,\n            \"9\": 556,\n            \":\": 333,\n            \";\": 333,\n            \"<\": 584,\n            \"=\": 584,\n            \">\": 584,\n            \"?\": 611,\n            \"@\": 975,\n            \"A\": 722,\n            \"B\": 722,\n            \"C\": 722,\n            \"D\": 722,\n            \"E\": 667,\n            \"F\": 611,\n            \"G\": 778,\n            \"H\": 722,\n            \"I\": 278,\n            \"J\": 556,\n            \"K\": 722,\n            \"L\": 611,\n            \"M\": 833,\n            \"N\": 722,\n            \"O\": 778,\n            \"P\": 667,\n            \"Q\": 778,\n            \"R\": 722,\n            \"S\": 667,\n            \"T\": 611,\n            \"U\": 722,\n            \"V\": 667,\n            \"W\": 944,\n            \"X\": 667,\n            \"Y\": 667,\n            \"Z\": 611,\n            \"[\": 333,\n            \"\\\\\": 278,\n            \"]\": 333,\n            \"^\": 584,\n            \"_\": 556,\n            \"\\u2018\": 278,\n            \"a\": 556,\n            \"b\": 611,\n            \"c\": 556,\n            \"d\": 611,\n            \"e\": 556,\n            \"f\": 333,\n            \"g\": 611,\n            \"h\": 611,\n            \"i\": 278,\n            \"j\": 278,\n            \"k\": 556,\n            \"l\": 278,\n            \"m\": 889,\n            \"n\": 611,\n            \"o\": 611,\n            \"p\": 611,\n            \"q\": 611,\n            \"r\": 389,\n            \"s\": 556,\n            \"t\": 333,\n            \"u\": 611,\n            \"v\": 556,\n            \"w\": 778,\n            \"x\": 556,\n            \"y\": 556,\n            \"z\": 500,\n            \"{\": 389,\n            \"|\": 280,\n            \"}\": 389,\n            \"~\": 584,\n            \"\\xa1\": 333,\n            \"\\xa2\": 556,\n            \"\\xa3\": 556,\n            \"\\u2044\": 167,\n            \"\\xa5\": 556,\n            \"\\u0192\": 556,\n            \"\\xa7\": 556,\n            \"\\xa4\": 556,\n            \"'\": 238,\n            \"\\u201c\": 500,\n            \"\\xab\": 556,\n            \"\\u2039\": 333,\n            \"\\u203a\": 333,\n            \"\\ufb01\": 611,\n            \"\\ufb02\": 611,\n            \"\\u2013\": 556,\n            \"\\u2020\": 556,\n            \"\\u2021\": 556,\n            \"\\xb7\": 278,\n            \"\\xb6\": 556,\n            \"\\u2022\": 350,\n            \"\\u201a\": 278,\n            \"\\u201e\": 500,\n            \"\\u201d\": 500,\n            \"\\xbb\": 556,\n            \"\\u2026\": 1000,\n            \"\\u2030\": 1000,\n            \"\\xbf\": 611,\n            \"`\": 333,\n            \"\\xb4\": 333,\n            \"\\u02c6\": 333,\n            \"\\u02dc\": 333,\n            \"\\xaf\": 333,\n            \"\\u02d8\": 333,\n            \"\\u02d9\": 333,\n            \"\\xa8\": 333,\n            \"\\u02da\": 333,\n            \"\\xb8\": 333,\n            \"\\u02dd\": 333,\n            \"\\u02db\": 333,\n            \"\\u02c7\": 333,\n            \"\\u2014\": 1000,\n            \"\\xc6\": 1000,\n            \"\\xaa\": 370,\n            \"\\u0141\": 611,\n            \"\\xd8\": 778,\n            \"\\u0152\": 1000,\n            \"\\xba\": 365,\n            \"\\xe6\": 889,\n            \"\\u0131\": 278,\n            \"\\u0142\": 278,\n            \"\\xf8\": 611,\n            \"\\u0153\": 944,\n            \"\\xdf\": 611,\n            \"\\xcf\": 278,\n            \"\\xe9\": 556,\n            \"\\u0103\": 556,\n            \"\\u0171\": 611,\n            \"\\u011b\": 556,\n            \"\\u0178\": 667,\n            \"\\xf7\": 584,\n            \"\\xdd\": 667,\n            \"\\xc2\": 722,\n            \"\\xe1\": 556,\n            \"\\xdb\": 722,\n            \"\\xfd\": 556,\n            \"\\u0219\": 556,\n            \"\\xea\": 556,\n            \"\\u016e\": 722,\n            \"\\xdc\": 722,\n            \"\\u0105\": 556,\n            \"\\xda\": 722,\n            \"\\u0173\": 611,\n            \"\\xcb\": 667,\n            \"\\u0110\": 722,\n            \"\\uf6c3\": 250,\n            \"\\xa9\": 737,\n            \"\\u0112\": 667,\n            \"\\u010d\": 556,\n            \"\\xe5\": 556,\n            \"\\u0145\": 722,\n            \"\\u013a\": 278,\n            \"\\xe0\": 556,\n            \"\\u0162\": 611,\n            \"\\u0106\": 722,\n            \"\\xe3\": 556,\n            \"\\u0116\": 667,\n            \"\\u0161\": 556,\n            \"\\u015f\": 556,\n            \"\\xed\": 278,\n            \"\\u25ca\": 494,\n            \"\\u0158\": 722,\n            \"\\u0122\": 778,\n            \"\\xfb\": 611,\n            \"\\xe2\": 556,\n            \"\\u0100\": 722,\n            \"\\u0159\": 389,\n            \"\\xe7\": 556,\n            \"\\u017b\": 611,\n            \"\\xde\": 667,\n            \"\\u014c\": 778,\n            \"\\u0154\": 722,\n            \"\\u015a\": 667,\n            \"\\u010f\": 743,\n            \"\\u016a\": 722,\n            \"\\u016f\": 611,\n            \"\\xb3\": 333,\n            \"\\xd2\": 778,\n            \"\\xc0\": 722,\n            \"\\u0102\": 722,\n            \"\\xd7\": 584,\n            \"\\xfa\": 611,\n            \"\\u0164\": 611,\n            \"\\u2202\": 494,\n            \"\\xff\": 556,\n            \"\\u0143\": 722,\n            \"\\xee\": 278,\n            \"\\xca\": 667,\n            \"\\xe4\": 556,\n            \"\\xeb\": 556,\n            \"\\u0107\": 556,\n            \"\\u0144\": 611,\n            \"\\u016b\": 611,\n            \"\\u0147\": 722,\n            \"\\xcd\": 278,\n            \"\\xb1\": 584,\n            \"\\xa6\": 280,\n            \"\\xae\": 737,\n            \"\\u011e\": 778,\n            \"\\u0130\": 278,\n            \"\\u2211\": 600,\n            \"\\xc8\": 667,\n            \"\\u0155\": 389,\n            \"\\u014d\": 611,\n            \"\\u0179\": 611,\n            \"\\u017d\": 611,\n            \"\\u2265\": 549,\n            \"\\xd0\": 722,\n            \"\\xc7\": 722,\n            \"\\u013c\": 278,\n            \"\\u0165\": 389,\n            \"\\u0119\": 556,\n            \"\\u0172\": 722,\n            \"\\xc1\": 722,\n            \"\\xc4\": 722,\n            \"\\xe8\": 556,\n            \"\\u017a\": 500,\n            \"\\u012f\": 278,\n            \"\\xd3\": 778,\n            \"\\xf3\": 611,\n            \"\\u0101\": 556,\n            \"\\u015b\": 556,\n            \"\\xef\": 278,\n            \"\\xd4\": 778,\n            \"\\xd9\": 722,\n            \"\\u2206\": 612,\n            \"\\xfe\": 611,\n            \"\\xb2\": 333,\n            \"\\xd6\": 778,\n            \"\\xb5\": 611,\n            \"\\xec\": 278,\n            \"\\u0151\": 611,\n            \"\\u0118\": 667,\n            \"\\u0111\": 611,\n            \"\\xbe\": 834,\n            \"\\u015e\": 667,\n            \"\\u013e\": 400,\n            \"\\u0136\": 722,\n            \"\\u0139\": 611,\n            \"\\u2122\": 1000,\n            \"\\u0117\": 556,\n            \"\\xcc\": 278,\n            \"\\u012a\": 278,\n            \"\\u013d\": 611,\n            \"\\xbd\": 834,\n            \"\\u2264\": 549,\n            \"\\xf4\": 611,\n            \"\\xf1\": 611,\n            \"\\u0170\": 722,\n            \"\\xc9\": 667,\n            \"\\u0113\": 556,\n            \"\\u011f\": 611,\n            \"\\xbc\": 834,\n            \"\\u0160\": 667,\n            \"\\u0218\": 667,\n            \"\\u0150\": 778,\n            \"\\xb0\": 400,\n            \"\\xf2\": 611,\n            \"\\u010c\": 722,\n            \"\\xf9\": 611,\n            \"\\u221a\": 549,\n            \"\\u010e\": 722,\n            \"\\u0157\": 389,\n            \"\\xd1\": 722,\n            \"\\xf5\": 611,\n            \"\\u0156\": 722,\n            \"\\u013b\": 611,\n            \"\\xc3\": 722,\n            \"\\u0104\": 722,\n            \"\\xc5\": 722,\n            \"\\xd5\": 778,\n            \"\\u017c\": 500,\n            \"\\u011a\": 667,\n            \"\\u012e\": 278,\n            \"\\u0137\": 556,\n            \"\\u2212\": 584,\n            \"\\xce\": 278,\n            \"\\u0148\": 611,\n            \"\\u0163\": 333,\n            \"\\xac\": 584,\n            \"\\xf6\": 611,\n            \"\\xfc\": 611,\n            \"\\u2260\": 549,\n            \"\\u0123\": 611,\n            \"\\xf0\": 611,\n            \"\\u017e\": 500,\n            \"\\u0146\": 611,\n            \"\\xb9\": 333,\n            \"\\u012b\": 278,\n            \"\\u20ac\": 556,\n        },\n    ),\n    # Generated from Helvetica-BoldOblique.afm\n    # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated.  All Rights Reserved.\n    # Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries.\n    \"Helvetica-BoldOblique\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Helvetica-BoldOblique\",\n            family=\"Helvetica\",\n            weight=\"Bold\",\n            ascent=718,\n            descent=-207,\n            cap_height=718,\n            x_height=532,\n            italic_angle=-12,\n            flags=96,\n            bbox=(-174.0, -228.0, 1114.0, 962.0),\n        ),\n        character_widths={\n            \" \": 278,\n            \"default\": 556,\n            \"!\": 333,\n            '\"': 474,\n            \"#\": 556,\n            \"$\": 556,\n            \"%\": 889,\n            \"&\": 722,\n            \"\\u2019\": 278,\n            \"(\": 333,\n            \")\": 333,\n            \"*\": 389,\n            \"+\": 584,\n            \",\": 278,\n            \"-\": 333,\n            \".\": 278,\n            \"/\": 278,\n            \"0\": 556,\n            \"1\": 556,\n            \"2\": 556,\n            \"3\": 556,\n            \"4\": 556,\n            \"5\": 556,\n            \"6\": 556,\n            \"7\": 556,\n            \"8\": 556,\n            \"9\": 556,\n            \":\": 333,\n            \";\": 333,\n            \"<\": 584,\n            \"=\": 584,\n            \">\": 584,\n            \"?\": 611,\n            \"@\": 975,\n            \"A\": 722,\n            \"B\": 722,\n            \"C\": 722,\n            \"D\": 722,\n            \"E\": 667,\n            \"F\": 611,\n            \"G\": 778,\n            \"H\": 722,\n            \"I\": 278,\n            \"J\": 556,\n            \"K\": 722,\n            \"L\": 611,\n            \"M\": 833,\n            \"N\": 722,\n            \"O\": 778,\n            \"P\": 667,\n            \"Q\": 778,\n            \"R\": 722,\n            \"S\": 667,\n            \"T\": 611,\n            \"U\": 722,\n            \"V\": 667,\n            \"W\": 944,\n            \"X\": 667,\n            \"Y\": 667,\n            \"Z\": 611,\n            \"[\": 333,\n            \"\\\\\": 278,\n            \"]\": 333,\n            \"^\": 584,\n            \"_\": 556,\n            \"\\u2018\": 278,\n            \"a\": 556,\n            \"b\": 611,\n            \"c\": 556,\n            \"d\": 611,\n            \"e\": 556,\n            \"f\": 333,\n            \"g\": 611,\n            \"h\": 611,\n            \"i\": 278,\n            \"j\": 278,\n            \"k\": 556,\n            \"l\": 278,\n            \"m\": 889,\n            \"n\": 611,\n            \"o\": 611,\n            \"p\": 611,\n            \"q\": 611,\n            \"r\": 389,\n            \"s\": 556,\n            \"t\": 333,\n            \"u\": 611,\n            \"v\": 556,\n            \"w\": 778,\n            \"x\": 556,\n            \"y\": 556,\n            \"z\": 500,\n            \"{\": 389,\n            \"|\": 280,\n            \"}\": 389,\n            \"~\": 584,\n            \"\\xa1\": 333,\n            \"\\xa2\": 556,\n            \"\\xa3\": 556,\n            \"\\u2044\": 167,\n            \"\\xa5\": 556,\n            \"\\u0192\": 556,\n            \"\\xa7\": 556,\n            \"\\xa4\": 556,\n            \"'\": 238,\n            \"\\u201c\": 500,\n            \"\\xab\": 556,\n            \"\\u2039\": 333,\n            \"\\u203a\": 333,\n            \"\\ufb01\": 611,\n            \"\\ufb02\": 611,\n            \"\\u2013\": 556,\n            \"\\u2020\": 556,\n            \"\\u2021\": 556,\n            \"\\xb7\": 278,\n            \"\\xb6\": 556,\n            \"\\u2022\": 350,\n            \"\\u201a\": 278,\n            \"\\u201e\": 500,\n            \"\\u201d\": 500,\n            \"\\xbb\": 556,\n            \"\\u2026\": 1000,\n            \"\\u2030\": 1000,\n            \"\\xbf\": 611,\n            \"`\": 333,\n            \"\\xb4\": 333,\n            \"\\u02c6\": 333,\n            \"\\u02dc\": 333,\n            \"\\xaf\": 333,\n            \"\\u02d8\": 333,\n            \"\\u02d9\": 333,\n            \"\\xa8\": 333,\n            \"\\u02da\": 333,\n            \"\\xb8\": 333,\n            \"\\u02dd\": 333,\n            \"\\u02db\": 333,\n            \"\\u02c7\": 333,\n            \"\\u2014\": 1000,\n            \"\\xc6\": 1000,\n            \"\\xaa\": 370,\n            \"\\u0141\": 611,\n            \"\\xd8\": 778,\n            \"\\u0152\": 1000,\n            \"\\xba\": 365,\n            \"\\xe6\": 889,\n            \"\\u0131\": 278,\n            \"\\u0142\": 278,\n            \"\\xf8\": 611,\n            \"\\u0153\": 944,\n            \"\\xdf\": 611,\n            \"\\xcf\": 278,\n            \"\\xe9\": 556,\n            \"\\u0103\": 556,\n            \"\\u0171\": 611,\n            \"\\u011b\": 556,\n            \"\\u0178\": 667,\n            \"\\xf7\": 584,\n            \"\\xdd\": 667,\n            \"\\xc2\": 722,\n            \"\\xe1\": 556,\n            \"\\xdb\": 722,\n            \"\\xfd\": 556,\n            \"\\u0219\": 556,\n            \"\\xea\": 556,\n            \"\\u016e\": 722,\n            \"\\xdc\": 722,\n            \"\\u0105\": 556,\n            \"\\xda\": 722,\n            \"\\u0173\": 611,\n            \"\\xcb\": 667,\n            \"\\u0110\": 722,\n            \"\\uf6c3\": 250,\n            \"\\xa9\": 737,\n            \"\\u0112\": 667,\n            \"\\u010d\": 556,\n            \"\\xe5\": 556,\n            \"\\u0145\": 722,\n            \"\\u013a\": 278,\n            \"\\xe0\": 556,\n            \"\\u0162\": 611,\n            \"\\u0106\": 722,\n            \"\\xe3\": 556,\n            \"\\u0116\": 667,\n            \"\\u0161\": 556,\n            \"\\u015f\": 556,\n            \"\\xed\": 278,\n            \"\\u25ca\": 494,\n            \"\\u0158\": 722,\n            \"\\u0122\": 778,\n            \"\\xfb\": 611,\n            \"\\xe2\": 556,\n            \"\\u0100\": 722,\n            \"\\u0159\": 389,\n            \"\\xe7\": 556,\n            \"\\u017b\": 611,\n            \"\\xde\": 667,\n            \"\\u014c\": 778,\n            \"\\u0154\": 722,\n            \"\\u015a\": 667,\n            \"\\u010f\": 743,\n            \"\\u016a\": 722,\n            \"\\u016f\": 611,\n            \"\\xb3\": 333,\n            \"\\xd2\": 778,\n            \"\\xc0\": 722,\n            \"\\u0102\": 722,\n            \"\\xd7\": 584,\n            \"\\xfa\": 611,\n            \"\\u0164\": 611,\n            \"\\u2202\": 494,\n            \"\\xff\": 556,\n            \"\\u0143\": 722,\n            \"\\xee\": 278,\n            \"\\xca\": 667,\n            \"\\xe4\": 556,\n            \"\\xeb\": 556,\n            \"\\u0107\": 556,\n            \"\\u0144\": 611,\n            \"\\u016b\": 611,\n            \"\\u0147\": 722,\n            \"\\xcd\": 278,\n            \"\\xb1\": 584,\n            \"\\xa6\": 280,\n            \"\\xae\": 737,\n            \"\\u011e\": 778,\n            \"\\u0130\": 278,\n            \"\\u2211\": 600,\n            \"\\xc8\": 667,\n            \"\\u0155\": 389,\n            \"\\u014d\": 611,\n            \"\\u0179\": 611,\n            \"\\u017d\": 611,\n            \"\\u2265\": 549,\n            \"\\xd0\": 722,\n            \"\\xc7\": 722,\n            \"\\u013c\": 278,\n            \"\\u0165\": 389,\n            \"\\u0119\": 556,\n            \"\\u0172\": 722,\n            \"\\xc1\": 722,\n            \"\\xc4\": 722,\n            \"\\xe8\": 556,\n            \"\\u017a\": 500,\n            \"\\u012f\": 278,\n            \"\\xd3\": 778,\n            \"\\xf3\": 611,\n            \"\\u0101\": 556,\n            \"\\u015b\": 556,\n            \"\\xef\": 278,\n            \"\\xd4\": 778,\n            \"\\xd9\": 722,\n            \"\\u2206\": 612,\n            \"\\xfe\": 611,\n            \"\\xb2\": 333,\n            \"\\xd6\": 778,\n            \"\\xb5\": 611,\n            \"\\xec\": 278,\n            \"\\u0151\": 611,\n            \"\\u0118\": 667,\n            \"\\u0111\": 611,\n            \"\\xbe\": 834,\n            \"\\u015e\": 667,\n            \"\\u013e\": 400,\n            \"\\u0136\": 722,\n            \"\\u0139\": 611,\n            \"\\u2122\": 1000,\n            \"\\u0117\": 556,\n            \"\\xcc\": 278,\n            \"\\u012a\": 278,\n            \"\\u013d\": 611,\n            \"\\xbd\": 834,\n            \"\\u2264\": 549,\n            \"\\xf4\": 611,\n            \"\\xf1\": 611,\n            \"\\u0170\": 722,\n            \"\\xc9\": 667,\n            \"\\u0113\": 556,\n            \"\\u011f\": 611,\n            \"\\xbc\": 834,\n            \"\\u0160\": 667,\n            \"\\u0218\": 667,\n            \"\\u0150\": 778,\n            \"\\xb0\": 400,\n            \"\\xf2\": 611,\n            \"\\u010c\": 722,\n            \"\\xf9\": 611,\n            \"\\u221a\": 549,\n            \"\\u010e\": 722,\n            \"\\u0157\": 389,\n            \"\\xd1\": 722,\n            \"\\xf5\": 611,\n            \"\\u0156\": 722,\n            \"\\u013b\": 611,\n            \"\\xc3\": 722,\n            \"\\u0104\": 722,\n            \"\\xc5\": 722,\n            \"\\xd5\": 778,\n            \"\\u017c\": 500,\n            \"\\u011a\": 667,\n            \"\\u012e\": 278,\n            \"\\u0137\": 556,\n            \"\\u2212\": 584,\n            \"\\xce\": 278,\n            \"\\u0148\": 611,\n            \"\\u0163\": 333,\n            \"\\xac\": 584,\n            \"\\xf6\": 611,\n            \"\\xfc\": 611,\n            \"\\u2260\": 549,\n            \"\\u0123\": 611,\n            \"\\xf0\": 611,\n            \"\\u017e\": 500,\n            \"\\u0146\": 611,\n            \"\\xb9\": 333,\n            \"\\u012b\": 278,\n            \"\\u20ac\": 556,\n        },\n    ),\n    # Generated from Helvetica-Oblique.afm\n    # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated.  All Rights Reserved.\n    # Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries.\n    \"Helvetica-Oblique\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Helvetica-Oblique\",\n            family=\"Helvetica\",\n            weight=\"Medium\",\n            ascent=718,\n            descent=-207,\n            cap_height=718,\n            x_height=523,\n            italic_angle=-12,\n            flags=96,\n            bbox=(-170.0, -225.0, 1116.0, 931.0),\n        ),\n        character_widths={\n            \" \": 278,\n            \"default\": 556,\n            \"!\": 278,\n            '\"': 355,\n            \"#\": 556,\n            \"$\": 556,\n            \"%\": 889,\n            \"&\": 667,\n            \"\\u2019\": 222,\n            \"(\": 333,\n            \")\": 333,\n            \"*\": 389,\n            \"+\": 584,\n            \",\": 278,\n            \"-\": 333,\n            \".\": 278,\n            \"/\": 278,\n            \"0\": 556,\n            \"1\": 556,\n            \"2\": 556,\n            \"3\": 556,\n            \"4\": 556,\n            \"5\": 556,\n            \"6\": 556,\n            \"7\": 556,\n            \"8\": 556,\n            \"9\": 556,\n            \":\": 278,\n            \";\": 278,\n            \"<\": 584,\n            \"=\": 584,\n            \">\": 584,\n            \"?\": 556,\n            \"@\": 1015,\n            \"A\": 667,\n            \"B\": 667,\n            \"C\": 722,\n            \"D\": 722,\n            \"E\": 667,\n            \"F\": 611,\n            \"G\": 778,\n            \"H\": 722,\n            \"I\": 278,\n            \"J\": 500,\n            \"K\": 667,\n            \"L\": 556,\n            \"M\": 833,\n            \"N\": 722,\n            \"O\": 778,\n            \"P\": 667,\n            \"Q\": 778,\n            \"R\": 722,\n            \"S\": 667,\n            \"T\": 611,\n            \"U\": 722,\n            \"V\": 667,\n            \"W\": 944,\n            \"X\": 667,\n            \"Y\": 667,\n            \"Z\": 611,\n            \"[\": 278,\n            \"\\\\\": 278,\n            \"]\": 278,\n            \"^\": 469,\n            \"_\": 556,\n            \"\\u2018\": 222,\n            \"a\": 556,\n            \"b\": 556,\n            \"c\": 500,\n            \"d\": 556,\n            \"e\": 556,\n            \"f\": 278,\n            \"g\": 556,\n            \"h\": 556,\n            \"i\": 222,\n            \"j\": 222,\n            \"k\": 500,\n            \"l\": 222,\n            \"m\": 833,\n            \"n\": 556,\n            \"o\": 556,\n            \"p\": 556,\n            \"q\": 556,\n            \"r\": 333,\n            \"s\": 500,\n            \"t\": 278,\n            \"u\": 556,\n            \"v\": 500,\n            \"w\": 722,\n            \"x\": 500,\n            \"y\": 500,\n            \"z\": 500,\n            \"{\": 334,\n            \"|\": 260,\n            \"}\": 334,\n            \"~\": 584,\n            \"\\xa1\": 333,\n            \"\\xa2\": 556,\n            \"\\xa3\": 556,\n            \"\\u2044\": 167,\n            \"\\xa5\": 556,\n            \"\\u0192\": 556,\n            \"\\xa7\": 556,\n            \"\\xa4\": 556,\n            \"'\": 191,\n            \"\\u201c\": 333,\n            \"\\xab\": 556,\n            \"\\u2039\": 333,\n            \"\\u203a\": 333,\n            \"\\ufb01\": 500,\n            \"\\ufb02\": 500,\n            \"\\u2013\": 556,\n            \"\\u2020\": 556,\n            \"\\u2021\": 556,\n            \"\\xb7\": 278,\n            \"\\xb6\": 537,\n            \"\\u2022\": 350,\n            \"\\u201a\": 222,\n            \"\\u201e\": 333,\n            \"\\u201d\": 333,\n            \"\\xbb\": 556,\n            \"\\u2026\": 1000,\n            \"\\u2030\": 1000,\n            \"\\xbf\": 611,\n            \"`\": 333,\n            \"\\xb4\": 333,\n            \"\\u02c6\": 333,\n            \"\\u02dc\": 333,\n            \"\\xaf\": 333,\n            \"\\u02d8\": 333,\n            \"\\u02d9\": 333,\n            \"\\xa8\": 333,\n            \"\\u02da\": 333,\n            \"\\xb8\": 333,\n            \"\\u02dd\": 333,\n            \"\\u02db\": 333,\n            \"\\u02c7\": 333,\n            \"\\u2014\": 1000,\n            \"\\xc6\": 1000,\n            \"\\xaa\": 370,\n            \"\\u0141\": 556,\n            \"\\xd8\": 778,\n            \"\\u0152\": 1000,\n            \"\\xba\": 365,\n            \"\\xe6\": 889,\n            \"\\u0131\": 278,\n            \"\\u0142\": 222,\n            \"\\xf8\": 611,\n            \"\\u0153\": 944,\n            \"\\xdf\": 611,\n            \"\\xcf\": 278,\n            \"\\xe9\": 556,\n            \"\\u0103\": 556,\n            \"\\u0171\": 556,\n            \"\\u011b\": 556,\n            \"\\u0178\": 667,\n            \"\\xf7\": 584,\n            \"\\xdd\": 667,\n            \"\\xc2\": 667,\n            \"\\xe1\": 556,\n            \"\\xdb\": 722,\n            \"\\xfd\": 500,\n            \"\\u0219\": 500,\n            \"\\xea\": 556,\n            \"\\u016e\": 722,\n            \"\\xdc\": 722,\n            \"\\u0105\": 556,\n            \"\\xda\": 722,\n            \"\\u0173\": 556,\n            \"\\xcb\": 667,\n            \"\\u0110\": 722,\n            \"\\uf6c3\": 250,\n            \"\\xa9\": 737,\n            \"\\u0112\": 667,\n            \"\\u010d\": 500,\n            \"\\xe5\": 556,\n            \"\\u0145\": 722,\n            \"\\u013a\": 222,\n            \"\\xe0\": 556,\n            \"\\u0162\": 611,\n            \"\\u0106\": 722,\n            \"\\xe3\": 556,\n            \"\\u0116\": 667,\n            \"\\u0161\": 500,\n            \"\\u015f\": 500,\n            \"\\xed\": 278,\n            \"\\u25ca\": 471,\n            \"\\u0158\": 722,\n            \"\\u0122\": 778,\n            \"\\xfb\": 556,\n            \"\\xe2\": 556,\n            \"\\u0100\": 667,\n            \"\\u0159\": 333,\n            \"\\xe7\": 500,\n            \"\\u017b\": 611,\n            \"\\xde\": 667,\n            \"\\u014c\": 778,\n            \"\\u0154\": 722,\n            \"\\u015a\": 667,\n            \"\\u010f\": 643,\n            \"\\u016a\": 722,\n            \"\\u016f\": 556,\n            \"\\xb3\": 333,\n            \"\\xd2\": 778,\n            \"\\xc0\": 667,\n            \"\\u0102\": 667,\n            \"\\xd7\": 584,\n            \"\\xfa\": 556,\n            \"\\u0164\": 611,\n            \"\\u2202\": 476,\n            \"\\xff\": 500,\n            \"\\u0143\": 722,\n            \"\\xee\": 278,\n            \"\\xca\": 667,\n            \"\\xe4\": 556,\n            \"\\xeb\": 556,\n            \"\\u0107\": 500,\n            \"\\u0144\": 556,\n            \"\\u016b\": 556,\n            \"\\u0147\": 722,\n            \"\\xcd\": 278,\n            \"\\xb1\": 584,\n            \"\\xa6\": 260,\n            \"\\xae\": 737,\n            \"\\u011e\": 778,\n            \"\\u0130\": 278,\n            \"\\u2211\": 600,\n            \"\\xc8\": 667,\n            \"\\u0155\": 333,\n            \"\\u014d\": 556,\n            \"\\u0179\": 611,\n            \"\\u017d\": 611,\n            \"\\u2265\": 549,\n            \"\\xd0\": 722,\n            \"\\xc7\": 722,\n            \"\\u013c\": 222,\n            \"\\u0165\": 317,\n            \"\\u0119\": 556,\n            \"\\u0172\": 722,\n            \"\\xc1\": 667,\n            \"\\xc4\": 667,\n            \"\\xe8\": 556,\n            \"\\u017a\": 500,\n            \"\\u012f\": 222,\n            \"\\xd3\": 778,\n            \"\\xf3\": 556,\n            \"\\u0101\": 556,\n            \"\\u015b\": 500,\n            \"\\xef\": 278,\n            \"\\xd4\": 778,\n            \"\\xd9\": 722,\n            \"\\u2206\": 612,\n            \"\\xfe\": 556,\n            \"\\xb2\": 333,\n            \"\\xd6\": 778,\n            \"\\xb5\": 556,\n            \"\\xec\": 278,\n            \"\\u0151\": 556,\n            \"\\u0118\": 667,\n            \"\\u0111\": 556,\n            \"\\xbe\": 834,\n            \"\\u015e\": 667,\n            \"\\u013e\": 299,\n            \"\\u0136\": 667,\n            \"\\u0139\": 556,\n            \"\\u2122\": 1000,\n            \"\\u0117\": 556,\n            \"\\xcc\": 278,\n            \"\\u012a\": 278,\n            \"\\u013d\": 556,\n            \"\\xbd\": 834,\n            \"\\u2264\": 549,\n            \"\\xf4\": 556,\n            \"\\xf1\": 556,\n            \"\\u0170\": 722,\n            \"\\xc9\": 667,\n            \"\\u0113\": 556,\n            \"\\u011f\": 556,\n            \"\\xbc\": 834,\n            \"\\u0160\": 667,\n            \"\\u0218\": 667,\n            \"\\u0150\": 778,\n            \"\\xb0\": 400,\n            \"\\xf2\": 556,\n            \"\\u010c\": 722,\n            \"\\xf9\": 556,\n            \"\\u221a\": 453,\n            \"\\u010e\": 722,\n            \"\\u0157\": 333,\n            \"\\xd1\": 722,\n            \"\\xf5\": 556,\n            \"\\u0156\": 722,\n            \"\\u013b\": 556,\n            \"\\xc3\": 667,\n            \"\\u0104\": 667,\n            \"\\xc5\": 667,\n            \"\\xd5\": 778,\n            \"\\u017c\": 500,\n            \"\\u011a\": 667,\n            \"\\u012e\": 278,\n            \"\\u0137\": 500,\n            \"\\u2212\": 584,\n            \"\\xce\": 278,\n            \"\\u0148\": 556,\n            \"\\u0163\": 278,\n            \"\\xac\": 584,\n            \"\\xf6\": 556,\n            \"\\xfc\": 556,\n            \"\\u2260\": 549,\n            \"\\u0123\": 556,\n            \"\\xf0\": 556,\n            \"\\u017e\": 500,\n            \"\\u0146\": 556,\n            \"\\xb9\": 333,\n            \"\\u012b\": 278,\n            \"\\u20ac\": 556,\n        },\n    ),\n    # Generated from Symbol.afm\n    # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All rights reserved.\n    \"Symbol\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Symbol\",\n            family=\"Symbol\",\n            weight=\"Medium\",\n            ascent=0.0,\n            descent=0.0,\n            cap_height=0.0,\n            x_height=0.0,\n            italic_angle=0,\n            flags=4,\n            bbox=(-180.0, -293.0, 1090.0, 1010.0),\n        ),\n        character_widths={\n            \" \": 250,\n            \"default\": 500,\n            \"!\": 333,\n            \"\\u2200\": 713,\n            \"#\": 500,\n            \"\\u2203\": 549,\n            \"%\": 833,\n            \"&\": 778,\n            \"\\u220b\": 439,\n            \"(\": 333,\n            \")\": 333,\n            \"\\u2217\": 500,\n            \"+\": 549,\n            \",\": 250,\n            \"\\u2212\": 549,\n            \".\": 250,\n            \"/\": 278,\n            \"0\": 500,\n            \"1\": 500,\n            \"2\": 500,\n            \"3\": 500,\n            \"4\": 500,\n            \"5\": 500,\n            \"6\": 500,\n            \"7\": 500,\n            \"8\": 500,\n            \"9\": 500,\n            \":\": 278,\n            \";\": 278,\n            \"<\": 549,\n            \"=\": 549,\n            \">\": 549,\n            \"?\": 444,\n            \"\\u2245\": 549,\n            \"\\u0391\": 722,\n            \"\\u0392\": 667,\n            \"\\u03a7\": 722,\n            \"\\u2206\": 612,\n            \"\\u0395\": 611,\n            \"\\u03a6\": 763,\n            \"\\u0393\": 603,\n            \"\\u0397\": 722,\n            \"\\u0399\": 333,\n            \"\\u03d1\": 631,\n            \"\\u039a\": 722,\n            \"\\u039b\": 686,\n            \"\\u039c\": 889,\n            \"\\u039d\": 722,\n            \"\\u039f\": 722,\n            \"\\u03a0\": 768,\n            \"\\u0398\": 741,\n            \"\\u03a1\": 556,\n            \"\\u03a3\": 592,\n            \"\\u03a4\": 611,\n            \"\\u03a5\": 690,\n            \"\\u03c2\": 439,\n            \"\\u2126\": 768,\n            \"\\u039e\": 645,\n            \"\\u03a8\": 795,\n            \"\\u0396\": 611,\n            \"[\": 333,\n            \"\\u2234\": 863,\n            \"]\": 333,\n            \"\\u22a5\": 658,\n            \"_\": 500,\n            \"\\uf8e5\": 500,\n            \"\\u03b1\": 631,\n            \"\\u03b2\": 549,\n            \"\\u03c7\": 549,\n            \"\\u03b4\": 494,\n            \"\\u03b5\": 439,\n            \"\\u03c6\": 521,\n            \"\\u03b3\": 411,\n            \"\\u03b7\": 603,\n            \"\\u03b9\": 329,\n            \"\\u03d5\": 603,\n            \"\\u03ba\": 549,\n            \"\\u03bb\": 549,\n            \"\\xb5\": 576,\n            \"\\u03bd\": 521,\n            \"\\u03bf\": 549,\n            \"\\u03c0\": 549,\n            \"\\u03b8\": 521,\n            \"\\u03c1\": 549,\n            \"\\u03c3\": 603,\n            \"\\u03c4\": 439,\n            \"\\u03c5\": 576,\n            \"\\u03d6\": 713,\n            \"\\u03c9\": 686,\n            \"\\u03be\": 493,\n            \"\\u03c8\": 686,\n            \"\\u03b6\": 494,\n            \"{\": 480,\n            \"|\": 200,\n            \"}\": 480,\n            \"\\u223c\": 549,\n            \"\\u20ac\": 750,\n            \"\\u03d2\": 620,\n            \"\\u2032\": 247,\n            \"\\u2264\": 549,\n            \"\\u2044\": 167,\n            \"\\u221e\": 713,\n            \"\\u0192\": 500,\n            \"\\u2663\": 753,\n            \"\\u2666\": 753,\n            \"\\u2665\": 753,\n            \"\\u2660\": 753,\n            \"\\u2194\": 1042,\n            \"\\u2190\": 987,\n            \"\\u2191\": 603,\n            \"\\u2192\": 987,\n            \"\\u2193\": 603,\n            \"\\xb0\": 400,\n            \"\\xb1\": 549,\n            \"\\u2033\": 411,\n            \"\\u2265\": 549,\n            \"\\xd7\": 549,\n            \"\\u221d\": 713,\n            \"\\u2202\": 494,\n            \"\\u2022\": 460,\n            \"\\xf7\": 549,\n            \"\\u2260\": 549,\n            \"\\u2261\": 549,\n            \"\\u2248\": 549,\n            \"\\u2026\": 1000,\n            \"\\uf8e6\": 603,\n            \"\\uf8e7\": 1000,\n            \"\\u21b5\": 658,\n            \"\\u2135\": 823,\n            \"\\u2111\": 686,\n            \"\\u211c\": 795,\n            \"\\u2118\": 987,\n            \"\\u2297\": 768,\n            \"\\u2295\": 768,\n            \"\\u2205\": 823,\n            \"\\u2229\": 768,\n            \"\\u222a\": 768,\n            \"\\u2283\": 713,\n            \"\\u2287\": 713,\n            \"\\u2284\": 713,\n            \"\\u2282\": 713,\n            \"\\u2286\": 713,\n            \"\\u2208\": 713,\n            \"\\u2209\": 713,\n            \"\\u2220\": 768,\n            \"\\u2207\": 713,\n            \"\\uf6da\": 790,\n            \"\\uf6d9\": 790,\n            \"\\uf6db\": 890,\n            \"\\u220f\": 823,\n            \"\\u221a\": 549,\n            \"\\u22c5\": 250,\n            \"\\xac\": 713,\n            \"\\u2227\": 603,\n            \"\\u2228\": 603,\n            \"\\u21d4\": 1042,\n            \"\\u21d0\": 987,\n            \"\\u21d1\": 603,\n            \"\\u21d2\": 987,\n            \"\\u21d3\": 603,\n            \"\\u25ca\": 494,\n            \"\\u2329\": 329,\n            \"\\uf8e8\": 790,\n            \"\\uf8e9\": 790,\n            \"\\uf8ea\": 786,\n            \"\\u2211\": 713,\n            \"\\uf8eb\": 384,\n            \"\\uf8ec\": 384,\n            \"\\uf8ed\": 384,\n            \"\\uf8ee\": 384,\n            \"\\uf8ef\": 384,\n            \"\\uf8f0\": 384,\n            \"\\uf8f1\": 494,\n            \"\\uf8f2\": 494,\n            \"\\uf8f3\": 494,\n            \"\\uf8f4\": 494,\n            \"\\u232a\": 329,\n            \"\\u222b\": 274,\n            \"\\u2320\": 686,\n            \"\\uf8f5\": 686,\n            \"\\u2321\": 686,\n            \"\\uf8f6\": 384,\n            \"\\uf8f7\": 384,\n            \"\\uf8f8\": 384,\n            \"\\uf8f9\": 384,\n            \"\\uf8fa\": 384,\n            \"\\uf8fb\": 384,\n            \"\\uf8fc\": 494,\n            \"\\uf8fd\": 494,\n            \"\\uf8fe\": 494,\n            \"\\uf8ff\": 790,\n        },\n    ),\n    # Generated from Times-Bold.afm\n    # Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated.  All Rights\n    # Reserved.  Times is a trademark of Linotype-Hell AG and/or its subsidiaries.\n    \"Times-Bold\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Times-Bold\",\n            family=\"Times\",\n            weight=\"Bold\",\n            ascent=683,\n            descent=-217,\n            cap_height=676,\n            x_height=461,\n            italic_angle=0,\n            flags=34,\n            bbox=(-168.0, -218.0, 1000.0, 935.0),\n        ),\n        character_widths={\n            \" \": 250,\n            \"default\": 500,\n            \"!\": 333,\n            '\"': 555,\n            \"#\": 500,\n            \"$\": 500,\n            \"%\": 1000,\n            \"&\": 833,\n            \"\\u2019\": 333,\n            \"(\": 333,\n            \")\": 333,\n            \"*\": 500,\n            \"+\": 570,\n            \",\": 250,\n            \"-\": 333,\n            \".\": 250,\n            \"/\": 278,\n            \"0\": 500,\n            \"1\": 500,\n            \"2\": 500,\n            \"3\": 500,\n            \"4\": 500,\n            \"5\": 500,\n            \"6\": 500,\n            \"7\": 500,\n            \"8\": 500,\n            \"9\": 500,\n            \":\": 333,\n            \";\": 333,\n            \"<\": 570,\n            \"=\": 570,\n            \">\": 570,\n            \"?\": 500,\n            \"@\": 930,\n            \"A\": 722,\n            \"B\": 667,\n            \"C\": 722,\n            \"D\": 722,\n            \"E\": 667,\n            \"F\": 611,\n            \"G\": 778,\n            \"H\": 778,\n            \"I\": 389,\n            \"J\": 500,\n            \"K\": 778,\n            \"L\": 667,\n            \"M\": 944,\n            \"N\": 722,\n            \"O\": 778,\n            \"P\": 611,\n            \"Q\": 778,\n            \"R\": 722,\n            \"S\": 556,\n            \"T\": 667,\n            \"U\": 722,\n            \"V\": 722,\n            \"W\": 1000,\n            \"X\": 722,\n            \"Y\": 722,\n            \"Z\": 667,\n            \"[\": 333,\n            \"\\\\\": 278,\n            \"]\": 333,\n            \"^\": 581,\n            \"_\": 500,\n            \"\\u2018\": 333,\n            \"a\": 500,\n            \"b\": 556,\n            \"c\": 444,\n            \"d\": 556,\n            \"e\": 444,\n            \"f\": 333,\n            \"g\": 500,\n            \"h\": 556,\n            \"i\": 278,\n            \"j\": 333,\n            \"k\": 556,\n            \"l\": 278,\n            \"m\": 833,\n            \"n\": 556,\n            \"o\": 500,\n            \"p\": 556,\n            \"q\": 556,\n            \"r\": 444,\n            \"s\": 389,\n            \"t\": 333,\n            \"u\": 556,\n            \"v\": 500,\n            \"w\": 722,\n            \"x\": 500,\n            \"y\": 500,\n            \"z\": 444,\n            \"{\": 394,\n            \"|\": 220,\n            \"}\": 394,\n            \"~\": 520,\n            \"\\xa1\": 333,\n            \"\\xa2\": 500,\n            \"\\xa3\": 500,\n            \"\\u2044\": 167,\n            \"\\xa5\": 500,\n            \"\\u0192\": 500,\n            \"\\xa7\": 500,\n            \"\\xa4\": 500,\n            \"'\": 278,\n            \"\\u201c\": 500,\n            \"\\xab\": 500,\n            \"\\u2039\": 333,\n            \"\\u203a\": 333,\n            \"\\ufb01\": 556,\n            \"\\ufb02\": 556,\n            \"\\u2013\": 500,\n            \"\\u2020\": 500,\n            \"\\u2021\": 500,\n            \"\\xb7\": 250,\n            \"\\xb6\": 540,\n            \"\\u2022\": 350,\n            \"\\u201a\": 333,\n            \"\\u201e\": 500,\n            \"\\u201d\": 500,\n            \"\\xbb\": 500,\n            \"\\u2026\": 1000,\n            \"\\u2030\": 1000,\n            \"\\xbf\": 500,\n            \"`\": 333,\n            \"\\xb4\": 333,\n            \"\\u02c6\": 333,\n            \"\\u02dc\": 333,\n            \"\\xaf\": 333,\n            \"\\u02d8\": 333,\n            \"\\u02d9\": 333,\n            \"\\xa8\": 333,\n            \"\\u02da\": 333,\n            \"\\xb8\": 333,\n            \"\\u02dd\": 333,\n            \"\\u02db\": 333,\n            \"\\u02c7\": 333,\n            \"\\u2014\": 1000,\n            \"\\xc6\": 1000,\n            \"\\xaa\": 300,\n            \"\\u0141\": 667,\n            \"\\xd8\": 778,\n            \"\\u0152\": 1000,\n            \"\\xba\": 330,\n            \"\\xe6\": 722,\n            \"\\u0131\": 278,\n            \"\\u0142\": 278,\n            \"\\xf8\": 500,\n            \"\\u0153\": 722,\n            \"\\xdf\": 556,\n            \"\\xcf\": 389,\n            \"\\xe9\": 444,\n            \"\\u0103\": 500,\n            \"\\u0171\": 556,\n            \"\\u011b\": 444,\n            \"\\u0178\": 722,\n            \"\\xf7\": 570,\n            \"\\xdd\": 722,\n            \"\\xc2\": 722,\n            \"\\xe1\": 500,\n            \"\\xdb\": 722,\n            \"\\xfd\": 500,\n            \"\\u0219\": 389,\n            \"\\xea\": 444,\n            \"\\u016e\": 722,\n            \"\\xdc\": 722,\n            \"\\u0105\": 500,\n            \"\\xda\": 722,\n            \"\\u0173\": 556,\n            \"\\xcb\": 667,\n            \"\\u0110\": 722,\n            \"\\uf6c3\": 250,\n            \"\\xa9\": 747,\n            \"\\u0112\": 667,\n            \"\\u010d\": 444,\n            \"\\xe5\": 500,\n            \"\\u0145\": 722,\n            \"\\u013a\": 278,\n            \"\\xe0\": 500,\n            \"\\u0162\": 667,\n            \"\\u0106\": 722,\n            \"\\xe3\": 500,\n            \"\\u0116\": 667,\n            \"\\u0161\": 389,\n            \"\\u015f\": 389,\n            \"\\xed\": 278,\n            \"\\u25ca\": 494,\n            \"\\u0158\": 722,\n            \"\\u0122\": 778,\n            \"\\xfb\": 556,\n            \"\\xe2\": 500,\n            \"\\u0100\": 722,\n            \"\\u0159\": 444,\n            \"\\xe7\": 444,\n            \"\\u017b\": 667,\n            \"\\xde\": 611,\n            \"\\u014c\": 778,\n            \"\\u0154\": 722,\n            \"\\u015a\": 556,\n            \"\\u010f\": 672,\n            \"\\u016a\": 722,\n            \"\\u016f\": 556,\n            \"\\xb3\": 300,\n            \"\\xd2\": 778,\n            \"\\xc0\": 722,\n            \"\\u0102\": 722,\n            \"\\xd7\": 570,\n            \"\\xfa\": 556,\n            \"\\u0164\": 667,\n            \"\\u2202\": 494,\n            \"\\xff\": 500,\n            \"\\u0143\": 722,\n            \"\\xee\": 278,\n            \"\\xca\": 667,\n            \"\\xe4\": 500,\n            \"\\xeb\": 444,\n            \"\\u0107\": 444,\n            \"\\u0144\": 556,\n            \"\\u016b\": 556,\n            \"\\u0147\": 722,\n            \"\\xcd\": 389,\n            \"\\xb1\": 570,\n            \"\\xa6\": 220,\n            \"\\xae\": 747,\n            \"\\u011e\": 778,\n            \"\\u0130\": 389,\n            \"\\u2211\": 600,\n            \"\\xc8\": 667,\n            \"\\u0155\": 444,\n            \"\\u014d\": 500,\n            \"\\u0179\": 667,\n            \"\\u017d\": 667,\n            \"\\u2265\": 549,\n            \"\\xd0\": 722,\n            \"\\xc7\": 722,\n            \"\\u013c\": 278,\n            \"\\u0165\": 416,\n            \"\\u0119\": 444,\n            \"\\u0172\": 722,\n            \"\\xc1\": 722,\n            \"\\xc4\": 722,\n            \"\\xe8\": 444,\n            \"\\u017a\": 444,\n            \"\\u012f\": 278,\n            \"\\xd3\": 778,\n            \"\\xf3\": 500,\n            \"\\u0101\": 500,\n            \"\\u015b\": 389,\n            \"\\xef\": 278,\n            \"\\xd4\": 778,\n            \"\\xd9\": 722,\n            \"\\u2206\": 612,\n            \"\\xfe\": 556,\n            \"\\xb2\": 300,\n            \"\\xd6\": 778,\n            \"\\xb5\": 556,\n            \"\\xec\": 278,\n            \"\\u0151\": 500,\n            \"\\u0118\": 667,\n            \"\\u0111\": 556,\n            \"\\xbe\": 750,\n            \"\\u015e\": 556,\n            \"\\u013e\": 394,\n            \"\\u0136\": 778,\n            \"\\u0139\": 667,\n            \"\\u2122\": 1000,\n            \"\\u0117\": 444,\n            \"\\xcc\": 389,\n            \"\\u012a\": 389,\n            \"\\u013d\": 667,\n            \"\\xbd\": 750,\n            \"\\u2264\": 549,\n            \"\\xf4\": 500,\n            \"\\xf1\": 556,\n            \"\\u0170\": 722,\n            \"\\xc9\": 667,\n            \"\\u0113\": 444,\n            \"\\u011f\": 500,\n            \"\\xbc\": 750,\n            \"\\u0160\": 556,\n            \"\\u0218\": 556,\n            \"\\u0150\": 778,\n            \"\\xb0\": 400,\n            \"\\xf2\": 500,\n            \"\\u010c\": 722,\n            \"\\xf9\": 556,\n            \"\\u221a\": 549,\n            \"\\u010e\": 722,\n            \"\\u0157\": 444,\n            \"\\xd1\": 722,\n            \"\\xf5\": 500,\n            \"\\u0156\": 722,\n            \"\\u013b\": 667,\n            \"\\xc3\": 722,\n            \"\\u0104\": 722,\n            \"\\xc5\": 722,\n            \"\\xd5\": 778,\n            \"\\u017c\": 444,\n            \"\\u011a\": 667,\n            \"\\u012e\": 389,\n            \"\\u0137\": 556,\n            \"\\u2212\": 570,\n            \"\\xce\": 389,\n            \"\\u0148\": 556,\n            \"\\u0163\": 333,\n            \"\\xac\": 570,\n            \"\\xf6\": 500,\n            \"\\xfc\": 556,\n            \"\\u2260\": 549,\n            \"\\u0123\": 500,\n            \"\\xf0\": 500,\n            \"\\u017e\": 444,\n            \"\\u0146\": 556,\n            \"\\xb9\": 300,\n            \"\\u012b\": 278,\n            \"\\u20ac\": 500,\n        },\n    ),\n    # Generated from Times-BoldItalic.afm\n    # Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated.  All Rights\n    # Reserved.  Times is a trademark of Linotype-Hell AG and/or its subsidiaries.\n    \"Times-BoldItalic\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Times-BoldItalic\",\n            family=\"Times\",\n            weight=\"Bold\",\n            ascent=683,\n            descent=-217,\n            cap_height=669,\n            x_height=462,\n            italic_angle=-15,\n            flags=98,\n            bbox=(-200.0, -218.0, 996.0, 921.0),\n        ),\n        character_widths={\n            \" \": 250,\n            \"default\": 500,\n            \"!\": 389,\n            '\"': 555,\n            \"#\": 500,\n            \"$\": 500,\n            \"%\": 833,\n            \"&\": 778,\n            \"\\u2019\": 333,\n            \"(\": 333,\n            \")\": 333,\n            \"*\": 500,\n            \"+\": 570,\n            \",\": 250,\n            \"-\": 333,\n            \".\": 250,\n            \"/\": 278,\n            \"0\": 500,\n            \"1\": 500,\n            \"2\": 500,\n            \"3\": 500,\n            \"4\": 500,\n            \"5\": 500,\n            \"6\": 500,\n            \"7\": 500,\n            \"8\": 500,\n            \"9\": 500,\n            \":\": 333,\n            \";\": 333,\n            \"<\": 570,\n            \"=\": 570,\n            \">\": 570,\n            \"?\": 500,\n            \"@\": 832,\n            \"A\": 667,\n            \"B\": 667,\n            \"C\": 667,\n            \"D\": 722,\n            \"E\": 667,\n            \"F\": 667,\n            \"G\": 722,\n            \"H\": 778,\n            \"I\": 389,\n            \"J\": 500,\n            \"K\": 667,\n            \"L\": 611,\n            \"M\": 889,\n            \"N\": 722,\n            \"O\": 722,\n            \"P\": 611,\n            \"Q\": 722,\n            \"R\": 667,\n            \"S\": 556,\n            \"T\": 611,\n            \"U\": 722,\n            \"V\": 667,\n            \"W\": 889,\n            \"X\": 667,\n            \"Y\": 611,\n            \"Z\": 611,\n            \"[\": 333,\n            \"\\\\\": 278,\n            \"]\": 333,\n            \"^\": 570,\n            \"_\": 500,\n            \"\\u2018\": 333,\n            \"a\": 500,\n            \"b\": 500,\n            \"c\": 444,\n            \"d\": 500,\n            \"e\": 444,\n            \"f\": 333,\n            \"g\": 500,\n            \"h\": 556,\n            \"i\": 278,\n            \"j\": 278,\n            \"k\": 500,\n            \"l\": 278,\n            \"m\": 778,\n            \"n\": 556,\n            \"o\": 500,\n            \"p\": 500,\n            \"q\": 500,\n            \"r\": 389,\n            \"s\": 389,\n            \"t\": 278,\n            \"u\": 556,\n            \"v\": 444,\n            \"w\": 667,\n            \"x\": 500,\n            \"y\": 444,\n            \"z\": 389,\n            \"{\": 348,\n            \"|\": 220,\n            \"}\": 348,\n            \"~\": 570,\n            \"\\xa1\": 389,\n            \"\\xa2\": 500,\n            \"\\xa3\": 500,\n            \"\\u2044\": 167,\n            \"\\xa5\": 500,\n            \"\\u0192\": 500,\n            \"\\xa7\": 500,\n            \"\\xa4\": 500,\n            \"'\": 278,\n            \"\\u201c\": 500,\n            \"\\xab\": 500,\n            \"\\u2039\": 333,\n            \"\\u203a\": 333,\n            \"\\ufb01\": 556,\n            \"\\ufb02\": 556,\n            \"\\u2013\": 500,\n            \"\\u2020\": 500,\n            \"\\u2021\": 500,\n            \"\\xb7\": 250,\n            \"\\xb6\": 500,\n            \"\\u2022\": 350,\n            \"\\u201a\": 333,\n            \"\\u201e\": 500,\n            \"\\u201d\": 500,\n            \"\\xbb\": 500,\n            \"\\u2026\": 1000,\n            \"\\u2030\": 1000,\n            \"\\xbf\": 500,\n            \"`\": 333,\n            \"\\xb4\": 333,\n            \"\\u02c6\": 333,\n            \"\\u02dc\": 333,\n            \"\\xaf\": 333,\n            \"\\u02d8\": 333,\n            \"\\u02d9\": 333,\n            \"\\xa8\": 333,\n            \"\\u02da\": 333,\n            \"\\xb8\": 333,\n            \"\\u02dd\": 333,\n            \"\\u02db\": 333,\n            \"\\u02c7\": 333,\n            \"\\u2014\": 1000,\n            \"\\xc6\": 944,\n            \"\\xaa\": 266,\n            \"\\u0141\": 611,\n            \"\\xd8\": 722,\n            \"\\u0152\": 944,\n            \"\\xba\": 300,\n            \"\\xe6\": 722,\n            \"\\u0131\": 278,\n            \"\\u0142\": 278,\n            \"\\xf8\": 500,\n            \"\\u0153\": 722,\n            \"\\xdf\": 500,\n            \"\\xcf\": 389,\n            \"\\xe9\": 444,\n            \"\\u0103\": 500,\n            \"\\u0171\": 556,\n            \"\\u011b\": 444,\n            \"\\u0178\": 611,\n            \"\\xf7\": 570,\n            \"\\xdd\": 611,\n            \"\\xc2\": 667,\n            \"\\xe1\": 500,\n            \"\\xdb\": 722,\n            \"\\xfd\": 444,\n            \"\\u0219\": 389,\n            \"\\xea\": 444,\n            \"\\u016e\": 722,\n            \"\\xdc\": 722,\n            \"\\u0105\": 500,\n            \"\\xda\": 722,\n            \"\\u0173\": 556,\n            \"\\xcb\": 667,\n            \"\\u0110\": 722,\n            \"\\uf6c3\": 250,\n            \"\\xa9\": 747,\n            \"\\u0112\": 667,\n            \"\\u010d\": 444,\n            \"\\xe5\": 500,\n            \"\\u0145\": 722,\n            \"\\u013a\": 278,\n            \"\\xe0\": 500,\n            \"\\u0162\": 611,\n            \"\\u0106\": 667,\n            \"\\xe3\": 500,\n            \"\\u0116\": 667,\n            \"\\u0161\": 389,\n            \"\\u015f\": 389,\n            \"\\xed\": 278,\n            \"\\u25ca\": 494,\n            \"\\u0158\": 667,\n            \"\\u0122\": 722,\n            \"\\xfb\": 556,\n            \"\\xe2\": 500,\n            \"\\u0100\": 667,\n            \"\\u0159\": 389,\n            \"\\xe7\": 444,\n            \"\\u017b\": 611,\n            \"\\xde\": 611,\n            \"\\u014c\": 722,\n            \"\\u0154\": 667,\n            \"\\u015a\": 556,\n            \"\\u010f\": 608,\n            \"\\u016a\": 722,\n            \"\\u016f\": 556,\n            \"\\xb3\": 300,\n            \"\\xd2\": 722,\n            \"\\xc0\": 667,\n            \"\\u0102\": 667,\n            \"\\xd7\": 570,\n            \"\\xfa\": 556,\n            \"\\u0164\": 611,\n            \"\\u2202\": 494,\n            \"\\xff\": 444,\n            \"\\u0143\": 722,\n            \"\\xee\": 278,\n            \"\\xca\": 667,\n            \"\\xe4\": 500,\n            \"\\xeb\": 444,\n            \"\\u0107\": 444,\n            \"\\u0144\": 556,\n            \"\\u016b\": 556,\n            \"\\u0147\": 722,\n            \"\\xcd\": 389,\n            \"\\xb1\": 570,\n            \"\\xa6\": 220,\n            \"\\xae\": 747,\n            \"\\u011e\": 722,\n            \"\\u0130\": 389,\n            \"\\u2211\": 600,\n            \"\\xc8\": 667,\n            \"\\u0155\": 389,\n            \"\\u014d\": 500,\n            \"\\u0179\": 611,\n            \"\\u017d\": 611,\n            \"\\u2265\": 549,\n            \"\\xd0\": 722,\n            \"\\xc7\": 667,\n            \"\\u013c\": 278,\n            \"\\u0165\": 366,\n            \"\\u0119\": 444,\n            \"\\u0172\": 722,\n            \"\\xc1\": 667,\n            \"\\xc4\": 667,\n            \"\\xe8\": 444,\n            \"\\u017a\": 389,\n            \"\\u012f\": 278,\n            \"\\xd3\": 722,\n            \"\\xf3\": 500,\n            \"\\u0101\": 500,\n            \"\\u015b\": 389,\n            \"\\xef\": 278,\n            \"\\xd4\": 722,\n            \"\\xd9\": 722,\n            \"\\u2206\": 612,\n            \"\\xfe\": 500,\n            \"\\xb2\": 300,\n            \"\\xd6\": 722,\n            \"\\xb5\": 576,\n            \"\\xec\": 278,\n            \"\\u0151\": 500,\n            \"\\u0118\": 667,\n            \"\\u0111\": 500,\n            \"\\xbe\": 750,\n            \"\\u015e\": 556,\n            \"\\u013e\": 382,\n            \"\\u0136\": 667,\n            \"\\u0139\": 611,\n            \"\\u2122\": 1000,\n            \"\\u0117\": 444,\n            \"\\xcc\": 389,\n            \"\\u012a\": 389,\n            \"\\u013d\": 611,\n            \"\\xbd\": 750,\n            \"\\u2264\": 549,\n            \"\\xf4\": 500,\n            \"\\xf1\": 556,\n            \"\\u0170\": 722,\n            \"\\xc9\": 667,\n            \"\\u0113\": 444,\n            \"\\u011f\": 500,\n            \"\\xbc\": 750,\n            \"\\u0160\": 556,\n            \"\\u0218\": 556,\n            \"\\u0150\": 722,\n            \"\\xb0\": 400,\n            \"\\xf2\": 500,\n            \"\\u010c\": 667,\n            \"\\xf9\": 556,\n            \"\\u221a\": 549,\n            \"\\u010e\": 722,\n            \"\\u0157\": 389,\n            \"\\xd1\": 722,\n            \"\\xf5\": 500,\n            \"\\u0156\": 667,\n            \"\\u013b\": 611,\n            \"\\xc3\": 667,\n            \"\\u0104\": 667,\n            \"\\xc5\": 667,\n            \"\\xd5\": 722,\n            \"\\u017c\": 389,\n            \"\\u011a\": 667,\n            \"\\u012e\": 389,\n            \"\\u0137\": 500,\n            \"\\u2212\": 606,\n            \"\\xce\": 389,\n            \"\\u0148\": 556,\n            \"\\u0163\": 278,\n            \"\\xac\": 606,\n            \"\\xf6\": 500,\n            \"\\xfc\": 556,\n            \"\\u2260\": 549,\n            \"\\u0123\": 500,\n            \"\\xf0\": 500,\n            \"\\u017e\": 389,\n            \"\\u0146\": 556,\n            \"\\xb9\": 300,\n            \"\\u012b\": 278,\n            \"\\u20ac\": 500,\n        },\n    ),\n    # Generated from Times-Italic.afm\n    # Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated.  All Rights\n    # Reserved.  Times is a trademark of Linotype-Hell AG and/or its subsidiaries.\n    \"Times-Italic\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Times-Italic\",\n            family=\"Times\",\n            weight=\"Medium\",\n            ascent=683,\n            descent=-217,\n            cap_height=653,\n            x_height=441,\n            italic_angle=-15.5,\n            flags=98,\n            bbox=(-169.0, -217.0, 1010.0, 883.0),\n        ),\n        character_widths={\n            \" \": 250,\n            \"default\": 500,\n            \"!\": 333,\n            '\"': 420,\n            \"#\": 500,\n            \"$\": 500,\n            \"%\": 833,\n            \"&\": 778,\n            \"\\u2019\": 333,\n            \"(\": 333,\n            \")\": 333,\n            \"*\": 500,\n            \"+\": 675,\n            \",\": 250,\n            \"-\": 333,\n            \".\": 250,\n            \"/\": 278,\n            \"0\": 500,\n            \"1\": 500,\n            \"2\": 500,\n            \"3\": 500,\n            \"4\": 500,\n            \"5\": 500,\n            \"6\": 500,\n            \"7\": 500,\n            \"8\": 500,\n            \"9\": 500,\n            \":\": 333,\n            \";\": 333,\n            \"<\": 675,\n            \"=\": 675,\n            \">\": 675,\n            \"?\": 500,\n            \"@\": 920,\n            \"A\": 611,\n            \"B\": 611,\n            \"C\": 667,\n            \"D\": 722,\n            \"E\": 611,\n            \"F\": 611,\n            \"G\": 722,\n            \"H\": 722,\n            \"I\": 333,\n            \"J\": 444,\n            \"K\": 667,\n            \"L\": 556,\n            \"M\": 833,\n            \"N\": 667,\n            \"O\": 722,\n            \"P\": 611,\n            \"Q\": 722,\n            \"R\": 611,\n            \"S\": 500,\n            \"T\": 556,\n            \"U\": 722,\n            \"V\": 611,\n            \"W\": 833,\n            \"X\": 611,\n            \"Y\": 556,\n            \"Z\": 556,\n            \"[\": 389,\n            \"\\\\\": 278,\n            \"]\": 389,\n            \"^\": 422,\n            \"_\": 500,\n            \"\\u2018\": 333,\n            \"a\": 500,\n            \"b\": 500,\n            \"c\": 444,\n            \"d\": 500,\n            \"e\": 444,\n            \"f\": 278,\n            \"g\": 500,\n            \"h\": 500,\n            \"i\": 278,\n            \"j\": 278,\n            \"k\": 444,\n            \"l\": 278,\n            \"m\": 722,\n            \"n\": 500,\n            \"o\": 500,\n            \"p\": 500,\n            \"q\": 500,\n            \"r\": 389,\n            \"s\": 389,\n            \"t\": 278,\n            \"u\": 500,\n            \"v\": 444,\n            \"w\": 667,\n            \"x\": 444,\n            \"y\": 444,\n            \"z\": 389,\n            \"{\": 400,\n            \"|\": 275,\n            \"}\": 400,\n            \"~\": 541,\n            \"\\xa1\": 389,\n            \"\\xa2\": 500,\n            \"\\xa3\": 500,\n            \"\\u2044\": 167,\n            \"\\xa5\": 500,\n            \"\\u0192\": 500,\n            \"\\xa7\": 500,\n            \"\\xa4\": 500,\n            \"'\": 214,\n            \"\\u201c\": 556,\n            \"\\xab\": 500,\n            \"\\u2039\": 333,\n            \"\\u203a\": 333,\n            \"\\ufb01\": 500,\n            \"\\ufb02\": 500,\n            \"\\u2013\": 500,\n            \"\\u2020\": 500,\n            \"\\u2021\": 500,\n            \"\\xb7\": 250,\n            \"\\xb6\": 523,\n            \"\\u2022\": 350,\n            \"\\u201a\": 333,\n            \"\\u201e\": 556,\n            \"\\u201d\": 556,\n            \"\\xbb\": 500,\n            \"\\u2026\": 889,\n            \"\\u2030\": 1000,\n            \"\\xbf\": 500,\n            \"`\": 333,\n            \"\\xb4\": 333,\n            \"\\u02c6\": 333,\n            \"\\u02dc\": 333,\n            \"\\xaf\": 333,\n            \"\\u02d8\": 333,\n            \"\\u02d9\": 333,\n            \"\\xa8\": 333,\n            \"\\u02da\": 333,\n            \"\\xb8\": 333,\n            \"\\u02dd\": 333,\n            \"\\u02db\": 333,\n            \"\\u02c7\": 333,\n            \"\\u2014\": 889,\n            \"\\xc6\": 889,\n            \"\\xaa\": 276,\n            \"\\u0141\": 556,\n            \"\\xd8\": 722,\n            \"\\u0152\": 944,\n            \"\\xba\": 310,\n            \"\\xe6\": 667,\n            \"\\u0131\": 278,\n            \"\\u0142\": 278,\n            \"\\xf8\": 500,\n            \"\\u0153\": 667,\n            \"\\xdf\": 500,\n            \"\\xcf\": 333,\n            \"\\xe9\": 444,\n            \"\\u0103\": 500,\n            \"\\u0171\": 500,\n            \"\\u011b\": 444,\n            \"\\u0178\": 556,\n            \"\\xf7\": 675,\n            \"\\xdd\": 556,\n            \"\\xc2\": 611,\n            \"\\xe1\": 500,\n            \"\\xdb\": 722,\n            \"\\xfd\": 444,\n            \"\\u0219\": 389,\n            \"\\xea\": 444,\n            \"\\u016e\": 722,\n            \"\\xdc\": 722,\n            \"\\u0105\": 500,\n            \"\\xda\": 722,\n            \"\\u0173\": 500,\n            \"\\xcb\": 611,\n            \"\\u0110\": 722,\n            \"\\uf6c3\": 250,\n            \"\\xa9\": 760,\n            \"\\u0112\": 611,\n            \"\\u010d\": 444,\n            \"\\xe5\": 500,\n            \"\\u0145\": 667,\n            \"\\u013a\": 278,\n            \"\\xe0\": 500,\n            \"\\u0162\": 556,\n            \"\\u0106\": 667,\n            \"\\xe3\": 500,\n            \"\\u0116\": 611,\n            \"\\u0161\": 389,\n            \"\\u015f\": 389,\n            \"\\xed\": 278,\n            \"\\u25ca\": 471,\n            \"\\u0158\": 611,\n            \"\\u0122\": 722,\n            \"\\xfb\": 500,\n            \"\\xe2\": 500,\n            \"\\u0100\": 611,\n            \"\\u0159\": 389,\n            \"\\xe7\": 444,\n            \"\\u017b\": 556,\n            \"\\xde\": 611,\n            \"\\u014c\": 722,\n            \"\\u0154\": 611,\n            \"\\u015a\": 500,\n            \"\\u010f\": 544,\n            \"\\u016a\": 722,\n            \"\\u016f\": 500,\n            \"\\xb3\": 300,\n            \"\\xd2\": 722,\n            \"\\xc0\": 611,\n            \"\\u0102\": 611,\n            \"\\xd7\": 675,\n            \"\\xfa\": 500,\n            \"\\u0164\": 556,\n            \"\\u2202\": 476,\n            \"\\xff\": 444,\n            \"\\u0143\": 667,\n            \"\\xee\": 278,\n            \"\\xca\": 611,\n            \"\\xe4\": 500,\n            \"\\xeb\": 444,\n            \"\\u0107\": 444,\n            \"\\u0144\": 500,\n            \"\\u016b\": 500,\n            \"\\u0147\": 667,\n            \"\\xcd\": 333,\n            \"\\xb1\": 675,\n            \"\\xa6\": 275,\n            \"\\xae\": 760,\n            \"\\u011e\": 722,\n            \"\\u0130\": 333,\n            \"\\u2211\": 600,\n            \"\\xc8\": 611,\n            \"\\u0155\": 389,\n            \"\\u014d\": 500,\n            \"\\u0179\": 556,\n            \"\\u017d\": 556,\n            \"\\u2265\": 549,\n            \"\\xd0\": 722,\n            \"\\xc7\": 667,\n            \"\\u013c\": 278,\n            \"\\u0165\": 300,\n            \"\\u0119\": 444,\n            \"\\u0172\": 722,\n            \"\\xc1\": 611,\n            \"\\xc4\": 611,\n            \"\\xe8\": 444,\n            \"\\u017a\": 389,\n            \"\\u012f\": 278,\n            \"\\xd3\": 722,\n            \"\\xf3\": 500,\n            \"\\u0101\": 500,\n            \"\\u015b\": 389,\n            \"\\xef\": 278,\n            \"\\xd4\": 722,\n            \"\\xd9\": 722,\n            \"\\u2206\": 612,\n            \"\\xfe\": 500,\n            \"\\xb2\": 300,\n            \"\\xd6\": 722,\n            \"\\xb5\": 500,\n            \"\\xec\": 278,\n            \"\\u0151\": 500,\n            \"\\u0118\": 611,\n            \"\\u0111\": 500,\n            \"\\xbe\": 750,\n            \"\\u015e\": 500,\n            \"\\u013e\": 300,\n            \"\\u0136\": 667,\n            \"\\u0139\": 556,\n            \"\\u2122\": 980,\n            \"\\u0117\": 444,\n            \"\\xcc\": 333,\n            \"\\u012a\": 333,\n            \"\\u013d\": 611,\n            \"\\xbd\": 750,\n            \"\\u2264\": 549,\n            \"\\xf4\": 500,\n            \"\\xf1\": 500,\n            \"\\u0170\": 722,\n            \"\\xc9\": 611,\n            \"\\u0113\": 444,\n            \"\\u011f\": 500,\n            \"\\xbc\": 750,\n            \"\\u0160\": 500,\n            \"\\u0218\": 500,\n            \"\\u0150\": 722,\n            \"\\xb0\": 400,\n            \"\\xf2\": 500,\n            \"\\u010c\": 667,\n            \"\\xf9\": 500,\n            \"\\u221a\": 453,\n            \"\\u010e\": 722,\n            \"\\u0157\": 389,\n            \"\\xd1\": 667,\n            \"\\xf5\": 500,\n            \"\\u0156\": 611,\n            \"\\u013b\": 556,\n            \"\\xc3\": 611,\n            \"\\u0104\": 611,\n            \"\\xc5\": 611,\n            \"\\xd5\": 722,\n            \"\\u017c\": 389,\n            \"\\u011a\": 611,\n            \"\\u012e\": 333,\n            \"\\u0137\": 444,\n            \"\\u2212\": 675,\n            \"\\xce\": 333,\n            \"\\u0148\": 500,\n            \"\\u0163\": 278,\n            \"\\xac\": 675,\n            \"\\xf6\": 500,\n            \"\\xfc\": 500,\n            \"\\u2260\": 549,\n            \"\\u0123\": 500,\n            \"\\xf0\": 500,\n            \"\\u017e\": 389,\n            \"\\u0146\": 500,\n            \"\\xb9\": 300,\n            \"\\u012b\": 278,\n            \"\\u20ac\": 500,\n        },\n    ),\n    # Generated from Times-Roman.afm\n    # Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated.  All Rights\n    # Reserved.  Times is a trademark of Linotype-Hell AG and/or its subsidiaries.\n    \"Times-Roman\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"Times-Roman\",\n            family=\"Times\",\n            weight=\"Roman\",\n            ascent=683,\n            descent=-217,\n            cap_height=662,\n            x_height=450,\n            italic_angle=0,\n            flags=34,\n            bbox=(-168.0, -218.0, 1000.0, 898.0),\n        ),\n        character_widths={\n            \" \": 250,\n            \"default\": 500,\n            \"!\": 333,\n            '\"': 408,\n            \"#\": 500,\n            \"$\": 500,\n            \"%\": 833,\n            \"&\": 778,\n            \"\\u2019\": 333,\n            \"(\": 333,\n            \")\": 333,\n            \"*\": 500,\n            \"+\": 564,\n            \",\": 250,\n            \"-\": 333,\n            \".\": 250,\n            \"/\": 278,\n            \"0\": 500,\n            \"1\": 500,\n            \"2\": 500,\n            \"3\": 500,\n            \"4\": 500,\n            \"5\": 500,\n            \"6\": 500,\n            \"7\": 500,\n            \"8\": 500,\n            \"9\": 500,\n            \":\": 278,\n            \";\": 278,\n            \"<\": 564,\n            \"=\": 564,\n            \">\": 564,\n            \"?\": 444,\n            \"@\": 921,\n            \"A\": 722,\n            \"B\": 667,\n            \"C\": 667,\n            \"D\": 722,\n            \"E\": 611,\n            \"F\": 556,\n            \"G\": 722,\n            \"H\": 722,\n            \"I\": 333,\n            \"J\": 389,\n            \"K\": 722,\n            \"L\": 611,\n            \"M\": 889,\n            \"N\": 722,\n            \"O\": 722,\n            \"P\": 556,\n            \"Q\": 722,\n            \"R\": 667,\n            \"S\": 556,\n            \"T\": 611,\n            \"U\": 722,\n            \"V\": 722,\n            \"W\": 944,\n            \"X\": 722,\n            \"Y\": 722,\n            \"Z\": 611,\n            \"[\": 333,\n            \"\\\\\": 278,\n            \"]\": 333,\n            \"^\": 469,\n            \"_\": 500,\n            \"\\u2018\": 333,\n            \"a\": 444,\n            \"b\": 500,\n            \"c\": 444,\n            \"d\": 500,\n            \"e\": 444,\n            \"f\": 333,\n            \"g\": 500,\n            \"h\": 500,\n            \"i\": 278,\n            \"j\": 278,\n            \"k\": 500,\n            \"l\": 278,\n            \"m\": 778,\n            \"n\": 500,\n            \"o\": 500,\n            \"p\": 500,\n            \"q\": 500,\n            \"r\": 333,\n            \"s\": 389,\n            \"t\": 278,\n            \"u\": 500,\n            \"v\": 500,\n            \"w\": 722,\n            \"x\": 500,\n            \"y\": 500,\n            \"z\": 444,\n            \"{\": 480,\n            \"|\": 200,\n            \"}\": 480,\n            \"~\": 541,\n            \"\\xa1\": 333,\n            \"\\xa2\": 500,\n            \"\\xa3\": 500,\n            \"\\u2044\": 167,\n            \"\\xa5\": 500,\n            \"\\u0192\": 500,\n            \"\\xa7\": 500,\n            \"\\xa4\": 500,\n            \"'\": 180,\n            \"\\u201c\": 444,\n            \"\\xab\": 500,\n            \"\\u2039\": 333,\n            \"\\u203a\": 333,\n            \"\\ufb01\": 556,\n            \"\\ufb02\": 556,\n            \"\\u2013\": 500,\n            \"\\u2020\": 500,\n            \"\\u2021\": 500,\n            \"\\xb7\": 250,\n            \"\\xb6\": 453,\n            \"\\u2022\": 350,\n            \"\\u201a\": 333,\n            \"\\u201e\": 444,\n            \"\\u201d\": 444,\n            \"\\xbb\": 500,\n            \"\\u2026\": 1000,\n            \"\\u2030\": 1000,\n            \"\\xbf\": 444,\n            \"`\": 333,\n            \"\\xb4\": 333,\n            \"\\u02c6\": 333,\n            \"\\u02dc\": 333,\n            \"\\xaf\": 333,\n            \"\\u02d8\": 333,\n            \"\\u02d9\": 333,\n            \"\\xa8\": 333,\n            \"\\u02da\": 333,\n            \"\\xb8\": 333,\n            \"\\u02dd\": 333,\n            \"\\u02db\": 333,\n            \"\\u02c7\": 333,\n            \"\\u2014\": 1000,\n            \"\\xc6\": 889,\n            \"\\xaa\": 276,\n            \"\\u0141\": 611,\n            \"\\xd8\": 722,\n            \"\\u0152\": 889,\n            \"\\xba\": 310,\n            \"\\xe6\": 667,\n            \"\\u0131\": 278,\n            \"\\u0142\": 278,\n            \"\\xf8\": 500,\n            \"\\u0153\": 722,\n            \"\\xdf\": 500,\n            \"\\xcf\": 333,\n            \"\\xe9\": 444,\n            \"\\u0103\": 444,\n            \"\\u0171\": 500,\n            \"\\u011b\": 444,\n            \"\\u0178\": 722,\n            \"\\xf7\": 564,\n            \"\\xdd\": 722,\n            \"\\xc2\": 722,\n            \"\\xe1\": 444,\n            \"\\xdb\": 722,\n            \"\\xfd\": 500,\n            \"\\u0219\": 389,\n            \"\\xea\": 444,\n            \"\\u016e\": 722,\n            \"\\xdc\": 722,\n            \"\\u0105\": 444,\n            \"\\xda\": 722,\n            \"\\u0173\": 500,\n            \"\\xcb\": 611,\n            \"\\u0110\": 722,\n            \"\\uf6c3\": 250,\n            \"\\xa9\": 760,\n            \"\\u0112\": 611,\n            \"\\u010d\": 444,\n            \"\\xe5\": 444,\n            \"\\u0145\": 722,\n            \"\\u013a\": 278,\n            \"\\xe0\": 444,\n            \"\\u0162\": 611,\n            \"\\u0106\": 667,\n            \"\\xe3\": 444,\n            \"\\u0116\": 611,\n            \"\\u0161\": 389,\n            \"\\u015f\": 389,\n            \"\\xed\": 278,\n            \"\\u25ca\": 471,\n            \"\\u0158\": 667,\n            \"\\u0122\": 722,\n            \"\\xfb\": 500,\n            \"\\xe2\": 444,\n            \"\\u0100\": 722,\n            \"\\u0159\": 333,\n            \"\\xe7\": 444,\n            \"\\u017b\": 611,\n            \"\\xde\": 556,\n            \"\\u014c\": 722,\n            \"\\u0154\": 667,\n            \"\\u015a\": 556,\n            \"\\u010f\": 588,\n            \"\\u016a\": 722,\n            \"\\u016f\": 500,\n            \"\\xb3\": 300,\n            \"\\xd2\": 722,\n            \"\\xc0\": 722,\n            \"\\u0102\": 722,\n            \"\\xd7\": 564,\n            \"\\xfa\": 500,\n            \"\\u0164\": 611,\n            \"\\u2202\": 476,\n            \"\\xff\": 500,\n            \"\\u0143\": 722,\n            \"\\xee\": 278,\n            \"\\xca\": 611,\n            \"\\xe4\": 444,\n            \"\\xeb\": 444,\n            \"\\u0107\": 444,\n            \"\\u0144\": 500,\n            \"\\u016b\": 500,\n            \"\\u0147\": 722,\n            \"\\xcd\": 333,\n            \"\\xb1\": 564,\n            \"\\xa6\": 200,\n            \"\\xae\": 760,\n            \"\\u011e\": 722,\n            \"\\u0130\": 333,\n            \"\\u2211\": 600,\n            \"\\xc8\": 611,\n            \"\\u0155\": 333,\n            \"\\u014d\": 500,\n            \"\\u0179\": 611,\n            \"\\u017d\": 611,\n            \"\\u2265\": 549,\n            \"\\xd0\": 722,\n            \"\\xc7\": 667,\n            \"\\u013c\": 278,\n            \"\\u0165\": 326,\n            \"\\u0119\": 444,\n            \"\\u0172\": 722,\n            \"\\xc1\": 722,\n            \"\\xc4\": 722,\n            \"\\xe8\": 444,\n            \"\\u017a\": 444,\n            \"\\u012f\": 278,\n            \"\\xd3\": 722,\n            \"\\xf3\": 500,\n            \"\\u0101\": 444,\n            \"\\u015b\": 389,\n            \"\\xef\": 278,\n            \"\\xd4\": 722,\n            \"\\xd9\": 722,\n            \"\\u2206\": 612,\n            \"\\xfe\": 500,\n            \"\\xb2\": 300,\n            \"\\xd6\": 722,\n            \"\\xb5\": 500,\n            \"\\xec\": 278,\n            \"\\u0151\": 500,\n            \"\\u0118\": 611,\n            \"\\u0111\": 500,\n            \"\\xbe\": 750,\n            \"\\u015e\": 556,\n            \"\\u013e\": 344,\n            \"\\u0136\": 722,\n            \"\\u0139\": 611,\n            \"\\u2122\": 980,\n            \"\\u0117\": 444,\n            \"\\xcc\": 333,\n            \"\\u012a\": 333,\n            \"\\u013d\": 611,\n            \"\\xbd\": 750,\n            \"\\u2264\": 549,\n            \"\\xf4\": 500,\n            \"\\xf1\": 500,\n            \"\\u0170\": 722,\n            \"\\xc9\": 611,\n            \"\\u0113\": 444,\n            \"\\u011f\": 500,\n            \"\\xbc\": 750,\n            \"\\u0160\": 556,\n            \"\\u0218\": 556,\n            \"\\u0150\": 722,\n            \"\\xb0\": 400,\n            \"\\xf2\": 500,\n            \"\\u010c\": 667,\n            \"\\xf9\": 500,\n            \"\\u221a\": 453,\n            \"\\u010e\": 722,\n            \"\\u0157\": 333,\n            \"\\xd1\": 722,\n            \"\\xf5\": 500,\n            \"\\u0156\": 667,\n            \"\\u013b\": 611,\n            \"\\xc3\": 722,\n            \"\\u0104\": 722,\n            \"\\xc5\": 722,\n            \"\\xd5\": 722,\n            \"\\u017c\": 444,\n            \"\\u011a\": 611,\n            \"\\u012e\": 333,\n            \"\\u0137\": 500,\n            \"\\u2212\": 564,\n            \"\\xce\": 333,\n            \"\\u0148\": 500,\n            \"\\u0163\": 278,\n            \"\\xac\": 564,\n            \"\\xf6\": 500,\n            \"\\xfc\": 500,\n            \"\\u2260\": 549,\n            \"\\u0123\": 500,\n            \"\\xf0\": 500,\n            \"\\u017e\": 444,\n            \"\\u0146\": 500,\n            \"\\xb9\": 300,\n            \"\\u012b\": 278,\n            \"\\u20ac\": 500,\n        },\n    ),\n    # Generated from ZapfDingbats.afm\n    # Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated. All Rights Reserved.\n    # ITC Zapf Dingbats is a registered trademark of International Typeface Corporation.\n    \"ZapfDingbats\": CoreFontMetrics(\n        font_descriptor=FontDescriptor(\n            name=\"ZapfDingbats\",\n            family=\"ZapfDingbats\",\n            weight=\"Medium\",\n            ascent=0.0,\n            descent=0.0,\n            cap_height=0.0,\n            x_height=0.0,\n            italic_angle=0,\n            flags=4,\n            bbox=(-1.0, -143.0, 981.0, 820.0),\n        ),\n        character_widths={\n            \" \": 790,\n            \"default\": 1580,\n            \"\\x01\": 974,\n            \"\\x02\": 961,\n            \"\\xca\": 974,\n            \"\\x03\": 980,\n            \"\\x04\": 719,\n            \"\\x05\": 789,\n            \"w\": 790,\n            \"v\": 791,\n            \"u\": 690,\n            \"\\x0b\": 960,\n            \"\\x0c\": 939,\n            \"\\r\": 549,\n            \"\\x0e\": 855,\n            \"\\x0f\": 911,\n            \"\\x10\": 933,\n            \"i\": 911,\n            \"\\x11\": 945,\n            \"\\x12\": 974,\n            \"\\x13\": 755,\n            \"\\x14\": 846,\n            \"\\x15\": 762,\n            \"\\x16\": 761,\n            \"\\x17\": 571,\n            \"\\x18\": 677,\n            \"\\x19\": 763,\n            \"\\x1a\": 760,\n            \"\\x1b\": 759,\n            \"\\x1c\": 754,\n            \"\\x06\": 494,\n            \"\\x07\": 552,\n            \"\\x08\": 537,\n            \"\\t\": 577,\n            \"\\n\": 692,\n            \"\\x1d\": 786,\n            \"\\x1e\": 788,\n            \"\\x1f\": 788,\n            \"!\": 793,\n            '\"': 794,\n            \"#\": 816,\n            \"$\": 823,\n            \"%\": 789,\n            \"&\": 841,\n            \"'\": 823,\n            \"(\": 833,\n            \")\": 816,\n            \"*\": 831,\n            \"+\": 923,\n            \",\": 744,\n            \"-\": 723,\n            \".\": 749,\n            \"/\": 790,\n            \"0\": 792,\n            \"1\": 695,\n            \"2\": 776,\n            \"3\": 768,\n            \"4\": 792,\n            \"5\": 759,\n            \"6\": 707,\n            \"7\": 708,\n            \"8\": 682,\n            \"9\": 701,\n            \":\": 826,\n            \";\": 815,\n            \"<\": 789,\n            \"=\": 789,\n            \">\": 707,\n            \"?\": 687,\n            \"@\": 696,\n            \"A\": 689,\n            \"B\": 786,\n            \"C\": 787,\n            \"D\": 713,\n            \"E\": 791,\n            \"F\": 785,\n            \"G\": 791,\n            \"H\": 873,\n            \"I\": 761,\n            \"J\": 762,\n            \"\\xcb\": 762,\n            \"K\": 759,\n            \"\\xcc\": 759,\n            \"L\": 892,\n            \"M\": 892,\n            \"N\": 788,\n            \"O\": 784,\n            \"Q\": 438,\n            \"R\": 138,\n            \"S\": 277,\n            \"T\": 415,\n            \"a\": 392,\n            \"b\": 392,\n            \"c\": 668,\n            \"d\": 668,\n            \"Y\": 390,\n            \"Z\": 390,\n            \"]\": 317,\n            \"^\": 317,\n            \"[\": 276,\n            \"\\\\\": 276,\n            \"\\xcd\": 509,\n            \"U\": 509,\n            \"\\xce\": 410,\n            \"V\": 410,\n            \"W\": 234,\n            \"X\": 234,\n            \"_\": 334,\n            \"`\": 334,\n            \"e\": 732,\n            \"f\": 544,\n            \"g\": 544,\n            \"h\": 910,\n            \"j\": 667,\n            \"k\": 760,\n            \"l\": 760,\n            \"p\": 776,\n            \"o\": 595,\n            \"n\": 694,\n            \"m\": 626,\n            \"x\": 788,\n            \"y\": 788,\n            \"z\": 788,\n            \"{\": 788,\n            \"|\": 788,\n            \"}\": 788,\n            \"~\": 788,\n            \"\\x7f\": 788,\n            \"\\x80\": 788,\n            \"\\x81\": 788,\n            \"\\x82\": 788,\n            \"\\x83\": 788,\n            \"\\x84\": 788,\n            \"\\x85\": 788,\n            \"\\x86\": 788,\n            \"\\x87\": 788,\n            \"\\x88\": 788,\n            \"\\x89\": 788,\n            \"\\x8a\": 788,\n            \"\\x8b\": 788,\n            \"\\x8c\": 788,\n            \"\\x8d\": 788,\n            \"\\x8e\": 788,\n            \"\\x8f\": 788,\n            \"\\x90\": 788,\n            \"\\x91\": 788,\n            \"\\x92\": 788,\n            \"\\x93\": 788,\n            \"\\x94\": 788,\n            \"\\x95\": 788,\n            \"\\x96\": 788,\n            \"\\x97\": 788,\n            \"\\x98\": 788,\n            \"\\x99\": 788,\n            \"\\x9a\": 788,\n            \"\\x9b\": 788,\n            \"\\x9c\": 788,\n            \"\\x9d\": 788,\n            \"\\x9e\": 788,\n            \"\\x9f\": 788,\n            \"\\xa0\": 894,\n            \"\\xa1\": 838,\n            \"\\xa3\": 1016,\n            \"\\xa4\": 458,\n            \"\\xc4\": 748,\n            \"\\xa5\": 924,\n            \"\\xc0\": 748,\n            \"\\xa6\": 918,\n            \"\\xa7\": 927,\n            \"\\xa8\": 928,\n            \"\\xa9\": 928,\n            \"\\xaa\": 834,\n            \"\\xab\": 873,\n            \"\\xac\": 828,\n            \"\\xad\": 924,\n            \"\\xa2\": 924,\n            \"\\xae\": 917,\n            \"\\xaf\": 930,\n            \"\\xb0\": 931,\n            \"\\xb1\": 463,\n            \"\\xb2\": 883,\n            \"\\xb3\": 836,\n            \"\\xc1\": 836,\n            \"\\xb4\": 867,\n            \"\\xc7\": 867,\n            \"\\xb5\": 696,\n            \"\\xc8\": 696,\n            \"\\xb6\": 874,\n            \"\\xc9\": 874,\n            \"\\xb7\": 760,\n            \"\\xb8\": 946,\n            \"\\xc5\": 771,\n            \"\\xb9\": 865,\n            \"\\xc2\": 771,\n            \"\\xc6\": 888,\n            \"\\xba\": 967,\n            \"\\xc3\": 888,\n            \"\\xbb\": 831,\n            \"\\xbc\": 873,\n            \"\\xbd\": 927,\n            \"\\xbe\": 970,\n            \"\\xbf\": 918,\n        },\n    ),\n}\n\n\n# Add aliases per table H.3 on pp. 1109-1110 of the PDF 1.7 reference\nCORE_FONT_METRICS[\"Arial\"] = CORE_FONT_METRICS[\"Helvetica\"]\nCORE_FONT_METRICS[\"Arial,Italic\"] = CORE_FONT_METRICS[\"Helvetica-Oblique\"]\nCORE_FONT_METRICS[\"Arial,Bold\"] = CORE_FONT_METRICS[\"Helvetica-Bold\"]\nCORE_FONT_METRICS[\"Arial,BoldItalic\"] = CORE_FONT_METRICS[\"Helvetica-BoldOblique\"]\nCORE_FONT_METRICS[\"CourierNew\"] = CORE_FONT_METRICS[\"Courier\"]\nCORE_FONT_METRICS[\"CourierNew,Italic\"] = CORE_FONT_METRICS[\"Courier-Oblique\"]\nCORE_FONT_METRICS[\"CourierNew,Bold\"] = CORE_FONT_METRICS[\"Courier-Bold\"]\nCORE_FONT_METRICS[\"CourierNew,BoldItalic\"] = CORE_FONT_METRICS[\"Courier-BoldOblique\"]\nCORE_FONT_METRICS[\"TimesNewRoman\"] = CORE_FONT_METRICS[\"Times-Roman\"]\nCORE_FONT_METRICS[\"TimesNewRoman,Italic\"] = CORE_FONT_METRICS[\"Times-Italic\"]\nCORE_FONT_METRICS[\"TimesNewRoman,Bold\"] = CORE_FONT_METRICS[\"Times-Bold\"]\nCORE_FONT_METRICS[\"TimesNewRoman,BoldItalic\"] = CORE_FONT_METRICS[\"Times-BoldItalic\"]\n"
  },
  {
    "path": "pypdf/_codecs/pdfdoc.py",
    "content": "# PDFDocEncoding Character Set: Table D.2 of PDF Reference 1.7\n# C.1 Predefined encodings sorted by character name of another PDF reference\n# Some indices have '\\u0000' although they should have something else:\n# 22: should be '\\u0017'\n_pdfdoc_encoding = [\n    \"\\u0000\",\n    \"\\u0001\",\n    \"\\u0002\",\n    \"\\u0003\",\n    \"\\u0004\",\n    \"\\u0005\",\n    \"\\u0006\",\n    \"\\u0007\",  # 0 -  7\n    \"\\u0008\",\n    \"\\u0009\",\n    \"\\u000a\",\n    \"\\u000b\",\n    \"\\u000c\",\n    \"\\u000d\",\n    \"\\u000e\",\n    \"\\u000f\",  # 8 - 15\n    \"\\u0010\",\n    \"\\u0011\",\n    \"\\u0012\",\n    \"\\u0013\",\n    \"\\u0014\",\n    \"\\u0015\",\n    \"\\u0000\",\n    \"\\u0017\",  # 16 - 23\n    \"\\u02d8\",\n    \"\\u02c7\",\n    \"\\u02c6\",\n    \"\\u02d9\",\n    \"\\u02dd\",\n    \"\\u02db\",\n    \"\\u02da\",\n    \"\\u02dc\",  # 24 - 31\n    \"\\u0020\",\n    \"\\u0021\",\n    \"\\u0022\",\n    \"\\u0023\",\n    \"\\u0024\",\n    \"\\u0025\",\n    \"\\u0026\",\n    \"\\u0027\",  # 32 - 39\n    \"\\u0028\",\n    \"\\u0029\",\n    \"\\u002a\",\n    \"\\u002b\",\n    \"\\u002c\",\n    \"\\u002d\",\n    \"\\u002e\",\n    \"\\u002f\",  # 40 - 47\n    \"\\u0030\",\n    \"\\u0031\",\n    \"\\u0032\",\n    \"\\u0033\",\n    \"\\u0034\",\n    \"\\u0035\",\n    \"\\u0036\",\n    \"\\u0037\",  # 48 - 55\n    \"\\u0038\",\n    \"\\u0039\",\n    \"\\u003a\",\n    \"\\u003b\",\n    \"\\u003c\",\n    \"\\u003d\",\n    \"\\u003e\",\n    \"\\u003f\",  # 56 - 63\n    \"\\u0040\",\n    \"\\u0041\",\n    \"\\u0042\",\n    \"\\u0043\",\n    \"\\u0044\",\n    \"\\u0045\",\n    \"\\u0046\",\n    \"\\u0047\",  # 64 - 71\n    \"\\u0048\",\n    \"\\u0049\",\n    \"\\u004a\",\n    \"\\u004b\",\n    \"\\u004c\",\n    \"\\u004d\",\n    \"\\u004e\",\n    \"\\u004f\",  # 72 - 79\n    \"\\u0050\",\n    \"\\u0051\",\n    \"\\u0052\",\n    \"\\u0053\",\n    \"\\u0054\",\n    \"\\u0055\",\n    \"\\u0056\",\n    \"\\u0057\",  # 80 - 87\n    \"\\u0058\",\n    \"\\u0059\",\n    \"\\u005a\",\n    \"\\u005b\",\n    \"\\u005c\",\n    \"\\u005d\",\n    \"\\u005e\",\n    \"\\u005f\",  # 88 - 95\n    \"\\u0060\",\n    \"\\u0061\",\n    \"\\u0062\",\n    \"\\u0063\",\n    \"\\u0064\",\n    \"\\u0065\",\n    \"\\u0066\",\n    \"\\u0067\",  # 96 - 103\n    \"\\u0068\",\n    \"\\u0069\",\n    \"\\u006a\",\n    \"\\u006b\",\n    \"\\u006c\",\n    \"\\u006d\",\n    \"\\u006e\",\n    \"\\u006f\",  # 104 - 111\n    \"\\u0070\",\n    \"\\u0071\",\n    \"\\u0072\",\n    \"\\u0073\",\n    \"\\u0074\",\n    \"\\u0075\",\n    \"\\u0076\",\n    \"\\u0077\",  # 112 - 119\n    \"\\u0078\",\n    \"\\u0079\",\n    \"\\u007a\",\n    \"\\u007b\",\n    \"\\u007c\",\n    \"\\u007d\",\n    \"\\u007e\",\n    \"\\u0000\",  # 120 - 127\n    \"\\u2022\",\n    \"\\u2020\",\n    \"\\u2021\",\n    \"\\u2026\",\n    \"\\u2014\",\n    \"\\u2013\",\n    \"\\u0192\",\n    \"\\u2044\",  # 128 - 135\n    \"\\u2039\",\n    \"\\u203a\",\n    \"\\u2212\",\n    \"\\u2030\",\n    \"\\u201e\",\n    \"\\u201c\",\n    \"\\u201d\",\n    \"\\u2018\",  # 136 - 143\n    \"\\u2019\",\n    \"\\u201a\",\n    \"\\u2122\",\n    \"\\ufb01\",\n    \"\\ufb02\",\n    \"\\u0141\",\n    \"\\u0152\",\n    \"\\u0160\",  # 144 - 151\n    \"\\u0178\",\n    \"\\u017d\",\n    \"\\u0131\",\n    \"\\u0142\",\n    \"\\u0153\",\n    \"\\u0161\",\n    \"\\u017e\",\n    \"\\u0000\",  # 152 - 159\n    \"\\u20ac\",\n    \"\\u00a1\",\n    \"\\u00a2\",\n    \"\\u00a3\",\n    \"\\u00a4\",\n    \"\\u00a5\",\n    \"\\u00a6\",\n    \"\\u00a7\",  # 160 - 167\n    \"\\u00a8\",\n    \"\\u00a9\",\n    \"\\u00aa\",\n    \"\\u00ab\",\n    \"\\u00ac\",\n    \"\\u0000\",\n    \"\\u00ae\",\n    \"\\u00af\",  # 168 - 175\n    \"\\u00b0\",\n    \"\\u00b1\",\n    \"\\u00b2\",\n    \"\\u00b3\",\n    \"\\u00b4\",\n    \"\\u00b5\",\n    \"\\u00b6\",\n    \"\\u00b7\",  # 176 - 183\n    \"\\u00b8\",\n    \"\\u00b9\",\n    \"\\u00ba\",\n    \"\\u00bb\",\n    \"\\u00bc\",\n    \"\\u00bd\",\n    \"\\u00be\",\n    \"\\u00bf\",  # 184 - 191\n    \"\\u00c0\",\n    \"\\u00c1\",\n    \"\\u00c2\",\n    \"\\u00c3\",\n    \"\\u00c4\",\n    \"\\u00c5\",\n    \"\\u00c6\",\n    \"\\u00c7\",  # 192 - 199\n    \"\\u00c8\",\n    \"\\u00c9\",\n    \"\\u00ca\",\n    \"\\u00cb\",\n    \"\\u00cc\",\n    \"\\u00cd\",\n    \"\\u00ce\",\n    \"\\u00cf\",  # 200 - 207\n    \"\\u00d0\",\n    \"\\u00d1\",\n    \"\\u00d2\",\n    \"\\u00d3\",\n    \"\\u00d4\",\n    \"\\u00d5\",\n    \"\\u00d6\",\n    \"\\u00d7\",  # 208 - 215\n    \"\\u00d8\",\n    \"\\u00d9\",\n    \"\\u00da\",\n    \"\\u00db\",\n    \"\\u00dc\",\n    \"\\u00dd\",\n    \"\\u00de\",\n    \"\\u00df\",  # 216 - 223\n    \"\\u00e0\",\n    \"\\u00e1\",\n    \"\\u00e2\",\n    \"\\u00e3\",\n    \"\\u00e4\",\n    \"\\u00e5\",\n    \"\\u00e6\",\n    \"\\u00e7\",  # 224 - 231\n    \"\\u00e8\",\n    \"\\u00e9\",\n    \"\\u00ea\",\n    \"\\u00eb\",\n    \"\\u00ec\",\n    \"\\u00ed\",\n    \"\\u00ee\",\n    \"\\u00ef\",  # 232 - 239\n    \"\\u00f0\",\n    \"\\u00f1\",\n    \"\\u00f2\",\n    \"\\u00f3\",\n    \"\\u00f4\",\n    \"\\u00f5\",\n    \"\\u00f6\",\n    \"\\u00f7\",  # 240 - 247\n    \"\\u00f8\",\n    \"\\u00f9\",\n    \"\\u00fa\",\n    \"\\u00fb\",\n    \"\\u00fc\",\n    \"\\u00fd\",\n    \"\\u00fe\",\n    \"\\u00ff\",  # 248 - 255\n]\n\nassert len(_pdfdoc_encoding) == 256\n"
  },
  {
    "path": "pypdf/_codecs/std.py",
    "content": "_std_encoding = [\n    \"\\x00\",\n    \"\\x01\",\n    \"\\x02\",\n    \"\\x03\",\n    \"\\x04\",\n    \"\\x05\",\n    \"\\x06\",\n    \"\\x07\",\n    \"\\x08\",\n    \"\\t\",\n    \"\\n\",\n    \"\\x0b\",\n    \"\\x0c\",\n    \"\\r\",\n    \"\\x0e\",\n    \"\\x0f\",\n    \"\\x10\",\n    \"\\x11\",\n    \"\\x12\",\n    \"\\x13\",\n    \"\\x14\",\n    \"\\x15\",\n    \"\\x16\",\n    \"\\x17\",\n    \"\\x18\",\n    \"\\x19\",\n    \"\\x1a\",\n    \"\\x1b\",\n    \"\\x1c\",\n    \"\\x1d\",\n    \"\\x1e\",\n    \"\\x1f\",\n    \" \",\n    \"!\",\n    '\"',\n    \"#\",\n    \"$\",\n    \"%\",\n    \"&\",\n    \"’\",\n    \"(\",\n    \")\",\n    \"*\",\n    \"+\",\n    \",\",\n    \"-\",\n    \".\",\n    \"/\",\n    \"0\",\n    \"1\",\n    \"2\",\n    \"3\",\n    \"4\",\n    \"5\",\n    \"6\",\n    \"7\",\n    \"8\",\n    \"9\",\n    \":\",\n    \";\",\n    \"<\",\n    \"=\",\n    \">\",\n    \"?\",\n    \"@\",\n    \"A\",\n    \"B\",\n    \"C\",\n    \"D\",\n    \"E\",\n    \"F\",\n    \"G\",\n    \"H\",\n    \"I\",\n    \"J\",\n    \"K\",\n    \"L\",\n    \"M\",\n    \"N\",\n    \"O\",\n    \"P\",\n    \"Q\",\n    \"R\",\n    \"S\",\n    \"T\",\n    \"U\",\n    \"V\",\n    \"W\",\n    \"X\",\n    \"Y\",\n    \"Z\",\n    \"[\",\n    \"\\\\\",\n    \"]\",\n    \"^\",\n    \"_\",\n    \"‘\",\n    \"a\",\n    \"b\",\n    \"c\",\n    \"d\",\n    \"e\",\n    \"f\",\n    \"g\",\n    \"h\",\n    \"i\",\n    \"j\",\n    \"k\",\n    \"l\",\n    \"m\",\n    \"n\",\n    \"o\",\n    \"p\",\n    \"q\",\n    \"r\",\n    \"s\",\n    \"t\",\n    \"u\",\n    \"v\",\n    \"w\",\n    \"x\",\n    \"y\",\n    \"z\",\n    \"{\",\n    \"|\",\n    \"}\",\n    \"~\",\n    \"\\x7f\",\n    \"\\x80\",\n    \"\\x81\",\n    \"\\x82\",\n    \"\\x83\",\n    \"\\x84\",\n    \"\\x85\",\n    \"\\x86\",\n    \"\\x87\",\n    \"\\x88\",\n    \"\\x89\",\n    \"\\x8a\",\n    \"\\x8b\",\n    \"\\x8c\",\n    \"\\x8d\",\n    \"\\x8e\",\n    \"\\x8f\",\n    \"\\x90\",\n    \"\\x91\",\n    \"\\x92\",\n    \"\\x93\",\n    \"\\x94\",\n    \"\\x95\",\n    \"\\x96\",\n    \"\\x97\",\n    \"\\x98\",\n    \"\\x99\",\n    \"\\x9a\",\n    \"\\x9b\",\n    \"\\x9c\",\n    \"\\x9d\",\n    \"\\x9e\",\n    \"\\x9f\",\n    \"\\xa0\",\n    \"¡\",\n    \"¢\",\n    \"£\",\n    \"⁄\",\n    \"¥\",\n    \"ƒ\",\n    \"§\",\n    \"¤\",\n    \"'\",\n    \"“\",\n    \"«\",\n    \"‹\",\n    \"›\",\n    \"ﬁ\",\n    \"ﬂ\",\n    \"°\",\n    \"–\",\n    \"†\",\n    \"‡\",\n    \"·\",\n    \"µ\",\n    \"¶\",\n    \"•\",\n    \"‚\",\n    \"„\",\n    \"”\",\n    \"»\",\n    \"…\",\n    \"‰\",\n    \"¾\",\n    \"¿\",\n    \"À\",\n    \"`\",\n    \"´\",\n    \"ˆ\",\n    \"˜\",\n    \"¯\",\n    \"˘\",\n    \"˙\",\n    \"¨\",\n    \"É\",\n    \"˚\",\n    \"¸\",\n    \"Ì\",\n    \"˝\",\n    \"˛\",\n    \"ˇ\",\n    \"—\",\n    \"Ñ\",\n    \"Ò\",\n    \"Ó\",\n    \"Ô\",\n    \"Õ\",\n    \"Ö\",\n    \"×\",\n    \"Ø\",\n    \"Ù\",\n    \"Ú\",\n    \"Û\",\n    \"Ü\",\n    \"Ý\",\n    \"Þ\",\n    \"ß\",\n    \"à\",\n    \"Æ\",\n    \"â\",\n    \"ª\",\n    \"ä\",\n    \"å\",\n    \"æ\",\n    \"ç\",\n    \"Ł\",\n    \"Ø\",\n    \"Œ\",\n    \"º\",\n    \"ì\",\n    \"í\",\n    \"î\",\n    \"ï\",\n    \"ð\",\n    \"æ\",\n    \"ò\",\n    \"ó\",\n    \"ô\",\n    \"ı\",\n    \"ö\",\n    \"÷\",\n    \"ł\",\n    \"ø\",\n    \"œ\",\n    \"ß\",\n    \"ü\",\n    \"ý\",\n    \"þ\",\n    \"ÿ\",\n]\n"
  },
  {
    "path": "pypdf/_codecs/symbol.py",
    "content": "# manually generated from https://www.unicode.org/Public/MAPPINGS/VENDORS/ADOBE/symbol.txt\n_symbol_encoding = [\n    \"\\u0000\",\n    \"\\u0001\",\n    \"\\u0002\",\n    \"\\u0003\",\n    \"\\u0004\",\n    \"\\u0005\",\n    \"\\u0006\",\n    \"\\u0007\",\n    \"\\u0008\",\n    \"\\u0009\",\n    \"\\u000A\",\n    \"\\u000B\",\n    \"\\u000C\",\n    \"\\u000D\",\n    \"\\u000E\",\n    \"\\u000F\",\n    \"\\u0010\",\n    \"\\u0011\",\n    \"\\u0012\",\n    \"\\u0013\",\n    \"\\u0014\",\n    \"\\u0015\",\n    \"\\u0016\",\n    \"\\u0017\",\n    \"\\u0018\",\n    \"\\u0019\",\n    \"\\u001A\",\n    \"\\u001B\",\n    \"\\u001C\",\n    \"\\u001D\",\n    \"\\u001E\",\n    \"\\u001F\",\n    \"\\u0020\",\n    \"\\u0021\",\n    \"\\u2200\",\n    \"\\u0023\",\n    \"\\u2203\",\n    \"\\u0025\",\n    \"\\u0026\",\n    \"\\u220B\",\n    \"\\u0028\",\n    \"\\u0029\",\n    \"\\u2217\",\n    \"\\u002B\",\n    \"\\u002C\",\n    \"\\u2212\",\n    \"\\u002E\",\n    \"\\u002F\",\n    \"\\u0030\",\n    \"\\u0031\",\n    \"\\u0032\",\n    \"\\u0033\",\n    \"\\u0034\",\n    \"\\u0035\",\n    \"\\u0036\",\n    \"\\u0037\",\n    \"\\u0038\",\n    \"\\u0039\",\n    \"\\u003A\",\n    \"\\u003B\",\n    \"\\u003C\",\n    \"\\u003D\",\n    \"\\u003E\",\n    \"\\u003F\",\n    \"\\u2245\",\n    \"\\u0391\",\n    \"\\u0392\",\n    \"\\u03A7\",\n    \"\\u0394\",\n    \"\\u0395\",\n    \"\\u03A6\",\n    \"\\u0393\",\n    \"\\u0397\",\n    \"\\u0399\",\n    \"\\u03D1\",\n    \"\\u039A\",\n    \"\\u039B\",\n    \"\\u039C\",\n    \"\\u039D\",\n    \"\\u039F\",\n    \"\\u03A0\",\n    \"\\u0398\",\n    \"\\u03A1\",\n    \"\\u03A3\",\n    \"\\u03A4\",\n    \"\\u03A5\",\n    \"\\u03C2\",\n    \"\\u03A9\",\n    \"\\u039E\",\n    \"\\u03A8\",\n    \"\\u0396\",\n    \"\\u005B\",\n    \"\\u2234\",\n    \"\\u005D\",\n    \"\\u22A5\",\n    \"\\u005F\",\n    \"\\uF8E5\",\n    \"\\u03B1\",\n    \"\\u03B2\",\n    \"\\u03C7\",\n    \"\\u03B4\",\n    \"\\u03B5\",\n    \"\\u03C6\",\n    \"\\u03B3\",\n    \"\\u03B7\",\n    \"\\u03B9\",\n    \"\\u03D5\",\n    \"\\u03BA\",\n    \"\\u03BB\",\n    \"\\u00B5\",\n    \"\\u03BD\",\n    \"\\u03BF\",\n    \"\\u03C0\",\n    \"\\u03B8\",\n    \"\\u03C1\",\n    \"\\u03C3\",\n    \"\\u03C4\",\n    \"\\u03C5\",\n    \"\\u03D6\",\n    \"\\u03C9\",\n    \"\\u03BE\",\n    \"\\u03C8\",\n    \"\\u03B6\",\n    \"\\u007B\",\n    \"\\u007C\",\n    \"\\u007D\",\n    \"\\u223C\",\n    \"\\u007F\",\n    \"\\u0080\",\n    \"\\u0081\",\n    \"\\u0082\",\n    \"\\u0083\",\n    \"\\u0084\",\n    \"\\u0085\",\n    \"\\u0086\",\n    \"\\u0087\",\n    \"\\u0088\",\n    \"\\u0089\",\n    \"\\u008A\",\n    \"\\u008B\",\n    \"\\u008C\",\n    \"\\u008D\",\n    \"\\u008E\",\n    \"\\u008F\",\n    \"\\u0090\",\n    \"\\u0091\",\n    \"\\u0092\",\n    \"\\u0093\",\n    \"\\u0094\",\n    \"\\u0095\",\n    \"\\u0096\",\n    \"\\u0097\",\n    \"\\u0098\",\n    \"\\u0099\",\n    \"\\u009A\",\n    \"\\u009B\",\n    \"\\u009C\",\n    \"\\u009D\",\n    \"\\u009E\",\n    \"\\u009F\",\n    \"\\u20AC\",\n    \"\\u03D2\",\n    \"\\u2032\",\n    \"\\u2264\",\n    \"\\u2044\",\n    \"\\u221E\",\n    \"\\u0192\",\n    \"\\u2663\",\n    \"\\u2666\",\n    \"\\u2665\",\n    \"\\u2660\",\n    \"\\u2194\",\n    \"\\u2190\",\n    \"\\u2191\",\n    \"\\u2192\",\n    \"\\u2193\",\n    \"\\u00B0\",\n    \"\\u00B1\",\n    \"\\u2033\",\n    \"\\u2265\",\n    \"\\u00D7\",\n    \"\\u221D\",\n    \"\\u2202\",\n    \"\\u2022\",\n    \"\\u00F7\",\n    \"\\u2260\",\n    \"\\u2261\",\n    \"\\u2248\",\n    \"\\u2026\",\n    \"\\uF8E6\",\n    \"\\uF8E7\",\n    \"\\u21B5\",\n    \"\\u2135\",\n    \"\\u2111\",\n    \"\\u211C\",\n    \"\\u2118\",\n    \"\\u2297\",\n    \"\\u2295\",\n    \"\\u2205\",\n    \"\\u2229\",\n    \"\\u222A\",\n    \"\\u2283\",\n    \"\\u2287\",\n    \"\\u2284\",\n    \"\\u2282\",\n    \"\\u2286\",\n    \"\\u2208\",\n    \"\\u2209\",\n    \"\\u2220\",\n    \"\\u2207\",\n    \"\\uF6DA\",\n    \"\\uF6D9\",\n    \"\\uF6DB\",\n    \"\\u220F\",\n    \"\\u221A\",\n    \"\\u22C5\",\n    \"\\u00AC\",\n    \"\\u2227\",\n    \"\\u2228\",\n    \"\\u21D4\",\n    \"\\u21D0\",\n    \"\\u21D1\",\n    \"\\u21D2\",\n    \"\\u21D3\",\n    \"\\u25CA\",\n    \"\\u2329\",\n    \"\\uF8E8\",\n    \"\\uF8E9\",\n    \"\\uF8EA\",\n    \"\\u2211\",\n    \"\\uF8EB\",\n    \"\\uF8EC\",\n    \"\\uF8ED\",\n    \"\\uF8EE\",\n    \"\\uF8EF\",\n    \"\\uF8F0\",\n    \"\\uF8F1\",\n    \"\\uF8F2\",\n    \"\\uF8F3\",\n    \"\\uF8F4\",\n    \"\\u00F0\",\n    \"\\u232A\",\n    \"\\u222B\",\n    \"\\u2320\",\n    \"\\uF8F5\",\n    \"\\u2321\",\n    \"\\uF8F6\",\n    \"\\uF8F7\",\n    \"\\uF8F8\",\n    \"\\uF8F9\",\n    \"\\uF8FA\",\n    \"\\uF8FB\",\n    \"\\uF8FC\",\n    \"\\uF8FD\",\n    \"\\uF8FE\",\n    \"\\u00FF\",\n]\nassert len(_symbol_encoding) == 256\n"
  },
  {
    "path": "pypdf/_codecs/zapfding.py",
    "content": "#  manually generated from https://www.unicode.org/Public/MAPPINGS/VENDORS/ADOBE/zdingbat.txt\n\n_zapfding_encoding = [\n    \"\\u0000\",\n    \"\\u0001\",\n    \"\\u0002\",\n    \"\\u0003\",\n    \"\\u0004\",\n    \"\\u0005\",\n    \"\\u0006\",\n    \"\\u0007\",\n    \"\\u0008\",\n    \"\\u0009\",\n    \"\\u000A\",\n    \"\\u000B\",\n    \"\\u000C\",\n    \"\\u000D\",\n    \"\\u000E\",\n    \"\\u000F\",\n    \"\\u0010\",\n    \"\\u0011\",\n    \"\\u0012\",\n    \"\\u0013\",\n    \"\\u0014\",\n    \"\\u0015\",\n    \"\\u0016\",\n    \"\\u0017\",\n    \"\\u0018\",\n    \"\\u0019\",\n    \"\\u001A\",\n    \"\\u001B\",\n    \"\\u001C\",\n    \"\\u001D\",\n    \"\\u001E\",\n    \"\\u001F\",\n    \"\\u0020\",\n    \"\\u2701\",\n    \"\\u2702\",\n    \"\\u2703\",\n    \"\\u2704\",\n    \"\\u260E\",\n    \"\\u2706\",\n    \"\\u2707\",\n    \"\\u2708\",\n    \"\\u2709\",\n    \"\\u261B\",\n    \"\\u261E\",\n    \"\\u270C\",\n    \"\\u270D\",\n    \"\\u270E\",\n    \"\\u270F\",\n    \"\\u2710\",\n    \"\\u2711\",\n    \"\\u2712\",\n    \"\\u2713\",\n    \"\\u2714\",\n    \"\\u2715\",\n    \"\\u2716\",\n    \"\\u2717\",\n    \"\\u2718\",\n    \"\\u2719\",\n    \"\\u271A\",\n    \"\\u271B\",\n    \"\\u271C\",\n    \"\\u271D\",\n    \"\\u271E\",\n    \"\\u271F\",\n    \"\\u2720\",\n    \"\\u2721\",\n    \"\\u2722\",\n    \"\\u2723\",\n    \"\\u2724\",\n    \"\\u2725\",\n    \"\\u2726\",\n    \"\\u2727\",\n    \"\\u2605\",\n    \"\\u2729\",\n    \"\\u272A\",\n    \"\\u272B\",\n    \"\\u272C\",\n    \"\\u272D\",\n    \"\\u272E\",\n    \"\\u272F\",\n    \"\\u2730\",\n    \"\\u2731\",\n    \"\\u2732\",\n    \"\\u2733\",\n    \"\\u2734\",\n    \"\\u2735\",\n    \"\\u2736\",\n    \"\\u2737\",\n    \"\\u2738\",\n    \"\\u2739\",\n    \"\\u273A\",\n    \"\\u273B\",\n    \"\\u273C\",\n    \"\\u273D\",\n    \"\\u273E\",\n    \"\\u273F\",\n    \"\\u2740\",\n    \"\\u2741\",\n    \"\\u2742\",\n    \"\\u2743\",\n    \"\\u2744\",\n    \"\\u2745\",\n    \"\\u2746\",\n    \"\\u2747\",\n    \"\\u2748\",\n    \"\\u2749\",\n    \"\\u274A\",\n    \"\\u274B\",\n    \"\\u25CF\",\n    \"\\u274D\",\n    \"\\u25A0\",\n    \"\\u274F\",\n    \"\\u2750\",\n    \"\\u2751\",\n    \"\\u2752\",\n    \"\\u25B2\",\n    \"\\u25BC\",\n    \"\\u25C6\",\n    \"\\u2756\",\n    \"\\u25D7\",\n    \"\\u2758\",\n    \"\\u2759\",\n    \"\\u275A\",\n    \"\\u275B\",\n    \"\\u275C\",\n    \"\\u275D\",\n    \"\\u275E\",\n    \"\\u007F\",\n    \"\\uF8D7\",\n    \"\\uF8D8\",\n    \"\\uF8D9\",\n    \"\\uF8DA\",\n    \"\\uF8DB\",\n    \"\\uF8DC\",\n    \"\\uF8DD\",\n    \"\\uF8DE\",\n    \"\\uF8DF\",\n    \"\\uF8E0\",\n    \"\\uF8E1\",\n    \"\\uF8E2\",\n    \"\\uF8E3\",\n    \"\\uF8E4\",\n    \"\\u008E\",\n    \"\\u008F\",\n    \"\\u0090\",\n    \"\\u0091\",\n    \"\\u0092\",\n    \"\\u0093\",\n    \"\\u0094\",\n    \"\\u0095\",\n    \"\\u0096\",\n    \"\\u0097\",\n    \"\\u0098\",\n    \"\\u0099\",\n    \"\\u009A\",\n    \"\\u009B\",\n    \"\\u009C\",\n    \"\\u009D\",\n    \"\\u009E\",\n    \"\\u009F\",\n    \"\\u00A0\",\n    \"\\u2761\",\n    \"\\u2762\",\n    \"\\u2763\",\n    \"\\u2764\",\n    \"\\u2765\",\n    \"\\u2766\",\n    \"\\u2767\",\n    \"\\u2663\",\n    \"\\u2666\",\n    \"\\u2665\",\n    \"\\u2660\",\n    \"\\u2460\",\n    \"\\u2461\",\n    \"\\u2462\",\n    \"\\u2463\",\n    \"\\u2464\",\n    \"\\u2465\",\n    \"\\u2466\",\n    \"\\u2467\",\n    \"\\u2468\",\n    \"\\u2469\",\n    \"\\u2776\",\n    \"\\u2777\",\n    \"\\u2778\",\n    \"\\u2779\",\n    \"\\u277A\",\n    \"\\u277B\",\n    \"\\u277C\",\n    \"\\u277D\",\n    \"\\u277E\",\n    \"\\u277F\",\n    \"\\u2780\",\n    \"\\u2781\",\n    \"\\u2782\",\n    \"\\u2783\",\n    \"\\u2784\",\n    \"\\u2785\",\n    \"\\u2786\",\n    \"\\u2787\",\n    \"\\u2788\",\n    \"\\u2789\",\n    \"\\u278A\",\n    \"\\u278B\",\n    \"\\u278C\",\n    \"\\u278D\",\n    \"\\u278E\",\n    \"\\u278F\",\n    \"\\u2790\",\n    \"\\u2791\",\n    \"\\u2792\",\n    \"\\u2793\",\n    \"\\u2794\",\n    \"\\u2192\",\n    \"\\u2194\",\n    \"\\u2195\",\n    \"\\u2798\",\n    \"\\u2799\",\n    \"\\u279A\",\n    \"\\u279B\",\n    \"\\u279C\",\n    \"\\u279D\",\n    \"\\u279E\",\n    \"\\u279F\",\n    \"\\u27A0\",\n    \"\\u27A1\",\n    \"\\u27A2\",\n    \"\\u27A3\",\n    \"\\u27A4\",\n    \"\\u27A5\",\n    \"\\u27A6\",\n    \"\\u27A7\",\n    \"\\u27A8\",\n    \"\\u27A9\",\n    \"\\u27AA\",\n    \"\\u27AB\",\n    \"\\u27AC\",\n    \"\\u27AD\",\n    \"\\u27AE\",\n    \"\\u27AF\",\n    \"\\u00F0\",\n    \"\\u27B1\",\n    \"\\u27B2\",\n    \"\\u27B3\",\n    \"\\u27B4\",\n    \"\\u27B5\",\n    \"\\u27B6\",\n    \"\\u27B7\",\n    \"\\u27B8\",\n    \"\\u27B9\",\n    \"\\u27BA\",\n    \"\\u27BB\",\n    \"\\u27BC\",\n    \"\\u27BD\",\n    \"\\u27BE\",\n    \"\\u00FF\",\n]\nassert len(_zapfding_encoding) == 256\n"
  },
  {
    "path": "pypdf/_crypt_providers/__init__.py",
    "content": "# Copyright (c) 2023, exiledkingcc\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nfrom pypdf._crypt_providers._base import CryptBase, CryptIdentity\n\ntry:\n    from pypdf._crypt_providers._cryptography import (\n        CryptAES,\n        CryptRC4,\n        aes_cbc_decrypt,\n        aes_cbc_encrypt,\n        aes_ecb_decrypt,\n        aes_ecb_encrypt,\n        crypt_provider,\n        rc4_decrypt,\n        rc4_encrypt,\n    )\n    from pypdf._utils import Version\n\n    if Version(crypt_provider[1]) <= Version(\"3.0\"):\n        # This is due to the backend parameter being required back then:\n        # https://cryptography.io/en/latest/changelog/#v3-1\n        raise ImportError(\"cryptography<=3.0 is not supported\")  # pragma: no cover\nexcept ImportError:\n    try:\n        from pypdf._crypt_providers._pycryptodome import (  # type: ignore\n            CryptAES,\n            CryptRC4,\n            aes_cbc_decrypt,\n            aes_cbc_encrypt,\n            aes_ecb_decrypt,\n            aes_ecb_encrypt,\n            crypt_provider,\n            rc4_decrypt,\n            rc4_encrypt,\n        )\n    except ImportError:\n        from pypdf._crypt_providers._fallback import (  # type: ignore\n            CryptAES,\n            CryptRC4,\n            aes_cbc_decrypt,\n            aes_cbc_encrypt,\n            aes_ecb_decrypt,\n            aes_ecb_encrypt,\n            crypt_provider,\n            rc4_decrypt,\n            rc4_encrypt,\n        )\n\n__all__ = [\n    \"CryptAES\",\n    \"CryptBase\",\n    \"CryptIdentity\",\n    \"CryptRC4\",\n    \"aes_cbc_decrypt\",\n    \"aes_cbc_encrypt\",\n    \"aes_ecb_decrypt\",\n    \"aes_ecb_encrypt\",\n    \"crypt_provider\",\n    \"rc4_decrypt\",\n    \"rc4_encrypt\",\n]\n"
  },
  {
    "path": "pypdf/_crypt_providers/_base.py",
    "content": "# Copyright (c) 2023, exiledkingcc\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\n\nclass CryptBase:\n    def encrypt(self, data: bytes) -> bytes:  # pragma: no cover\n        return data\n\n    def decrypt(self, data: bytes) -> bytes:  # pragma: no cover\n        return data\n\n\nclass CryptIdentity(CryptBase):\n    pass\n"
  },
  {
    "path": "pypdf/_crypt_providers/_cryptography.py",
    "content": "# Copyright (c) 2023, exiledkingcc\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nimport secrets\n\nfrom cryptography import __version__\nfrom cryptography.hazmat.primitives import padding\nfrom cryptography.hazmat.primitives.ciphers.algorithms import AES\n\ntry:\n    # 43.0.0 - https://cryptography.io/en/latest/changelog/#v43-0-0\n    from cryptography.hazmat.decrepit.ciphers.algorithms import ARC4\nexcept ImportError:\n    from cryptography.hazmat.primitives.ciphers.algorithms import ARC4\nfrom cryptography.hazmat.primitives.ciphers.base import Cipher\nfrom cryptography.hazmat.primitives.ciphers.modes import CBC, ECB\n\nfrom pypdf._crypt_providers._base import CryptBase\n\ncrypt_provider = (\"cryptography\", __version__)\n\n\nclass CryptRC4(CryptBase):\n    def __init__(self, key: bytes) -> None:\n        self.cipher = Cipher(ARC4(key), mode=None)\n\n    def encrypt(self, data: bytes) -> bytes:\n        encryptor = self.cipher.encryptor()\n        return encryptor.update(data) + encryptor.finalize()\n\n    def decrypt(self, data: bytes) -> bytes:\n        decryptor = self.cipher.decryptor()\n        return decryptor.update(data) + decryptor.finalize()\n\n\nclass CryptAES(CryptBase):\n    def __init__(self, key: bytes) -> None:\n        self.alg = AES(key)\n\n    def encrypt(self, data: bytes) -> bytes:\n        iv = secrets.token_bytes(16)\n        pad = padding.PKCS7(128).padder()\n        data = pad.update(data) + pad.finalize()\n\n        cipher = Cipher(self.alg, CBC(iv))\n        encryptor = cipher.encryptor()\n        return iv + encryptor.update(data) + encryptor.finalize()\n\n    def decrypt(self, data: bytes) -> bytes:\n        iv = data[:16]\n        data = data[16:]\n        # for empty encrypted data\n        if not data:\n            return data\n\n        # just for robustness, it does not happen under normal circumstances\n        if len(data) % 16 != 0:\n            pad = padding.PKCS7(128).padder()\n            data = pad.update(data) + pad.finalize()\n\n        cipher = Cipher(self.alg, CBC(iv))\n        decryptor = cipher.decryptor()\n        d = decryptor.update(data) + decryptor.finalize()\n        return d[: -d[-1]]\n\n\ndef rc4_encrypt(key: bytes, data: bytes) -> bytes:\n    encryptor = Cipher(ARC4(key), mode=None).encryptor()\n    return encryptor.update(data) + encryptor.finalize()\n\n\ndef rc4_decrypt(key: bytes, data: bytes) -> bytes:\n    decryptor = Cipher(ARC4(key), mode=None).decryptor()\n    return decryptor.update(data) + decryptor.finalize()\n\n\ndef aes_ecb_encrypt(key: bytes, data: bytes) -> bytes:\n    encryptor = Cipher(AES(key), mode=ECB()).encryptor()\n    return encryptor.update(data) + encryptor.finalize()\n\n\ndef aes_ecb_decrypt(key: bytes, data: bytes) -> bytes:\n    decryptor = Cipher(AES(key), mode=ECB()).decryptor()\n    return decryptor.update(data) + decryptor.finalize()\n\n\ndef aes_cbc_encrypt(key: bytes, iv: bytes, data: bytes) -> bytes:\n    encryptor = Cipher(AES(key), mode=CBC(iv)).encryptor()\n    return encryptor.update(data) + encryptor.finalize()\n\n\ndef aes_cbc_decrypt(key: bytes, iv: bytes, data: bytes) -> bytes:\n    decryptor = Cipher(AES(key), mode=CBC(iv)).decryptor()\n    return decryptor.update(data) + decryptor.finalize()\n"
  },
  {
    "path": "pypdf/_crypt_providers/_fallback.py",
    "content": "# Copyright (c) 2023, exiledkingcc\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nfrom pypdf._crypt_providers._base import CryptBase\nfrom pypdf.errors import DependencyError\n\n_DEPENDENCY_ERROR_STR = \"cryptography>=3.1 is required for AES algorithm\"\n\n\ncrypt_provider = (\"local_crypt_fallback\", \"0.0.0\")\n\n\nclass CryptRC4(CryptBase):\n    def __init__(self, key: bytes) -> None:\n        self.s = bytearray(range(256))\n        j = 0\n        for i in range(256):\n            j = (j + self.s[i] + key[i % len(key)]) % 256\n            self.s[i], self.s[j] = self.s[j], self.s[i]\n\n    def encrypt(self, data: bytes) -> bytes:\n        s = bytearray(self.s)\n        out = [0 for _ in range(len(data))]\n        i, j = 0, 0\n        for k in range(len(data)):\n            i = (i + 1) % 256\n            j = (j + s[i]) % 256\n            s[i], s[j] = s[j], s[i]\n            x = s[(s[i] + s[j]) % 256]\n            out[k] = data[k] ^ x\n        return bytes(out)\n\n    def decrypt(self, data: bytes) -> bytes:\n        return self.encrypt(data)\n\n\nclass CryptAES(CryptBase):\n    def __init__(self, key: bytes) -> None:\n        pass\n\n    def encrypt(self, data: bytes) -> bytes:\n        raise DependencyError(_DEPENDENCY_ERROR_STR)\n\n    def decrypt(self, data: bytes) -> bytes:\n        raise DependencyError(_DEPENDENCY_ERROR_STR)\n\n\ndef rc4_encrypt(key: bytes, data: bytes) -> bytes:\n    return CryptRC4(key).encrypt(data)\n\n\ndef rc4_decrypt(key: bytes, data: bytes) -> bytes:\n    return CryptRC4(key).decrypt(data)\n\n\ndef aes_ecb_encrypt(key: bytes, data: bytes) -> bytes:\n    raise DependencyError(_DEPENDENCY_ERROR_STR)\n\n\ndef aes_ecb_decrypt(key: bytes, data: bytes) -> bytes:\n    raise DependencyError(_DEPENDENCY_ERROR_STR)\n\n\ndef aes_cbc_encrypt(key: bytes, iv: bytes, data: bytes) -> bytes:\n    raise DependencyError(_DEPENDENCY_ERROR_STR)\n\n\ndef aes_cbc_decrypt(key: bytes, iv: bytes, data: bytes) -> bytes:\n    raise DependencyError(_DEPENDENCY_ERROR_STR)\n"
  },
  {
    "path": "pypdf/_crypt_providers/_pycryptodome.py",
    "content": "# Copyright (c) 2023, exiledkingcc\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nimport secrets\n\nfrom Crypto import __version__\nfrom Crypto.Cipher import AES, ARC4\nfrom Crypto.Util.Padding import pad\n\nfrom pypdf._crypt_providers._base import CryptBase\n\ncrypt_provider = (\"pycryptodome\", __version__)\n\n\nclass CryptRC4(CryptBase):\n    def __init__(self, key: bytes) -> None:\n        self.key = key\n\n    def encrypt(self, data: bytes) -> bytes:\n        return ARC4.ARC4Cipher(self.key).encrypt(data)\n\n    def decrypt(self, data: bytes) -> bytes:\n        return ARC4.ARC4Cipher(self.key).decrypt(data)\n\n\nclass CryptAES(CryptBase):\n    def __init__(self, key: bytes) -> None:\n        self.key = key\n\n    def encrypt(self, data: bytes) -> bytes:\n        iv = secrets.token_bytes(16)\n        data = pad(data, 16)\n        aes = AES.new(self.key, AES.MODE_CBC, iv)\n        return iv + aes.encrypt(data)\n\n    def decrypt(self, data: bytes) -> bytes:\n        iv = data[:16]\n        data = data[16:]\n        # for empty encrypted data\n        if not data:\n            return data\n\n        # just for robustness, it does not happen under normal circumstances\n        if len(data) % 16 != 0:\n            data = pad(data, 16)\n\n        aes = AES.new(self.key, AES.MODE_CBC, iv)\n        d = aes.decrypt(data)\n        return d[: -d[-1]]\n\n\ndef rc4_encrypt(key: bytes, data: bytes) -> bytes:\n    return ARC4.ARC4Cipher(key).encrypt(data)\n\n\ndef rc4_decrypt(key: bytes, data: bytes) -> bytes:\n    return ARC4.ARC4Cipher(key).decrypt(data)\n\n\ndef aes_ecb_encrypt(key: bytes, data: bytes) -> bytes:\n    return AES.new(key, AES.MODE_ECB).encrypt(data)\n\n\ndef aes_ecb_decrypt(key: bytes, data: bytes) -> bytes:\n    return AES.new(key, AES.MODE_ECB).decrypt(data)\n\n\ndef aes_cbc_encrypt(key: bytes, iv: bytes, data: bytes) -> bytes:\n    return AES.new(key, AES.MODE_CBC, iv).encrypt(data)\n\n\ndef aes_cbc_decrypt(key: bytes, iv: bytes, data: bytes) -> bytes:\n    return AES.new(key, AES.MODE_CBC, iv).decrypt(data)\n"
  },
  {
    "path": "pypdf/_doc_common.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com>\n# Copyright (c) 2024, Pubpub-ZZ\n#\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nimport struct\nfrom abc import abstractmethod\nfrom collections.abc import Generator, Iterable, Iterator, Mapping\nfrom datetime import datetime\nfrom typing import (\n    Any,\n    Optional,\n    Union,\n    cast,\n)\n\nfrom ._encryption import Encryption\nfrom ._page import PageObject, _VirtualList\nfrom ._page_labels import index2label as page_index2page_label\nfrom ._utils import (\n    deprecation_with_replacement,\n    logger_warning,\n    parse_iso8824_date,\n)\nfrom .constants import CatalogAttributes as CA\nfrom .constants import CatalogDictionary as CD\nfrom .constants import (\n    CheckboxRadioButtonAttributes,\n    GoToActionArguments,\n    PagesAttributes,\n    UserAccessPermissions,\n)\nfrom .constants import Core as CO\nfrom .constants import DocumentInformationAttributes as DI\nfrom .constants import FieldDictionaryAttributes as FA\nfrom .constants import PageAttributes as PG\nfrom .errors import PdfReadError, PyPdfError\nfrom .filters import _decompress_with_limit\nfrom .generic import (\n    ArrayObject,\n    BooleanObject,\n    ByteStringObject,\n    Destination,\n    DictionaryObject,\n    EncodedStreamObject,\n    Field,\n    Fit,\n    FloatObject,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    PdfObject,\n    TextStringObject,\n    TreeObject,\n    ViewerPreferences,\n    create_string_object,\n    is_null_or_none,\n)\nfrom .generic._files import EmbeddedFile\nfrom .types import OutlineType, PagemodeType\nfrom .xmp import XmpInformation\n\n\ndef convert_to_int(d: bytes, size: int) -> Union[int, tuple[Any, ...]]:\n    if size > 8:\n        raise PdfReadError(\"Invalid size in convert_to_int\")\n    d = b\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\" + d\n    d = d[-8:]\n    return struct.unpack(\">q\", d)[0]\n\n\nclass DocumentInformation(DictionaryObject):\n    \"\"\"\n    A class representing the basic document metadata provided in a PDF File.\n    This class is accessible through\n    :py:class:`PdfReader.metadata<pypdf.PdfReader.metadata>`.\n\n    All text properties of the document metadata have\n    *two* properties, e.g. author and author_raw. The non-raw property will\n    always return a ``TextStringObject``, making it ideal for a case where the\n    metadata is being displayed. The raw property can sometimes return a\n    ``ByteStringObject``, if pypdf was unable to decode the string's text\n    encoding; this requires additional safety in the caller and therefore is not\n    as commonly accessed.\n    \"\"\"\n\n    def __init__(self) -> None:\n        DictionaryObject.__init__(self)\n\n    def _get_text(self, key: str) -> Optional[str]:\n        retval = self.get(key, None)\n        if isinstance(retval, TextStringObject):\n            return retval\n        if isinstance(retval, ByteStringObject):\n            return str(retval)\n        return None\n\n    @property\n    def title(self) -> Optional[str]:\n        \"\"\"\n        Read-only property accessing the document's title.\n\n        Returns a ``TextStringObject`` or ``None`` if the title is not\n        specified.\n        \"\"\"\n        return (\n            self._get_text(DI.TITLE) or self.get(DI.TITLE).get_object()  # type: ignore\n            if self.get(DI.TITLE)\n            else None\n        )\n\n    @property\n    def title_raw(self) -> Optional[str]:\n        \"\"\"The \"raw\" version of title; can return a ``ByteStringObject``.\"\"\"\n        return self.get(DI.TITLE)\n\n    @property\n    def author(self) -> Optional[str]:\n        \"\"\"\n        Read-only property accessing the document's author.\n\n        Returns a ``TextStringObject`` or ``None`` if the author is not\n        specified.\n        \"\"\"\n        return self._get_text(DI.AUTHOR)\n\n    @property\n    def author_raw(self) -> Optional[str]:\n        \"\"\"The \"raw\" version of author; can return a ``ByteStringObject``.\"\"\"\n        return self.get(DI.AUTHOR)\n\n    @property\n    def subject(self) -> Optional[str]:\n        \"\"\"\n        Read-only property accessing the document's subject.\n\n        Returns a ``TextStringObject`` or ``None`` if the subject is not\n        specified.\n        \"\"\"\n        return self._get_text(DI.SUBJECT)\n\n    @property\n    def subject_raw(self) -> Optional[str]:\n        \"\"\"The \"raw\" version of subject; can return a ``ByteStringObject``.\"\"\"\n        return self.get(DI.SUBJECT)\n\n    @property\n    def creator(self) -> Optional[str]:\n        \"\"\"\n        Read-only property accessing the document's creator.\n\n        If the document was converted to PDF from another format, this is the\n        name of the application (e.g. OpenOffice) that created the original\n        document from which it was converted. Returns a ``TextStringObject`` or\n        ``None`` if the creator is not specified.\n        \"\"\"\n        return self._get_text(DI.CREATOR)\n\n    @property\n    def creator_raw(self) -> Optional[str]:\n        \"\"\"The \"raw\" version of creator; can return a ``ByteStringObject``.\"\"\"\n        return self.get(DI.CREATOR)\n\n    @property\n    def producer(self) -> Optional[str]:\n        \"\"\"\n        Read-only property accessing the document's producer.\n\n        If the document was converted to PDF from another format, this is the\n        name of the application (for example, macOS Quartz) that converted it to\n        PDF. Returns a ``TextStringObject`` or ``None`` if the producer is not\n        specified.\n        \"\"\"\n        return self._get_text(DI.PRODUCER)\n\n    @property\n    def producer_raw(self) -> Optional[str]:\n        \"\"\"The \"raw\" version of producer; can return a ``ByteStringObject``.\"\"\"\n        return self.get(DI.PRODUCER)\n\n    @property\n    def creation_date(self) -> Optional[datetime]:\n        \"\"\"Read-only property accessing the document's creation date.\"\"\"\n        return parse_iso8824_date(self._get_text(DI.CREATION_DATE))\n\n    @property\n    def creation_date_raw(self) -> Optional[str]:\n        \"\"\"\n        The \"raw\" version of creation date; can return a ``ByteStringObject``.\n\n        Typically in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix\n        is the offset from UTC.\n        \"\"\"\n        return self.get(DI.CREATION_DATE)\n\n    @property\n    def modification_date(self) -> Optional[datetime]:\n        \"\"\"\n        Read-only property accessing the document's modification date.\n\n        The date and time the document was most recently modified.\n        \"\"\"\n        return parse_iso8824_date(self._get_text(DI.MOD_DATE))\n\n    @property\n    def modification_date_raw(self) -> Optional[str]:\n        \"\"\"\n        The \"raw\" version of modification date; can return a\n        ``ByteStringObject``.\n\n        Typically in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix\n        is the offset from UTC.\n        \"\"\"\n        return self.get(DI.MOD_DATE)\n\n    @property\n    def keywords(self) -> Optional[str]:\n        \"\"\"\n        Read-only property accessing the document's keywords.\n\n        Returns a ``TextStringObject`` or ``None`` if keywords are not\n        specified.\n        \"\"\"\n        return self._get_text(DI.KEYWORDS)\n\n    @property\n    def keywords_raw(self) -> Optional[str]:\n        \"\"\"The \"raw\" version of keywords; can return a ``ByteStringObject``.\"\"\"\n        return self.get(DI.KEYWORDS)\n\n\nclass PdfDocCommon:\n    \"\"\"\n    Common functions from PdfWriter and PdfReader objects.\n\n    This root class is strongly abstracted.\n    \"\"\"\n\n    strict: bool = False  # default\n\n    flattened_pages: Optional[list[PageObject]] = None\n\n    _encryption: Optional[Encryption] = None\n\n    _readonly: bool = False\n\n    @property\n    @abstractmethod\n    def root_object(self) -> DictionaryObject:\n        ...  # pragma: no cover\n\n    @property\n    @abstractmethod\n    def pdf_header(self) -> str:\n        ...  # pragma: no cover\n\n    @abstractmethod\n    def get_object(\n        self, indirect_reference: Union[int, IndirectObject]\n    ) -> Optional[PdfObject]:\n        ...  # pragma: no cover\n\n    @abstractmethod\n    def _replace_object(self, indirect: IndirectObject, obj: PdfObject) -> PdfObject:\n        ...  # pragma: no cover\n\n    @property\n    @abstractmethod\n    def _info(self) -> Optional[DictionaryObject]:\n        ...  # pragma: no cover\n\n    @property\n    def metadata(self) -> Optional[DocumentInformation]:\n        \"\"\"\n        Retrieve the PDF file's document information dictionary, if it exists.\n\n        Note that some PDF files use metadata streams instead of document\n        information dictionaries, and these metadata streams will not be\n        accessed by this function.\n        \"\"\"\n        retval = DocumentInformation()\n        if self._info is None:\n            return None\n        retval.update(self._info)\n        return retval\n\n    @property\n    def xmp_metadata(self) -> Optional[XmpInformation]:\n        ...  # pragma: no cover\n\n    @property\n    def viewer_preferences(self) -> Optional[ViewerPreferences]:\n        \"\"\"Returns the existing ViewerPreferences as an overloaded dictionary.\"\"\"\n        o = self.root_object.get(CD.VIEWER_PREFERENCES, None)\n        if o is None:\n            return None\n        o = o.get_object()\n        if not isinstance(o, ViewerPreferences):\n            o = ViewerPreferences(o)\n            if hasattr(o, \"indirect_reference\") and o.indirect_reference is not None:\n                self._replace_object(o.indirect_reference, o)\n            else:\n                self.root_object[NameObject(CD.VIEWER_PREFERENCES)] = o\n        return o\n\n    def get_num_pages(self) -> int:\n        \"\"\"\n        Calculate the number of pages in this PDF file.\n\n        Returns:\n            The number of pages of the parsed PDF file.\n\n        Raises:\n            PdfReadError: If restrictions prevent this action.\n\n        \"\"\"\n        # Flattened pages will not work on an encrypted PDF;\n        # the PDF file's page count is used in this case. Otherwise,\n        # the original method (flattened page count) is used.\n        if self.is_encrypted:\n            return self.root_object[\"/Pages\"][\"/Count\"]  # type: ignore\n        if self.flattened_pages is None:\n            self._flatten(self._readonly)\n        assert self.flattened_pages is not None\n        return len(self.flattened_pages)\n\n    def get_page(self, page_number: int) -> PageObject:\n        \"\"\"\n        Retrieve a page by number from this PDF file.\n        Most of the time ``.pages[page_number]`` is preferred.\n\n        Args:\n            page_number: The page number to retrieve\n                (pages begin at zero)\n\n        Returns:\n            A :class:`PageObject<pypdf._page.PageObject>` instance.\n\n        \"\"\"\n        if self.flattened_pages is None:\n            self._flatten(self._readonly)\n        assert self.flattened_pages is not None, \"hint for mypy\"\n        return self.flattened_pages[page_number]\n\n    def _get_page_in_node(\n        self,\n        page_number: int,\n    ) -> tuple[DictionaryObject, int]:\n        \"\"\"\n        Retrieve the node and position within the /Kids containing the page.\n        If page_number is greater than the number of pages, it returns the top node, -1.\n        \"\"\"\n        top = cast(DictionaryObject, self.root_object[\"/Pages\"])\n\n        def recursive_call(\n            node: DictionaryObject, mi: int\n        ) -> tuple[Optional[PdfObject], int]:\n            ma = cast(int, node.get(\"/Count\", 1))  # default 1 for /Page types\n            if node[\"/Type\"] == \"/Page\":\n                if page_number == mi:\n                    return node, -1\n                return None, mi + 1\n            if (page_number - mi) >= ma:  # not in nodes below\n                if node == top:\n                    return top, -1\n                return None, mi + ma\n            for idx, kid in enumerate(cast(ArrayObject, node[\"/Kids\"])):\n                kid = cast(DictionaryObject, kid.get_object())\n                n, i = recursive_call(kid, mi)\n                if n is not None:  # page has just been found ...\n                    if i < 0:  # ... just below!\n                        return node, idx\n                    # ... at lower levels\n                    return n, i\n                mi = i\n            raise PyPdfError(\"Unexpectedly cannot find the node.\")\n\n        node, idx = recursive_call(top, 0)\n        assert isinstance(node, DictionaryObject), \"mypy\"\n        return node, idx\n\n    @property\n    def named_destinations(self) -> dict[str, Destination]:\n        \"\"\"A read-only dictionary which maps names to destinations.\"\"\"\n        return self._get_named_destinations()\n\n    def get_named_dest_root(self) -> ArrayObject:\n        named_dest = ArrayObject()\n        if CA.NAMES in self.root_object and isinstance(\n            self.root_object[CA.NAMES], DictionaryObject\n        ):\n            names = cast(DictionaryObject, self.root_object[CA.NAMES])\n            if CA.DESTS in names and isinstance(names[CA.DESTS], DictionaryObject):\n                # §3.6.3 Name Dictionary (PDF spec 1.7)\n                dests = cast(DictionaryObject, names[CA.DESTS])\n                dests_ref = dests.indirect_reference\n                if CA.NAMES in dests:\n                    # §7.9.6, entries in a name tree node dictionary\n                    named_dest = cast(ArrayObject, dests[CA.NAMES])\n                else:\n                    named_dest = ArrayObject()\n                    dests[NameObject(CA.NAMES)] = named_dest\n            elif hasattr(self, \"_add_object\"):\n                dests = DictionaryObject()\n                dests_ref = self._add_object(dests)\n                names[NameObject(CA.DESTS)] = dests_ref\n                dests[NameObject(CA.NAMES)] = named_dest\n\n        elif hasattr(self, \"_add_object\"):\n            names = DictionaryObject()\n            names_ref = self._add_object(names)\n            self.root_object[NameObject(CA.NAMES)] = names_ref\n            dests = DictionaryObject()\n            dests_ref = self._add_object(dests)\n            names[NameObject(CA.DESTS)] = dests_ref\n            dests[NameObject(CA.NAMES)] = named_dest\n\n        return named_dest\n\n    ## common\n    def _get_named_destinations(\n        self,\n        tree: Union[TreeObject, None] = None,\n        retval: Optional[dict[str, Destination]] = None,\n    ) -> dict[str, Destination]:\n        \"\"\"\n        Retrieve the named destinations present in the document.\n\n        Args:\n            tree: The current tree.\n            retval: The previously retrieved destinations for nested calls.\n\n        Returns:\n            A dictionary which maps names to destinations.\n\n        \"\"\"\n        if retval is None:\n            retval = {}\n            catalog = self.root_object\n\n            # get the name tree\n            if CA.DESTS in catalog:\n                tree = cast(TreeObject, catalog[CA.DESTS])\n            elif CA.NAMES in catalog:\n                names = cast(DictionaryObject, catalog[CA.NAMES])\n                if CA.DESTS in names:\n                    tree = cast(TreeObject, names[CA.DESTS])\n\n        if is_null_or_none(tree):\n            return retval\n        assert tree is not None, \"mypy\"\n\n        if PagesAttributes.KIDS in tree:\n            # recurse down the tree\n            for kid in cast(ArrayObject, tree[PagesAttributes.KIDS]):\n                self._get_named_destinations(kid.get_object(), retval)\n        # §7.9.6, entries in a name tree node dictionary\n        elif CA.NAMES in tree:  # /Kids and /Names are exclusives (§7.9.6)\n            names = cast(DictionaryObject, tree[CA.NAMES])\n            i = 0\n            while i < len(names):\n                key = names[i].get_object()\n                i += 1\n                if not isinstance(key, (bytes, str)):\n                    continue\n                try:\n                    value = names[i].get_object()\n                except IndexError:\n                    break\n                i += 1\n                if isinstance(value, DictionaryObject):\n                    if \"/D\" in value:\n                        value = value[\"/D\"]\n                    else:\n                        continue\n                dest = self._build_destination(key, value)\n                if dest is not None:\n                    retval[cast(str, dest[\"/Title\"])] = dest\n                    # Remain backwards-compatible.\n                    retval[str(key)] = dest\n        else:  # case where Dests is in root catalog (PDF 1.7 specs, §2 about PDF 1.1)\n            for k__, v__ in tree.items():\n                val = v__.get_object()\n                if isinstance(val, DictionaryObject):\n                    if \"/D\" in val:\n                        val = val[\"/D\"].get_object()\n                    else:\n                        continue\n                dest = self._build_destination(k__, val)\n                if dest is not None:\n                    retval[k__] = dest\n        return retval\n\n    # A select group of relevant field attributes. For the complete list,\n    # see §12.3.2 of the PDF 1.7 or PDF 2.0 specification.\n\n    def get_fields(\n        self,\n        tree: Optional[TreeObject] = None,\n        retval: Optional[dict[Any, Any]] = None,\n        fileobj: Optional[Any] = None,\n        stack: Optional[list[PdfObject]] = None,\n    ) -> Optional[dict[str, Any]]:\n        \"\"\"\n        Extract field data if this PDF contains interactive form fields.\n\n        The *tree*, *retval*, *stack* parameters are for recursive use.\n\n        Args:\n            tree: Current object to parse.\n            retval: In-progress list of fields.\n            fileobj: A file object (usually a text file) to write\n                a report to on all interactive form fields found.\n            stack: List of already parsed objects.\n\n        Returns:\n            A dictionary where each key is a field name, and each\n            value is a :class:`Field<pypdf.generic.Field>` object. By\n            default, the mapping name is used for keys.\n            ``None`` if form data could not be located.\n\n        \"\"\"\n        field_attributes = FA.attributes_dict()\n        field_attributes.update(CheckboxRadioButtonAttributes.attributes_dict())\n        if retval is None:\n            retval = {}\n            catalog = self.root_object\n            stack = []\n            # get the AcroForm tree\n            if CD.ACRO_FORM in catalog:\n                tree = cast(Optional[TreeObject], catalog[CD.ACRO_FORM])\n            else:\n                return None\n        if tree is None:\n            return retval\n        assert stack is not None\n        if \"/Fields\" in tree:\n            fields = cast(ArrayObject, tree[\"/Fields\"])\n            for f in fields:\n                field = f.get_object()\n                self._build_field(field, retval, fileobj, field_attributes, stack)\n        elif any(attr in tree for attr in field_attributes):\n            # Tree is a field\n            self._build_field(tree, retval, fileobj, field_attributes, stack)\n        return retval\n\n    def _get_qualified_field_name(self, parent: DictionaryObject) -> str:\n        if \"/TM\" in parent:\n            return cast(str, parent[\"/TM\"])\n        if \"/Parent\" in parent:\n            return (\n                self._get_qualified_field_name(\n                    cast(DictionaryObject, parent[\"/Parent\"])\n                )\n                + \".\"\n                + cast(str, parent.get(\"/T\", \"\"))\n            )\n        return cast(str, parent.get(\"/T\", \"\"))\n\n    def _build_field(\n        self,\n        field: Union[TreeObject, DictionaryObject],\n        retval: dict[Any, Any],\n        fileobj: Any,\n        field_attributes: Any,\n        stack: list[PdfObject],\n    ) -> None:\n        if all(attr not in field for attr in (\"/T\", \"/TM\")):\n            return\n        key = self._get_qualified_field_name(field)\n        if fileobj:\n            self._write_field(fileobj, field, field_attributes)\n            fileobj.write(\"\\n\")\n        retval[key] = Field(field)\n        obj = retval[key].indirect_reference.get_object()  # to get the full object\n        if obj.get(FA.FT, \"\") == \"/Ch\" and obj.get(NameObject(FA.Opt)):\n            retval[key][NameObject(\"/_States_\")] = obj[NameObject(FA.Opt)]\n        if obj.get(FA.FT, \"\") == \"/Btn\" and \"/AP\" in obj:\n            #  Checkbox\n            retval[key][NameObject(\"/_States_\")] = ArrayObject(\n                list(obj[\"/AP\"][\"/N\"].keys())\n            )\n            if \"/Off\" not in retval[key][\"/_States_\"]:\n                retval[key][NameObject(\"/_States_\")].append(NameObject(\"/Off\"))\n        elif obj.get(FA.FT, \"\") == \"/Btn\" and obj.get(FA.Ff, 0) & FA.FfBits.Radio != 0:\n            states: list[str] = []\n            retval[key][NameObject(\"/_States_\")] = ArrayObject(states)\n            for k in obj.get(FA.Kids, {}):\n                k = k.get_object()\n                for s in list(k[\"/AP\"][\"/N\"].keys()):\n                    if s not in states:\n                        states.append(s)\n                retval[key][NameObject(\"/_States_\")] = ArrayObject(states)\n            if (\n                obj.get(FA.Ff, 0) & FA.FfBits.NoToggleToOff != 0\n                and \"/Off\" in retval[key][\"/_States_\"]\n            ):\n                del retval[key][\"/_States_\"][retval[key][\"/_States_\"].index(\"/Off\")]\n        # at last for order\n        self._check_kids(field, retval, fileobj, stack)\n\n    def _check_kids(\n        self,\n        tree: Union[TreeObject, DictionaryObject],\n        retval: Any,\n        fileobj: Any,\n        stack: list[PdfObject],\n    ) -> None:\n        if tree in stack:\n            logger_warning(\n                f\"{self._get_qualified_field_name(tree)} already parsed\", __name__\n            )\n            return\n        stack.append(tree)\n        if PagesAttributes.KIDS in tree:\n            # recurse down the tree\n            for kid in tree[PagesAttributes.KIDS]:  # type: ignore\n                kid = kid.get_object()\n                self.get_fields(kid, retval, fileobj, stack)\n\n    def _write_field(self, fileobj: Any, field: Any, field_attributes: Any) -> None:\n        field_attributes_tuple = FA.attributes()\n        field_attributes_tuple = (\n            field_attributes_tuple + CheckboxRadioButtonAttributes.attributes()\n        )\n\n        for attr in field_attributes_tuple:\n            if attr in (\n                FA.Kids,\n                FA.AA,\n            ):\n                continue\n            attr_name = field_attributes[attr]\n            try:\n                if attr == FA.FT:\n                    # Make the field type value clearer\n                    types = {\n                        \"/Btn\": \"Button\",\n                        \"/Tx\": \"Text\",\n                        \"/Ch\": \"Choice\",\n                        \"/Sig\": \"Signature\",\n                    }\n                    if field[attr] in types:\n                        fileobj.write(f\"{attr_name}: {types[field[attr]]}\\n\")\n                elif attr == FA.Parent:\n                    # Let's just write the name of the parent\n                    try:\n                        name = field[attr][FA.TM]\n                    except KeyError:\n                        name = field[attr][FA.T]\n                    fileobj.write(f\"{attr_name}: {name}\\n\")\n                else:\n                    fileobj.write(f\"{attr_name}: {field[attr]}\\n\")\n            except KeyError:\n                # Field attribute is N/A or unknown, so don't write anything\n                pass\n\n    def get_form_text_fields(self, full_qualified_name: bool = False) -> dict[str, Any]:\n        \"\"\"\n        Retrieve form fields from the document with textual data.\n\n        Args:\n            full_qualified_name: to get full name\n\n        Returns:\n            A dictionary. The key is the name of the form field,\n            the value is the content of the field.\n\n            If the document contains multiple form fields with the same name, the\n            second and following will get the suffix .2, .3, ...\n\n        \"\"\"\n\n        def indexed_key(k: str, fields: dict[Any, Any]) -> str:\n            if k not in fields:\n                return k\n            return (\n                k\n                + \".\"\n                + str(sum(1 for kk in fields if kk.startswith(k + \".\")) + 2)\n            )\n\n        # Retrieve document form fields\n        formfields = self.get_fields()\n        if formfields is None:\n            return {}\n        ff = {}\n        for field, value in formfields.items():\n            if value.get(\"/FT\") == \"/Tx\":\n                if full_qualified_name:\n                    ff[field] = value.get(\"/V\")\n                else:\n                    ff[indexed_key(cast(str, value[\"/T\"]), ff)] = value.get(\"/V\")\n        return ff\n\n    def get_pages_showing_field(\n        self, field: Union[Field, PdfObject, IndirectObject]\n    ) -> list[PageObject]:\n        \"\"\"\n        Provides list of pages where the field is called.\n\n        Args:\n            field: Field Object, PdfObject or IndirectObject referencing a Field\n\n        Returns:\n            List of pages:\n                - Empty list:\n                    The field has no widgets attached\n                    (either hidden field or ancestor field).\n                - Single page list:\n                    Page where the widget is present\n                    (most common).\n                - Multi-page list:\n                    Field with multiple kids widgets\n                    (example: radio buttons, field repeated on multiple pages).\n\n        \"\"\"\n\n        def _get_inherited(obj: DictionaryObject, key: str) -> Any:\n            if key in obj:\n                return obj[key]\n            if \"/Parent\" in obj:\n                return _get_inherited(\n                    cast(DictionaryObject, obj[\"/Parent\"].get_object()), key\n                )\n            return None\n\n        try:\n            # to cope with all types\n            field = cast(DictionaryObject, field.indirect_reference.get_object())  # type: ignore\n        except Exception as exc:\n            raise ValueError(\"Field type is invalid\") from exc\n        if is_null_or_none(_get_inherited(field, \"/FT\")):\n            raise ValueError(\"Field is not valid\")\n        ret = []\n        if field.get(\"/Subtype\", \"\") == \"/Widget\":\n            if \"/P\" in field:\n                ret = [field[\"/P\"].get_object()]\n            else:\n                ret = [\n                    p\n                    for p in self.pages\n                    if field.indirect_reference in p.get(\"/Annots\", \"\")\n                ]\n        else:\n            kids = field.get(\"/Kids\", ())\n            for k in kids:\n                k = k.get_object()\n                if (k.get(\"/Subtype\", \"\") == \"/Widget\") and (\"/T\" not in k):\n                    # Kid that is just a widget, not a field:\n                    if \"/P\" in k:\n                        ret += [k[\"/P\"].get_object()]\n                    else:\n                        ret += [\n                            p\n                            for p in self.pages\n                            if k.indirect_reference in p.get(\"/Annots\", \"\")\n                        ]\n        return [\n            x\n            if isinstance(x, PageObject)\n            else (self.pages[self._get_page_number_by_indirect(x.indirect_reference)])  # type: ignore\n            for x in ret\n        ]\n\n    @property\n    def open_destination(\n        self,\n    ) -> Union[None, Destination, TextStringObject, ByteStringObject]:\n        \"\"\"\n        Property to access the opening destination (``/OpenAction`` entry in\n        the PDF catalog). It returns ``None`` if the entry does not exist\n        or is not set.\n\n        Raises:\n            Exception: If a destination is invalid.\n\n        \"\"\"\n        if \"/OpenAction\" not in self.root_object:\n            return None\n        oa: Any = self.root_object[\"/OpenAction\"]\n        if isinstance(oa, bytes):  # pragma: no cover\n            oa = oa.decode()\n        if isinstance(oa, str):\n            return create_string_object(oa)\n        if isinstance(oa, ArrayObject):\n            try:\n                page, typ, *array = oa\n                fit = Fit(typ, tuple(array))\n                return Destination(\"OpenAction\", page, fit)\n            except Exception as exc:\n                raise Exception(f\"Invalid Destination {oa}: {exc}\")\n        else:\n            return None\n\n    @open_destination.setter\n    def open_destination(self, dest: Union[None, str, Destination, PageObject]) -> None:\n        raise NotImplementedError(\"No setter for open_destination\")\n\n    @property\n    def outline(self) -> OutlineType:\n        \"\"\"\n        Read-only property for the outline present in the document\n        (i.e., a collection of 'outline items' which are also known as\n        'bookmarks').\n        \"\"\"\n        return self._get_outline()\n\n    def _get_outline(\n        self,\n        node: Optional[DictionaryObject] = None,\n        outline: Optional[Any] = None,\n        visited: Optional[set[int]] = None,\n    ) -> OutlineType:\n        if outline is None:\n            outline = []\n            catalog = self.root_object\n\n            # get the outline dictionary and named destinations\n            if CO.OUTLINES in catalog:\n                lines = cast(DictionaryObject, catalog[CO.OUTLINES])\n\n                if isinstance(lines, NullObject):\n                    return outline\n\n                # §12.3.3 Document outline, entries in the outline dictionary\n                if not is_null_or_none(lines) and \"/First\" in lines:\n                    node = cast(DictionaryObject, lines[\"/First\"])\n            self._named_destinations = self._get_named_destinations()\n\n        if node is None:\n            return outline\n\n        # see if there are any more outline items\n        if visited is None:\n            visited = set()\n        while True:\n            node_id = id(node)\n            if node_id in visited:\n                logger_warning(f\"Detected cycle in outline structure for {node}\", __name__)\n                break\n            visited.add(node_id)\n\n            outline_obj = self._build_outline_item(node)\n            if outline_obj:\n                outline.append(outline_obj)\n\n            # check for sub-outline\n            if \"/First\" in node:\n                sub_outline: list[Any] = []\n                # Pass a copy to allow multiple outer entries to reference the same inner one.\n                inner_visited = visited.copy()\n                self._get_outline(\n                    node=cast(DictionaryObject, node[\"/First\"]),\n                    outline=sub_outline,\n                    visited=inner_visited,\n                )\n                if sub_outline:\n                    outline.append(sub_outline)\n\n            if \"/Next\" not in node:\n                break\n            node = cast(DictionaryObject, node[\"/Next\"])\n\n        return outline\n\n    @property\n    def threads(self) -> Optional[ArrayObject]:\n        \"\"\"\n        Read-only property for the list of threads.\n\n        See §12.4.3 from the PDF 1.7 or 2.0 specification.\n\n        It is an array of dictionaries with \"/F\" (the first bead in the thread)\n        and \"/I\" (a thread information dictionary containing information about\n        the thread, such as its title, author, and creation date) properties or\n        None if there are no articles.\n\n        Since PDF 2.0 it can also contain an indirect reference to a metadata\n        stream containing information about the thread, such as its title,\n        author, and creation date.\n        \"\"\"\n        catalog = self.root_object\n        if CO.THREADS in catalog:\n            return cast(\"ArrayObject\", catalog[CO.THREADS])\n        return None\n\n    @abstractmethod\n    def _get_page_number_by_indirect(\n        self, indirect_reference: Union[None, int, NullObject, IndirectObject]\n    ) -> Optional[int]:\n        ...  # pragma: no cover\n\n    def get_page_number(self, page: PageObject) -> Optional[int]:\n        \"\"\"\n        Retrieve page number of a given PageObject.\n\n        Args:\n            page: The page to get page number. Should be\n                an instance of :class:`PageObject<pypdf._page.PageObject>`\n\n        Returns:\n            The page number or None if page is not found\n\n        \"\"\"\n        return self._get_page_number_by_indirect(page.indirect_reference)\n\n    def get_destination_page_number(self, destination: Destination) -> Optional[int]:\n        \"\"\"\n        Retrieve page number of a given Destination object.\n\n        Args:\n            destination: The destination to get page number.\n\n        Returns:\n            The page number or None if page is not found\n\n        \"\"\"\n        return self._get_page_number_by_indirect(destination.page)\n\n    def _build_destination(\n        self,\n        title: Union[str, bytes],\n        array: Optional[\n            list[\n                Union[NumberObject, IndirectObject, None, NullObject, DictionaryObject]\n            ]\n        ],\n    ) -> Destination:\n        page, typ = None, None\n        # handle outline items with missing or invalid destination\n        if (\n            isinstance(array, (NullObject, str))\n            or (isinstance(array, ArrayObject) and len(array) == 0)\n            or array is None\n        ):\n            page = NullObject()\n            return Destination(title, page, Fit.fit())\n        page, typ, *array = array  # type: ignore\n        try:\n            return Destination(title, page, Fit(fit_type=typ, fit_args=array))  # type: ignore\n        except PdfReadError:\n            logger_warning(f\"Unknown destination: {title!r} {array}\", __name__)\n            if self.strict:\n                raise\n            # create a link to first Page\n            tmp = self.pages[0].indirect_reference\n            indirect_reference = NullObject() if tmp is None else tmp\n            return Destination(title, indirect_reference, Fit.fit())\n\n    def _build_outline_item(self, node: DictionaryObject) -> Optional[Destination]:\n        dest, title, outline_item = None, None, None\n\n        # title required for valid outline\n        # §12.3.3, entries in an outline item dictionary\n        try:\n            title = cast(\"str\", node[\"/Title\"])\n        except KeyError:\n            if self.strict:\n                raise PdfReadError(f\"Outline Entry Missing /Title attribute: {node!r}\")\n            title = \"\"\n\n        if \"/A\" in node:\n            # Action, PDF 1.7 and PDF 2.0 §12.6 (only type GoTo supported)\n            action = cast(DictionaryObject, node[\"/A\"])\n            action_type = cast(NameObject, action[GoToActionArguments.S])\n            if action_type == \"/GoTo\":\n                if GoToActionArguments.D in action:\n                    dest = action[GoToActionArguments.D]\n                elif self.strict:\n                    raise PdfReadError(f\"Outline Action Missing /D attribute: {node!r}\")\n        elif \"/Dest\" in node:\n            # Destination, PDF 1.7 and PDF 2.0 §12.3.2\n            dest = node[\"/Dest\"]\n            # if array was referenced in another object, will be a dict w/ key \"/D\"\n            if isinstance(dest, DictionaryObject) and \"/D\" in dest:\n                dest = dest[\"/D\"]\n\n        if isinstance(dest, ArrayObject):\n            outline_item = self._build_destination(title, dest)\n        elif isinstance(dest, str):\n            # named destination, addresses NameObject Issue #193\n            # TODO: Keep named destination instead of replacing it?\n            try:\n                outline_item = self._build_destination(\n                    title, self._named_destinations[dest].dest_array\n                )\n            except KeyError:\n                # named destination not found in Name Dict\n                outline_item = self._build_destination(title, None)\n        elif dest is None:\n            # outline item not required to have destination or action\n            # PDFv1.7 Table 153\n            outline_item = self._build_destination(title, dest)\n        else:\n            if self.strict:\n                raise PdfReadError(f\"Unexpected destination {dest!r}\")\n            logger_warning(\n                f\"Removed unexpected destination {dest!r} from destination\",\n                __name__,\n            )\n            outline_item = self._build_destination(title, None)\n\n        # if outline item created, add color, format, and child count if present\n        if outline_item:\n            if \"/C\" in node:\n                # Color of outline item font in (R, G, B) with values ranging 0.0-1.0\n                outline_item[NameObject(\"/C\")] = ArrayObject(FloatObject(c) for c in node[\"/C\"])  # type: ignore\n            if \"/F\" in node:\n                # specifies style characteristics bold and/or italic\n                # with 1=italic, 2=bold, 3=both\n                outline_item[NameObject(\"/F\")] = node[\"/F\"]\n            if \"/Count\" in node:\n                # absolute value = num. visible children\n                # with positive = open/unfolded, negative = closed/folded\n                outline_item[NameObject(\"/Count\")] = node[\"/Count\"]\n            #  if count is 0 we will consider it as open (to have available is_open)\n            outline_item[NameObject(\"/%is_open%\")] = BooleanObject(\n                node.get(\"/Count\", 0) >= 0\n            )\n        outline_item.node = node\n        try:\n            outline_item.indirect_reference = node.indirect_reference\n        except AttributeError:\n            pass\n        return outline_item\n\n    @property\n    def pages(self) -> list[PageObject]:\n        \"\"\"\n        Property that emulates a list of :class:`PageObject<pypdf._page.PageObject>`.\n        This property allows to get a page or a range of pages.\n\n        Note:\n            For PdfWriter only: Provides the capability to remove a page/range of\n            page from the list (using the del operator). Remember: Only the page\n            entry is removed, as the objects beneath can be used elsewhere. A\n            solution to completely remove them - if they are not used anywhere - is\n            to write to a buffer/temporary file and then load it into a new\n            PdfWriter.\n\n        \"\"\"\n        return _VirtualList(self.get_num_pages, self.get_page)  # type: ignore\n\n    @property\n    def page_labels(self) -> list[str]:\n        \"\"\"\n        A list of labels for the pages in this document.\n\n        This property is read-only. The labels are in the order that the pages\n        appear in the document.\n        \"\"\"\n        return [page_index2page_label(self, i) for i in range(len(self.pages))]\n\n    @property\n    def page_layout(self) -> Optional[str]:\n        \"\"\"\n        Get the page layout currently being used.\n\n        .. list-table:: Valid ``layout`` values\n           :widths: 50 200\n\n           * - /NoLayout\n             - Layout explicitly not specified\n           * - /SinglePage\n             - Show one page at a time\n           * - /OneColumn\n             - Show one column at a time\n           * - /TwoColumnLeft\n             - Show pages in two columns, odd-numbered pages on the left\n           * - /TwoColumnRight\n             - Show pages in two columns, odd-numbered pages on the right\n           * - /TwoPageLeft\n             - Show two pages at a time, odd-numbered pages on the left\n           * - /TwoPageRight\n             - Show two pages at a time, odd-numbered pages on the right\n        \"\"\"\n        try:\n            return cast(NameObject, self.root_object[CD.PAGE_LAYOUT])\n        except KeyError:\n            return None\n\n    @property\n    def page_mode(self) -> Optional[PagemodeType]:\n        \"\"\"\n        Get the page mode currently being used.\n\n        .. list-table:: Valid ``mode`` values\n           :widths: 50 200\n\n           * - /UseNone\n             - Do not show outline or thumbnails panels\n           * - /UseOutlines\n             - Show outline (aka bookmarks) panel\n           * - /UseThumbs\n             - Show page thumbnails panel\n           * - /FullScreen\n             - Fullscreen view\n           * - /UseOC\n             - Show Optional Content Group (OCG) panel\n           * - /UseAttachments\n             - Show attachments panel\n        \"\"\"\n        try:\n            return self.root_object[\"/PageMode\"]  # type: ignore\n        except KeyError:\n            return None\n\n    def _flatten(\n        self,\n        list_only: bool = False,\n        pages: Union[None, DictionaryObject, PageObject] = None,\n        inherit: Optional[dict[str, Any]] = None,\n        indirect_reference: Optional[IndirectObject] = None,\n    ) -> None:\n        \"\"\"\n        Process the document pages to ease searching.\n\n        Attributes of a page may inherit from ancestor nodes\n        in the page tree. Flattening means moving\n        any inheritance data into descendant nodes,\n        effectively removing the inheritance dependency.\n\n        Note: It is distinct from another use of \"flattening\" applied to PDFs.\n        Flattening a PDF also means combining all the contents into one single layer\n        and making the file less editable.\n\n        Args:\n            list_only: Will only list the pages within _flatten_pages.\n            pages:\n            inherit:\n            indirect_reference: Used recursively to flatten the /Pages object.\n\n        \"\"\"\n        inheritable_page_attributes = (\n            NameObject(PG.RESOURCES),\n            NameObject(PG.MEDIABOX),\n            NameObject(PG.CROPBOX),\n            NameObject(PG.ROTATE),\n        )\n        if inherit is None:\n            inherit = {}\n        if pages is None:\n            # Fix issue 327: set flattened_pages attribute only for\n            # decrypted file\n            catalog = self.root_object\n            pages = catalog.get(\"/Pages\").get_object()  # type: ignore\n            if not isinstance(pages, DictionaryObject):\n                raise PdfReadError(\"Invalid object in /Pages\")\n            self.flattened_pages = []\n\n        if PagesAttributes.TYPE in pages:\n            t = cast(str, pages[PagesAttributes.TYPE])\n        # if the page tree node has no /Type, consider as a page if /Kids is also missing\n        elif PagesAttributes.KIDS not in pages:\n            t = \"/Page\"\n        else:\n            t = \"/Pages\"\n\n        if t == \"/Pages\":\n            for attr in inheritable_page_attributes:\n                if attr in pages:\n                    inherit[attr] = pages[attr]\n            pages_reference = getattr(pages, \"indirect_reference\", object())\n            for page in cast(ArrayObject, pages[PagesAttributes.KIDS]):\n                if getattr(page, \"indirect_reference\", object()) == pages_reference:\n                    raise PdfReadError(\"Detected cyclic page references.\")\n\n                addt = {}\n                if isinstance(page, IndirectObject):\n                    addt[\"indirect_reference\"] = page\n                obj = page.get_object()\n                if obj:\n                    # damaged file may have invalid child in /Pages\n                    try:\n                        self._flatten(list_only, obj, inherit, **addt)\n                    except RecursionError:\n                        raise PdfReadError(\n                            \"Maximum recursion depth reached during page flattening.\"\n                        )\n        elif t == \"/Page\":\n            for attr_in, value in inherit.items():\n                # if the page has its own value, it does not inherit the\n                # parent's value\n                if attr_in not in pages:\n                    pages[attr_in] = value\n            page_obj = PageObject(self, indirect_reference)\n            if not list_only:\n                page_obj.update(pages)\n\n            # TODO: Could flattened_pages be None at this point?\n            self.flattened_pages.append(page_obj)  # type: ignore\n\n    def remove_page(\n        self,\n        page: Union[int, PageObject, IndirectObject],\n        clean: bool = False,\n    ) -> None:\n        \"\"\"\n        Remove page from pages list.\n\n        Args:\n            page:\n                * :class:`int`: Page number to be removed.\n                * :class:`~pypdf._page.PageObject`: page to be removed. If the page appears many times\n                  only the first one will be removed.\n                * :class:`~pypdf.generic.IndirectObject`: Reference to page to be removed.\n\n            clean: replace PageObject with NullObject to prevent annotations\n                or destinations to reference a detached page.\n\n        \"\"\"\n        if self.flattened_pages is None:\n            self._flatten(self._readonly)\n        assert self.flattened_pages is not None\n        if isinstance(page, IndirectObject):\n            p = page.get_object()\n            if not isinstance(p, PageObject):\n                logger_warning(\"IndirectObject is not referencing a page\", __name__)\n                return\n            page = p\n\n        if not isinstance(page, int):\n            try:\n                page = self.flattened_pages.index(page)\n            except ValueError:\n                logger_warning(\"Cannot find page in pages\", __name__)\n                return\n        if not (0 <= page < len(self.flattened_pages)):\n            logger_warning(\"Page number is out of range\", __name__)\n            return\n\n        ind = self.pages[page].indirect_reference\n        del self.pages[page]\n        if clean and ind is not None:\n            self._replace_object(ind, NullObject())\n\n    def _get_indirect_object(self, num: int, gen: int) -> Optional[PdfObject]:\n        \"\"\"\n        Used to ease development.\n\n        This is equivalent to generic.IndirectObject(num,gen,self).get_object()\n\n        Args:\n            num: The object number of the indirect object.\n            gen: The generation number of the indirect object.\n\n        Returns:\n            A PdfObject\n\n        \"\"\"\n        return IndirectObject(num, gen, self).get_object()\n\n    def decode_permissions(\n        self, permissions_code: int\n    ) -> dict[str, bool]:  # pragma: no cover\n        \"\"\"Take the permissions as an integer, return the allowed access.\"\"\"\n        deprecation_with_replacement(\n            old_name=\"decode_permissions\",\n            new_name=\"user_access_permissions\",\n            removed_in=\"5.0.0\",\n        )\n\n        permissions_mapping = {\n            \"print\": UserAccessPermissions.PRINT,\n            \"modify\": UserAccessPermissions.MODIFY,\n            \"copy\": UserAccessPermissions.EXTRACT,\n            \"annotations\": UserAccessPermissions.ADD_OR_MODIFY,\n            \"forms\": UserAccessPermissions.FILL_FORM_FIELDS,\n            # Do not fix typo, as part of official, but deprecated API.\n            \"accessability\": UserAccessPermissions.EXTRACT_TEXT_AND_GRAPHICS,\n            \"assemble\": UserAccessPermissions.ASSEMBLE_DOC,\n            \"print_high_quality\": UserAccessPermissions.PRINT_TO_REPRESENTATION,\n        }\n\n        return {\n            key: permissions_code & flag != 0\n            for key, flag in permissions_mapping.items()\n        }\n\n    @property\n    def user_access_permissions(self) -> Optional[UserAccessPermissions]:\n        \"\"\"\n        Get the user access permissions for encrypted documents.\n        Returns None if not encrypted.\n\n        .. warning::\n\n            For AES-256 encrypted documents (R=5/R=6), the returned\n            permissions are derived from the ``/P`` field, which is\n            only trustworthy if the ``/Perms`` integrity check passed.\n            Check :attr:`are_permissions_valid` to verify.\n        \"\"\"\n        if self._encryption is None:\n            return None\n        return UserAccessPermissions(self._encryption.P)\n\n    @property\n    def are_permissions_valid(self) -> Optional[bool]:\n        \"\"\"\n        Whether the ``/Perms`` integrity check passed for this document.\n\n        For AES-256 encrypted documents (R=5/R=6), the ``/Perms`` field\n        is an encrypted copy of the permissions that can be verified\n        independently. Returns ``False`` if this check fails (the ``/P``\n        permissions may have been tampered with).\n\n        Returns ``None`` if the document is not encrypted or has not yet\n        been decrypted via :meth:`decrypt()<pypdf.PdfReader.decrypt>`.\n        Returns ``True`` for non-AES-256 encryption (no ``/Perms`` to check).\n        \"\"\"\n        if self._encryption is None:\n            return None\n        if not self._encryption.is_decrypted():\n            return None\n        return self._encryption._are_permissions_valid\n\n    @property\n    @abstractmethod\n    def is_encrypted(self) -> bool:\n        \"\"\"\n        Read-only boolean property showing whether this PDF file is encrypted.\n\n        Note that this property, if true, will remain true even after the\n        :meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called.\n        \"\"\"\n        ...  # pragma: no cover\n\n    @property\n    def xfa(self) -> Optional[dict[str, Any]]:\n        retval: dict[str, Any] = {}\n        catalog = self.root_object\n\n        if \"/AcroForm\" not in catalog or not catalog[\"/AcroForm\"]:\n            return None\n\n        tree = cast(TreeObject, catalog[\"/AcroForm\"])\n\n        if \"/XFA\" in tree:\n            fields = cast(ArrayObject, tree[\"/XFA\"])\n            i = iter(fields)\n            for f in i:\n                tag = f\n                f = next(i)\n                if isinstance(f, IndirectObject):\n                    field = cast(Optional[EncodedStreamObject], f.get_object())\n                    if field:\n                        es = _decompress_with_limit(field._data)\n                        retval[tag] = es\n        return retval\n\n    @property\n    def attachments(self) -> Mapping[str, list[bytes]]:\n        \"\"\"Mapping of attachment filenames to their content.\"\"\"\n        return LazyDict(\n            {\n                name: (self._get_attachment_list, name)\n                for name in self._list_attachments()\n            }\n        )\n\n    @property\n    def attachment_list(self) -> Generator[EmbeddedFile, None, None]:\n        \"\"\"Iterable of attachment objects.\"\"\"\n        yield from EmbeddedFile._load(self.root_object)\n\n    def _list_attachments(self) -> list[str]:\n        \"\"\"\n        Retrieves the list of filenames of file attachments.\n\n        Returns:\n            list of filenames\n\n        \"\"\"\n        names = []\n        for entry in self.attachment_list:\n            names.append(entry.name)\n            if (name := entry.alternative_name) != entry.name and name:\n                names.append(name)\n        return names\n\n    def _get_attachment_list(self, name: str) -> list[bytes]:\n        out = self._get_attachments(name)[name]\n        if isinstance(out, list):\n            return out\n        return [out]\n\n    def _get_attachments(\n        self, filename: Optional[str] = None\n    ) -> dict[str, Union[bytes, list[bytes]]]:\n        \"\"\"\n        Retrieves all or selected file attachments of the PDF as a dictionary of file names\n        and the file data as a bytestring.\n\n        Args:\n            filename: If filename is None, then a dictionary of all attachments\n                will be returned, where the key is the filename and the value\n                is the content. Otherwise, a dictionary with just a single key\n                - the filename - and its content will be returned.\n\n        Returns:\n            dictionary of filename -> Union[bytestring or List[ByteString]]\n            If the filename exists multiple times a list of the different versions will be provided.\n\n        \"\"\"\n        attachments: dict[str, Union[bytes, list[bytes]]] = {}\n        for entry in self.attachment_list:\n            names = set()\n            alternative_name = entry.alternative_name\n            if filename is not None:\n                if filename in {entry.name, alternative_name}:\n                    name = entry.name if filename == entry.name else alternative_name\n                    names.add(name)\n                else:\n                    continue\n            else:\n                names = {entry.name, alternative_name}\n\n            for name in names:\n                if name is None:\n                    continue\n                if name in attachments:\n                    if not isinstance(attachments[name], list):\n                        attachments[name] = [attachments[name]]  # type:ignore\n                    attachments[name].append(entry.content)  # type:ignore\n                else:\n                    attachments[name] = entry.content\n        return attachments\n\n    @abstractmethod\n    def _repr_mimebundle_(\n        self,\n        include: Union[None, Iterable[str]] = None,\n        exclude: Union[None, Iterable[str]] = None,\n    ) -> dict[str, Any]:\n        \"\"\"\n        Integration into Jupyter Notebooks.\n\n        This method returns a dictionary that maps a mime-type to its\n        representation.\n\n        .. seealso::\n\n            https://ipython.readthedocs.io/en/stable/config/integrating.html\n        \"\"\"\n        ...  # pragma: no cover\n\n\nclass LazyDict(Mapping[Any, Any]):\n    def __init__(self, *args: Any, **kwargs: Any) -> None:\n        self._raw_dict = dict(*args, **kwargs)\n\n    def __getitem__(self, key: str) -> Any:\n        func, arg = self._raw_dict.__getitem__(key)\n        return func(arg)\n\n    def __iter__(self) -> Iterator[Any]:\n        return iter(self._raw_dict)\n\n    def __len__(self) -> int:\n        return len(self._raw_dict)\n\n    def __str__(self) -> str:\n        return f\"LazyDict(keys={list(self.keys())})\"\n"
  },
  {
    "path": "pypdf/_encryption.py",
    "content": "# Copyright (c) 2022, exiledkingcc\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\nimport hashlib\nimport secrets\nimport struct\nfrom enum import Enum, IntEnum\nfrom typing import Any, Optional, Union, cast\n\nfrom pypdf._crypt_providers import (\n    CryptAES,\n    CryptBase,\n    CryptIdentity,\n    CryptRC4,\n    aes_cbc_decrypt,\n    aes_cbc_encrypt,\n    aes_ecb_decrypt,\n    aes_ecb_encrypt,\n    rc4_decrypt,\n    rc4_encrypt,\n)\n\nfrom ._utils import logger_warning\nfrom .generic import (\n    ArrayObject,\n    ByteStringObject,\n    DictionaryObject,\n    NameObject,\n    NumberObject,\n    PdfObject,\n    StreamObject,\n    TextStringObject,\n    create_string_object,\n)\n\n\nclass CryptFilter:\n    def __init__(\n        self,\n        stm_crypt: CryptBase,\n        str_crypt: CryptBase,\n        ef_crypt: CryptBase,\n    ) -> None:\n        self.stm_crypt = stm_crypt\n        self.str_crypt = str_crypt\n        self.ef_crypt = ef_crypt\n\n    def encrypt_object(self, obj: PdfObject) -> PdfObject:\n        if isinstance(obj, ByteStringObject):\n            data = self.str_crypt.encrypt(obj.original_bytes)\n            obj = ByteStringObject(data)\n        elif isinstance(obj, TextStringObject):\n            data = self.str_crypt.encrypt(obj.get_encoded_bytes())\n            obj = ByteStringObject(data)\n        elif isinstance(obj, StreamObject):\n            obj2 = StreamObject()\n            obj2.update(obj)\n            obj2.set_data(self.stm_crypt.encrypt(obj._data))\n            for key, value in obj.items():  # Dont forget the Stream dict.\n                obj2[key] = self.encrypt_object(value)\n            obj = obj2\n        elif isinstance(obj, DictionaryObject):\n            obj2 = DictionaryObject()  # type: ignore\n            for key, value in obj.items():\n                obj2[key] = self.encrypt_object(value)\n            obj = obj2\n        elif isinstance(obj, ArrayObject):\n            obj = ArrayObject(self.encrypt_object(x) for x in obj)\n        return obj\n\n    def decrypt_object(self, obj: PdfObject) -> PdfObject:\n        if isinstance(obj, (ByteStringObject, TextStringObject)):\n            data = self.str_crypt.decrypt(obj.original_bytes)\n            obj = create_string_object(data)\n        elif isinstance(obj, StreamObject):\n            obj._data = self.stm_crypt.decrypt(obj._data)\n            for key, value in obj.items():  # Dont forget the Stream dict.\n                obj[key] = self.decrypt_object(value)\n        elif isinstance(obj, DictionaryObject):\n            for key, value in obj.items():\n                obj[key] = self.decrypt_object(value)\n        elif isinstance(obj, ArrayObject):\n            for i in range(len(obj)):\n                obj[i] = self.decrypt_object(obj[i])\n        return obj\n\n\n_PADDING = (\n    b\"\\x28\\xbf\\x4e\\x5e\\x4e\\x75\\x8a\\x41\\x64\\x00\\x4e\\x56\\xff\\xfa\\x01\\x08\"\n    b\"\\x2e\\x2e\\x00\\xb6\\xd0\\x68\\x3e\\x80\\x2f\\x0c\\xa9\\xfe\\x64\\x53\\x69\\x7a\"\n)\n\n\ndef _padding(data: bytes) -> bytes:\n    return (data + _PADDING)[:32]\n\n\nclass AlgV4:\n    @staticmethod\n    def compute_key(\n        password: bytes,\n        rev: int,\n        key_size: int,\n        o_entry: bytes,\n        P: int,\n        id1_entry: bytes,\n        metadata_encrypted: bool,\n    ) -> bytes:\n        \"\"\"\n        Algorithm 2: Computing an encryption key.\n\n        a) Pad or truncate the password string to exactly 32 bytes. If the\n           password string is more than 32 bytes long,\n           use only its first 32 bytes; if it is less than 32 bytes long, pad it\n           by appending the required number of\n           additional bytes from the beginning of the following padding string:\n                < 28 BF 4E 5E 4E 75 8A 41 64 00 4E 56 FF FA 01 08\n                2E 2E 00 B6 D0 68 3E 80 2F 0C A9 FE 64 53 69 7A >\n           That is, if the password string is n bytes long, append\n           the first 32 - n bytes of the padding string to the end\n           of the password string. If the password string is empty\n           (zero-length), meaning there is no user password,\n           substitute the entire padding string in its place.\n\n        b) Initialize the MD5 hash function and pass the result of step (a)\n           as input to this function.\n        c) Pass the value of the encryption dictionary’s O entry to the\n           MD5 hash function. (\"Algorithm 3: Computing\n           the encryption dictionary’s O (owner password) value\" shows how the\n           O value is computed.)\n        d) Convert the integer value of the P entry to a 32-bit unsigned binary\n           number and pass these bytes to the\n           MD5 hash function, low-order byte first.\n        e) Pass the first element of the file’s file identifier array (the value\n           of the ID entry in the document’s trailer\n           dictionary; see Table 15) to the MD5 hash function.\n        f) (Security handlers of revision 4 or greater) If document metadata is\n           not being encrypted, pass 4 bytes with\n           the value 0xFFFFFFFF to the MD5 hash function.\n        g) Finish the hash.\n        h) (Security handlers of revision 3 or greater) Do the following\n           50 times: Take the output from the previous\n           MD5 hash and pass the first n bytes of the output as input into a new\n           MD5 hash, where n is the number of\n           bytes of the encryption key as defined by the value of the encryption\n           dictionary’s Length entry.\n        i) Set the encryption key to the first n bytes of the output from the\n           final MD5 hash, where n shall always be 5\n           for security handlers of revision 2 but, for security handlers of\n           revision 3 or greater, shall depend on the\n           value of the encryption dictionary’s Length entry.\n\n        Args:\n            password: The encryption secret as a bytes-string\n            rev: The encryption revision (see PDF standard)\n            key_size: The size of the key in bytes\n            o_entry: The owner entry\n            P: A set of flags specifying which operations shall be permitted\n                when the document is opened with user access. If bit 2 is set to 1,\n                all other bits are ignored and all operations are permitted.\n                If bit 2 is set to 0, permission for operations are based on the\n                values of the remaining flags defined in Table 24.\n            id1_entry:\n            metadata_encrypted: A boolean indicating if the metadata is encrypted.\n\n        Returns:\n            The u_hash digest of length key_size\n\n        \"\"\"\n        a = _padding(password)\n        u_hash = hashlib.md5(a)\n        u_hash.update(o_entry)\n        u_hash.update(struct.pack(\"<I\", P))\n        u_hash.update(id1_entry)\n        if rev >= 4 and not metadata_encrypted:\n            u_hash.update(b\"\\xff\\xff\\xff\\xff\")\n        u_hash_digest = u_hash.digest()\n        length = key_size // 8\n        if rev >= 3:\n            for _ in range(50):\n                u_hash_digest = hashlib.md5(u_hash_digest[:length]).digest()\n        return u_hash_digest[:length]\n\n    @staticmethod\n    def compute_O_value_key(owner_password: bytes, rev: int, key_size: int) -> bytes:\n        \"\"\"\n        Algorithm 3: Computing the encryption dictionary’s O (owner password) value.\n\n        a) Pad or truncate the owner password string as described in step (a)\n           of \"Algorithm 2: Computing an encryption key\".\n           If there is no owner password, use the user password instead.\n        b) Initialize the MD5 hash function and pass the result of step (a) as\n           input to this function.\n        c) (Security handlers of revision 3 or greater) Do the following 50 times:\n           Take the output from the previous\n           MD5 hash and pass it as input into a new MD5 hash.\n        d) Create an RC4 encryption key using the first n bytes of the output\n           from the final MD5 hash, where n shall\n           always be 5 for security handlers of revision 2 but, for security\n           handlers of revision 3 or greater, shall\n           depend on the value of the encryption dictionary’s Length entry.\n        e) Pad or truncate the user password string as described in step (a) of\n           \"Algorithm 2: Computing an encryption key\".\n        f) Encrypt the result of step (e), using an RC4 encryption function with\n           the encryption key obtained in step (d).\n        g) (Security handlers of revision 3 or greater) Do the following 19 times:\n           Take the output from the previous\n           invocation of the RC4 function and pass it as input to a new\n           invocation of the function; use an encryption\n           key generated by taking each byte of the encryption key obtained in\n           step (d) and performing an XOR\n           (exclusive or) operation between that byte and the single-byte value\n           of the iteration counter (from 1 to 19).\n        h) Store the output from the final invocation of the RC4 function as\n           the value of the O entry in the encryption dictionary.\n\n        Args:\n            owner_password:\n            rev: The encryption revision (see PDF standard)\n            key_size: The size of the key in bytes\n\n        Returns:\n            The RC4 key\n\n        \"\"\"\n        a = _padding(owner_password)\n        o_hash_digest = hashlib.md5(a).digest()\n\n        if rev >= 3:\n            for _ in range(50):\n                o_hash_digest = hashlib.md5(o_hash_digest).digest()\n\n        return o_hash_digest[: key_size // 8]\n\n    @staticmethod\n    def compute_O_value(rc4_key: bytes, user_password: bytes, rev: int) -> bytes:\n        \"\"\"\n        See :func:`compute_O_value_key`.\n\n        Args:\n            rc4_key:\n            user_password:\n            rev: The encryption revision (see PDF standard)\n\n        Returns:\n            The RC4 encrypted\n\n        \"\"\"\n        a = _padding(user_password)\n        rc4_enc = rc4_encrypt(rc4_key, a)\n        if rev >= 3:\n            for i in range(1, 20):\n                key = bytes(x ^ i for x in rc4_key)\n                rc4_enc = rc4_encrypt(key, rc4_enc)\n        return rc4_enc\n\n    @staticmethod\n    def compute_U_value(key: bytes, rev: int, id1_entry: bytes) -> bytes:\n        \"\"\"\n        Algorithm 4: Computing the encryption dictionary’s U (user password) value.\n\n        (Security handlers of revision 2)\n\n        a) Create an encryption key based on the user password string, as\n           described in \"Algorithm 2: Computing an encryption key\".\n        b) Encrypt the 32-byte padding string shown in step (a) of\n           \"Algorithm 2: Computing an encryption key\", using an RC4 encryption\n           function with the encryption key from the preceding step.\n        c) Store the result of step (b) as the value of the U entry in the\n           encryption dictionary.\n\n        Args:\n            key:\n            rev: The encryption revision (see PDF standard)\n            id1_entry:\n\n        Returns:\n            The value\n\n        \"\"\"\n        if rev <= 2:\n            return rc4_encrypt(key, _PADDING)\n\n        \"\"\"\n        Algorithm 5: Computing the encryption dictionary’s U (user password) value.\n\n        (Security handlers of revision 3 or greater)\n\n        a) Create an encryption key based on the user password string, as\n           described in \"Algorithm 2: Computing an encryption key\".\n        b) Initialize the MD5 hash function and pass the 32-byte padding string\n           shown in step (a) of \"Algorithm 2:\n           Computing an encryption key\" as input to this function.\n        c) Pass the first element of the file’s file identifier array (the value\n           of the ID entry in the document’s trailer\n           dictionary; see Table 15) to the hash function and finish the hash.\n        d) Encrypt the 16-byte result of the hash, using an RC4 encryption\n           function with the encryption key from step (a).\n        e) Do the following 19 times: Take the output from the previous\n           invocation of the RC4 function and pass it as input to a new\n           invocation of the function; use an encryption key generated by\n           taking each byte of the original encryption key obtained in\n           step (a) and performing an XOR (exclusive or) operation between that\n           byte and the single-byte value of the iteration counter (from 1 to 19).\n        f) Append 16 bytes of arbitrary padding to the output from the final\n           invocation of the RC4 function and store the 32-byte result as the\n           value of the U entry in the encryption dictionary.\n        \"\"\"\n        u_hash = hashlib.md5(_PADDING)\n        u_hash.update(id1_entry)\n        rc4_enc = rc4_encrypt(key, u_hash.digest())\n        for i in range(1, 20):\n            rc4_key = bytes(x ^ i for x in key)\n            rc4_enc = rc4_encrypt(rc4_key, rc4_enc)\n        return _padding(rc4_enc)\n\n    @staticmethod\n    def verify_user_password(\n        user_password: bytes,\n        rev: int,\n        key_size: int,\n        o_entry: bytes,\n        u_entry: bytes,\n        P: int,\n        id1_entry: bytes,\n        metadata_encrypted: bool,\n    ) -> bytes:\n        \"\"\"\n        Algorithm 6: Authenticating the user password.\n\n        a) Perform all but the last step of \"Algorithm 4: Computing the\n           encryption dictionary’s U (user password) value (Security handlers of\n           revision 2)\" or \"Algorithm 5: Computing the encryption dictionary’s U\n           (user password) value (Security handlers of revision 3 or greater)\"\n           using the supplied password string.\n        b) If the result of step (a) is equal to the value of the encryption\n           dictionary’s U entry (comparing on the first 16 bytes in the case of\n           security handlers of revision 3 or greater), the password supplied is\n           the correct user password. The key obtained in step (a) (that is, in\n           the first step of \"Algorithm 4: Computing the encryption\n           dictionary’s U (user password) value\n           (Security handlers of revision 2)\" or\n           \"Algorithm 5: Computing the encryption dictionary’s U (user password)\n           value (Security handlers of revision 3 or greater)\") shall be used\n           to decrypt the document.\n\n        Args:\n            user_password: The user password as a bytes stream\n            rev: The encryption revision (see PDF standard)\n            key_size: The size of the key in bytes\n            o_entry: The owner entry\n            u_entry: The user entry\n            P: A set of flags specifying which operations shall be permitted\n                when the document is opened with user access. If bit 2 is set to 1,\n                all other bits are ignored and all operations are permitted.\n                If bit 2 is set to 0, permission for operations are based on the\n                values of the remaining flags defined in Table 24.\n            id1_entry:\n            metadata_encrypted: A boolean indicating if the metadata is encrypted.\n\n        Returns:\n            The key\n\n        \"\"\"\n        key = AlgV4.compute_key(\n            user_password, rev, key_size, o_entry, P, id1_entry, metadata_encrypted\n        )\n        u_value = AlgV4.compute_U_value(key, rev, id1_entry)\n        if rev >= 3:\n            u_value = u_value[:16]\n            u_entry = u_entry[:16]\n        if u_value != u_entry:\n            key = b\"\"\n        return key\n\n    @staticmethod\n    def verify_owner_password(\n        owner_password: bytes,\n        rev: int,\n        key_size: int,\n        o_entry: bytes,\n        u_entry: bytes,\n        P: int,\n        id1_entry: bytes,\n        metadata_encrypted: bool,\n    ) -> bytes:\n        \"\"\"\n        Algorithm 7: Authenticating the owner password.\n\n        a) Compute an encryption key from the supplied password string, as\n           described in steps (a) to (d) of\n           \"Algorithm 3: Computing the encryption dictionary’s O (owner password)\n           value\".\n        b) (Security handlers of revision 2 only) Decrypt the value of the\n           encryption dictionary’s O entry, using an RC4\n           encryption function with the encryption key computed in step (a).\n           (Security handlers of revision 3 or greater) Do the following 20 times:\n           Decrypt the value of the encryption dictionary’s O entry (first iteration)\n           or the output from the previous iteration (all subsequent iterations),\n           using an RC4 encryption function with a different encryption key at\n           each iteration. The key shall be generated by taking the original key\n           (obtained in step (a)) and performing an XOR (exclusive or) operation\n           between each byte of the key and the single-byte value of the\n           iteration counter (from 19 to 0).\n        c) The result of step (b) purports to be the user password.\n           Authenticate this user password using\n           \"Algorithm 6: Authenticating the user password\".\n           If it is correct, the password supplied is the correct owner password.\n\n        Args:\n            owner_password:\n            rev: The encryption revision (see PDF standard)\n            key_size: The size of the key in bytes\n            o_entry: The owner entry\n            u_entry: The user entry\n            P: A set of flags specifying which operations shall be permitted\n                when the document is opened with user access. If bit 2 is set to 1,\n                all other bits are ignored and all operations are permitted.\n                If bit 2 is set to 0, permission for operations are based on the\n                values of the remaining flags defined in Table 24.\n            id1_entry:\n            metadata_encrypted: A boolean indicating if the metadata is encrypted.\n\n        Returns:\n            bytes\n\n        \"\"\"\n        rc4_key = AlgV4.compute_O_value_key(owner_password, rev, key_size)\n\n        if rev <= 2:\n            user_password = rc4_decrypt(rc4_key, o_entry)\n        else:\n            user_password = o_entry\n            for i in range(19, -1, -1):\n                key = bytes(x ^ i for x in rc4_key)\n                user_password = rc4_decrypt(key, user_password)\n        return AlgV4.verify_user_password(\n            user_password,\n            rev,\n            key_size,\n            o_entry,\n            u_entry,\n            P,\n            id1_entry,\n            metadata_encrypted,\n        )\n\n\nclass AlgV5:\n    @staticmethod\n    def verify_owner_password(\n        R: int, password: bytes, o_value: bytes, oe_value: bytes, u_value: bytes\n    ) -> bytes:\n        \"\"\"\n        Algorithm 3.2a Computing an encryption key.\n\n        To understand the algorithm below, it is necessary to treat the O and U\n        strings in the Encrypt dictionary as made up of three sections.\n        The first 32 bytes are a hash value (explained below). The next 8 bytes\n        are called the Validation Salt. The final 8 bytes are called the Key Salt.\n\n        1. The password string is generated from Unicode input by processing the\n           input string with the SASLprep (IETF RFC 4013) profile of\n           stringprep (IETF RFC 3454), and then converting to a UTF-8\n           representation.\n        2. Truncate the UTF-8 representation to 127 bytes if it is longer than\n           127 bytes.\n        3. Test the password against the owner key by computing the SHA-256 hash\n           of the UTF-8 password concatenated with the 8 bytes of owner\n           Validation Salt, concatenated with the 48-byte U string. If the\n           32-byte result matches the first 32 bytes of the O string, this is\n           the owner password.\n           Compute an intermediate owner key by computing the SHA-256 hash of\n           the UTF-8 password concatenated with the 8 bytes of owner Key Salt,\n           concatenated with the 48-byte U string. The 32-byte result is the\n           key used to decrypt the 32-byte OE string using AES-256 in CBC mode\n           with no padding and an initialization vector of zero.\n           The 32-byte result is the file encryption key.\n        4. Test the password against the user key by computing the SHA-256 hash\n           of the UTF-8 password concatenated with the 8 bytes of user\n           Validation Salt. If the 32 byte result matches the first 32 bytes of\n           the U string, this is the user password.\n           Compute an intermediate user key by computing the SHA-256 hash of the\n           UTF-8 password concatenated with the 8 bytes of user Key Salt.\n           The 32-byte result is the key used to decrypt the 32-byte\n           UE string using AES-256 in CBC mode with no padding and an\n           initialization vector of zero. The 32-byte result is the file\n           encryption key.\n        5. Decrypt the 16-byte Perms string using AES-256 in ECB mode with an\n           initialization vector of zero and the file encryption key as the key.\n           Verify that bytes 9-11 of the result are the characters ‘a’, ‘d’, ‘b’.\n           Bytes 0-3 of the decrypted Perms entry, treated as a little-endian\n           integer, are the user permissions.\n           They should match the value in the P key.\n\n        Args:\n            R: A number specifying which revision of the standard security\n                handler shall be used to interpret this dictionary\n            password: The owner password\n            o_value: A 32-byte string, based on both the owner and user passwords,\n                that shall be used in computing the encryption key and in\n                determining whether a valid owner password was entered\n            oe_value:\n            u_value: A 32-byte string, based on the user password, that shall be\n                used in determining whether to prompt the user for a password and,\n                if so, whether a valid user or owner password was entered.\n\n        Returns:\n            The key\n\n        \"\"\"\n        password = password[:127]\n        if (\n            AlgV5.calculate_hash(R, password, o_value[32:40], u_value[:48])\n            != o_value[:32]\n        ):\n            return b\"\"\n        iv = bytes(0 for _ in range(16))\n        tmp_key = AlgV5.calculate_hash(R, password, o_value[40:48], u_value[:48])\n        return aes_cbc_decrypt(tmp_key, iv, oe_value)\n\n    @staticmethod\n    def verify_user_password(\n        R: int, password: bytes, u_value: bytes, ue_value: bytes\n    ) -> bytes:\n        \"\"\"\n        See :func:`verify_owner_password`.\n\n        Args:\n            R: A number specifying which revision of the standard security\n                handler shall be used to interpret this dictionary\n            password: The user password\n            u_value: A 32-byte string, based on the user password, that shall be\n                used in determining whether to prompt the user for a password\n                and, if so, whether a valid user or owner password was entered.\n            ue_value:\n\n        Returns:\n            bytes\n\n        \"\"\"\n        password = password[:127]\n        if AlgV5.calculate_hash(R, password, u_value[32:40], b\"\") != u_value[:32]:\n            return b\"\"\n        iv = bytes(0 for _ in range(16))\n        tmp_key = AlgV5.calculate_hash(R, password, u_value[40:48], b\"\")\n        return aes_cbc_decrypt(tmp_key, iv, ue_value)\n\n    @staticmethod\n    def calculate_hash(R: int, password: bytes, salt: bytes, udata: bytes) -> bytes:\n        # https://github.com/qpdf/qpdf/blob/main/libqpdf/QPDF_encryption.cc\n        k = hashlib.sha256(password + salt + udata).digest()\n        if R < 6:\n            return k\n        count = 0\n        while True:\n            count += 1\n            k1 = password + k + udata\n            e = aes_cbc_encrypt(k[:16], k[16:32], k1 * 64)\n            hash_fn = (\n                hashlib.sha256,\n                hashlib.sha384,\n                hashlib.sha512,\n            )[sum(e[:16]) % 3]\n            k = hash_fn(e).digest()\n            if count >= 64 and e[-1] <= count - 32:\n                break\n        return k[:32]\n\n    @staticmethod\n    def verify_perms(\n        key: bytes, perms: bytes, p: int, metadata_encrypted: bool\n    ) -> bool:\n        \"\"\"\n        See :func:`verify_owner_password` and :func:`compute_perms_value`.\n\n        Args:\n            key: The owner password\n            perms:\n            p: A set of flags specifying which operations shall be permitted\n                when the document is opened with user access.\n                If bit 2 is set to 1, all other bits are ignored and all\n                operations are permitted.\n                If bit 2 is set to 0, permission for operations are based on\n                the values of the remaining flags defined in Table 24.\n            metadata_encrypted:\n\n        Returns:\n            A boolean\n\n        \"\"\"\n        b8 = b\"T\" if metadata_encrypted else b\"F\"\n        p1 = struct.pack(\"<I\", p) + b\"\\xff\\xff\\xff\\xff\" + b8 + b\"adb\"\n        p2 = aes_ecb_decrypt(key, perms)\n        return p1 == p2[:12]\n\n    @staticmethod\n    def generate_values(\n        R: int,\n        user_password: bytes,\n        owner_password: bytes,\n        key: bytes,\n        p: int,\n        metadata_encrypted: bool,\n    ) -> dict[Any, Any]:\n        user_password = user_password[:127]\n        owner_password = owner_password[:127]\n        u_value, ue_value = AlgV5.compute_U_value(R, user_password, key)\n        o_value, oe_value = AlgV5.compute_O_value(R, owner_password, key, u_value)\n        perms = AlgV5.compute_Perms_value(key, p, metadata_encrypted)\n        return {\n            \"/U\": u_value,\n            \"/UE\": ue_value,\n            \"/O\": o_value,\n            \"/OE\": oe_value,\n            \"/Perms\": perms,\n        }\n\n    @staticmethod\n    def compute_U_value(R: int, password: bytes, key: bytes) -> tuple[bytes, bytes]:\n        \"\"\"\n        Algorithm 3.8 Computing the encryption dictionary’s U (user password)\n        and UE (user encryption key) values.\n\n        1. Generate 16 random bytes of data using a strong random number generator.\n           The first 8 bytes are the User Validation Salt. The second 8 bytes\n           are the User Key Salt. Compute the 32-byte SHA-256 hash of the\n           password concatenated with the User Validation Salt. The 48-byte\n           string consisting of the 32-byte hash followed by the User\n           Validation Salt followed by the User Key Salt is stored as the U key.\n        2. Compute the 32-byte SHA-256 hash of the password concatenated with\n           the User Key Salt. Using this hash as the key, encrypt the file\n           encryption key using AES-256 in CBC mode with no padding and an\n           initialization vector of zero. The resulting 32-byte string is stored\n           as the UE key.\n\n        Args:\n            R:\n            password:\n            key:\n\n        Returns:\n            A tuple (u-value, ue value)\n\n        \"\"\"\n        random_bytes = secrets.token_bytes(16)\n        val_salt = random_bytes[:8]\n        key_salt = random_bytes[8:]\n        u_value = AlgV5.calculate_hash(R, password, val_salt, b\"\") + val_salt + key_salt\n\n        tmp_key = AlgV5.calculate_hash(R, password, key_salt, b\"\")\n        iv = bytes(0 for _ in range(16))\n        ue_value = aes_cbc_encrypt(tmp_key, iv, key)\n        return u_value, ue_value\n\n    @staticmethod\n    def compute_O_value(\n        R: int, password: bytes, key: bytes, u_value: bytes\n    ) -> tuple[bytes, bytes]:\n        \"\"\"\n        Algorithm 3.9 Computing the encryption dictionary’s O (owner password)\n        and OE (owner encryption key) values.\n\n        1. Generate 16 random bytes of data using a strong random number\n           generator. The first 8 bytes are the Owner Validation Salt. The\n           second 8 bytes are the Owner Key Salt. Compute the 32-byte SHA-256\n           hash of the password concatenated with the Owner Validation Salt and\n           then concatenated with the 48-byte U string as generated in\n           Algorithm 3.8. The 48-byte string consisting of the 32-byte hash\n           followed by the Owner Validation Salt followed by the Owner Key Salt\n           is stored as the O key.\n        2. Compute the 32-byte SHA-256 hash of the password concatenated with\n           the Owner Key Salt and then concatenated with the 48-byte U string as\n           generated in Algorithm 3.8. Using this hash as the key,\n           encrypt the file encryption key using AES-256 in CBC mode with\n           no padding and an initialization vector of zero.\n           The resulting 32-byte string is stored as the OE key.\n\n        Args:\n            R:\n            password:\n            key:\n            u_value: A 32-byte string, based on the user password, that shall be\n                used in determining whether to prompt the user for a password\n                and, if so, whether a valid user or owner password was entered.\n\n        Returns:\n            A tuple (O value, OE value)\n\n        \"\"\"\n        random_bytes = secrets.token_bytes(16)\n        val_salt = random_bytes[:8]\n        key_salt = random_bytes[8:]\n        o_value = (\n            AlgV5.calculate_hash(R, password, val_salt, u_value) + val_salt + key_salt\n        )\n        tmp_key = AlgV5.calculate_hash(R, password, key_salt, u_value[:48])\n        iv = bytes(0 for _ in range(16))\n        oe_value = aes_cbc_encrypt(tmp_key, iv, key)\n        return o_value, oe_value\n\n    @staticmethod\n    def compute_Perms_value(key: bytes, p: int, metadata_encrypted: bool) -> bytes:\n        \"\"\"\n        Algorithm 3.10 Computing the encryption dictionary’s Perms\n        (permissions) value.\n\n        1. Extend the permissions (contents of the P integer) to 64 bits by\n           setting the upper 32 bits to all 1’s.\n           (This allows for future extension without changing the format.)\n        2. Record the 8 bytes of permission in the bytes 0-7 of the block,\n           low order byte first.\n        3. Set byte 8 to the ASCII value ' T ' or ' F ' according to the\n           EncryptMetadata Boolean.\n        4. Set bytes 9-11 to the ASCII characters ' a ', ' d ', ' b '.\n        5. Set bytes 12-15 to 4 bytes of random data, which will be ignored.\n        6. Encrypt the 16-byte block using AES-256 in ECB mode with an\n           initialization vector of zero, using the file encryption key as the\n           key. The result (16 bytes) is stored as the Perms string, and checked\n           for validity when the file is opened.\n\n        Args:\n            key:\n            p: A set of flags specifying which operations shall be permitted\n                when the document is opened with user access. If bit 2 is set to 1,\n                all other bits are ignored and all operations are permitted.\n                If bit 2 is set to 0, permission for operations are based on the\n                values of the remaining flags defined in Table 24.\n            metadata_encrypted: A boolean indicating if the metadata is encrypted.\n\n        Returns:\n            The perms value\n\n        \"\"\"\n        b8 = b\"T\" if metadata_encrypted else b\"F\"\n        rr = secrets.token_bytes(4)\n        data = struct.pack(\"<I\", p) + b\"\\xff\\xff\\xff\\xff\" + b8 + b\"adb\" + rr\n        return aes_ecb_encrypt(key, data)\n\n\nclass PasswordType(IntEnum):\n    NOT_DECRYPTED = 0\n    USER_PASSWORD = 1\n    OWNER_PASSWORD = 2\n\n\nclass EncryptAlgorithm(tuple, Enum):  # type: ignore # noqa: SLOT001\n    # V, R, Length\n    RC4_40 = (1, 2, 40)\n    RC4_128 = (2, 3, 128)\n    AES_128 = (4, 4, 128)\n    AES_256_R5 = (5, 5, 256)\n    AES_256 = (5, 6, 256)\n\n\nclass EncryptionValues:\n    O: bytes  # noqa: E741\n    U: bytes\n    OE: bytes\n    UE: bytes\n    Perms: bytes\n\n\nclass Encryption:\n    \"\"\"\n    Collects and manages parameters for PDF document encryption and decryption.\n\n    Args:\n        V: A code specifying the algorithm to be used in encrypting and\n           decrypting the document.\n        R: The revision of the standard security handler.\n        Length: The length of the encryption key in bits.\n        P: A set of flags specifying which operations shall be permitted\n           when the document is opened with user access\n        entry: The encryption dictionary object.\n        EncryptMetadata: Whether to encrypt metadata in the document.\n        first_id_entry: The first 16 bytes of the file's original ID.\n        StmF: The name of the crypt filter that shall be used by default\n              when decrypting streams.\n        StrF: The name of the crypt filter that shall be used when decrypting\n              all strings in the document.\n        EFF: The name of the crypt filter that shall be used when\n             encrypting embedded file streams that do not have their own\n             crypt filter specifier.\n        values: Additional encryption parameters.\n\n    \"\"\"\n\n    def __init__(\n        self,\n        V: int,\n        R: int,\n        Length: int,\n        P: int,\n        entry: DictionaryObject,\n        EncryptMetadata: bool,\n        first_id_entry: bytes,\n        StmF: str,\n        StrF: str,\n        EFF: str,\n        values: Optional[EncryptionValues],\n    ) -> None:\n        # §7.6.2, entries common to all encryption dictionaries\n        # use same name as keys of encryption dictionaries entries\n        self.V = V\n        self.R = R\n        self.Length = Length  # key_size\n        self.P = (P + 0x100000000) % 0x100000000  # maybe P < 0\n        self.EncryptMetadata = EncryptMetadata\n        self.id1_entry = first_id_entry\n        self.StmF = StmF\n        self.StrF = StrF\n        self.EFF = EFF\n        self.values: EncryptionValues = values or EncryptionValues()\n\n        self._password_type = PasswordType.NOT_DECRYPTED\n        self._key: Optional[bytes] = None\n        self._are_permissions_valid: bool = True\n\n    def is_decrypted(self) -> bool:\n        return self._password_type != PasswordType.NOT_DECRYPTED\n\n    def encrypt_object(self, obj: PdfObject, idnum: int, generation: int) -> PdfObject:\n        # skip calculate key\n        if not self._is_encryption_object(obj):\n            return obj\n\n        cf = self._make_crypt_filter(idnum, generation)\n        return cf.encrypt_object(obj)\n\n    def decrypt_object(self, obj: PdfObject, idnum: int, generation: int) -> PdfObject:\n        # skip calculate key\n        if not self._is_encryption_object(obj):\n            return obj\n\n        cf = self._make_crypt_filter(idnum, generation)\n        return cf.decrypt_object(obj)\n\n    @staticmethod\n    def _is_encryption_object(obj: PdfObject) -> bool:\n        return isinstance(\n            obj,\n            (\n                ByteStringObject,\n                TextStringObject,\n                StreamObject,\n                ArrayObject,\n                DictionaryObject,\n            ),\n        )\n\n    def _make_crypt_filter(self, idnum: int, generation: int) -> CryptFilter:\n        \"\"\"\n        Algorithm 1: Encryption of data using the RC4 or AES algorithms.\n\n        a) Obtain the object number and generation number from the object\n           identifier of the string or stream to be encrypted\n           (see 7.3.10, \"Indirect Objects\"). If the string is a direct object,\n           use the identifier of the indirect object containing it.\n        b) For all strings and streams without crypt filter specifier; treating\n           the object number and generation number as binary integers, extend\n           the original n-byte encryption key to n + 5 bytes by appending the\n           low-order 3 bytes of the object number and the low-order 2 bytes of\n           the generation number in that order, low-order byte first.\n           (n is 5 unless the value of V in the encryption dictionary is greater\n           than 1, in which case n is the value of Length divided by 8.)\n           If using the AES algorithm, extend the encryption key an additional\n           4 bytes by adding the value “sAlT”, which corresponds to the\n           hexadecimal values 0x73, 0x41, 0x6C, 0x54. (This addition is done for\n           backward compatibility and is not intended to provide additional\n           security.)\n        c) Initialize the MD5 hash function and pass the result of step (b) as\n           input to this function.\n        d) Use the first (n + 5) bytes, up to a maximum of 16, of the output\n           from the MD5 hash as the key for the RC4 or AES symmetric key\n           algorithms, along with the string or stream data to be encrypted.\n           If using the AES algorithm, the Cipher Block Chaining (CBC) mode,\n           which requires an initialization vector, is used. The block size\n           parameter is set to 16 bytes, and the initialization vector is a\n           16-byte random number that is stored as the first 16 bytes of the\n           encrypted stream or string.\n\n        Algorithm 3.1a Encryption of data using the AES algorithm\n        1. Use the 32-byte file encryption key for the AES-256 symmetric key\n           algorithm, along with the string or stream data to be encrypted.\n           Use the AES algorithm in Cipher Block Chaining (CBC) mode, which\n           requires an initialization vector. The block size parameter is set to\n           16 bytes, and the initialization vector is a 16-byte random number\n           that is stored as the first 16 bytes of the encrypted stream or string.\n           The output is the encrypted data to be stored in the PDF file.\n        \"\"\"\n        pack1 = struct.pack(\"<i\", idnum)[:3]\n        pack2 = struct.pack(\"<i\", generation)[:2]\n\n        assert self._key\n        key = self._key\n        n = 5 if self.V == 1 else self.Length // 8\n        key_data = key[:n] + pack1 + pack2\n        key_hash = hashlib.md5(key_data)\n        rc4_key = key_hash.digest()[: min(n + 5, 16)]\n\n        # for AES-128\n        key_hash.update(b\"sAlT\")\n        aes128_key = key_hash.digest()[: min(n + 5, 16)]\n\n        # for AES-256\n        aes256_key = key\n\n        stm_crypt = self._get_crypt(self.StmF, rc4_key, aes128_key, aes256_key)\n        str_crypt = self._get_crypt(self.StrF, rc4_key, aes128_key, aes256_key)\n        ef_crypt = self._get_crypt(self.EFF, rc4_key, aes128_key, aes256_key)\n\n        return CryptFilter(stm_crypt, str_crypt, ef_crypt)\n\n    @staticmethod\n    def _get_crypt(\n        method: str, rc4_key: bytes, aes128_key: bytes, aes256_key: bytes\n    ) -> CryptBase:\n        if method == \"/AESV2\":\n            return CryptAES(aes128_key)\n        if method == \"/AESV3\":\n            return CryptAES(aes256_key)\n        if method == \"/Identity\":\n            return CryptIdentity()\n\n        return CryptRC4(rc4_key)\n\n    @staticmethod\n    def _encode_password(password: Union[bytes, str]) -> bytes:\n        if isinstance(password, str):\n            try:\n                pwd = password.encode(\"latin-1\")\n            except Exception:\n                pwd = password.encode(\"utf-8\")\n        else:\n            pwd = password\n        return pwd\n\n    def verify(self, password: Union[bytes, str]) -> PasswordType:\n        pwd = self._encode_password(password)\n        key, rc = self.verify_v4(pwd) if self.V <= 4 else self.verify_v5(pwd)\n        if rc != PasswordType.NOT_DECRYPTED:\n            self._password_type = rc\n            self._key = key\n        return rc\n\n    def verify_v4(self, password: bytes) -> tuple[bytes, PasswordType]:\n        # verify owner password first\n        key = AlgV4.verify_owner_password(\n            password,\n            self.R,\n            self.Length,\n            self.values.O,\n            self.values.U,\n            self.P,\n            self.id1_entry,\n            self.EncryptMetadata,\n        )\n        if key:\n            return key, PasswordType.OWNER_PASSWORD\n        key = AlgV4.verify_user_password(\n            password,\n            self.R,\n            self.Length,\n            self.values.O,\n            self.values.U,\n            self.P,\n            self.id1_entry,\n            self.EncryptMetadata,\n        )\n        if key:\n            return key, PasswordType.USER_PASSWORD\n        return b\"\", PasswordType.NOT_DECRYPTED\n\n    def verify_v5(self, password: bytes) -> tuple[bytes, PasswordType]:\n        # TODO: use SASLprep process\n        # verify owner password first\n        key = AlgV5.verify_owner_password(\n            self.R, password, self.values.O, self.values.OE, self.values.U\n        )\n        rc = PasswordType.OWNER_PASSWORD\n        if not key:\n            key = AlgV5.verify_user_password(\n                self.R, password, self.values.U, self.values.UE\n            )\n            rc = PasswordType.USER_PASSWORD\n        if not key:\n            return b\"\", PasswordType.NOT_DECRYPTED\n\n        # verify Perms\n        self._are_permissions_valid = AlgV5.verify_perms(key, self.values.Perms, self.P, self.EncryptMetadata)\n        if not self._are_permissions_valid:\n            logger_warning(\"ignore '/Perms' verify failed\", __name__)\n        return key, rc\n\n    def write_entry(\n        self, user_password: str, owner_password: Optional[str]\n    ) -> DictionaryObject:\n        user_pwd = self._encode_password(user_password)\n        owner_pwd = self._encode_password(owner_password) if owner_password else None\n        if owner_pwd is None:\n            owner_pwd = user_pwd\n\n        if self.V <= 4:\n            self.compute_values_v4(user_pwd, owner_pwd)\n        else:\n            self._key = secrets.token_bytes(self.Length // 8)\n            values = AlgV5.generate_values(\n                self.R, user_pwd, owner_pwd, self._key, self.P, self.EncryptMetadata\n            )\n            self.values.O = values[\"/O\"]\n            self.values.U = values[\"/U\"]\n            self.values.OE = values[\"/OE\"]\n            self.values.UE = values[\"/UE\"]\n            self.values.Perms = values[\"/Perms\"]\n\n        dict_obj = DictionaryObject()\n        dict_obj[NameObject(\"/V\")] = NumberObject(self.V)\n        dict_obj[NameObject(\"/R\")] = NumberObject(self.R)\n        dict_obj[NameObject(\"/Length\")] = NumberObject(self.Length)\n        dict_obj[NameObject(\"/P\")] = NumberObject(self.P)\n        dict_obj[NameObject(\"/Filter\")] = NameObject(\"/Standard\")\n        # ignore /EncryptMetadata\n\n        dict_obj[NameObject(\"/O\")] = ByteStringObject(self.values.O)\n        dict_obj[NameObject(\"/U\")] = ByteStringObject(self.values.U)\n\n        if self.V >= 4:\n            # TODO: allow different method\n            std_cf = DictionaryObject()\n            std_cf[NameObject(\"/AuthEvent\")] = NameObject(\"/DocOpen\")\n            std_cf[NameObject(\"/CFM\")] = NameObject(self.StmF)\n            std_cf[NameObject(\"/Length\")] = NumberObject(self.Length // 8)\n            cf = DictionaryObject()\n            cf[NameObject(\"/StdCF\")] = std_cf\n            dict_obj[NameObject(\"/CF\")] = cf\n            dict_obj[NameObject(\"/StmF\")] = NameObject(\"/StdCF\")\n            dict_obj[NameObject(\"/StrF\")] = NameObject(\"/StdCF\")\n            # ignore EFF\n            # dict_obj[NameObject(\"/EFF\")] = NameObject(\"/StdCF\")\n\n        if self.V >= 5:\n            dict_obj[NameObject(\"/OE\")] = ByteStringObject(self.values.OE)\n            dict_obj[NameObject(\"/UE\")] = ByteStringObject(self.values.UE)\n            dict_obj[NameObject(\"/Perms\")] = ByteStringObject(self.values.Perms)\n        return dict_obj\n\n    def compute_values_v4(self, user_password: bytes, owner_password: bytes) -> None:\n        rc4_key = AlgV4.compute_O_value_key(owner_password, self.R, self.Length)\n        o_value = AlgV4.compute_O_value(rc4_key, user_password, self.R)\n\n        key = AlgV4.compute_key(\n            user_password,\n            self.R,\n            self.Length,\n            o_value,\n            self.P,\n            self.id1_entry,\n            self.EncryptMetadata,\n        )\n        u_value = AlgV4.compute_U_value(key, self.R, self.id1_entry)\n\n        self._key = key\n        self.values.O = o_value\n        self.values.U = u_value\n\n    @staticmethod\n    def read(encryption_entry: DictionaryObject, first_id_entry: bytes) -> \"Encryption\":\n        if encryption_entry.get(\"/Filter\") != \"/Standard\":\n            raise NotImplementedError(\n                \"only Standard PDF encryption handler is available\"\n            )\n        if \"/SubFilter\" in encryption_entry:\n            raise NotImplementedError(\"/SubFilter NOT supported\")\n\n        stm_filter = \"/V2\"\n        str_filter = \"/V2\"\n        ef_filter = \"/V2\"\n\n        alg_ver = encryption_entry.get(\"/V\", 0)\n        if alg_ver not in (1, 2, 3, 4, 5):\n            raise NotImplementedError(f\"Encryption V={alg_ver} NOT supported\")\n        if alg_ver >= 4:\n            filters = encryption_entry[\"/CF\"]\n\n            stm_filter = encryption_entry.get(\"/StmF\", \"/Identity\")\n            str_filter = encryption_entry.get(\"/StrF\", \"/Identity\")\n            ef_filter = encryption_entry.get(\"/EFF\", stm_filter)\n\n            if stm_filter != \"/Identity\":\n                stm_filter = filters[stm_filter][\"/CFM\"]  # type: ignore\n            if str_filter != \"/Identity\":\n                str_filter = filters[str_filter][\"/CFM\"]  # type: ignore\n            if ef_filter != \"/Identity\":\n                ef_filter = filters[ef_filter][\"/CFM\"]  # type: ignore\n\n            allowed_methods = (\"/Identity\", \"/V2\", \"/AESV2\", \"/AESV3\")\n            if stm_filter not in allowed_methods:\n                raise NotImplementedError(f\"StmF Method {stm_filter} NOT supported!\")\n            if str_filter not in allowed_methods:\n                raise NotImplementedError(f\"StrF Method {str_filter} NOT supported!\")\n            if ef_filter not in allowed_methods:\n                raise NotImplementedError(f\"EFF Method {ef_filter} NOT supported!\")\n\n        alg_rev = cast(int, encryption_entry[\"/R\"])\n        perm_flags = cast(int, encryption_entry[\"/P\"])\n        key_bits = encryption_entry.get(\"/Length\", 40)\n        if alg_ver == 4 and stm_filter == \"/AESV2\":\n            cf_dict = cast(DictionaryObject, filters[encryption_entry[\"/StmF\"]])  # type: ignore[index]\n            # CF /Length is in bytes (default 16 for AES-128), convert to bits\n            key_bits = cast(int, cf_dict.get(\"/Length\", 16)) * 8\n        encrypt_metadata = encryption_entry.get(\"/EncryptMetadata\")\n        encrypt_metadata = (\n            encrypt_metadata.value if encrypt_metadata is not None else True\n        )\n        values = EncryptionValues()\n        values.O = cast(ByteStringObject, encryption_entry[\"/O\"]).original_bytes\n        values.U = cast(ByteStringObject, encryption_entry[\"/U\"]).original_bytes\n        values.OE = encryption_entry.get(\"/OE\", ByteStringObject()).original_bytes\n        values.UE = encryption_entry.get(\"/UE\", ByteStringObject()).original_bytes\n        values.Perms = encryption_entry.get(\"/Perms\", ByteStringObject()).original_bytes\n        return Encryption(\n            V=alg_ver,\n            R=alg_rev,\n            Length=key_bits,\n            P=perm_flags,\n            EncryptMetadata=encrypt_metadata,\n            first_id_entry=first_id_entry,\n            values=values,\n            StrF=str_filter,\n            StmF=stm_filter,\n            EFF=ef_filter,\n            entry=encryption_entry,  # Dummy entry for the moment; will get removed\n        )\n\n    @staticmethod\n    def make(\n        alg: EncryptAlgorithm, permissions: int, first_id_entry: bytes\n    ) -> \"Encryption\":\n        alg_ver, alg_rev, key_bits = alg\n\n        stm_filter, str_filter, ef_filter = \"/V2\", \"/V2\", \"/V2\"\n\n        if alg == EncryptAlgorithm.AES_128:\n            stm_filter, str_filter, ef_filter = \"/AESV2\", \"/AESV2\", \"/AESV2\"\n        elif alg in (EncryptAlgorithm.AES_256_R5, EncryptAlgorithm.AES_256):\n            stm_filter, str_filter, ef_filter = \"/AESV3\", \"/AESV3\", \"/AESV3\"\n\n        return Encryption(\n            V=alg_ver,\n            R=alg_rev,\n            Length=key_bits,\n            P=permissions,\n            EncryptMetadata=True,\n            first_id_entry=first_id_entry,\n            values=None,\n            StrF=str_filter,\n            StmF=stm_filter,\n            EFF=ef_filter,\n            entry=DictionaryObject(),  # Dummy entry for the moment; will get removed\n        )\n"
  },
  {
    "path": "pypdf/_font.py",
    "content": "from collections.abc import Sequence\nfrom dataclasses import dataclass, field\nfrom typing import Any, Union, cast\n\nfrom pypdf.generic import ArrayObject, DictionaryObject, NameObject\n\nfrom ._cmap import get_encoding\nfrom ._codecs.adobe_glyphs import adobe_glyphs\nfrom ._utils import logger_warning\nfrom .constants import FontFlags\n\n\n@dataclass(frozen=True)\nclass FontDescriptor:\n    \"\"\"\n    Represents the FontDescriptor dictionary as defined in the PDF specification.\n    This contains both descriptive and metric information.\n\n    The defaults are derived from the mean values of the 14 core fonts, rounded\n    to 100.\n    \"\"\"\n\n    name: str = \"Unknown\"\n    family: str = \"Unknown\"\n    weight: str = \"Unknown\"\n\n    ascent: float = 700.0\n    descent: float = -200.0\n    cap_height: float = 600.0\n    x_height: float = 500.0\n    italic_angle: float = 0.0  # Non-italic\n    flags: int = 32  # Non-serif, non-symbolic, not fixed width\n    bbox: tuple[float, float, float, float] = field(default_factory=lambda: (-100.0, -200.0, 1000.0, 900.0))\n\n\n@dataclass(frozen=True)\nclass CoreFontMetrics:\n    font_descriptor: FontDescriptor\n    character_widths: dict[str, int]\n\n\n@dataclass\nclass Font:\n    \"\"\"\n    A font object for use during text extraction and for producing\n    text appearance streams.\n\n    Attributes:\n        name: Font name, derived from font[\"/BaseFont\"]\n        character_map: The font's character map\n        encoding: Font encoding\n        sub_type: The font type, such as Type1, TrueType, or Type3.\n        font_descriptor: Font metrics, including a mapping of characters to widths\n        character_widths: A mapping of characters to widths\n        space_width: The width of a space, or an approximation\n        interpretable: Default True. If False, the font glyphs cannot\n            be translated to characters, e.g. Type3 fonts that do not define\n            a '/ToUnicode' mapping.\n\n    \"\"\"\n\n    name: str\n    encoding: Union[str, dict[int, str]]\n    character_map: dict[Any, Any] = field(default_factory=dict)\n    sub_type: str = \"Unknown\"\n    font_descriptor: FontDescriptor = field(default_factory=FontDescriptor)\n    character_widths: dict[str, int] = field(default_factory=lambda: {\"default\": 500})\n    space_width: Union[float, int] = 250\n    interpretable: bool = True\n\n    @staticmethod\n    def _collect_tt_t1_character_widths(\n        pdf_font_dict: DictionaryObject,\n        char_map: dict[Any, Any],\n        encoding: Union[str, dict[int, str]],\n        current_widths: dict[str, int]\n    ) -> None:\n        \"\"\"Parses a TrueType or Type1 font's /Widths array from a font dictionary and updates character widths\"\"\"\n        widths_array = cast(ArrayObject, pdf_font_dict[\"/Widths\"])\n        first_char = pdf_font_dict.get(\"/FirstChar\", 0)\n        if not isinstance(encoding, str):\n            # This means that encoding is a dict\n            current_widths.update({\n                encoding.get(idx + first_char, chr(idx + first_char)): width\n                for idx, width in enumerate(widths_array)\n            })\n            return\n\n        # We map the character code directly to the character\n        # using the string encoding\n        for idx, width in enumerate(widths_array):\n            # Often \"idx == 0\" will denote the .notdef character, but we add it anyway\n            char_code = idx + first_char  # This is a raw code\n            # Get the \"raw\" character or byte representation\n            raw_char = bytes([char_code]).decode(encoding, \"surrogatepass\")\n            # Translate raw_char to the REAL Unicode character using the char_map\n            unicode_char = char_map.get(raw_char)\n            if unicode_char:\n                current_widths[unicode_char] = int(width)\n            else:\n                current_widths[raw_char] = int(width)\n\n    @staticmethod\n    def _collect_cid_character_widths(\n        d_font: DictionaryObject, char_map: dict[Any, Any], current_widths: dict[str, int]\n    ) -> None:\n        \"\"\"Parses the /W array from a DescendantFont dictionary and updates character widths.\"\"\"\n        ord_map = {\n            ord(_target): _surrogate\n            for _target, _surrogate in char_map.items()\n            if isinstance(_target, str)\n        }\n        # /W width definitions have two valid formats which can be mixed and matched:\n        #   (1) A character start index followed by a list of widths, e.g.\n        #       `45 [500 600 700]` applies widths 500, 600, 700 to characters 45-47.\n        #   (2) A character start index, a character stop index, and a width, e.g.\n        #       `45 65 500` applies width 500 to characters 45-65.\n        skip_count = 0\n        _w = d_font.get(\"/W\", [])\n        for idx, w_entry in enumerate(_w):\n            w_entry = w_entry.get_object()\n            if skip_count:\n                skip_count -= 1\n                continue\n            if not isinstance(w_entry, (int, float)):\n                # We should never get here due to skip_count above. But\n                # sometimes we do.\n                logger_warning(f\"Expected numeric value for width, got {w_entry}. Ignoring it.\", __name__)\n                continue\n            # check for format (1): `int [int int int int ...]`\n            w_next_entry = _w[idx + 1].get_object()\n            if isinstance(w_next_entry, Sequence):\n                start_idx, width_list = w_entry, w_next_entry\n                current_widths.update(\n                    {\n                        ord_map[_cidx]: _width\n                        for _cidx, _width in zip(\n                            range(\n                                cast(int, start_idx),\n                                cast(int, start_idx) + len(width_list),\n                                1,\n                            ),\n                            width_list,\n                        )\n                        if _cidx in ord_map\n                    }\n                )\n                skip_count = 1\n            # check for format (2): `int int int`\n            elif isinstance(w_next_entry, (int, float)) and isinstance(\n                _w[idx + 2].get_object(), (int, float)\n            ):\n                start_idx, stop_idx, const_width = (\n                    w_entry,\n                    w_next_entry,\n                    _w[idx + 2].get_object(),\n                )\n                current_widths.update(\n                    {\n                        ord_map[_cidx]: const_width\n                        for _cidx in range(\n                            cast(int, start_idx), cast(int, stop_idx + 1), 1\n                        )\n                        if _cidx in ord_map\n                    }\n                )\n                skip_count = 2\n            else:\n                # This handles the case of out of bounds (reaching the end of the width definitions\n                # while expecting more elements).\n                logger_warning(\n                    f\"Invalid font width definition. Last element: {w_entry}.\",\n                    __name__\n                )\n\n    @staticmethod\n    def _add_default_width(current_widths: dict[str, int], flags: int) -> None:\n        if not current_widths:\n            current_widths[\"default\"] = 500\n            return\n\n        if \" \" in current_widths and current_widths[\" \"] != 0:\n            # Setting default to once or twice the space width, depending on fixed pitch\n            if (flags & FontFlags.FIXED_PITCH) == FontFlags.FIXED_PITCH:\n                current_widths[\"default\"] = current_widths[\" \"]\n                return\n\n            current_widths[\"default\"] = int(2 * current_widths[\" \"])\n            return\n\n        # Use the average width of existing glyph widths\n        valid_widths = [w for w in current_widths.values() if w > 0]\n        current_widths[\"default\"] = sum(valid_widths) // len(valid_widths) if valid_widths else 500\n\n    @staticmethod\n    def _parse_font_descriptor(font_descriptor_obj: DictionaryObject) -> dict[str, Any]:\n        font_descriptor_kwargs: dict[Any, Any] = {}\n        for source_key, target_key in [\n            (\"/FontName\", \"name\"),\n            (\"/FontFamily\", \"family\"),\n            (\"/FontWeight\", \"weight\"),\n            (\"/Ascent\", \"ascent\"),\n            (\"/Descent\", \"descent\"),\n            (\"/CapHeight\", \"cap_height\"),\n            (\"/XHeight\", \"x_height\"),\n            (\"/ItalicAngle\", \"italic_angle\"),\n            (\"/Flags\", \"flags\"),\n            (\"/FontBBox\", \"bbox\")\n        ]:\n            if source_key in font_descriptor_obj:\n                font_descriptor_kwargs[target_key] = font_descriptor_obj[source_key]\n        # Handle missing bbox gracefully - PDFs may have fonts without valid bounding boxes\n        if \"bbox\" in font_descriptor_kwargs:\n            bbox_tuple = tuple(map(float, font_descriptor_kwargs[\"bbox\"]))\n            assert len(bbox_tuple) == 4, bbox_tuple\n            font_descriptor_kwargs[\"bbox\"] = bbox_tuple\n        return font_descriptor_kwargs\n\n    @classmethod\n    def from_font_resource(\n        cls,\n        pdf_font_dict: DictionaryObject,\n    ) -> \"Font\":\n        from pypdf._codecs.core_font_metrics import CORE_FONT_METRICS  # noqa: PLC0415\n\n        # Can collect base_font, name and encoding directly from font resource\n        name = pdf_font_dict.get(\"/BaseFont\", \"Unknown\").removeprefix(\"/\")\n        sub_type = pdf_font_dict.get(\"/Subtype\", \"Unknown\").removeprefix(\"/\")\n        encoding, character_map = get_encoding(pdf_font_dict)\n        font_descriptor = None\n        character_widths: dict[str, int] = {}\n        interpretable = True\n\n        # Deal with fonts by type; Type1, TrueType and certain Type3\n        if pdf_font_dict.get(\"/Subtype\") in (\"/Type1\", \"/MMType1\", \"/TrueType\", \"/Type3\"):\n            # Type3 fonts that do not specify a \"/ToUnicode\" mapping cannot be\n            # reliably converted into character codes unless all named chars\n            # in /CharProcs map to a standard adobe glyph. See §9.10.2 of the\n            # PDF 1.7 standard.\n            if sub_type == \"Type3\" and \"/ToUnicode\" not in pdf_font_dict:\n                interpretable = all(\n                    cname in adobe_glyphs\n                    for cname in pdf_font_dict.get(\"/CharProcs\") or []\n                )\n            if interpretable:  # Save some overhead if font is not interpretable\n                if \"/Widths\" in pdf_font_dict:\n                    cls._collect_tt_t1_character_widths(\n                        pdf_font_dict, character_map, encoding, character_widths\n                    )\n                elif name in CORE_FONT_METRICS:\n                    font_descriptor = CORE_FONT_METRICS[name].font_descriptor\n                    character_widths = CORE_FONT_METRICS[name].character_widths\n                if \"/FontDescriptor\" in pdf_font_dict:\n                    font_descriptor_obj = pdf_font_dict.get(\"/FontDescriptor\", DictionaryObject()).get_object()\n                    if \"/MissingWidth\" in font_descriptor_obj:\n                        character_widths[\"default\"] = cast(int, font_descriptor_obj[\"/MissingWidth\"].get_object())\n                    font_descriptor = FontDescriptor(**cls._parse_font_descriptor(font_descriptor_obj))\n                elif \"/FontBBox\" in pdf_font_dict:\n                    # For Type3 without Font Descriptor but with FontBBox, see Table 110 in the PDF specification 2.0\n                    bbox_tuple = tuple(map(float, cast(ArrayObject, pdf_font_dict[\"/FontBBox\"])))\n                    assert len(bbox_tuple) == 4, bbox_tuple\n                    font_descriptor = FontDescriptor(name=name, bbox=bbox_tuple)\n\n        else:\n            # Composite font or CID font - CID fonts have a /W array mapping character codes\n            # to widths stashed in /DescendantFonts. No need to test for /DescendantFonts though,\n            # because all other fonts have already been dealt with.\n            d_font: DictionaryObject\n            for d_font_idx, d_font in enumerate(\n                cast(ArrayObject, pdf_font_dict[\"/DescendantFonts\"])\n            ):\n                d_font = cast(DictionaryObject, d_font.get_object())\n                cast(ArrayObject, pdf_font_dict[\"/DescendantFonts\"])[d_font_idx] = d_font\n                cls._collect_cid_character_widths(\n                    d_font, character_map, character_widths\n                )\n                if \"/DW\" in d_font:\n                    character_widths[\"default\"] = cast(int, d_font[\"/DW\"].get_object())\n                font_descriptor_obj = d_font.get(\"/FontDescriptor\", DictionaryObject()).get_object()\n                font_descriptor = FontDescriptor(**cls._parse_font_descriptor(font_descriptor_obj))\n\n        if not font_descriptor:\n            font_descriptor = FontDescriptor(name=name)\n\n        if character_widths.get(\"default\", 0) == 0:\n            cls._add_default_width(character_widths, font_descriptor.flags)\n        space_width = character_widths.get(\" \", 0)\n        if space_width == 0:\n            if (font_descriptor.flags & FontFlags.FIXED_PITCH) == FontFlags.FIXED_PITCH:\n                space_width = character_widths[\"default\"]\n            else:\n                space_width = character_widths[\"default\"] // 2\n\n        return cls(\n            name=name,\n            sub_type=sub_type,\n            encoding=encoding,\n            font_descriptor=font_descriptor,\n            character_map=character_map,\n            character_widths=character_widths,\n            space_width=space_width,\n            interpretable=interpretable\n        )\n\n    def as_font_resource(self) -> DictionaryObject:\n        # For now, this returns a font resource that only works with the 14 Adobe Core fonts.\n        return (\n            DictionaryObject({\n                NameObject(\"/Subtype\"): NameObject(\"/Type1\"),\n                NameObject(\"/Name\"): NameObject(f\"/{self.name}\"),\n                NameObject(\"/Type\"): NameObject(\"/Font\"),\n                NameObject(\"/BaseFont\"): NameObject(f\"/{self.name}\"),\n                NameObject(\"/Encoding\"): NameObject(\"/WinAnsiEncoding\")\n            })\n        )\n\n    def text_width(self, text: str = \"\") -> float:\n        \"\"\"Sum of character widths specified in PDF font for the supplied text.\"\"\"\n        return sum(\n            [self.character_widths.get(char, self.character_widths[\"default\"]) for char in text], 0.0\n        )\n"
  },
  {
    "path": "pypdf/_page.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com>\n#\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nimport math\nfrom collections.abc import Iterable, Iterator, Sequence\nfrom copy import deepcopy\nfrom dataclasses import asdict, dataclass\nfrom decimal import Decimal\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import (\n    Any,\n    Callable,\n    Literal,\n    Optional,\n    Union,\n    cast,\n    overload,\n)\n\nfrom ._font import Font\nfrom ._protocols import PdfCommonDocProtocol\nfrom ._text_extraction import (\n    _layout_mode,\n)\nfrom ._text_extraction._text_extractor import TextExtraction\nfrom ._utils import (\n    CompressedTransformationMatrix,\n    TransformationMatrixType,\n    _human_readable_bytes,\n    deprecate,\n    logger_warning,\n    matrix_multiply,\n)\nfrom .constants import _INLINE_IMAGE_KEY_MAPPING, _INLINE_IMAGE_VALUE_MAPPING\nfrom .constants import AnnotationDictionaryAttributes as ADA\nfrom .constants import ImageAttributes as IA\nfrom .constants import PageAttributes as PG\nfrom .constants import Resources as RES\nfrom .errors import PageSizeNotDefinedError, PdfReadError\nfrom .generic import (\n    ArrayObject,\n    ContentStream,\n    DictionaryObject,\n    EncodedStreamObject,\n    FloatObject,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    PdfObject,\n    RectangleObject,\n    StreamObject,\n    is_null_or_none,\n)\n\ntry:\n    from PIL.Image import Image\n\n    pil_not_imported = False\nexcept ImportError:\n    Image = object  # type: ignore[assignment,misc,unused-ignore]  # TODO: Remove unused-ignore on Python 3.10\n    pil_not_imported = True  # error will be raised only when using images\n\nMERGE_CROP_BOX = \"cropbox\"  # pypdf <= 3.4.0 used \"trimbox\"\n\n\ndef _get_rectangle(self: Any, name: str, defaults: Iterable[str]) -> RectangleObject:\n    retval: Union[None, RectangleObject, ArrayObject, IndirectObject] = self.get(name)\n    if isinstance(retval, RectangleObject):\n        return retval\n    if is_null_or_none(retval):\n        for d in defaults:\n            retval = self.get(d)\n            if retval is not None:\n                break\n    if isinstance(retval, IndirectObject):\n        retval = self.pdf.get_object(retval)\n    if isinstance(retval, ArrayObject) and (length := len(retval)) > 4:\n        logger_warning(f\"Expected four values, got {length}: {retval}\", __name__)\n        retval = RectangleObject(tuple(retval[:4]))\n    else:\n        retval = RectangleObject(retval)  # type: ignore\n    _set_rectangle(self, name, retval)\n    return retval\n\n\ndef _set_rectangle(self: Any, name: str, value: Union[RectangleObject, float]) -> None:\n    self[NameObject(name)] = value\n\n\ndef _delete_rectangle(self: Any, name: str) -> None:\n    del self[name]\n\n\ndef _create_rectangle_accessor(name: str, fallback: Iterable[str]) -> property:\n    return property(\n        lambda self: _get_rectangle(self, name, fallback),\n        lambda self, value: _set_rectangle(self, name, value),\n        lambda self: _delete_rectangle(self, name),\n    )\n\n\nclass Transformation:\n    \"\"\"\n    Represent a 2D transformation.\n\n    The transformation between two coordinate systems is represented by a 3-by-3\n    transformation matrix with the following form::\n\n        a b 0\n        c d 0\n        e f 1\n\n    Because a transformation matrix has only six elements that can be changed,\n    it is usually specified in PDF as the six-element array [ a b c d e f ].\n\n    Coordinate transformations are expressed as matrix multiplications::\n\n                                 a b 0\n     [ x′ y′ 1 ] = [ x y 1 ] ×   c d 0\n                                 e f 1\n\n\n    Example:\n        >>> from pypdf import PdfWriter, Transformation\n        >>> page = PdfWriter().add_blank_page(800, 600)\n        >>> op = Transformation().scale(sx=2, sy=3).translate(tx=10, ty=20)\n        >>> page.add_transformation(op)\n\n    \"\"\"\n\n    def __init__(self, ctm: CompressedTransformationMatrix = (1, 0, 0, 1, 0, 0)) -> None:\n        self.ctm = ctm\n\n    @property\n    def matrix(self) -> TransformationMatrixType:\n        \"\"\"\n        Return the transformation matrix as a tuple of tuples in the form:\n\n        ((a, b, 0), (c, d, 0), (e, f, 1))\n        \"\"\"\n        return (\n            (self.ctm[0], self.ctm[1], 0),\n            (self.ctm[2], self.ctm[3], 0),\n            (self.ctm[4], self.ctm[5], 1),\n        )\n\n    @staticmethod\n    def compress(matrix: TransformationMatrixType) -> CompressedTransformationMatrix:\n        \"\"\"\n        Compresses the transformation matrix into a tuple of (a, b, c, d, e, f).\n\n        Args:\n            matrix: The transformation matrix as a tuple of tuples.\n\n        Returns:\n            A tuple representing the transformation matrix as (a, b, c, d, e, f)\n\n        \"\"\"\n        return (\n            matrix[0][0],\n            matrix[0][1],\n            matrix[1][0],\n            matrix[1][1],\n            matrix[2][0],\n            matrix[2][1],\n        )\n\n    def _to_cm(self) -> str:\n        # Returns the cm operation string for the given transformation matrix\n        return (\n            f\"{self.ctm[0]:.4f} {self.ctm[1]:.4f} {self.ctm[2]:.4f} \"\n            f\"{self.ctm[3]:.4f} {self.ctm[4]:.4f} {self.ctm[5]:.4f} cm\"\n        )\n\n    def transform(self, m: \"Transformation\") -> \"Transformation\":\n        \"\"\"\n        Apply one transformation to another.\n\n        Args:\n            m: a Transformation to apply.\n\n        Returns:\n            A new ``Transformation`` instance\n\n        Example:\n            >>> from pypdf import PdfWriter, Transformation\n            >>> height, width = 40, 50\n            >>> page = PdfWriter().add_blank_page(800, 600)\n            >>> op = Transformation((1, 0, 0, -1, 0, height)) # vertical mirror\n            >>> op = Transformation().transform(Transformation((-1, 0, 0, 1, width, 0)))  # horizontal mirror\n            >>> page.add_transformation(op)\n\n        \"\"\"\n        ctm = Transformation.compress(matrix_multiply(self.matrix, m.matrix))\n        return Transformation(ctm)\n\n    def translate(self, tx: float = 0, ty: float = 0) -> \"Transformation\":\n        \"\"\"\n        Translate the contents of a page.\n\n        Args:\n            tx: The translation along the x-axis.\n            ty: The translation along the y-axis.\n\n        Returns:\n            A new ``Transformation`` instance\n\n        \"\"\"\n        m = self.ctm\n        return Transformation(ctm=(m[0], m[1], m[2], m[3], m[4] + tx, m[5] + ty))\n\n    def scale(\n        self, sx: Optional[float] = None, sy: Optional[float] = None\n    ) -> \"Transformation\":\n        \"\"\"\n        Scale the contents of a page towards the origin of the coordinate system.\n\n        Typically, that is the lower-left corner of the page. That can be\n        changed by translating the contents / the page boxes.\n\n        Args:\n            sx: The scale factor along the x-axis.\n            sy: The scale factor along the y-axis.\n\n        Returns:\n            A new Transformation instance with the scaled matrix.\n\n        \"\"\"\n        if sx is None and sy is None:\n            raise ValueError(\"Either sx or sy must be specified\")\n        if sx is None:\n            sx = sy\n        if sy is None:\n            sy = sx\n        assert sx is not None\n        assert sy is not None\n        op: TransformationMatrixType = ((sx, 0, 0), (0, sy, 0), (0, 0, 1))\n        ctm = Transformation.compress(matrix_multiply(self.matrix, op))\n        return Transformation(ctm)\n\n    def rotate(self, rotation: float) -> \"Transformation\":\n        \"\"\"\n        Rotate the contents of a page.\n\n        Args:\n            rotation: The angle of rotation in degrees.\n\n        Returns:\n            A new ``Transformation`` instance with the rotated matrix.\n\n        \"\"\"\n        rotation = math.radians(rotation)\n        op: TransformationMatrixType = (\n            (math.cos(rotation), math.sin(rotation), 0),\n            (-math.sin(rotation), math.cos(rotation), 0),\n            (0, 0, 1),\n        )\n        ctm = Transformation.compress(matrix_multiply(self.matrix, op))\n        return Transformation(ctm)\n\n    def __repr__(self) -> str:\n        return f\"Transformation(ctm={self.ctm})\"\n\n    @overload\n    def apply_on(self, pt: list[float], as_object: bool = False) -> list[float]:\n        ...\n\n    @overload\n    def apply_on(\n        self, pt: tuple[float, float], as_object: bool = False\n    ) -> tuple[float, float]:\n        ...\n\n    def apply_on(\n        self,\n        pt: Union[tuple[float, float], list[float]],\n        as_object: bool = False,\n    ) -> Union[tuple[float, float], list[float]]:\n        \"\"\"\n        Apply the transformation matrix on the given point.\n\n        Args:\n            pt: A tuple or list representing the point in the form (x, y).\n            as_object: If True, return items as FloatObject, otherwise as plain floats.\n\n        Returns:\n            A tuple or list representing the transformed point in the form (x', y')\n\n        \"\"\"\n        typ = FloatObject if as_object else float\n        pt1 = (\n            typ(float(pt[0]) * self.ctm[0] + float(pt[1]) * self.ctm[2] + self.ctm[4]),\n            typ(float(pt[0]) * self.ctm[1] + float(pt[1]) * self.ctm[3] + self.ctm[5]),\n        )\n        return list(pt1) if isinstance(pt, list) else pt1\n\n\n@dataclass\nclass ImageFile:\n    \"\"\"\n    Image within the PDF file. *This object is not designed to be built.*\n\n    This object should not be modified except using :func:`ImageFile.replace` to replace the image with a new one.\n    \"\"\"\n\n    name: str = \"\"\n    \"\"\"\n    Filename as identified within the PDF file.\n    \"\"\"\n\n    data: bytes = b\"\"\n    \"\"\"\n    Data as bytes.\n    \"\"\"\n\n    image: Optional[Image] = None\n    \"\"\"\n    Data as PIL image.\n    \"\"\"\n\n    indirect_reference: Optional[IndirectObject] = None\n    \"\"\"\n    Reference to the object storing the stream.\n    \"\"\"\n\n    def replace(self, new_image: Image, **kwargs: Any) -> None:\n        \"\"\"\n        Replace the image with a new PIL image.\n\n        Args:\n            new_image (PIL.Image.Image): The new PIL image to replace the existing image.\n            **kwargs: Additional keyword arguments to pass to `Image.save()`.\n\n        Raises:\n            TypeError: If the image is inline or in a PdfReader.\n            TypeError: If the image does not belong to a PdfWriter.\n            TypeError: If `new_image` is not a PIL Image.\n\n        Note:\n            This method replaces the existing image with a new image.\n            It is not allowed for inline images or images within a PdfReader.\n            The `kwargs` parameter allows passing additional parameters\n            to `Image.save()`, such as quality.\n\n        \"\"\"\n        if pil_not_imported:\n            raise ImportError(\n                \"pillow is required to do image extraction. \"\n                \"It can be installed via 'pip install pypdf[image]'\"\n            )\n\n        from ._reader import PdfReader  # noqa: PLC0415\n        from .generic import DictionaryObject, PdfObject  # noqa: PLC0415\n        from .generic._image_xobject import _xobj_to_image  # noqa: PLC0415\n\n        if self.indirect_reference is None:\n            raise TypeError(\"Cannot update an inline image.\")\n        if not hasattr(self.indirect_reference.pdf, \"_id_translated\"):\n            raise TypeError(\"Cannot update an image not belonging to a PdfWriter.\")\n        if not isinstance(new_image, Image):\n            raise TypeError(\"new_image shall be a PIL Image\")\n        b = BytesIO()\n        new_image.save(b, \"PDF\", **kwargs)\n        reader = PdfReader(b)\n        page_image = reader.pages[0].images[0]\n        assert page_image.indirect_reference is not None\n        self.indirect_reference.pdf._objects[self.indirect_reference.idnum - 1] = (\n            page_image.indirect_reference.get_object()\n        )\n        cast(\n            PdfObject, self.indirect_reference.get_object()\n        ).indirect_reference = self.indirect_reference\n        # change the object attributes\n        extension, byte_stream, img = _xobj_to_image(\n            cast(DictionaryObject, self.indirect_reference.get_object()),\n            pillow_parameters=kwargs,\n        )\n        assert extension is not None\n        self.name = self.name[: self.name.rfind(\".\")] + extension\n        self.data = byte_stream\n        self.image = img\n\n    def __str__(self) -> str:\n        return f\"{self.__class__.__name__}(name={self.name}, data: {_human_readable_bytes(len(self.data))})\"\n\n    def __repr__(self) -> str:\n        return self.__str__()[:-1] + f\", hash: {hash(self.data)})\"\n\n\nclass VirtualListImages(Sequence[ImageFile]):\n    \"\"\"\n    Provides access to images referenced within a page.\n    Only one copy will be returned if the usage is used on the same page multiple times.\n    See :func:`PageObject.images` for more details.\n    \"\"\"\n\n    def __init__(\n        self,\n        ids_function: Callable[[], list[Union[str, list[str]]]],\n        get_function: Callable[[Union[str, list[str], tuple[str]]], ImageFile],\n    ) -> None:\n        self.ids_function = ids_function\n        self.get_function = get_function\n        self.current = -1\n\n    def __len__(self) -> int:\n        return len(self.ids_function())\n\n    def keys(self) -> list[Union[str, list[str]]]:\n        return self.ids_function()\n\n    def items(self) -> list[tuple[Union[str, list[str]], ImageFile]]:\n        return [(x, self[x]) for x in self.ids_function()]\n\n    @overload\n    def __getitem__(self, index: Union[int, str, list[str]]) -> ImageFile:\n        ...\n\n    @overload\n    def __getitem__(self, index: slice) -> Sequence[ImageFile]:\n        ...\n\n    def __getitem__(\n        self, index: Union[int, slice, str, list[str], tuple[str]]\n    ) -> Union[ImageFile, Sequence[ImageFile]]:\n        lst = self.ids_function()\n        if isinstance(index, slice):\n            indices = range(*index.indices(len(self)))\n            lst = [lst[x] for x in indices]\n            cls = type(self)\n            return cls((lambda: lst), self.get_function)\n        if isinstance(index, (str, list, tuple)):\n            return self.get_function(index)\n        if not isinstance(index, int):\n            raise TypeError(\"Invalid sequence indices type\")\n        len_self = len(lst)\n        if index < 0:\n            # support negative indexes\n            index += len_self\n        if not (0 <= index < len_self):\n            raise IndexError(\"Sequence index out of range\")\n        return self.get_function(lst[index])\n\n    def __iter__(self) -> Iterator[ImageFile]:\n        for i in range(len(self)):\n            yield self[i]\n\n    def __str__(self) -> str:\n        p = [f\"Image_{i}={n}\" for i, n in enumerate(self.ids_function())]\n        return f\"[{', '.join(p)}]\"\n\n\nclass PageObject(DictionaryObject):\n    \"\"\"\n    PageObject represents a single page within a PDF file.\n\n    Typically these objects will be created by accessing the\n    :attr:`pages<pypdf.PdfReader.pages>` property of the\n    :class:`PdfReader<pypdf.PdfReader>` class, but it is\n    also possible to create an empty page with the\n    :meth:`create_blank_page()<pypdf._page.PageObject.create_blank_page>` static method.\n\n    Args:\n        pdf: PDF file the page belongs to.\n        indirect_reference: Stores the original indirect reference to\n            this object in its source PDF\n\n    \"\"\"\n\n    original_page: \"PageObject\"  # very local use in writer when appending\n\n    def __init__(\n        self,\n        pdf: Optional[PdfCommonDocProtocol] = None,\n        indirect_reference: Optional[IndirectObject] = None,\n    ) -> None:\n        DictionaryObject.__init__(self)\n        self.pdf = pdf\n        self.inline_images: Optional[dict[str, ImageFile]] = None\n        self.indirect_reference = indirect_reference\n        if not is_null_or_none(indirect_reference):\n            assert indirect_reference is not None, \"mypy\"\n            self.update(cast(DictionaryObject, indirect_reference.get_object()))\n        self._font_width_maps: dict[str, tuple[dict[str, float], str, float]] = {}\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Note: this function is overloaded to return the same results\n        as a DictionaryObject.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash(\n            (DictionaryObject, tuple(((k, v.hash_bin()) for k, v in self.items())))\n        )\n\n    def hash_value_data(self) -> bytes:\n        data = super().hash_value_data()\n        data += f\"{id(self)}\".encode()\n        return data\n\n    @property\n    def user_unit(self) -> float:\n        \"\"\"\n        A read-only positive number giving the size of user space units.\n\n        It is in multiples of 1/72 inch. Hence a value of 1 means a user\n        space unit is 1/72 inch, and a value of 3 means that a user\n        space unit is 3/72 inch.\n        \"\"\"\n        return self.get(PG.USER_UNIT, 1)\n\n    @staticmethod\n    def create_blank_page(\n        pdf: Optional[PdfCommonDocProtocol] = None,\n        width: Union[float, Decimal, None] = None,\n        height: Union[float, Decimal, None] = None,\n    ) -> \"PageObject\":\n        \"\"\"\n        Return a new blank page.\n\n        If ``width`` or ``height`` is ``None``, try to get the page size\n        from the last page of *pdf*.\n\n        Args:\n            pdf: PDF file the page is within.\n            width: The width of the new page expressed in default user\n                space units.\n            height: The height of the new page expressed in default user\n                space units.\n\n        Returns:\n            The new blank page\n\n        Raises:\n            PageSizeNotDefinedError: if ``pdf`` is ``None`` or contains\n                no page\n\n        \"\"\"\n        page = PageObject(pdf)\n\n        # Creates a new page (cf PDF Reference §7.7.3.3)\n        page.__setitem__(NameObject(PG.TYPE), NameObject(\"/Page\"))\n        page.__setitem__(NameObject(PG.PARENT), NullObject())\n        page.__setitem__(NameObject(PG.RESOURCES), DictionaryObject())\n        if width is None or height is None:\n            if pdf is not None and len(pdf.pages) > 0:\n                lastpage = pdf.pages[len(pdf.pages) - 1]\n                width = lastpage.mediabox.width\n                height = lastpage.mediabox.height\n            else:\n                raise PageSizeNotDefinedError\n        page.__setitem__(\n            NameObject(PG.MEDIABOX), RectangleObject((0, 0, width, height))  # type: ignore\n        )\n\n        return page\n\n    def _get_ids_image(\n        self,\n        obj: Optional[DictionaryObject] = None,\n        ancest: Optional[list[str]] = None,\n        call_stack: Optional[list[Any]] = None,\n    ) -> list[Union[str, list[str]]]:\n        if call_stack is None:\n            call_stack = []\n        _i = getattr(obj, \"indirect_reference\", None)\n        if _i in call_stack:\n            return []\n        call_stack.append(_i)\n        if self.inline_images is None:\n            self.inline_images = self._get_inline_images()\n        if obj is None:\n            obj = self\n        if ancest is None:\n            ancest = []\n        lst: list[Union[str, list[str]]] = []\n        if (\n                PG.RESOURCES not in obj or\n                is_null_or_none(resources := obj[PG.RESOURCES]) or\n                RES.XOBJECT not in cast(DictionaryObject, resources)\n        ):\n            return [] if self.inline_images is None else list(self.inline_images.keys())\n\n        x_object = resources[RES.XOBJECT].get_object()  # type: ignore\n        for o in x_object:\n            if not isinstance(x_object[o], StreamObject):\n                continue\n            if x_object[o][IA.SUBTYPE] == \"/Image\":\n                lst.append(o if len(ancest) == 0 else [*ancest, o])\n            else:  # is a form with possible images inside\n                lst.extend(self._get_ids_image(x_object[o], [*ancest, o], call_stack))\n        assert self.inline_images is not None\n        lst.extend(list(self.inline_images.keys()))\n        return lst\n\n    def _get_image(\n        self,\n        id: Union[str, list[str], tuple[str]],\n        obj: Optional[DictionaryObject] = None,\n    ) -> ImageFile:\n        if obj is None:\n            obj = cast(DictionaryObject, self)\n        if isinstance(id, tuple):\n            id = list(id)\n        if isinstance(id, list) and len(id) == 1:\n            id = id[0]\n        xobjs: Optional[DictionaryObject] = None\n        try:\n            xobjs = cast(\n                DictionaryObject, cast(DictionaryObject, obj[PG.RESOURCES])[RES.XOBJECT]\n            )\n        except KeyError as exc:\n            if not (id[0] == \"~\" and id[-1] == \"~\"):\n                raise KeyError(\n                    f\"Cannot access image object {id} without XObject resources\"\n                ) from exc\n        if isinstance(id, str):\n            if id[0] == \"~\" and id[-1] == \"~\":\n                if self.inline_images is None:\n                    self.inline_images = self._get_inline_images()\n                if self.inline_images is None:\n                    raise KeyError(\"No inline image can be found\")\n                return self.inline_images[id]\n\n            assert xobjs is not None\n            from .generic._image_xobject import _xobj_to_image  # noqa: PLC0415\n            imgd = _xobj_to_image(cast(DictionaryObject, xobjs[id]))\n            extension, byte_stream = imgd[:2]\n            return ImageFile(\n                name=f\"{id[1:]}{extension}\",\n                data=byte_stream,\n                image=imgd[2],\n                indirect_reference=xobjs[id].indirect_reference,\n            )\n        # in a subobject\n        assert xobjs is not None\n        ids = id[1:]\n        return self._get_image(ids, cast(DictionaryObject, xobjs[id[0]]))\n\n    @property\n    def images(self) -> VirtualListImages:\n        \"\"\"\n        Read-only property emulating a list of images on a page.\n\n        Get a list of all images on the page. The key can be:\n        - A string (for the top object)\n        - A tuple (for images within XObject forms)\n        - An integer\n\n        Examples:\n            * `reader.pages[0].images[0]`        # return first image\n            * `reader.pages[0].images['/I0']`    # return image '/I0'\n            * `reader.pages[0].images['/TP1','/Image1']` # return image '/Image1' within '/TP1' XObject form\n            * `for img in reader.pages[0].images:` # loops through all objects\n\n        images.keys() and images.items() can be used.\n\n        The ImageFile has the following properties:\n\n            * `.name` : name of the object\n            * `.data` : bytes of the object\n            * `.image` : PIL Image Object\n            * `.indirect_reference` : object reference\n\n        and the following methods:\n            `.replace(new_image: PIL.Image.Image, **kwargs)` :\n                replace the image in the pdf with the new image\n                applying the saving parameters indicated (such as quality)\n\n        Example usage:\n\n            reader.pages[0].images[0].replace(Image.open(\"new_image.jpg\"), quality=20)\n\n        Inline images are extracted and named ~0~, ~1~, ..., with the\n        indirect_reference set to None.\n\n        \"\"\"\n        return VirtualListImages(self._get_ids_image, self._get_image)\n\n    def _translate_value_inline_image(self, k: str, v: PdfObject) -> PdfObject:\n        \"\"\"Translate values used in inline image\"\"\"\n        try:\n            v = NameObject(_INLINE_IMAGE_VALUE_MAPPING[cast(str, v)])\n        except (TypeError, KeyError):\n            if isinstance(v, NameObject):\n                # It is a custom name, thus we have to look in resources.\n                # The only applicable case is for ColorSpace.\n                try:\n                    res = cast(DictionaryObject, self[\"/Resources\"])[\"/ColorSpace\"]\n                    v = cast(DictionaryObject, res)[v]\n                except KeyError:  # for res and v\n                    raise PdfReadError(f\"Cannot find resource entry {v} for {k}\")\n        return v\n\n    def _get_inline_images(self) -> dict[str, ImageFile]:\n        \"\"\"Load inline images. Entries will be identified as `~1~`.\"\"\"\n        content = self.get_contents()\n        if is_null_or_none(content):\n            return {}\n        imgs_data = []\n        assert content is not None, \"mypy\"\n        for param, ope in content.operations:\n            if ope == b\"INLINE IMAGE\":\n                imgs_data.append(\n                    {\"settings\": param[\"settings\"], \"__streamdata__\": param[\"data\"]}\n                )\n            elif ope in (b\"BI\", b\"EI\", b\"ID\"):  # pragma: no cover\n                raise PdfReadError(\n                    f\"{ope!r} operator met whereas not expected, \"\n                    \"please share use case with pypdf dev team\"\n                )\n        files = {}\n        for num, ii in enumerate(imgs_data):\n            init = {\n                \"__streamdata__\": ii[\"__streamdata__\"],\n                \"/Length\": len(ii[\"__streamdata__\"]),\n            }\n            for k, v in ii[\"settings\"].items():\n                if k in {\"/Length\", \"/L\"}:  # no length is expected\n                    continue\n                if isinstance(v, list):\n                    v = ArrayObject(\n                        [self._translate_value_inline_image(k, x) for x in v]\n                    )\n                else:\n                    v = self._translate_value_inline_image(k, v)\n                k = NameObject(_INLINE_IMAGE_KEY_MAPPING[k])\n                if k not in init:\n                    init[k] = v\n            ii[\"object\"] = EncodedStreamObject.initialize_from_dictionary(init)\n            from .generic._image_xobject import _xobj_to_image  # noqa: PLC0415\n            extension, byte_stream, img = _xobj_to_image(ii[\"object\"])\n            files[f\"~{num}~\"] = ImageFile(\n                name=f\"~{num}~{extension}\",\n                data=byte_stream,\n                image=img,\n                indirect_reference=None,\n            )\n        return files\n\n    @property\n    def rotation(self) -> int:\n        \"\"\"\n        The visual rotation of the page.\n\n        This number has to be a multiple of 90 degrees: 0, 90, 180, or 270 are\n        valid values. This property does not affect ``/Contents``.\n        \"\"\"\n        rotate_obj = self.get(PG.ROTATE, 0)\n        return rotate_obj if isinstance(rotate_obj, int) else rotate_obj.get_object()\n\n    @rotation.setter\n    def rotation(self, r: float) -> None:\n        self[NameObject(PG.ROTATE)] = NumberObject((((int(r) + 45) // 90) * 90) % 360)\n\n    def transfer_rotation_to_content(self) -> None:\n        \"\"\"\n        Apply the rotation of the page to the content and the media/crop/...\n        boxes.\n\n        It is recommended to apply this function before page merging.\n        \"\"\"\n        r = -self.rotation  # rotation to apply is in the otherway\n        self.rotation = 0\n        mb = RectangleObject(self.mediabox)\n        trsf = (\n            Transformation()\n            .translate(\n                -float(mb.left + mb.width / 2), -float(mb.bottom + mb.height / 2)\n            )\n            .rotate(r)\n        )\n        pt1 = trsf.apply_on(mb.lower_left)\n        pt2 = trsf.apply_on(mb.upper_right)\n        trsf = trsf.translate(-min(pt1[0], pt2[0]), -min(pt1[1], pt2[1]))\n        self.add_transformation(trsf, False)\n        for b in [\"/MediaBox\", \"/CropBox\", \"/BleedBox\", \"/TrimBox\", \"/ArtBox\"]:\n            if b in self:\n                rr = RectangleObject(self[b])  # type: ignore\n                pt1 = trsf.apply_on(rr.lower_left)\n                pt2 = trsf.apply_on(rr.upper_right)\n                self[NameObject(b)] = RectangleObject(\n                    (\n                        min(pt1[0], pt2[0]),\n                        min(pt1[1], pt2[1]),\n                        max(pt1[0], pt2[0]),\n                        max(pt1[1], pt2[1]),\n                    )\n                )\n\n    def rotate(self, angle: int) -> \"PageObject\":\n        \"\"\"\n        Rotate a page clockwise by increments of 90 degrees.\n\n        Args:\n            angle: Angle to rotate the page. Must be an increment of 90 deg.\n\n        Returns:\n            The rotated PageObject\n\n        \"\"\"\n        if angle % 90 != 0:\n            raise ValueError(\"Rotation angle must be a multiple of 90\")\n        self[NameObject(PG.ROTATE)] = NumberObject(self.rotation + angle)\n        return self\n\n    def _merge_resources(\n        self,\n        res1: DictionaryObject,\n        res2: DictionaryObject,\n        resource: Any,\n        new_res1: bool = True,\n    ) -> tuple[dict[str, Any], dict[str, Any]]:\n        try:\n            assert isinstance(self.indirect_reference, IndirectObject)\n            pdf = self.indirect_reference.pdf\n            is_pdf_writer = hasattr(\n                pdf, \"_add_object\"\n            )  # expect isinstance(pdf, PdfWriter)\n        except (AssertionError, AttributeError):\n            pdf = None\n            is_pdf_writer = False\n\n        def compute_unique_key(base_key: str) -> tuple[str, bool]:\n            \"\"\"\n            Find a key that either doesn't already exist or has the same value\n            (indicated by the bool)\n\n            Args:\n                base_key: An index is added to this to get the computed key\n\n            Returns:\n                A tuple (computed key, bool) where the boolean indicates\n                if there is a resource of the given computed_key with the same\n                value.\n\n            \"\"\"\n            value = page2res.raw_get(base_key)\n            # TODO: a possible improvement for writer, the indirect_reference\n            # cannot be found because translated\n\n            # try the current key first (e.g. \"foo\"), but otherwise iterate\n            # through \"foo-0\", \"foo-1\", etc. new_res can contain only finitely\n            # many keys, thus this'll eventually end, even if it's been crafted\n            # to be maximally annoying.\n            computed_key = base_key\n            idx = 0\n            while computed_key in new_res:\n                if new_res.raw_get(computed_key) == value:\n                    # there's already a resource of this name, with the exact\n                    # same value\n                    return computed_key, True\n                computed_key = f\"{base_key}-{idx}\"\n                idx += 1\n            return computed_key, False\n\n        if new_res1:\n            new_res = DictionaryObject()\n            new_res.update(res1.get(resource, DictionaryObject()).get_object())\n        else:\n            new_res = cast(DictionaryObject, res1[resource])\n        page2res = cast(\n            DictionaryObject, res2.get(resource, DictionaryObject()).get_object()\n        )\n        rename_res = {}\n        for key in page2res:\n            unique_key, same_value = compute_unique_key(key)\n            newname = NameObject(unique_key)\n            if key != unique_key:\n                # we have to use a different name for this\n                rename_res[key] = newname\n\n            if not same_value:\n                if is_pdf_writer:\n                    new_res[newname] = page2res.raw_get(key).clone(pdf)\n                    try:\n                        new_res[newname] = new_res[newname].indirect_reference\n                    except AttributeError:\n                        pass\n                else:\n                    new_res[newname] = page2res.raw_get(key)\n            lst = sorted(new_res.items())\n            new_res.clear()\n            for el in lst:\n                new_res[el[0]] = el[1]\n        return new_res, rename_res\n\n    @staticmethod\n    def _content_stream_rename(\n        stream: ContentStream,\n        rename: dict[Any, Any],\n        pdf: Optional[PdfCommonDocProtocol],\n    ) -> ContentStream:\n        if not rename:\n            return stream\n        stream = ContentStream(stream, pdf)\n        for operands, _operator in stream.operations:\n            if isinstance(operands, list):\n                for i, op in enumerate(operands):\n                    if isinstance(op, NameObject):\n                        operands[i] = rename.get(op, op)\n            elif isinstance(operands, dict):\n                for i, op in operands.items():\n                    if isinstance(op, NameObject):\n                        operands[i] = rename.get(op, op)\n            else:\n                raise KeyError(f\"Type of operands is {type(operands)}\")\n        return stream\n\n    @staticmethod\n    def _add_transformation_matrix(\n        contents: Any,\n        pdf: Optional[PdfCommonDocProtocol],\n        ctm: CompressedTransformationMatrix,\n    ) -> ContentStream:\n        \"\"\"Add transformation matrix at the beginning of the given contents stream.\"\"\"\n        contents = ContentStream(contents, pdf)\n        contents.operations.insert(\n            0,\n            [\n                [FloatObject(x) for x in ctm],\n                b\"cm\",\n            ],\n        )\n        return contents\n\n    def _get_contents_as_bytes(self) -> Optional[bytes]:\n        \"\"\"\n        Return the page contents as bytes.\n\n        Returns:\n            The ``/Contents`` object as bytes, or ``None`` if it doesn't exist.\n\n        \"\"\"\n        if PG.CONTENTS in self:\n            obj = self[PG.CONTENTS].get_object()\n            if isinstance(obj, list):\n                return b\"\".join(x.get_object().get_data() for x in obj)\n            return cast(EncodedStreamObject, obj).get_data()\n        return None\n\n    def get_contents(self) -> Optional[ContentStream]:\n        \"\"\"\n        Access the page contents.\n\n        Returns:\n            The ``/Contents`` object, or ``None`` if it does not exist.\n            ``/Contents`` is optional, as described in §7.7.3.3 of the PDF Reference.\n\n        \"\"\"\n        if PG.CONTENTS in self:\n            try:\n                pdf = cast(IndirectObject, self.indirect_reference).pdf\n            except AttributeError:\n                pdf = None\n            obj = self[PG.CONTENTS]\n            if is_null_or_none(obj):\n                return None\n            resolved_object = obj.get_object()\n            return ContentStream(resolved_object, pdf)\n        return None\n\n    def replace_contents(\n        self, content: Union[None, ContentStream, EncodedStreamObject, ArrayObject]\n    ) -> None:\n        \"\"\"\n        Replace the page contents with the new content and nullify old objects\n        Args:\n            content: new content; if None delete the content field.\n        \"\"\"\n        if not hasattr(self, \"indirect_reference\") or self.indirect_reference is None:\n            # the page is not attached : the content is directly attached.\n            self[NameObject(PG.CONTENTS)] = content\n            return\n\n        from pypdf._writer import PdfWriter  # noqa: PLC0415\n        if not isinstance(self.indirect_reference.pdf, PdfWriter):\n            deprecate(\n                \"Calling `PageObject.replace_contents()` for pages not assigned to a writer is deprecated \"\n                \"and will be removed in pypdf 7.0.0. Attach the page to the writer first or use \"\n                \"`PdfWriter(clone_from=...)` directly. The existing approach has proved being unreliable.\"\n            )\n\n        writer = self.indirect_reference.pdf\n        if isinstance(self.get(PG.CONTENTS, None), ArrayObject):\n            content_array = cast(ArrayObject, self[PG.CONTENTS])\n            for reference in content_array:\n                try:\n                    writer._replace_object(indirect_reference=reference.indirect_reference, obj=NullObject())\n                except ValueError:\n                    # Occurs when called on PdfReader.\n                    pass\n\n        if isinstance(content, ArrayObject):\n            content = ArrayObject(writer._add_object(obj) for obj in content)\n\n        if is_null_or_none(content):\n            if PG.CONTENTS not in self:\n                return\n            assert self[PG.CONTENTS].indirect_reference is not None\n            writer._replace_object(indirect_reference=self[PG.CONTENTS].indirect_reference, obj=NullObject())\n            del self[PG.CONTENTS]\n        elif not hasattr(self.get(PG.CONTENTS, None), \"indirect_reference\"):\n            try:\n                self[NameObject(PG.CONTENTS)] = writer._add_object(content)\n            except AttributeError:\n                # applies at least for page not in writer\n                # as a backup solution, we put content as an object although not in accordance with pdf ref\n                # this will be fixed with the _add_object\n                self[NameObject(PG.CONTENTS)] = content\n        else:\n            assert content is not None, \"mypy\"\n            content.indirect_reference = self[\n                PG.CONTENTS\n            ].indirect_reference  # TODO: in the future may require generation management\n            try:\n                writer._replace_object(indirect_reference=content.indirect_reference, obj=content)\n            except AttributeError:\n                # applies at least for page not in writer\n                # as a backup solution, we put content as an object although not in accordance with pdf ref\n                # this will be fixed with the _add_object\n                self[NameObject(PG.CONTENTS)] = content\n        # forces recalculation of inline_images\n        self.inline_images = None\n\n    def merge_page(\n        self, page2: \"PageObject\", expand: bool = False, over: bool = True\n    ) -> None:\n        \"\"\"\n        Merge the content streams of two pages into one.\n\n        Resource references (e.g. fonts) are maintained from both pages.\n        The mediabox, cropbox, etc of this page are not altered.\n        The parameter page's content stream will\n        be added to the end of this page's content stream,\n        meaning that it will be drawn after, or \"on top\" of this page.\n\n        Args:\n            page2: The page to be merged into this one. Should be\n                an instance of :class:`PageObject<PageObject>`.\n            over: set the page2 content over page1 if True (default) else under\n            expand: If True, the current page dimensions will be\n                expanded to accommodate the dimensions of the page to be merged.\n\n        \"\"\"\n        self._merge_page(page2, over=over, expand=expand)\n\n    def _merge_page(\n        self,\n        page2: \"PageObject\",\n        page2transformation: Optional[Callable[[Any], ContentStream]] = None,\n        ctm: Optional[CompressedTransformationMatrix] = None,\n        over: bool = True,\n        expand: bool = False,\n    ) -> None:\n        # First we work on merging the resource dictionaries. This allows us\n        # to find out what symbols in the content streams we might need to\n        # rename.\n        try:\n            assert isinstance(self.indirect_reference, IndirectObject)\n            if hasattr(\n                self.indirect_reference.pdf, \"_add_object\"\n            ):  # to detect PdfWriter\n                return self._merge_page_writer(\n                    page2, page2transformation, ctm, over, expand\n                )\n        except (AssertionError, AttributeError):\n            pass\n\n        new_resources = DictionaryObject()\n        rename = {}\n        original_resources = cast(DictionaryObject, self.get(PG.RESOURCES, DictionaryObject()).get_object())\n        page2resources = cast(DictionaryObject, page2.get(PG.RESOURCES, DictionaryObject()).get_object())\n        new_annots = ArrayObject()\n\n        for page in (self, page2):\n            if PG.ANNOTS in page:\n                annots = page[PG.ANNOTS]\n                if isinstance(annots, ArrayObject):\n                    new_annots.extend(annots)\n\n        for res in (\n            RES.EXT_G_STATE,\n            RES.FONT,\n            RES.XOBJECT,\n            RES.COLOR_SPACE,\n            RES.PATTERN,\n            RES.SHADING,\n            RES.PROPERTIES,\n        ):\n            new, newrename = self._merge_resources(\n                original_resources, page2resources, res\n            )\n            if new:\n                new_resources[NameObject(res)] = new\n                rename.update(newrename)\n\n        # Combine /ProcSet sets, making sure there's a consistent order\n        new_resources[NameObject(RES.PROC_SET)] = ArrayObject(\n            sorted(\n                set(\n                    original_resources.get(RES.PROC_SET, ArrayObject()).get_object()\n                ).union(\n                    set(page2resources.get(RES.PROC_SET, ArrayObject()).get_object())\n                )\n            )\n        )\n\n        new_content_array = ArrayObject()\n        original_content = self.get_contents()\n        if original_content is not None:\n            original_content.isolate_graphics_state()\n            new_content_array.append(original_content)\n\n        page2content = page2.get_contents()\n        if page2content is not None:\n            rect = getattr(page2, MERGE_CROP_BOX)\n            page2content.operations.insert(\n                0,\n                (\n                    map(\n                        FloatObject,\n                        [\n                            rect.left,\n                            rect.bottom,\n                            rect.width,\n                            rect.height,\n                        ],\n                    ),\n                    b\"re\",\n                ),\n            )\n            page2content.operations.insert(1, ([], b\"W\"))\n            page2content.operations.insert(2, ([], b\"n\"))\n            if page2transformation is not None:\n                page2content = page2transformation(page2content)\n            page2content = PageObject._content_stream_rename(\n                page2content, rename, self.pdf\n            )\n            page2content.isolate_graphics_state()\n            if over:\n                new_content_array.append(page2content)\n            else:\n                new_content_array.insert(0, page2content)\n\n        # if expanding the page to fit a new page, calculate the new media box size\n        if expand:\n            self._expand_mediabox(page2, ctm)\n\n        self.replace_contents(ContentStream(new_content_array, self.pdf))\n        self[NameObject(PG.RESOURCES)] = new_resources\n        self[NameObject(PG.ANNOTS)] = new_annots\n        return None\n\n    def _merge_page_writer(\n        self,\n        page2: \"PageObject\",\n        page2transformation: Optional[Callable[[Any], ContentStream]] = None,\n        ctm: Optional[CompressedTransformationMatrix] = None,\n        over: bool = True,\n        expand: bool = False,\n    ) -> None:\n        # First we work on merging the resource dictionaries. This allows us\n        # to find which symbols in the content streams we might need to\n        # rename.\n        assert isinstance(self.indirect_reference, IndirectObject)\n        pdf = self.indirect_reference.pdf\n\n        rename = {}\n        if PG.RESOURCES not in self:\n            self[NameObject(PG.RESOURCES)] = DictionaryObject()\n        original_resources = cast(DictionaryObject, self[PG.RESOURCES].get_object())\n        if PG.RESOURCES not in page2:\n            page2resources = DictionaryObject()\n        else:\n            page2resources = cast(DictionaryObject, page2[PG.RESOURCES].get_object())\n\n        for res in (\n            RES.EXT_G_STATE,\n            RES.FONT,\n            RES.XOBJECT,\n            RES.COLOR_SPACE,\n            RES.PATTERN,\n            RES.SHADING,\n            RES.PROPERTIES,\n        ):\n            if res in page2resources:\n                if res not in original_resources:\n                    original_resources[NameObject(res)] = DictionaryObject()\n                _, newrename = self._merge_resources(\n                    original_resources, page2resources, res, False\n                )\n                rename.update(newrename)\n        # Combine /ProcSet sets.\n        if RES.PROC_SET in page2resources:\n            if RES.PROC_SET not in original_resources:\n                original_resources[NameObject(RES.PROC_SET)] = ArrayObject()\n            arr = cast(ArrayObject, original_resources[RES.PROC_SET])\n            for x in cast(ArrayObject, page2resources[RES.PROC_SET]):\n                if x not in arr:\n                    arr.append(x)\n            arr.sort()\n\n        if PG.ANNOTS in page2:\n            if PG.ANNOTS not in self:\n                self[NameObject(PG.ANNOTS)] = ArrayObject()\n            annots = cast(ArrayObject, self[PG.ANNOTS].get_object())\n            if ctm is None:\n                trsf = Transformation()\n            else:\n                trsf = Transformation(ctm)\n            # Ensure we are working on a copy of the list. Otherwise, if both pages\n            # are the same object, we might run into an infinite loop.\n            for a in cast(ArrayObject, deepcopy(page2[PG.ANNOTS])):\n                a = a.get_object()\n                aa = a.clone(\n                    pdf,\n                    ignore_fields=(\"/P\", \"/StructParent\", \"/Parent\"),\n                    force_duplicate=True,\n                )\n                r = cast(ArrayObject, a[\"/Rect\"])\n                pt1 = trsf.apply_on((r[0], r[1]), True)\n                pt2 = trsf.apply_on((r[2], r[3]), True)\n                aa[NameObject(\"/Rect\")] = ArrayObject(\n                    (\n                        min(pt1[0], pt2[0]),\n                        min(pt1[1], pt2[1]),\n                        max(pt1[0], pt2[0]),\n                        max(pt1[1], pt2[1]),\n                    )\n                )\n                if \"/QuadPoints\" in a:\n                    q = cast(ArrayObject, a[\"/QuadPoints\"])\n                    aa[NameObject(\"/QuadPoints\")] = ArrayObject(\n                        trsf.apply_on((q[0], q[1]), True)\n                        + trsf.apply_on((q[2], q[3]), True)\n                        + trsf.apply_on((q[4], q[5]), True)\n                        + trsf.apply_on((q[6], q[7]), True)\n                    )\n                try:\n                    aa[\"/Popup\"][NameObject(\"/Parent\")] = aa.indirect_reference\n                except KeyError:\n                    pass\n                try:\n                    aa[NameObject(\"/P\")] = self.indirect_reference\n                    annots.append(aa.indirect_reference)\n                except AttributeError:\n                    pass\n\n        new_content_array = ArrayObject()\n        original_content = self.get_contents()\n        if original_content is not None:\n            original_content.isolate_graphics_state()\n            new_content_array.append(original_content)\n\n        page2content = page2.get_contents()\n        if page2content is not None:\n            rect = getattr(page2, MERGE_CROP_BOX)\n            page2content.operations.insert(\n                0,\n                (\n                    map(\n                        FloatObject,\n                        [\n                            rect.left,\n                            rect.bottom,\n                            rect.width,\n                            rect.height,\n                        ],\n                    ),\n                    b\"re\",\n                ),\n            )\n            page2content.operations.insert(1, ([], b\"W\"))\n            page2content.operations.insert(2, ([], b\"n\"))\n            if page2transformation is not None:\n                page2content = page2transformation(page2content)\n            page2content = PageObject._content_stream_rename(\n                page2content, rename, self.pdf\n            )\n            page2content.isolate_graphics_state()\n            if over:\n                new_content_array.append(page2content)\n            else:\n                new_content_array.insert(0, page2content)\n\n        # if expanding the page to fit a new page, calculate the new media box size\n        if expand:\n            self._expand_mediabox(page2, ctm)\n\n        self.replace_contents(new_content_array)\n\n    def _expand_mediabox(\n        self, page2: \"PageObject\", ctm: Optional[CompressedTransformationMatrix]\n    ) -> None:\n        corners1 = (\n            self.mediabox.left.as_numeric(),\n            self.mediabox.bottom.as_numeric(),\n            self.mediabox.right.as_numeric(),\n            self.mediabox.top.as_numeric(),\n        )\n        corners2 = (\n            page2.mediabox.left.as_numeric(),\n            page2.mediabox.bottom.as_numeric(),\n            page2.mediabox.left.as_numeric(),\n            page2.mediabox.top.as_numeric(),\n            page2.mediabox.right.as_numeric(),\n            page2.mediabox.top.as_numeric(),\n            page2.mediabox.right.as_numeric(),\n            page2.mediabox.bottom.as_numeric(),\n        )\n        if ctm is not None:\n            ctm = tuple(float(x) for x in ctm)  # type: ignore[assignment]\n            new_x = tuple(\n                ctm[0] * corners2[i] + ctm[2] * corners2[i + 1] + ctm[4]\n                for i in range(0, 8, 2)\n            )\n            new_y = tuple(\n                ctm[1] * corners2[i] + ctm[3] * corners2[i + 1] + ctm[5]\n                for i in range(0, 8, 2)\n            )\n        else:\n            new_x = corners2[0:8:2]\n            new_y = corners2[1:8:2]\n        lowerleft = (min(new_x), min(new_y))\n        upperright = (max(new_x), max(new_y))\n        lowerleft = (min(corners1[0], lowerleft[0]), min(corners1[1], lowerleft[1]))\n        upperright = (\n            max(corners1[2], upperright[0]),\n            max(corners1[3], upperright[1]),\n        )\n\n        self.mediabox.lower_left = lowerleft\n        self.mediabox.upper_right = upperright\n\n    def merge_transformed_page(\n        self,\n        page2: \"PageObject\",\n        ctm: Union[CompressedTransformationMatrix, Transformation],\n        over: bool = True,\n        expand: bool = False,\n    ) -> None:\n        \"\"\"\n        Similar to :meth:`~pypdf._page.PageObject.merge_page`, but a transformation\n        matrix is applied to the merged stream.\n\n        Args:\n          page2: The page to be merged into this one.\n          ctm: a 6-element tuple containing the operands of the\n                 transformation matrix\n          over: set the page2 content over page1 if True (default) else under\n          expand: Whether the page should be expanded to fit the dimensions\n            of the page to be merged.\n\n        \"\"\"\n        if isinstance(ctm, Transformation):\n            ctm = ctm.ctm\n        self._merge_page(\n            page2,\n            lambda page2_content: PageObject._add_transformation_matrix(\n                page2_content, page2.pdf, ctm\n            ),\n            ctm,\n            over,\n            expand,\n        )\n\n    def merge_scaled_page(\n        self, page2: \"PageObject\", scale: float, over: bool = True, expand: bool = False\n    ) -> None:\n        \"\"\"\n        Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be merged\n        is scaled by applying a transformation matrix.\n\n        Args:\n          page2: The page to be merged into this one.\n          scale: The scaling factor\n          over: set the page2 content over page1 if True (default) else under\n          expand: Whether the page should be expanded to fit the\n            dimensions of the page to be merged.\n\n        \"\"\"\n        op = Transformation().scale(scale, scale)\n        self.merge_transformed_page(page2, op, over, expand)\n\n    def merge_rotated_page(\n        self,\n        page2: \"PageObject\",\n        rotation: float,\n        over: bool = True,\n        expand: bool = False,\n    ) -> None:\n        \"\"\"\n        Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be merged\n        is rotated by applying a transformation matrix.\n\n        Args:\n          page2: The page to be merged into this one.\n          rotation: The angle of the rotation, in degrees\n          over: set the page2 content over page1 if True (default) else under\n          expand: Whether the page should be expanded to fit the\n            dimensions of the page to be merged.\n\n        \"\"\"\n        op = Transformation().rotate(rotation)\n        self.merge_transformed_page(page2, op, over, expand)\n\n    def merge_translated_page(\n        self,\n        page2: \"PageObject\",\n        tx: float,\n        ty: float,\n        over: bool = True,\n        expand: bool = False,\n    ) -> None:\n        \"\"\"\n        Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be\n        merged is translated by applying a transformation matrix.\n\n        Args:\n          page2: the page to be merged into this one.\n          tx: The translation on X axis\n          ty: The translation on Y axis\n          over: set the page2 content over page1 if True (default) else under\n          expand: Whether the page should be expanded to fit the\n            dimensions of the page to be merged.\n\n        \"\"\"\n        op = Transformation().translate(tx, ty)\n        self.merge_transformed_page(page2, op, over, expand)\n\n    def add_transformation(\n        self,\n        ctm: Union[Transformation, CompressedTransformationMatrix],\n        expand: bool = False,\n    ) -> None:\n        \"\"\"\n        Apply a transformation matrix to the page.\n\n        Args:\n            ctm: A 6-element tuple containing the operands of the\n                transformation matrix. Alternatively, a\n                :py:class:`Transformation<pypdf.Transformation>`\n                object can be passed.\n\n        See :doc:`/user/cropping-and-transforming`.\n\n        \"\"\"\n        if isinstance(ctm, Transformation):\n            ctm = ctm.ctm\n        content = self.get_contents()\n        if content is not None:\n            content = PageObject._add_transformation_matrix(content, self.pdf, ctm)\n            content.isolate_graphics_state()\n            self.replace_contents(content)\n        # if expanding the page to fit a new page, calculate the new media box size\n        if expand:\n            corners = [\n                self.mediabox.left.as_numeric(),\n                self.mediabox.bottom.as_numeric(),\n                self.mediabox.left.as_numeric(),\n                self.mediabox.top.as_numeric(),\n                self.mediabox.right.as_numeric(),\n                self.mediabox.top.as_numeric(),\n                self.mediabox.right.as_numeric(),\n                self.mediabox.bottom.as_numeric(),\n            ]\n\n            ctm = tuple(float(x) for x in ctm)  # type: ignore[assignment]\n            new_x = [\n                ctm[0] * corners[i] + ctm[2] * corners[i + 1] + ctm[4]\n                for i in range(0, 8, 2)\n            ]\n            new_y = [\n                ctm[1] * corners[i] + ctm[3] * corners[i + 1] + ctm[5]\n                for i in range(0, 8, 2)\n            ]\n\n            self.mediabox.lower_left = (min(new_x), min(new_y))\n            self.mediabox.upper_right = (max(new_x), max(new_y))\n\n    def scale(self, sx: float, sy: float) -> None:\n        \"\"\"\n        Scale a page by the given factors by applying a transformation matrix\n        to its content and updating the page size.\n\n        This updates the various page boundaries (bleedbox, trimbox, etc.)\n        and the contents of the page.\n\n        Args:\n            sx: The scaling factor on horizontal axis.\n            sy: The scaling factor on vertical axis.\n\n        \"\"\"\n        self.add_transformation((sx, 0, 0, sy, 0, 0))\n        self.bleedbox = self.bleedbox.scale(sx, sy)\n        self.trimbox = self.trimbox.scale(sx, sy)\n        self.artbox = self.artbox.scale(sx, sy)\n        self.cropbox = self.cropbox.scale(sx, sy)\n        self.mediabox = self.mediabox.scale(sx, sy)\n\n        if PG.ANNOTS in self:\n            annotations = self[PG.ANNOTS]\n            if isinstance(annotations, ArrayObject):\n                for annotation in annotations:\n                    annotation_obj = annotation.get_object()\n                    if ADA.Rect in annotation_obj:\n                        rectangle = annotation_obj[ADA.Rect]\n                        if isinstance(rectangle, ArrayObject):\n                            rectangle[0] = FloatObject(float(rectangle[0]) * sx)\n                            rectangle[1] = FloatObject(float(rectangle[1]) * sy)\n                            rectangle[2] = FloatObject(float(rectangle[2]) * sx)\n                            rectangle[3] = FloatObject(float(rectangle[3]) * sy)\n\n        if PG.VP in self:\n            viewport = self[PG.VP]\n            if isinstance(viewport, ArrayObject):\n                bbox = viewport[0][\"/BBox\"]\n            else:\n                bbox = viewport[\"/BBox\"]  # type: ignore\n            scaled_bbox = RectangleObject(\n                (\n                    float(bbox[0]) * sx,\n                    float(bbox[1]) * sy,\n                    float(bbox[2]) * sx,\n                    float(bbox[3]) * sy,\n                )\n            )\n            if isinstance(viewport, ArrayObject):\n                self[NameObject(PG.VP)][NumberObject(0)][  # type: ignore\n                    NameObject(\"/BBox\")\n                ] = scaled_bbox\n            else:\n                self[NameObject(PG.VP)][NameObject(\"/BBox\")] = scaled_bbox  # type: ignore\n\n    def scale_by(self, factor: float) -> None:\n        \"\"\"\n        Scale a page by the given factor by applying a transformation matrix to\n        its content and updating the page size.\n\n        Args:\n            factor: The scaling factor (for both X and Y axis).\n\n        \"\"\"\n        self.scale(factor, factor)\n\n    def scale_to(self, width: float, height: float) -> None:\n        \"\"\"\n        Scale a page to the specified dimensions by applying a transformation\n        matrix to its content and updating the page size.\n\n        Args:\n            width: The new width.\n            height: The new height.\n\n        \"\"\"\n        sx = width / float(self.mediabox.width)\n        sy = height / float(self.mediabox.height)\n        self.scale(sx, sy)\n\n    def compress_content_streams(self, level: int = -1) -> None:\n        \"\"\"\n        Compress the size of this page by joining all content streams and\n        applying a FlateDecode filter.\n\n        However, it is possible that this function will perform no action if\n        content stream compression becomes \"automatic\".\n        \"\"\"\n        content = self.get_contents()\n        if content is not None:\n            content_obj = content.flate_encode(level)\n            try:\n                content.indirect_reference.pdf._objects[  # type: ignore\n                    content.indirect_reference.idnum - 1  # type: ignore\n                ] = content_obj\n            except AttributeError:\n                if self.indirect_reference is not None and hasattr(\n                    self.indirect_reference.pdf, \"_add_object\"\n                ):\n                    self.replace_contents(content_obj)\n                else:\n                    raise ValueError(\"Page must be part of a PdfWriter\")\n\n    @property\n    def page_number(self) -> Optional[int]:\n        \"\"\"\n        Read-only property which returns the page number within the PDF file.\n\n        Returns:\n            Page number; None if the page is not attached to a PDF.\n\n        \"\"\"\n        if self.indirect_reference is None:\n            return None\n        try:\n            lst = self.indirect_reference.pdf.pages\n            return lst.index(self)\n        except ValueError:\n            return None\n\n    def _debug_for_extract(self) -> str:  # pragma: no cover\n        out = \"\"\n        for ope, op in ContentStream(\n            self[\"/Contents\"].get_object(), self.pdf, \"bytes\"\n        ).operations:\n            if op == b\"TJ\":\n                s = [x for x in ope[0] if isinstance(x, str)]\n            else:\n                s = []\n            out += op.decode(\"utf-8\") + \" \" + \"\".join(s) + ope.__repr__() + \"\\n\"\n        out += \"\\n=============================\\n\"\n        try:\n            for fo in self[PG.RESOURCES][\"/Font\"]:  # type:ignore\n                out += fo + \"\\n\"\n                out += self[PG.RESOURCES][\"/Font\"][fo].__repr__() + \"\\n\"  # type:ignore\n                try:\n                    enc_repr = self[PG.RESOURCES][\"/Font\"][fo][  # type:ignore\n                        \"/Encoding\"\n                    ].__repr__()\n                    out += enc_repr + \"\\n\"\n                except Exception:\n                    pass\n                try:\n                    out += (\n                        self[PG.RESOURCES][\"/Font\"][fo][  # type:ignore\n                            \"/ToUnicode\"\n                        ]\n                        .get_data()\n                        .decode()\n                        + \"\\n\"\n                    )\n                except Exception:\n                    pass\n\n        except KeyError:\n            out += \"No Font\\n\"\n        return out\n\n    def _extract_text(\n        self,\n        obj: Any,\n        pdf: Any,\n        orientations: tuple[int, ...] = (0, 90, 180, 270),\n        space_width: float = 200.0,\n        content_key: Optional[str] = PG.CONTENTS,\n        visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None,\n        visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None,\n        visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None,\n    ) -> str:\n        \"\"\"\n        See extract_text for most arguments.\n\n        Args:\n            content_key: indicate the default key where to extract data\n                None = the object; this allows reusing the function on an XObject\n                default = \"/Content\"\n\n        \"\"\"\n        extractor = TextExtraction()\n        font_resources: dict[str, DictionaryObject] = {}\n        fonts: dict[str, Font] = {}\n\n        try:\n            objr = obj\n            while NameObject(PG.RESOURCES) not in objr:\n                # /Resources can be inherited so we look to parents\n                objr = objr[\"/Parent\"].get_object()\n                # If no parents then no /Resources will be available,\n                # so an exception will be raised\n            resources_dict = cast(DictionaryObject, objr[PG.RESOURCES])\n        except Exception:\n            # No resources means no text is possible (no font); we consider the\n            # file as not damaged, no need to check for TJ or Tj\n            return \"\"\n\n        if (\n            not is_null_or_none(resources_dict)\n            and \"/Font\" in resources_dict\n            and (font_resources_dict := cast(DictionaryObject, resources_dict[\"/Font\"]))\n        ):\n            for font_resource in font_resources_dict:\n                try:\n                    font_resource_object = cast(DictionaryObject, font_resources_dict[font_resource].get_object())\n                    font_resources[font_resource] = font_resource_object\n                    fonts[font_resource] = Font.from_font_resource(font_resource_object)\n                    # Override space width, if applicable\n                    if fonts[font_resource].character_widths.get(\" \", 0) == 0:\n                        fonts[font_resource].space_width = space_width\n                except (AttributeError, TypeError):\n                    pass\n\n        try:\n            content = (\n                obj[content_key].get_object() if isinstance(content_key, str) else obj\n            )\n            if not isinstance(content, ContentStream):\n                content = ContentStream(content, pdf, \"bytes\")\n        except (AttributeError, KeyError):  # no content can be extracted (certainly empty page)\n            return \"\"\n        # We check all strings are TextStringObjects. ByteStringObjects\n        # are strings where the byte->string encoding was unknown, so adding\n        # them to the text here would be gibberish.\n\n        # Initialize the extractor with the necessary parameters\n        extractor.initialize_extraction(orientations, visitor_text, font_resources, fonts)\n\n        for operands, operator in content.operations:\n            if visitor_operand_before is not None:\n                visitor_operand_before(operator, operands, extractor.cm_matrix, extractor.tm_matrix)\n            # Multiple operators are handled here\n            if operator == b\"'\":\n                extractor.process_operation(b\"T*\", [])\n                extractor.process_operation(b\"Tj\", operands)\n            elif operator == b'\"':\n                extractor.process_operation(b\"Tw\", [operands[0]])\n                extractor.process_operation(b\"Tc\", [operands[1]])\n                extractor.process_operation(b\"T*\", [])\n                extractor.process_operation(b\"Tj\", operands[2:])\n            elif operator == b\"TJ\":\n                # The space width may be smaller than the font width, so the width should be 95%.\n                _confirm_space_width = extractor._space_width * 0.95\n                if operands:\n                    for op in operands[0]:\n                        if isinstance(op, (str, bytes)):\n                            extractor.process_operation(b\"Tj\", [op])\n                        if isinstance(op, (int, float, NumberObject, FloatObject)) and (\n                            abs(float(op)) >= _confirm_space_width\n                            and extractor.text\n                            and extractor.text[-1] != \" \"\n                        ):\n                            extractor.process_operation(b\"Tj\", [\" \"])\n            elif operator == b\"TD\":\n                extractor.process_operation(b\"TL\", [-operands[1]])\n                extractor.process_operation(b\"Td\", operands)\n            elif operator == b\"Do\":\n                extractor.output += extractor.text\n                if visitor_text is not None:\n                    visitor_text(\n                        extractor.text,\n                        extractor.memo_cm,\n                        extractor.memo_tm,\n                        extractor.font_resource,\n                        extractor.font_size,\n                    )\n                try:\n                    if extractor.output[-1] != \"\\n\":\n                        extractor.output += \"\\n\"\n                        if visitor_text is not None:\n                            visitor_text(\n                                \"\\n\",\n                                extractor.memo_cm,\n                                extractor.memo_tm,\n                                extractor.font_resource,\n                                extractor.font_size,\n                            )\n                except IndexError:\n                    pass\n                try:\n                    xobj = resources_dict[\"/XObject\"]\n                    if xobj[operands[0]][\"/Subtype\"] != \"/Image\":  # type: ignore\n                        text = self.extract_xform_text(\n                            xobj[operands[0]],  # type: ignore\n                            orientations,\n                            space_width,\n                            visitor_operand_before,\n                            visitor_operand_after,\n                            visitor_text,\n                        )\n                        extractor.output += text\n                        if visitor_text is not None:\n                            visitor_text(\n                                text,\n                                extractor.memo_cm,\n                                extractor.memo_tm,\n                                extractor.font_resource,\n                                extractor.font_size,\n                            )\n                except Exception as exception:\n                    logger_warning(\n                        f\"Impossible to decode XFormObject {operands[0]}: {exception}\",\n                        __name__,\n                    )\n                finally:\n                    extractor.text = \"\"\n                    extractor.memo_cm = extractor.cm_matrix.copy()\n                    extractor.memo_tm = extractor.tm_matrix.copy()\n            else:\n                extractor.process_operation(operator, operands)\n            if visitor_operand_after is not None:\n                visitor_operand_after(operator, operands, extractor.cm_matrix, extractor.tm_matrix)\n        extractor.output += extractor.text  # just in case\n        if extractor.text != \"\" and visitor_text is not None:\n            visitor_text(\n                extractor.text,\n                extractor.memo_cm,\n                extractor.memo_tm,\n                extractor.font_resource,\n                extractor.font_size,\n            )\n        return extractor.output\n\n    def _layout_mode_fonts(self) -> dict[str, Font]:\n        \"\"\"\n        Get fonts formatted for \"layout\" mode text extraction.\n\n        Returns:\n            Dict[str, Font]: dictionary of Font instances keyed by font name\n\n        \"\"\"\n        # Font retrieval logic adapted from pypdf.PageObject._extract_text()\n        objr: Any = self\n        fonts: dict[str, Font] = {}\n        while objr is not None:\n            try:\n                resources_dict: Any = objr[PG.RESOURCES]\n            except KeyError:\n                resources_dict = {}\n            if \"/Font\" in resources_dict and self.pdf is not None:\n                for font_name in resources_dict[\"/Font\"]:\n                    fonts[font_name] = Font.from_font_resource(resources_dict[\"/Font\"][font_name])\n            try:\n                objr = objr[\"/Parent\"].get_object()\n            except KeyError:\n                objr = None\n\n        return fonts\n\n    def _layout_mode_text(\n        self,\n        space_vertically: bool = True,\n        scale_weight: float = 1.25,\n        strip_rotated: bool = True,\n        debug_path: Optional[Path] = None,\n        font_height_weight: float = 1,\n    ) -> str:\n        \"\"\"\n        Get text preserving fidelity to source PDF text layout.\n\n        Args:\n            space_vertically: include blank lines inferred from y distance + font\n                height. Defaults to True.\n            scale_weight: multiplier for string length when calculating weighted\n                average character width. Defaults to 1.25.\n            strip_rotated: Removes text that is rotated w.r.t. to the page from\n                layout mode output. Defaults to True.\n            debug_path (Path | None): if supplied, must target a directory.\n                creates the following files with debug information for layout mode\n                functions if supplied:\n                  - fonts.json: output of self._layout_mode_fonts\n                  - tjs.json: individual text render ops with corresponding transform matrices\n                  - bts.json: text render ops left justified and grouped by BT/ET operators\n                  - bt_groups.json: BT/ET operations grouped by rendered y-coord (aka lines)\n                Defaults to None.\n            font_height_weight: multiplier for font height when calculating\n                blank lines. Defaults to 1.\n\n        Returns:\n            str: multiline string containing page text in a fixed width format that\n                closely adheres to the rendered layout in the source pdf.\n\n        \"\"\"\n        fonts = self._layout_mode_fonts()\n        if debug_path:  # pragma: no cover\n            import json  # noqa: PLC0415\n\n            debug_path.joinpath(\"fonts.json\").write_text(\n                json.dumps(fonts, indent=2, default=asdict),\n                \"utf-8\"\n            )\n\n        ops = iter(\n            ContentStream(self[\"/Contents\"].get_object(), self.pdf, \"bytes\").operations\n        )\n        bt_groups = _layout_mode.text_show_operations(\n            ops, fonts, strip_rotated, debug_path\n        )\n\n        if not bt_groups:\n            return \"\"\n\n        ty_groups = _layout_mode.y_coordinate_groups(bt_groups, debug_path)\n\n        char_width = _layout_mode.fixed_char_width(bt_groups, scale_weight)\n\n        return _layout_mode.fixed_width_page(ty_groups, char_width, space_vertically, font_height_weight)\n\n    def extract_text(\n        self,\n        *args: Any,\n        orientations: Union[int, tuple[int, ...]] = (0, 90, 180, 270),\n        space_width: float = 200.0,\n        visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None,\n        visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None,\n        visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None,\n        extraction_mode: Literal[\"plain\", \"layout\"] = \"plain\",\n        **kwargs: Any,\n    ) -> str:\n        \"\"\"\n        Locate all text drawing commands, in the order they are provided in the\n        content stream, and extract the text.\n\n        This works well for some PDF files, but poorly for others, depending on\n        the generator used. This will be refined in the future.\n\n        Do not rely on the order of text coming out of this function, as it\n        will change if this function is made more sophisticated.\n\n        Arabic and Hebrew are extracted in the correct order.\n        If required a custom RTL range of characters can be defined;\n        see function set_custom_rtl.\n\n        Additionally you can provide visitor methods to get informed on all\n        operations and all text objects.\n        For example in some PDF files this can be useful to parse tables.\n\n        Args:\n            orientations: list of orientations extract_text will look for\n                default = (0, 90, 180, 270)\n                note: currently only 0 (up),90 (turned left), 180 (upside down),\n                270 (turned right)\n                Silently ignored in \"layout\" mode.\n            space_width: force default space width\n                if not extracted from font (default: 200)\n                Silently ignored in \"layout\" mode.\n            visitor_operand_before: function to be called before processing an operation.\n                It has four arguments: operator, operand-arguments,\n                current transformation matrix and text matrix.\n                Ignored with a warning in \"layout\" mode.\n            visitor_operand_after: function to be called after processing an operation.\n                It has four arguments: operator, operand-arguments,\n                current transformation matrix and text matrix.\n                Ignored with a warning in \"layout\" mode.\n            visitor_text: function to be called when extracting some text at some position.\n                It has five arguments: text, current transformation matrix,\n                text matrix, font-dictionary and font-size.\n                The font-dictionary may be None in case of unknown fonts.\n                If not None it may e.g. contain key \"/BaseFont\" with value \"/Arial,Bold\".\n                Ignored with a warning in \"layout\" mode.\n            extraction_mode (Literal[\"plain\", \"layout\"]): \"plain\" for legacy functionality,\n                \"layout\" for experimental layout mode functionality.\n                NOTE: orientations, space_width, and visitor_* parameters are NOT respected\n                in \"layout\" mode.\n\n        kwargs:\n            layout_mode_space_vertically (bool): include blank lines inferred from\n                y distance + font height. Defaults to True.\n            layout_mode_scale_weight (float): multiplier for string length when calculating\n                weighted average character width. Defaults to 1.25.\n            layout_mode_strip_rotated (bool): layout mode does not support rotated text.\n                Set to False to include rotated text anyway. If rotated text is discovered,\n                layout will be degraded and a warning will result. Defaults to True.\n            layout_mode_debug_path (Path | None): if supplied, must target a directory.\n                creates the following files with debug information for layout mode\n                functions if supplied:\n\n                  - fonts.json: output of self._layout_mode_fonts\n                  - tjs.json: individual text render ops with corresponding transform matrices\n                  - bts.json: text render ops left justified and grouped by BT/ET operators\n                  - bt_groups.json: BT/ET operations grouped by rendered y-coord (aka lines)\n            layout_mode_font_height_weight (float): multiplier for font height when calculating\n                blank lines. Defaults to 1.\n\n        Returns:\n            The extracted text\n\n        \"\"\"\n        if extraction_mode not in [\"plain\", \"layout\"]:\n            raise ValueError(f\"Invalid text extraction mode '{extraction_mode}'\")\n        if extraction_mode == \"layout\":\n            for visitor in (\n                \"visitor_operand_before\",\n                \"visitor_operand_after\",\n                \"visitor_text\",\n            ):\n                if locals()[visitor]:\n                    logger_warning(\n                        f\"Argument {visitor} is ignored in layout mode\",\n                        __name__,\n                    )\n            return self._layout_mode_text(\n                space_vertically=kwargs.get(\"layout_mode_space_vertically\", True),\n                scale_weight=kwargs.get(\"layout_mode_scale_weight\", 1.25),\n                strip_rotated=kwargs.get(\"layout_mode_strip_rotated\", True),\n                debug_path=kwargs.get(\"layout_mode_debug_path\"),\n                font_height_weight=kwargs.get(\"layout_mode_font_height_weight\", 1)\n            )\n        if len(args) >= 1:\n            if isinstance(args[0], str):\n                if len(args) >= 3:\n                    if isinstance(args[2], (tuple, int)):\n                        orientations = args[2]\n                    else:\n                        raise TypeError(f\"Invalid positional parameter {args[2]}\")\n                if len(args) >= 4:\n                    if isinstance(args[3], (float, int)):\n                        space_width = args[3]\n                    else:\n                        raise TypeError(f\"Invalid positional parameter {args[3]}\")\n            elif isinstance(args[0], (tuple, int)):\n                orientations = args[0]\n                if len(args) >= 2:\n                    if isinstance(args[1], (float, int)):\n                        space_width = args[1]\n                    else:\n                        raise TypeError(f\"Invalid positional parameter {args[1]}\")\n            else:\n                raise TypeError(f\"Invalid positional parameter {args[0]}\")\n\n        if isinstance(orientations, int):\n            orientations = (orientations,)\n\n        return self._extract_text(\n            self,\n            self.pdf,\n            orientations,\n            space_width,\n            PG.CONTENTS,\n            visitor_operand_before,\n            visitor_operand_after,\n            visitor_text,\n        )\n\n    def extract_xform_text(\n        self,\n        xform: EncodedStreamObject,\n        orientations: tuple[int, ...] = (0, 90, 270, 360),\n        space_width: float = 200.0,\n        visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None,\n        visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None,\n        visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None,\n    ) -> str:\n        \"\"\"\n        Extract text from an XObject.\n\n        Args:\n            xform:\n            orientations:\n            space_width:  force default space width (if not extracted from font (default 200)\n            visitor_operand_before:\n            visitor_operand_after:\n            visitor_text:\n\n        Returns:\n            The extracted text\n\n        \"\"\"\n        return self._extract_text(\n            xform,\n            self.pdf,\n            orientations,\n            space_width,\n            None,\n            visitor_operand_before,\n            visitor_operand_after,\n            visitor_text,\n        )\n\n    def _get_fonts(self) -> tuple[set[str], set[str]]:\n        \"\"\"\n        Get the names of embedded fonts and unembedded fonts.\n\n        Returns:\n            A tuple (set of embedded fonts, set of unembedded fonts)\n\n        \"\"\"\n        obj = self.get_object()\n        assert isinstance(obj, DictionaryObject)\n        fonts: set[str] = set()\n        embedded: set[str] = set()\n        fonts, embedded = _get_fonts_walk(obj, fonts, embedded)\n        unembedded = fonts - embedded\n        return embedded, unembedded\n\n    mediabox = _create_rectangle_accessor(PG.MEDIABOX, ())\n    \"\"\"A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in\n    default user space units, defining the boundaries of the physical medium on\n    which the page is intended to be displayed or printed.\"\"\"\n\n    cropbox = _create_rectangle_accessor(\"/CropBox\", (PG.MEDIABOX,))\n    \"\"\"\n    A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in\n    default user space units, defining the visible region of default user\n    space.\n\n    When the page is displayed or printed, its contents are to be clipped\n    (cropped) to this rectangle and then imposed on the output medium in some\n    implementation-defined manner. Default value: same as\n    :attr:`mediabox<mediabox>`.\n    \"\"\"\n\n    bleedbox = _create_rectangle_accessor(\"/BleedBox\", (\"/CropBox\", PG.MEDIABOX))\n    \"\"\"A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in\n    default user space units, defining the region to which the contents of the\n    page should be clipped when output in a production environment.\"\"\"\n\n    trimbox = _create_rectangle_accessor(\"/TrimBox\", (\"/CropBox\", PG.MEDIABOX))\n    \"\"\"A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in\n    default user space units, defining the intended dimensions of the finished\n    page after trimming.\"\"\"\n\n    artbox = _create_rectangle_accessor(\"/ArtBox\", (\"/CropBox\", PG.MEDIABOX))\n    \"\"\"A :class:`RectangleObject<pypdf.generic.RectangleObject>`, expressed in\n    default user space units, defining the extent of the page's meaningful\n    content as intended by the page's creator.\"\"\"\n\n    @property\n    def annotations(self) -> Optional[ArrayObject]:\n        if \"/Annots\" not in self:\n            return None\n        return cast(ArrayObject, self[\"/Annots\"])\n\n    @annotations.setter\n    def annotations(self, value: Optional[ArrayObject]) -> None:\n        \"\"\"\n        Set the annotations array of the page.\n\n        Typically you do not want to set this value, but append to it.\n        If you append to it, remember to add the object first to the writer\n        and only add the indirect object.\n        \"\"\"\n        if value is None:\n            if \"/Annots\" not in self:\n                return\n            del self[NameObject(\"/Annots\")]\n        else:\n            self[NameObject(\"/Annots\")] = value\n\n\nclass _VirtualList(Sequence[PageObject]):\n    def __init__(\n        self,\n        length_function: Callable[[], int],\n        get_function: Callable[[int], PageObject],\n    ) -> None:\n        self.length_function = length_function\n        self.get_function = get_function\n        self.current = -1\n\n    def __len__(self) -> int:\n        return self.length_function()\n\n    @overload\n    def __getitem__(self, index: int) -> PageObject:\n        ...\n\n    @overload\n    def __getitem__(self, index: slice) -> Sequence[PageObject]:\n        ...\n\n    def __getitem__(\n        self, index: Union[int, slice]\n    ) -> Union[PageObject, Sequence[PageObject]]:\n        if isinstance(index, slice):\n            indices = range(*index.indices(len(self)))\n            cls = type(self)\n            return cls(indices.__len__, lambda idx: self[indices[idx]])\n        if not isinstance(index, int):\n            raise TypeError(\"Sequence indices must be integers\")\n        len_self = len(self)\n        if index < 0:\n            # support negative indexes\n            index += len_self\n        if not (0 <= index < len_self):\n            raise IndexError(\"Sequence index out of range\")\n        return self.get_function(index)\n\n    def __delitem__(self, index: Union[int, slice]) -> None:\n        if isinstance(index, slice):\n            r = list(range(*index.indices(len(self))))\n            # pages have to be deleted from last to first\n            r.sort()\n            r.reverse()\n            for p in r:\n                del self[p]  # recursive call\n            return\n        if not isinstance(index, int):\n            raise TypeError(\"Index must be integers\")\n        len_self = len(self)\n        if index < 0:\n            # support negative indexes\n            index += len_self\n        if not (0 <= index < len_self):\n            raise IndexError(\"Index out of range\")\n        ind = self[index].indirect_reference\n        assert ind is not None\n        parent: Optional[PdfObject] = cast(DictionaryObject, ind.get_object()).get(\n            \"/Parent\", None\n        )\n        first = True\n        while parent is not None:\n            parent = cast(DictionaryObject, parent.get_object())\n            try:\n                i = cast(ArrayObject, parent[\"/Kids\"]).index(ind)\n                del cast(ArrayObject, parent[\"/Kids\"])[i]\n                first = False\n                try:\n                    assert ind is not None\n                    del ind.pdf.flattened_pages[index]  # case of page in a Reader\n                except Exception:  # pragma: no cover\n                    pass\n                if \"/Count\" in parent:\n                    parent[NameObject(\"/Count\")] = NumberObject(\n                        cast(int, parent[\"/Count\"]) - 1\n                    )\n                if len(cast(ArrayObject, parent[\"/Kids\"])) == 0:\n                    # No more objects in this part of this subtree\n                    ind = parent.indirect_reference\n                parent = parent.get(\"/Parent\", None)\n            except ValueError:  # from index\n                if first:\n                    raise PdfReadError(f\"Page not found in page tree: {ind}\")\n                break\n\n    def __iter__(self) -> Iterator[PageObject]:\n        for i in range(len(self)):\n            yield self[i]\n\n    def __str__(self) -> str:\n        p = [f\"PageObject({i})\" for i in range(self.length_function())]\n        return f\"[{', '.join(p)}]\"\n\n\ndef _get_fonts_walk(\n    obj: DictionaryObject,\n    fnt: set[str],\n    emb: set[str],\n) -> tuple[set[str], set[str]]:\n    \"\"\"\n    Get the set of all fonts and all embedded fonts.\n\n    Args:\n        obj: Page resources dictionary\n        fnt: font\n        emb: embedded fonts\n\n    Returns:\n        A tuple (fnt, emb)\n\n    If there is a key called 'BaseFont', that is a font that is used in the document.\n    If there is a key called 'FontName' and another key in the same dictionary object\n    that is called 'FontFilex' (where x is null, 2, or 3), then that fontname is\n    embedded.\n\n    We create and add to two sets, fnt = fonts used and emb = fonts embedded.\n\n    \"\"\"\n    fontkeys = (\"/FontFile\", \"/FontFile2\", \"/FontFile3\")\n\n    def process_font(f: DictionaryObject) -> None:\n        nonlocal fnt, emb\n        f = cast(DictionaryObject, f.get_object())  # to be sure\n        if \"/BaseFont\" in f:\n            fnt.add(cast(str, f[\"/BaseFont\"]))\n\n        if (\n            (\"/CharProcs\" in f)\n            or (\n                \"/FontDescriptor\" in f\n                and any(\n                    x in cast(DictionaryObject, f[\"/FontDescriptor\"]) for x in fontkeys\n                )\n            )\n            or (\n                \"/DescendantFonts\" in f\n                and \"/FontDescriptor\"\n                in cast(\n                    DictionaryObject,\n                    cast(ArrayObject, f[\"/DescendantFonts\"])[0].get_object(),\n                )\n                and any(\n                    x\n                    in cast(\n                        DictionaryObject,\n                        cast(\n                            DictionaryObject,\n                            cast(ArrayObject, f[\"/DescendantFonts\"])[0].get_object(),\n                        )[\"/FontDescriptor\"],\n                    )\n                    for x in fontkeys\n                )\n            )\n        ):\n            # the list comprehension ensures there is FontFile\n            try:\n                emb.add(cast(str, f[\"/BaseFont\"]))\n            except KeyError:\n                emb.add(\"(\" + cast(str, f[\"/Subtype\"]) + \")\")\n\n    if \"/DR\" in obj and \"/Font\" in cast(DictionaryObject, obj[\"/DR\"]):\n        for f in cast(DictionaryObject, cast(DictionaryObject, obj[\"/DR\"])[\"/Font\"]):\n            process_font(f)\n    if \"/Resources\" in obj:\n        if \"/Font\" in cast(DictionaryObject, obj[\"/Resources\"]):\n            for f in cast(\n                DictionaryObject, cast(DictionaryObject, obj[\"/Resources\"])[\"/Font\"]\n            ).values():\n                process_font(f)\n        if \"/XObject\" in cast(DictionaryObject, obj[\"/Resources\"]):\n            for x in cast(\n                DictionaryObject, cast(DictionaryObject, obj[\"/Resources\"])[\"/XObject\"]\n            ).values():\n                _get_fonts_walk(cast(DictionaryObject, x.get_object()), fnt, emb)\n    if \"/Annots\" in obj:\n        for a in cast(ArrayObject, obj[\"/Annots\"]):\n            _get_fonts_walk(cast(DictionaryObject, a.get_object()), fnt, emb)\n    if \"/AP\" in obj:\n        if (\n            cast(DictionaryObject, cast(DictionaryObject, obj[\"/AP\"])[\"/N\"]).get(\n                \"/Type\"\n            )\n            == \"/XObject\"\n        ):\n            _get_fonts_walk(\n                cast(DictionaryObject, cast(DictionaryObject, obj[\"/AP\"])[\"/N\"]),\n                fnt,\n                emb,\n            )\n        else:\n            for a in cast(DictionaryObject, cast(DictionaryObject, obj[\"/AP\"])[\"/N\"]):\n                _get_fonts_walk(cast(DictionaryObject, a), fnt, emb)\n    return fnt, emb  # return the sets for each page\n"
  },
  {
    "path": "pypdf/_page_labels.py",
    "content": "\"\"\"\nPage labels are shown by PDF viewers as \"the page number\".\n\nA page has a numeric index, starting at 0. Additionally, the page\nhas a label. In the most simple case:\n\n    label = index + 1\n\nHowever, the title page and the table of contents might have Roman numerals as\npage labels. This makes things more complicated.\n\nExample 1\n---------\n\n>>> reader.root_object[\"/PageLabels\"][\"/Nums\"]\n[0, IndirectObject(18, 0, 139929798197504),\n 8, IndirectObject(19, 0, 139929798197504)]\n>>> reader.get_object(reader.root_object[\"/PageLabels\"][\"/Nums\"][1])\n{'/S': '/r'}\n>>> reader.get_object(reader.root_object[\"/PageLabels\"][\"/Nums\"][3])\n{'/S': '/D'}\n\nExample 2\n---------\nThe following is a document with pages labeled\ni, ii, iii, iv, 1, 2, 3, A-8, A-9, ...\n\n1 0 obj\n    << /Type /Catalog\n       /PageLabels << /Nums [\n                        0 << /S /r >>\n                        4 << /S /D >>\n                        7 << /S /D\n                             /P ( A- )\n                             /St 8\n                        >>\n                        % A number tree containing\n                        % three page label dictionaries\n                        ]\n                   >>\n    ...\n    >>\nendobj\n\n\n§12.4.2 PDF Specification 1.7 and 2.0\n=====================================\n\nEntries in a page label dictionary\n----------------------------------\nThe /S key:\nD       Decimal Arabic numerals\nR       Uppercase Roman numerals\nr       Lowercase Roman numerals\nA       Uppercase letters (A to Z for the first 26 pages,\n                           AA to ZZ for the next 26, and so on)\na       Lowercase letters (a to z for the first 26 pages,\n                           aa to zz for the next 26, and so on)\n\"\"\"\n\nfrom collections.abc import Callable, Iterator\nfrom typing import Optional, cast\n\nfrom ._protocols import PdfCommonDocProtocol\nfrom ._utils import logger_warning\nfrom .generic import (\n    ArrayObject,\n    DictionaryObject,\n    NullObject,\n    NumberObject,\n    is_null_or_none,\n)\n\n\ndef number2uppercase_roman_numeral(num: int) -> str:\n    roman = [\n        (1000, \"M\"),\n        (900, \"CM\"),\n        (500, \"D\"),\n        (400, \"CD\"),\n        (100, \"C\"),\n        (90, \"XC\"),\n        (50, \"L\"),\n        (40, \"XL\"),\n        (10, \"X\"),\n        (9, \"IX\"),\n        (5, \"V\"),\n        (4, \"IV\"),\n        (1, \"I\"),\n    ]\n\n    def roman_num(num: int) -> Iterator[str]:\n        for decimal, roman_repr in roman:\n            x, _ = divmod(num, decimal)\n            yield roman_repr * x\n            num -= decimal * x\n            if num <= 0:\n                break\n\n    return \"\".join(list(roman_num(num)))\n\n\ndef number2lowercase_roman_numeral(number: int) -> str:\n    return number2uppercase_roman_numeral(number).lower()\n\n\ndef number2uppercase_letter(number: int) -> str:\n    if number <= 0:\n        raise ValueError(\"Expecting a positive number\")\n    alphabet = [chr(i) for i in range(ord(\"A\"), ord(\"Z\") + 1)]\n    rep = \"\"\n    while number > 0:\n        remainder = number % 26\n        if remainder == 0:\n            remainder = 26\n        rep = alphabet[remainder - 1] + rep\n        # update\n        number -= remainder\n        number = number // 26\n    return rep\n\n\ndef number2lowercase_letter(number: int) -> str:\n    return number2uppercase_letter(number).lower()\n\n\ndef get_label_from_nums(dictionary_object: DictionaryObject, index: int) -> str:\n    # [Nums] shall be an array of the form\n    #   [ key_1 value_1 key_2 value_2 ... key_n value_n ]\n    # where each key_i is an integer and the corresponding\n    # value_i shall be the object associated with that key.\n    # The keys shall be sorted in numerical order,\n    # analogously to the arrangement of keys in a name tree\n    # as described in 7.9.6, \"Name Trees.\"\n    nums = cast(ArrayObject, dictionary_object[\"/Nums\"])\n    i = 0\n    value = None\n    start_index = 0\n    while i < len(nums):\n        start_index = nums[i]\n        value = nums[i + 1].get_object()\n        if i + 2 == len(nums):\n            break\n        if nums[i + 2] > index:\n            break\n        i += 2\n    m: dict[Optional[str], Callable[[int], str]] = {\n        None: lambda _: \"\",\n        \"/D\": str,\n        \"/R\": number2uppercase_roman_numeral,\n        \"/r\": number2lowercase_roman_numeral,\n        \"/A\": number2uppercase_letter,\n        \"/a\": number2lowercase_letter,\n    }\n    # if /Nums array is not following the specification or if /Nums is empty\n    if not isinstance(value, dict):\n        return str(index + 1)  # Fallback\n    start = value.get(\"/St\", 1)\n    prefix = value.get(\"/P\", \"\")\n    mapping_function = m[value.get(\"/S\")]\n    return prefix + mapping_function(index - start_index + start)\n\n\ndef index2label(reader: PdfCommonDocProtocol, index: int) -> str:\n    \"\"\"\n    See 7.9.7 \"Number Trees\".\n\n    Args:\n        reader: The PdfReader\n        index: The index of the page\n\n    Returns:\n        The label of the page, e.g. \"iv\" or \"4\".\n\n    \"\"\"\n    root = cast(DictionaryObject, reader.root_object)\n    if \"/PageLabels\" not in root:\n        return str(index + 1)  # Fallback\n    number_tree = cast(DictionaryObject, root[\"/PageLabels\"].get_object())\n    if \"/Nums\" in number_tree:\n        return get_label_from_nums(number_tree, index)\n    if \"/Kids\" in number_tree and not isinstance(number_tree[\"/Kids\"], NullObject):\n        # number_tree = {'/Kids': [IndirectObject(7333, 0, 140132998195856), ...]}\n        # Limit maximum depth.\n        level = 0\n        while level < 100:\n            kids = cast(list[DictionaryObject], number_tree[\"/Kids\"])\n            for kid in kids:\n                # kid = {'/Limits': [0, 63], '/Nums': [0, {'/P': 'C1'}, ...]}\n                limits = cast(list[int], kid[\"/Limits\"])\n                if limits[0] <= index <= limits[1]:\n                    if not is_null_or_none(kid.get(\"/Kids\", None)):\n                        # Recursive definition.\n                        level += 1\n                        if level == 100:  # pragma: no cover\n                            raise NotImplementedError(\n                                \"Too deep nesting is not supported.\"\n                            )\n                        number_tree = kid\n                        # Exit the inner `for` loop and continue at the next level with the\n                        # next iteration of the `while` loop.\n                        break\n                    return get_label_from_nums(kid, index)\n            else:\n                # When there are no kids, make sure to exit the `while` loop directly\n                # and continue with the fallback.\n                break\n\n    logger_warning(f\"Could not reliably determine page label for {index}.\", __name__)\n    return str(index + 1)  # Fallback if neither /Nums nor /Kids is in the number_tree\n\n\ndef nums_insert(\n    key: NumberObject,\n    value: DictionaryObject,\n    nums: ArrayObject,\n) -> None:\n    \"\"\"\n    Insert a key, value pair in a Nums array.\n\n    See 7.9.7 \"Number Trees\".\n\n    Args:\n        key: number key of the entry\n        value: value of the entry\n        nums: Nums array to modify\n\n    \"\"\"\n    if len(nums) % 2 != 0:\n        raise ValueError(\"A nums like array must have an even number of elements\")\n\n    i = len(nums)\n    while i != 0 and key <= nums[i - 2]:\n        i = i - 2\n\n    if i < len(nums) and key == nums[i]:\n        nums[i + 1] = value\n    else:\n        nums.insert(i, key)\n        nums.insert(i + 1, value)\n\n\ndef nums_clear_range(\n    key: NumberObject,\n    page_index_to: int,\n    nums: ArrayObject,\n) -> None:\n    \"\"\"\n    Remove all entries in a number tree in a range after an entry.\n\n    See 7.9.7 \"Number Trees\".\n\n    Args:\n        key: number key of the entry before the range\n        page_index_to: The page index of the upper limit of the range\n        nums: Nums array to modify\n\n    \"\"\"\n    if len(nums) % 2 != 0:\n        raise ValueError(\"A nums like array must have an even number of elements\")\n    if page_index_to < key:\n        raise ValueError(\"page_index_to must be greater or equal than key\")\n\n    i = nums.index(key) + 2\n    while i < len(nums) and nums[i] <= page_index_to:\n        nums.pop(i)\n        nums.pop(i)\n\n\ndef nums_next(\n    key: NumberObject,\n    nums: ArrayObject,\n) -> tuple[Optional[NumberObject], Optional[DictionaryObject]]:\n    \"\"\"\n    Return the (key, value) pair of the entry after the given one.\n\n    See 7.9.7 \"Number Trees\".\n\n    Args:\n        key: number key of the entry\n        nums: Nums array\n\n    \"\"\"\n    if len(nums) % 2 != 0:\n        raise ValueError(\"A nums like array must have an even number of elements\")\n\n    i = nums.index(key) + 2\n    if i < len(nums):\n        return (nums[i], nums[i + 1])\n    return (None, None)\n"
  },
  {
    "path": "pypdf/_protocols.py",
    "content": "\"\"\"Helpers for working with PDF types.\"\"\"\n\nfrom abc import abstractmethod\nfrom pathlib import Path\nfrom typing import IO, Any, Optional, Protocol, Union\n\nfrom ._utils import StrByteType, StreamType\n\n\nclass PdfObjectProtocol(Protocol):\n    indirect_reference: Any\n\n    def clone(\n        self,\n        pdf_dest: Any,\n        force_duplicate: bool = False,\n        ignore_fields: Union[tuple[str, ...], list[str], None] = (),\n    ) -> Any:\n        ...  # pragma: no cover\n\n    def _reference_clone(self, clone: Any, pdf_dest: Any) -> Any:\n        ...  # pragma: no cover\n\n    def get_object(self) -> Optional[\"PdfObjectProtocol\"]:\n        ...  # pragma: no cover\n\n    def hash_value(self) -> bytes:\n        ...  # pragma: no cover\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        ...  # pragma: no cover\n\n\nclass XmpInformationProtocol(PdfObjectProtocol):\n    pass\n\n\nclass PdfCommonDocProtocol(Protocol):\n    @property\n    def pdf_header(self) -> str:\n        ...  # pragma: no cover\n\n    @property\n    def pages(self) -> list[Any]:\n        ...  # pragma: no cover\n\n    @property\n    def root_object(self) -> PdfObjectProtocol:\n        ...  # pragma: no cover\n\n    def get_object(self, indirect_reference: Any) -> Optional[PdfObjectProtocol]:\n        ...  # pragma: no cover\n\n    @property\n    def strict(self) -> bool:\n        ...  # pragma: no cover\n\n\nclass PdfReaderProtocol(PdfCommonDocProtocol, Protocol):\n    @property\n    @abstractmethod\n    def xref(self) -> dict[int, dict[int, Any]]:\n        ...  # pragma: no cover\n\n    @property\n    @abstractmethod\n    def trailer(self) -> dict[str, Any]:\n        ...  # pragma: no cover\n\n\nclass PdfWriterProtocol(PdfCommonDocProtocol, Protocol):\n    _objects: list[Any]\n    _id_translated: dict[int, dict[int, int]]\n\n    incremental: bool\n    _reader: Any  # PdfReader\n\n    @abstractmethod\n    def write(self, stream: Union[Path, StrByteType]) -> tuple[bool, IO[Any]]:\n        ...  # pragma: no cover\n\n    @abstractmethod\n    def _add_object(self, obj: Any) -> Any:\n        ...  # pragma: no cover\n"
  },
  {
    "path": "pypdf/_reader.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com>\n#\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nimport os\nimport re\nimport sys\nfrom collections.abc import Iterable\nfrom io import BytesIO, UnsupportedOperation\nfrom pathlib import Path\nfrom types import TracebackType\nfrom typing import (\n    TYPE_CHECKING,\n    Any,\n    Callable,\n    Optional,\n    Union,\n    cast,\n)\n\nif sys.version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing_extensions import Self\n\nfrom ._doc_common import PdfDocCommon, convert_to_int\nfrom ._encryption import Encryption, PasswordType\nfrom ._utils import (\n    WHITESPACES_AS_BYTES,\n    StrByteType,\n    StreamType,\n    logger_warning,\n    read_non_whitespace,\n    read_previous_line,\n    read_until_whitespace,\n    skip_over_comment,\n    skip_over_whitespace,\n)\nfrom .constants import TrailerKeys as TK\nfrom .errors import (\n    EmptyFileError,\n    FileNotDecryptedError,\n    LimitReachedError,\n    PdfReadError,\n    PdfStreamError,\n    WrongPasswordError,\n)\nfrom .generic import (\n    ArrayObject,\n    ContentStream,\n    DecodedStreamObject,\n    DictionaryObject,\n    EncodedStreamObject,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    PdfObject,\n    StreamObject,\n    TextStringObject,\n    is_null_or_none,\n    read_object,\n)\nfrom .xmp import XmpInformation\n\nif TYPE_CHECKING:\n    from ._page import PageObject\n\n\nclass PdfReader(PdfDocCommon):\n    \"\"\"\n    Initialize a PdfReader object.\n\n    This operation can take some time, as the PDF stream's cross-reference\n    tables are read into memory.\n\n    Args:\n        stream: A File object or an object that supports the standard read\n            and seek methods similar to a File object. Could also be a\n            string representing a path to a PDF file.\n        strict: Determines whether user should be warned of all\n            problems and also causes some correctable problems to be fatal.\n            Defaults to ``False``.\n        password: Decrypt PDF file at initialization. If the\n            password is None, the file will not be decrypted.\n            Defaults to ``None``.\n        root_object_recovery_limit: The maximum number of objects to query\n            for recovering the Root object in non-strict mode. To disable\n            this security measure, pass ``None``.\n\n    \"\"\"\n\n    def __init__(\n        self,\n        stream: Union[StrByteType, Path],\n        strict: bool = False,\n        password: Union[None, str, bytes] = None,\n        *,\n        root_object_recovery_limit: Optional[int] = 10_000,\n    ) -> None:\n        self.strict = strict\n        self.flattened_pages: Optional[list[PageObject]] = None\n\n        #: Storage of parsed PDF objects.\n        self.resolved_objects: dict[tuple[Any, Any], Optional[PdfObject]] = {}\n\n        self._startxref: int = 0\n        self.xref_index = 0\n        self.xref: dict[int, dict[Any, Any]] = {}\n        self.xref_free_entry: dict[int, dict[Any, Any]] = {}\n        self.xref_objStm: dict[int, tuple[Any, Any]] = {}\n        self.trailer = DictionaryObject()\n\n        # Security parameters.\n        self._root_object_recovery_limit = (\n            root_object_recovery_limit if isinstance(root_object_recovery_limit, int) else sys.maxsize\n        )\n\n        # Map page indirect_reference number to page number\n        self._page_id2num: Optional[dict[Any, Any]] = None\n\n        self._validated_root: Optional[DictionaryObject] = None\n\n        self._initialize_stream(stream)\n        self._known_objects: set[tuple[int, int]] = set()\n\n        self._override_encryption = False\n        self._encryption: Optional[Encryption] = None\n        if self.is_encrypted:\n            self._handle_encryption(password)\n        elif password is not None:\n            raise PdfReadError(\"Not an encrypted file\")\n\n    def _initialize_stream(self, stream: Union[StrByteType, Path]) -> None:\n        if hasattr(stream, \"mode\") and \"b\" not in stream.mode:\n            logger_warning(\n                \"PdfReader stream/file object is not in binary mode. \"\n                \"It may not be read correctly.\",\n                __name__,\n            )\n        self._stream_opened = False\n        if isinstance(stream, (str, Path)):\n            with open(stream, \"rb\") as fh:\n                stream = BytesIO(fh.read())\n            self._stream_opened = True\n        self.read(stream)\n        self.stream = stream\n\n    def _handle_encryption(self, password: Optional[Union[str, bytes]]) -> None:\n        self._override_encryption = True\n        # Some documents may not have a /ID, use two empty\n        # byte strings instead. Solves\n        # https://github.com/py-pdf/pypdf/issues/608\n        id_entry = self.trailer.get(TK.ID)\n        id1_entry = id_entry[0].get_object().original_bytes if id_entry else b\"\"\n        encrypt_entry = cast(DictionaryObject, self.trailer[TK.ENCRYPT].get_object())\n        self._encryption = Encryption.read(encrypt_entry, id1_entry)\n\n        # try empty password if no password provided\n        pwd = password if password is not None else b\"\"\n        if (\n            self._encryption.verify(pwd) == PasswordType.NOT_DECRYPTED\n            and password is not None\n        ):\n            # raise if password provided\n            raise WrongPasswordError(\"Wrong password\")\n        self._override_encryption = False\n\n    def __enter__(self) -> Self:\n        return self\n\n    def __exit__(\n        self,\n        exc_type: Optional[type[BaseException]],\n        exc_val: Optional[BaseException],\n        exc_tb: Optional[TracebackType],\n    ) -> None:\n        self.close()\n\n    def close(self) -> None:\n        \"\"\"Close the stream if opened in __init__ and clear memory.\"\"\"\n        if self._stream_opened:\n            self.stream.close()\n        self.flattened_pages = []\n        self.resolved_objects = {}\n        self.trailer = DictionaryObject()\n        self.xref = {}\n        self.xref_free_entry = {}\n        self.xref_objStm = {}\n\n    @property\n    def root_object(self) -> DictionaryObject:\n        \"\"\"Provide access to \"/Root\". Standardized with PdfWriter.\"\"\"\n        if self._validated_root:\n            return self._validated_root\n        root = self.trailer.get(TK.ROOT)\n        if is_null_or_none(root):\n            logger_warning('Cannot find \"/Root\" key in trailer', __name__)\n        elif (\n            cast(DictionaryObject, cast(PdfObject, root).get_object()).get(\"/Type\")\n            == \"/Catalog\"\n        ):\n            self._validated_root = cast(\n                DictionaryObject, cast(PdfObject, root).get_object()\n            )\n        else:\n            logger_warning(\"Invalid Root object in trailer\", __name__)\n        if self._validated_root is None:\n            logger_warning('Searching object with \"/Catalog\" key', __name__)\n            number_of_objects = cast(int, self.trailer.get(\"/Size\", 0))\n            for i in range(number_of_objects):\n                if i >= self._root_object_recovery_limit:\n                    raise LimitReachedError(\"Maximum Root object recovery limit reached.\")\n                try:\n                    obj = self.get_object(i + 1)\n                except Exception:  # to be sure to capture all errors\n                    obj = None\n                if isinstance(obj, DictionaryObject) and obj.get(\"/Type\") == \"/Catalog\":\n                    self._validated_root = obj\n                    logger_warning(f\"Root found at {obj.indirect_reference!r}\", __name__)\n                    break\n        if self._validated_root is None:\n            if not is_null_or_none(root) and \"/Pages\" in cast(DictionaryObject, cast(PdfObject, root).get_object()):\n                logger_warning(\n                    f\"Possible root found at {cast(PdfObject, root).indirect_reference!r}, but missing /Catalog key\",\n                    __name__\n                )\n                self._validated_root = cast(\n                    DictionaryObject, cast(PdfObject, root).get_object()\n                )\n            else:\n                raise PdfReadError(\"Cannot find Root object in pdf\")\n        return self._validated_root\n\n    @property\n    def _info(self) -> Optional[DictionaryObject]:\n        \"\"\"\n        Provide access to \"/Info\". Standardized with PdfWriter.\n\n        Returns:\n            /Info Dictionary; None if the entry does not exist\n\n        \"\"\"\n        info = self.trailer.get(TK.INFO, None)\n        if is_null_or_none(info):\n            return None\n        assert info is not None, \"mypy\"\n        info = info.get_object()\n        if not isinstance(info, DictionaryObject):\n            raise PdfReadError(\n                \"Trailer not found or does not point to a document information dictionary\"\n            )\n        return info\n\n    @property\n    def _ID(self) -> Optional[ArrayObject]:\n        \"\"\"\n        Provide access to \"/ID\". Standardized with PdfWriter.\n\n        Returns:\n            /ID array; None if the entry does not exist\n\n        \"\"\"\n        id = self.trailer.get(TK.ID, None)\n        if is_null_or_none(id):\n            return None\n        assert id is not None, \"mypy\"\n        return cast(ArrayObject, id.get_object())\n\n    @property\n    def pdf_header(self) -> str:\n        \"\"\"\n        The first 8 bytes of the file.\n\n        This is typically something like ``'%PDF-1.6'`` and can be used to\n        detect if the file is actually a PDF file and which version it is.\n        \"\"\"\n        # TODO: Make this return a bytes object for consistency\n        #       but that needs a deprecation\n        loc = self.stream.tell()\n        self.stream.seek(0, 0)\n        pdf_file_version = self.stream.read(8).decode(\"utf-8\", \"backslashreplace\")\n        self.stream.seek(loc, 0)  # return to where it was\n        return pdf_file_version\n\n    @property\n    def xmp_metadata(self) -> Optional[XmpInformation]:\n        \"\"\"XMP (Extensible Metadata Platform) data.\"\"\"\n        try:\n            self._override_encryption = True\n            return cast(XmpInformation, self.root_object.xmp_metadata)\n        finally:\n            self._override_encryption = False\n\n    def _get_page_number_by_indirect(\n        self, indirect_reference: Union[None, int, NullObject, IndirectObject]\n    ) -> Optional[int]:\n        \"\"\"\n        Retrieve the page number from an indirect reference.\n\n        Args:\n            indirect_reference: The indirect reference to locate.\n\n        Returns:\n            Page number or None.\n\n        \"\"\"\n        if self._page_id2num is None:\n            self._page_id2num = {\n                x.indirect_reference.idnum: i for i, x in enumerate(self.pages)  # type: ignore\n            }\n\n        if is_null_or_none(indirect_reference):\n            return None\n        assert isinstance(indirect_reference, (int, IndirectObject)), \"mypy\"\n        if isinstance(indirect_reference, int):\n            idnum = indirect_reference\n        else:\n            idnum = indirect_reference.idnum\n        assert self._page_id2num is not None, \"hint for mypy\"\n        return self._page_id2num.get(idnum, None)\n\n    def _get_object_from_stream(\n        self, indirect_reference: IndirectObject\n    ) -> Union[int, PdfObject, str]:\n        # indirect reference to object in object stream\n        # read the entire object stream into memory\n        stmnum, _idx = self.xref_objStm[indirect_reference.idnum]\n        obj_stm: EncodedStreamObject = IndirectObject(stmnum, 0, self).get_object()  # type: ignore\n        # This is an xref to a stream, so its type better be a stream\n        assert cast(str, obj_stm[\"/Type\"]) == \"/ObjStm\"\n        # Parse ALL objects in this stream in one pass and cache them.\n        # This avoids O(N²) behavior when many objects from the same stream\n        # are resolved individually (each call would re-parse the header).\n        stream_data = BytesIO(obj_stm.get_data())\n        n = int(obj_stm[\"/N\"])  # type: ignore[call-overload]\n        first_offset = int(obj_stm[\"/First\"])  # type: ignore[call-overload]\n\n        # Phase 1: Read the index (objnum, offset) pairs from the header.\n        obj_index: list[tuple[int, int]] = []\n        for _i in range(n):\n            read_non_whitespace(stream_data)\n            stream_data.seek(-1, 1)\n            objnum = NumberObject.read_from_stream(stream_data)\n            read_non_whitespace(stream_data)\n            stream_data.seek(-1, 1)\n            offset = NumberObject.read_from_stream(stream_data)\n            read_non_whitespace(stream_data)\n            stream_data.seek(-1, 1)\n            obj_index.append((int(objnum), int(offset)))\n\n        # Phase 2: Parse each object and cache it.\n        target_obj: Union[int, PdfObject, str] = NullObject()\n        found = False\n        for i, (obj_num, obj_offset) in enumerate(obj_index):\n            # Skip objects already in the cache.\n            cached = self.cache_get_indirect_object(0, obj_num)\n            if cached is not None:\n                if obj_num == indirect_reference.idnum:\n                    target_obj = cached\n                    found = True\n                continue\n\n            stream_data.seek(first_offset + obj_offset, 0)\n\n            # To cope with case where the 'pointer' is on a white space\n            read_non_whitespace(stream_data)\n            stream_data.seek(-1, 1)\n\n            try:\n                obj = read_object(stream_data, self)\n            except PdfStreamError as exc:\n                # Stream object cannot be read. Normally, a critical error, but\n                # Adobe Reader doesn't complain, so continue (in strict mode?)\n                logger_warning(\n                    f\"Invalid stream (index {i}) within object \"\n                    f\"{obj_num} 0: {exc}\",\n                    __name__,\n                )\n                if self.strict:  # pragma: no cover\n                    raise PdfReadError(\n                        f\"Cannot read object stream: {exc}\"\n                    )  # pragma: no cover\n                obj = NullObject()  # pragma: no cover\n\n            # Only cache if this object is still registered in xref_objStm.\n            # Incremental updates may override objects originally in the stream;\n            # caching those stale versions would shadow the newer xref entry.\n            if obj_num in self.xref_objStm:\n                self.cache_indirect_object(0, obj_num, obj)  # type: ignore[arg-type]\n\n            if obj_num == indirect_reference.idnum:\n                target_obj = obj\n                found = True\n\n        if not found and self.strict:  # pragma: no cover\n            raise PdfReadError(\n                \"This is a fatal error in strict mode.\"\n            )  # pragma: no cover\n        return target_obj\n\n    def get_object(\n        self, indirect_reference: Union[int, IndirectObject]\n    ) -> Optional[PdfObject]:\n        if isinstance(indirect_reference, int):\n            indirect_reference = IndirectObject(indirect_reference, 0, self)\n        retval = self.cache_get_indirect_object(\n            indirect_reference.generation, indirect_reference.idnum\n        )\n        if retval is not None:\n            return retval\n        if (\n            indirect_reference.generation == 0\n            and indirect_reference.idnum in self.xref_objStm\n        ):\n            retval = self._get_object_from_stream(indirect_reference)  # type: ignore\n        elif (\n            indirect_reference.generation in self.xref\n            and indirect_reference.idnum in self.xref[indirect_reference.generation]\n        ):\n            if self.xref_free_entry.get(indirect_reference.generation, {}).get(\n                indirect_reference.idnum, False\n            ):\n                return NullObject()\n            start = self.xref[indirect_reference.generation][indirect_reference.idnum]\n            self.stream.seek(start, 0)\n            try:\n                idnum, generation = self.read_object_header(self.stream)\n                if (\n                    idnum != indirect_reference.idnum\n                    or generation != indirect_reference.generation\n                ):\n                    raise PdfReadError(\"Not matching, we parse the file for it\")\n            except Exception:\n                if hasattr(self.stream, \"getbuffer\"):\n                    buf = bytes(self.stream.getbuffer())\n                else:\n                    p = self.stream.tell()\n                    self.stream.seek(0, 0)\n                    buf = self.stream.read(-1)\n                    self.stream.seek(p, 0)\n                m = re.search(\n                    rf\"\\s{indirect_reference.idnum}\\s+{indirect_reference.generation}\\s+obj\".encode(),\n                    buf,\n                )\n                if m is not None:\n                    logger_warning(\n                        f\"Object ID {indirect_reference.idnum},{indirect_reference.generation} ref repaired\",\n                        __name__,\n                    )\n                    self.xref[indirect_reference.generation][\n                        indirect_reference.idnum\n                    ] = (m.start(0) + 1)\n                    self.stream.seek(m.start(0) + 1)\n                    idnum, generation = self.read_object_header(self.stream)\n                else:\n                    idnum = -1\n                    generation = -1  # exception will be raised below\n            if idnum != indirect_reference.idnum and self.xref_index:\n                # xref table probably had bad indexes due to not being zero-indexed\n                if self.strict:\n                    raise PdfReadError(\n                        f\"Expected object ID ({indirect_reference.idnum} {indirect_reference.generation}) \"\n                        f\"does not match actual ({idnum} {generation}); \"\n                        \"xref table not zero-indexed.\"\n                    )\n                # xref table is corrected in non-strict mode\n            elif idnum != indirect_reference.idnum and self.strict:\n                # some other problem\n                raise PdfReadError(\n                    f\"Expected object ID ({indirect_reference.idnum} {indirect_reference.generation}) \"\n                    f\"does not match actual ({idnum} {generation}).\"\n                )\n            if self.strict:\n                assert generation == indirect_reference.generation\n\n            current_object = (indirect_reference.idnum, indirect_reference.generation)\n            if current_object in self._known_objects:\n                raise PdfReadError(f\"Detected loop with self reference for {indirect_reference!r}.\")\n            self._known_objects.add(current_object)\n            retval = read_object(self.stream, self)  # type: ignore\n            self._known_objects.remove(current_object)\n\n            # override encryption is used for the /Encrypt dictionary\n            if not self._override_encryption and self._encryption is not None:\n                # if we don't have the encryption key:\n                if not self._encryption.is_decrypted():\n                    raise FileNotDecryptedError(\"File has not been decrypted\")\n                # otherwise, decrypt here...\n                retval = cast(PdfObject, retval)\n                retval = self._encryption.decrypt_object(\n                    retval, indirect_reference.idnum, indirect_reference.generation\n                )\n        else:\n            if hasattr(self.stream, \"getbuffer\"):\n                buf = bytes(self.stream.getbuffer())\n            else:\n                p = self.stream.tell()\n                self.stream.seek(0, 0)\n                buf = self.stream.read(-1)\n                self.stream.seek(p, 0)\n            m = re.search(\n                rf\"\\s{indirect_reference.idnum}\\s+{indirect_reference.generation}\\s+obj\".encode(),\n                buf,\n            )\n            if m is not None:\n                logger_warning(\n                    f\"Object {indirect_reference.idnum} {indirect_reference.generation} found\",\n                    __name__,\n                )\n                if indirect_reference.generation not in self.xref:\n                    self.xref[indirect_reference.generation] = {}\n                self.xref[indirect_reference.generation][indirect_reference.idnum] = (\n                    m.start(0) + 1\n                )\n                self.stream.seek(m.end(0) + 1)\n                skip_over_whitespace(self.stream)\n                self.stream.seek(-1, 1)\n                retval = read_object(self.stream, self)  # type: ignore\n\n                # override encryption is used for the /Encrypt dictionary\n                if not self._override_encryption and self._encryption is not None:\n                    # if we don't have the encryption key:\n                    if not self._encryption.is_decrypted():\n                        raise FileNotDecryptedError(\"File has not been decrypted\")\n                    # otherwise, decrypt here...\n                    retval = cast(PdfObject, retval)\n                    retval = self._encryption.decrypt_object(\n                        retval, indirect_reference.idnum, indirect_reference.generation\n                    )\n            else:\n                logger_warning(\n                    f\"Object {indirect_reference.idnum} {indirect_reference.generation} not defined.\",\n                    __name__,\n                )\n                if self.strict:\n                    raise PdfReadError(\"Could not find object.\")\n        # For ObjStm objects, _get_object_from_stream already cached\n        # the result during batch parsing; skip the redundant cache write\n        # to avoid \"Overwriting cache\" warnings. For non-ObjStm objects\n        # (including encrypted ones that need decrypted values cached),\n        # always write.\n        if not (\n            indirect_reference.generation == 0\n            and indirect_reference.idnum in self.xref_objStm\n        ):\n            self.cache_indirect_object(\n                indirect_reference.generation, indirect_reference.idnum, retval\n            )\n        return retval\n\n    def read_object_header(self, stream: StreamType) -> tuple[int, int]:\n        # Should never be necessary to read out whitespace, since the\n        # cross-reference table should put us in the right spot to read the\n        # object header. In reality some files have stupid cross-reference\n        # tables that are off by whitespace bytes.\n        skip_over_comment(stream)\n        extra = skip_over_whitespace(stream)\n        stream.seek(-1, 1)\n        idnum = read_until_whitespace(stream)\n        extra |= skip_over_whitespace(stream)\n        stream.seek(-1, 1)\n        generation = read_until_whitespace(stream)\n        extra |= skip_over_whitespace(stream)\n        stream.seek(-1, 1)\n\n        # although it's not used, it might still be necessary to read\n        _obj = stream.read(3)\n\n        read_non_whitespace(stream)\n        stream.seek(-1, 1)\n        if extra and self.strict:\n            logger_warning(\n                f\"Superfluous whitespace found in object header {idnum} {generation}\",  # type: ignore\n                __name__,\n            )\n        return int(idnum), int(generation)\n\n    def cache_get_indirect_object(\n        self, generation: int, idnum: int\n    ) -> Optional[PdfObject]:\n        try:\n            return self.resolved_objects.get((generation, idnum))\n        except RecursionError:\n            raise PdfReadError(\"Maximum recursion depth reached.\")\n\n    def cache_indirect_object(\n        self, generation: int, idnum: int, obj: Optional[PdfObject]\n    ) -> Optional[PdfObject]:\n        if (generation, idnum) in self.resolved_objects:\n            msg = f\"Overwriting cache for {generation} {idnum}\"\n            if self.strict:\n                raise PdfReadError(msg)\n            logger_warning(msg, __name__)\n        self.resolved_objects[(generation, idnum)] = obj\n        if obj is not None:\n            obj.indirect_reference = IndirectObject(idnum, generation, self)\n        return obj\n\n    def _replace_object(self, indirect_reference: IndirectObject, obj: PdfObject) -> PdfObject:\n        # function reserved for future development\n        if indirect_reference.pdf != self:\n            raise ValueError(\"Cannot update PdfReader with external object\")\n        if (indirect_reference.generation, indirect_reference.idnum) not in self.resolved_objects:\n            raise ValueError(\"Cannot find referenced object\")\n        self.resolved_objects[(indirect_reference.generation, indirect_reference.idnum)] = obj\n        obj.indirect_reference = indirect_reference\n        return obj\n\n    def read(self, stream: StreamType) -> None:\n        \"\"\"\n        Read and process the PDF stream, extracting necessary data.\n\n        Args:\n            stream: The PDF file stream.\n\n        \"\"\"\n        self._basic_validation(stream)\n        self._find_eof_marker(stream)\n        startxref = self._find_startxref_pos(stream)\n        self._startxref = startxref\n\n        # check and eventually correct the startxref only if not strict\n        xref_issue_nr = self._get_xref_issues(stream, startxref)\n        if xref_issue_nr != 0:\n            if self.strict and xref_issue_nr:\n                raise PdfReadError(\"Broken xref table\")\n            logger_warning(f\"incorrect startxref pointer({xref_issue_nr})\", __name__)\n\n        # read all cross-reference tables and their trailers\n        self._read_xref_tables_and_trailers(stream, startxref, xref_issue_nr)\n\n        # if not zero-indexed, verify that the table is correct; change it if necessary\n        if self.xref_index and not self.strict:\n            loc = stream.tell()\n            for gen, xref_entry in self.xref.items():\n                if gen == 65535:\n                    continue\n                xref_k = sorted(\n                    xref_entry.keys()\n                )  # ensure ascending to prevent damage\n                for id in xref_k:\n                    stream.seek(xref_entry[id], 0)\n                    try:\n                        pid, _pgen = self.read_object_header(stream)\n                    except ValueError:\n                        self._rebuild_xref_table(stream)\n                        break\n                    if pid == id - self.xref_index:\n                        # fixing index item per item is required for revised PDF.\n                        self.xref[gen][pid] = self.xref[gen][id]\n                        del self.xref[gen][id]\n                    # if not, then either it's just plain wrong, or the\n                    # non-zero-index is actually correct\n            stream.seek(loc, 0)  # return to where it was\n\n        # remove wrong objects (not pointing to correct structures) - cf #2326\n        if not self.strict:\n            loc = stream.tell()\n            for gen, xref_entry in self.xref.items():\n                if gen == 65535:\n                    continue\n                ids = list(xref_entry.keys())\n                for id in ids:\n                    stream.seek(xref_entry[id], 0)\n                    try:\n                        self.read_object_header(stream)\n                    except ValueError:\n                        logger_warning(\n                            f\"Ignoring wrong pointing object {id} {gen} (offset {xref_entry[id]})\",\n                            __name__,\n                        )\n                        del xref_entry[id]  # we can delete the id, we are parsing ids\n            stream.seek(loc, 0)  # return to where it was\n\n    def _basic_validation(self, stream: StreamType) -> None:\n        \"\"\"Ensure the stream is valid and not empty.\"\"\"\n        stream.seek(0, os.SEEK_SET)\n        try:\n            header_byte = stream.read(5)\n        except UnicodeDecodeError:\n            raise UnsupportedOperation(\"cannot read header\")\n        if header_byte == b\"\":\n            raise EmptyFileError(\"Cannot read an empty file\")\n        if header_byte != b\"%PDF-\":\n            if self.strict:\n                raise PdfReadError(\n                    f\"PDF starts with '{header_byte.decode('utf8')}', \"\n                    \"but '%PDF-' expected\"\n                )\n            logger_warning(f\"invalid pdf header: {header_byte}\", __name__)\n        stream.seek(0, os.SEEK_END)\n\n    def _find_eof_marker(self, stream: StreamType) -> None:\n        \"\"\"\n        Jump to the %%EOF marker.\n\n        According to the specs, the %%EOF marker should be at the very end of\n        the file. Hence for standard-compliant PDF documents this function will\n        read only the last part (DEFAULT_BUFFER_SIZE).\n        \"\"\"\n        HEADER_SIZE = 8  # to parse whole file, Header is e.g. '%PDF-1.6'\n        line = b\"\"\n        first = True\n        while not line.startswith(b\"%%EOF\"):\n            if line != b\"\" and first:\n                if any(\n                    line.strip().endswith(tr) for tr in (b\"%%EO\", b\"%%E\", b\"%%\", b\"%\")\n                ):\n                    # Consider the file as truncated while\n                    # having enough confidence to carry on.\n                    logger_warning(\"EOF marker seems truncated\", __name__)\n                    break\n                first = False\n            if b\"startxref\" in line:\n                logger_warning(\n                    \"CAUTION: startxref found while searching for %%EOF. \"\n                    \"The file might be truncated and some data might not be read.\",\n                    __name__,\n                )\n            if stream.tell() < HEADER_SIZE:\n                if self.strict:\n                    raise PdfReadError(\"EOF marker not found\")\n                logger_warning(\"EOF marker not found\", __name__)\n            line = read_previous_line(stream)\n\n    def _find_startxref_pos(self, stream: StreamType) -> int:\n        \"\"\"\n        Find startxref entry - the location of the xref table.\n\n        Args:\n            stream:\n\n        Returns:\n            The bytes offset\n\n        \"\"\"\n        line = read_previous_line(stream)\n        try:\n            startxref = int(line)\n        except ValueError:\n            # 'startxref' may be on the same line as the location\n            if not line.startswith(b\"startxref\"):\n                raise PdfReadError(\"startxref not found\")\n            startxref = int(line[9:].strip())\n            logger_warning(\"startxref on same line as offset\", __name__)\n        else:\n            line = read_previous_line(stream)\n            if not line.startswith(b\"startxref\"):\n                raise PdfReadError(\"startxref not found\")\n        return startxref\n\n    def _read_standard_xref_table(self, stream: StreamType) -> None:\n        # standard cross-reference table\n        ref = stream.read(3)\n        if ref != b\"ref\":\n            raise PdfReadError(\"xref table read error\")\n        read_non_whitespace(stream)\n        stream.seek(-1, 1)\n        first_time = True  # check if the first time looking at the xref table\n        while True:\n            num = cast(int, read_object(stream, self))\n            if first_time and num != 0:\n                self.xref_index = num\n                if self.strict:\n                    logger_warning(\n                        \"Xref table not zero-indexed. ID numbers for objects will be corrected.\",\n                        __name__,\n                    )\n                    # if table not zero indexed, could be due to error from when PDF was created\n                    # which will lead to mismatched indices later on, only warned and corrected if self.strict==True\n            first_time = False\n            read_non_whitespace(stream)\n            stream.seek(-1, 1)\n            size = cast(int, read_object(stream, self))\n            if not isinstance(size, int):\n                logger_warning(\n                    \"Invalid/Truncated xref table. Rebuilding it.\",\n                    __name__,\n                )\n                self._rebuild_xref_table(stream)\n                stream.read()\n                return\n            read_non_whitespace(stream)\n            stream.seek(-1, 1)\n            cnt = 0\n            while cnt < size:\n                line = stream.read(20)\n                if not line:\n                    raise PdfReadError(\"Unexpected empty line in Xref table.\")\n\n                # It's very clear in section 3.4.3 of the PDF spec\n                # that all cross-reference table lines are a fixed\n                # 20 bytes (as of PDF 1.7). However, some files have\n                # 21-byte entries (or more) due to the use of \\r\\n\n                # (CRLF) EOL's. Detect that case, and adjust the line\n                # until it does not begin with a \\r (CR) or \\n (LF).\n                while line[0] in b\"\\x0D\\x0A\":\n                    stream.seek(-20 + 1, 1)\n                    line = stream.read(20)\n\n                # On the other hand, some malformed PDF files\n                # use a single character EOL without a preceding\n                # space. Detect that case, and seek the stream\n                # back one character (0-9 means we've bled into\n                # the next xref entry, t means we've bled into the\n                # text \"trailer\"):\n                if line[-1] in b\"0123456789t\":\n                    stream.seek(-1, 1)\n\n                try:\n                    offset_b, generation_b = line[:16].split(b\" \")\n                    entry_type_b = line[17:18]\n\n                    offset, generation = int(offset_b), int(generation_b)\n                except Exception:\n                    if hasattr(stream, \"getbuffer\"):\n                        buf = bytes(stream.getbuffer())\n                    else:\n                        p = stream.tell()\n                        stream.seek(0, 0)\n                        buf = stream.read(-1)\n                        stream.seek(p)\n\n                    f = re.search(rf\"{num}\\s+(\\d+)\\s+obj\".encode(), buf)\n                    if f is None:\n                        logger_warning(\n                            f\"entry {num} in Xref table invalid; object not found\",\n                            __name__,\n                        )\n                        generation = 65535\n                        offset = -1\n                        entry_type_b = b\"f\"\n                    else:\n                        logger_warning(\n                            f\"entry {num} in Xref table invalid but object found\",\n                            __name__,\n                        )\n                        generation = int(f.group(1))\n                        offset = f.start()\n\n                if generation not in self.xref:\n                    self.xref[generation] = {}\n                    self.xref_free_entry[generation] = {}\n                if num in self.xref[generation]:\n                    # It really seems like we should allow the last\n                    # xref table in the file to override previous\n                    # ones. Since we read the file backwards, assume\n                    # any existing key is already set correctly.\n                    pass\n                else:\n                    if entry_type_b == b\"n\":\n                        self.xref[generation][num] = offset\n                    try:\n                        self.xref_free_entry[generation][num] = entry_type_b == b\"f\"\n                    except Exception:\n                        pass\n                    try:\n                        self.xref_free_entry[65535][num] = entry_type_b == b\"f\"\n                    except Exception:\n                        pass\n                cnt += 1\n                num += 1\n            read_non_whitespace(stream)\n            stream.seek(-1, 1)\n            trailer_tag = stream.read(7)\n            if trailer_tag != b\"trailer\":\n                # more xrefs!\n                stream.seek(-7, 1)\n            else:\n                break\n\n    def _read_xref_tables_and_trailers(\n        self, stream: StreamType, startxref: Optional[int], xref_issue_nr: int\n    ) -> None:\n        \"\"\"Read the cross-reference tables and trailers in the PDF stream.\"\"\"\n        self.xref = {}\n        self.xref_free_entry = {}\n        self.xref_objStm = {}\n        self.trailer = DictionaryObject()\n        visited_xref_offsets: set[int] = set()\n        while startxref is not None:\n            # Detect circular /Prev references in the xref chain\n            if startxref in visited_xref_offsets:\n                logger_warning(\n                    f\"Circular xref chain detected at offset {startxref}, stopping\",\n                    __name__,\n                )\n                break\n            visited_xref_offsets.add(startxref)\n            # load the xref table\n            stream.seek(startxref, 0)\n            x = stream.read(1)\n            if x in b\"\\r\\n\":\n                x = stream.read(1)\n            if x == b\"x\":\n                startxref = self._read_xref(stream)\n            elif xref_issue_nr:\n                try:\n                    self._rebuild_xref_table(stream)\n                    break\n                except Exception:\n                    xref_issue_nr = 0\n            elif x.isdigit():\n                try:\n                    xrefstream = self._read_pdf15_xref_stream(stream)\n                except Exception as e:\n                    if TK.ROOT in self.trailer:\n                        logger_warning(\n                            f\"Previous trailer cannot be read: {e.args}\", __name__\n                        )\n                        break\n                    raise PdfReadError(f\"Trailer cannot be read: {e!s}\")\n                self._process_xref_stream(xrefstream)\n                if \"/Prev\" in xrefstream:\n                    startxref = cast(int, xrefstream[\"/Prev\"])\n                else:\n                    break\n            else:\n                startxref = self._read_xref_other_error(stream, startxref)\n\n    def _process_xref_stream(self, xrefstream: DictionaryObject) -> None:\n        \"\"\"Process and handle the xref stream.\"\"\"\n        trailer_keys = TK.ROOT, TK.ENCRYPT, TK.INFO, TK.ID, TK.SIZE\n        for key in trailer_keys:\n            if key in xrefstream and key not in self.trailer:\n                self.trailer[NameObject(key)] = xrefstream.raw_get(key)\n        if \"/XRefStm\" in xrefstream:\n            p = self.stream.tell()\n            self.stream.seek(cast(int, xrefstream[\"/XRefStm\"]) + 1, 0)\n            self._read_pdf15_xref_stream(self.stream)\n            self.stream.seek(p, 0)\n\n    def _read_xref(self, stream: StreamType) -> Optional[int]:\n        self._read_standard_xref_table(stream)\n        if stream.read(1) == b\"\":\n            return None\n        stream.seek(-1, 1)\n        read_non_whitespace(stream)\n        stream.seek(-1, 1)\n        new_trailer = cast(dict[str, Any], read_object(stream, self))\n        for key, value in new_trailer.items():\n            if key not in self.trailer:\n                self.trailer[key] = value\n        if \"/XRefStm\" in new_trailer:\n            p = stream.tell()\n            stream.seek(cast(int, new_trailer[\"/XRefStm\"]) + 1, 0)\n            try:\n                self._read_pdf15_xref_stream(stream)\n            except Exception:\n                logger_warning(\n                    f\"XRef object at {new_trailer['/XRefStm']} can not be read, some object may be missing\",\n                    __name__,\n                )\n            stream.seek(p, 0)\n        if \"/Prev\" in new_trailer:\n            return new_trailer[\"/Prev\"]\n        return None\n\n    def _read_xref_other_error(\n        self, stream: StreamType, startxref: int\n    ) -> Optional[int]:\n        # some PDFs have /Prev=0 in the trailer, instead of no /Prev\n        if startxref == 0:\n            if self.strict:\n                raise PdfReadError(\n                    \"/Prev=0 in the trailer (try opening with strict=False)\"\n                )\n            logger_warning(\n                \"/Prev=0 in the trailer - assuming there is no previous xref table\",\n                __name__,\n            )\n            return None\n        # bad xref character at startxref. Let's see if we can find\n        # the xref table nearby, as we've observed this error with an\n        # off-by-one before.\n        stream.seek(-11, 1)\n        tmp = stream.read(20)\n        xref_loc = tmp.find(b\"xref\")\n        if xref_loc != -1:\n            startxref -= 10 - xref_loc\n            return startxref\n        # No explicit xref table, try finding a cross-reference stream.\n        stream.seek(startxref, 0)\n        for look in range(25):  # value extended to cope with more linearized files\n            if stream.read(1).isdigit():\n                # This is not a standard PDF, consider adding a warning\n                startxref += look\n                return startxref\n        # no xref table found at specified location\n        if \"/Root\" in self.trailer and not self.strict:\n            # if Root has been already found, just raise warning\n            logger_warning(\"Invalid parent xref., rebuild xref\", __name__)\n            try:\n                self._rebuild_xref_table(stream)\n                return None\n            except Exception:\n                raise PdfReadError(\"Cannot rebuild xref\")\n        raise PdfReadError(\"Could not find xref table at specified location\")\n\n    def _read_pdf15_xref_stream(\n        self, stream: StreamType\n    ) -> Union[ContentStream, EncodedStreamObject, DecodedStreamObject]:\n        \"\"\"Read the cross-reference stream for PDF 1.5+.\"\"\"\n        stream.seek(-1, 1)\n        idnum, generation = self.read_object_header(stream)\n        xrefstream = cast(ContentStream, read_object(stream, self))\n        if cast(str, xrefstream[\"/Type\"]) != \"/XRef\":\n            raise PdfReadError(f\"Unexpected type {xrefstream['/Type']!r}\")\n        self.cache_indirect_object(generation, idnum, xrefstream)\n\n        # Index pairs specify the subsections in the dictionary.\n        # If none, create one subsection that spans everything.\n        if \"/Size\" not in xrefstream:\n            # According to table 17 of the PDF 2.0 specification, this key is required.\n            raise PdfReadError(f\"Size missing from XRef stream {xrefstream!r}!\")\n        idx_pairs = xrefstream.get(\"/Index\", [0, xrefstream[\"/Size\"]])\n\n        entry_sizes = cast(dict[Any, Any], xrefstream.get(\"/W\"))\n        assert len(entry_sizes) >= 3\n        if self.strict and len(entry_sizes) > 3:\n            raise PdfReadError(f\"Too many entry sizes: {entry_sizes}\")\n\n        stream_data = BytesIO(xrefstream.get_data())\n\n        def get_entry(i: int) -> Union[int, tuple[int, ...]]:\n            # Reads the correct number of bytes for each entry. See the\n            # discussion of the W parameter in PDF spec table 17.\n            if entry_sizes[i] > 0:\n                d = stream_data.read(entry_sizes[i])\n                return convert_to_int(d, entry_sizes[i])\n\n            # PDF Spec Table 17: A value of zero for an element in the\n            # W array indicates...the default value shall be used\n            if i == 0:\n                return 1  # First value defaults to 1\n            return 0\n\n        def used_before(num: int, generation: Union[int, tuple[int, ...]]) -> bool:\n            # We move backwards through the xrefs, don't replace any.\n            return num in self.xref.get(generation, []) or num in self.xref_objStm  # type: ignore\n\n        # Iterate through each subsection\n        self._read_xref_subsections(idx_pairs, get_entry, used_before)\n        return xrefstream\n\n    @staticmethod\n    def _get_xref_issues(stream: StreamType, startxref: int) -> int:\n        \"\"\"\n        Return an int which indicates an issue. 0 means there is no issue.\n\n        Args:\n            stream:\n            startxref:\n\n        Returns:\n            0 means no issue, other values represent specific issues.\n\n        \"\"\"\n        if startxref == 0:\n            return 4\n\n        stream.seek(startxref - 1, 0)  # -1 to check character before\n        line = stream.read(1)\n        if line == b\"j\":\n            line = stream.read(1)\n        if line not in b\"\\r\\n \\t\":\n            return 1\n        line = stream.read(4)\n        if line != b\"xref\":\n            # not a xref so check if it is an XREF object\n            line = b\"\"\n            while line in b\"0123456789 \\t\":\n                line = stream.read(1)\n                if line == b\"\":\n                    return 2\n            line += stream.read(2)  # 1 char already read, +2 to check \"obj\"\n            if line.lower() != b\"obj\":\n                return 3\n        return 0\n\n    @classmethod\n    def _find_pdf_objects(cls, data: bytes) -> Iterable[tuple[int, int, int]]:\n        index = 0\n        ord_0 = ord(\"0\")\n        ord_9 = ord(\"9\")\n        while True:\n            index = data.find(b\" obj\", index)\n            if index == -1:\n                return\n\n            index_before_space = index - 1\n\n            # Skip whitespace backwards\n            while index_before_space >= 0 and data[index_before_space] in WHITESPACES_AS_BYTES:\n                index_before_space -= 1\n\n            # Read generation number\n            generation_end = index_before_space + 1\n            while index_before_space >= 0 and ord_0 <= data[index_before_space] <= ord_9:\n                index_before_space -= 1\n            generation_start = index_before_space + 1\n\n            # Skip whitespace\n            while index_before_space >= 0 and data[index_before_space] in WHITESPACES_AS_BYTES:\n                index_before_space -= 1\n\n            # Read object number\n            object_end = index_before_space + 1\n            while index_before_space >= 0 and ord_0 <= data[index_before_space] <= ord_9:\n                index_before_space -= 1\n            object_start = index_before_space + 1\n\n            # Validate\n            if object_start < object_end and generation_start < generation_end:\n                object_number = int(data[object_start:object_end])\n                generation_number = int(data[generation_start:generation_end])\n\n                yield object_number, generation_number, object_start\n\n            index += 4  # len(b\" obj\")\n\n    @classmethod\n    def _find_pdf_trailers(cls, data: bytes) -> Iterable[int]:\n        index = 0\n        data_length = len(data)\n        while True:\n            index = data.find(b\"trailer\", index)\n            if index == -1:\n                return\n\n            index_after_trailer = index + 7  # len(b\"trailer\")\n\n            # Skip whitespace\n            while index_after_trailer < data_length and data[index_after_trailer] in WHITESPACES_AS_BYTES:\n                index_after_trailer += 1\n\n            # Must be dictionary start\n            if index_after_trailer + 1 < data_length and data[index_after_trailer:index_after_trailer+2] == b\"<<\":\n                yield index_after_trailer  # offset of '<<'\n\n            index += 7  # len(b\"trailer\")\n\n    def _rebuild_xref_table(self, stream: StreamType) -> None:\n        self.xref = {}\n        stream.seek(0, 0)\n        stream_data = stream.read(-1)\n\n        for object_number, generation_number, object_start in self._find_pdf_objects(stream_data):\n            if generation_number not in self.xref:\n                self.xref[generation_number] = {}\n            self.xref[generation_number][object_number] = object_start\n\n        logger_warning(\"parsing for Object Streams\", __name__)\n        for generation_number in self.xref:\n            for object_number in self.xref[generation_number]:\n                # get_object in manual\n                stream.seek(self.xref[generation_number][object_number], 0)\n                try:\n                    _ = self.read_object_header(stream)\n                    obj = cast(StreamObject, read_object(stream, self))\n                    if obj.get(\"/Type\", \"\") != \"/ObjStm\":\n                        continue\n                    object_stream = BytesIO(obj.get_data())\n                    actual_count = 0\n                    while True:\n                        current = read_until_whitespace(object_stream)\n                        if not current.isdigit():\n                            break\n                        inner_object_number = int(current)\n                        skip_over_whitespace(object_stream)\n                        object_stream.seek(-1, 1)\n                        current = read_until_whitespace(object_stream)\n                        if not current.isdigit():  # pragma: no cover\n                            break  # pragma: no cover\n                        inner_generation_number = int(current)\n                        self.xref_objStm[inner_object_number] = (object_number, inner_generation_number)\n                        actual_count += 1\n                    if actual_count != obj.get(\"/N\"):  # pragma: no cover\n                        logger_warning(  # pragma: no cover\n                            f\"found {actual_count} objects within Object({object_number},{generation_number})\"\n                            f\" whereas {obj.get('/N')} expected\",\n                            __name__,\n                        )\n                except Exception:  # could be multiple causes\n                    pass\n\n        stream.seek(0, 0)\n        for position in self._find_pdf_trailers(stream_data):\n            stream.seek(position, 0)\n            new_trailer = cast(dict[Any, Any], read_object(stream, self))\n            # Here, we are parsing the file from start to end, the new data have to erase the existing.\n            for key, value in new_trailer.items():\n                self.trailer[key] = value\n\n    def _read_xref_subsections(\n        self,\n        idx_pairs: list[int],\n        get_entry: Callable[[int], Union[int, tuple[int, ...]]],\n        used_before: Callable[[int, Union[int, tuple[int, ...]]], bool],\n    ) -> None:\n        \"\"\"Read and process the subsections of the xref.\"\"\"\n        for start, size in self._pairs(idx_pairs):\n            # The subsections must increase\n            for num in range(start, start + size):\n                # The first entry is the type\n                xref_type = get_entry(0)\n                # The rest of the elements depend on the xref_type\n                if xref_type == 0:\n                    # linked list of free objects\n                    next_free_object = get_entry(1)  # noqa: F841\n                    next_generation = get_entry(2)  # noqa: F841\n                elif xref_type == 1:\n                    # objects that are in use but are not compressed\n                    byte_offset = get_entry(1)\n                    generation = get_entry(2)\n                    if generation not in self.xref:\n                        self.xref[generation] = {}  # type: ignore\n                    if not used_before(num, generation):\n                        self.xref[generation][num] = byte_offset  # type: ignore\n                elif xref_type == 2:\n                    # compressed objects\n                    objstr_num = get_entry(1)\n                    obstr_idx = get_entry(2)\n                    generation = 0  # PDF spec table 18, generation is 0\n                    if not used_before(num, generation):\n                        self.xref_objStm[num] = (objstr_num, obstr_idx)\n                elif self.strict:\n                    raise PdfReadError(f\"Unknown xref type: {xref_type}\")\n\n    def _pairs(self, array: list[int]) -> Iterable[tuple[int, int]]:\n        \"\"\"Iterate over pairs in the array.\"\"\"\n        i = 0\n        while i + 1 < len(array):\n            yield array[i], array[i + 1]\n            i += 2\n\n    def decrypt(self, password: Union[str, bytes]) -> PasswordType:\n        \"\"\"\n        When using an encrypted / secured PDF file with the PDF Standard\n        encryption handler, this function will allow the file to be decrypted.\n        It checks the given password against the document's user password and\n        owner password, and then stores the resulting decryption key if either\n        password is correct.\n\n        It does not matter which password was matched. Both passwords provide\n        the correct decryption key that will allow the document to be used with\n        this library.\n\n        Args:\n            password: The password to match.\n\n        Returns:\n            An indicator if the document was decrypted and whether it was the\n            owner password or the user password.\n\n        \"\"\"\n        if not self._encryption:\n            raise PdfReadError(\"Not encrypted file\")\n        # TODO: raise Exception for wrong password\n        return self._encryption.verify(password)\n\n    @property\n    def is_encrypted(self) -> bool:\n        \"\"\"\n        Read-only boolean property showing whether this PDF file is encrypted.\n\n        Note that this property, if true, will remain true even after the\n        :meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called.\n        \"\"\"\n        return TK.ENCRYPT in self.trailer\n\n    def add_form_topname(self, name: str) -> Optional[DictionaryObject]:\n        \"\"\"\n        Add a top level form that groups all form fields below it.\n\n        Args:\n            name: text string of the \"/T\" Attribute of the created object\n\n        Returns:\n            The created object. ``None`` means no object was created.\n\n        \"\"\"\n        catalog = self.root_object\n\n        if \"/AcroForm\" not in catalog or not isinstance(\n            catalog[\"/AcroForm\"], DictionaryObject\n        ):\n            return None\n        acroform = cast(DictionaryObject, catalog[NameObject(\"/AcroForm\")])\n        if \"/Fields\" not in acroform:\n            # TODO: No error but this may be extended for XFA Forms\n            return None\n\n        interim = DictionaryObject()\n        interim[NameObject(\"/T\")] = TextStringObject(name)\n        interim[NameObject(\"/Kids\")] = acroform[NameObject(\"/Fields\")]\n        self.cache_indirect_object(\n            0,\n            max(i for (g, i) in self.resolved_objects if g == 0) + 1,\n            interim,\n        )\n        arr = ArrayObject()\n        arr.append(interim.indirect_reference)\n        acroform[NameObject(\"/Fields\")] = arr\n        for o in cast(ArrayObject, interim[\"/Kids\"]):\n            obj = o.get_object()\n            if \"/Parent\" in obj:\n                logger_warning(\n                    f\"Top Level Form Field {obj.indirect_reference} have a non-expected parent\",\n                    __name__,\n                )\n            obj[NameObject(\"/Parent\")] = interim.indirect_reference\n        return interim\n\n    def rename_form_topname(self, name: str) -> Optional[DictionaryObject]:\n        \"\"\"\n        Rename top level form field that all form fields below it.\n\n        Args:\n            name: text string of the \"/T\" field of the created object\n\n        Returns:\n            The modified object. ``None`` means no object was modified.\n\n        \"\"\"\n        catalog = self.root_object\n\n        if \"/AcroForm\" not in catalog or not isinstance(\n            catalog[\"/AcroForm\"], DictionaryObject\n        ):\n            return None\n        acroform = cast(DictionaryObject, catalog[NameObject(\"/AcroForm\")])\n        if \"/Fields\" not in acroform:\n            return None\n\n        interim = cast(\n            DictionaryObject,\n            cast(ArrayObject, acroform[NameObject(\"/Fields\")])[0].get_object(),\n        )\n        interim[NameObject(\"/T\")] = TextStringObject(name)\n        return interim\n\n    def _repr_mimebundle_(\n        self,\n        include: Union[None, Iterable[str]] = None,\n        exclude: Union[None, Iterable[str]] = None,\n    ) -> dict[str, Any]:\n        \"\"\"\n        Integration into Jupyter Notebooks.\n\n        This method returns a dictionary that maps a mime-type to its\n        representation.\n\n        .. seealso::\n\n            https://ipython.readthedocs.io/en/stable/config/integrating.html\n        \"\"\"\n        self.stream.seek(0)\n        pdf_data = self.stream.read()\n        data = {\n            \"application/pdf\": pdf_data,\n        }\n\n        if include is not None:\n            # Filter representations based on include list\n            data = {k: v for k, v in data.items() if k in include}\n\n        if exclude is not None:\n            # Remove representations based on exclude list\n            data = {k: v for k, v in data.items() if k not in exclude}\n\n        return data\n"
  },
  {
    "path": "pypdf/_text_extraction/__init__.py",
    "content": "\"\"\"\nCode related to text extraction.\n\nSome parts are still in _page.py. In doubt, they will stay there.\n\"\"\"\n\nimport math\nfrom typing import Any, Callable, Optional, Union\n\nfrom .._font import Font\nfrom ..generic import DictionaryObject, TextStringObject, encode_pdfdocencoding\n\nCUSTOM_RTL_MIN: int = -1\nCUSTOM_RTL_MAX: int = -1\nCUSTOM_RTL_SPECIAL_CHARS: list[int] = []\nLAYOUT_NEW_BT_GROUP_SPACE_WIDTHS: int = 5\n\n\nclass OrientationNotFoundError(Exception):\n    pass\n\n\ndef set_custom_rtl(\n    _min: Union[str, int, None] = None,\n    _max: Union[str, int, None] = None,\n    specials: Union[str, list[int], None] = None,\n) -> tuple[int, int, list[int]]:\n    \"\"\"\n    Change the Right-To-Left and special characters custom parameters.\n\n    Args:\n        _min: The new minimum value for the range of custom characters that\n            will be written right to left.\n            If set to ``None``, the value will not be changed.\n            If set to an integer or string, it will be converted to its ASCII code.\n            The default value is -1, which sets no additional range to be converted.\n        _max: The new maximum value for the range of custom characters that will\n            be written right to left.\n            If set to ``None``, the value will not be changed.\n            If set to an integer or string, it will be converted to its ASCII code.\n            The default value is -1, which sets no additional range to be converted.\n        specials: The new list of special characters to be inserted in the\n            current insertion order.\n            If set to ``None``, the current value will not be changed.\n            If set to a string, it will be converted to a list of ASCII codes.\n            The default value is an empty list.\n\n    Returns:\n        A tuple containing the new values for ``CUSTOM_RTL_MIN``,\n        ``CUSTOM_RTL_MAX``, and ``CUSTOM_RTL_SPECIAL_CHARS``.\n\n    \"\"\"\n    global CUSTOM_RTL_MIN, CUSTOM_RTL_MAX, CUSTOM_RTL_SPECIAL_CHARS\n    if isinstance(_min, int):\n        CUSTOM_RTL_MIN = _min\n    elif isinstance(_min, str):\n        CUSTOM_RTL_MIN = ord(_min)\n    if isinstance(_max, int):\n        CUSTOM_RTL_MAX = _max\n    elif isinstance(_max, str):\n        CUSTOM_RTL_MAX = ord(_max)\n    if isinstance(specials, str):\n        CUSTOM_RTL_SPECIAL_CHARS = [ord(x) for x in specials]\n    elif isinstance(specials, list):\n        CUSTOM_RTL_SPECIAL_CHARS = specials\n    return CUSTOM_RTL_MIN, CUSTOM_RTL_MAX, CUSTOM_RTL_SPECIAL_CHARS\n\n\ndef mult(m: list[float], n: list[float]) -> list[float]:\n    return [\n        m[0] * n[0] + m[1] * n[2],\n        m[0] * n[1] + m[1] * n[3],\n        m[2] * n[0] + m[3] * n[2],\n        m[2] * n[1] + m[3] * n[3],\n        m[4] * n[0] + m[5] * n[2] + n[4],\n        m[4] * n[1] + m[5] * n[3] + n[5],\n    ]\n\n\ndef orient(m: list[float]) -> int:\n    if m[3] > 1e-6:\n        return 0\n    if m[3] < -1e-6:\n        return 180\n    if m[1] > 0:\n        return 90\n    return 270\n\n\ndef crlf_space_check(\n    text: str,\n    cmtm_prev: tuple[list[float], list[float]],\n    cmtm_matrix: tuple[list[float], list[float]],\n    memo_cmtm: tuple[list[float], list[float]],\n    font_resource: Optional[DictionaryObject],\n    orientations: tuple[int, ...],\n    output: str,\n    font_size: float,\n    visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]],\n    str_widths: float,\n    spacewidth: float,\n    str_height: float,\n) -> tuple[str, str, list[float], list[float]]:\n    cm_prev = cmtm_prev[0]\n    tm_prev = cmtm_prev[1]\n    cm_matrix = cmtm_matrix[0]\n    tm_matrix = cmtm_matrix[1]\n    memo_cm = memo_cmtm[0]\n    memo_tm = memo_cmtm[1]\n\n    m_prev = mult(tm_prev, cm_prev)\n    m = mult(tm_matrix, cm_matrix)\n    orientation = orient(m)\n    delta_x = m[4] - m_prev[4]\n    delta_y = m[5] - m_prev[5]\n    # Table 108 of the 1.7 reference (\"Text positioning operators\")\n    scale_prev_x = math.sqrt(tm_prev[0]**2 + tm_prev[1]**2)\n    scale_prev_y = math.sqrt(tm_prev[2]**2 + tm_prev[3]**2)\n    scale_y = math.sqrt(tm_matrix[2]**2 + tm_matrix[3]**2)\n    cm_prev = m\n\n    if orientation not in orientations:\n        raise OrientationNotFoundError\n    if orientation in (0, 180):\n        moved_height: float = delta_y\n        moved_width: float = delta_x\n    elif orientation in (90, 270):\n        moved_height = delta_x\n        moved_width = delta_y\n    try:\n        if abs(moved_height) > 0.8 * min(str_height * scale_prev_y, font_size * scale_y):\n            if (output + text)[-1] != \"\\n\":\n                output += text + \"\\n\"\n                if visitor_text is not None:\n                    visitor_text(\n                        text + \"\\n\",\n                        memo_cm,\n                        memo_tm,\n                        font_resource,\n                        font_size,\n                    )\n                text = \"\"\n        elif (\n            (moved_width >= (spacewidth + str_widths) * scale_prev_x)\n            and (output + text)[-1] != \" \"\n        ):\n            text += \" \"\n    except Exception:\n        pass\n    tm_prev = tm_matrix.copy()\n    cm_prev = cm_matrix.copy()\n    return text, output, cm_prev, tm_prev\n\n\ndef get_text_operands(\n    operands: list[Union[str, TextStringObject]],\n    cm_matrix: list[float],\n    tm_matrix: list[float],\n    font: Font,\n    orientations: tuple[int, ...]\n) -> tuple[str, bool]:\n    t: str = \"\"\n    is_str_operands = False\n    m = mult(tm_matrix, cm_matrix)\n    orientation = orient(m)\n    if orientation in orientations and len(operands) > 0:\n        if isinstance(operands[0], str):\n            t = operands[0]\n            is_str_operands = True\n        else:\n            t = \"\"\n            tt: bytes = (\n                encode_pdfdocencoding(operands[0])\n                if isinstance(operands[0], str)\n                else operands[0]\n            )\n            if isinstance(font.encoding, str):\n                try:\n                    t = tt.decode(font.encoding, \"surrogatepass\")  # apply str encoding\n                except Exception:\n                    # the data does not match the expectation,\n                    # we use the alternative ;\n                    # text extraction may not be good\n                    t = tt.decode(\n                        \"utf-16-be\" if font.encoding == \"charmap\" else \"charmap\",\n                        \"surrogatepass\",\n                    )  # apply str encoding\n            else:  # apply dict encoding\n                t = \"\".join(\n                    [font.encoding[x] if x in font.encoding else bytes((x,)).decode() for x in tt]\n                )\n    return (t, is_str_operands)\n\n\ndef get_display_str(\n    text: str,\n    cm_matrix: list[float],\n    tm_matrix: list[float],\n    font_resource: Optional[DictionaryObject],\n    font: Font,\n    text_operands: str,\n    font_size: float,\n    rtl_dir: bool,\n    visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]]\n) -> tuple[str, bool, float]:\n    # \"\\u0590 - \\u08FF \\uFB50 - \\uFDFF\"\n    widths: float = 0.0\n    for x in [font.character_map.get(x, x) for x in text_operands]:\n        # x can be a sequence of bytes ; ex: habibi.pdf\n        if len(x) == 1:\n            xx = ord(x)\n        else:\n            xx = 1\n        # fmt: off\n        if (\n            # cases where the current inserting order is kept\n            (xx <= 0x2F)                        # punctuations but...\n            or 0x3A <= xx <= 0x40               # numbers (x30-39)\n            or 0x2000 <= xx <= 0x206F           # upper punctuations..\n            or 0x20A0 <= xx <= 0x21FF           # but (numbers) indices/exponents\n            or xx in CUSTOM_RTL_SPECIAL_CHARS   # customized....\n        ):\n            text = x + text if rtl_dir else text + x\n        elif (  # right-to-left characters set\n            0x0590 <= xx <= 0x08FF\n            or 0xFB1D <= xx <= 0xFDFF\n            or 0xFE70 <= xx <= 0xFEFF\n            or CUSTOM_RTL_MIN <= xx <= CUSTOM_RTL_MAX\n        ):\n            if not rtl_dir:\n                rtl_dir = True\n                if visitor_text is not None:\n                    visitor_text(text, cm_matrix, tm_matrix, font_resource, font_size)\n                text = \"\"\n            text = x + text\n        else:  # left-to-right\n            if rtl_dir:\n                rtl_dir = False\n                if visitor_text is not None:\n                    visitor_text(text, cm_matrix, tm_matrix, font_resource, font_size)\n                text = \"\"\n            text = text + x\n        widths += font.space_width if x == \" \" else font.text_width(x)\n        # fmt: on\n    return text, rtl_dir, widths\n"
  },
  {
    "path": "pypdf/_text_extraction/_layout_mode/__init__.py",
    "content": "\"\"\"Layout mode text extraction extension for pypdf\"\"\"\nfrom ..._font import Font\nfrom ._fixed_width_page import (\n    fixed_char_width,\n    fixed_width_page,\n    text_show_operations,\n    y_coordinate_groups,\n)\n\n__all__ = [\n    \"Font\",\n    \"fixed_char_width\",\n    \"fixed_width_page\",\n    \"text_show_operations\",\n    \"y_coordinate_groups\",\n]\n"
  },
  {
    "path": "pypdf/_text_extraction/_layout_mode/_fixed_width_page.py",
    "content": "\"\"\"Extract PDF text preserving the layout of the source PDF\"\"\"\n\nfrom collections.abc import Iterator\nfrom itertools import groupby\nfrom math import ceil\nfrom pathlib import Path\nfrom typing import Any, Literal, Optional, TypedDict\n\nfrom ..._font import Font\nfrom ..._utils import logger_warning\nfrom .. import LAYOUT_NEW_BT_GROUP_SPACE_WIDTHS\nfrom ._text_state_manager import TextStateManager\nfrom ._text_state_params import TextStateParams\n\n\nclass BTGroup(TypedDict):\n    \"\"\"\n    Dict describing a line of text rendered within a BT/ET operator pair.\n    If multiple text show operations render text on the same line, the text\n    will be combined into a single BTGroup dict.\n\n    Keys:\n        tx: x coordinate of first character in BTGroup\n        ty: y coordinate of first character in BTGroup\n        font_size: nominal font size\n        font_height: effective font height\n        text: rendered text\n        displaced_tx: x coordinate of last character in BTGroup\n        flip_sort: -1 if page is upside down, else 1\n    \"\"\"\n\n    tx: float\n    ty: float\n    font_size: float\n    font_height: float\n    text: str\n    displaced_tx: float\n    flip_sort: Literal[-1, 1]\n\n\ndef bt_group(tj_op: TextStateParams, rendered_text: str, dispaced_tx: float) -> BTGroup:\n    \"\"\"\n    BTGroup constructed from a TextStateParams instance, rendered text, and\n    displaced tx value.\n\n    Args:\n        tj_op (TextStateParams): TextStateParams instance\n        rendered_text (str): rendered text\n        dispaced_tx (float): x coordinate of last character in BTGroup\n\n    \"\"\"\n    return BTGroup(\n        tx=tj_op.tx,\n        ty=tj_op.ty,\n        font_size=tj_op.font_size,\n        font_height=tj_op.font_height,\n        text=rendered_text,\n        displaced_tx=dispaced_tx,\n        flip_sort=-1 if tj_op.flip_vertical else 1,\n    )\n\n\ndef recurs_to_target_op(\n    ops: Iterator[tuple[list[Any], bytes]],\n    text_state_mgr: TextStateManager,\n    end_target: Literal[b\"Q\", b\"ET\"],\n    fonts: dict[str, Font],\n    strip_rotated: bool = True,\n) -> tuple[list[BTGroup], list[TextStateParams]]:\n    \"\"\"\n    Recurse operators between BT/ET and/or q/Q operators managing the transform\n    stack and capturing text positioning and rendering data.\n\n    Args:\n        ops: iterator of operators in content stream\n        text_state_mgr: a TextStateManager instance\n        end_target: Either b\"Q\" (ends b\"q\" op) or b\"ET\" (ends b\"BT\" op)\n        fonts: font dictionary as returned by PageObject._layout_mode_fonts()\n\n    Returns:\n        tuple: list of BTGroup dicts + list of TextStateParams dataclass instances.\n\n    \"\"\"\n    # 1 entry per line of text rendered within each BT/ET operation.\n    bt_groups: list[BTGroup] = []\n\n    # 1 entry per text show operator (Tj/TJ/'/\")\n    tj_ops: list[TextStateParams] = []\n\n    if end_target == b\"Q\":\n        # add new q level. cm's added at this level will be popped at next b'Q'\n        text_state_mgr.add_q()\n\n    for operands, op in ops:\n        # The loop is broken by the end target, or exits normally when there are no more ops.\n        if op == end_target:\n            if op == b\"Q\":\n                text_state_mgr.remove_q()\n            if op == b\"ET\":\n                if not tj_ops:\n                    return bt_groups, tj_ops\n                _text = \"\"\n                bt_idx = 0  # idx of first tj in this bt group\n                last_displaced_tx = tj_ops[bt_idx].displaced_tx\n                last_ty = tj_ops[bt_idx].ty\n                for _idx, _tj in enumerate(\n                    tj_ops\n                ):  # ... build text from new Tj operators\n                    if strip_rotated and _tj.rotated:\n                        continue\n                    if not _tj.font.interpretable:  # generates warning\n                        continue\n                    # if the y position of the text is greater than the font height, assume\n                    # the text is on a new line and start a new group\n                    if abs(_tj.ty - last_ty) > _tj.font_height:\n                        if _text.strip():\n                            bt_groups.append(\n                                bt_group(tj_ops[bt_idx], _text, last_displaced_tx)\n                            )\n                        bt_idx = _idx\n                        _text = \"\"\n\n                    # if the x position of the text is less than the last x position by\n                    # more than 5 spaces widths, assume the text order should be flipped\n                    # and start a new group\n                    if (\n                        last_displaced_tx - _tj.tx\n                        > _tj.space_tx * LAYOUT_NEW_BT_GROUP_SPACE_WIDTHS\n                    ):\n                        if _text.strip():\n                            bt_groups.append(\n                                bt_group(tj_ops[bt_idx], _text, last_displaced_tx)\n                            )\n                        bt_idx = _idx\n                        last_displaced_tx = _tj.displaced_tx\n                        _text = \"\"\n\n                    # calculate excess x translation based on ending tx of previous Tj.\n                    # multiply by bool (_idx != bt_idx) to ensure spaces aren't double\n                    # applied to the first tj of a BTGroup in fixed_width_page().\n                    excess_tx = round(_tj.tx - last_displaced_tx, 3) * (_idx != bt_idx)\n                    # space_tx could be 0 if either Tz or font_size was 0 for this _tj.\n                    spaces = int(excess_tx // _tj.space_tx) if _tj.space_tx else 0\n                    new_text = f'{\" \" * spaces}{_tj.txt}'\n\n                    last_ty = _tj.ty\n                    _text = f\"{_text}{new_text}\"\n                    last_displaced_tx = _tj.displaced_tx\n                if _text:\n                    bt_groups.append(bt_group(tj_ops[bt_idx], _text, last_displaced_tx))\n                text_state_mgr.reset_tm()\n            break\n        if op == b\"q\":\n            bts, tjs = recurs_to_target_op(\n                ops, text_state_mgr, b\"Q\", fonts, strip_rotated\n            )\n            bt_groups.extend(bts)\n            tj_ops.extend(tjs)\n        elif op == b\"cm\":\n            text_state_mgr.add_cm(*operands)\n        elif op == b\"BT\":\n            bts, tjs = recurs_to_target_op(\n                ops, text_state_mgr, b\"ET\", fonts, strip_rotated\n            )\n            bt_groups.extend(bts)\n            tj_ops.extend(tjs)\n        elif op == b\"Tj\":\n            tj_ops.append(text_state_mgr.text_state_params(operands[0]))\n        elif op == b\"TJ\":\n            _tj = text_state_mgr.text_state_params()\n            for tj_op in operands[0]:\n                if isinstance(tj_op, bytes):\n                    _tj = text_state_mgr.text_state_params(tj_op)\n                    tj_ops.append(_tj)\n                else:\n                    text_state_mgr.add_trm(_tj.displacement_matrix(td_offset=tj_op))\n        elif op == b\"'\":\n            text_state_mgr.reset_trm()\n            text_state_mgr.add_tm([0, -text_state_mgr.TL])\n            tj_ops.append(text_state_mgr.text_state_params(operands[0]))\n        elif op == b'\"':\n            text_state_mgr.reset_trm()\n            text_state_mgr.set_state_param(b\"Tw\", operands[0])\n            text_state_mgr.set_state_param(b\"Tc\", operands[1])\n            text_state_mgr.add_tm([0, -text_state_mgr.TL])\n            tj_ops.append(text_state_mgr.text_state_params(operands[2]))\n        elif op in (b\"Td\", b\"Tm\", b\"TD\", b\"T*\"):\n            text_state_mgr.reset_trm()\n            if op == b\"Tm\":\n                text_state_mgr.reset_tm()\n            elif op == b\"TD\":\n                text_state_mgr.set_state_param(b\"TL\", -operands[1])\n            elif op == b\"T*\":\n                operands = [0, -text_state_mgr.TL]\n            text_state_mgr.add_tm(operands)\n        elif op == b\"Tf\":\n            text_state_mgr.set_font(fonts[operands[0]], operands[1])\n        else:  # handle Tc, Tw, Tz, TL, and Ts operators\n            text_state_mgr.set_state_param(op, operands)\n    else:\n        logger_warning(\n            f\"Unbalanced target operations, expected {end_target!r}.\",\n            __name__,\n        )\n    return bt_groups, tj_ops\n\n\ndef y_coordinate_groups(\n    bt_groups: list[BTGroup], debug_path: Optional[Path] = None\n) -> dict[int, list[BTGroup]]:\n    \"\"\"\n    Group text operations by rendered y coordinate, i.e. the line number.\n\n    Args:\n        bt_groups: list of dicts as returned by text_show_operations()\n        debug_path (Path, optional): Path to a directory for saving debug output.\n\n    Returns:\n        Dict[int, List[BTGroup]]: dict of lists of text rendered by each BT operator\n            keyed by y coordinate\n\n    \"\"\"\n    ty_groups = {\n        ty: sorted(grp, key=lambda x: x[\"tx\"])\n        for ty, grp in groupby(\n            bt_groups, key=lambda bt_grp: int(bt_grp[\"ty\"] * bt_grp[\"flip_sort\"])\n        )\n    }\n    # combine groups whose y coordinates differ by less than the effective font height\n    # (accounts for mixed fonts and other minor oddities)\n    last_ty = next(iter(ty_groups))\n    last_txs = {int(_t[\"tx\"]) for _t in ty_groups[last_ty] if _t[\"text\"].strip()}\n    for ty in list(ty_groups)[1:]:\n        fsz = min(ty_groups[_y][0][\"font_height\"] for _y in (ty, last_ty))\n        txs = {int(_t[\"tx\"]) for _t in ty_groups[ty] if _t[\"text\"].strip()}\n        # prevent merge if both groups are rendering in the same x position.\n        no_text_overlap = not (txs & last_txs)\n        offset_less_than_font_height = abs(ty - last_ty) < fsz\n        if no_text_overlap and offset_less_than_font_height:\n            ty_groups[last_ty] = sorted(\n                ty_groups.pop(ty) + ty_groups[last_ty], key=lambda x: x[\"tx\"]\n            )\n            last_txs |= txs\n        else:\n            last_ty = ty\n            last_txs = txs\n    if debug_path:  # pragma: no cover\n        import json  # noqa: PLC0415\n\n        debug_path.joinpath(\"bt_groups.json\").write_text(\n            json.dumps(ty_groups, indent=2, default=str), \"utf-8\"\n        )\n    return ty_groups\n\n\ndef text_show_operations(\n    ops: Iterator[tuple[list[Any], bytes]],\n    fonts: dict[str, Font],\n    strip_rotated: bool = True,\n    debug_path: Optional[Path] = None,\n) -> list[BTGroup]:\n    \"\"\"\n    Extract text from BT/ET operator pairs.\n\n    Args:\n        ops (Iterator[Tuple[List, bytes]]): iterator of operators in content stream\n        fonts (Dict[str, Font]): font dictionary\n        strip_rotated: Removes text if rotated w.r.t. to the page. Defaults to True.\n        debug_path (Path, optional): Path to a directory for saving debug output.\n\n    Returns:\n        List[BTGroup]: list of dicts of text rendered by each BT operator\n\n    \"\"\"\n    state_mgr = TextStateManager()  # transformation stack manager\n    bt_groups: list[BTGroup] = []  # BT operator dict\n    tj_ops: list[TextStateParams] = []  # Tj/TJ operator data\n    for operands, op in ops:\n        if op in (b\"BT\", b\"q\"):\n            bts, tjs = recurs_to_target_op(\n                ops, state_mgr, b\"ET\" if op == b\"BT\" else b\"Q\", fonts, strip_rotated\n            )\n            bt_groups.extend(bts)\n            tj_ops.extend(tjs)\n        elif op == b\"Tf\":\n            state_mgr.set_font(fonts[operands[0]], operands[1])\n        else:  # set Tc, Tw, Tz, TL, and Ts if required. ignores all other ops\n            state_mgr.set_state_param(op, operands)\n\n    if any(tj.rotated for tj in tj_ops):\n        if strip_rotated:\n            logger_warning(\n                \"Rotated text discovered. Output will be incomplete.\", __name__\n            )\n        else:\n            logger_warning(\n                \"Rotated text discovered. Layout will be degraded.\", __name__\n            )\n    if not all(tj.font.interpretable for tj in tj_ops):\n        logger_warning(\n            \"PDF contains an uninterpretable font. Output will be incomplete.\", __name__\n        )\n\n    # left align the data, i.e. decrement all tx values by min(tx)\n    min_x = min((x[\"tx\"] for x in bt_groups), default=0.0)\n    bt_groups = [\n        dict(ogrp, tx=ogrp[\"tx\"] - min_x, displaced_tx=ogrp[\"displaced_tx\"] - min_x)  # type: ignore[misc]\n        for ogrp in sorted(\n            bt_groups, key=lambda x: (x[\"ty\"] * x[\"flip_sort\"], -x[\"tx\"]), reverse=True\n        )\n    ]\n\n    if debug_path:  # pragma: no cover\n        import json  # noqa: PLC0415\n\n        debug_path.joinpath(\"bts.json\").write_text(\n            json.dumps(bt_groups, indent=2, default=str), \"utf-8\"\n        )\n        debug_path.joinpath(\"tjs.json\").write_text(\n            json.dumps(\n                tj_ops, indent=2, default=lambda x: getattr(x, \"to_dict\", str)(x)\n            ),\n            \"utf-8\",\n        )\n    return bt_groups\n\n\ndef fixed_char_width(bt_groups: list[BTGroup], scale_weight: float = 1.25) -> float:\n    \"\"\"\n    Calculate average character width weighted by the length of the rendered\n    text in each sample for conversion to fixed-width layout.\n\n    Args:\n        bt_groups (List[BTGroup]): List of dicts of text rendered by each\n            BT operator\n\n    Returns:\n        float: fixed character width\n\n    \"\"\"\n    char_widths = []\n    for _bt in bt_groups:\n        _len = len(_bt[\"text\"]) * scale_weight\n        char_widths.append(((_bt[\"displaced_tx\"] - _bt[\"tx\"]) / _len, _len))\n    return sum(_w * _l for _w, _l in char_widths) / sum(_l for _, _l in char_widths)\n\n\ndef fixed_width_page(\n    ty_groups: dict[int, list[BTGroup]], char_width: float, space_vertically: bool, font_height_weight: float\n) -> str:\n    \"\"\"\n    Generate page text from text operations grouped by rendered y coordinate.\n\n    Args:\n        ty_groups: dict of text show ops as returned by y_coordinate_groups()\n        char_width: fixed character width\n        space_vertically: include blank lines inferred from y distance + font height.\n        font_height_weight: multiplier for font height when calculating blank lines.\n\n    Returns:\n        str: page text in a fixed width format that closely adheres to the rendered\n            layout in the source pdf.\n\n    \"\"\"\n    lines: list[str] = []\n    last_y_coord = 0\n    table = str.maketrans(dict.fromkeys(range(14, 32), \" \"))\n    for y_coord, line_data in ty_groups.items():\n        if space_vertically and lines:\n            fh = line_data[0][\"font_height\"]\n            blank_lines = 0 if fh == 0 else (\n                int(abs(y_coord - last_y_coord) / (fh * font_height_weight)) - 1\n            )\n            lines.extend([\"\"] * blank_lines)\n\n        line_parts = []  # It uses a list to construct the line, avoiding string concatenation.\n        current_len = 0  # Track the size with int instead of len(str) overhead.\n        last_disp = 0.0\n        for bt_op in line_data:\n            tx = bt_op[\"tx\"]\n            offset = int(tx // char_width)\n            needed_spaces = offset - current_len\n            if needed_spaces > 0 and ceil(last_disp) < int(tx):\n                padding = \" \" * needed_spaces\n                line_parts.append(padding)\n                current_len += needed_spaces\n\n            raw_text = bt_op[\"text\"]\n            text = raw_text.translate(table)\n            line_parts.append(text)\n            current_len += len(text)\n            last_disp = bt_op[\"displaced_tx\"]\n\n        full_line = \"\".join(line_parts).rstrip()\n        if full_line.strip() or (space_vertically and lines):\n            lines.append(full_line)\n\n        last_y_coord = y_coord\n\n    return \"\\n\".join(lines)\n"
  },
  {
    "path": "pypdf/_text_extraction/_layout_mode/_text_state_manager.py",
    "content": "\"\"\"manage the PDF transform stack during \"layout\" mode text extraction\"\"\"\n\nfrom collections import ChainMap, Counter\nfrom collections import ChainMap as ChainMapType\nfrom collections import Counter as CounterType\nfrom collections.abc import MutableMapping\nfrom typing import Any, Union\n\nfrom ..._font import Font\nfrom ...errors import PdfReadError\nfrom .. import mult\nfrom ._text_state_params import TextStateParams\n\nTextStateManagerChainMapType = ChainMapType[Union[int, str], Union[float, bool]]\nTextStateManagerDictType = MutableMapping[Union[int, str], Union[float, bool]]\n\n\nclass TextStateManager:\n    \"\"\"\n    Tracks the current text state including cm/tm/trm transformation matrices.\n\n    Attributes:\n        transform_stack (ChainMap): ChainMap of cm/tm transformation matrices\n        q_queue (Counter[int]): Counter of q operators\n        q_depth (List[int]): list of q operator nesting levels\n        Tc (float): character spacing\n        Tw (float): word spacing\n        Tz (int): horizontal scaling\n        TL (float): leading\n        Ts (float): text rise\n        font (Font): font object\n        font_size (int | float): font size\n\n    \"\"\"\n\n    def __init__(self) -> None:\n        self.transform_stack: TextStateManagerChainMapType = ChainMap(\n            self.new_transform()\n        )\n        self.q_queue: CounterType[int] = Counter()\n        self.q_depth = [0]\n        self.Tc: float = 0.0\n        self.Tw: float = 0.0\n        self.Tz: float = 100.0\n        self.TL: float = 0.0\n        self.Ts: float = 0.0\n        self.font_stack: list[tuple[Union[Font, None], Union[int, float]]] = []\n        self.font: Union[Font, None] = None\n        self.font_size: Union[int, float] = 0\n\n    def set_state_param(self, op: bytes, value: Union[float, list[Any]]) -> None:\n        \"\"\"\n        Set a text state parameter. Supports Tc, Tz, Tw, TL, and Ts operators.\n\n        Args:\n            op: operator read from PDF stream as bytes. No action is taken\n                for unsupported operators (see supported operators above).\n            value (float | List[Any]): new parameter value. If a list,\n                value[0] is used.\n\n        \"\"\"\n        if op not in [b\"Tc\", b\"Tz\", b\"Tw\", b\"TL\", b\"Ts\"]:\n            return\n        self.__setattr__(op.decode(), value[0] if isinstance(value, list) else value)\n\n    def set_font(self, font: Font, size: float) -> None:\n        \"\"\"\n        Set the current font and font_size.\n\n        Args:\n            font (Font): a layout mode Font\n            size (float): font size\n\n        \"\"\"\n        self.font = font\n        self.font_size = size\n\n    def text_state_params(self, value: Union[bytes, str] = \"\") -> TextStateParams:\n        \"\"\"\n        Create a TextStateParams instance to display a text string. Type[bytes] values\n        will be decoded implicitly.\n\n        Args:\n            value (str | bytes): text to associate with the captured state.\n\n        Raises:\n            PdfReadError: if font not set (no Tf operator in incoming pdf content stream)\n\n        Returns:\n            TextStateParams: current text state parameters\n\n        \"\"\"\n        if not isinstance(self.font, Font):\n            raise PdfReadError(\n                \"font not set: is PDF missing a Tf operator?\"\n            )  # pragma: no cover\n        if isinstance(value, bytes):\n            try:\n                if isinstance(self.font.encoding, str):\n                    txt = value.decode(self.font.encoding, \"surrogatepass\")\n                else:\n                    txt = \"\".join(\n                        self.font.encoding[x]\n                        if x in self.font.encoding\n                        else bytes((x,)).decode()\n                        for x in value\n                    )\n            except (UnicodeEncodeError, UnicodeDecodeError):\n                txt = value.decode(\"utf-8\", \"replace\")\n            txt = \"\".join(\n                self.font.character_map.get(x, x) for x in txt\n            )\n        else:\n            txt = value\n        return TextStateParams(\n            txt,\n            self.font,\n            self.font_size,\n            self.Tc,\n            self.Tw,\n            self.Tz,\n            self.TL,\n            self.Ts,\n            self.effective_transform,\n        )\n\n    @staticmethod\n    def raw_transform(\n        _a: float = 1.0,\n        _b: float = 0.0,\n        _c: float = 0.0,\n        _d: float = 1.0,\n        _e: float = 0.0,\n        _f: float = 0.0,\n    ) -> dict[int, float]:\n        \"\"\"Only a/b/c/d/e/f matrix params\"\"\"\n        return dict(zip(range(6), map(float, (_a, _b, _c, _d, _e, _f))))\n\n    @staticmethod\n    def new_transform(\n        _a: float = 1.0,\n        _b: float = 0.0,\n        _c: float = 0.0,\n        _d: float = 1.0,\n        _e: float = 0.0,\n        _f: float = 0.0,\n        is_text: bool = False,\n        is_render: bool = False,\n    ) -> TextStateManagerDictType:\n        \"\"\"Standard a/b/c/d/e/f matrix params + 'is_text' and 'is_render' keys\"\"\"\n        result: Any = TextStateManager.raw_transform(_a, _b, _c, _d, _e, _f)\n        result.update({\"is_text\": is_text, \"is_render\": is_render})\n        return result\n\n    def reset_tm(self) -> TextStateManagerChainMapType:\n        \"\"\"Clear all transforms from chainmap having is_text==True or is_render==True\"\"\"\n        while (\n            self.transform_stack.maps[0][\"is_text\"]\n            or self.transform_stack.maps[0][\"is_render\"]\n        ):\n            self.transform_stack = self.transform_stack.parents\n        return self.transform_stack\n\n    def reset_trm(self) -> TextStateManagerChainMapType:\n        \"\"\"Clear all transforms from chainmap having is_render==True\"\"\"\n        while self.transform_stack.maps[0][\"is_render\"]:\n            self.transform_stack = self.transform_stack.parents\n        return self.transform_stack\n\n    def remove_q(self) -> TextStateManagerChainMapType:\n        \"\"\"Rewind to stack prior state after closing a 'q' with internal 'cm' ops\"\"\"\n        self.font, self.font_size = self.font_stack.pop(-1)\n        self.transform_stack = self.reset_tm()\n        self.transform_stack.maps = self.transform_stack.maps[\n            self.q_queue.pop(self.q_depth.pop(), 0) :\n        ]\n        return self.transform_stack\n\n    def add_q(self) -> None:\n        \"\"\"Add another level to q_queue\"\"\"\n        self.font_stack.append((self.font, self.font_size))\n        self.q_depth.append(len(self.q_depth))\n\n    def add_cm(self, *args: Any) -> TextStateManagerChainMapType:\n        \"\"\"Concatenate an additional transform matrix\"\"\"\n        self.transform_stack = self.reset_tm()\n        self.q_queue.update(self.q_depth[-1:])\n        self.transform_stack = self.transform_stack.new_child(self.new_transform(*args))\n        return self.transform_stack\n\n    def _complete_matrix(self, operands: list[float]) -> list[float]:\n        \"\"\"Adds a, b, c, and d to an \"e/f only\" operand set (e.g Td)\"\"\"\n        if len(operands) == 2:  # this is a Td operator or equivalent\n            operands = [1.0, 0.0, 0.0, 1.0, *operands]\n        return operands\n\n    def add_tm(self, operands: list[float]) -> TextStateManagerChainMapType:\n        \"\"\"Append a text transform matrix\"\"\"\n        self.transform_stack = self.transform_stack.new_child(\n            self.new_transform(  # type: ignore[misc]\n                *self._complete_matrix(operands), is_text=True  # type: ignore[arg-type]\n            )\n        )\n        return self.transform_stack\n\n    def add_trm(self, operands: list[float]) -> TextStateManagerChainMapType:\n        \"\"\"Append a text rendering transform matrix\"\"\"\n        self.transform_stack = self.transform_stack.new_child(\n            self.new_transform(  # type: ignore[misc]\n                *self._complete_matrix(operands), is_text=True, is_render=True  # type: ignore[arg-type]\n            )\n        )\n        return self.transform_stack\n\n    @property\n    def effective_transform(self) -> list[float]:\n        \"\"\"Current effective transform accounting for cm, tm, and trm transforms\"\"\"\n        eff_transform = [*self.transform_stack.maps[0].values()]\n        for transform in self.transform_stack.maps[1:]:\n            eff_transform = mult(eff_transform, transform)  # type: ignore[arg-type]  # dict has int keys 0-5\n        return eff_transform\n"
  },
  {
    "path": "pypdf/_text_extraction/_layout_mode/_text_state_params.py",
    "content": "\"\"\"A dataclass that captures the CTM and Text State for a tj operation\"\"\"\n\nimport math\nfrom dataclasses import dataclass, field\nfrom typing import Any, Union\n\nfrom ..._font import Font\nfrom .. import mult, orient\n\n\n@dataclass\nclass TextStateParams:\n    \"\"\"\n    Text state parameters and operator values for a single text value in a\n    TJ or Tj PDF operation.\n\n    Attributes:\n        txt (str): the text to be rendered.\n        font (Font): font object\n        font_size (int | float): font size\n        Tc (float): character spacing. Defaults to 0.0.\n        Tw (float): word spacing. Defaults to 0.0.\n        Tz (float): horizontal scaling. Defaults to 100.0.\n        TL (float): leading, vertical displacement between text lines. Defaults to 0.0.\n        Ts (float): text rise. Used for super/subscripts. Defaults to 0.0.\n        transform (List[float]): effective transformation matrix.\n        tx (float): x cood of rendered text, i.e. self.transform[4]\n        ty (float): y cood of rendered text. May differ from self.transform[5] per self.Ts.\n        displaced_tx (float): x coord immediately following rendered text\n        space_tx (float): tx for a space character\n        font_height (float): effective font height accounting for CTM\n        flip_vertical (bool): True if y axis has been inverted (i.e. if self.transform[3] < 0.)\n        rotated (bool): True if the text orientation is rotated with respect to the page.\n\n    \"\"\"\n\n    txt: str\n    font: Font\n    font_size: Union[int, float]\n    Tc: float = 0.0\n    Tw: float = 0.0\n    Tz: float = 100.0\n    TL: float = 0.0\n    Ts: float = 0.0\n    transform: list[float] = field(\n        default_factory=lambda: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n    )\n    tx: float = field(default=0.0, init=False)\n    ty: float = field(default=0.0, init=False)\n    displaced_tx: float = field(default=0.0, init=False)\n    space_tx: float = field(default=0.0, init=False)\n    font_height: float = field(default=0.0, init=False)\n    flip_vertical: bool = field(default=False, init=False)\n    rotated: bool = field(default=False, init=False)\n\n    def __post_init__(self) -> None:\n        if orient(self.transform) in (90, 270):\n            self.transform = mult(\n                [1.0, -self.transform[1], -self.transform[2], 1.0, 0.0, 0.0],\n                self.transform,\n            )\n            self.rotated = True\n        # self.transform[0] AND self.transform[3] < 0 indicates true rotation.\n        # If only self.transform[3] < 0, the y coords are simply inverted.\n        if orient(self.transform) == 180 and self.transform[0] < -1e-6:\n            self.transform = mult([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0], self.transform)\n            self.rotated = True\n        self.displaced_tx = self.displaced_transform()[4]\n        self.tx = self.transform[4]\n        self.ty = self.render_transform()[5]\n        self.space_tx = round(self.word_tx(\" \"), 3)\n        if self.space_tx < 1e-6:\n            # if the \" \" char is assigned 0 width (e.g. for fine tuned spacing\n            # with TJ int operators a la crazyones.pdf), calculate space_tx as\n            # a td_offset of -1 * font.space_width where font.space_width is\n            # the space_width calculated in _font.py.\n            self.space_tx = round(self.word_tx(\"\", -self.font.space_width), 3)\n        self.font_height = self.font_size * math.sqrt(\n            self.transform[1] ** 2 + self.transform[3] ** 2\n        )\n        # flip_vertical handles PDFs generated by Microsoft Word's \"publish\" command.\n        self.flip_vertical = self.transform[3] < -1e-6  # inverts y axis\n\n    def font_size_matrix(self) -> list[float]:\n        \"\"\"Font size matrix\"\"\"\n        return [\n            self.font_size * (self.Tz / 100.0),\n            0.0,\n            0.0,\n            self.font_size,\n            0.0,\n            self.Ts,\n        ]\n\n    def displaced_transform(self) -> list[float]:\n        \"\"\"Effective transform matrix after text has been rendered.\"\"\"\n        return mult(self.displacement_matrix(), self.transform)\n\n    def render_transform(self) -> list[float]:\n        \"\"\"Effective transform matrix accounting for font size, Tz, and Ts.\"\"\"\n        return mult(self.font_size_matrix(), self.transform)\n\n    def displacement_matrix(\n        self, word: Union[str, None] = None, td_offset: float = 0.0\n    ) -> list[float]:\n        \"\"\"\n        Text displacement matrix\n\n        Args:\n            word (str, optional): Defaults to None in which case self.txt displacement is\n                returned.\n            td_offset (float, optional): translation applied by TD operator. Defaults to 0.0.\n\n        \"\"\"\n        word = word if word is not None else self.txt\n        return [1.0, 0.0, 0.0, 1.0, self.word_tx(word, td_offset), 0.0]\n\n    def word_tx(self, word: str, td_offset: float = 0.0) -> float:\n        \"\"\"Horizontal text displacement for any word according this text state\"\"\"\n        width: float = 0.0\n        for char in word:\n            if char == \" \":\n                width += self.font.space_width\n            else:\n                width += self.font.text_width(char)\n        return (\n            (self.font_size * ((width - td_offset) / 1000.0))\n            + self.Tc\n            + word.count(\" \") * self.Tw\n        ) * (self.Tz / 100.0)\n\n    @staticmethod\n    def to_dict(inst: \"TextStateParams\") -> dict[str, Any]:\n        \"\"\"Dataclass to dict for json.dumps serialization\"\"\"\n        return {k: getattr(inst, k) for k in inst.__dataclass_fields__ if k != \"font\"}\n"
  },
  {
    "path": "pypdf/_text_extraction/_text_extractor.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com>\n#\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nimport math\nfrom typing import Any, Callable, Optional, Union\n\nfrom .._font import Font, FontDescriptor\nfrom ..generic import DictionaryObject, TextStringObject\nfrom . import OrientationNotFoundError, crlf_space_check, get_display_str, get_text_operands, mult\n\n\nclass TextExtraction:\n    \"\"\"\n    A class to handle PDF text extraction operations.\n\n    This class encapsulates all the state and operations needed for extracting\n    text from PDF content streams, replacing the nested functions and nonlocal\n    variables in the original implementation.\n    \"\"\"\n\n    def __init__(self) -> None:\n        self._font_width_maps: dict[str, tuple[dict[Any, float], str, float]] = {}\n\n        # Text extraction state variables\n        self.cm_matrix: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n        self.tm_matrix: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n        self.cm_stack: list[\n            tuple[\n                list[float],\n                Optional[DictionaryObject],\n                Font,\n                float,\n                float,\n                float,\n                float,\n            ]\n        ] = []\n\n        # Store the last modified matrices; can be an intermediate position\n        self.cm_prev: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n        self.tm_prev: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n\n        # Store the position at the beginning of building the text\n        self.memo_cm: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n        self.memo_tm: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n\n        self.char_scale = 1.0\n        self.space_scale = 1.0\n        self._space_width: float = 500.0  # will be set correctly at first Tf\n        self._actual_str_size: dict[str, float] = {\n            \"str_widths\": 0.0,\n            \"str_height\": 0.0,\n        }  # will be set to string length calculation result\n        self.TL = 0.0\n        self.font_size = 12.0  # init just in case of\n\n        # Text extraction variables\n        self.text: str = \"\"\n        self.output: str = \"\"\n        self.rtl_dir: bool = False  # right-to-left\n        self.font_resource: Optional[DictionaryObject] = None\n        self.font = Font(\n            name = \"NotInitialized\",\n            sub_type=\"Unknown\",\n            encoding=\"charmap\",\n            font_descriptor=FontDescriptor(),\n            )\n        self.orientations: tuple[int, ...] = (0, 90, 180, 270)\n        self.visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None\n        self.font_resources: dict[str, DictionaryObject] = {}\n        self.fonts: dict[str, Font] = {}\n\n        self.operation_handlers = {\n            b\"BT\": self._handle_bt,\n            b\"ET\": self._handle_et,\n            b\"q\": self._handle_save_graphics_state,\n            b\"Q\": self._handle_restore_graphics_state,\n            b\"cm\": self._handle_cm,\n            b\"Tz\": self._handle_tz,\n            b\"Tw\": self._handle_tw,\n            b\"TL\": self._handle_tl,\n            b\"Tf\": self._handle_tf,\n            b\"Td\": self._handle_td,\n            b\"Tm\": self._handle_tm,\n            b\"T*\": self._handle_t_star,\n            b\"Tj\": self._handle_tj_operation,\n        }\n\n    def initialize_extraction(\n        self,\n        orientations: tuple[int, ...] = (0, 90, 180, 270),\n        visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None,\n        font_resources: Optional[dict[str, DictionaryObject]] = None,\n        fonts: Optional[dict[str, Font]] = None\n    ) -> None:\n        \"\"\"Initialize the extractor with extraction parameters.\"\"\"\n        self.orientations = orientations\n        self.visitor_text = visitor_text\n        self.font_resources = font_resources or {}\n        self.fonts = fonts or {}\n\n        # Reset state\n        self.text = \"\"\n        self.output = \"\"\n        self.rtl_dir = False\n\n    def compute_str_widths(self, str_widths: float) -> float:\n        return str_widths / 1000\n\n    def process_operation(self, operator: bytes, operands: list[Any]) -> None:\n        if operator in self.operation_handlers:\n            handler = self.operation_handlers[operator]\n            str_widths = handler(operands)\n\n            # Post-process operations that affect text positioning\n            if operator in {b\"Td\", b\"Tm\", b\"T*\", b\"Tj\"}:\n                self._post_process_text_operation(str_widths or 0.0)\n\n    def _post_process_text_operation(self, str_widths: float) -> None:\n        \"\"\"Handle common post-processing for text positioning operations.\"\"\"\n        try:\n            self.text, self.output, self.cm_prev, self.tm_prev = crlf_space_check(\n                self.text,\n                (self.cm_prev, self.tm_prev),\n                (self.cm_matrix, self.tm_matrix),\n                (self.memo_cm, self.memo_tm),\n                self.font_resource,\n                self.orientations,\n                self.output,\n                self.font_size,\n                self.visitor_text,\n                str_widths,\n                self.compute_str_widths(self.font_size * self._space_width),\n                self._actual_str_size[\"str_height\"],\n            )\n            if self.text == \"\":\n                self.memo_cm = self.cm_matrix.copy()\n                self.memo_tm = self.tm_matrix.copy()\n        except OrientationNotFoundError:\n            pass\n\n    def _handle_tj(\n        self,\n        text: str,\n        operands: list[Union[str, TextStringObject]],\n        cm_matrix: list[float],\n        tm_matrix: list[float],\n        font_resource: Optional[DictionaryObject],\n        font: Font,\n        orientations: tuple[int, ...],\n        font_size: float,\n        rtl_dir: bool,\n        visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]],\n        actual_str_size: dict[str, float],\n    ) -> tuple[str, bool, dict[str, float]]:\n        text_operands, is_str_operands = get_text_operands(\n            operands, cm_matrix, tm_matrix, font, orientations\n        )\n        if is_str_operands:\n            text += text_operands\n            font_widths = sum([font.space_width if x == \" \" else font.text_width(x) for x in text_operands])\n        else:\n            text, rtl_dir, font_widths = get_display_str(\n                text,\n                cm_matrix,\n                tm_matrix,  # text matrix\n                font_resource,\n                font,\n                text_operands,\n                font_size,\n                rtl_dir,\n                visitor_text,\n            )\n        actual_str_size[\"str_widths\"] += font_widths * font_size\n        actual_str_size[\"str_height\"] = font_size\n        return text, rtl_dir, actual_str_size\n\n    def _flush_text(self) -> None:\n        \"\"\"Flush accumulated text to output and call visitor if present.\"\"\"\n        self.output += self.text\n        if self.visitor_text is not None:\n            self.visitor_text(self.text, self.memo_cm, self.memo_tm, self.font_resource, self.font_size)\n        self.text = \"\"\n        self.memo_cm = self.cm_matrix.copy()\n        self.memo_tm = self.tm_matrix.copy()\n\n    # Operation handlers\n\n    def _handle_bt(self, operands: list[Any]) -> None:\n        \"\"\"Handle BT (Begin Text) operation - Table 5.4 page 405.\"\"\"\n        self.tm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n        self._flush_text()\n\n    def _handle_et(self, operands: list[Any]) -> None:\n        \"\"\"Handle ET (End Text) operation - Table 5.4 page 405.\"\"\"\n        self._flush_text()\n\n    def _handle_save_graphics_state(self, operands: list[Any]) -> None:\n        \"\"\"Handle q (Save graphics state) operation - Table 4.7 page 219.\"\"\"\n        self.cm_stack.append(\n            (\n                self.cm_matrix,\n                self.font_resource,\n                self.font,\n                self.font_size,\n                self.char_scale,\n                self.space_scale,\n                self.TL,\n            )\n        )\n\n    def _handle_restore_graphics_state(self, operands: list[Any]) -> None:\n        \"\"\"Handle Q (Restore graphics state) operation - Table 4.7 page 219.\"\"\"\n        try:\n            (\n                self.cm_matrix,\n                self.font_resource,\n                self.font,\n                self.font_size,\n                self.char_scale,\n                self.space_scale,\n                self.TL,\n            ) = self.cm_stack.pop()\n        except Exception:\n            self.cm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n\n    def _handle_cm(self, operands: list[Any]) -> None:\n        \"\"\"Handle cm (Modify current matrix) operation - Table 4.7 page 219.\"\"\"\n        self.output += self.text\n        if self.visitor_text is not None:\n            self.visitor_text(self.text, self.memo_cm, self.memo_tm, self.font_resource, self.font_size)\n        self.text = \"\"\n        try:\n            self.cm_matrix = mult([float(operand) for operand in operands[:6]], self.cm_matrix)\n        except Exception:\n            self.cm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]\n        self.memo_cm = self.cm_matrix.copy()\n        self.memo_tm = self.tm_matrix.copy()\n\n    def _handle_tz(self, operands: list[Any]) -> None:\n        \"\"\"Handle Tz (Set horizontal text scaling) operation - Table 5.2 page 398.\"\"\"\n        self.char_scale = float(operands[0]) / 100 if operands else 1.0\n\n    def _handle_tw(self, operands: list[Any]) -> None:\n        \"\"\"Handle Tw (Set word spacing) operation - Table 5.2 page 398.\"\"\"\n        self.space_scale = 1.0 + float(operands[0] if operands else 0.0)\n\n    def _handle_tl(self, operands: list[Any]) -> None:\n        \"\"\"Handle TL (Set Text Leading) operation - Table 5.2 page 398.\"\"\"\n        scale_x = math.sqrt(self.tm_matrix[0] ** 2 + self.tm_matrix[2] ** 2)\n        self.TL = float(operands[0] if operands else 0.0) * self.font_size * scale_x\n\n    def _handle_tf(self, operands: list[Any]) -> None:\n        \"\"\"Handle Tf (Set font size) operation - Table 5.2 page 398.\"\"\"\n        if self.text != \"\":\n            self.output += self.text  # .translate(cmap)\n            if self.visitor_text is not None:\n                self.visitor_text(self.text, self.memo_cm, self.memo_tm, self.font_resource, self.font_size)\n        self.text = \"\"\n        self.memo_cm = self.cm_matrix.copy()\n        self.memo_tm = self.tm_matrix.copy()\n        try:\n            self.font_resource = self.font_resources[operands[0]]\n            self.font = self.fonts[operands[0]]\n        except KeyError:  # font not found\n            self.font_resource = None\n            font_descriptor = FontDescriptor()\n            self.font = Font(\n                \"Unknown\",\n                space_width=250,\n                encoding=dict.fromkeys(range(256), \"�\"),\n                font_descriptor=font_descriptor,\n                character_map={},\n            )\n\n        self._space_width = self.font.space_width / 2  # Actually the width of _half_ a space...\n        try:\n            self.font_size = float(operands[1])\n        except Exception:\n            pass  # keep previous size\n\n    def _handle_td(self, operands: list[Any]) -> float:\n        \"\"\"Handle Td (Move text position) operation - Table 5.5 page 406.\"\"\"\n        # A special case is a translating only tm:\n        # tm = [1, 0, 0, 1, e, f]\n        # i.e. tm[4] += tx, tm[5] += ty.\n        tx, ty = float(operands[0]), float(operands[1])\n        self.tm_matrix[4] += tx * self.tm_matrix[0] + ty * self.tm_matrix[2]\n        self.tm_matrix[5] += tx * self.tm_matrix[1] + ty * self.tm_matrix[3]\n        str_widths = self.compute_str_widths(self._actual_str_size[\"str_widths\"])\n        self._actual_str_size[\"str_widths\"] = 0.0\n        return str_widths\n\n    def _handle_tm(self, operands: list[Any]) -> float:\n        \"\"\"Handle Tm (Set text matrix) operation - Table 5.5 page 406.\"\"\"\n        self.tm_matrix = [float(operand) for operand in operands[:6]]\n        str_widths = self.compute_str_widths(self._actual_str_size[\"str_widths\"])\n        self._actual_str_size[\"str_widths\"] = 0.0\n        return str_widths\n\n    def _handle_t_star(self, operands: list[Any]) -> float:\n        \"\"\"Handle T* (Move to next line) operation - Table 5.5 page 406.\"\"\"\n        self.tm_matrix[4] -= self.TL * self.tm_matrix[2]\n        self.tm_matrix[5] -= self.TL * self.tm_matrix[3]\n        str_widths = self.compute_str_widths(self._actual_str_size[\"str_widths\"])\n        self._actual_str_size[\"str_widths\"] = 0.0\n        return str_widths\n\n    def _handle_tj_operation(self, operands: list[Any]) -> float:\n        \"\"\"Handle Tj (Show text) operation - Table 5.5 page 406.\"\"\"\n        self.text, self.rtl_dir, self._actual_str_size = self._handle_tj(\n            self.text,\n            operands,\n            self.cm_matrix,\n            self.tm_matrix,\n            self.font_resource,\n            self.font,\n            self.orientations,\n            self.font_size,\n            self.rtl_dir,\n            self.visitor_text,\n            self._actual_str_size,\n        )\n        return 0.0  # str_widths will be handled in post-processing\n"
  },
  {
    "path": "pypdf/_utils.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Utility functions for PDF library.\"\"\"\n__author__ = \"Mathieu Fenniak\"\n__author_email__ = \"biziqe@mathieu.fenniak.net\"\n\nimport functools\nimport logging\nimport re\nimport sys\nimport warnings\nfrom dataclasses import dataclass\nfrom datetime import datetime, timezone\nfrom io import DEFAULT_BUFFER_SIZE\nfrom os import SEEK_CUR\nfrom re import Pattern\nfrom typing import (\n    IO,\n    Any,\n    Optional,\n    Union,\n    overload,\n)\n\nif sys.version_info[:2] >= (3, 10):\n    # Python 3.10+: https://www.python.org/dev/peps/pep-0484/\n    from typing import TypeAlias\nelse:\n    from typing_extensions import TypeAlias\n\nif sys.version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing_extensions import Self\n\nfrom .errors import (\n    STREAM_TRUNCATED_PREMATURELY,\n    DeprecationError,\n    PdfStreamError,\n)\n\nTransformationMatrixType: TypeAlias = tuple[\n    tuple[float, float, float], tuple[float, float, float], tuple[float, float, float]\n]\nCompressedTransformationMatrix: TypeAlias = tuple[\n    float, float, float, float, float, float\n]\n\nStreamType = IO[Any]\nStrByteType = Union[str, StreamType]\n\n\ndef parse_iso8824_date(text: Optional[str]) -> Optional[datetime]:\n    orgtext = text\n    if not text:\n        return None\n    if text[0].isdigit():\n        text = \"D:\" + text\n    if text.endswith((\"Z\", \"z\")):\n        text += \"0000\"\n    text = text.replace(\"z\", \"+\").replace(\"Z\", \"+\").replace(\"'\", \"\")\n    i = max(text.find(\"+\"), text.find(\"-\"))\n    if i > 0 and i != len(text) - 5:\n        text += \"00\"\n    for f in (\n        \"D:%Y\",\n        \"D:%Y%m\",\n        \"D:%Y%m%d\",\n        \"D:%Y%m%d%H\",\n        \"D:%Y%m%d%H%M\",\n        \"D:%Y%m%d%H%M%S\",\n        \"D:%Y%m%d%H%M%S%z\",\n    ):\n        try:\n            d = datetime.strptime(text, f)  # noqa: DTZ007\n        except ValueError:\n            continue\n        else:\n            if text.endswith(\"+0000\"):\n                d = d.replace(tzinfo=timezone.utc)\n            return d\n    raise ValueError(f\"Can not convert date: {orgtext}\")\n\n\ndef format_iso8824_date(dt: datetime) -> str:\n    \"\"\"\n    Convert a datetime object to PDF date string format.\n\n    Converts datetime to the PDF date format D:YYYYMMDDHHmmSSOHH'mm\n    as specified in the PDF Reference.\n\n    Args:\n        dt: A datetime object to convert.\n\n    Returns:\n        A date string in PDF format.\n    \"\"\"\n    date_str = dt.strftime(\"D:%Y%m%d%H%M%S\")\n    if dt.tzinfo is not None:\n        offset = dt.utcoffset()\n        assert offset is not None\n        total_seconds = int(offset.total_seconds())\n        hours, remainder = divmod(abs(total_seconds), 3600)\n        minutes = remainder // 60\n        sign = \"+\" if total_seconds >= 0 else \"-\"\n        date_str += f\"{sign}{hours:02d}'{minutes:02d}'\"\n    return date_str\n\n\ndef _get_max_pdf_version_header(header1: str, header2: str) -> str:\n    versions = (\n        \"%PDF-1.3\",\n        \"%PDF-1.4\",\n        \"%PDF-1.5\",\n        \"%PDF-1.6\",\n        \"%PDF-1.7\",\n        \"%PDF-2.0\",\n    )\n    pdf_header_indices = []\n    if header1 in versions:\n        pdf_header_indices.append(versions.index(header1))\n    if header2 in versions:\n        pdf_header_indices.append(versions.index(header2))\n    if len(pdf_header_indices) == 0:\n        raise ValueError(f\"Neither {header1!r} nor {header2!r} are proper headers\")\n    return versions[max(pdf_header_indices)]\n\n\nWHITESPACES = (b\"\\x00\", b\"\\t\", b\"\\n\", b\"\\f\", b\"\\r\", b\" \")\nWHITESPACES_AS_BYTES = b\"\".join(WHITESPACES)\nWHITESPACES_AS_REGEXP = b\"[\" + WHITESPACES_AS_BYTES + b\"]\"\n\n\ndef read_until_whitespace(stream: StreamType, maxchars: Optional[int] = None) -> bytes:\n    \"\"\"\n    Read non-whitespace characters and return them.\n\n    Stops upon encountering whitespace or when maxchars is reached.\n\n    Args:\n        stream: The data stream from which was read.\n        maxchars: The maximum number of bytes returned; by default unlimited.\n\n    Returns:\n        The data which was read.\n\n    \"\"\"\n    txt = b\"\"\n    while True:\n        tok = stream.read(1)\n        if tok.isspace() or not tok:\n            break\n        txt += tok\n        if len(txt) == maxchars:\n            break\n    return txt\n\n\ndef read_non_whitespace(stream: StreamType) -> bytes:\n    \"\"\"\n    Find and read the next non-whitespace character (ignores whitespace).\n\n    Args:\n        stream: The data stream from which was read.\n\n    Returns:\n        The data which was read.\n\n    \"\"\"\n    tok = stream.read(1)\n    while tok in WHITESPACES:\n        tok = stream.read(1)\n    return tok\n\n\ndef skip_over_whitespace(stream: StreamType) -> bool:\n    \"\"\"\n    Similar to read_non_whitespace, but return a boolean if at least one\n    whitespace character was read.\n\n    Args:\n        stream: The data stream from which was read.\n\n    Returns:\n        True if one or more whitespace was skipped, otherwise return False.\n\n    \"\"\"\n    tok = stream.read(1)\n    cnt = 0\n    while tok in WHITESPACES:\n        cnt += 1\n        tok = stream.read(1)\n    return cnt > 0\n\n\ndef check_if_whitespace_only(value: bytes) -> bool:\n    \"\"\"\n    Check if the given value consists of whitespace characters only.\n\n    Args:\n        value: The bytes to check.\n\n    Returns:\n        True if the value only has whitespace characters, otherwise return False.\n\n    \"\"\"\n    return all(b in WHITESPACES_AS_BYTES for b in value)\n\n\ndef skip_over_comment(stream: StreamType) -> None:\n    tok = stream.read(1)\n    stream.seek(-1, 1)\n    if tok == b\"%\":\n        while tok not in (b\"\\n\", b\"\\r\"):\n            tok = stream.read(1)\n            if tok == b\"\":\n                raise PdfStreamError(\"File ended unexpectedly.\")\n\n\ndef read_until_regex(stream: StreamType, regex: Pattern[bytes]) -> bytes:\n    \"\"\"\n    Read until the regular expression pattern matched (ignore the match).\n    Treats EOF on the underlying stream as the end of the token to be matched.\n\n    Args:\n        regex: re.Pattern\n\n    Returns:\n        The read bytes.\n\n    \"\"\"\n    parts: list[bytes] = []\n    total_len = 0\n    tail = b\"\"\n    chunk_size = 16\n    while True:\n        tok = stream.read(chunk_size)\n        if not tok:\n            return b\"\".join(parts)\n        # Search overlap of previous tail + new chunk to catch\n        # multi-byte regex matches spanning chunk boundaries.\n        buf = tail + tok\n        m = regex.search(buf)\n        if m is not None:\n            overlap = len(tail)\n            actual_start = total_len - overlap + m.start()\n            stream.seek(actual_start - total_len - len(tok), 1)\n            parts.append(tok)\n            return b\"\".join(parts)[:actual_start]\n        parts.append(tok)\n        total_len += len(tok)\n        # Fixed overlap: 16 bytes is sufficient for the short\n        # delimiter patterns used in PDF parsing.\n        tail = tok[-16:]\n        if chunk_size < 8192:\n            chunk_size <<= 1\n    return b\"\".join(parts)\n\n\ndef read_block_backwards(stream: StreamType, to_read: int) -> bytes:\n    \"\"\"\n    Given a stream at position X, read a block of size to_read ending at position X.\n\n    This changes the stream's position to the beginning of where the block was\n    read.\n\n    Args:\n        stream:\n        to_read:\n\n    Returns:\n        The data which was read.\n\n    \"\"\"\n    if stream.tell() < to_read:\n        raise PdfStreamError(\"Could not read malformed PDF file\")\n    # Seek to the start of the block we want to read.\n    stream.seek(-to_read, SEEK_CUR)\n    read = stream.read(to_read)\n    # Seek to the start of the block we read after reading it.\n    stream.seek(-to_read, SEEK_CUR)\n    return read\n\n\ndef read_previous_line(stream: StreamType) -> bytes:\n    \"\"\"\n    Given a byte stream with current position X, return the previous line.\n\n    All characters between the first CR/LF byte found before X\n    (or, the start of the file, if no such byte is found) and position X\n    After this call, the stream will be positioned one byte after the\n    first non-CRLF character found beyond the first CR/LF byte before X,\n    or, if no such byte is found, at the beginning of the stream.\n\n    Args:\n        stream: StreamType:\n\n    Returns:\n        The data which was read.\n\n    \"\"\"\n    line_content = []\n    found_crlf = False\n    if stream.tell() == 0:\n        raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)\n    while True:\n        to_read = min(DEFAULT_BUFFER_SIZE, stream.tell())\n        if to_read == 0:\n            break\n        # Read the block. After this, our stream will be one\n        # beyond the initial position.\n        block = read_block_backwards(stream, to_read)\n        idx = len(block) - 1\n        if not found_crlf:\n            # We haven't found our first CR/LF yet.\n            # Read off characters until we hit one.\n            while idx >= 0 and block[idx] not in b\"\\r\\n\":\n                idx -= 1\n            if idx >= 0:\n                found_crlf = True\n        if found_crlf:\n            # We found our first CR/LF already (on this block or\n            # a previous one).\n            # Our combined line is the remainder of the block\n            # plus any previously read blocks.\n            line_content.append(block[idx + 1 :])\n            # Continue to read off any more CRLF characters.\n            while idx >= 0 and block[idx] in b\"\\r\\n\":\n                idx -= 1\n        else:\n            # Didn't find CR/LF yet - add this block to our\n            # previously read blocks and continue.\n            line_content.append(block)\n        if idx >= 0:\n            # We found the next non-CRLF character.\n            # Set the stream position correctly, then break\n            stream.seek(idx + 1, SEEK_CUR)\n            break\n    # Join all the blocks in the line (which are in reverse order)\n    return b\"\".join(line_content[::-1])\n\n\ndef matrix_multiply(\n    a: TransformationMatrixType, b: TransformationMatrixType\n) -> TransformationMatrixType:\n    return tuple(  # type: ignore[return-value]\n        tuple(sum(float(i) * float(j) for i, j in zip(row, col)) for col in zip(*b))\n        for row in a\n    )\n\n\ndef mark_location(stream: StreamType) -> None:\n    \"\"\"Create text file showing current location in context.\"\"\"\n    # Mainly for debugging\n    radius = 5000\n    stream.seek(-radius, 1)\n    with open(\"pypdf_pdfLocation.txt\", \"wb\") as output_fh:\n        output_fh.write(stream.read(radius))\n        output_fh.write(b\"HERE\")\n        output_fh.write(stream.read(radius))\n    stream.seek(-radius, 1)\n\n\n@overload\ndef ord_(b: str) -> int:\n    ...\n\n\n@overload\ndef ord_(b: bytes) -> bytes:\n    ...\n\n\n@overload\ndef ord_(b: int) -> int:\n    ...\n\n\ndef ord_(b: Union[int, str, bytes]) -> Union[int, bytes]:\n    if isinstance(b, str):\n        return ord(b)\n    return b\n\n\ndef deprecate(msg: str, stacklevel: int = 3) -> None:\n    warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel)\n\n\ndef deprecation(msg: str) -> None:\n    raise DeprecationError(msg)\n\n\ndef deprecate_with_replacement(old_name: str, new_name: str, removed_in: str) -> None:\n    \"\"\"Issue a warning that a feature will be removed, but has a replacement.\"\"\"\n    deprecate(\n        f\"{old_name} is deprecated and will be removed in pypdf {removed_in}. Use {new_name} instead.\",\n        4,\n    )\n\n\ndef deprecation_with_replacement(old_name: str, new_name: str, removed_in: str) -> None:\n    \"\"\"Raise an exception that a feature was already removed, but has a replacement.\"\"\"\n    deprecation(\n        f\"{old_name} is deprecated and was removed in pypdf {removed_in}. Use {new_name} instead.\"\n    )\n\n\ndef deprecate_no_replacement(name: str, removed_in: str) -> None:\n    \"\"\"Issue a warning that a feature will be removed without replacement.\"\"\"\n    deprecate(f\"{name} is deprecated and will be removed in pypdf {removed_in}.\", 4)\n\n\ndef deprecation_no_replacement(name: str, removed_in: str) -> None:\n    \"\"\"Raise an exception that a feature was already removed without replacement.\"\"\"\n    deprecation(f\"{name} is deprecated and was removed in pypdf {removed_in}.\")\n\n\ndef logger_error(message: str, *, source: str, **values: Any) -> None:\n    \"\"\"\n    Use this instead of logger.error directly.\n\n    That allows people to overwrite it more easily.\n\n    See the docs on when to use which:\n    https://pypdf.readthedocs.io/en/latest/user/suppress-warnings.html\n    \"\"\"\n    logging.getLogger(source).error(message, values)\n\n\ndef logger_warning(msg: str, src: str) -> None:\n    \"\"\"\n    Use this instead of logger.warning directly.\n\n    That allows people to overwrite it more easily.\n\n    ## Exception, warnings.warn, logger_warning\n    - Exceptions should be used if the user should write code that deals with\n      an error case, e.g. the PDF being completely broken.\n    - warnings.warn should be used if the user needs to fix their code, e.g.\n      DeprecationWarnings\n    - logger_warning should be used if the user needs to know that an issue was\n      handled by pypdf, e.g. a non-compliant PDF being read in a way that\n      pypdf could apply a robustness fix to still read it. This applies mainly\n      to strict=False mode.\n    \"\"\"\n    logging.getLogger(src).warning(msg)\n\n\ndef rename_kwargs(\n    func_name: str, kwargs: dict[str, Any], aliases: dict[str, str], fail: bool = False\n) -> None:\n    \"\"\"\n    Helper function to deprecate arguments.\n\n    Args:\n        func_name: Name of the function to be deprecated\n        kwargs:\n        aliases:\n        fail:\n\n    \"\"\"\n    for old_term, new_term in aliases.items():\n        if old_term in kwargs:\n            if fail:\n                raise DeprecationError(\n                    f\"{old_term} is deprecated as an argument. Use {new_term} instead\"\n                )\n            if new_term in kwargs:\n                raise TypeError(\n                    f\"{func_name} received both {old_term} and {new_term} as \"\n                    f\"an argument. {old_term} is deprecated. \"\n                    f\"Use {new_term} instead.\"\n                )\n            kwargs[new_term] = kwargs.pop(old_term)\n            warnings.warn(\n                message=(\n                    f\"{old_term} is deprecated as an argument. Use {new_term} instead\"\n                ),\n                category=DeprecationWarning,\n                stacklevel=3,\n            )\n\n\ndef _human_readable_bytes(bytes: int) -> str:\n    if bytes < 10**3:\n        return f\"{bytes} Byte\"\n    if bytes < 10**6:\n        return f\"{bytes / 10**3:.1f} kB\"\n    if bytes < 10**9:\n        return f\"{bytes / 10**6:.1f} MB\"\n    return f\"{bytes / 10**9:.1f} GB\"\n\n\n# The following class has been copied from Django:\n# https://github.com/django/django/blob/adae619426b6f50046b3daaa744db52989c9d6db/django/utils/functional.py#L51-L65\n# It received some modifications to comply with our own coding standards.\n#\n# Original license:\n#\n# ---------------------------------------------------------------------------------\n# Copyright (c) Django Software Foundation and individual contributors.\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without modification,\n# are permitted provided that the following conditions are met:\n#\n#     1. Redistributions of source code must retain the above copyright notice,\n#        this list of conditions and the following disclaimer.\n#\n#     2. Redistributions in binary form must reproduce the above copyright\n#        notice, this list of conditions and the following disclaimer in the\n#        documentation and/or other materials provided with the distribution.\n#\n#     3. Neither the name of Django nor the names of its contributors may be used\n#        to endorse or promote products derived from this software without\n#        specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\" AND\n# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED\n# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE\n# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR\n# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES\n# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;\n# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON\n# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT\n# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS\n# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.\n# ---------------------------------------------------------------------------------\nclass classproperty:  # noqa: N801\n    \"\"\"\n    Decorator that converts a method with a single cls argument into a property\n    that can be accessed directly from the class.\n    \"\"\"\n\n    def __init__(self, method=None) -> None:  # type: ignore  # noqa: ANN001\n        self.fget = method\n\n    def __get__(self, instance, cls=None) -> Any:  # type: ignore  # noqa: ANN001\n        return self.fget(cls)\n\n    def getter(self, method) -> Self:  # type: ignore  # noqa: ANN001\n        self.fget = method\n        return self\n\n\n@dataclass\nclass File:\n    from .generic import IndirectObject  # noqa: PLC0415\n\n    name: str = \"\"\n    \"\"\"\n    Filename as identified within the PDF file.\n    \"\"\"\n    data: bytes = b\"\"\n    \"\"\"\n    Data as bytes.\n    \"\"\"\n    indirect_reference: Optional[IndirectObject] = None\n    \"\"\"\n    Reference to the object storing the stream.\n    \"\"\"\n\n    def __str__(self) -> str:\n        return f\"{self.__class__.__name__}(name={self.name}, data: {_human_readable_bytes(len(self.data))})\"\n\n    def __repr__(self) -> str:\n        return self.__str__()[:-1] + f\", hash: {hash(self.data)})\"\n\n\n@functools.total_ordering\nclass Version:\n    COMPONENT_PATTERN = re.compile(r\"^(\\d+)(.*)$\")\n\n    def __init__(self, version_str: str) -> None:\n        self.version_str = version_str\n        self.components = self._parse_version(version_str)\n\n    def _parse_version(self, version_str: str) -> list[tuple[int, str]]:\n        components = version_str.split(\".\")\n        parsed_components = []\n        for component in components:\n            match = Version.COMPONENT_PATTERN.match(component)\n            if not match:\n                parsed_components.append((0, component))\n                continue\n            integer_prefix = match.group(1)\n            suffix = match.group(2)\n            if integer_prefix is None:\n                integer_prefix = 0\n            parsed_components.append((int(integer_prefix), suffix))\n        return parsed_components\n\n    def __eq__(self, other: object) -> bool:\n        if not isinstance(other, Version):\n            return False\n        return self.components == other.components\n\n    def __hash__(self) -> int:\n        # Convert to tuple as lists cannot be hashed.\n        return hash((self.__class__, tuple(self.components)))\n\n    def __lt__(self, other: Any) -> bool:\n        if not isinstance(other, Version):\n            raise ValueError(f\"Version cannot be compared against {type(other)}\")\n\n        for self_component, other_component in zip(self.components, other.components):\n            self_value, self_suffix = self_component\n            other_value, other_suffix = other_component\n\n            if self_value < other_value:\n                return True\n            if self_value > other_value:\n                return False\n\n            if self_suffix < other_suffix:\n                return True\n            if self_suffix > other_suffix:\n                return False\n\n        return len(self.components) < len(other.components)\n"
  },
  {
    "path": "pypdf/_version.py",
    "content": "__version__ = \"6.9.1\"\n"
  },
  {
    "path": "pypdf/_writer.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# Copyright (c) 2007, Ashish Kulkarni <kulkarni.ashish@gmail.com>\n#\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nimport decimal\nimport enum\nimport hashlib\nimport re\nimport struct\nimport sys\nimport uuid\nfrom collections.abc import Iterable, Mapping\nfrom io import BytesIO, FileIO, IOBase\nfrom itertools import compress\nfrom pathlib import Path\nfrom re import Pattern\nfrom types import TracebackType\nfrom typing import (\n    IO,\n    Any,\n    Callable,\n    Optional,\n    Union,\n    cast,\n)\n\nif sys.version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing_extensions import Self\n\nfrom ._doc_common import DocumentInformation, PdfDocCommon\nfrom ._encryption import EncryptAlgorithm, Encryption\nfrom ._page import PageObject, Transformation\nfrom ._page_labels import nums_clear_range, nums_insert, nums_next\nfrom ._reader import PdfReader\nfrom ._utils import (\n    StrByteType,\n    StreamType,\n    _get_max_pdf_version_header,\n    deprecation_no_replacement,\n    logger_warning,\n)\nfrom .constants import AnnotationDictionaryAttributes as AA\nfrom .constants import CatalogAttributes as CA\nfrom .constants import (\n    CatalogDictionary,\n    GoToActionArguments,\n    ImageType,\n    InteractiveFormDictEntries,\n    OutlineFontFlag,\n    PageLabelStyle,\n    PagesAttributes,\n    TypFitArguments,\n    UserAccessPermissions,\n)\nfrom .constants import Core as CO\nfrom .constants import FieldDictionaryAttributes as FA\nfrom .constants import PageAttributes as PG\nfrom .constants import TrailerKeys as TK\nfrom .errors import PdfReadError, PyPdfError\nfrom .generic import (\n    PAGE_FIT,\n    ArrayObject,\n    BooleanObject,\n    ByteStringObject,\n    ContentStream,\n    Destination,\n    DictionaryObject,\n    EmbeddedFile,\n    Fit,\n    FloatObject,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    PdfObject,\n    RectangleObject,\n    ReferenceLink,\n    StreamObject,\n    TextStringObject,\n    TreeObject,\n    ViewerPreferences,\n    create_string_object,\n    extract_links,\n    hex_to_rgb,\n    is_null_or_none,\n)\nfrom .generic._appearance_stream import TextStreamAppearance\nfrom .pagerange import PageRange, PageRangeSpec\nfrom .types import (\n    AnnotationSubtype,\n    BorderArrayType,\n    LayoutType,\n    OutlineItemType,\n    OutlineType,\n    PagemodeType,\n)\nfrom .xmp import XmpInformation\n\nALL_DOCUMENT_PERMISSIONS = UserAccessPermissions.all()\n\n\nclass ObjectDeletionFlag(enum.IntFlag):\n    NONE = 0\n    TEXT = enum.auto()\n    LINKS = enum.auto()\n    ATTACHMENTS = enum.auto()\n    OBJECTS_3D = enum.auto()\n    ALL_ANNOTATIONS = enum.auto()\n    XOBJECT_IMAGES = enum.auto()\n    INLINE_IMAGES = enum.auto()\n    DRAWING_IMAGES = enum.auto()\n    IMAGES = XOBJECT_IMAGES | INLINE_IMAGES | DRAWING_IMAGES\n\n\ndef _rolling_checksum(stream: BytesIO, blocksize: int = 65536) -> str:\n    hash = hashlib.md5(usedforsecurity=False)\n    for block in iter(lambda: stream.read(blocksize), b\"\"):\n        hash.update(block)\n    return hash.hexdigest()\n\n\nclass PdfWriter(PdfDocCommon):\n    \"\"\"\n    Write a PDF file out, given pages produced by another class or through\n    cloning a PDF file during initialization.\n\n    Typically data is added from a :class:`PdfReader<pypdf.PdfReader>`.\n\n    Args:\n        clone_from: identical to fileobj (for compatibility)\n\n        incremental: If true, loads the document and set the PdfWriter in incremental mode.\n\n            When writing incrementally, the original document is written first and new/modified\n            content is appended. To be used for signed document/forms to keep signature valid.\n\n        full: If true, loads all the objects (always full if incremental = True).\n            This parameter may allow loading large PDFs.\n\n        strict: If true, pypdf will raise an exception if a PDF does not follow the specification.\n            If false, pypdf will try to be forgiving and do something reasonable, but it will log\n            a warning message. It is a best-effort approach.\n\n    \"\"\"\n\n    def __init__(\n        self,\n        fileobj: Union[None, PdfReader, StrByteType, Path] = \"\",\n        clone_from: Union[None, PdfReader, StrByteType, Path] = None,\n        incremental: bool = False,\n        full: bool = False,\n        strict: bool = False,\n    ) -> None:\n        self.strict = strict\n        \"\"\"\n        If true, pypdf will raise an exception if a PDF does not follow the specification.\n        If false, pypdf will try to be forgiving and do something reasonable, but it will log\n        a warning message. It is a best-effort approach.\n        \"\"\"\n\n        self.incremental = incremental or full\n        \"\"\"\n        Returns if the PdfWriter object has been started in incremental mode.\n        \"\"\"\n\n        self._objects: list[Optional[PdfObject]] = []\n        \"\"\"\n        The indirect objects in the PDF.\n        For the incremental case, it will be filled with None\n        in clone_reader_document_root.\n        \"\"\"\n\n        self._original_hash: list[int] = []\n        \"\"\"\n        List of hashes after import; used to identify changes.\n        \"\"\"\n\n        self._idnum_hash: dict[bytes, tuple[IndirectObject, list[IndirectObject]]] = {}\n        \"\"\"\n        Maps hash values of indirect objects to the list of IndirectObjects.\n        This is used for compression.\n        \"\"\"\n\n        self._id_translated: dict[int, dict[int, int]] = {}\n        \"\"\"List of already translated IDs.\n           dict[id(pdf)][(idnum, generation)]\n        \"\"\"\n\n        self._info_obj: Optional[PdfObject]\n        \"\"\"The PDF files's document information dictionary,\n        defined by Info in the PDF file's trailer dictionary.\"\"\"\n\n        self._ID: Union[ArrayObject, None] = None\n        \"\"\"The PDF file identifier,\n        defined by the ID in the PDF file's trailer dictionary.\"\"\"\n\n        self._unresolved_links: list[tuple[ReferenceLink, ReferenceLink]] = []\n        \"Tracks links in pages added to the writer for resolving later.\"\n        self._merged_in_pages: dict[Optional[IndirectObject], Optional[IndirectObject]] = {}\n        \"Tracks pages added to the writer and what page they turned into.\"\n\n        if self.incremental:\n            if isinstance(fileobj, (str, Path)):\n                with open(fileobj, \"rb\") as f:\n                    fileobj = BytesIO(f.read(-1))\n            if isinstance(fileobj, BytesIO):\n                fileobj = PdfReader(fileobj)\n            if not isinstance(fileobj, PdfReader):\n                raise PyPdfError(\"Invalid type for incremental mode\")\n            self._reader = fileobj  # prev content is in _reader.stream\n            self._header = fileobj.pdf_header.encode()\n            self._readonly = True  # TODO: to be analysed\n        else:\n            self._header = b\"%PDF-1.3\"\n            self._info_obj = self._add_object(\n                DictionaryObject(\n                    {NameObject(\"/Producer\"): create_string_object(\"pypdf\")}\n                )\n            )\n\n        def _get_clone_from(\n            fileobj: Union[None, PdfReader, str, Path, IO[Any], BytesIO],\n            clone_from: Union[None, PdfReader, str, Path, IO[Any], BytesIO],\n        ) -> Union[None, PdfReader, str, Path, IO[Any], BytesIO]:\n            if isinstance(fileobj, (str, Path, IO, BytesIO)) and (\n                fileobj == \"\" or clone_from is not None\n            ):\n                return clone_from\n            cloning = True\n            if isinstance(fileobj, (str, Path)) and (\n                not Path(str(fileobj)).exists()\n                or Path(str(fileobj)).stat().st_size == 0\n            ):\n                cloning = False\n            if isinstance(fileobj, (IOBase, BytesIO)):\n                t = fileobj.tell()\n                if fileobj.seek(0, 2) == 0:\n                    cloning = False\n                fileobj.seek(t, 0)\n            if cloning:\n                clone_from = fileobj\n            return clone_from\n\n        clone_from = _get_clone_from(fileobj, clone_from)\n        # To prevent overwriting\n        self.temp_fileobj = fileobj\n        self.fileobj = \"\"\n        self._with_as_usage = False\n        self._cloned = False\n        # The root of our page tree node\n        pages = DictionaryObject(\n            {\n                NameObject(PagesAttributes.TYPE): NameObject(\"/Pages\"),\n                NameObject(PagesAttributes.COUNT): NumberObject(0),\n                NameObject(PagesAttributes.KIDS): ArrayObject(),\n            }\n        )\n        self.flattened_pages = []\n        self._encryption: Optional[Encryption] = None\n        self._encrypt_entry: Optional[DictionaryObject] = None\n\n        if clone_from is not None:\n            if not isinstance(clone_from, PdfReader):\n                clone_from = PdfReader(clone_from)\n            self.clone_document_from_reader(clone_from)\n            self._cloned = True\n        else:\n            self._pages = self._add_object(pages)\n            self._root_object = DictionaryObject(\n                {\n                    NameObject(PagesAttributes.TYPE): NameObject(CO.CATALOG),\n                    NameObject(CO.PAGES): self._pages,\n                }\n            )\n            self._add_object(self._root_object)\n        if full and not incremental:\n            self.incremental = False\n        if isinstance(self._ID, list):\n            if isinstance(self._ID[0], TextStringObject):\n                self._ID[0] = ByteStringObject(self._ID[0].get_original_bytes())\n            if isinstance(self._ID[1], TextStringObject):\n                self._ID[1] = ByteStringObject(self._ID[1].get_original_bytes())\n\n    # for commonality\n    @property\n    def is_encrypted(self) -> bool:\n        \"\"\"\n        Read-only boolean property showing whether this PDF file is encrypted.\n\n        Note that this property, if true, will remain true even after the\n        :meth:`decrypt()<pypdf.PdfReader.decrypt>` method is called.\n        \"\"\"\n        return False\n\n    @property\n    def root_object(self) -> DictionaryObject:\n        \"\"\"\n        Provide direct access to PDF Structure.\n\n        Note:\n            Recommended only for read access.\n\n        \"\"\"\n        return self._root_object\n\n    @property\n    def _info(self) -> Optional[DictionaryObject]:\n        \"\"\"\n        Provide access to \"/Info\". Standardized with PdfReader.\n\n        Returns:\n            /Info Dictionary; None if the entry does not exist\n\n        \"\"\"\n        return (\n            None\n            if self._info_obj is None\n            else cast(DictionaryObject, self._info_obj.get_object())\n        )\n\n    @_info.setter\n    def _info(self, value: Optional[Union[IndirectObject, DictionaryObject]]) -> None:\n        if value is None:\n            try:\n                self._objects[self._info_obj.indirect_reference.idnum - 1] = None  # type: ignore\n            except (KeyError, AttributeError):\n                pass\n            self._info_obj = None\n        else:\n            if self._info_obj is None:\n                self._info_obj = self._add_object(DictionaryObject())\n            obj = cast(DictionaryObject, self._info_obj.get_object())\n            obj.clear()\n            obj.update(cast(DictionaryObject, value.get_object()))\n\n    @property\n    def xmp_metadata(self) -> Optional[XmpInformation]:\n        \"\"\"XMP (Extensible Metadata Platform) data.\"\"\"\n        return cast(XmpInformation, self.root_object.xmp_metadata)\n\n    @xmp_metadata.setter\n    def xmp_metadata(self, value: Union[XmpInformation, bytes, None]) -> None:\n        \"\"\"XMP (Extensible Metadata Platform) data.\"\"\"\n        if value is None:\n            if \"/Metadata\" in self.root_object:\n                del self.root_object[\"/Metadata\"]\n            return\n\n        metadata = self.root_object.get(\"/Metadata\", None)\n        if not isinstance(metadata, IndirectObject):\n            if metadata is not None:\n                del self.root_object[\"/Metadata\"]\n            metadata_stream = StreamObject()\n            stream_reference = self._add_object(metadata_stream)\n            self.root_object[NameObject(\"/Metadata\")] = stream_reference\n        else:\n            metadata_stream = cast(StreamObject, metadata.get_object())\n\n        if isinstance(value, XmpInformation):\n            bytes_data = value.stream.get_data()\n        else:\n            bytes_data = value\n        metadata_stream.set_data(bytes_data)\n\n    @property\n    def with_as_usage(self) -> bool:\n        deprecation_no_replacement(\"with_as_usage\", \"5.0\")\n        return self._with_as_usage\n\n    @with_as_usage.setter\n    def with_as_usage(self, value: bool) -> None:\n        deprecation_no_replacement(\"with_as_usage\", \"5.0\")\n        self._with_as_usage = value\n\n    def __enter__(self) -> Self:\n        \"\"\"Store how writer is initialized by 'with'.\"\"\"\n        c: bool = self._cloned\n        t = self.temp_fileobj\n        self.__init__()  # type: ignore\n        self._cloned = c\n        self._with_as_usage = True\n        self.fileobj = t  # type: ignore\n        return self\n\n    def __exit__(\n        self,\n        exc_type: Optional[type[BaseException]],\n        exc: Optional[BaseException],\n        traceback: Optional[TracebackType],\n    ) -> None:\n        \"\"\"Write data to the fileobj.\"\"\"\n        if self.fileobj and not self._cloned:\n            self.write(self.fileobj)\n\n    @property\n    def pdf_header(self) -> str:\n        \"\"\"\n        Read/Write property of the PDF header that is written.\n\n        This should be something like ``'%PDF-1.5'``. It is recommended to set\n        the lowest version that supports all features which are used within the\n        PDF file.\n\n        Note: `pdf_header` returns a string but accepts bytes or str for writing\n        \"\"\"\n        return self._header.decode()\n\n    @pdf_header.setter\n    def pdf_header(self, new_header: Union[str, bytes]) -> None:\n        if isinstance(new_header, str):\n            new_header = new_header.encode()\n        self._header = new_header\n\n    def _add_object(self, obj: PdfObject) -> IndirectObject:\n        if (\n            getattr(obj, \"indirect_reference\", None) is not None\n            and obj.indirect_reference.pdf == self  # type: ignore\n        ):\n            return obj.indirect_reference  # type: ignore\n        # check for /Contents in Pages (/Contents in annotations are strings)\n        if isinstance(obj, DictionaryObject) and isinstance(\n            obj.get(PG.CONTENTS, None), (ArrayObject, DictionaryObject)\n        ):\n            obj[NameObject(PG.CONTENTS)] = self._add_object(obj[PG.CONTENTS])\n        self._objects.append(obj)\n        obj.indirect_reference = IndirectObject(len(self._objects), 0, self)\n        return obj.indirect_reference\n\n    def get_object(\n        self,\n        indirect_reference: Union[int, IndirectObject],\n    ) -> PdfObject:\n        if isinstance(indirect_reference, int):\n            obj = self._objects[indirect_reference - 1]\n        elif indirect_reference.pdf != self:\n            raise ValueError(\"PDF must be self\")\n        else:\n            obj = self._objects[indirect_reference.idnum - 1]\n        assert obj is not None, \"mypy\"\n        return obj\n\n    def _replace_object(\n        self,\n        indirect_reference: Union[int, IndirectObject],\n        obj: PdfObject,\n    ) -> PdfObject:\n        if isinstance(indirect_reference, IndirectObject):\n            if indirect_reference.pdf != self:\n                raise ValueError(\"PDF must be self\")\n            indirect_reference = indirect_reference.idnum\n        gen = self._objects[indirect_reference - 1].indirect_reference.generation  # type: ignore\n        if (\n            getattr(obj, \"indirect_reference\", None) is not None\n            and obj.indirect_reference.pdf != self  # type: ignore\n        ):\n            obj = obj.clone(self)\n        self._objects[indirect_reference - 1] = obj\n        obj.indirect_reference = IndirectObject(indirect_reference, gen, self)\n\n        assert isinstance(obj, PdfObject), \"mypy\"\n        return obj\n\n    def _add_page(\n        self,\n        page: PageObject,\n        index: int,\n        excluded_keys: Iterable[str] = (),\n    ) -> PageObject:\n        if not isinstance(page, PageObject) or page.get(PagesAttributes.TYPE, None) != CO.PAGE:\n            raise ValueError(\"Invalid page object\")\n        assert self.flattened_pages is not None, \"for mypy\"\n        page_org = page\n        excluded_keys = list(excluded_keys)\n        excluded_keys += [PagesAttributes.PARENT, \"/StructParents\"]\n        # Acrobat does not accept two indirect references pointing on the same\n        # page; therefore in order to add multiple copies of the same\n        # page, we need to create a new dictionary for the page, however the\n        # objects below (including content) are not duplicated:\n        try:  # delete an already existing page\n            del self._id_translated[id(page_org.indirect_reference.pdf)][  # type: ignore\n                page_org.indirect_reference.idnum  # type: ignore\n            ]\n        except Exception:\n            pass\n\n        page = cast(\n            \"PageObject\", page_org.clone(self, False, excluded_keys).get_object()\n        )\n        if page_org.pdf is not None:\n            other = page_org.pdf.pdf_header\n            self.pdf_header = _get_max_pdf_version_header(self.pdf_header, other)\n\n        node, idx = self._get_page_in_node(index)\n        page[NameObject(PagesAttributes.PARENT)] = node.indirect_reference\n\n        if idx >= 0:\n            cast(ArrayObject, node[PagesAttributes.KIDS]).insert(idx, page.indirect_reference)\n            self.flattened_pages.insert(index, page)\n        else:\n            cast(ArrayObject, node[PagesAttributes.KIDS]).append(page.indirect_reference)\n            self.flattened_pages.append(page)\n        recurse = 0\n        while not is_null_or_none(node):\n            node = cast(DictionaryObject, node.get_object())\n            node[NameObject(PagesAttributes.COUNT)] = NumberObject(cast(int, node[PagesAttributes.COUNT]) + 1)\n            node = node.get(PagesAttributes.PARENT, None)  # type: ignore[assignment]  # TODO: Fix.\n            recurse += 1\n            if recurse > 1000:\n                raise PyPdfError(\"Too many recursive calls!\")\n\n        if page_org.pdf is not None:\n            # the page may contain links to other pages, and those other\n            # pages may or may not already be added.  we store the\n            # information we need, so that we can resolve the references\n            # later.\n            self._unresolved_links.extend(extract_links(page, page_org))\n            self._merged_in_pages[page_org.indirect_reference] = page.indirect_reference\n\n        return page\n\n    def set_need_appearances_writer(self, state: bool = True) -> None:\n        \"\"\"\n        Sets the \"NeedAppearances\" flag in the PDF writer.\n\n        The \"NeedAppearances\" flag indicates whether the appearance dictionary\n        for form fields should be automatically generated by the PDF viewer or\n        if the embedded appearance should be used.\n\n        Args:\n            state: The actual value of the NeedAppearances flag.\n\n        Returns:\n            None\n\n        \"\"\"\n        # See §12.7.2 and §7.7.2 for more information:\n        # https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf\n        try:\n            # get the AcroForm tree\n            if CatalogDictionary.ACRO_FORM not in self._root_object:\n                self._root_object[\n                    NameObject(CatalogDictionary.ACRO_FORM)\n                ] = self._add_object(DictionaryObject())\n\n            need_appearances = NameObject(InteractiveFormDictEntries.NeedAppearances)\n            cast(DictionaryObject, self._root_object[CatalogDictionary.ACRO_FORM])[\n                need_appearances\n            ] = BooleanObject(state)\n        except Exception as exc:  # pragma: no cover\n            logger_warning(\n                f\"set_need_appearances_writer({state}) catch : {exc}\", __name__\n            )\n\n    def create_viewer_preferences(self) -> ViewerPreferences:\n        o = ViewerPreferences()\n        self._root_object[\n            NameObject(CatalogDictionary.VIEWER_PREFERENCES)\n        ] = self._add_object(o)\n        return o\n\n    def add_page(\n        self,\n        page: PageObject,\n        excluded_keys: Iterable[str] = (),\n    ) -> PageObject:\n        \"\"\"\n        Add a page to this PDF file.\n\n        Recommended for advanced usage including the adequate excluded_keys.\n\n        The page is usually acquired from a :class:`PdfReader<pypdf.PdfReader>`\n        instance.\n\n        Args:\n            page: The page to add to the document. Should be\n                an instance of :class:`PageObject<pypdf._page.PageObject>`\n            excluded_keys:\n\n        Returns:\n            The added PageObject.\n\n        \"\"\"\n        assert self.flattened_pages is not None, \"mypy\"\n        return self._add_page(page, len(self.flattened_pages), excluded_keys)\n\n    def insert_page(\n        self,\n        page: PageObject,\n        index: int = 0,\n        excluded_keys: Iterable[str] = (),\n    ) -> PageObject:\n        \"\"\"\n        Insert a page in this PDF file. The page is usually acquired from a\n        :class:`PdfReader<pypdf.PdfReader>` instance.\n\n        Args:\n            page: The page to add to the document.\n            index: Position at which the page will be inserted.\n            excluded_keys:\n\n        Returns:\n            The added PageObject.\n\n        \"\"\"\n        assert self.flattened_pages is not None, \"mypy\"\n        if index < 0:\n            index += len(self.flattened_pages)\n        if index < 0:\n            raise ValueError(\"Invalid index value\")\n        if index >= len(self.flattened_pages):\n            return self.add_page(page, excluded_keys)\n        return self._add_page(page, index, excluded_keys)\n\n    def _get_page_number_by_indirect(\n        self, indirect_reference: Union[None, int, NullObject, IndirectObject]\n    ) -> Optional[int]:\n        \"\"\"\n        Generate _page_id2num.\n\n        Args:\n            indirect_reference:\n\n        Returns:\n            The page number or None\n\n        \"\"\"\n        # To provide same function as in PdfReader\n        if is_null_or_none(indirect_reference):\n            return None\n        assert indirect_reference is not None, \"mypy\"\n        if isinstance(indirect_reference, int):\n            indirect_reference = IndirectObject(indirect_reference, 0, self)\n        obj = indirect_reference.get_object()\n        if isinstance(obj, PageObject):\n            return obj.page_number\n        return None\n\n    def add_blank_page(\n        self, width: Optional[float] = None, height: Optional[float] = None\n    ) -> PageObject:\n        \"\"\"\n        Append a blank page to this PDF file and return it.\n\n        If no page size is specified, use the size of the last page.\n\n        Args:\n            width: The width of the new page expressed in default user\n                space units.\n            height: The height of the new page expressed in default\n                user space units.\n\n        Returns:\n            The newly appended page.\n\n        Raises:\n            PageSizeNotDefinedError: if width and height are not defined\n                and previous page does not exist.\n\n        \"\"\"\n        page = PageObject.create_blank_page(self, width, height)\n        return self.add_page(page)\n\n    def insert_blank_page(\n        self,\n        width: Optional[Union[float, decimal.Decimal]] = None,\n        height: Optional[Union[float, decimal.Decimal]] = None,\n        index: int = 0,\n    ) -> PageObject:\n        \"\"\"\n        Insert a blank page to this PDF file and return it.\n\n        If no page size is specified for a dimension, use the size of the last page.\n\n        Args:\n            width: The width of the new page in default user space units.\n            height: The height of the new page in default user space units.\n            index: Position to add the page.\n\n        Returns:\n            The newly inserted page.\n\n        Raises:\n            PageSizeNotDefinedError: if width and height are not defined\n                and previous page does not exist.\n            IndexError: Index is outside of [-self.get_num_pages(), self.get_num_pages()]\n        \"\"\"\n        num_pages = self.get_num_pages()\n        if abs(index) <= num_pages:\n            # Use the chosen index, but do not exceed the available pages\n            fixed_index = min(index, num_pages - 1)\n            mediabox = self.pages[fixed_index].mediabox\n            if width is None or width <= 0:\n                width = mediabox.width\n            if height is None or height <= 0:\n                height = mediabox.height\n        else:\n            raise IndexError(f\"Index should be in range [-{num_pages}, {num_pages}]\")\n\n        page = PageObject.create_blank_page(self, width, height)\n        self.insert_page(page, index)\n        return page\n\n    @property\n    def open_destination(\n        self,\n    ) -> Union[None, Destination, TextStringObject, ByteStringObject]:\n        return super().open_destination\n\n    @open_destination.setter\n    def open_destination(self, dest: Union[None, str, Destination, PageObject]) -> None:\n        if dest is None:\n            try:\n                del self._root_object[\"/OpenAction\"]\n            except KeyError:\n                pass\n        elif isinstance(dest, str):\n            self._root_object[NameObject(\"/OpenAction\")] = TextStringObject(dest)\n        elif isinstance(dest, Destination):\n            self._root_object[NameObject(\"/OpenAction\")] = dest.dest_array\n        elif isinstance(dest, PageObject):\n            self._root_object[NameObject(\"/OpenAction\")] = Destination(\n                \"Opening\",\n                dest.indirect_reference\n                if dest.indirect_reference is not None\n                else NullObject(),\n                PAGE_FIT,\n            ).dest_array\n\n    def add_js(self, javascript: str) -> None:\n        \"\"\"\n        Add JavaScript which will launch upon opening this PDF.\n\n        Args:\n            javascript: Your JavaScript.\n\n        Example:\n            This will launch the print window when the PDF is opened.\n\n            >>> from pypdf import PdfWriter\n            >>> output = PdfWriter()\n            >>> output.add_js(\"this.print({bUI:true,bSilent:false,bShrinkToFit:true});\")\n\n        \"\"\"\n        # Names / JavaScript preferred to be able to add multiple scripts\n        if \"/Names\" not in self._root_object:\n            self._root_object[NameObject(CA.NAMES)] = DictionaryObject()\n        names = cast(DictionaryObject, self._root_object[CA.NAMES])\n        if \"/JavaScript\" not in names:\n            names[NameObject(\"/JavaScript\")] = DictionaryObject(\n                {NameObject(\"/Names\"): ArrayObject()}\n            )\n        js_list = cast(\n            ArrayObject, cast(DictionaryObject, names[\"/JavaScript\"])[\"/Names\"]\n        )\n        # We need a name for parameterized JavaScript in the PDF file,\n        # but it can be anything.\n        js_list.append(create_string_object(str(uuid.uuid4())))\n\n        js = DictionaryObject(\n            {\n                NameObject(PagesAttributes.TYPE): NameObject(\"/Action\"),\n                NameObject(\"/S\"): NameObject(\"/JavaScript\"),\n                NameObject(\"/JS\"): TextStringObject(f\"{javascript}\"),\n            }\n        )\n        js_list.append(self._add_object(js))\n\n    def add_attachment(self, filename: str, data: Union[str, bytes]) -> \"EmbeddedFile\":\n        \"\"\"\n        Embed a file inside the PDF.\n\n        Reference:\n        https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf\n        Section 7.11.3\n\n        Args:\n            filename: The filename to display.\n            data: The data in the file.\n\n        Returns:\n            EmbeddedFile instance for the newly created embedded file.\n\n        \"\"\"\n        return EmbeddedFile._create_new(self, filename, data)\n\n    def append_pages_from_reader(\n        self,\n        reader: PdfReader,\n        after_page_append: Optional[Callable[[PageObject], None]] = None,\n    ) -> None:\n        \"\"\"\n        Copy pages from reader to writer. Includes an optional callback\n        parameter which is invoked after pages are appended to the writer.\n\n        ``append`` should be preferred.\n\n        Args:\n            reader: a PdfReader object from which to copy page\n                annotations to this writer object. The writer's annots\n                will then be updated.\n            after_page_append:\n                Callback function that is invoked after each page is appended to\n                the writer. Signature includes a reference to the appended page\n                (delegates to append_pages_from_reader). The single parameter of\n                the callback is a reference to the page just appended to the\n                document.\n\n        \"\"\"\n        reader_num_pages = len(reader.pages)\n        # Copy pages from reader to writer\n        for reader_page_number in range(reader_num_pages):\n            reader_page = reader.pages[reader_page_number]\n            writer_page = self.add_page(reader_page)\n            # Trigger callback, pass writer page as parameter\n            if callable(after_page_append):\n                after_page_append(writer_page)\n\n    def _merge_content_stream_to_page(\n        self,\n        page: PageObject,\n        new_content_data: bytes,\n    ) -> None:\n        \"\"\"\n        Combines existing content stream(s) with new content (as bytes).\n\n        Args:\n            page: The page to which the new content data will be added.\n            new_content_data: A binary-encoded new content stream, for\n                instance the commands to draw an XObject.\n        \"\"\"\n        # First resolve the existing page content. This always is an IndirectObject:\n        # PDF Explained by John Whitington\n        # https://www.oreilly.com/library/view/pdf-explained/9781449321581/ch04.html\n        if NameObject(\"/Contents\") in page:\n            existing_content_ref = page[NameObject(\"/Contents\")]\n            existing_content = existing_content_ref.get_object()\n\n            if isinstance(existing_content, ArrayObject):\n                # Create a new StreamObject for the new_content_data\n                new_stream_obj = StreamObject()\n                new_stream_obj.set_data(new_content_data)\n                existing_content.append(self._add_object(new_stream_obj))\n                page[NameObject(\"/Contents\")] = self._add_object(existing_content)\n            if isinstance(existing_content, StreamObject):\n                # Merge new content to existing StreamObject\n                merged_data = existing_content.get_data() + b\"\\n\" + new_content_data\n                new_stream = StreamObject()\n                new_stream.set_data(merged_data)\n                page[NameObject(\"/Contents\")] = self._add_object(new_stream)\n        else:\n            # If no existing content, then we have an empty page.\n            # Create a new StreamObject in a new /Contents entry.\n            new_stream = StreamObject()\n            new_stream.set_data(new_content_data)\n            page[NameObject(\"/Contents\")] = self._add_object(new_stream)\n\n    def _add_apstream_object(\n            self,\n            page: PageObject,\n            appearance_stream_obj: StreamObject,\n            object_name: str,\n            x_offset: float,\n            y_offset: float,\n        ) -> None:\n        \"\"\"\n        Adds an appearance stream to the page content in the form of\n        an XObject.\n\n        Args:\n            page: The page to which to add the appearance stream.\n            appearance_stream_obj: The appearance stream.\n            object_name: The name of the appearance stream.\n            x_offset: The horizontal offset for the appearance stream.\n            y_offset: The vertical offset for the appearance stream.\n        \"\"\"\n        # Prepare XObject resource dictionary on the page. This currently\n        # only deals with font resources, but can easily be adapted to also\n        # include other resources.\n        pg_res = cast(DictionaryObject, page[PG.RESOURCES])\n        if \"/Resources\" in appearance_stream_obj:\n            ap_stream_res = cast(DictionaryObject, appearance_stream_obj[\"/Resources\"])\n            ap_stream_font_dict = cast(DictionaryObject, ap_stream_res.get(\"/Font\", DictionaryObject()))\n            if \"/Font\" not in pg_res:\n                font_dict_ref = self._add_object(DictionaryObject())\n                pg_res[NameObject(\"/Font\")] = font_dict_ref\n            pg_font_res = cast(DictionaryObject, pg_res[\"/Font\"].get_object())\n            # Merge fonts from the appearance stream into the page's font resources\n            for font_name, font_res in ap_stream_font_dict.items():\n                if font_name not in pg_font_res:\n                    font_res_ref = self._add_object(font_res)\n                    pg_font_res[font_name] = font_res_ref\n        # Always add the resolved stream object to the writer to get a new IndirectObject.\n        # This ensures we have a valid IndirectObject managed by *this* writer.\n        xobject_ref = self._add_object(appearance_stream_obj)\n        xobject_name = NameObject(f\"/Fm_{object_name}\")._sanitize()\n        if \"/XObject\" not in pg_res:\n            pg_res[NameObject(\"/XObject\")] = DictionaryObject()\n        pg_xo_res  = cast(DictionaryObject, pg_res[\"/XObject\"])\n        if xobject_name not in pg_xo_res:\n            pg_xo_res[xobject_name] = xobject_ref\n        else:\n            logger_warning(\n                f\"XObject {xobject_name!r} already added to page resources. This might be an issue.\",\n                __name__\n            )\n        xobject_cm = Transformation().translate(x_offset, y_offset)\n        xobject_drawing_commands = f\"q\\n{xobject_cm._to_cm()}\\n{xobject_name} Do\\nQ\".encode()\n        self._merge_content_stream_to_page(page, xobject_drawing_commands)\n\n    FFBITS_NUL = FA.FfBits(0)\n\n    def update_page_form_field_values(\n        self,\n        page: Union[PageObject, list[PageObject], None],\n        fields: Mapping[str, Union[str, list[str], tuple[str, str, float]]],\n        flags: FA.FfBits = FFBITS_NUL,\n        auto_regenerate: Optional[bool] = True,\n        flatten: bool = False,\n    ) -> None:\n        \"\"\"\n        Update the form field values for a given page from a fields dictionary.\n\n        Copy field texts and values from fields to page.\n        If the field links to a parent object, add the information to the parent.\n\n        Args:\n            page: `PageObject` - references **PDF writer's page** where the\n                annotations and field data will be updated.\n                `List[Pageobject]` - provides list of pages to be processed.\n                `None` - all pages.\n            fields: a Python dictionary of:\n\n                * field names (/T) as keys and text values (/V) as value\n                * field names (/T) as keys and list of text values (/V) for multiple choice list\n                * field names (/T) as keys and tuple of:\n                    * text values (/V)\n                    * font id (e.g. /F1, the font id must exist)\n                    * font size (0 for autosize)\n\n            flags: A set of flags from :class:`~pypdf.constants.FieldDictionaryAttributes.FfBits`.\n\n            auto_regenerate: Set/unset the need_appearances flag;\n                the flag is unchanged if auto_regenerate is None.\n\n            flatten: Whether or not to flatten the annotation. If True, this adds the annotation's\n                appearance stream to the page contents. Note that this option does not remove the\n                annotation itself.\n\n        \"\"\"\n        if CatalogDictionary.ACRO_FORM not in self._root_object:\n            raise PyPdfError(\"No /AcroForm dictionary in PDF of PdfWriter Object\")\n        acro_form = cast(DictionaryObject, self._root_object[CatalogDictionary.ACRO_FORM])\n        if InteractiveFormDictEntries.Fields not in acro_form:\n            raise PyPdfError(\"No /Fields dictionary in PDF of PdfWriter Object\")\n        if isinstance(auto_regenerate, bool):\n            self.set_need_appearances_writer(auto_regenerate)\n        # Iterate through pages, update field values\n        if page is None:\n            page = list(self.pages)\n        if isinstance(page, list):\n            for p in page:\n                if PG.ANNOTS in p:  # just to prevent warnings\n                    self.update_page_form_field_values(p, fields, flags, None, flatten=flatten)\n            return\n        if PG.ANNOTS not in page:\n            logger_warning(\"No fields to update on this page\", __name__)\n            return\n        appearance_stream_obj: Optional[StreamObject] = None\n\n        for annotation in page[PG.ANNOTS]:  # type: ignore\n            annotation = cast(DictionaryObject, annotation.get_object())\n            if annotation.get(\"/Subtype\", \"\") != \"/Widget\":\n                continue\n            if \"/FT\" in annotation and \"/T\" in annotation:\n                parent_annotation = annotation\n            else:\n                parent_annotation = annotation.get(\n                    PG.PARENT, DictionaryObject()\n                ).get_object()\n\n            for field, value in fields.items():\n                rectangle = cast(RectangleObject, annotation[AA.Rect])\n                if not (\n                    self._get_qualified_field_name(parent_annotation) == field\n                    or parent_annotation.get(\"/T\", None) == field\n                ):\n                    continue\n                if (\n                    parent_annotation.get(\"/FT\", None) == \"/Ch\"\n                    and \"/I\" in parent_annotation\n                ):\n                    del parent_annotation[\"/I\"]\n                if flags:\n                    annotation[NameObject(FA.Ff)] = NumberObject(flags)\n                # Set the field value\n                if not (value is None and flatten):  # Only change values if given by user and not flattening.\n                    if isinstance(value, list):\n                        lst = ArrayObject(TextStringObject(v) for v in value)\n                        parent_annotation[NameObject(FA.V)] = lst\n                    elif isinstance(value, tuple):\n                        annotation[NameObject(FA.V)] = TextStringObject(\n                            value[0],\n                        )\n                    else:\n                        parent_annotation[NameObject(FA.V)] = TextStringObject(value)\n                # Get or create the field's appearance stream object\n                if parent_annotation.get(FA.FT) == \"/Btn\":\n                    # Checkbox button (no /FT found in Radio widgets);\n                    # We can find the associated appearance stream object\n                    # within the annotation.\n                    v = NameObject(value)\n                    ap = cast(DictionaryObject, annotation[NameObject(AA.AP)])\n                    normal_ap = cast(DictionaryObject, ap[\"/N\"])\n                    if v not in normal_ap:\n                        v = NameObject(\"/Off\")\n                    appearance_stream_obj = normal_ap.get(v)\n                    # Other cases will be updated through the for loop\n                    annotation[NameObject(AA.AS)] = v\n                    annotation[NameObject(FA.V)] = v\n                elif (\n                    parent_annotation.get(FA.FT) == \"/Tx\"\n                    or parent_annotation.get(FA.FT) == \"/Ch\"\n                ):\n                    # Textbox; we need to generate the appearance stream object\n                    if isinstance(value, tuple):\n                        appearance_stream_obj = TextStreamAppearance.from_text_annotation(\n                            acro_form, parent_annotation, annotation, value[1], value[2]\n                        )\n                    else:\n                        appearance_stream_obj = TextStreamAppearance.from_text_annotation(\n                            acro_form, parent_annotation, annotation\n                        )\n                    # Add the appearance stream object\n                    if AA.AP not in annotation:\n                        annotation[NameObject(AA.AP)] = DictionaryObject(\n                            {NameObject(\"/N\"): self._add_object(appearance_stream_obj)}\n                        )\n                    elif \"/N\" not in (ap:= cast(DictionaryObject, annotation[AA.AP])):\n                        cast(DictionaryObject, annotation[NameObject(AA.AP)])[\n                            NameObject(\"/N\")\n                        ] = self._add_object(appearance_stream_obj)\n                    else:  # [/AP][/N] exists\n                        n = annotation[AA.AP][\"/N\"].indirect_reference.idnum  # type: ignore\n                        self._objects[n - 1] = appearance_stream_obj\n                        appearance_stream_obj.indirect_reference = IndirectObject(n, 0, self)\n                elif (\n                    annotation.get(FA.FT) == \"/Sig\"\n                ):  # deprecated  # not implemented yet\n                    logger_warning(\"Signature forms not implemented yet\", __name__)\n                if flatten and appearance_stream_obj is not None:\n                    self._add_apstream_object(page, appearance_stream_obj, field, rectangle[0], rectangle[1])\n\n    def reattach_fields(\n        self, page: Optional[PageObject] = None\n    ) -> list[DictionaryObject]:\n        \"\"\"\n        Parse annotations within the page looking for orphan fields and\n        reattach then into the Fields Structure.\n\n        Args:\n            page: page to analyze.\n                  If none is provided, all pages will be analyzed.\n\n        Returns:\n            list of reattached fields.\n\n        \"\"\"\n        lst = []\n        if page is None:\n            for p in self.pages:\n                lst += self.reattach_fields(p)\n            return lst\n\n        try:\n            af = cast(DictionaryObject, self._root_object[CatalogDictionary.ACRO_FORM])\n        except KeyError:\n            af = DictionaryObject()\n            self._root_object[NameObject(CatalogDictionary.ACRO_FORM)] = af\n        try:\n            fields = cast(ArrayObject, af[InteractiveFormDictEntries.Fields])\n        except KeyError:\n            fields = ArrayObject()\n            af[NameObject(InteractiveFormDictEntries.Fields)] = fields\n\n        if \"/Annots\" not in page:\n            return lst\n        annotations = cast(ArrayObject, page[\"/Annots\"])\n        for idx, annotation in enumerate(annotations):\n            is_indirect = isinstance(annotation, IndirectObject)\n            annotation = cast(DictionaryObject, annotation.get_object())\n            if annotation.get(\"/Subtype\", \"\") == \"/Widget\" and \"/FT\" in annotation:\n                if (\n                    \"indirect_reference\" in annotation.__dict__\n                    and annotation.indirect_reference in fields\n                ):\n                    continue\n                if not is_indirect:\n                    annotations[idx] = self._add_object(annotation)\n                fields.append(annotation.indirect_reference)\n                lst.append(annotation)\n        return lst\n\n    def clone_reader_document_root(self, reader: PdfReader) -> None:\n        \"\"\"\n        Copy the reader document root to the writer and all sub-elements,\n        including pages, threads, outlines,... For partial insertion, ``append``\n        should be considered.\n\n        Args:\n            reader: PdfReader from which the document root should be copied.\n\n        \"\"\"\n        self._info_obj = None\n        if self.incremental:\n            self._objects = [None] * (cast(int, reader.trailer[\"/Size\"]) - 1)\n            for i in range(len(self._objects)):\n                o = reader.get_object(i + 1)\n                if o is not None:\n                    self._objects[i] = o.replicate(self)\n        else:\n            self._objects.clear()\n        self._root_object = reader.root_object.clone(self)\n        self._pages = self._root_object.raw_get(\"/Pages\")\n\n        if len(self._objects) > cast(int, reader.trailer[\"/Size\"]):\n            if self.strict:\n                raise PdfReadError(\n                    f\"Object count {len(self._objects)} exceeds defined trailer size {reader.trailer['/Size']}\"\n                )\n            logger_warning(\n                f\"Object count {len(self._objects)} exceeds defined trailer size {reader.trailer['/Size']}\",\n                __name__\n            )\n\n        # must be done here before rewriting\n        if self.incremental:\n            self._original_hash = [\n                (obj.hash_bin() if obj is not None else 0) for obj in self._objects\n            ]\n\n        try:\n            self._flatten()\n        except IndexError:\n            raise PdfReadError(\"Got index error while flattening.\")\n\n        assert self.flattened_pages is not None\n        for p in self.flattened_pages:\n            self._replace_object(cast(IndirectObject, p.indirect_reference).idnum, p)\n            if not self.incremental:\n                p[NameObject(\"/Parent\")] = self._pages\n        if not self.incremental:\n            cast(DictionaryObject, self._pages.get_object())[\n                NameObject(\"/Kids\")\n            ] = ArrayObject([p.indirect_reference for p in self.flattened_pages])\n\n    def clone_document_from_reader(\n        self,\n        reader: PdfReader,\n        after_page_append: Optional[Callable[[PageObject], None]] = None,\n    ) -> None:\n        \"\"\"\n        Create a copy (clone) of a document from a PDF file reader cloning\n        section '/Root' and '/Info' and '/ID' of the pdf.\n\n        Args:\n            reader: PDF file reader instance from which the clone\n                should be created.\n            after_page_append:\n                Callback function that is invoked after each page is appended to\n                the writer. Signature includes a reference to the appended page\n                (delegates to append_pages_from_reader). The single parameter of\n                the callback is a reference to the page just appended to the\n                document.\n\n        \"\"\"\n        self.clone_reader_document_root(reader)\n        inf = reader._info\n        if self.incremental:\n            if inf is not None:\n                self._info_obj = cast(\n                    IndirectObject, inf.clone(self).indirect_reference\n                )\n                assert isinstance(self._info, DictionaryObject), \"for mypy\"\n                self._original_hash[\n                    self._info_obj.indirect_reference.idnum - 1\n                ] = self._info.hash_bin()\n        elif inf is not None:\n            self._info_obj = self._add_object(\n                DictionaryObject(cast(DictionaryObject, inf.get_object()))\n            )\n        # else: _info_obj = None done in clone_reader_document_root()\n\n        try:\n            self._ID = cast(ArrayObject, reader._ID).clone(self)\n        except AttributeError:\n            pass\n\n        if callable(after_page_append):\n            for page in cast(\n                ArrayObject, cast(DictionaryObject, self._pages.get_object())[\"/Kids\"]\n            ):\n                after_page_append(page.get_object())\n\n    def _compute_document_identifier(self) -> ByteStringObject:\n        stream = BytesIO()\n        self._write_pdf_structure(stream)\n        stream.seek(0)\n        return ByteStringObject(_rolling_checksum(stream).encode(\"utf8\"))\n\n    def generate_file_identifiers(self) -> None:\n        \"\"\"\n        Generate an identifier for the PDF that will be written.\n\n        The only point of this is ensuring uniqueness. Reproducibility is not\n        required.\n        When a file is first written, both identifiers shall be set to the same value.\n        If both identifiers match when a file reference is resolved, it is very\n        likely that the correct and unchanged file has been found. If only the first\n        identifier matches, a different version of the correct file has been found.\n        see §14.4 \"File Identifiers\".\n        \"\"\"\n        if self._ID:\n            id1 = self._ID[0]\n            id2 = self._compute_document_identifier()\n        else:\n            id1 = self._compute_document_identifier()\n            id2 = id1\n        self._ID = ArrayObject((id1, id2))\n\n    def encrypt(\n        self,\n        user_password: str,\n        owner_password: Optional[str] = None,\n        use_128bit: bool = True,\n        permissions_flag: UserAccessPermissions = ALL_DOCUMENT_PERMISSIONS,\n        *,\n        algorithm: Optional[str] = None,\n    ) -> None:\n        \"\"\"\n        Encrypt this PDF file with the PDF Standard encryption handler.\n\n        Args:\n            user_password: The password which allows for opening\n                and reading the PDF file with the restrictions provided.\n            owner_password: The password which allows for\n                opening the PDF files without any restrictions. By default,\n                the owner password is the same as the user password.\n            use_128bit: flag as to whether to use 128bit\n                encryption. When false, 40bit encryption will be used.\n                By default, this flag is on.\n            permissions_flag: permissions as described in\n                Table 3.20 of the PDF 1.7 specification. A bit value of 1 means\n                the permission is granted.\n                Hence an integer value of -1 will set all flags.\n                Bit position 3 is for printing, 4 is for modifying content,\n                5 and 6 control annotations, 9 for form fields,\n                10 for extraction of text and graphics.\n            algorithm: encrypt algorithm. Values may be one of \"RC4-40\", \"RC4-128\",\n                \"AES-128\", \"AES-256-R5\", \"AES-256\". If it is valid,\n                `use_128bit` will be ignored.\n\n        \"\"\"\n        if owner_password is None:\n            owner_password = user_password\n\n        if algorithm is not None:\n            try:\n                alg = getattr(EncryptAlgorithm, algorithm.replace(\"-\", \"_\"))\n            except AttributeError:\n                raise ValueError(f\"Algorithm '{algorithm}' NOT supported\")\n        else:\n            alg = EncryptAlgorithm.RC4_128\n            if not use_128bit:\n                alg = EncryptAlgorithm.RC4_40\n        self.generate_file_identifiers()\n        assert self._ID\n        self._encryption = Encryption.make(alg, permissions_flag, self._ID[0])\n        # in case call `encrypt` again\n        entry = self._encryption.write_entry(user_password, owner_password)\n        if self._encrypt_entry:\n            # replace old encrypt_entry\n            assert self._encrypt_entry.indirect_reference is not None\n            entry.indirect_reference = self._encrypt_entry.indirect_reference\n            self._objects[entry.indirect_reference.idnum - 1] = entry\n        else:\n            self._add_object(entry)\n        self._encrypt_entry = entry\n\n    def _resolve_links(self) -> None:\n        \"\"\"Patch up links that were added to the document earlier, to\n        make sure they still point to the same pages.\n        \"\"\"\n        for (new_link, old_link) in self._unresolved_links:\n            old_page = old_link.find_referenced_page()\n            if not old_page:\n                continue\n            new_page = self._merged_in_pages.get(old_page)\n            if new_page is None:\n                continue\n            new_link.patch_reference(self, new_page)\n\n    def write_stream(self, stream: StreamType) -> None:\n        if hasattr(stream, \"mode\") and \"b\" not in stream.mode:\n            logger_warning(\n                f\"File <{stream.name}> to write to is not in binary mode. \"\n                \"It may not be written to correctly.\",\n                __name__,\n            )\n        self._resolve_links()\n\n        if self.incremental:\n            self._reader.stream.seek(0)\n            stream.write(self._reader.stream.read(-1))\n            if len(self.list_objects_in_increment()) > 0:\n                self._write_increment(stream)  # writes objs, xref stream and startxref\n        else:\n            object_positions, free_objects = self._write_pdf_structure(stream)\n            xref_location = self._write_xref_table(\n                stream, object_positions, free_objects\n            )\n            self._write_trailer(stream, xref_location)\n\n    def write(self, stream: Union[Path, StrByteType]) -> tuple[bool, IO[Any]]:\n        \"\"\"\n        Write the collection of pages added to this object out as a PDF file.\n\n        Args:\n            stream: An object to write the file to. The object can support\n                the write method and the tell method, similar to a file object, or\n                be a file path, just like the fileobj, just named it stream to keep\n                existing workflow.\n\n        Returns:\n            A tuple (bool, IO).\n\n        \"\"\"\n        my_file = False\n\n        if stream == \"\":\n            raise ValueError(f\"Output({stream=}) is empty.\")\n\n        if isinstance(stream, (str, Path)):\n            stream = FileIO(stream, \"wb\")\n            my_file = True\n\n        self.write_stream(stream)\n\n        if my_file:\n            stream.close()\n        else:\n            stream.flush()\n\n        return my_file, stream\n\n    def list_objects_in_increment(self) -> list[IndirectObject]:\n        \"\"\"\n        For analysis or debugging.\n        Provides the list of new or modified objects that will be written\n        in the increment.\n        Deleted objects will not be freed but will become orphans.\n\n        Returns:\n            List of new or modified IndirectObjects\n\n        \"\"\"\n        original_hash_count = len(self._original_hash)\n        return [\n            cast(IndirectObject, obj).indirect_reference\n            for i, obj in enumerate(self._objects)\n            if (\n                obj is not None\n                and (\n                    i >= original_hash_count\n                    or obj.hash_bin() != self._original_hash[i]\n                )\n            )\n        ]\n\n    def _write_increment(self, stream: StreamType) -> None:\n        object_positions = {}\n        object_blocks = []\n        current_start = -1\n        current_stop = -2\n        original_hash_count = len(self._original_hash)\n        for i, obj in enumerate(self._objects):\n            if obj is not None and (\n                i >= original_hash_count\n                or obj.hash_bin() != self._original_hash[i]\n            ):\n                idnum = i + 1\n                assert isinstance(obj, PdfObject), \"mypy\"\n                # first write new/modified object\n                object_positions[idnum] = stream.tell()\n                stream.write(f\"{idnum} 0 obj\\n\".encode())\n                \"\"\" encryption is not operational\n                if self._encryption and obj != self._encrypt_entry:\n                    obj = self._encryption.encrypt_object(obj, idnum, 0)\n                \"\"\"\n                obj.write_to_stream(stream)\n                stream.write(b\"\\nendobj\\n\")\n\n                # prepare xref\n                if idnum != current_stop:\n                    if current_start > 0:\n                        object_blocks.append(\n                            [current_start, current_stop - current_start]\n                        )\n                    current_start = idnum\n                current_stop = idnum + 1\n        assert current_start > 0, \"for pytest only\"\n        object_blocks.append([current_start, current_stop - current_start])\n        # write incremented xref\n        xref_location = stream.tell()\n        xr_id = len(self._objects) + 1\n        stream.write(f\"{xr_id} 0 obj\".encode())\n        init_data = {\n            NameObject(\"/Type\"): NameObject(\"/XRef\"),\n            NameObject(\"/Size\"): NumberObject(xr_id + 1),\n            NameObject(\"/Root\"): self.root_object.indirect_reference,\n            NameObject(\"/Filter\"): NameObject(\"/FlateDecode\"),\n            NameObject(\"/Index\"): ArrayObject(\n                [NumberObject(_it) for _su in object_blocks for _it in _su]\n            ),\n            NameObject(\"/W\"): ArrayObject(\n                [NumberObject(1), NumberObject(4), NumberObject(1)]\n            ),\n            \"__streamdata__\": b\"\",\n        }\n        if self._info is not None and (\n            self._info.indirect_reference.idnum - 1  # type: ignore\n            >= len(self._original_hash)\n            or cast(IndirectObject, self._info).hash_bin()  # kept for future\n            != self._original_hash[\n                self._info.indirect_reference.idnum - 1  # type: ignore\n            ]\n        ):\n            init_data[NameObject(TK.INFO)] = self._info.indirect_reference\n        init_data[NameObject(TK.PREV)] = NumberObject(self._reader._startxref)\n        if self._ID:\n            init_data[NameObject(TK.ID)] = self._ID\n        xr = StreamObject.initialize_from_dictionary(init_data)\n        xr.set_data(\n            b\"\".join(\n                [struct.pack(b\">BIB\", 1, _pos, 0) for _pos in object_positions.values()]\n            )\n        )\n        xr.write_to_stream(stream)\n        stream.write(f\"\\nendobj\\nstartxref\\n{xref_location}\\n%%EOF\\n\".encode())  # eof\n\n    def _write_pdf_structure(self, stream: StreamType) -> tuple[list[int], list[int]]:\n        object_positions = []\n        free_objects = []\n        stream.write(self.pdf_header.encode() + b\"\\n\")\n        stream.write(b\"%\\xE2\\xE3\\xCF\\xD3\\n\")\n\n        for idnum, obj in enumerate(self._objects, start=1):\n            if obj is not None:\n                object_positions.append(stream.tell())\n                stream.write(f\"{idnum} 0 obj\\n\".encode())\n                if self._encryption and obj != self._encrypt_entry:\n                    obj = self._encryption.encrypt_object(obj, idnum, 0)\n                obj.write_to_stream(stream)\n                stream.write(b\"\\nendobj\\n\")\n            else:\n                object_positions.append(-1)\n                free_objects.append(idnum)\n        free_objects.append(0)  # add 0 to loop in accordance with specification\n        return object_positions, free_objects\n\n    def _write_xref_table(\n        self, stream: StreamType, object_positions: list[int], free_objects: list[int]\n    ) -> int:\n        xref_location = stream.tell()\n        stream.write(b\"xref\\n\")\n        stream.write(f\"0 {len(self._objects) + 1}\\n\".encode())\n        stream.write(f\"{free_objects[0]:0>10} {65535:0>5} f \\n\".encode())\n        free_idx = 1\n        for offset in object_positions:\n            if offset > 0:\n                stream.write(f\"{offset:0>10} {0:0>5} n \\n\".encode())\n            else:\n                stream.write(f\"{free_objects[free_idx]:0>10} {1:0>5} f \\n\".encode())\n                free_idx += 1\n        return xref_location\n\n    def _write_trailer(self, stream: StreamType, xref_location: int) -> None:\n        \"\"\"\n        Write the PDF trailer to the stream.\n\n        To quote the PDF specification:\n            [The] trailer [gives] the location of the cross-reference table and\n            of certain special objects within the body of the file.\n        \"\"\"\n        stream.write(b\"trailer\\n\")\n        trailer = DictionaryObject(\n            {\n                NameObject(TK.SIZE): NumberObject(len(self._objects) + 1),\n                NameObject(TK.ROOT): self.root_object.indirect_reference,\n            }\n        )\n        if self._info is not None:\n            trailer[NameObject(TK.INFO)] = self._info.indirect_reference\n        if self._ID is not None:\n            trailer[NameObject(TK.ID)] = self._ID\n        if self._encrypt_entry:\n            trailer[NameObject(TK.ENCRYPT)] = self._encrypt_entry.indirect_reference\n        trailer.write_to_stream(stream)\n        stream.write(f\"\\nstartxref\\n{xref_location}\\n%%EOF\\n\".encode())  # eof\n\n    @property\n    def metadata(self) -> Optional[DocumentInformation]:\n        \"\"\"\n        Retrieve/set the PDF file's document information dictionary, if it exists.\n\n        Args:\n            value: dict with the entries to be set. if None : remove the /Info entry from the pdf.\n\n        Note that some PDF files use (XMP) metadata streams instead of document\n        information dictionaries, and these metadata streams will not be\n        accessed by this function, but by :meth:`~xmp_metadata`.\n\n        \"\"\"\n        return super().metadata\n\n    @metadata.setter\n    def metadata(\n        self,\n        value: Optional[Union[DocumentInformation, DictionaryObject, dict[Any, Any]]],\n    ) -> None:\n        if value is None:\n            self._info = None\n        else:\n            if self._info is not None:\n                self._info.clear()\n\n            self.add_metadata(value)\n\n    def add_metadata(self, infos: dict[str, Any]) -> None:\n        \"\"\"\n        Add custom metadata to the output.\n\n        Args:\n            infos: a Python dictionary where each key is a field\n                and each value is your new metadata.\n\n        \"\"\"\n        args = {}\n        if isinstance(infos, PdfObject):\n            infos = cast(DictionaryObject, infos.get_object())\n        for key, value in list(infos.items()):\n            if isinstance(value, PdfObject):\n                value = value.get_object()\n            args[NameObject(key)] = create_string_object(str(value))\n        if self._info is None:\n            self._info = DictionaryObject()\n        self._info.update(args)\n\n    def compress_identical_objects(\n        self,\n        remove_identicals: bool = True,\n        remove_orphans: bool = True,\n    ) -> None:\n        \"\"\"\n        Parse the PDF file and merge objects that have the same hash.\n        This will make objects common to multiple pages.\n        Recommended to be used just before writing output.\n\n        Args:\n            remove_identicals: Remove identical objects.\n            remove_orphans: Remove unreferenced objects.\n\n        \"\"\"\n\n        def replace_in_obj(\n            obj: PdfObject, crossref: dict[IndirectObject, IndirectObject]\n        ) -> None:\n            if isinstance(obj, DictionaryObject):\n                key_val = obj.items()\n            elif isinstance(obj, ArrayObject):\n                key_val = enumerate(obj)  # type: ignore\n            else:\n                return\n            assert isinstance(obj, (DictionaryObject, ArrayObject))\n            for k, v in key_val:\n                if isinstance(v, IndirectObject):\n                    orphans[v.idnum - 1] = False\n                    if v in crossref:\n                        obj[k] = crossref[v]\n                else:\n                    \"\"\"the filtering on DictionaryObject and ArrayObject only\n                    will be performed within replace_in_obj\"\"\"\n                    replace_in_obj(v, crossref)\n\n        # _idnum_hash :dict[hash]=(1st_ind_obj,[other_indir_objs,...])\n        self._idnum_hash = {}\n        orphans = [True] * len(self._objects)\n        # look for similar objects\n        for idx, obj in enumerate(self._objects):\n            if is_null_or_none(obj):\n                continue\n            assert obj is not None, \"mypy\"  # mypy: TypeGuard of `is_null_or_none` does not help here.\n            assert isinstance(obj.indirect_reference, IndirectObject)\n            h = obj.hash_value()\n            if remove_identicals and h in self._idnum_hash:\n                self._idnum_hash[h][1].append(obj.indirect_reference)\n                self._objects[idx] = None\n            else:\n                self._idnum_hash[h] = (obj.indirect_reference, [])\n\n        # generate the dict converting others to 1st\n        cnv = {v[0]: v[1] for v in self._idnum_hash.values() if len(v[1]) > 0}\n        cnv_rev: dict[IndirectObject, IndirectObject] = {}\n        for k, v in cnv.items():\n            cnv_rev.update(zip(v, (k,) * len(v)))\n\n        # replace reference to merged objects\n        for obj in self._objects:\n            if isinstance(obj, (DictionaryObject, ArrayObject)):\n                replace_in_obj(obj, cnv_rev)\n\n        # remove orphans (if applicable)\n        orphans[self.root_object.indirect_reference.idnum - 1] = False  # type: ignore\n\n        if not is_null_or_none(self._info):\n            orphans[self._info.indirect_reference.idnum - 1] = False  # type: ignore\n\n        try:\n            orphans[self._ID.indirect_reference.idnum - 1] = False  # type: ignore\n        except AttributeError:\n            pass\n        for i in compress(range(len(self._objects)), orphans):\n            self._objects[i] = None\n\n    def get_reference(self, obj: PdfObject) -> IndirectObject:\n        idnum = self._objects.index(obj) + 1\n        ref = IndirectObject(idnum, 0, self)\n        assert ref.get_object() == obj\n        return ref\n\n    def get_outline_root(self) -> TreeObject:\n        if CO.OUTLINES in self._root_object:\n            # Entries in the catalog dictionary\n            outline = cast(TreeObject, self._root_object[CO.OUTLINES])\n            if not isinstance(outline, TreeObject):\n                t = TreeObject(outline)\n                self._replace_object(outline.indirect_reference.idnum, t)\n                outline = t\n            idnum = self._objects.index(outline) + 1\n            outline_ref = IndirectObject(idnum, 0, self)\n            assert outline_ref.get_object() == outline\n        else:\n            outline = TreeObject()\n            outline.update({})\n            outline_ref = self._add_object(outline)\n            self._root_object[NameObject(CO.OUTLINES)] = outline_ref\n\n        return outline\n\n    def get_threads_root(self) -> ArrayObject:\n        \"\"\"\n        The list of threads.\n\n        See §12.4.3 of the PDF 1.7 or PDF 2.0 specification.\n\n        Returns:\n            An array (possibly empty) of Dictionaries with an ``/F`` key,\n            and optionally information about the thread in ``/I`` or ``/Metadata`` keys.\n\n        \"\"\"\n        if CO.THREADS in self._root_object:\n            # Entries in the catalog dictionary\n            threads = cast(ArrayObject, self._root_object[CO.THREADS])\n        else:\n            threads = ArrayObject()\n            self._root_object[NameObject(CO.THREADS)] = threads\n        return threads\n\n    @property\n    def threads(self) -> ArrayObject:\n        \"\"\"\n        Read-only property for the list of threads.\n\n        See §12.4.3 of the PDF 1.7 or PDF 2.0 specification.\n\n        Each element is a dictionary with an ``/F`` key, and optionally\n        information about the thread in ``/I`` or ``/Metadata`` keys.\n        \"\"\"\n        return self.get_threads_root()\n\n    def add_outline_item_destination(\n        self,\n        page_destination: Union[IndirectObject, PageObject, TreeObject],\n        parent: Union[None, TreeObject, IndirectObject] = None,\n        before: Union[None, TreeObject, IndirectObject] = None,\n        is_open: bool = True,\n    ) -> IndirectObject:\n        page_destination = cast(PageObject, page_destination.get_object())\n        if isinstance(page_destination, PageObject):\n            return self.add_outline_item_destination(\n                Destination(\n                    f\"page #{page_destination.page_number}\",\n                    cast(IndirectObject, page_destination.indirect_reference),\n                    Fit.fit(),\n                )\n            )\n\n        if parent is None:\n            parent = self.get_outline_root()\n\n        page_destination[NameObject(\"/%is_open%\")] = BooleanObject(is_open)\n        parent = cast(TreeObject, parent.get_object())\n        page_destination_ref = self._add_object(page_destination)\n        if before is not None:\n            before = before.indirect_reference\n        parent.insert_child(\n            page_destination_ref,\n            before,\n            self,\n            page_destination.inc_parent_counter_outline\n            if is_open\n            else (lambda x, y: 0),  # noqa: ARG005\n        )\n        if \"/Count\" not in page_destination:\n            page_destination[NameObject(\"/Count\")] = NumberObject(0)\n\n        return page_destination_ref\n\n    def add_outline_item_dict(\n        self,\n        outline_item: OutlineItemType,\n        parent: Union[None, TreeObject, IndirectObject] = None,\n        before: Union[None, TreeObject, IndirectObject] = None,\n        is_open: bool = True,\n    ) -> IndirectObject:\n        outline_item_object = TreeObject()\n        outline_item_object.update(outline_item)\n\n        \"\"\"code currently unreachable\n        if \"/A\" in outline_item:\n            action = DictionaryObject()\n            a_dict = cast(DictionaryObject, outline_item[\"/A\"])\n            for k, v in list(a_dict.items()):\n                action[NameObject(str(k))] = v\n            action_ref = self._add_object(action)\n            outline_item_object[NameObject(\"/A\")] = action_ref\n        \"\"\"\n        return self.add_outline_item_destination(\n            outline_item_object, parent, before, is_open\n        )\n\n    def add_outline_item(\n        self,\n        title: str,\n        page_number: Union[None, PageObject, IndirectObject, int],\n        parent: Union[None, TreeObject, IndirectObject] = None,\n        before: Union[None, TreeObject, IndirectObject] = None,\n        color: Optional[Union[tuple[float, float, float], str]] = None,\n        bold: bool = False,\n        italic: bool = False,\n        fit: Fit = PAGE_FIT,\n        is_open: bool = True,\n    ) -> IndirectObject:\n        \"\"\"\n        Add an outline item (commonly referred to as a \"Bookmark\") to the PDF file.\n\n        Args:\n            title: Title to use for this outline item.\n            page_number: Page number this outline item will point to.\n            parent: A reference to a parent outline item to create nested\n                outline items.\n            before:\n            color: Color of the outline item's font as a red, green, blue tuple\n                from 0.0 to 1.0 or as a Hex String (#RRGGBB)\n            bold: Outline item font is bold\n            italic: Outline item font is italic\n            fit: The fit of the destination page.\n\n        Returns:\n            The added outline item as an indirect object.\n\n        \"\"\"\n        page_ref: Union[None, NullObject, IndirectObject, NumberObject]\n        if isinstance(italic, Fit):  # it means that we are on the old params\n            if fit is not None and page_number is None:\n                page_number = fit\n            return self.add_outline_item(\n                title, page_number, parent, None, before, color, bold, italic, is_open=is_open\n            )\n        if page_number is None:\n            action_ref = None\n        else:\n            if isinstance(page_number, IndirectObject):\n                page_ref = page_number\n            elif isinstance(page_number, PageObject):\n                page_ref = page_number.indirect_reference\n            elif isinstance(page_number, int):\n                try:\n                    page_ref = self.pages[page_number].indirect_reference\n                except IndexError:\n                    page_ref = NumberObject(page_number)\n            if page_ref is None:\n                logger_warning(\n                    f\"can not find reference of page {page_number}\",\n                    __name__,\n                )\n                page_ref = NullObject()\n            dest = Destination(\n                NameObject(\"/\" + title + \" outline item\"),\n                page_ref,\n                fit,\n            )\n\n            action_ref = self._add_object(\n                DictionaryObject(\n                    {\n                        NameObject(GoToActionArguments.D): dest.dest_array,\n                        NameObject(GoToActionArguments.S): NameObject(\"/GoTo\"),\n                    }\n                )\n            )\n        outline_item = self._add_object(\n            _create_outline_item(action_ref, title, color, italic, bold)\n        )\n\n        if parent is None:\n            parent = self.get_outline_root()\n        return self.add_outline_item_destination(outline_item, parent, before, is_open)\n\n    def add_outline(self) -> None:\n        raise NotImplementedError(\n            \"This method is not yet implemented. Use :meth:`add_outline_item` instead.\"\n        )\n\n    def add_named_destination_array(\n        self, title: TextStringObject, destination: Union[IndirectObject, ArrayObject]\n    ) -> None:\n        named_dest = self.get_named_dest_root()\n        i = 0\n        while i < len(named_dest):\n            if title < named_dest[i]:\n                named_dest.insert(i, destination)\n                named_dest.insert(i, TextStringObject(title))\n                return\n            i += 2\n        named_dest.extend([TextStringObject(title), destination])\n        return\n\n    def add_named_destination_object(\n        self,\n        page_destination: PdfObject,\n    ) -> IndirectObject:\n        page_destination_ref = self._add_object(page_destination.dest_array)  # type: ignore\n        self.add_named_destination_array(\n            cast(\"TextStringObject\", page_destination[\"/Title\"]), page_destination_ref  # type: ignore\n        )\n\n        return page_destination_ref\n\n    def add_named_destination(\n        self,\n        title: str,\n        page_number: int,\n    ) -> IndirectObject:\n        page_ref = self.get_object(self._pages)[PagesAttributes.KIDS][page_number]  # type: ignore\n        dest = DictionaryObject()\n        dest.update(\n            {\n                NameObject(GoToActionArguments.D): ArrayObject(\n                    [page_ref, NameObject(TypFitArguments.FIT_H), NumberObject(826)]\n                ),\n                NameObject(GoToActionArguments.S): NameObject(\"/GoTo\"),\n            }\n        )\n\n        dest_ref = self._add_object(dest)\n        if not isinstance(title, TextStringObject):\n            title = TextStringObject(str(title))\n\n        self.add_named_destination_array(title, dest_ref)\n        return dest_ref\n\n    def remove_links(self) -> None:\n        \"\"\"Remove links and annotations from this output.\"\"\"\n        for page in self.pages:\n            self.remove_objects_from_page(page, ObjectDeletionFlag.ALL_ANNOTATIONS)\n\n    def remove_annotations(\n        self, subtypes: Optional[Union[AnnotationSubtype, Iterable[AnnotationSubtype]]]\n    ) -> None:\n        \"\"\"\n        Remove annotations by annotation subtype.\n\n        Args:\n            subtypes: subtype or list of subtypes to be removed.\n                Examples are: \"/Link\", \"/FileAttachment\", \"/Sound\",\n                \"/Movie\", \"/Screen\", ...\n                If you want to remove all annotations, use subtypes=None.\n\n        \"\"\"\n        for page in self.pages:\n            self._remove_annots_from_page(page, subtypes)\n\n    def _remove_annots_from_page(\n        self,\n        page: Union[IndirectObject, PageObject, DictionaryObject],\n        subtypes: Optional[Iterable[str]],\n    ) -> None:\n        page = cast(DictionaryObject, page.get_object())\n        if PG.ANNOTS in page:\n            i = 0\n            while i < len(cast(ArrayObject, page[PG.ANNOTS])):\n                an = cast(ArrayObject, page[PG.ANNOTS])[i]\n                obj = cast(DictionaryObject, an.get_object())\n                if subtypes is None or cast(str, obj[\"/Subtype\"]) in subtypes:\n                    if isinstance(an, IndirectObject):\n                        self._objects[an.idnum - 1] = NullObject()  # to reduce PDF size\n                    del page[PG.ANNOTS][i]  # type:ignore\n                else:\n                    i += 1\n\n    def remove_objects_from_page(\n        self,\n        page: Union[PageObject, DictionaryObject],\n        to_delete: Union[ObjectDeletionFlag, Iterable[ObjectDeletionFlag]],\n        text_filters: Optional[dict[str, Any]] = None\n    ) -> None:\n        \"\"\"\n        Remove objects specified by ``to_delete`` from the given page.\n\n        Args:\n            page: Page object to clean up.\n            to_delete: Objects to be deleted; can be a ``ObjectDeletionFlag``\n                or a list of ObjectDeletionFlag\n            text_filters: Properties of text to be deleted, if applicable. Optional.\n                This is a Python dictionary with the following properties:\n\n                * font_ids: List of font resource IDs (such as /F1 or /T1_0) to be deleted.\n\n        \"\"\"\n        if isinstance(to_delete, (list, tuple)):\n            for to_d in to_delete:\n                self.remove_objects_from_page(page, to_d)\n            return None\n        assert isinstance(to_delete, ObjectDeletionFlag)\n\n        if to_delete & ObjectDeletionFlag.LINKS:\n            return self._remove_annots_from_page(page, (\"/Link\",))\n        if to_delete & ObjectDeletionFlag.ATTACHMENTS:\n            return self._remove_annots_from_page(\n                page, (\"/FileAttachment\", \"/Sound\", \"/Movie\", \"/Screen\")\n            )\n        if to_delete & ObjectDeletionFlag.OBJECTS_3D:\n            return self._remove_annots_from_page(page, (\"/3D\",))\n        if to_delete & ObjectDeletionFlag.ALL_ANNOTATIONS:\n            return self._remove_annots_from_page(page, None)\n\n        jump_operators = []\n        if to_delete & ObjectDeletionFlag.DRAWING_IMAGES:\n            jump_operators = [\n                b\"w\", b\"J\", b\"j\", b\"M\", b\"d\", b\"i\",\n                b\"W\", b\"W*\",\n                b\"b\", b\"b*\", b\"B\", b\"B*\", b\"S\", b\"s\", b\"f\", b\"f*\", b\"F\", b\"n\",\n                b\"m\", b\"l\", b\"c\", b\"v\", b\"y\", b\"h\", b\"re\",\n                b\"sh\"\n            ]\n        if to_delete & ObjectDeletionFlag.TEXT:\n            jump_operators = [b\"Tj\", b\"TJ\", b\"'\", b'\"']\n\n        if not isinstance(page, PageObject):\n            page = PageObject(self, page.indirect_reference)  # pragma: no cover\n        if \"/Contents\" in page:\n            content = cast(ContentStream, page.get_contents())\n\n            images, forms = self._remove_objects_from_page__clean_forms(\n                elt=page, stack=[], jump_operators=jump_operators, to_delete=to_delete, text_filters=text_filters,\n            )\n\n            self._remove_objects_from_page__clean(\n                content=content, images=images, forms=forms,\n                jump_operators=jump_operators, to_delete=to_delete,\n                text_filters=text_filters\n            )\n            page.replace_contents(content)\n        return [], []  # type: ignore[return-value]\n\n    def _remove_objects_from_page__clean(\n            self,\n            content: ContentStream,\n            images: list[str],\n            forms: list[str],\n            jump_operators: list[bytes],\n            to_delete: ObjectDeletionFlag,\n            text_filters: Optional[dict[str, Any]] = None,\n    ) -> None:\n        font_id = None\n        font_ids_to_delete = []\n        if text_filters and to_delete & ObjectDeletionFlag.TEXT:\n            font_ids_to_delete = text_filters.get(\"font_ids\", [])\n\n        i = 0\n        while i < len(content.operations):\n            operands, operator = content.operations[i]\n            if operator == b\"Tf\":\n                font_id = operands[0]\n            if (\n                (\n                    operator == b\"INLINE IMAGE\"\n                    and (to_delete & ObjectDeletionFlag.INLINE_IMAGES)\n                )\n                or (operator in jump_operators)\n                or (\n                    operator == b\"Do\"\n                    and (to_delete & ObjectDeletionFlag.XOBJECT_IMAGES)\n                    and (operands[0] in images)\n                )\n            ):\n                if (\n                    not to_delete & ObjectDeletionFlag.TEXT\n                    or (to_delete & ObjectDeletionFlag.TEXT and not text_filters)\n                    or (to_delete & ObjectDeletionFlag.TEXT and font_id in font_ids_to_delete)\n                ):\n                    del content.operations[i]\n                else:\n                    i += 1\n            else:\n                i += 1\n        content.get_data()  # this ensures ._data is rebuilt from the .operations\n\n    def _remove_objects_from_page__clean_forms(\n            self,\n            elt: DictionaryObject,\n            stack: list[DictionaryObject],\n            jump_operators: list[bytes],\n            to_delete: ObjectDeletionFlag,\n            text_filters: Optional[dict[str, Any]] = None,\n    ) -> tuple[list[str], list[str]]:\n        # elt in recursive call is a new ContentStream object, so we have to check the indirect_reference\n        if (elt in stack) or (\n                hasattr(elt, \"indirect_reference\") and any(\n                    elt.indirect_reference == getattr(x, \"indirect_reference\", -1)\n                    for x in stack\n                )\n        ):\n            # to prevent infinite looping\n            return [], []  # pragma: no cover\n        try:\n            d = cast(\n                dict[Any, Any],\n                cast(DictionaryObject, elt[\"/Resources\"])[\"/XObject\"],\n            )\n        except KeyError:\n            d = {}\n        images = []\n        forms = []\n        for k, v in d.items():\n            o = v.get_object()\n            try:\n                content: Any = None\n                if (\n                        to_delete & ObjectDeletionFlag.XOBJECT_IMAGES\n                        and o[\"/Subtype\"] == \"/Image\"\n                ):\n                    content = NullObject()  # to delete the image keeping the entry\n                    images.append(k)\n                if o[\"/Subtype\"] == \"/Form\":\n                    forms.append(k)\n                    if isinstance(o, ContentStream):\n                        content = o\n                    else:\n                        content = ContentStream(o, self)\n                        content.update(\n                            {\n                                k1: v1\n                                for k1, v1 in o.items()\n                                if k1 not in [\"/Length\", \"/Filter\", \"/DecodeParms\"]\n                            }\n                        )\n                        try:\n                            content.indirect_reference = o.indirect_reference\n                        except AttributeError:  # pragma: no cover\n                            pass\n                    stack.append(elt)\n\n                    # clean subforms\n                    self._remove_objects_from_page__clean_forms(\n                        elt=content, stack=stack, jump_operators=jump_operators, to_delete=to_delete,\n                        text_filters=text_filters,\n                    )\n                if content is not None:\n                    if isinstance(v, IndirectObject):\n                        self._objects[v.idnum - 1] = content\n                    else:\n                        # should only occur in a PDF not respecting PDF spec\n                        # where streams must be indirected.\n                        d[k] = self._add_object(content)  # pragma: no cover\n            except (TypeError, KeyError):\n                pass\n        for im in images:\n            del d[im]  # for clean-up\n        if isinstance(elt, StreamObject):  # for /Form\n            if not isinstance(elt, ContentStream):  # pragma: no cover\n                e = ContentStream(elt, self)\n                e.update(elt.items())\n                elt = e\n            # clean the content\n            self._remove_objects_from_page__clean(\n                content=elt, images=images, forms=forms, jump_operators=jump_operators,\n                to_delete=to_delete, text_filters=text_filters\n            )\n        return images, forms\n\n    def remove_images(\n        self,\n        to_delete: ImageType = ImageType.ALL,\n    ) -> None:\n        \"\"\"\n        Remove images from this output.\n\n        Args:\n            to_delete: The type of images to be deleted\n                (default = all images types)\n\n        \"\"\"\n        if isinstance(to_delete, bool):\n            to_delete = ImageType.ALL\n\n        i = ObjectDeletionFlag.NONE\n\n        for image in (\"XOBJECT_IMAGES\", \"INLINE_IMAGES\", \"DRAWING_IMAGES\"):\n            if to_delete & ImageType[image]:\n                i |= ObjectDeletionFlag[image]\n\n        for page in self.pages:\n            self.remove_objects_from_page(page, i)\n\n    def remove_text(self, font_names: Optional[list[str]] = None) -> None:\n        \"\"\"\n        Remove text from the PDF.\n\n        Args:\n            font_names: List of font names to remove, such as \"Helvetica-Bold\".\n                Optional. If not specified, all text will be removed.\n        \"\"\"\n        if not font_names:\n            font_names = []\n\n        for page in self.pages:\n            resource_ids_to_remove = []\n\n            # Content streams reference fonts and other resources with names like \"/F1\" or \"/T1_0\"\n            # Font names need to be converted to resource names/IDs for easier removal\n            if font_names:\n                # Recursively loop through page objects to gather font info\n                def get_font_info(\n                    obj: Any,\n                    font_info: Optional[dict[str, Any]] = None,\n                    key: Optional[str] = None\n                ) -> dict[str, Any]:\n                    if font_info is None:\n                        font_info = {}\n                    if isinstance(obj, IndirectObject):\n                        obj = obj.get_object()\n                    if isinstance(obj, dict):\n                        if obj.get(\"/Type\") == \"/Font\":\n                            font_name = obj.get(\"/BaseFont\", \"\")\n                            # Normalize font names like \"/RRXFFV+Palatino-Bold\" to \"Palatino-Bold\"\n                            normalized_font_name = font_name.lstrip(\"/\").split(\"+\")[-1]\n                            if normalized_font_name not in font_info:\n                                font_info[normalized_font_name] = {\n                                    \"normalized_font_name\": normalized_font_name,\n                                    \"resource_ids\": [],\n                                }\n                            if key not in font_info[normalized_font_name][\"resource_ids\"]:\n                                font_info[normalized_font_name][\"resource_ids\"].append(key)\n                        for k in obj:\n                            font_info = get_font_info(obj[k], font_info, k)\n                    elif isinstance(obj, (list, ArrayObject)):\n                        for child_obj in obj:\n                            font_info = get_font_info(child_obj, font_info)\n                    return font_info\n\n                # Add relevant resource names for removal\n                font_info = get_font_info(page.get(\"/Resources\"))\n                for font_name in font_names:\n                    if font_name in font_info:\n                        resource_ids_to_remove.extend(font_info[font_name][\"resource_ids\"])\n\n            text_filters = {}\n            if font_names:\n                text_filters[\"font_ids\"] = resource_ids_to_remove\n            self.remove_objects_from_page(page, ObjectDeletionFlag.TEXT, text_filters=text_filters)\n\n    def add_uri(\n        self,\n        page_number: int,\n        uri: str,\n        rect: RectangleObject,\n        border: Optional[ArrayObject] = None,\n    ) -> None:\n        \"\"\"\n        Add an URI from a rectangular area to the specified page.\n\n        Args:\n            page_number: index of the page on which to place the URI action.\n            uri: URI of resource to link to.\n            rect: :class:`RectangleObject<pypdf.generic.RectangleObject>` or\n                array of four integers specifying the clickable rectangular area\n                ``[xLL, yLL, xUR, yUR]``, or string in the form\n                ``\"[ xLL yLL xUR yUR ]\"``.\n            border: if provided, an array describing border-drawing\n                properties. See the PDF spec for details. No border will be\n                drawn if this argument is omitted.\n\n        \"\"\"\n        page_link = self.get_object(self._pages)[PagesAttributes.KIDS][page_number]  # type: ignore\n        page_ref = cast(dict[str, Any], self.get_object(page_link))\n\n        border_arr: BorderArrayType\n        if border is not None:\n            border_arr = [NumberObject(n) for n in border[:3]]\n            if len(border) == 4:\n                dash_pattern = ArrayObject([NumberObject(n) for n in border[3]])\n                border_arr.append(dash_pattern)\n        else:\n            border_arr = [NumberObject(2), NumberObject(2), NumberObject(2)]\n\n        if isinstance(rect, str):\n            rect = NumberObject(rect)\n        elif isinstance(rect, RectangleObject):\n            pass\n        else:\n            rect = RectangleObject(rect)\n\n        lnk2 = DictionaryObject()\n        lnk2.update(\n            {\n                NameObject(\"/S\"): NameObject(\"/URI\"),\n                NameObject(\"/URI\"): TextStringObject(uri),\n            }\n        )\n        lnk = DictionaryObject()\n        lnk.update(\n            {\n                NameObject(AA.Type): NameObject(\"/Annot\"),\n                NameObject(AA.Subtype): NameObject(\"/Link\"),\n                NameObject(AA.P): page_link,\n                NameObject(AA.Rect): rect,\n                NameObject(\"/H\"): NameObject(\"/I\"),\n                NameObject(AA.Border): ArrayObject(border_arr),\n                NameObject(\"/A\"): lnk2,\n            }\n        )\n        lnk_ref = self._add_object(lnk)\n\n        if PG.ANNOTS in page_ref:\n            page_ref[PG.ANNOTS].append(lnk_ref)\n        else:\n            page_ref[NameObject(PG.ANNOTS)] = ArrayObject([lnk_ref])\n\n    _valid_layouts = (\n        \"/NoLayout\",\n        \"/SinglePage\",\n        \"/OneColumn\",\n        \"/TwoColumnLeft\",\n        \"/TwoColumnRight\",\n        \"/TwoPageLeft\",\n        \"/TwoPageRight\",\n    )\n\n    def _get_page_layout(self) -> Optional[LayoutType]:\n        try:\n            return cast(LayoutType, self._root_object[\"/PageLayout\"])\n        except KeyError:\n            return None\n\n    def _set_page_layout(self, layout: Union[NameObject, LayoutType]) -> None:\n        \"\"\"\n        Set the page layout.\n\n        Args:\n            layout: The page layout to be used.\n\n        .. list-table:: Valid ``layout`` arguments\n           :widths: 50 200\n\n           * - /NoLayout\n             - Layout explicitly not specified\n           * - /SinglePage\n             - Show one page at a time\n           * - /OneColumn\n             - Show one column at a time\n           * - /TwoColumnLeft\n             - Show pages in two columns, odd-numbered pages on the left\n           * - /TwoColumnRight\n             - Show pages in two columns, odd-numbered pages on the right\n           * - /TwoPageLeft\n             - Show two pages at a time, odd-numbered pages on the left\n           * - /TwoPageRight\n             - Show two pages at a time, odd-numbered pages on the right\n\n        \"\"\"\n        if not isinstance(layout, NameObject):\n            if layout not in self._valid_layouts:\n                logger_warning(\n                    f\"Layout should be one of: {'', ''.join(self._valid_layouts)}\",\n                    __name__,\n                )\n            layout = NameObject(layout)\n        self._root_object.update({NameObject(\"/PageLayout\"): layout})\n\n    def set_page_layout(self, layout: LayoutType) -> None:\n        \"\"\"\n        Set the page layout.\n\n        Args:\n            layout: The page layout to be used\n\n        .. list-table:: Valid ``layout`` arguments\n           :widths: 50 200\n\n           * - /NoLayout\n             - Layout explicitly not specified\n           * - /SinglePage\n             - Show one page at a time\n           * - /OneColumn\n             - Show one column at a time\n           * - /TwoColumnLeft\n             - Show pages in two columns, odd-numbered pages on the left\n           * - /TwoColumnRight\n             - Show pages in two columns, odd-numbered pages on the right\n           * - /TwoPageLeft\n             - Show two pages at a time, odd-numbered pages on the left\n           * - /TwoPageRight\n             - Show two pages at a time, odd-numbered pages on the right\n\n        \"\"\"\n        self._set_page_layout(layout)\n\n    @property\n    def page_layout(self) -> Optional[LayoutType]:\n        \"\"\"\n        Page layout property.\n\n        .. list-table:: Valid ``layout`` values\n           :widths: 50 200\n\n           * - /NoLayout\n             - Layout explicitly not specified\n           * - /SinglePage\n             - Show one page at a time\n           * - /OneColumn\n             - Show one column at a time\n           * - /TwoColumnLeft\n             - Show pages in two columns, odd-numbered pages on the left\n           * - /TwoColumnRight\n             - Show pages in two columns, odd-numbered pages on the right\n           * - /TwoPageLeft\n             - Show two pages at a time, odd-numbered pages on the left\n           * - /TwoPageRight\n             - Show two pages at a time, odd-numbered pages on the right\n        \"\"\"\n        return self._get_page_layout()\n\n    @page_layout.setter\n    def page_layout(self, layout: LayoutType) -> None:\n        self._set_page_layout(layout)\n\n    _valid_modes = (\n        \"/UseNone\",\n        \"/UseOutlines\",\n        \"/UseThumbs\",\n        \"/FullScreen\",\n        \"/UseOC\",\n        \"/UseAttachments\",\n    )\n\n    def _get_page_mode(self) -> Optional[PagemodeType]:\n        try:\n            return cast(PagemodeType, self._root_object[\"/PageMode\"])\n        except KeyError:\n            return None\n\n    @property\n    def page_mode(self) -> Optional[PagemodeType]:\n        \"\"\"\n        Page mode property.\n\n        .. list-table:: Valid ``mode`` values\n           :widths: 50 200\n\n           * - /UseNone\n             - Do not show outline or thumbnails panels\n           * - /UseOutlines\n             - Show outline (aka bookmarks) panel\n           * - /UseThumbs\n             - Show page thumbnails panel\n           * - /FullScreen\n             - Fullscreen view\n           * - /UseOC\n             - Show Optional Content Group (OCG) panel\n           * - /UseAttachments\n             - Show attachments panel\n        \"\"\"\n        return self._get_page_mode()\n\n    @page_mode.setter\n    def page_mode(self, mode: PagemodeType) -> None:\n        if isinstance(mode, NameObject):\n            mode_name: NameObject = mode\n        else:\n            if mode not in self._valid_modes:\n                logger_warning(\n                    f\"Mode should be one of: {', '.join(self._valid_modes)}\", __name__\n                )\n            mode_name = NameObject(mode)\n        self._root_object.update({NameObject(\"/PageMode\"): mode_name})\n\n    def add_annotation(\n        self,\n        page_number: Union[int, PageObject],\n        annotation: dict[str, Any],\n    ) -> DictionaryObject:\n        \"\"\"\n        Add a single annotation to the page.\n        The added annotation must be a new annotation.\n        It cannot be recycled.\n\n        Args:\n            page_number: PageObject or page index.\n            annotation: Annotation to be added (created with annotation).\n\n        Returns:\n            The inserted object.\n            This can be used for popup creation, for example.\n\n        \"\"\"\n        page = page_number\n        if isinstance(page, int):\n            page = self.pages[page]\n        elif not isinstance(page, PageObject):\n            raise TypeError(\"page: invalid type\")\n\n        to_add = cast(DictionaryObject, _pdf_objectify(annotation))\n        to_add[NameObject(\"/P\")] = page.indirect_reference\n\n        if page.annotations is None:\n            page[NameObject(\"/Annots\")] = ArrayObject()\n        assert page.annotations is not None\n\n        # Internal link annotations need the correct object type for the\n        # destination\n        if to_add.get(\"/Subtype\") == \"/Link\" and \"/Dest\" in to_add:\n            tmp = cast(dict[Any, Any], to_add[NameObject(\"/Dest\")])\n            dest = Destination(\n                NameObject(\"/LinkName\"),\n                tmp[\"target_page_index\"],\n                Fit(\n                    fit_type=tmp[\"fit\"], fit_args=dict(tmp)[\"fit_args\"]\n                ),  # I have no clue why this dict-hack is necessary\n            )\n            to_add[NameObject(\"/Dest\")] = dest.dest_array\n\n        page.annotations.append(self._add_object(to_add))\n\n        if to_add.get(\"/Subtype\") == \"/Popup\" and NameObject(\"/Parent\") in to_add:\n            cast(DictionaryObject, to_add[\"/Parent\"].get_object())[\n                NameObject(\"/Popup\")\n            ] = to_add.indirect_reference\n\n        return to_add\n\n    def clean_page(self, page: Union[PageObject, IndirectObject]) -> PageObject:\n        \"\"\"\n        Perform some clean up in the page.\n        Currently: convert NameObject named destination to TextStringObject\n        (required for names/dests list)\n\n        Args:\n            page:\n\n        Returns:\n            The cleaned PageObject\n\n        \"\"\"\n        page = cast(\"PageObject\", page.get_object())\n        for a in page.get(\"/Annots\", []):\n            a_obj = a.get_object()\n            d = a_obj.get(\"/Dest\", None)\n            act = a_obj.get(\"/A\", None)\n            if isinstance(d, NameObject):\n                a_obj[NameObject(\"/Dest\")] = TextStringObject(d)\n            elif act is not None:\n                act = act.get_object()\n                d = act.get(\"/D\", None)\n                if isinstance(d, NameObject):\n                    act[NameObject(\"/D\")] = TextStringObject(d)\n        return page\n\n    def _create_stream(\n        self, fileobj: Union[Path, StrByteType, PdfReader]\n    ) -> tuple[IOBase, Optional[Encryption]]:\n        # If the fileobj parameter is a string, assume it is a path\n        # and create a file object at that location. If it is a file,\n        # copy the file's contents into a BytesIO stream object; if\n        # it is a PdfReader, copy that reader's stream into a\n        # BytesIO stream.\n        # If fileobj is none of the above types, it is not modified\n        encryption_obj = None\n        stream: IOBase\n        if isinstance(fileobj, (str, Path)):\n            with FileIO(fileobj, \"rb\") as f:\n                stream = BytesIO(f.read())\n        elif isinstance(fileobj, PdfReader):\n            if fileobj._encryption:\n                encryption_obj = fileobj._encryption\n            orig_tell = fileobj.stream.tell()\n            fileobj.stream.seek(0)\n            stream = BytesIO(fileobj.stream.read())\n\n            # reset the stream to its original location\n            fileobj.stream.seek(orig_tell)\n        elif hasattr(fileobj, \"seek\") and hasattr(fileobj, \"read\"):\n            fileobj.seek(0)\n            filecontent = fileobj.read()\n            stream = BytesIO(filecontent)\n        else:\n            raise NotImplementedError(\n                \"Merging requires an object that PdfReader can parse. \"\n                \"Typically, that is a Path or a string representing a Path, \"\n                \"a file object, or an object implementing .seek and .read. \"\n                \"Passing a PdfReader directly works as well.\"\n            )\n        return stream, encryption_obj\n\n    def append(\n        self,\n        fileobj: Union[StrByteType, PdfReader, Path],\n        outline_item: Union[\n            str, None, PageRange, tuple[int, int], tuple[int, int, int], list[int]\n        ] = None,\n        pages: Union[\n            None,\n            PageRange,\n            tuple[int, int],\n            tuple[int, int, int],\n            list[int],\n            list[PageObject],\n        ] = None,\n        import_outline: bool = True,\n        excluded_fields: Optional[Union[list[str], tuple[str, ...]]] = None,\n    ) -> None:\n        \"\"\"\n        Identical to the :meth:`merge()<merge>` method, but assumes you want to\n        concatenate all pages onto the end of the file instead of specifying a\n        position.\n\n        Args:\n            fileobj: A File Object or an object that supports the standard\n                read and seek methods similar to a File Object. Could also be a\n                string representing a path to a PDF file.\n            outline_item: Optionally, you may specify a string to build an\n                outline (aka 'bookmark') to identify the beginning of the\n                included file.\n            pages: Can be a :class:`PageRange<pypdf.pagerange.PageRange>`\n                or a ``(start, stop[, step])`` tuple\n                or a list of pages to be processed\n                to merge only the specified range of pages from the source\n                document into the output document.\n            import_outline: You may prevent the source document's\n                outline (collection of outline items, previously referred to as\n                'bookmarks') from being imported by specifying this as ``False``.\n            excluded_fields: Provide the list of fields/keys to be ignored\n                if ``/Annots`` is part of the list, the annotation will be ignored\n                if ``/B`` is part of the list, the articles will be ignored\n\n        \"\"\"\n        if excluded_fields is None:\n            excluded_fields = ()\n        if isinstance(outline_item, (tuple, list, PageRange)):\n            if isinstance(pages, bool):\n                if not isinstance(import_outline, bool):\n                    excluded_fields = import_outline\n                import_outline = pages\n            pages = outline_item\n            self.merge(\n                None,\n                fileobj,\n                None,\n                pages,\n                import_outline,\n                excluded_fields,\n            )\n        else:  # if isinstance(outline_item, str):\n            self.merge(\n                None,\n                fileobj,\n                outline_item,\n                pages,\n                import_outline,\n                excluded_fields,\n            )\n\n    def merge(\n        self,\n        position: Optional[int],\n        fileobj: Union[Path, StrByteType, PdfReader],\n        outline_item: Optional[str] = None,\n        pages: Optional[Union[PageRangeSpec, list[PageObject]]] = None,\n        import_outline: bool = True,\n        excluded_fields: Optional[Union[list[str], tuple[str, ...]]] = (),\n    ) -> None:\n        \"\"\"\n        Merge the pages from the given file into the output file at the\n        specified page number.\n\n        Args:\n            position: The *page number* to insert this file. File will\n                be inserted after the given number.\n            fileobj: A File Object or an object that supports the standard\n                read and seek methods similar to a File Object. Could also be a\n                string representing a path to a PDF file.\n            outline_item: Optionally, you may specify a string to build an outline\n                (aka 'bookmark') to identify the\n                beginning of the included file.\n            pages: can be a :class:`PageRange<pypdf.pagerange.PageRange>`\n                or a ``(start, stop[, step])`` tuple\n                or a list of pages to be processed\n                to merge only the specified range of pages from the source\n                document into the output document.\n            import_outline: You may prevent the source document's\n                outline (collection of outline items, previously referred to as\n                'bookmarks') from being imported by specifying this as ``False``.\n            excluded_fields: provide the list of fields/keys to be ignored\n                if ``/Annots`` is part of the list, the annotation will be ignored\n                if ``/B`` is part of the list, the articles will be ignored\n\n        Raises:\n            TypeError: The pages attribute is not configured properly\n\n        \"\"\"\n        if isinstance(fileobj, PdfDocCommon):\n            reader = fileobj\n        else:\n            stream, _encryption_obj = self._create_stream(fileobj)\n            # Create a new PdfReader instance using the stream\n            # (either file or BytesIO or StringIO) created above\n            reader = PdfReader(stream, strict=False)  # type: ignore[arg-type]\n\n        if excluded_fields is None:\n            excluded_fields = ()\n        # Find the range of pages to merge.\n        if pages is None:\n            pages = list(range(len(reader.pages)))\n        elif isinstance(pages, PageRange):\n            pages = list(range(*pages.indices(len(reader.pages))))\n        elif isinstance(pages, list):\n            pass  # keep unchanged\n        elif isinstance(pages, tuple) and len(pages) <= 3:\n            pages = list(range(*pages))\n        elif not isinstance(pages, tuple):\n            raise TypeError(\n                '\"pages\" must be a tuple of (start, stop[, step]) or a list'\n            )\n\n        srcpages = {}\n        for page in pages:\n            if isinstance(page, PageObject):\n                pg = page\n            else:\n                pg = reader.pages[page]\n            assert pg.indirect_reference is not None\n            if position is None:\n                # numbers in the exclude list identifies that the exclusion is\n                # only applicable to 1st level of cloning\n                srcpages[pg.indirect_reference.idnum] = self.add_page(\n                    pg, [*list(excluded_fields), 1, \"/B\", 1, \"/Annots\"]  # type: ignore\n                )\n            else:\n                srcpages[pg.indirect_reference.idnum] = self.insert_page(\n                    pg, position, [*list(excluded_fields), 1, \"/B\", 1, \"/Annots\"]  # type: ignore\n                )\n                position += 1\n            srcpages[pg.indirect_reference.idnum].original_page = pg\n\n        reader._named_destinations = (\n            reader.named_destinations\n        )  # need for the outline processing below\n\n        arr: Any\n\n        for dest in reader._named_destinations.values():\n            self._merge__process_named_dests(dest=dest, reader=reader, srcpages=srcpages)\n\n        outline_item_typ: TreeObject\n        if outline_item is not None:\n            outline_item_typ = cast(\n                \"TreeObject\",\n                self.add_outline_item(\n                    TextStringObject(outline_item),\n                    next(iter(srcpages.values())).indirect_reference,\n                    fit=PAGE_FIT,\n                ).get_object(),\n            )\n        else:\n            outline_item_typ = self.get_outline_root()\n\n        _ro = reader.root_object\n        if import_outline and CO.OUTLINES in _ro:\n            outline = self._get_filtered_outline(\n                _ro.get(CO.OUTLINES, None), srcpages, reader\n            )\n            self._insert_filtered_outline(\n                outline, outline_item_typ, None\n            )  # TODO: use before parameter\n\n        if \"/Annots\" not in excluded_fields:\n            for pag in srcpages.values():\n                lst = self._insert_filtered_annotations(\n                    pag.original_page.get(\"/Annots\", []), pag, srcpages, reader\n                )\n                if len(lst) > 0:\n                    pag[NameObject(\"/Annots\")] = lst\n                self.clean_page(pag)\n\n        if \"/AcroForm\" in _ro and not is_null_or_none(_ro[\"/AcroForm\"]):\n            if \"/AcroForm\" not in self._root_object:\n                self._root_object[NameObject(\"/AcroForm\")] = self._add_object(\n                    cast(\n                        DictionaryObject,\n                        reader.root_object[\"/AcroForm\"],\n                    ).clone(self, False, (\"/Fields\",))\n                )\n                arr = ArrayObject()\n            else:\n                arr = cast(\n                    ArrayObject,\n                    cast(DictionaryObject, self._root_object[\"/AcroForm\"])[\"/Fields\"],\n                )\n            trslat = self._id_translated[id(reader)]\n            try:\n                for f in reader.root_object[\"/AcroForm\"][\"/Fields\"]:  # type: ignore\n                    try:\n                        ind = IndirectObject(trslat[f.idnum], 0, self)\n                        if ind not in arr:\n                            arr.append(ind)\n                    except KeyError:\n                        # for trslat[] which mean the field has not be copied\n                        # through the page\n                        pass\n            except KeyError:  # for /Acroform or /Fields are not existing\n                arr = self._add_object(ArrayObject())\n            cast(DictionaryObject, self._root_object[\"/AcroForm\"])[\n                NameObject(\"/Fields\")\n            ] = arr\n\n        if \"/B\" not in excluded_fields:\n            self.add_filtered_articles(\"\", srcpages, reader)\n\n    def _merge__process_named_dests(self, dest: Any, reader: PdfDocCommon, srcpages: dict[int, PageObject]) -> None:\n        arr: Any = dest.dest_array\n        if \"/Names\" in self._root_object and dest[\"/Title\"] in cast(\n            list[Any],\n            cast(\n                DictionaryObject,\n                cast(DictionaryObject, self._root_object[\"/Names\"]).get(\"/Dests\", DictionaryObject()),\n            ).get(\"/Names\", DictionaryObject()),\n        ):\n            # already exists: should not duplicate it\n            pass\n        elif dest[\"/Page\"] is None or isinstance(dest[\"/Page\"], NullObject):\n            pass\n        elif isinstance(dest[\"/Page\"], int):\n            # the page reference is a page number normally not a PDF Reference\n            # page numbers as int are normally accepted only in external goto\n            try:\n                p = reader.pages[dest[\"/Page\"]]\n            except IndexError:\n                return\n            assert p.indirect_reference is not None\n            try:\n                arr[NumberObject(0)] = NumberObject(\n                    srcpages[p.indirect_reference.idnum].page_number\n                )\n                self.add_named_destination_array(dest[\"/Title\"], arr)\n            except KeyError:\n                pass\n        elif dest[\"/Page\"].indirect_reference.idnum in srcpages:\n            arr[NumberObject(0)] = srcpages[\n                dest[\"/Page\"].indirect_reference.idnum\n            ].indirect_reference\n            self.add_named_destination_array(dest[\"/Title\"], arr)\n\n    def _add_articles_thread(\n        self,\n        thread: DictionaryObject,  # thread entry from the reader's array of threads\n        pages: dict[int, PageObject],\n        reader: PdfReader,\n    ) -> IndirectObject:\n        \"\"\"\n        Clone the thread with only the applicable articles.\n\n        Args:\n            thread:\n            pages:\n            reader:\n\n        Returns:\n            The added thread as an indirect reference\n\n        \"\"\"\n        nthread = thread.clone(\n            self, force_duplicate=True, ignore_fields=(\"/F\",)\n        )  # use of clone to keep link between reader and writer\n        self.threads.append(nthread.indirect_reference)\n        first_article = cast(\"DictionaryObject\", thread[\"/F\"])\n        current_article: Optional[DictionaryObject] = first_article\n        new_article: Optional[DictionaryObject] = None\n        while current_article is not None:\n            pag = self._get_cloned_page(\n                cast(\"PageObject\", current_article[\"/P\"]), pages, reader\n            )\n            if pag is not None:\n                if new_article is None:\n                    new_article = cast(\n                        \"DictionaryObject\",\n                        self._add_object(DictionaryObject()).get_object(),\n                    )\n                    new_first = new_article\n                    nthread[NameObject(\"/F\")] = new_article.indirect_reference\n                else:\n                    new_article2 = cast(\n                        \"DictionaryObject\",\n                        self._add_object(\n                            DictionaryObject(\n                                {NameObject(\"/V\"): new_article.indirect_reference}\n                            )\n                        ).get_object(),\n                    )\n                    new_article[NameObject(\"/N\")] = new_article2.indirect_reference\n                    new_article = new_article2\n                new_article[NameObject(\"/P\")] = pag\n                new_article[NameObject(\"/T\")] = nthread.indirect_reference\n                new_article[NameObject(\"/R\")] = current_article[\"/R\"]\n                pag_obj = cast(\"PageObject\", pag.get_object())\n                if \"/B\" not in pag_obj:\n                    pag_obj[NameObject(\"/B\")] = ArrayObject()\n                cast(\"ArrayObject\", pag_obj[\"/B\"]).append(\n                    new_article.indirect_reference\n                )\n            current_article = cast(\"DictionaryObject\", current_article[\"/N\"])\n            if current_article == first_article:\n                new_article[NameObject(\"/N\")] = new_first.indirect_reference  # type: ignore\n                new_first[NameObject(\"/V\")] = new_article.indirect_reference  # type: ignore\n                current_article = None\n        assert nthread.indirect_reference is not None\n        return nthread.indirect_reference\n\n    def add_filtered_articles(\n        self,\n        fltr: Union[\n            Pattern[Any], str\n        ],  # thread entry from the reader's array of threads\n        pages: dict[int, PageObject],\n        reader: PdfReader,\n    ) -> None:\n        \"\"\"\n        Add articles matching the defined criteria.\n\n        Args:\n            fltr:\n            pages:\n            reader:\n\n        \"\"\"\n        if isinstance(fltr, str):\n            fltr = re.compile(fltr)\n        elif not isinstance(fltr, Pattern):\n            fltr = re.compile(\"\")\n        for p in pages.values():\n            pp = p.original_page\n            for a in pp.get(\"/B\", ()):\n                a_obj = a.get_object()\n                if is_null_or_none(a_obj):\n                    continue\n                thr = a_obj.get(\"/T\")\n                if thr is None:\n                    continue\n                thr = thr.get_object()\n                if thr.indirect_reference.idnum not in self._id_translated[\n                    id(reader)\n                ] and fltr.search((thr.get(\"/I\", {})).get(\"/Title\", \"\")):\n                    self._add_articles_thread(thr, pages, reader)\n\n    def _get_cloned_page(\n        self,\n        page: Union[None, IndirectObject, PageObject, NullObject],\n        pages: dict[int, PageObject],\n        reader: PdfReader,\n    ) -> Optional[IndirectObject]:\n        if isinstance(page, NullObject):\n            return None\n        if isinstance(page, DictionaryObject) and page.get(\"/Type\", \"\") == \"/Page\":\n            _i = page.indirect_reference\n        elif isinstance(page, IndirectObject):\n            _i = page\n        try:\n            return pages[_i.idnum].indirect_reference  # type: ignore\n        except Exception:\n            return None\n\n    def _insert_filtered_annotations(\n        self,\n        annots: Union[IndirectObject, list[DictionaryObject], None],\n        page: PageObject,\n        pages: dict[int, PageObject],\n        reader: PdfReader,\n    ) -> list[Destination]:\n        outlist = ArrayObject()\n        if isinstance(annots, IndirectObject):\n            annots = cast(\"list[Any]\", annots.get_object())\n        if annots is None:\n            return outlist\n        if not isinstance(annots, list):\n            logger_warning(f\"Expected list of annotations, got {annots} of type {annots.__class__.__name__}.\", __name__)\n            return outlist\n        for an in annots:\n            ano = cast(\"DictionaryObject\", an.get_object())\n            if (\n                ano[\"/Subtype\"] != \"/Link\"\n                or \"/A\" not in ano\n                or cast(\"DictionaryObject\", ano[\"/A\"])[\"/S\"] != \"/GoTo\"\n                or \"/Dest\" in ano\n            ):\n                if \"/Dest\" not in ano:\n                    outlist.append(self._add_object(ano.clone(self)))\n                else:\n                    d = ano[\"/Dest\"]\n                    if isinstance(d, str):\n                        # it is a named dest\n                        if str(d) in self.get_named_dest_root():\n                            outlist.append(ano.clone(self).indirect_reference)\n                    else:\n                        d = cast(\"ArrayObject\", d)\n                        p = self._get_cloned_page(d[0], pages, reader)\n                        if p is not None:\n                            anc = ano.clone(self, ignore_fields=(\"/Dest\",))\n                            anc[NameObject(\"/Dest\")] = ArrayObject([p, *d[1:]])\n                            outlist.append(self._add_object(anc))\n            else:\n                d = cast(\"DictionaryObject\", ano[\"/A\"]).get(\"/D\", NullObject())\n                if is_null_or_none(d):\n                    continue\n                if isinstance(d, str):\n                    # it is a named dest\n                    if str(d) in self.get_named_dest_root():\n                        outlist.append(ano.clone(self).indirect_reference)\n                else:\n                    d = cast(\"ArrayObject\", d)\n                    p = self._get_cloned_page(d[0], pages, reader)\n                    if p is not None:\n                        anc = ano.clone(self, ignore_fields=(\"/D\",))\n                        cast(\"DictionaryObject\", anc[\"/A\"])[\n                            NameObject(\"/D\")\n                        ] = ArrayObject([p, *d[1:]])\n                        outlist.append(self._add_object(anc))\n        return outlist\n\n    def _get_filtered_outline(\n        self,\n        node: Any,\n        pages: dict[int, PageObject],\n        reader: PdfReader,\n    ) -> list[Destination]:\n        \"\"\"\n        Extract outline item entries that are part of the specified page set.\n\n        Args:\n            node:\n            pages:\n            reader:\n\n        Returns:\n            A list of destination objects.\n\n        \"\"\"\n        new_outline = []\n        if node is None:\n            node = NullObject()\n        node = node.get_object()\n        if is_null_or_none(node):\n            node = DictionaryObject()\n        if node.get(\"/Type\", \"\") == \"/Outlines\" or \"/Title\" not in node:\n            node = node.get(\"/First\", None)\n            if node is not None:\n                node = node.get_object()\n                new_outline += self._get_filtered_outline(node, pages, reader)\n        else:\n            v: Union[None, IndirectObject, NullObject]\n            while node is not None:\n                node = node.get_object()\n                o = cast(\"Destination\", reader._build_outline_item(node))\n                v = self._get_cloned_page(cast(\"PageObject\", o[\"/Page\"]), pages, reader)\n                if v is None:\n                    v = NullObject()\n                o[NameObject(\"/Page\")] = v\n                if \"/First\" in node:\n                    o._filtered_children = self._get_filtered_outline(\n                        node[\"/First\"], pages, reader\n                    )\n                else:\n                    o._filtered_children = []\n                if (\n                    not isinstance(o[\"/Page\"], NullObject)\n                    or len(o._filtered_children) > 0\n                ):\n                    new_outline.append(o)\n                node = node.get(\"/Next\", None)\n        return new_outline\n\n    def _clone_outline(self, dest: Destination) -> TreeObject:\n        n_ol = TreeObject()\n        self._add_object(n_ol)\n        n_ol[NameObject(\"/Title\")] = TextStringObject(dest[\"/Title\"])\n        if not isinstance(dest[\"/Page\"], NullObject):\n            if dest.node is not None and \"/A\" in dest.node:\n                n_ol[NameObject(\"/A\")] = dest.node[\"/A\"].clone(self)\n            else:\n                n_ol[NameObject(\"/Dest\")] = dest.dest_array\n        # TODO: /SE\n        if dest.node is not None:\n            n_ol[NameObject(\"/F\")] = NumberObject(dest.node.get(\"/F\", 0))\n            n_ol[NameObject(\"/C\")] = ArrayObject(\n                dest.node.get(\n                    \"/C\", [FloatObject(0.0), FloatObject(0.0), FloatObject(0.0)]\n                )\n            )\n        return n_ol\n\n    def _insert_filtered_outline(\n        self,\n        outlines: list[Destination],\n        parent: Union[TreeObject, IndirectObject],\n        before: Union[None, TreeObject, IndirectObject] = None,\n    ) -> None:\n        for dest in outlines:\n            # TODO: can be improved to keep A and SE entries (ignored for the moment)\n            # with np=self.add_outline_item_destination(dest,parent,before)\n            if dest.get(\"/Type\", \"\") == \"/Outlines\" or \"/Title\" not in dest:\n                np = parent\n            else:\n                np = self._clone_outline(dest)\n                cast(TreeObject, parent.get_object()).insert_child(np, before, self)\n            self._insert_filtered_outline(dest._filtered_children, np, None)\n\n    def close(self) -> None:\n        \"\"\"Implemented for API harmonization.\"\"\"\n        return\n\n    def find_outline_item(\n        self,\n        outline_item: dict[str, Any],\n        root: Optional[OutlineType] = None,\n    ) -> Optional[list[int]]:\n        if root is None:\n            o = self.get_outline_root()\n        else:\n            o = cast(\"TreeObject\", root)\n\n        i = 0\n        while o is not None:\n            if (\n                o.indirect_reference == outline_item\n                or o.get(\"/Title\", None) == outline_item\n            ):\n                return [i]\n            if \"/First\" in o:\n                res = self.find_outline_item(\n                    outline_item, cast(OutlineType, o[\"/First\"])\n                )\n                if res:\n                    return ([i] if \"/Title\" in o else []) + res\n            if \"/Next\" in o:\n                i += 1\n                o = cast(TreeObject, o[\"/Next\"])\n            else:\n                return None\n        raise PyPdfError(\"This line is theoretically unreachable.\")  # pragma: no cover\n\n    def reset_translation(\n        self, reader: Union[None, PdfReader, IndirectObject] = None\n    ) -> None:\n        \"\"\"\n        Reset the translation table between reader and the writer object.\n\n        Late cloning will create new independent objects.\n\n        Args:\n            reader: PdfReader or IndirectObject referencing a PdfReader object.\n                if set to None or omitted, all tables will be reset.\n\n        \"\"\"\n        if reader is None:\n            self._id_translated = {}\n        elif isinstance(reader, PdfReader):\n            try:\n                del self._id_translated[id(reader)]\n            except Exception:\n                pass\n        elif isinstance(reader, IndirectObject):\n            try:\n                del self._id_translated[id(reader.pdf)]\n            except Exception:\n                pass\n        else:\n            raise Exception(\"invalid parameter {reader}\")\n\n    def set_page_label(\n        self,\n        page_index_from: int,\n        page_index_to: int,\n        style: Optional[PageLabelStyle] = None,\n        prefix: Optional[str] = None,\n        start: Optional[int] = 0,\n    ) -> None:\n        \"\"\"\n        Set a page label to a range of pages.\n\n        Page indexes must be given starting from 0.\n        Labels must have a style, a prefix or both.\n        If a range is not assigned any page label, a decimal label starting from 1 is applied.\n\n        Args:\n            page_index_from: page index of the beginning of the range starting from 0\n            page_index_to: page index of the beginning of the range starting from 0\n            style: The numbering style to be used for the numeric portion of each page label:\n\n                       * ``/D`` Decimal Arabic numerals\n                       * ``/R`` Uppercase Roman numerals\n                       * ``/r`` Lowercase Roman numerals\n                       * ``/A`` Uppercase letters (A to Z for the first 26 pages,\n                         AA to ZZ for the next 26, and so on)\n                       * ``/a`` Lowercase letters (a to z for the first 26 pages,\n                         aa to zz for the next 26, and so on)\n\n            prefix: The label prefix for page labels in this range.\n            start:  The value of the numeric portion for the first page label\n                    in the range.\n                    Subsequent pages are numbered sequentially from this value,\n                    which must be greater than or equal to 1.\n                    Default value: 1.\n\n        \"\"\"\n        if style is None and prefix is None:\n            raise ValueError(\"At least one of style and prefix must be given\")\n        if page_index_from < 0:\n            raise ValueError(\"page_index_from must be greater or equal than 0\")\n        if page_index_to < page_index_from:\n            raise ValueError(\n                \"page_index_to must be greater or equal than page_index_from\"\n            )\n        if page_index_to >= len(self.pages):\n            raise ValueError(\"page_index_to exceeds number of pages\")\n        if start is not None and start != 0 and start < 1:\n            raise ValueError(\"If given, start must be greater or equal than one\")\n\n        self._set_page_label(page_index_from, page_index_to, style, prefix, start)\n\n    def _set_page_label(\n        self,\n        page_index_from: int,\n        page_index_to: int,\n        style: Optional[PageLabelStyle] = None,\n        prefix: Optional[str] = None,\n        start: Optional[int] = 0,\n    ) -> None:\n        \"\"\"\n        Set a page label to a range of pages.\n\n        Page indexes must be given starting from 0.\n        Labels must have a style, a prefix or both.\n        If a range is not assigned any page label a decimal label starting from 1 is applied.\n\n        Args:\n            page_index_from: page index of the beginning of the range starting from 0\n            page_index_to: page index of the beginning of the range starting from 0\n            style:  The numbering style to be used for the numeric portion of each page label:\n                        /D Decimal Arabic numerals\n                        /R Uppercase Roman numerals\n                        /r Lowercase Roman numerals\n                        /A Uppercase letters (A to Z for the first 26 pages,\n                           AA to ZZ for the next 26, and so on)\n                        /a Lowercase letters (a to z for the first 26 pages,\n                           aa to zz for the next 26, and so on)\n            prefix: The label prefix for page labels in this range.\n            start:  The value of the numeric portion for the first page label\n                    in the range.\n                    Subsequent pages are numbered sequentially from this value,\n                    which must be greater than or equal to 1. Default value: 1.\n\n        \"\"\"\n        default_page_label = DictionaryObject()\n        default_page_label[NameObject(\"/S\")] = NameObject(\"/D\")\n\n        new_page_label = DictionaryObject()\n        if style is not None:\n            new_page_label[NameObject(\"/S\")] = NameObject(style)\n        if prefix is not None:\n            new_page_label[NameObject(\"/P\")] = TextStringObject(prefix)\n        if start != 0:\n            new_page_label[NameObject(\"/St\")] = NumberObject(start)\n\n        if NameObject(CatalogDictionary.PAGE_LABELS) not in self._root_object:\n            nums = ArrayObject()\n            nums_insert(NumberObject(0), default_page_label, nums)\n            page_labels = TreeObject()\n            page_labels[NameObject(\"/Nums\")] = nums\n            self._root_object[NameObject(CatalogDictionary.PAGE_LABELS)] = page_labels\n\n        page_labels = cast(\n            TreeObject, self._root_object[NameObject(CatalogDictionary.PAGE_LABELS)]\n        )\n        nums = cast(ArrayObject, page_labels[NameObject(\"/Nums\")])\n\n        nums_insert(NumberObject(page_index_from), new_page_label, nums)\n        nums_clear_range(NumberObject(page_index_from), page_index_to, nums)\n        next_label_pos, *_ = nums_next(NumberObject(page_index_from), nums)\n        if next_label_pos != page_index_to + 1 and page_index_to + 1 < len(self.pages):\n            nums_insert(NumberObject(page_index_to + 1), default_page_label, nums)\n\n        page_labels[NameObject(\"/Nums\")] = nums\n        self._root_object[NameObject(CatalogDictionary.PAGE_LABELS)] = page_labels\n\n    def _repr_mimebundle_(\n        self,\n        include: Union[None, Iterable[str]] = None,\n        exclude: Union[None, Iterable[str]] = None,\n    ) -> dict[str, Any]:\n        \"\"\"\n        Integration into Jupyter Notebooks.\n\n        This method returns a dictionary that maps a mime-type to its\n        representation.\n\n        .. seealso::\n\n           https://ipython.readthedocs.io/en/stable/config/integrating.html\n        \"\"\"\n        pdf_data = BytesIO()\n        self.write(pdf_data)\n        data = {\n            \"application/pdf\": pdf_data,\n        }\n\n        if include is not None:\n            # Filter representations based on include list\n            data = {k: v for k, v in data.items() if k in include}\n\n        if exclude is not None:\n            # Remove representations based on exclude list\n            data = {k: v for k, v in data.items() if k not in exclude}\n\n        return data\n\n\ndef _pdf_objectify(obj: Union[dict[str, Any], str, float, list[Any]]) -> PdfObject:\n    if isinstance(obj, PdfObject):\n        return obj\n    if isinstance(obj, dict):\n        to_add = DictionaryObject()\n        for key, value in obj.items():\n            to_add[NameObject(key)] = _pdf_objectify(value)\n        return to_add\n    if isinstance(obj, str):\n        if obj.startswith(\"/\"):\n            return NameObject(obj)\n        return TextStringObject(obj)\n    if isinstance(obj, (float, int)):\n        return FloatObject(obj)\n    if isinstance(obj, list):\n        return ArrayObject(_pdf_objectify(i) for i in obj)\n    raise NotImplementedError(\n        f\"{type(obj)=} could not be cast to a PdfObject\"\n    )\n\n\ndef _create_outline_item(\n    action_ref: Union[None, IndirectObject],\n    title: str,\n    color: Union[tuple[float, float, float], str, None],\n    italic: bool,\n    bold: bool,\n) -> TreeObject:\n    outline_item = TreeObject()\n    if action_ref is not None:\n        outline_item[NameObject(\"/A\")] = action_ref\n    outline_item.update(\n        {\n            NameObject(\"/Title\"): create_string_object(title),\n        }\n    )\n    if color:\n        if isinstance(color, str):\n            color = hex_to_rgb(color)\n        outline_item.update(\n            {NameObject(\"/C\"): ArrayObject([FloatObject(c) for c in color])}\n        )\n    if italic or bold:\n        format_flag = 0\n        if italic:\n            format_flag += OutlineFontFlag.italic\n        if bold:\n            format_flag += OutlineFontFlag.bold\n        outline_item.update({NameObject(\"/F\"): NumberObject(format_flag)})\n    return outline_item\n"
  },
  {
    "path": "pypdf/annotations/__init__.py",
    "content": "\"\"\"\nPDF specifies several annotation types which pypdf makes available here.\n\nThe names of the annotations and their attributes do not reflect the names in\nthe specification in all cases. For example, the PDF standard defines a\n'Square' annotation that does not actually need to be square. For this reason,\npypdf calls it 'Rectangle'.\n\nAt their core, all annotation types are DictionaryObjects. That means if pypdf\ndoes not implement a feature, users can easily extend the given functionality.\n\"\"\"\n\n\nfrom ._base import NO_FLAGS, AnnotationDictionary\nfrom ._markup_annotations import (\n    Ellipse,\n    FreeText,\n    Highlight,\n    Line,\n    MarkupAnnotation,\n    Polygon,\n    PolyLine,\n    Rectangle,\n    Text,\n)\nfrom ._non_markup_annotations import Link, Popup\n\n__all__ = [\n    \"NO_FLAGS\",\n    \"AnnotationDictionary\",\n    \"Ellipse\",\n    \"FreeText\",\n    \"Highlight\",\n    \"Line\",\n    \"Link\",\n    \"MarkupAnnotation\",\n    \"PolyLine\",\n    \"Polygon\",\n    \"Popup\",\n    \"Rectangle\",\n    \"Text\",\n]\n"
  },
  {
    "path": "pypdf/annotations/_base.py",
    "content": "from abc import ABC\n\nfrom ..constants import AnnotationFlag\nfrom ..generic import NameObject, NumberObject\nfrom ..generic._data_structures import DictionaryObject\n\n\nclass AnnotationDictionary(DictionaryObject, ABC):\n    def __init__(self) -> None:\n        super().__init__()\n\n        from ..generic._base import NameObject  # noqa: PLC0415\n\n        # /Rect should not be added here as Polygon and PolyLine can automatically set it\n        self[NameObject(\"/Type\")] = NameObject(\"/Annot\")\n        # The flags were NOT added to the constructor on purpose:\n        # We expect that most users don't want to change the default.\n        # If they do, they can use the property. The default is 0.\n\n    @property\n    def flags(self) -> AnnotationFlag:\n        return self.get(NameObject(\"/F\"), AnnotationFlag(0))\n\n    @flags.setter\n    def flags(self, value: AnnotationFlag) -> None:\n        self[NameObject(\"/F\")] = NumberObject(value)\n\n\nNO_FLAGS = AnnotationFlag(0)\n"
  },
  {
    "path": "pypdf/annotations/_markup_annotations.py",
    "content": "import sys\nimport uuid\nfrom abc import ABC\nfrom typing import Any, Literal, Optional, Union\n\nfrom ..constants import AnnotationFlag\nfrom ..generic import ArrayObject, DictionaryObject\nfrom ..generic._base import (\n    BooleanObject,\n    FloatObject,\n    IndirectObject,\n    NameObject,\n    NumberObject,\n    TextStringObject,\n)\nfrom ..generic._rectangle import RectangleObject\nfrom ..generic._utils import hex_to_rgb\nfrom ._base import NO_FLAGS, AnnotationDictionary\n\nif sys.version_info[:2] >= (3, 10):\n    from typing import TypeAlias\nelse:\n    # PEP 613 introduced typing.TypeAlias with Python 3.10\n    # For older Python versions, the backport typing_extensions is necessary:\n    from typing_extensions import TypeAlias\n\n\nVertex: TypeAlias = tuple[float, float]\n\n\ndef _get_bounding_rectangle(vertices: list[Vertex]) -> RectangleObject:\n    x_min, y_min = vertices[0][0], vertices[0][1]\n    x_max, y_max = vertices[0][0], vertices[0][1]\n    for x, y in vertices:\n        x_min = min(x_min, x)\n        y_min = min(y_min, y)\n        x_max = max(x_max, x)\n        y_max = max(y_max, y)\n    return RectangleObject((x_min, y_min, x_max, y_max))\n\n\nclass MarkupAnnotation(AnnotationDictionary, ABC):\n    \"\"\"\n    Base class for all markup annotations.\n\n    Args:\n        title_bar: Text to be displayed in the title bar of the annotation;\n            by convention this is the name of the author\n        in_reply_to: The annotation that this annotation is \"in reply to\"\n            (PDF 1.5). Can be either an annotation (previously added using\n            :meth:`~pypdf.PdfWriter.add_annotation`) or a reference to the\n            target annotation.\n        reply_type: The relationship between this annotation and the one\n            specified by ``in_reply_to``. Either ``\"R\"`` (a reply, default)\n            or ``\"Group\"`` (grouped with the parent annotation). Raises\n            ``ValueError`` if a non-default value is provided without\n            ``in_reply_to``.\n        annotation_name: A text string uniquely identifying this annotation\n            among all annotations on its page. Automatically generated when\n            ``in_reply_to`` is set and no name is provided. Raises\n            ``ValueError`` if provided without ``in_reply_to``.\n\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        title_bar: Optional[str] = None,\n        in_reply_to: Optional[Union[DictionaryObject, IndirectObject]] = None,\n        reply_type: Literal[\"R\", \"Group\"] = \"R\",\n        annotation_name: Optional[str] = None,\n    ) -> None:\n        if title_bar is not None:\n            self[NameObject(\"/T\")] = TextStringObject(title_bar)\n        if annotation_name is not None and in_reply_to is None:\n            raise ValueError(\n                \"annotation_name is only supported when in_reply_to is set\"\n            )\n        if reply_type != \"R\" and in_reply_to is None:\n            raise ValueError(\n                \"reply_type is only meaningful when in_reply_to is set\"\n            )\n        if in_reply_to is not None:\n            if isinstance(in_reply_to, IndirectObject):\n                ref: IndirectObject = in_reply_to\n            else:\n                indirect_ref = getattr(in_reply_to, \"indirect_reference\", None)\n                if not isinstance(indirect_ref, IndirectObject):\n                    raise ValueError(\n                        \"in_reply_to must be a registered annotation \"\n                        \"(added via writer.add_annotation() first)\"\n                    )\n                ref = indirect_ref\n            self[NameObject(\"/IRT\")] = ref\n            self[NameObject(\"/RT\")] = NameObject(f\"/{reply_type}\")\n            if annotation_name is None:\n                annotation_name = str(uuid.uuid4())\n            self[NameObject(\"/NM\")] = TextStringObject(annotation_name)\n\n\nclass Text(MarkupAnnotation):\n    \"\"\"\n    A text annotation.\n\n    Args:\n        rect: array of four integers ``[xLL, yLL, xUR, yUR]``\n            specifying the clickable rectangular area\n        text: The text that is added to the document\n        open:\n        flags:\n\n    \"\"\"\n\n    def __init__(\n        self,\n        *,\n        rect: Union[RectangleObject, tuple[float, float, float, float]],\n        text: str,\n        open: bool = False,\n        flags: int = NO_FLAGS,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        self[NameObject(\"/Subtype\")] = NameObject(\"/Text\")\n        self[NameObject(\"/Rect\")] = RectangleObject(rect)\n        self[NameObject(\"/Contents\")] = TextStringObject(text)\n        self[NameObject(\"/Open\")] = BooleanObject(open)\n        self[NameObject(\"/Flags\")] = NumberObject(flags)\n\n\nclass FreeText(MarkupAnnotation):\n    \"\"\"A FreeText annotation\"\"\"\n\n    def __init__(\n        self,\n        *,\n        text: str,\n        rect: Union[RectangleObject, tuple[float, float, float, float]],\n        font: str = \"Helvetica\",\n        bold: bool = False,\n        italic: bool = False,\n        font_size: str = \"14pt\",\n        font_color: str = \"000000\",\n        border_color: Optional[str] = \"000000\",\n        background_color: Optional[str] = \"ffffff\",\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        self[NameObject(\"/Subtype\")] = NameObject(\"/FreeText\")\n        self[NameObject(\"/Rect\")] = RectangleObject(rect)\n\n        # Table 225 of the 1.7 reference (\"CSS2 style attributes used in rich text strings\")\n        font_str = \"font: \"\n        if italic:\n            font_str = f\"{font_str}italic \"\n        else:\n            font_str = f\"{font_str}normal \"\n        if bold:\n            font_str = f\"{font_str}bold \"\n        else:\n            font_str = f\"{font_str}normal \"\n        font_str = f\"{font_str}{font_size} {font}\"\n        font_str = f\"{font_str};text-align:left;color:#{font_color}\"\n\n        default_appearance_string = \"\"\n        if border_color:\n            for st in hex_to_rgb(border_color):\n                default_appearance_string = f\"{default_appearance_string}{st} \"\n            default_appearance_string = f\"{default_appearance_string}rg\"\n\n        self.update(\n            {\n                NameObject(\"/Subtype\"): NameObject(\"/FreeText\"),\n                NameObject(\"/Rect\"): RectangleObject(rect),\n                NameObject(\"/Contents\"): TextStringObject(text),\n                # font size color\n                NameObject(\"/DS\"): TextStringObject(font_str),\n                NameObject(\"/DA\"): TextStringObject(default_appearance_string),\n            }\n        )\n        if border_color is None:\n            # Border Style\n            self[NameObject(\"/BS\")] = DictionaryObject(\n                {\n                    # width of 0 means no border\n                    NameObject(\"/W\"): NumberObject(0)\n                }\n            )\n        if background_color is not None:\n            self[NameObject(\"/C\")] = ArrayObject(\n                [FloatObject(n) for n in hex_to_rgb(background_color)]\n            )\n\n\nclass Line(MarkupAnnotation):\n    def __init__(\n        self,\n        p1: Vertex,\n        p2: Vertex,\n        rect: Union[RectangleObject, tuple[float, float, float, float]],\n        text: str = \"\",\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        self.update(\n            {\n                NameObject(\"/Subtype\"): NameObject(\"/Line\"),\n                NameObject(\"/Rect\"): RectangleObject(rect),\n                NameObject(\"/L\"): ArrayObject(\n                    [\n                        FloatObject(p1[0]),\n                        FloatObject(p1[1]),\n                        FloatObject(p2[0]),\n                        FloatObject(p2[1]),\n                    ]\n                ),\n                NameObject(\"/LE\"): ArrayObject(\n                    [\n                        NameObject(\"/None\"),\n                        NameObject(\"/None\"),\n                    ]\n                ),\n                NameObject(\"/IC\"): ArrayObject(\n                    [\n                        FloatObject(0.5),\n                        FloatObject(0.5),\n                        FloatObject(0.5),\n                    ]\n                ),\n                NameObject(\"/Contents\"): TextStringObject(text),\n            }\n        )\n\n\nclass PolyLine(MarkupAnnotation):\n    def __init__(\n        self,\n        vertices: list[Vertex],\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        if len(vertices) == 0:\n            raise ValueError(\"A polyline needs at least 1 vertex with two coordinates\")\n        coord_list = []\n        for x, y in vertices:\n            coord_list.append(NumberObject(x))\n            coord_list.append(NumberObject(y))\n        self.update(\n            {\n                NameObject(\"/Subtype\"): NameObject(\"/PolyLine\"),\n                NameObject(\"/Vertices\"): ArrayObject(coord_list),\n                NameObject(\"/Rect\"): RectangleObject(_get_bounding_rectangle(vertices)),\n            }\n        )\n\n\nclass Rectangle(MarkupAnnotation):\n    def __init__(\n        self,\n        rect: Union[RectangleObject, tuple[float, float, float, float]],\n        *,\n        interior_color: Optional[str] = None,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        self.update(\n            {\n                NameObject(\"/Type\"): NameObject(\"/Annot\"),\n                NameObject(\"/Subtype\"): NameObject(\"/Square\"),\n                NameObject(\"/Rect\"): RectangleObject(rect),\n            }\n        )\n\n        if interior_color:\n            self[NameObject(\"/IC\")] = ArrayObject(\n                [FloatObject(n) for n in hex_to_rgb(interior_color)]\n            )\n\n\nclass Highlight(MarkupAnnotation):\n    def __init__(\n        self,\n        *,\n        rect: Union[RectangleObject, tuple[float, float, float, float]],\n        quad_points: ArrayObject,\n        highlight_color: str = \"ff0000\",\n        printing: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        self.update(\n            {\n                NameObject(\"/Subtype\"): NameObject(\"/Highlight\"),\n                NameObject(\"/Rect\"): RectangleObject(rect),\n                NameObject(\"/QuadPoints\"): quad_points,\n                NameObject(\"/C\"): ArrayObject(\n                    [FloatObject(n) for n in hex_to_rgb(highlight_color)]\n                ),\n            }\n        )\n        if printing:\n            self.flags = AnnotationFlag.PRINT\n\n\nclass Ellipse(MarkupAnnotation):\n    def __init__(\n        self,\n        rect: Union[RectangleObject, tuple[float, float, float, float]],\n        *,\n        interior_color: Optional[str] = None,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n\n        self.update(\n            {\n                NameObject(\"/Type\"): NameObject(\"/Annot\"),\n                NameObject(\"/Subtype\"): NameObject(\"/Circle\"),\n                NameObject(\"/Rect\"): RectangleObject(rect),\n            }\n        )\n\n        if interior_color:\n            self[NameObject(\"/IC\")] = ArrayObject(\n                [FloatObject(n) for n in hex_to_rgb(interior_color)]\n            )\n\n\nclass Polygon(MarkupAnnotation):\n    def __init__(\n        self,\n        vertices: list[tuple[float, float]],\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        if len(vertices) == 0:\n            raise ValueError(\"A polygon needs at least 1 vertex with two coordinates\")\n\n        coord_list = []\n        for x, y in vertices:\n            coord_list.append(NumberObject(x))\n            coord_list.append(NumberObject(y))\n        self.update(\n            {\n                NameObject(\"/Type\"): NameObject(\"/Annot\"),\n                NameObject(\"/Subtype\"): NameObject(\"/Polygon\"),\n                NameObject(\"/Vertices\"): ArrayObject(coord_list),\n                NameObject(\"/IT\"): NameObject(\"/PolygonCloud\"),\n                NameObject(\"/Rect\"): RectangleObject(_get_bounding_rectangle(vertices)),\n            }\n        )\n"
  },
  {
    "path": "pypdf/annotations/_non_markup_annotations.py",
    "content": "from typing import TYPE_CHECKING, Any, Optional, Union\n\nfrom ..generic._base import (\n    BooleanObject,\n    NameObject,\n    NumberObject,\n    TextStringObject,\n)\nfrom ..generic._data_structures import ArrayObject, DictionaryObject\nfrom ..generic._fit import DEFAULT_FIT, Fit\nfrom ..generic._rectangle import RectangleObject\nfrom ._base import AnnotationDictionary\n\n\nclass Link(AnnotationDictionary):\n    def __init__(\n        self,\n        *,\n        rect: Union[RectangleObject, tuple[float, float, float, float]],\n        border: Optional[ArrayObject] = None,\n        url: Optional[str] = None,\n        target_page_index: Optional[int] = None,\n        fit: Fit = DEFAULT_FIT,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        if TYPE_CHECKING:\n            from ..types import BorderArrayType  # noqa: PLC0415\n\n        is_external = url is not None\n        is_internal = target_page_index is not None\n        if not is_external and not is_internal:\n            raise ValueError(\n                \"Either 'url' or 'target_page_index' have to be provided. Both were None.\"\n            )\n        if is_external and is_internal:\n            raise ValueError(\n                \"Either 'url' or 'target_page_index' have to be provided. \"\n                f\"{url=}, {target_page_index=}\"\n            )\n\n        border_arr: BorderArrayType\n        if border is not None:\n            border_arr = [NumberObject(n) for n in border[:3]]\n            if len(border) == 4:\n                dash_pattern = ArrayObject([NumberObject(n) for n in border[3]])\n                border_arr.append(dash_pattern)\n        else:\n            border_arr = [NumberObject(0)] * 3\n\n        self.update(\n            {\n                NameObject(\"/Type\"): NameObject(\"/Annot\"),\n                NameObject(\"/Subtype\"): NameObject(\"/Link\"),\n                NameObject(\"/Rect\"): RectangleObject(rect),\n                NameObject(\"/Border\"): ArrayObject(border_arr),\n            }\n        )\n        if is_external:\n            self[NameObject(\"/A\")] = DictionaryObject(\n                {\n                    NameObject(\"/S\"): NameObject(\"/URI\"),\n                    NameObject(\"/Type\"): NameObject(\"/Action\"),\n                    NameObject(\"/URI\"): TextStringObject(url),\n                }\n            )\n        if is_internal:\n            # This needs to be updated later!\n            dest_deferred = DictionaryObject(\n                {\n                    \"target_page_index\": NumberObject(target_page_index),\n                    \"fit\": NameObject(fit.fit_type),\n                    \"fit_args\": fit.fit_args,\n                }\n            )\n            self[NameObject(\"/Dest\")] = dest_deferred\n\n\nclass Popup(AnnotationDictionary):\n    def __init__(\n        self,\n        *,\n        rect: Union[RectangleObject, tuple[float, float, float, float]],\n        parent: Optional[DictionaryObject] = None,\n        open: bool = False,\n        **kwargs: Any,\n    ) -> None:\n        super().__init__(**kwargs)\n        self.update(\n            {\n                NameObject(\"/Subtype\"): NameObject(\"/Popup\"),\n                NameObject(\"/Rect\"): RectangleObject(rect),\n                NameObject(\"/Open\"): BooleanObject(open),\n            }\n        )\n        if parent:\n            # This needs to be an indirect object\n            try:\n                self[NameObject(\"/Parent\")] = parent.indirect_reference\n            except AttributeError:\n                from .._utils import logger_warning  # noqa: PLC0415\n\n                logger_warning(\n                    \"Unregistered Parent object : No Parent field set\",\n                    __name__,\n                )\n"
  },
  {
    "path": "pypdf/constants.py",
    "content": "\"\"\"Various constants, enums, and flags to aid readability.\"\"\"\n\nfrom enum import Enum, IntFlag, auto, unique\n\n\nclass StrEnum(str, Enum):  # Once we are on Python 3.11+: enum.StrEnum\n    def __str__(self) -> str:\n        return str(self.value)\n\n\nclass Core:\n    \"\"\"Keywords that don't quite belong anywhere else.\"\"\"\n\n    OUTLINES = \"/Outlines\"\n    THREADS = \"/Threads\"\n    PAGE = \"/Page\"\n    PAGES = \"/Pages\"\n    CATALOG = \"/Catalog\"\n\n\nclass TrailerKeys:\n    SIZE = \"/Size\"\n    PREV = \"/Prev\"\n    ROOT = \"/Root\"\n    ENCRYPT = \"/Encrypt\"\n    INFO = \"/Info\"\n    ID = \"/ID\"\n\n\nclass CatalogAttributes:\n    NAMES = \"/Names\"\n    DESTS = \"/Dests\"\n\n\nclass EncryptionDictAttributes:\n    \"\"\"\n    Additional encryption dictionary entries for the standard security handler.\n\n    Table 3.19, Page 122.\n    Table 21 of the 2.0 manual.\n    \"\"\"\n\n    R = \"/R\"  # number, required; revision of the standard security handler\n    O = \"/O\"  # 32-byte string, required  # noqa: E741\n    U = \"/U\"  # 32-byte string, required\n    P = \"/P\"  # integer flag, required; permitted operations\n    ENCRYPT_METADATA = \"/EncryptMetadata\"  # boolean flag, optional\n\n\nclass UserAccessPermissions(IntFlag):\n    \"\"\"\n    Table 3.20 User access permissions.\n    Table 22 of the 2.0 manual.\n    \"\"\"\n\n    R1 = 1\n    R2 = 2\n    PRINT = 4\n    MODIFY = 8\n    EXTRACT = 16\n    ADD_OR_MODIFY = 32\n    R7 = 64\n    R8 = 128\n    FILL_FORM_FIELDS = 256\n    EXTRACT_TEXT_AND_GRAPHICS = 512\n    ASSEMBLE_DOC = 1024\n    PRINT_TO_REPRESENTATION = 2048\n    R13 = 2**12\n    R14 = 2**13\n    R15 = 2**14\n    R16 = 2**15\n    R17 = 2**16\n    R18 = 2**17\n    R19 = 2**18\n    R20 = 2**19\n    R21 = 2**20\n    R22 = 2**21\n    R23 = 2**22\n    R24 = 2**23\n    R25 = 2**24\n    R26 = 2**25\n    R27 = 2**26\n    R28 = 2**27\n    R29 = 2**28\n    R30 = 2**29\n    R31 = 2**30\n    R32 = 2**31\n\n    @classmethod\n    def _is_reserved(cls, name: str) -> bool:\n        \"\"\"Check if the given name corresponds to a reserved flag entry.\"\"\"\n        return name.startswith(\"R\") and name[1:].isdigit()\n\n    @classmethod\n    def _is_active(cls, name: str) -> bool:\n        \"\"\"Check if the given reserved name defaults to 1 = active.\"\"\"\n        return name not in {\"R1\", \"R2\"}\n\n    def to_dict(self) -> dict[str, bool]:\n        \"\"\"Convert the given flag value to a corresponding verbose name mapping.\"\"\"\n        result: dict[str, bool] = {}\n        for name, flag in UserAccessPermissions.__members__.items():\n            if UserAccessPermissions._is_reserved(name):\n                continue\n            result[name.lower()] = (self & flag) == flag\n        return result\n\n    @classmethod\n    def from_dict(cls, value: dict[str, bool]) -> \"UserAccessPermissions\":\n        \"\"\"Convert the verbose name mapping to the corresponding flag value.\"\"\"\n        value_copy = value.copy()\n        result = cls(0)\n        for name, flag in cls.__members__.items():\n            if cls._is_reserved(name):\n                # Reserved names have a required value. Use it.\n                if cls._is_active(name):\n                    result |= flag\n                continue\n            is_active = value_copy.pop(name.lower(), False)\n            if is_active:\n                result |= flag\n        if value_copy:\n            raise ValueError(f\"Unknown dictionary keys: {value_copy!r}\")\n        return result\n\n    @classmethod\n    def all(cls) -> \"UserAccessPermissions\":\n        return cls((2**32 - 1) - cls.R1 - cls.R2)\n\n\nclass Resources:\n    \"\"\"\n    Table 3.30 Entries in a resource dictionary.\n    Table 34 in the 2.0 reference.\n    \"\"\"\n\n    EXT_G_STATE = \"/ExtGState\"  # dictionary, optional\n    COLOR_SPACE = \"/ColorSpace\"  # dictionary, optional\n    PATTERN = \"/Pattern\"  # dictionary, optional\n    SHADING = \"/Shading\"  # dictionary, optional\n    XOBJECT = \"/XObject\"  # dictionary, optional\n    FONT = \"/Font\"  # dictionary, optional\n    PROC_SET = \"/ProcSet\"  # array, optional\n    PROPERTIES = \"/Properties\"  # dictionary, optional\n\n\nclass PagesAttributes:\n    \"\"\"§7.7.3.2 of the 1.7 and 2.0 reference.\"\"\"\n\n    TYPE = \"/Type\"  # name, required; must be /Pages\n    PARENT = \"/Parent\"  # dictionary, required; indirect reference to pages object\n    KIDS = \"/Kids\"  # array, required; List of indirect references\n    COUNT = \"/Count\"\n    # integer, required; the number of leaf nodes (page objects)\n    # that are descendants of this node within the page tree\n\n\nclass PageAttributes:\n    \"\"\"§7.7.3.3 of the 1.7 and 2.0 reference.\"\"\"\n\n    TYPE = \"/Type\"  # name, required; must be /Page\n    PARENT = \"/Parent\"  # dictionary, required; a pages object\n    LAST_MODIFIED = (\n        \"/LastModified\"  # date, optional; date and time of last modification\n    )\n    RESOURCES = \"/Resources\"  # dictionary, required if there are any\n    MEDIABOX = \"/MediaBox\"  # rectangle, required; rectangle specifying page size\n    CROPBOX = \"/CropBox\"  # rectangle, optional\n    BLEEDBOX = \"/BleedBox\"  # rectangle, optional\n    TRIMBOX = \"/TrimBox\"  # rectangle, optional\n    ARTBOX = \"/ArtBox\"  # rectangle, optional\n    BOX_COLOR_INFO = \"/BoxColorInfo\"  # dictionary, optional\n    CONTENTS = \"/Contents\"  # stream or array, optional\n    ROTATE = \"/Rotate\"  # integer, optional; page rotation in degrees\n    GROUP = \"/Group\"  # dictionary, optional; page group\n    THUMB = \"/Thumb\"  # stream, optional; indirect reference to image of the page\n    B = \"/B\"  # array, optional\n    DUR = \"/Dur\"  # number, optional\n    TRANS = \"/Trans\"  # dictionary, optional\n    ANNOTS = \"/Annots\"  # array, optional; an array of annotations\n    AA = \"/AA\"  # dictionary, optional\n    METADATA = \"/Metadata\"  # stream, optional\n    PIECE_INFO = \"/PieceInfo\"  # dictionary, optional\n    STRUCT_PARENTS = \"/StructParents\"  # integer, optional\n    ID = \"/ID\"  # byte string, optional\n    PZ = \"/PZ\"  # number, optional\n    SEPARATION_INFO = \"/SeparationInfo\"  # dictionary, optional\n    TABS = \"/Tabs\"  # name, optional\n    TEMPLATE_INSTANTIATED = \"/TemplateInstantiated\"  # name, optional\n    PRES_STEPS = \"/PresSteps\"  # dictionary, optional\n    USER_UNIT = \"/UserUnit\"  # number, optional\n    VP = \"/VP\"  # dictionary, optional\n    AF = \"/AF\"  # array of dictionaries, optional\n    OUTPUT_INTENTS = \"/OutputIntents\"  # array, optional\n    D_PART = \"/DPart\"  # dictionary, required, if this page is within the range of a DPart, not permitted otherwise\n\n\nclass FileSpecificationDictionaryEntries:\n    \"\"\"Table 3.41 Entries in a file specification dictionary.\"\"\"\n\n    Type = \"/Type\"\n    FS = \"/FS\"  # The name of the file system to be used to interpret this file specification\n    F = \"/F\"  # A file specification string of the form described in §3.10.1\n    UF = \"/UF\"  # A Unicode string of the file as described in §3.10.1\n    DOS = \"/DOS\"\n    Mac = \"/Mac\"\n    Unix = \"/Unix\"\n    ID = \"/ID\"\n    V = \"/V\"\n    EF = \"/EF\"  # dictionary, containing a subset of the keys F, UF, DOS, Mac, and Unix\n    RF = \"/RF\"  # dictionary, containing arrays of /EmbeddedFile\n    DESC = \"/Desc\"  # description of the file\n    Cl = \"/Cl\"\n\n\nclass StreamAttributes:\n    \"\"\"\n    Table 4.2.\n    Table 5 in the 2.0 reference.\n    \"\"\"\n\n    LENGTH = \"/Length\"  # integer, required\n    FILTER = \"/Filter\"  # name or array of names, optional\n    DECODE_PARMS = \"/DecodeParms\"  # variable, optional -- 'decodeParams is wrong\n\n\n@unique\nclass FilterTypes(StrEnum):\n    \"\"\"§7.4 of the 1.7 and 2.0 references.\"\"\"\n\n    ASCII_HEX_DECODE = \"/ASCIIHexDecode\"  # abbreviation: AHx\n    ASCII_85_DECODE = \"/ASCII85Decode\"  # abbreviation: A85\n    LZW_DECODE = \"/LZWDecode\"  # abbreviation: LZW\n    FLATE_DECODE = \"/FlateDecode\"  # abbreviation: Fl\n    RUN_LENGTH_DECODE = \"/RunLengthDecode\"  # abbreviation: RL\n    CCITT_FAX_DECODE = \"/CCITTFaxDecode\"  # abbreviation: CCF\n    DCT_DECODE = \"/DCTDecode\"  # abbreviation: DCT\n    JPX_DECODE = \"/JPXDecode\"\n    JBIG2_DECODE = \"/JBIG2Decode\"\n\n\nclass FilterTypeAbbreviations:\n    \"\"\"§8.9.7 of the 1.7 and 2.0 references.\"\"\"\n\n    AHx = \"/AHx\"\n    A85 = \"/A85\"\n    LZW = \"/LZW\"\n    FL = \"/Fl\"\n    RL = \"/RL\"\n    CCF = \"/CCF\"\n    DCT = \"/DCT\"\n\n\nclass LzwFilterParameters:\n    \"\"\"\n    Table 4.4.\n    Table 8 in the 2.0 reference.\n    \"\"\"\n\n    PREDICTOR = \"/Predictor\"  # integer\n    COLORS = \"/Colors\"  # integer\n    BITS_PER_COMPONENT = \"/BitsPerComponent\"  # integer\n    COLUMNS = \"/Columns\"  # integer\n    EARLY_CHANGE = \"/EarlyChange\"  # integer\n\n\nclass CcittFaxDecodeParameters:\n    \"\"\"\n    Table 4.5.\n    Table 11 in the 2.0 reference.\n    \"\"\"\n\n    K = \"/K\"  # integer\n    END_OF_LINE = \"/EndOfLine\"  # boolean\n    ENCODED_BYTE_ALIGN = \"/EncodedByteAlign\"  # boolean\n    COLUMNS = \"/Columns\"  # integer\n    ROWS = \"/Rows\"  # integer\n    END_OF_BLOCK = \"/EndOfBlock\"  # boolean\n    BLACK_IS_1 = \"/BlackIs1\"  # boolean\n    DAMAGED_ROWS_BEFORE_ERROR = \"/DamagedRowsBeforeError\"  # integer\n\n\nclass ImageAttributes:\n    \"\"\"§11.6.5 of the 1.7 and 2.0 references.\"\"\"\n\n    TYPE = \"/Type\"  # name, required; must be /XObject\n    SUBTYPE = \"/Subtype\"  # name, required; must be /Image\n    NAME = \"/Name\"  # name, required\n    WIDTH = \"/Width\"  # integer, required\n    HEIGHT = \"/Height\"  # integer, required\n    BITS_PER_COMPONENT = \"/BitsPerComponent\"  # integer, required\n    COLOR_SPACE = \"/ColorSpace\"  # name, required\n    DECODE = \"/Decode\"  # array, optional\n    INTENT = \"/Intent\"  # string, optional\n    INTERPOLATE = \"/Interpolate\"  # boolean, optional\n    IMAGE_MASK = \"/ImageMask\"  # boolean, optional\n    MASK = \"/Mask\"  # 1-bit image mask stream\n    S_MASK = \"/SMask\"  # dictionary or name, optional\n\n\nclass ColorSpaces:\n    DEVICE_RGB = \"/DeviceRGB\"\n    DEVICE_CMYK = \"/DeviceCMYK\"\n    DEVICE_GRAY = \"/DeviceGray\"\n\n\nclass TypArguments:\n    \"\"\"Table 8.2 of the PDF 1.7 reference.\"\"\"\n\n    LEFT = \"/Left\"\n    RIGHT = \"/Right\"\n    BOTTOM = \"/Bottom\"\n    TOP = \"/Top\"\n\n\nclass TypFitArguments:\n    \"\"\"Table 8.2 of the PDF 1.7 reference.\"\"\"\n\n    XYZ = \"/XYZ\"\n    FIT = \"/Fit\"\n    FIT_H = \"/FitH\"\n    FIT_V = \"/FitV\"\n    FIT_R = \"/FitR\"\n    FIT_B = \"/FitB\"\n    FIT_BH = \"/FitBH\"\n    FIT_BV = \"/FitBV\"\n\n\nclass GoToActionArguments:\n    S = \"/S\"  # name, required: type of action\n    D = \"/D\"  # name, byte string, or array, required: destination to jump to\n    SD = \"/SD\"  # array, optional: structure destination to jump to\n\n\nclass AnnotationDictionaryAttributes:\n    \"\"\"Table 8.15 Entries common to all annotation dictionaries.\"\"\"\n\n    Type = \"/Type\"\n    Subtype = \"/Subtype\"\n    Rect = \"/Rect\"\n    Contents = \"/Contents\"\n    P = \"/P\"\n    NM = \"/NM\"\n    M = \"/M\"\n    F = \"/F\"\n    AP = \"/AP\"\n    AS = \"/AS\"\n    DA = \"/DA\"\n    Border = \"/Border\"\n    C = \"/C\"\n    StructParent = \"/StructParent\"\n    OC = \"/OC\"\n\n\nclass InteractiveFormDictEntries:\n    Fields = \"/Fields\"\n    NeedAppearances = \"/NeedAppearances\"\n    SigFlags = \"/SigFlags\"\n    CO = \"/CO\"\n    DR = \"/DR\"\n    DA = \"/DA\"\n    Q = \"/Q\"\n    XFA = \"/XFA\"\n\n\nclass FieldDictionaryAttributes:\n    \"\"\"\n    Entries common to all field dictionaries (Table 8.69 PDF 1.7 reference)\n    (*very partially documented here*).\n\n    FFBits provides the constants used for `/Ff` from Table 8.70/8.75/8.77/8.79\n    \"\"\"\n\n    FT = \"/FT\"  # name, required for terminal fields\n    Parent = \"/Parent\"  # dictionary, required for children\n    Kids = \"/Kids\"  # array, sometimes required\n    T = \"/T\"  # text string, optional\n    TU = \"/TU\"  # text string, optional\n    TM = \"/TM\"  # text string, optional\n    Ff = \"/Ff\"  # integer, optional\n    V = \"/V\"  # text string or array, optional\n    DV = \"/DV\"  # text string, optional\n    AA = \"/AA\"  # dictionary, optional\n    Opt = \"/Opt\"  # array, optional\n\n    class FfBits(IntFlag):\n        \"\"\"\n        Ease building /Ff flags\n        Some entries may be specific to:\n\n        * Text (Tx) (Table 8.75 PDF 1.7 reference)\n        * Buttons (Btn) (Table 8.77 PDF 1.7 reference)\n        * Choice (Ch) (Table 8.79 PDF 1.7 reference)\n        \"\"\"\n\n        ReadOnly = 1 << 0\n        \"\"\"common to Tx/Btn/Ch in Table 8.70\"\"\"\n        Required = 1 << 1\n        \"\"\"common to Tx/Btn/Ch in Table 8.70\"\"\"\n        NoExport = 1 << 2\n        \"\"\"common to Tx/Btn/Ch in Table 8.70\"\"\"\n\n        Multiline = 1 << 12\n        \"\"\"Tx\"\"\"\n        Password = 1 << 13\n        \"\"\"Tx\"\"\"\n\n        NoToggleToOff = 1 << 14\n        \"\"\"Btn\"\"\"\n        Radio = 1 << 15\n        \"\"\"Btn\"\"\"\n        Pushbutton = 1 << 16\n        \"\"\"Btn\"\"\"\n\n        Combo = 1 << 17\n        \"\"\"Ch\"\"\"\n        Edit = 1 << 18\n        \"\"\"Ch\"\"\"\n        Sort = 1 << 19\n        \"\"\"Ch\"\"\"\n\n        FileSelect = 1 << 20\n        \"\"\"Tx\"\"\"\n\n        MultiSelect = 1 << 21\n        \"\"\"Tx\"\"\"\n\n        DoNotSpellCheck = 1 << 22\n        \"\"\"Tx/Ch\"\"\"\n        DoNotScroll = 1 << 23\n        \"\"\"Tx\"\"\"\n        Comb = 1 << 24\n        \"\"\"Tx\"\"\"\n\n        RadiosInUnison = 1 << 25\n        \"\"\"Btn\"\"\"\n\n        RichText = 1 << 25\n        \"\"\"Tx\"\"\"\n\n        CommitOnSelChange = 1 << 26\n        \"\"\"Ch\"\"\"\n\n    @classmethod\n    def attributes(cls) -> tuple[str, ...]:\n        \"\"\"\n        Get a tuple of all the attributes present in a Field Dictionary.\n\n        This method returns a tuple of all the attribute constants defined in\n        the FieldDictionaryAttributes class. These attributes correspond to the\n        entries that are common to all field dictionaries as specified in the\n        PDF 1.7 reference.\n\n        Returns:\n            A tuple containing all the attribute constants.\n\n        \"\"\"\n        return (\n            cls.TM,\n            cls.T,\n            cls.FT,\n            cls.Parent,\n            cls.TU,\n            cls.Ff,\n            cls.V,\n            cls.DV,\n            cls.Kids,\n            cls.AA,\n        )\n\n    @classmethod\n    def attributes_dict(cls) -> dict[str, str]:\n        \"\"\"\n        Get a dictionary of attribute keys and their human-readable names.\n\n        This method returns a dictionary where the keys are the attribute\n        constants defined in the FieldDictionaryAttributes class and the values\n        are their corresponding human-readable names. These attributes\n        correspond to the entries that are common to all field dictionaries as\n        specified in the PDF 1.7 reference.\n\n        Returns:\n            A dictionary containing attribute keys and their names.\n\n        \"\"\"\n        return {\n            cls.FT: \"Field Type\",\n            cls.Parent: \"Parent\",\n            cls.T: \"Field Name\",\n            cls.TU: \"Alternate Field Name\",\n            cls.TM: \"Mapping Name\",\n            cls.Ff: \"Field Flags\",\n            cls.V: \"Value\",\n            cls.DV: \"Default Value\",\n        }\n\n\nclass CheckboxRadioButtonAttributes:\n    \"\"\"Table 8.76 Field flags common to all field types.\"\"\"\n\n    Opt = \"/Opt\"  # Options, Optional\n\n    @classmethod\n    def attributes(cls) -> tuple[str, ...]:\n        \"\"\"\n        Get a tuple of all the attributes present in a Field Dictionary.\n\n        This method returns a tuple of all the attribute constants defined in\n        the CheckboxRadioButtonAttributes class. These attributes correspond to\n        the entries that are common to all field dictionaries as specified in\n        the PDF 1.7 reference.\n\n        Returns:\n            A tuple containing all the attribute constants.\n\n        \"\"\"\n        return (cls.Opt,)\n\n    @classmethod\n    def attributes_dict(cls) -> dict[str, str]:\n        \"\"\"\n        Get a dictionary of attribute keys and their human-readable names.\n\n        This method returns a dictionary where the keys are the attribute\n        constants defined in the CheckboxRadioButtonAttributes class and the\n        values are their corresponding human-readable names. These attributes\n        correspond to the entries that are common to all field dictionaries as\n        specified in the PDF 1.7 reference.\n\n        Returns:\n            A dictionary containing attribute keys and their names.\n\n        \"\"\"\n        return {\n            cls.Opt: \"Options\",\n        }\n\n\nclass FieldFlag(IntFlag):\n    \"\"\"Table 8.70 Field flags common to all field types.\"\"\"\n\n    READ_ONLY = 1\n    REQUIRED = 2\n    NO_EXPORT = 4\n\n\nclass DocumentInformationAttributes:\n    \"\"\"Table 10.2 Entries in the document information dictionary.\"\"\"\n\n    TITLE = \"/Title\"  # text string, optional\n    AUTHOR = \"/Author\"  # text string, optional\n    SUBJECT = \"/Subject\"  # text string, optional\n    KEYWORDS = \"/Keywords\"  # text string, optional\n    CREATOR = \"/Creator\"  # text string, optional\n    PRODUCER = \"/Producer\"  # text string, optional\n    CREATION_DATE = \"/CreationDate\"  # date, optional\n    MOD_DATE = \"/ModDate\"  # date, optional\n    TRAPPED = \"/Trapped\"  # name, optional\n\n\nclass PageLayouts:\n    \"\"\"\n    Page 84, PDF 1.4 reference.\n    Page 115, PDF 2.0 reference.\n    \"\"\"\n\n    SINGLE_PAGE = \"/SinglePage\"\n    ONE_COLUMN = \"/OneColumn\"\n    TWO_COLUMN_LEFT = \"/TwoColumnLeft\"\n    TWO_COLUMN_RIGHT = \"/TwoColumnRight\"\n    TWO_PAGE_LEFT = \"/TwoPageLeft\"  # (PDF 1.5)\n    TWO_PAGE_RIGHT = \"/TwoPageRight\"  # (PDF 1.5)\n\n\nclass GraphicsStateParameters:\n    \"\"\"Table 58 – Entries in a Graphics State Parameter Dictionary\"\"\"\n\n    TYPE = \"/Type\"  # name, optional\n    LW = \"/LW\"  # number, optional\n    LC = \"/LC\"  # integer, optional\n    LJ = \"/LJ\"  # integer, optional\n    ML = \"/ML\"  # number, optional\n    D = \"/D\"  # array, optional\n    RI = \"/RI\"  # name, optional\n    OP = \"/OP\"\n    op = \"/op\"\n    OPM = \"/OPM\"\n    FONT = \"/Font\"  # array, optional\n    BG = \"/BG\"\n    BG2 = \"/BG2\"\n    UCR = \"/UCR\"\n    UCR2 = \"/UCR2\"\n    TR = \"/TR\"\n    TR2 = \"/TR2\"\n    HT = \"/HT\"\n    FL = \"/FL\"\n    SM = \"/SM\"\n    SA = \"/SA\"\n    BM = \"/BM\"\n    S_MASK = \"/SMask\"  # dictionary or name, optional\n    CA = \"/CA\"\n    ca = \"/ca\"\n    AIS = \"/AIS\"\n    TK = \"/TK\"\n\n\nclass CatalogDictionary:\n    \"\"\"§7.7.2 of the 1.7 and 2.0 references.\"\"\"\n\n    TYPE = \"/Type\"  # name, required; must be /Catalog\n    VERSION = \"/Version\"  # name\n    EXTENSIONS = \"/Extensions\"  # dictionary, optional; ISO 32000-1\n    PAGES = \"/Pages\"  # dictionary, required\n    PAGE_LABELS = \"/PageLabels\"  # number tree, optional\n    NAMES = \"/Names\"  # dictionary, optional\n    DESTS = \"/Dests\"  # dictionary, optional\n    VIEWER_PREFERENCES = \"/ViewerPreferences\"  # dictionary, optional\n    PAGE_LAYOUT = \"/PageLayout\"  # name, optional\n    PAGE_MODE = \"/PageMode\"  # name, optional\n    OUTLINES = \"/Outlines\"  # dictionary, optional\n    THREADS = \"/Threads\"  # array, optional\n    OPEN_ACTION = \"/OpenAction\"  # array or dictionary or name, optional\n    AA = \"/AA\"  # dictionary, optional\n    URI = \"/URI\"  # dictionary, optional\n    ACRO_FORM = \"/AcroForm\"  # dictionary, optional\n    METADATA = \"/Metadata\"  # stream, optional\n    STRUCT_TREE_ROOT = \"/StructTreeRoot\"  # dictionary, optional\n    MARK_INFO = \"/MarkInfo\"  # dictionary, optional\n    LANG = \"/Lang\"  # text string, optional\n    SPIDER_INFO = \"/SpiderInfo\"  # dictionary, optional\n    OUTPUT_INTENTS = \"/OutputIntents\"  # array, optional\n    PIECE_INFO = \"/PieceInfo\"  # dictionary, optional\n    OC_PROPERTIES = \"/OCProperties\"  # dictionary, optional\n    PERMS = \"/Perms\"  # dictionary, optional\n    LEGAL = \"/Legal\"  # dictionary, optional\n    REQUIREMENTS = \"/Requirements\"  # array, optional\n    COLLECTION = \"/Collection\"  # dictionary, optional\n    NEEDS_RENDERING = \"/NeedsRendering\"  # boolean, optional\n    DSS = \"/DSS\"  # dictionary, optional\n    AF = \"/AF\"  # array of dictionaries, optional\n    D_PART_ROOT = \"/DPartRoot\"  # dictionary, optional\n\n\nclass OutlineFontFlag(IntFlag):\n    \"\"\"A class used as an enumerable flag for formatting an outline font.\"\"\"\n\n    italic = 1\n    bold = 2\n\n\nclass PageLabelStyle:\n    \"\"\"\n    Table 8.10 in the 1.7 reference.\n    Table 161 in the 2.0 reference.\n    \"\"\"\n\n    DECIMAL = \"/D\"  # Decimal Arabic numerals\n    UPPERCASE_ROMAN = \"/R\"  # Uppercase Roman numerals\n    LOWERCASE_ROMAN = \"/r\"  # Lowercase Roman numerals\n    UPPERCASE_LETTER = \"/A\"  # Uppercase letters\n    LOWERCASE_LETTER = \"/a\"  # Lowercase letters\n\n\nclass AnnotationFlag(IntFlag):\n    \"\"\"See §12.5.3 \"Annotation Flags\".\"\"\"\n\n    INVISIBLE = 1\n    HIDDEN = 2\n    PRINT = 4\n    NO_ZOOM = 8\n    NO_ROTATE = 16\n    NO_VIEW = 32\n    READ_ONLY = 64\n    LOCKED = 128\n    TOGGLE_NO_VIEW = 256\n    LOCKED_CONTENTS = 512\n\n\nPDF_KEYS = (\n    AnnotationDictionaryAttributes,\n    CatalogAttributes,\n    CatalogDictionary,\n    CcittFaxDecodeParameters,\n    CheckboxRadioButtonAttributes,\n    ColorSpaces,\n    Core,\n    DocumentInformationAttributes,\n    EncryptionDictAttributes,\n    FieldDictionaryAttributes,\n    FileSpecificationDictionaryEntries,\n    FilterTypeAbbreviations,\n    FilterTypes,\n    GoToActionArguments,\n    GraphicsStateParameters,\n    ImageAttributes,\n    InteractiveFormDictEntries,\n    LzwFilterParameters,\n    PageAttributes,\n    PageLayouts,\n    PagesAttributes,\n    Resources,\n    StreamAttributes,\n    TrailerKeys,\n    TypArguments,\n    TypFitArguments,\n)\n\n\nclass ImageType(IntFlag):\n    NONE = 0\n    XOBJECT_IMAGES = auto()\n    INLINE_IMAGES = auto()\n    DRAWING_IMAGES = auto()\n    ALL = XOBJECT_IMAGES | INLINE_IMAGES | DRAWING_IMAGES\n    IMAGES = ALL  # for consistency with ObjectDeletionFlag\n\n\n_INLINE_IMAGE_VALUE_MAPPING = {\n    \"/G\": \"/DeviceGray\",\n    \"/RGB\": \"/DeviceRGB\",\n    \"/CMYK\": \"/DeviceCMYK\",\n    \"/I\": \"/Indexed\",\n    \"/AHx\": \"/ASCIIHexDecode\",\n    \"/A85\": \"/ASCII85Decode\",\n    \"/LZW\": \"/LZWDecode\",\n    \"/Fl\": \"/FlateDecode\",\n    \"/RL\": \"/RunLengthDecode\",\n    \"/CCF\": \"/CCITTFaxDecode\",\n    \"/DCT\": \"/DCTDecode\",\n    \"/DeviceGray\": \"/DeviceGray\",\n    \"/DeviceRGB\": \"/DeviceRGB\",\n    \"/DeviceCMYK\": \"/DeviceCMYK\",\n    \"/Indexed\": \"/Indexed\",\n    \"/ASCIIHexDecode\": \"/ASCIIHexDecode\",\n    \"/ASCII85Decode\": \"/ASCII85Decode\",\n    \"/LZWDecode\": \"/LZWDecode\",\n    \"/FlateDecode\": \"/FlateDecode\",\n    \"/RunLengthDecode\": \"/RunLengthDecode\",\n    \"/CCITTFaxDecode\": \"/CCITTFaxDecode\",\n    \"/DCTDecode\": \"/DCTDecode\",\n    \"/RelativeColorimetric\": \"/RelativeColorimetric\",\n}\n\n_INLINE_IMAGE_KEY_MAPPING = {\n    \"/BPC\": \"/BitsPerComponent\",\n    \"/CS\": \"/ColorSpace\",\n    \"/D\": \"/Decode\",\n    \"/DP\": \"/DecodeParms\",\n    \"/F\": \"/Filter\",\n    \"/H\": \"/Height\",\n    \"/W\": \"/Width\",\n    \"/I\": \"/Interpolate\",\n    \"/Intent\": \"/Intent\",\n    \"/IM\": \"/ImageMask\",\n    \"/BitsPerComponent\": \"/BitsPerComponent\",\n    \"/ColorSpace\": \"/ColorSpace\",\n    \"/Decode\": \"/Decode\",\n    \"/DecodeParms\": \"/DecodeParms\",\n    \"/Filter\": \"/Filter\",\n    \"/Height\": \"/Height\",\n    \"/Width\": \"/Width\",\n    \"/Interpolate\": \"/Interpolate\",\n    \"/ImageMask\": \"/ImageMask\",\n}\n\n\nclass AFRelationship:\n    \"\"\"\n    Associated file relationship types, defining the relationship between\n    the PDF component and the associated file.\n\n    Defined in table 43 of the PDF 2.0 reference.\n    \"\"\"\n\n    SOURCE = \"/Source\"  # Original content source\n    DATA = \"/Data\"  # Base data for visual presentation\n    ALTERNATIVE = \"/Alternative\"  # Alternative content representation\n    SUPPLEMENT = \"/Supplement\"  # Supplemental representation of original source/data\n    ENCRYPTED_PAYLOAD = \"/EncryptedPayload\"  # Encrypted payload document\n    FORM_DATA = \"/FormData\"  # Data associated with AcroForm of this PDF\n    SCHEMA = \"/Schema\"  # Schema definition for associated object\n    UNSPECIFIED = \"/Unspecified\"  # Not known or cannot be described with values\n\n\nclass BorderStyles:\n    \"\"\"\n    A class defining border styles used in PDF documents.\n\n    Defined in table 168 of the PDF 2.0 reference.\n    \"\"\"\n\n    BEVELED = \"/B\"\n    DASHED = \"/D\"\n    INSET = \"/I\"\n    SOLID = \"/S\"\n    UNDERLINED = \"/U\"\n\n\nclass FontFlags(IntFlag):\n    \"\"\"\n    A class defining font flags in PDF document font descriptor resources.\n\n    Defined in table 121 of the PDF 2.0 reference.\n    \"\"\"\n\n    FIXED_PITCH = 1 << 0\n    SERIF = 1 << 1\n    SYMBOLIC = 1 << 2\n    SCRIPT = 1 << 3\n    NONSYMBOLIC = 1 << 5\n    ITALIC = 1 << 6\n    ALL_CAP = 1 << 16\n    SMALL_CAP = 1 << 17\n    FORCE_BOLD = 1 << 18\n"
  },
  {
    "path": "pypdf/errors.py",
    "content": "\"\"\"\nAll errors/exceptions pypdf raises and all of the warnings it uses.\n\nPlease note that broken PDF files might cause other Exceptions.\n\"\"\"\n\n\nclass DeprecationError(Exception):\n    \"\"\"Raised when a deprecated feature is used.\"\"\"\n\n\nclass DependencyError(Exception):\n    \"\"\"\n    Raised when a required dependency (a library or module that pypdf depends on)\n    is not available or cannot be imported.\n    \"\"\"\n\n\nclass PyPdfError(Exception):\n    \"\"\"Base class for all exceptions raised by pypdf.\"\"\"\n\n\nclass PdfReadError(PyPdfError):\n    \"\"\"Raised when there is an issue reading a PDF file.\"\"\"\n\n\nclass PageSizeNotDefinedError(PyPdfError):\n    \"\"\"Raised when the page size of a PDF document is not defined.\"\"\"\n\n\nclass PdfReadWarning(UserWarning):\n    \"\"\"Issued when there is a potential issue reading a PDF file, but it can still be read.\"\"\"\n\n\nclass PdfStreamError(PdfReadError):\n    \"\"\"Raised when there is an issue reading the stream of data in a PDF file.\"\"\"\n\n\nclass ParseError(PyPdfError):\n    \"\"\"\n    Raised when there is an issue parsing (analyzing and understanding the\n    structure and meaning of) a PDF file.\n    \"\"\"\n\n\nclass FileNotDecryptedError(PdfReadError):\n    \"\"\"\n    Raised when a PDF file that has been encrypted\n    (meaning it requires a password to be accessed) has not been successfully\n    decrypted.\n    \"\"\"\n\n\nclass WrongPasswordError(FileNotDecryptedError):\n    \"\"\"Raised when the wrong password is used to try to decrypt an encrypted PDF file.\"\"\"\n\n\nclass EmptyFileError(PdfReadError):\n    \"\"\"Raised when a PDF file is empty or has no content.\"\"\"\n\n\nclass EmptyImageDataError(PyPdfError):\n    \"\"\"Raised when trying to process an image that has no data.\"\"\"\n\n\nSTREAM_TRUNCATED_PREMATURELY = \"Stream has ended unexpectedly\"\n\n\nclass LimitReachedError(PyPdfError):\n    \"\"\"Raised when a limit is reached.\"\"\"\n\n\nclass XmpDocumentError(PyPdfError, RuntimeError):\n    \"\"\"Raised when the XMP XML document context is invalid or missing.\"\"\"\n"
  },
  {
    "path": "pypdf/filters.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\n\n\"\"\"\nImplementation of stream filters; §7.4 Filters of the PDF 2.0 specification.\n\n§8.9.7 Inline images of the PDF 2.0 specification has abbreviations that can be\nused for the names of filters in an inline image object.\n\"\"\"\n__author__ = \"Mathieu Fenniak\"\n__author_email__ = \"biziqe@mathieu.fenniak.net\"\n\nimport binascii\nimport math\nimport os\nimport shutil\nimport struct\nimport subprocess\nimport zlib\nfrom base64 import a85decode\nfrom dataclasses import dataclass\nfrom pathlib import Path\nfrom tempfile import TemporaryDirectory\nfrom typing import Any, Optional, Union, cast\n\nfrom ._codecs._codecs import LzwCodec as _LzwCodec\nfrom ._utils import (\n    WHITESPACES_AS_BYTES,\n    deprecate,\n    deprecation_with_replacement,\n    logger_warning,\n)\nfrom .constants import CcittFaxDecodeParameters as CCITT\nfrom .constants import FilterTypeAbbreviations as FTA\nfrom .constants import FilterTypes as FT\nfrom .constants import ImageAttributes as IA\nfrom .constants import LzwFilterParameters as LZW\nfrom .constants import StreamAttributes as SA\nfrom .errors import DependencyError, LimitReachedError, PdfReadError, PdfStreamError\nfrom .generic import (\n    ArrayObject,\n    DictionaryObject,\n    IndirectObject,\n    NullObject,\n    NumberObject,\n    StreamObject,\n    is_null_or_none,\n)\n\nMAX_DECLARED_STREAM_LENGTH = 75_000_000\nMAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH = 75_000_000\n\nJBIG2_MAX_OUTPUT_LENGTH = 75_000_000\nLZW_MAX_OUTPUT_LENGTH = 75_000_000\nRUN_LENGTH_MAX_OUTPUT_LENGTH = 75_000_000\nZLIB_MAX_OUTPUT_LENGTH = 75_000_000\nZLIB_MAX_RECOVERY_INPUT_LENGTH = 5_000_000\n\n# Reuse cached 1-byte values in the fallback loop to avoid per-byte allocations.\n_SINGLE_BYTES = tuple(bytes((i,)) for i in range(256))\n\n\ndef _decompress_with_limit(data: bytes) -> bytes:\n    decompressor = zlib.decompressobj()\n    result = decompressor.decompress(data, max_length=ZLIB_MAX_OUTPUT_LENGTH)\n    if decompressor.unconsumed_tail:\n        raise LimitReachedError(\n            f\"Limit reached while decompressing. {len(decompressor.unconsumed_tail)} bytes remaining.\"\n        )\n    return result\n\n\ndef decompress(data: bytes) -> bytes:\n    \"\"\"\n    Decompress the given data using zlib.\n\n    Attempts to decompress the input data using zlib.\n    If the decompression fails due to a zlib error, it falls back\n    to using a decompression object with a larger window size.\n\n    Please note that the output length is limited to avoid memory\n    issues. If you need to process larger content streams, consider\n    adapting ``pypdf.filters.ZLIB_MAX_OUTPUT_LENGTH``. In case you\n    are only dealing with trusted inputs and/or want to disable these\n    limits, set the value to `0`.\n\n    Args:\n        data: The input data to be decompressed.\n\n    Returns:\n        The decompressed data.\n\n    \"\"\"\n    try:\n        return _decompress_with_limit(data)\n    except zlib.error:\n        # First quick approach: There are known issues with faulty added bytes to the\n        # tail of the encoded stream from early Adobe Distiller or Pitstop versions\n        # with CR char as the default line separator (assumed by reverse engineering)\n        # that breaks the decoding process in the end.\n        #\n        # Try first to cut off some of the tail byte by byte, but limited to not\n        # iterate through too many loops and kill the performance for large streams,\n        # to then allow the final fallback to run. Added this intermediate attempt,\n        # because starting from the head of the stream byte by byte kills completely\n        # the performance for large streams (e.g., 6 MB) with the tail-byte-issue\n        # and takes ages. This solution is really fast:\n        max_tail_cut_off_bytes: int = 8\n        for i in range(1, min(max_tail_cut_off_bytes + 1, len(data))):\n            try:\n                return _decompress_with_limit(data[:-i])\n            except zlib.error:\n                pass\n\n        # If still failing, then try with increased window size.\n        decompressor = zlib.decompressobj(zlib.MAX_WBITS | 32)\n        result_str = b\"\"\n        remaining_limit = ZLIB_MAX_OUTPUT_LENGTH\n        data_length = len(data)\n        known_errors = set()\n        for index in range(data_length):\n            chunk = _SINGLE_BYTES[data[index]]\n            try:\n                decompressed = decompressor.decompress(chunk, max_length=remaining_limit)\n                result_str += decompressed\n                remaining_limit -= len(decompressed)\n                if remaining_limit <= 0:\n                    raise LimitReachedError(\n                        f\"Limit reached while decompressing. {data_length - index} bytes remaining.\"\n                    )\n            except zlib.error as error:\n                if index > ZLIB_MAX_RECOVERY_INPUT_LENGTH:\n                    raise LimitReachedError(\n                        f\"Recovery limit reached while decompressing. {data_length - index} bytes remaining.\"\n                    )\n                error_str = str(error)\n                if error_str in known_errors:\n                    continue\n                logger_warning(error_str, __name__)\n                known_errors.add(error_str)\n        return result_str\n\n\nclass FlateDecode:\n    @staticmethod\n    def decode(\n        data: bytes,\n        decode_parms: Optional[DictionaryObject] = None,\n        **kwargs: Any,\n    ) -> bytes:\n        \"\"\"\n        Decode data which is flate-encoded.\n\n        Args:\n          data: Flate-encoded data.\n          decode_parms: Additional decoding parameters.\n\n        Returns:\n          The flate-decoded data.\n\n        Raises:\n          PdfReadError: Unsupported parameters have been found.\n\n        \"\"\"\n        str_data = decompress(data)\n\n        if isinstance(decode_parms, DictionaryObject):\n            parameters = decode_parms\n        else:\n            parameters = DictionaryObject()\n\n        predictor = parameters.get(\"/Predictor\", 1)\n\n        # predictor 1 == no predictor\n        if predictor != 1:\n            columns, colors, bits_per_component = FlateDecode._get_parameters(parameters)\n\n            # PNG predictor can vary by row and so is the lead byte on each row\n            rowlength = (\n                math.ceil(columns * colors * bits_per_component / 8) + 1\n            )  # number of bytes\n\n            # TIFF prediction:\n            if predictor == 2:\n                rowlength -= 1  # remove the predictor byte\n                bpp = rowlength // columns\n                str_data = bytearray(str_data)\n                for i in range(len(str_data)):\n                    if i % rowlength >= bpp:\n                        str_data[i] = (str_data[i] + str_data[i - bpp]) % 256\n                str_data = bytes(str_data)\n            # PNG prediction:\n            elif 10 <= predictor <= 15:\n                str_data = FlateDecode._decode_png_prediction(\n                    str_data, columns, rowlength\n                )\n            else:\n                raise PdfReadError(f\"Unsupported flatedecode predictor {predictor!r}\")\n        return str_data\n\n    @staticmethod\n    def _get_parameters(parameters: DictionaryObject) -> tuple[int, int, int]:\n        # For details, see table 8 of ISO 32000-2:2020.\n        def get(key: str, default: int) -> int:\n            _value = parameters.get(key, NumberObject(default)).get_object()\n            if not isinstance(_value, int) or _value < 1:\n                raise PdfReadError(f\"Expected positive number for {key}, got {_value}!\")\n            return _value\n\n        columns = get(key=LZW.COLUMNS, default=1)\n        colors = get(key=LZW.COLORS, default=1)\n        bits_per_component = get(key=LZW.BITS_PER_COMPONENT, default=8)\n        return columns, colors, bits_per_component\n\n    @staticmethod\n    def _decode_png_prediction(data: bytes, columns: int, rowlength: int) -> bytes:\n        # PNG prediction can vary from row to row\n        if (remainder := len(data) % rowlength) != 0:\n            logger_warning(\"Image data is not rectangular. Adding padding.\", __name__)\n            data += b\"\\x00\" * (rowlength - remainder)\n            assert len(data) % rowlength == 0\n        output = []\n        prev_rowdata = (0,) * rowlength\n        bpp = (rowlength - 1) // columns  # recomputed locally to not change params\n        for row in range(0, len(data), rowlength):\n            rowdata: list[int] = list(data[row : row + rowlength])\n            filter_byte = rowdata[0]\n\n            if filter_byte == 0:\n                # PNG None Predictor\n                pass\n            elif filter_byte == 1:\n                # PNG Sub Predictor\n                for i in range(bpp + 1, rowlength):\n                    rowdata[i] = (rowdata[i] + rowdata[i - bpp]) % 256\n            elif filter_byte == 2:\n                # PNG Up Predictor\n                for i in range(1, rowlength):\n                    rowdata[i] = (rowdata[i] + prev_rowdata[i]) % 256\n            elif filter_byte == 3:\n                # PNG Average Predictor\n                for i in range(1, bpp + 1):\n                    floor = prev_rowdata[i] // 2\n                    rowdata[i] = (rowdata[i] + floor) % 256\n                for i in range(bpp + 1, rowlength):\n                    left = rowdata[i - bpp]\n                    floor = (left + prev_rowdata[i]) // 2\n                    rowdata[i] = (rowdata[i] + floor) % 256\n            elif filter_byte == 4:\n                # PNG Paeth Predictor\n                for i in range(1, bpp + 1):\n                    rowdata[i] = (rowdata[i] + prev_rowdata[i]) % 256\n                for i in range(bpp + 1, rowlength):\n                    left = rowdata[i - bpp]\n                    up = prev_rowdata[i]\n                    up_left = prev_rowdata[i - bpp]\n\n                    p = left + up - up_left\n                    dist_left = abs(p - left)\n                    dist_up = abs(p - up)\n                    dist_up_left = abs(p - up_left)\n\n                    if dist_left <= dist_up and dist_left <= dist_up_left:\n                        paeth = left\n                    elif dist_up <= dist_up_left:\n                        paeth = up\n                    else:\n                        paeth = up_left\n\n                    rowdata[i] = (rowdata[i] + paeth) % 256\n            else:\n                raise PdfReadError(\n                    f\"Unsupported PNG filter {filter_byte!r}\"\n                )  # pragma: no cover\n            prev_rowdata = tuple(rowdata)\n            output.extend(rowdata[1:])\n        return bytes(output)\n\n    @staticmethod\n    def encode(data: bytes, level: int = -1) -> bytes:\n        \"\"\"\n        Compress the input data using zlib.\n\n        Args:\n            data: The data to be compressed.\n            level: See https://docs.python.org/3/library/zlib.html#zlib.compress\n\n        Returns:\n            The compressed data.\n\n        \"\"\"\n        return zlib.compress(data, level)\n\n\nclass ASCIIHexDecode:\n    \"\"\"\n    The ASCIIHexDecode filter decodes data that has been encoded in ASCII\n    hexadecimal form into a base-7 ASCII format.\n    \"\"\"\n\n    @staticmethod\n    def decode(\n        data: Union[str, bytes],\n        decode_parms: Optional[DictionaryObject] = None,\n        **kwargs: Any,\n    ) -> bytes:\n        \"\"\"\n        Decode an ASCII-Hex encoded data stream.\n\n        Args:\n          data: a str sequence of hexadecimal-encoded values to be\n            converted into a base-7 ASCII string\n          decode_parms: this filter does not use parameters.\n\n        Returns:\n          A string conversion in base-7 ASCII, where each of its values\n          v is such that 0 <= ord(v) <= 127.\n\n        Raises:\n          PdfStreamError:\n\n        \"\"\"\n        if isinstance(data, str):\n            data = data.encode()\n\n        # Stop at EOD\n        eod = data.find(b\">\")\n        if eod == -1:\n            logger_warning(\n                \"missing EOD in ASCIIHexDecode, check if output is OK\",\n                __name__,\n            )\n            hex_data = data\n        else:\n            hex_data = data[:eod]\n\n        # Remove whitespace\n        hex_data = b\"\".join(hex_data.split())\n\n        # Pad if odd length\n        if len(hex_data) % 2 == 1:\n            hex_data += b\"0\"\n\n        return binascii.unhexlify(hex_data)\n\n\nclass RunLengthDecode:\n    \"\"\"\n    The RunLengthDecode filter decodes data that has been encoded in a\n    simple byte-oriented format based on run length.\n    The encoded data is a sequence of runs, where each run consists of\n    a length byte followed by 1 to 128 bytes of data. If the length byte is\n    in the range 0 to 127,\n    the following length + 1 (1 to 128) bytes are copied literally during\n    decompression.\n    If length is in the range 129 to 255, the following single byte is to be\n    copied 257 − length (2 to 128) times during decompression. A length value\n    of 128 denotes EOD.\n    \"\"\"\n\n    @staticmethod\n    def decode(\n        data: bytes,\n        decode_parms: Optional[DictionaryObject] = None,\n        **kwargs: Any,\n    ) -> bytes:\n        \"\"\"\n        Decode a run length encoded data stream.\n\n        Args:\n          data: a bytes sequence of length/data\n          decode_parms: this filter does not use parameters.\n\n        Returns:\n          A bytes decompressed sequence.\n\n        Raises:\n          PdfStreamError:\n\n        \"\"\"\n        lst = []\n        index = 0\n        data_length = len(data)\n        total_length = 0\n        while True:\n            if index >= data_length:\n                logger_warning(\n                    \"missing EOD in RunLengthDecode, check if output is OK\", __name__\n                )\n                break  # Reached end of string without an EOD\n            length = data[index]\n            index += 1\n            if length == 128:\n                if index < data_length:\n                    # We should first check, if we have an inner stream from a multi-encoded\n                    # stream with a faulty trailing newline that we can decode properly.\n                    # We will just ignore the last byte and raise a warning ...\n                    if (index == data_length - 1) and (data[index : index + 1] == b\"\\n\"):\n                        logger_warning(\n                            \"Found trailing newline in stream data, check if output is OK\", __name__\n                        )\n                        break\n                    # Raising an exception here breaks all image extraction for this file, which might\n                    # not be desirable. For this reason, indicate that the output is most likely wrong,\n                    # as processing stopped after the first EOD marker. See issue #3517.\n                    logger_warning(\n                        \"Early EOD in RunLengthDecode, check if output is OK\", __name__\n                    )\n                break\n            if length < 128:\n                length += 1\n                lst.append(data[index : (index + length)])\n                index += length\n            else:  # >128\n                length = 257 - length\n                lst.append(bytes((data[index],)) * length)\n                index += 1\n            total_length += length\n            if total_length > RUN_LENGTH_MAX_OUTPUT_LENGTH:\n                raise LimitReachedError(\"Limit reached while decompressing.\")\n        return b\"\".join(lst)\n\n\nclass LZWDecode:\n    class Decoder:\n        STOP = 257\n        CLEARDICT = 256\n\n        def __init__(self, data: bytes) -> None:\n            self.data = data\n\n        def decode(self) -> bytes:\n            return _LzwCodec(max_output_length=LZW_MAX_OUTPUT_LENGTH).decode(self.data)\n\n    @staticmethod\n    def decode(\n        data: bytes,\n        decode_parms: Optional[DictionaryObject] = None,\n        **kwargs: Any,\n    ) -> bytes:\n        \"\"\"\n        Decode an LZW encoded data stream.\n\n        Args:\n          data: ``bytes`` or ``str`` text to decode.\n          decode_parms: a dictionary of parameter values.\n\n        Returns:\n          decoded data.\n\n        \"\"\"\n        # decode_parms is unused here\n        return LZWDecode.Decoder(data).decode()\n\n\nclass ASCII85Decode:\n    \"\"\"Decodes string ASCII85-encoded data into a byte format.\"\"\"\n\n    @staticmethod\n    def decode(\n        data: Union[str, bytes],\n        decode_parms: Optional[DictionaryObject] = None,\n        **kwargs: Any,\n    ) -> bytes:\n        \"\"\"\n        Decode an Ascii85 encoded data stream.\n\n        Args:\n          data: ``bytes`` or ``str`` text to decode.\n          decode_parms: this filter does not use parameters.\n\n        Returns:\n          decoded data.\n\n        \"\"\"\n        if isinstance(data, str):\n            data = data.encode()\n        data = data.strip(WHITESPACES_AS_BYTES)\n        if len(data) > 2 and data.endswith(b\">\"):\n            data = data[:-1].rstrip(WHITESPACES_AS_BYTES) + data[-1:]\n        try:\n            return a85decode(data, adobe=True, ignorechars=WHITESPACES_AS_BYTES)\n        except ValueError as error:\n            if error.args[0] == \"Ascii85 encoded byte sequences must end with b'~>'\":\n                logger_warning(\"Ignoring missing Ascii85 end marker.\", __name__)\n                return a85decode(data, adobe=False, ignorechars=WHITESPACES_AS_BYTES)\n            raise\n\n\nclass DCTDecode:\n    @staticmethod\n    def decode(\n        data: bytes,\n        decode_parms: Optional[DictionaryObject] = None,\n        **kwargs: Any,\n    ) -> bytes:\n        \"\"\"\n        Decompresses data encoded using a DCT (discrete cosine transform)\n        technique based on the JPEG standard (IS0/IEC 10918),\n        reproducing image sample data that approximates the original data.\n\n        Args:\n          data: text to decode.\n          decode_parms: this filter does not use parameters.\n\n        Returns:\n          decoded data.\n\n        \"\"\"\n        return data\n\n\nclass JPXDecode:\n    @staticmethod\n    def decode(\n        data: bytes,\n        decode_parms: Optional[DictionaryObject] = None,\n        **kwargs: Any,\n    ) -> bytes:\n        \"\"\"\n        Decompresses data encoded using the wavelet-based JPEG 2000 standard,\n        reproducing the original image data.\n\n        Args:\n          data: text to decode.\n          decode_parms: this filter does not use parameters.\n\n        Returns:\n          decoded data.\n\n        \"\"\"\n        return data\n\n\n@dataclass\nclass CCITTParameters:\n    \"\"\"§7.4.6, optional parameters for the CCITTFaxDecode filter.\"\"\"\n\n    K: int = 0\n    columns: int = 1728\n    rows: int = 0\n    EndOfLine: Union[bool, None] = False\n    EncodedByteAlign: Union[bool, None] = False\n    EndOfBlock: Union[bool, None] = True\n    BlackIs1: bool = False\n    DamagedRowsBeforeError: Union[int, None] = 0\n\n    @property\n    def group(self) -> int:\n        if self.K < 0:\n            # Pure two-dimensional encoding (Group 4)\n            CCITTgroup = 4\n        else:\n            # K == 0: Pure one-dimensional encoding (Group 3, 1-D)\n            # K > 0: Mixed one- and two-dimensional encoding (Group 3, 2-D)\n            CCITTgroup = 3\n        return CCITTgroup\n\n\ndef __create_old_class_instance(\n    K: int = 0,\n    columns: int = 0,\n    rows: int = 0\n) -> CCITTParameters:\n    deprecation_with_replacement(\"CCITParameters\", \"CCITTParameters\", \"6.0.0\")\n    return CCITTParameters(K, columns, rows)\n\n\n# Create an alias for the old class name\nCCITParameters = __create_old_class_instance\n\n\nclass CCITTFaxDecode:\n    \"\"\"\n    §7.4.6, CCITTFaxDecode filter (ISO 32000).\n\n    Either Group 3 or Group 4 CCITT facsimile (fax) encoding.\n    CCITT encoding is bit-oriented, not byte-oriented.\n\n    §7.4.6, optional parameters for the CCITTFaxDecode filter.\n    \"\"\"\n\n    @staticmethod\n    def _get_parameters(\n        parameters: Union[None, ArrayObject, DictionaryObject, IndirectObject],\n        rows: Union[int, IndirectObject],\n    ) -> CCITTParameters:\n        ccitt_parameters = CCITTParameters(rows=int(rows))\n        if parameters:\n            parameters_unwrapped = cast(\n                Union[ArrayObject, DictionaryObject], parameters.get_object()\n            )\n            if isinstance(parameters_unwrapped, ArrayObject):\n                for decode_parm in parameters_unwrapped:\n                    if CCITT.K in decode_parm:\n                        ccitt_parameters.K = decode_parm[CCITT.K].get_object()\n                    if CCITT.COLUMNS in decode_parm:\n                        ccitt_parameters.columns = decode_parm[CCITT.COLUMNS].get_object()\n                    if CCITT.BLACK_IS_1 in decode_parm:\n                        ccitt_parameters.BlackIs1 = decode_parm[CCITT.BLACK_IS_1].get_object().value\n            else:\n                if CCITT.K in parameters_unwrapped:\n                    ccitt_parameters.K = parameters_unwrapped[CCITT.K].get_object()  # type: ignore\n                if CCITT.COLUMNS in parameters_unwrapped:\n                    ccitt_parameters.columns = parameters_unwrapped[CCITT.COLUMNS].get_object()  # type: ignore\n                if CCITT.BLACK_IS_1 in parameters_unwrapped:\n                    ccitt_parameters.BlackIs1 = parameters_unwrapped[CCITT.BLACK_IS_1].get_object().value  # type: ignore\n        return ccitt_parameters\n\n    @staticmethod\n    def decode(\n        data: bytes,\n        decode_parms: Optional[DictionaryObject] = None,\n        height: int = 0,\n        **kwargs: Any,\n    ) -> bytes:\n        params = CCITTFaxDecode._get_parameters(decode_parms, height)\n\n        img_size = len(data)\n        tiff_header_struct = \"<2shlh\" + \"hhll\" * 8 + \"h\"\n        tiff_header = struct.pack(\n            tiff_header_struct,\n            b\"II\",  # Byte order indication: Little endian\n            42,     # Version number (always 42)\n            8,      # Offset to the first image file directory (IFD)\n            8,      # Number of tags in IFD\n            256,    # ImageWidth, LONG, 1, width\n            4,\n            1,\n            params.columns,\n            257,    # ImageLength, LONG, 1, length\n            4,\n            1,\n            params.rows,\n            258,    # BitsPerSample, SHORT, 1, 1\n            3,\n            1,\n            1,\n            259,    # Compression, SHORT, 1, compression Type\n            3,\n            1,\n            params.group,\n            262,    # Thresholding, SHORT, 1, 0 = BlackIs1\n            3,\n            1,\n            int(params.BlackIs1),\n            273,    # StripOffsets, LONG, 1, length of header\n            4,\n            1,\n              struct.calcsize(\n                tiff_header_struct\n            ),\n            278,    # RowsPerStrip, LONG, 1, length\n            4,\n            1,\n            params.rows,\n            279,    # StripByteCounts, LONG, 1, size of image\n            4,\n            1,\n            img_size,\n            0,      # last IFD\n        )\n\n        return tiff_header + data\n\n\nJBIG2DEC_BINARY = shutil.which(\"jbig2dec\")\n\n\nclass JBIG2Decode:\n    @staticmethod\n    def decode(\n        data: bytes,\n        decode_parms: Optional[DictionaryObject] = None,\n        **kwargs: Any,\n    ) -> bytes:\n        if JBIG2DEC_BINARY is None:\n            raise DependencyError(\"jbig2dec binary is not available.\")\n\n        with TemporaryDirectory() as tempdir:\n            directory = Path(tempdir)\n            paths: list[Path] = []\n\n            if decode_parms and \"/JBIG2Globals\" in decode_parms:\n                jbig2_globals = decode_parms[\"/JBIG2Globals\"]\n                if not is_null_or_none(jbig2_globals) and not is_null_or_none(pointer := jbig2_globals.get_object()):\n                    assert pointer is not None, \"mypy\"\n                    if isinstance(pointer, StreamObject):\n                        path = directory.joinpath(\"globals.jbig2\")\n                        path.write_bytes(pointer.get_data())\n                        paths.append(path)\n\n            path = directory.joinpath(\"image.jbig2\")\n            path.write_bytes(data)\n            paths.append(path)\n\n            environment = os.environ.copy()\n            environment[\"LC_ALL\"] = \"C\"\n            result = subprocess.run(  # noqa: S603\n                [\n                    JBIG2DEC_BINARY,\n                    \"--embedded\",\n                    \"--format\", \"png\",\n                    \"--output\", \"-\",\n                    \"-M\", str(JBIG2_MAX_OUTPUT_LENGTH),\n                    *paths\n                ],\n                capture_output=True,\n                env=environment,\n            )\n            if b\"unrecognized option '--embedded'\" in result.stderr or b\"unrecognized option '-M'\" in result.stderr:\n                raise DependencyError(\"jbig2dec>=0.19 is required.\")\n            if b\"FATAL ERROR failed to allocate image data buffer\" in result.stderr:\n                raise LimitReachedError(\n                    f\"Memory limit reached while reading JBIG2 data:\\n{result.stderr.decode('utf-8')}\"\n                )\n            if result.stderr:\n                for line in result.stderr.decode(\"utf-8\").splitlines():\n                    logger_warning(line, __name__)\n            if result.returncode != 0:\n                raise PdfStreamError(f\"Unable to decode JBIG2 data. Exit code: {result.returncode}\")\n        return result.stdout\n\n    @staticmethod\n    def _is_binary_compatible() -> bool:\n        if not JBIG2DEC_BINARY:  # pragma: no cover\n            return False\n        result = subprocess.run(  # noqa: S603\n            [JBIG2DEC_BINARY, \"--version\"],\n            capture_output=True,\n            text=True,\n        )\n        version = result.stdout.split(\" \", maxsplit=1)[1]\n\n        from ._utils import Version  # noqa: PLC0415\n        return Version(version) >= Version(\"0.19\")\n\n\ndef _deprecate_inline_image_filters(filter_name: str, old_name: str, new_name: str) -> None:\n    if filter_name != old_name:\n        return\n    deprecate(\n        f\"The filter name {old_name} is deprecated and will be removed in pypdf 7.0.0. Use {new_name} instead.\",\n        4,\n    )\n\n\ndef decode_stream_data(stream: StreamObject) -> bytes:\n    \"\"\"\n    Decode the stream data based on the specified filters.\n\n    This function decodes the stream data using the filters provided in the\n    stream.\n\n    Args:\n        stream: The input stream object containing the data and filters.\n\n    Returns:\n        The decoded stream data.\n\n    Raises:\n        NotImplementedError: If an unsupported filter type is encountered.\n\n    \"\"\"\n    filters = stream.get(SA.FILTER, ())\n    if isinstance(filters, IndirectObject):\n        filters = cast(ArrayObject, filters.get_object())\n    if not isinstance(filters, ArrayObject):\n        # We have a single filter instance\n        filters = (filters,)\n    decode_parms = stream.get(SA.DECODE_PARMS, ({},) * len(filters))\n    if not isinstance(decode_parms, (list, tuple)):\n        decode_parms = (decode_parms,)\n    data: bytes = stream._data\n    # If there is no data to decode, we should not try to decode it.\n    if not data:\n        return data\n    for filter_name, params in zip(filters, decode_parms):\n        if isinstance(params, NullObject):\n            params = {}\n        if filter_name in (FT.ASCII_HEX_DECODE, FTA.AHx):\n            _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.AHx, new_name=FT.ASCII_HEX_DECODE)\n            data = ASCIIHexDecode.decode(data)\n        elif filter_name in (FT.ASCII_85_DECODE, FTA.A85):\n            _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.A85, new_name=FT.ASCII_85_DECODE)\n            data = ASCII85Decode.decode(data)\n        elif filter_name in (FT.LZW_DECODE, FTA.LZW):\n            _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.LZW, new_name=FT.LZW_DECODE)\n            data = LZWDecode.decode(data, params)\n        elif filter_name in (FT.FLATE_DECODE, FTA.FL):\n            _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.FL, new_name=FT.FLATE_DECODE)\n            data = FlateDecode.decode(data, params)\n        elif filter_name in (FT.RUN_LENGTH_DECODE, FTA.RL):\n            _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.RL, new_name=FT.RUN_LENGTH_DECODE)\n            data = RunLengthDecode.decode(data)\n        elif filter_name in (FT.CCITT_FAX_DECODE, FTA.CCF):\n            _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.CCF, new_name=FT.CCITT_FAX_DECODE)\n            height = stream.get(IA.HEIGHT, ())\n            data = CCITTFaxDecode.decode(data, params, height)\n        elif filter_name in (FT.DCT_DECODE, FTA.DCT):\n            _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.DCT, new_name=FT.DCT_DECODE)\n            data = DCTDecode.decode(data)\n        elif filter_name == FT.JPX_DECODE:\n            data = JPXDecode.decode(data)\n        elif filter_name == FT.JBIG2_DECODE:\n            data = JBIG2Decode.decode(data, params)\n        elif filter_name == \"/Crypt\":\n            if \"/Name\" in params or \"/Type\" in params:\n                raise NotImplementedError(\n                    \"/Crypt filter with /Name or /Type not supported yet\"\n                )\n        else:\n            raise NotImplementedError(f\"Unsupported filter {filter_name}\")\n    return data\n"
  },
  {
    "path": "pypdf/generic/__init__.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\n\"\"\"Implementation of generic PDF objects (dictionary, number, string, ...).\"\"\"\n__author__ = \"Mathieu Fenniak\"\n__author_email__ = \"biziqe@mathieu.fenniak.net\"\n\nfrom ..constants import OutlineFontFlag\nfrom ._base import (\n    BooleanObject,\n    ByteStringObject,\n    FloatObject,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    PdfObject,\n    TextStringObject,\n    encode_pdfdocencoding,\n    is_null_or_none,\n)\nfrom ._data_structures import (\n    ArrayObject,\n    ContentStream,\n    DecodedStreamObject,\n    Destination,\n    DictionaryObject,\n    EncodedStreamObject,\n    Field,\n    StreamObject,\n    TreeObject,\n    read_object,\n)\nfrom ._files import EmbeddedFile\nfrom ._fit import Fit\nfrom ._link import DirectReferenceLink, NamedReferenceLink, ReferenceLink, extract_links\nfrom ._outline import OutlineItem\nfrom ._rectangle import RectangleObject\nfrom ._utils import (\n    create_string_object,\n    decode_pdfdocencoding,\n    hex_to_rgb,\n    read_hex_string_from_stream,\n    read_string_from_stream,\n)\nfrom ._viewerpref import ViewerPreferences\n\nPAGE_FIT = Fit.fit()\n\n\n__all__ = [\n    \"PAGE_FIT\",\n    \"ArrayObject\",\n    \"BooleanObject\",\n    \"ByteStringObject\",\n    \"ContentStream\",\n    \"DecodedStreamObject\",\n    \"Destination\",\n    \"DictionaryObject\",\n    \"DirectReferenceLink\",\n    \"EmbeddedFile\",\n    \"EncodedStreamObject\",\n    \"Field\",\n    \"Fit\",\n    \"FloatObject\",\n    \"IndirectObject\",\n    \"NameObject\",\n    \"NamedReferenceLink\",\n    \"NullObject\",\n    \"NumberObject\",\n    \"OutlineFontFlag\",\n    \"OutlineItem\",\n    \"PdfObject\",\n    \"RectangleObject\",\n    \"ReferenceLink\",\n    \"StreamObject\",\n    \"TextStringObject\",\n    \"TreeObject\",\n    \"ViewerPreferences\",\n    # Utility functions\n    \"create_string_object\",\n    \"decode_pdfdocencoding\",\n    \"encode_pdfdocencoding\",\n    \"extract_links\",\n    \"hex_to_rgb\",\n    \"is_null_or_none\",\n    \"read_hex_string_from_stream\",\n    # Data structures core functions\n    \"read_object\",\n    \"read_string_from_stream\",\n]\n"
  },
  {
    "path": "pypdf/generic/_appearance_stream.py",
    "content": "import re\nfrom dataclasses import dataclass\nfrom enum import IntEnum\nfrom typing import Any, Optional, Union, cast\n\nfrom .._codecs import fill_from_encoding\nfrom .._codecs.core_font_metrics import CORE_FONT_METRICS\nfrom .._font import Font\nfrom .._utils import logger_warning\nfrom ..constants import AnnotationDictionaryAttributes, BorderStyles, FieldDictionaryAttributes\nfrom ..generic import (\n    DecodedStreamObject,\n    DictionaryObject,\n    NameObject,\n    NumberObject,\n    RectangleObject,\n)\nfrom ..generic._base import ByteStringObject, TextStringObject, is_null_or_none\n\nDEFAULT_FONT_SIZE_IN_MULTILINE = 12\n\n\n@dataclass\nclass BaseStreamConfig:\n    \"\"\"A container representing the basic layout of an appearance stream.\"\"\"\n    rectangle: Union[RectangleObject, tuple[float, float, float, float]] = (0.0, 0.0, 0.0, 0.0)\n    border_width: int = 1  # The width of the border in points\n    border_style: str = BorderStyles.SOLID\n\n\nclass BaseStreamAppearance(DecodedStreamObject):\n    \"\"\"A class representing the very base of an appearance stream, that is, a rectangle and a border.\"\"\"\n\n    def __init__(self, layout: Optional[BaseStreamConfig] = None) -> None:\n        \"\"\"\n        Takes the appearance stream layout as an argument.\n\n        Args:\n            layout: The basic layout parameters.\n        \"\"\"\n        super().__init__()\n        self._layout = layout or BaseStreamConfig()\n        self[NameObject(\"/Type\")] = NameObject(\"/XObject\")\n        self[NameObject(\"/Subtype\")] = NameObject(\"/Form\")\n        self[NameObject(\"/BBox\")] = RectangleObject(self._layout.rectangle)\n\n\nclass TextAlignment(IntEnum):\n    \"\"\"Defines the alignment options for text within a form field's appearance stream.\"\"\"\n\n    LEFT = 0\n    CENTER = 1\n    RIGHT = 2\n\n\nclass TextStreamAppearance(BaseStreamAppearance):\n    \"\"\"\n    A class representing the appearance stream for a text-based form field.\n\n    This class generates the content stream (the `ap_stream_data`) that dictates\n    how text is rendered within a form field's bounding box. It handles properties\n    like font, font size, color, multiline text, and text selection highlighting.\n    \"\"\"\n\n    def _scale_text(\n        self,\n        font: Font,\n        font_size: float,\n        leading_factor: float,\n        field_width: float,\n        field_height: float,\n        text: str,\n        min_font_size: float,\n        font_size_step: float = 0.2\n    ) -> tuple[list[tuple[float, str]], float]:\n        \"\"\"\n        Takes a piece of text and scales it to field_width or field_height, given font_name\n        and font_size. Wraps text where necessary.\n\n        Args:\n            font: The font to be used.\n            font_size: The font size in points.\n            leading_factor: The line distance.\n            field_width: The width of the field in which to fit the text.\n            field_height: The height of the field in which to fit the text.\n            text: The text to fit with the field.\n            min_font_size: The minimum font size at which to scale the text.\n            font_size_step: The amount by which to decrement font size per step while scaling.\n\n        Returns:\n            The text in the form of list of tuples, each tuple containing the length of a line\n            and its contents, and the font_size for these lines and lengths.\n        \"\"\"\n        orig_text = text\n        paragraphs = text.replace(\"\\n\", \"\\r\").split(\"\\r\")\n        wrapped_lines = []\n        current_line_words: list[str] = []\n        current_line_width: float = 0\n        space_width = font.space_width * font_size / 1000\n        for paragraph in paragraphs:\n            if not paragraph.strip():\n                wrapped_lines.append((0.0, \"\"))\n                continue\n            words = paragraph.split(\" \")\n            for i, word in enumerate(words):\n                word_width = font.text_width(word) * font_size / 1000\n                test_width = current_line_width + word_width + (space_width if i else 0)\n                if test_width > field_width and current_line_words:\n                    wrapped_lines.append((current_line_width, \" \".join(current_line_words)))\n                    current_line_words = [word]\n                    current_line_width = word_width\n                elif not current_line_words and word_width > field_width:\n                    wrapped_lines.append((word_width, word))\n                    current_line_words = []\n                    current_line_width = 0\n                else:\n                    if current_line_words:\n                        current_line_width += space_width\n                    current_line_words.append(word)\n                    current_line_width += word_width\n            if current_line_words:\n                wrapped_lines.append((current_line_width, \" \".join(current_line_words)))\n                current_line_words = []\n                current_line_width = 0\n        # Estimate total height.\n        estimated_total_height = font_size + (len(wrapped_lines) - 1) * leading_factor * font_size\n        if estimated_total_height > field_height:\n            # Text overflows height; Retry with smaller font size.\n            new_font_size = font_size - font_size_step\n            if new_font_size >= min_font_size:\n                return self._scale_text(\n                    font,\n                    new_font_size,\n                    leading_factor,\n                    field_width,\n                    field_height,\n                    orig_text,\n                    min_font_size,\n                    font_size_step\n                )\n        return wrapped_lines, round(font_size, 1)\n\n    def _generate_appearance_stream_data(\n        self,\n        text: str,\n        selection: Union[list[str], None],\n        font: Font,\n        font_glyph_byte_map: Optional[dict[str, bytes]] = None,\n        font_name: str = \"/Helv\",\n        font_size: float = 0.0,\n        font_color: str = \"0 g\",\n        is_multiline: bool = False,\n        alignment: TextAlignment = TextAlignment.LEFT,\n        is_comb: bool = False,\n        max_length: Optional[int] = None\n    ) -> bytes:\n        \"\"\"\n        Generates the raw bytes of the PDF appearance stream for a text field.\n\n        This private method assembles the PDF content stream operators to draw\n        the provided text within the specified rectangle. It handles text positioning,\n        font application, color, and special formatting like selected text.\n\n        Args:\n            text: The text to be rendered in the form field.\n            selection: An optional list of strings that should be highlighted as selected.\n            font: The font to use.\n            font_glyph_byte_map: An optional dictionary mapping characters to their\n                byte representation for glyph encoding.\n            font_name: The name of the font resource to use (e.g., \"/Helv\").\n            font_size: The font size. If 0, it is automatically calculated\n                based on whether the field is multiline or not.\n            font_color: The color to apply to the font, represented as a PDF\n                graphics state string (e.g., \"0 g\" for black).\n            is_multiline: A boolean indicating if the text field is multiline.\n            alignment: Text alignment, can be TextAlignment.LEFT, .RIGHT, or .CENTER.\n            is_comb: Boolean that designates fixed-length fields, where every character\n                fills one \"cell\", such as in a postcode.\n            max_length: Used if is_comb is set. The maximum number of characters for a fixed-\n                length field.\n\n        Returns:\n            A byte string containing the PDF content stream data.\n\n        \"\"\"\n        rectangle = self._layout.rectangle\n        font_glyph_byte_map = font_glyph_byte_map or {}\n        if isinstance(rectangle, tuple):\n            rectangle = RectangleObject(rectangle)\n        leading_factor = (font.font_descriptor.bbox[3] - font.font_descriptor.bbox[1]) / 1000.0\n\n        # Set margins based on border width and style, but never less than 1 point\n        factor = 2 if self._layout.border_style in {\"/B\", \"/I\"} else 1\n        margin = max(self._layout.border_width * factor, 1)\n        field_height = rectangle.height - 2 * margin\n        field_width = rectangle.width - 4 * margin\n\n        # If font_size is 0, apply the logic for multiline or large-as-possible font\n        if font_size == 0:\n            min_font_size = 4.0       # The mininum font size\n            if selection:             # Don't wrap text when dealing with a /Ch field, in order to prevent problems\n                is_multiline = False  # with matching \"selection\" with \"line\" later on.\n            if is_multiline:\n                font_size = DEFAULT_FONT_SIZE_IN_MULTILINE\n                lines, font_size = self._scale_text(\n                    font,\n                    font_size,\n                    leading_factor,\n                    field_width,\n                    field_height,\n                    text,\n                    min_font_size\n                )\n            else:\n                max_vertical_size = field_height / leading_factor\n                text_width_unscaled = font.text_width(text) / 1000\n                max_horizontal_size = field_width / (text_width_unscaled or 1)\n                font_size = round(max(min(max_vertical_size, max_horizontal_size), min_font_size), 1)\n                lines = [(text_width_unscaled * font_size, text)]\n        elif is_comb:\n            if max_length and len(text) > max_length:\n                logger_warning (\n                    f\"Length of text {text} exceeds maximum length ({max_length}) of field, input truncated.\",\n                    __name__\n                )\n            # We act as if each character is one line, because we draw it separately later on\n            lines = [(\n                font.text_width(char) * font_size / 1000,\n                char\n            ) for index, char in enumerate(text) if index < (max_length or len(text))]\n        else:\n            lines = [(\n                font.text_width(line) * font_size / 1000,\n                line\n            ) for line in text.replace(\"\\n\", \"\\r\").split(\"\\r\")]\n\n        # Set the vertical offset\n        if is_multiline:\n            y_offset = rectangle.height + margin - font.font_descriptor.bbox[3] * font_size / 1000.0\n        else:\n            y_offset = margin + ((field_height - font.font_descriptor.ascent * font_size / 1000) / 2)\n        default_appearance = f\"{font_name} {font_size} Tf {font_color}\"\n\n        ap_stream = (\n            f\"q\\n/Tx BMC \\nq\\n{2 * margin} {margin} {field_width} {field_height} \"\n            f\"re\\nW\\nBT\\n{default_appearance}\\n\"\n        ).encode()\n        current_x_pos: float = 0  # Initial virtual position within the text object.\n\n        for line_number, (line_width, line) in enumerate(lines):\n            if selection and line in selection:\n                # Might be improved, but cannot find how to get fill working => replaced with lined box\n                ap_stream += (\n                    f\"1 {y_offset - (line_number * font_size * leading_factor) - 1} \"\n                    f\"{rectangle.width - 2} {font_size + 2} re\\n\"\n                    f\"0.5 0.5 0.5 rg s\\n{default_appearance}\\n\"\n                ).encode()\n\n            # Calculate the desired absolute starting X for the current line\n            desired_abs_x_start: float = 0\n            if is_comb and max_length:\n                # Calculate the width of a cell for one character\n                cell_width = rectangle.width / max_length\n                # Space from the left edge of the cell to the character's baseline start\n                # line_width here is the *actual* character width in points for the single character 'line'\n                centering_offset_in_cell = (cell_width - line_width) / 2\n                # Absolute start X = (Cell Index, i.e., line_number * Cell Width) + Centering Offset\n                desired_abs_x_start = (line_number * cell_width) + centering_offset_in_cell\n            elif alignment == TextAlignment.RIGHT:\n                desired_abs_x_start = rectangle.width - margin * 2 - line_width\n            elif alignment == TextAlignment.CENTER:\n                desired_abs_x_start = (rectangle.width - line_width) / 2\n            else:  # Left aligned; default\n                desired_abs_x_start = margin * 2\n            # Calculate x_rel_offset: how much to move from the current_x_pos\n            # to reach the desired_abs_x_start.\n            x_rel_offset = desired_abs_x_start - current_x_pos\n\n            # Y-offset:\n            y_rel_offset: float = 0\n            if line_number == 0:\n                y_rel_offset = y_offset  # Initial vertical position\n            elif is_comb:\n                y_rel_offset = 0.0  # DO NOT move vertically for subsequent characters\n            else:\n                y_rel_offset = - font_size * leading_factor  # Move down by line height\n\n            # Td is a relative translation (Tx and Ty).\n            # It updates the current text position.\n            ap_stream += f\"{x_rel_offset} {y_rel_offset} Td\\n\".encode()\n            # Update current_x_pos based on the Td operation for the next iteration.\n            # This is the X position where the *current line* will start.\n            current_x_pos = desired_abs_x_start\n\n            encoded_line: list[bytes] = [\n                font_glyph_byte_map.get(c, c.encode(\"utf-16-be\")) for c in line\n            ]\n            if any(len(c) >= 2 for c in encoded_line):\n                ap_stream += b\"<\" + (b\"\".join(encoded_line)).hex().encode() + b\"> Tj\\n\"\n            else:\n                ap_stream += b\"(\" + b\"\".join(encoded_line) + b\") Tj\\n\"\n        ap_stream += b\"ET\\nQ\\nEMC\\nQ\\n\"\n        return ap_stream\n\n    def __init__(\n        self,\n        layout: Optional[BaseStreamConfig] = None,\n        text: str = \"\",\n        selection: Optional[list[str]] = None,\n        font_resource: Optional[DictionaryObject] = None,\n        font_name: str = \"/Helv\",\n        font_size: float = 0.0,\n        font_color: str = \"0 g\",\n        is_multiline: bool = False,\n        alignment: TextAlignment = TextAlignment.LEFT,\n        is_comb: bool = False,\n        max_length: Optional[int] = None\n    ) -> None:\n        \"\"\"\n        Initializes a TextStreamAppearance object.\n\n        This constructor creates a new PDF stream object configured as an XObject\n        of subtype Form. It uses the `_appearance_stream_data` method to generate\n        the content for the stream.\n\n        Args:\n            layout: The basic layout parameters.\n            text: The text to be rendered in the form field.\n            selection: An optional list of strings that should be highlighted as selected.\n            font_resource: An optional variable that represents a PDF font dictionary.\n            font_name: The name of the font resource, e.g., \"/Helv\".\n            font_size: The font size. If 0, it's auto-calculated.\n            font_color: The font color string.\n            is_multiline: A boolean indicating if the text field is multiline.\n            alignment: Text alignment, can be TextAlignment.LEFT, .RIGHT, or .CENTER.\n            is_comb: Boolean that designates fixed-length fields, where every character\n                fills one \"cell\", such as in a postcode.\n            max_length: Used if is_comb is set. The maximum number of characters for a fixed-\n                length field.\n\n        \"\"\"\n        super().__init__(layout)\n\n        # If a font resource was added, get the font character map\n        if font_resource:\n            font = Font.from_font_resource(font_resource)\n        else:\n            logger_warning(f\"Font dictionary for {font_name} not found; defaulting to Helvetica.\", __name__)\n            font_name = \"/Helv\"\n            core_font_metrics = CORE_FONT_METRICS[\"Helvetica\"]\n            font = Font(\n                name=\"Helvetica\",\n                character_map={},\n                encoding=dict(zip(range(256), fill_from_encoding(\"cp1252\"))),  # WinAnsiEncoding\n                sub_type=\"Type1\",\n                font_descriptor=core_font_metrics.font_descriptor,\n                character_widths=core_font_metrics.character_widths\n            )\n            font_resource = font.as_font_resource()\n\n        # Check whether the font resource is able to encode the text value.\n        encodable = True\n        try:\n            if isinstance(font.encoding, str):\n                text.encode(font.encoding, \"surrogatepass\")\n            else:\n                supported_chars = set(font.encoding.values())\n                if any(char not in supported_chars for char in text):\n                    encodable = False\n            # We should add a final check against the character_map (CMap) of the font,\n            # but we don't appear to have PDF forms with such fonts, so we skip this for\n            # now.\n\n        except UnicodeEncodeError:\n            encodable = False\n\n        if not encodable:\n            logger_warning(\n                f\"Text string '{text}' contains characters not supported by font encoding. \"\n                \"This may result in text corruption. \"\n                \"Consider calling writer.update_page_form_field_values with auto_regenerate=True.\",\n                __name__\n            )\n\n        font_glyph_byte_map: dict[str, bytes]\n        if isinstance(font.encoding, str):\n            font_glyph_byte_map = {\n                v: k.encode(font.encoding) for k, v in font.character_map.items()\n            }\n        else:\n            font_glyph_byte_map = {v: bytes((k,)) for k, v in font.encoding.items()}\n            font_encoding_rev = {v: bytes((k,)) for k, v in font.encoding.items()}\n            for key, value in font.character_map.items():\n                font_glyph_byte_map[value] = font_encoding_rev.get(key, key)\n\n        ap_stream_data = self._generate_appearance_stream_data(\n            text,\n            selection,\n            font,\n            font_glyph_byte_map,\n            font_name=font_name,\n            font_size=font_size,\n            font_color=font_color,\n            is_multiline=is_multiline,\n            alignment=alignment,\n            is_comb=is_comb,\n            max_length=max_length\n        )\n\n        self.set_data(ByteStringObject(ap_stream_data))\n        self[NameObject(\"/Length\")] = NumberObject(len(ap_stream_data))\n        # Update Resources with font information\n        self[NameObject(\"/Resources\")] = DictionaryObject({\n            NameObject(\"/Font\"): DictionaryObject({\n                NameObject(font_name): getattr(font_resource, \"indirect_reference\", font_resource)\n            })\n        })\n\n    @staticmethod\n    def _find_annotation_font_resource(\n            font_name: str,\n            annotation: DictionaryObject,\n            acro_form: DictionaryObject\n        ) -> tuple[str, DictionaryObject]:\n        # Try to find a resource dictionary for the font by examining the annotation and, if that fails,\n        # the AcroForm resources dictionary\n        acro_form_resources: Any = cast(\n            DictionaryObject,\n            annotation.get_inherited(\n                \"/DR\",\n                acro_form.get(\"/DR\", DictionaryObject()),\n            ),\n        )\n        acro_form_font_resources = acro_form_resources.get(\"/Font\", DictionaryObject())\n        font_resource = acro_form_font_resources.get(font_name, None)\n\n        # Normally, we should have found a font resource by now. However, when a user has provided a specific\n        # font name, we may not have found the associated font resource among the AcroForm resources. Also, in\n        # case of the 14 Adobe Core fonts, we may be expected to construct a font resource ourselves.\n        if is_null_or_none(font_resource):\n            if font_name.removeprefix(\"/\") not in CORE_FONT_METRICS:\n                # Default to Helvetica if we haven't found a font resource and cannot construct one ourselves.\n                logger_warning(f\"Font dictionary for {font_name} not found; defaulting to Helvetica.\", __name__)\n                font_name = \"/Helvetica\"\n            core_font_metrics = CORE_FONT_METRICS[font_name.removeprefix(\"/\")]\n            font_resource = Font(\n                name=font_name.removeprefix(\"/\"),\n                character_map={},\n                encoding=dict(zip(range(256), fill_from_encoding(\"cp1252\"))),  # WinAnsiEncoding\n                sub_type=\"Type1\",\n                font_descriptor=core_font_metrics.font_descriptor,\n                character_widths=core_font_metrics.character_widths\n            ).as_font_resource()\n\n        return font_name, font_resource\n\n    @classmethod\n    def from_text_annotation(\n        cls,\n        acro_form: DictionaryObject,  # _root_object[CatalogDictionary.ACRO_FORM])\n        field: DictionaryObject,\n        annotation: DictionaryObject,\n        user_font_name: str = \"\",\n        user_font_size: float = -1,\n    ) -> \"TextStreamAppearance\":\n        \"\"\"\n        Creates a TextStreamAppearance object from a text field annotation.\n\n        This class method is a factory for creating a `TextStreamAppearance`\n        instance by extracting all necessary information (bounding box, font,\n        text content, etc.) from the PDF field and annotation dictionaries.\n        It respects inheritance for properties like default appearance (`/DA`).\n\n        Args:\n            acro_form: The root AcroForm dictionary from the PDF catalog.\n            field: The field dictionary object.\n            annotation: The widget annotation dictionary object associated with the field.\n            user_font_name: An optional user-provided font name to override the\n                default. Defaults to an empty string.\n            user_font_size: An optional user-provided font size to override the\n                default. A value of -1 indicates no override.\n\n        Returns:\n            A new `TextStreamAppearance` instance configured for the given field.\n\n        \"\"\"\n        # Calculate rectangle dimensions\n        _rectangle = cast(RectangleObject, annotation[AnnotationDictionaryAttributes.Rect])\n        rectangle = RectangleObject((0, 0, abs(_rectangle[2] - _rectangle[0]), abs(_rectangle[3] - _rectangle[1])))\n\n        # Get default appearance dictionary from annotation\n        default_appearance = annotation.get_inherited(\n            AnnotationDictionaryAttributes.DA,\n            acro_form.get(AnnotationDictionaryAttributes.DA, None),\n        )\n        if not default_appearance:\n            # Create a default appearance if none was found in the annotation\n            default_appearance = TextStringObject(\"/Helv 0 Tf 0 g\")\n        else:\n            default_appearance = default_appearance.get_object()\n\n        # Retrieve field text and selected values\n        field_flags = field.get(FieldDictionaryAttributes.Ff, 0)\n        if (\n                field.get(FieldDictionaryAttributes.FT, \"/Tx\") == \"/Ch\" and\n                field_flags & FieldDictionaryAttributes.FfBits.Combo == 0\n        ):\n            text = \"\\n\".join(annotation.get_inherited(FieldDictionaryAttributes.Opt, []))\n            selection = field.get(\"/V\", [])\n            if not isinstance(selection, list):\n                selection = [selection]\n        else:  # /Tx\n            text = field.get(\"/V\", \"\")\n            selection = []\n\n        # Escape parentheses (PDF 1.7 reference, table 3.2, Literal Strings)\n        text = text.replace(\"\\\\\", \"\\\\\\\\\").replace(\"(\", r\"\\(\").replace(\")\", r\"\\)\")\n\n        # Derive font name, size and color from the default appearance. Also set\n        # user-provided font name and font size in the default appearance, if given.\n        # For a font name, this presumes that we can find an associated font resource\n        # dictionary. Uses the variable font_properties as an intermediate.\n        # As per the PDF spec:\n        # \"At a minimum, the string [that is, default_appearance] shall include a Tf (text\n        # font) operator along with its two operands, font and size\" (Section 12.7.4.3\n        # \"Variable text\" of the PDF 2.0 specification).\n        font_properties = [prop for prop in re.split(r\"\\s\", default_appearance) if prop]\n        font_name = font_properties.pop(font_properties.index(\"Tf\") - 2)\n        font_size = float(font_properties.pop(font_properties.index(\"Tf\") - 1))\n        font_properties.remove(\"Tf\")\n        font_color = \" \".join(font_properties)\n        # Determine the font name to use, prioritizing the user's input\n        if user_font_name:\n            font_name = user_font_name\n        # Determine the font size to use, prioritizing the user's input\n        if user_font_size > 0:\n            font_size = user_font_size\n\n        font_name, font_resource = cls._find_annotation_font_resource(font_name, annotation, acro_form)\n\n        # Retrieve formatting information\n        is_comb = False\n        max_length = None\n        if field_flags & FieldDictionaryAttributes.FfBits.Comb:\n            is_comb = True\n            max_length = annotation.get(\"/MaxLen\")\n        is_multiline = False\n        if field_flags & FieldDictionaryAttributes.FfBits.Multiline:\n            is_multiline = True\n        alignment = field.get(\"/Q\", TextAlignment.LEFT)\n        border_width = 1\n        border_style = BorderStyles.SOLID\n        if \"/BS\" in field:\n            border_width = cast(DictionaryObject, field[\"/BS\"]).get(\"/W\", border_width)\n            border_style = cast(DictionaryObject, field[\"/BS\"]).get(\"/S\", border_style)\n\n        # Create the TextStreamAppearance instance\n        layout = BaseStreamConfig(rectangle=rectangle, border_width=border_width, border_style=border_style)\n        new_appearance_stream = cls(\n            layout,\n            text,\n            selection,\n            font_resource,\n            font_name=font_name,\n            font_size=font_size,\n            font_color=font_color,\n            is_multiline=is_multiline,\n            alignment=alignment,\n            is_comb=is_comb,\n            max_length=max_length\n        )\n\n        if AnnotationDictionaryAttributes.AP in annotation:\n            for key, value in (\n                cast(DictionaryObject, annotation[AnnotationDictionaryAttributes.AP]).get(\"/N\", {}).items()\n            ):\n                if key in {\"/BBox\", \"/Length\", \"/Subtype\", \"/Type\", \"/Filter\"}:\n                    continue\n                # Don't overwrite font resources added by TextAppearanceStream.__init__\n                if key == \"/Resources\":\n                    if \"/Font\" not in value:\n                        value.get_object()[NameObject(\"/Font\")] = DictionaryObject()\n                    value[\"/Font\"].get_object()[NameObject(font_name)] = getattr(\n                        font_resource, \"indirect_reference\", font_resource\n                    )\n                else:\n                    new_appearance_stream[key] = value\n\n        return new_appearance_stream\n"
  },
  {
    "path": "pypdf/generic/_base.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\nimport binascii\nimport codecs\nimport hashlib\nimport re\nimport sys\nfrom collections.abc import Sequence\nfrom math import log10\nfrom struct import iter_unpack\nfrom typing import Any, Callable, ClassVar, Optional, Union, cast\n\nif sys.version_info[:2] >= (3, 10):\n    from typing import TypeGuard\nelse:\n    from typing_extensions import TypeGuard  # PEP 647\n\nif sys.version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing_extensions import Self\n\nfrom .._codecs import _pdfdoc_encoding_rev\nfrom .._protocols import PdfObjectProtocol, PdfWriterProtocol\nfrom .._utils import (\n    StreamType,\n    classproperty,\n    deprecation_no_replacement,\n    deprecation_with_replacement,\n    logger_warning,\n    read_non_whitespace,\n    read_until_regex,\n)\nfrom ..errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError\n\n__author__ = \"Mathieu Fenniak\"\n__author_email__ = \"biziqe@mathieu.fenniak.net\"\n\n\nclass PdfObject(PdfObjectProtocol):\n    # function for calculating a hash value\n    hash_func: Callable[..., \"hashlib._Hash\"] = hashlib.sha1\n    indirect_reference: Optional[\"IndirectObject\"]\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        raise NotImplementedError(\n            f\"{self.__class__.__name__} does not implement .hash_bin() so far\"\n        )\n\n    def hash_value_data(self) -> bytes:\n        return f\"{self}\".encode()\n\n    def hash_value(self) -> bytes:\n        return (\n            f\"{self.__class__.__name__}:\"\n            f\"{self.hash_func(self.hash_value_data()).hexdigest()}\"\n        ).encode()\n\n    def replicate(\n        self,\n        pdf_dest: PdfWriterProtocol,\n    ) -> \"PdfObject\":\n        \"\"\"\n        Clone object into pdf_dest (PdfWriterProtocol which is an interface for PdfWriter)\n        without ensuring links. This is used in clone_document_from_root with incremental = True.\n\n        Args:\n          pdf_dest: Target to clone to.\n\n        Returns:\n          The cloned PdfObject\n\n        \"\"\"\n        return self.clone(pdf_dest)\n\n    def clone(\n        self,\n        pdf_dest: PdfWriterProtocol,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"PdfObject\":\n        \"\"\"\n        Clone object into pdf_dest (PdfWriterProtocol which is an interface for PdfWriter).\n\n        By default, this method will call ``_reference_clone`` (see ``_reference``).\n\n\n        Args:\n          pdf_dest: Target to clone to.\n          force_duplicate: By default, if the object has already been cloned and referenced,\n            the copy will be returned; when ``True``, a new copy will be created.\n            (Default value = ``False``)\n          ignore_fields: List/tuple of field names (for dictionaries) that will be ignored\n            during cloning (applies to children duplication as well). If fields are to be\n            considered for a limited number of levels, you have to add it as integer, for\n            example ``[1,\"/B\",\"/TOTO\"]`` means that ``\"/B\"`` will be ignored at the first\n            level only but ``\"/TOTO\"`` on all levels.\n\n        Returns:\n          The cloned PdfObject\n\n        \"\"\"\n        raise NotImplementedError(\n            f\"{self.__class__.__name__} does not implement .clone so far\"\n        )\n\n    def _reference_clone(\n        self, clone: Any, pdf_dest: PdfWriterProtocol, force_duplicate: bool = False\n    ) -> PdfObjectProtocol:\n        \"\"\"\n        Reference the object within the _objects of pdf_dest only if\n        indirect_reference attribute exists (which means the objects was\n        already identified in xref/xobjstm) if object has been already\n        referenced do nothing.\n\n        Args:\n          clone:\n          pdf_dest:\n\n        Returns:\n          The clone\n\n        \"\"\"\n        try:\n            if not force_duplicate and clone.indirect_reference.pdf == pdf_dest:\n                return clone\n        except Exception:\n            pass\n        # if hasattr(clone, \"indirect_reference\"):\n        try:\n            ind = self.indirect_reference\n        except AttributeError:\n            return clone\n        if (\n            pdf_dest.incremental\n            and ind is not None\n            and ind.pdf == pdf_dest._reader\n            and ind.idnum <= len(pdf_dest._objects)\n        ):\n            i = ind.idnum\n        else:\n            i = len(pdf_dest._objects) + 1\n        if ind is not None:\n            if id(ind.pdf) not in pdf_dest._id_translated:\n                pdf_dest._id_translated[id(ind.pdf)] = {}\n                pdf_dest._id_translated[id(ind.pdf)][\"PreventGC\"] = ind.pdf  # type: ignore[index]\n            if (\n                not force_duplicate\n                and ind.idnum in pdf_dest._id_translated[id(ind.pdf)]\n            ):\n                obj = pdf_dest.get_object(\n                    pdf_dest._id_translated[id(ind.pdf)][ind.idnum]\n                )\n                assert obj is not None\n                return obj\n            pdf_dest._id_translated[id(ind.pdf)][ind.idnum] = i\n        try:\n            pdf_dest._objects[i - 1] = clone\n        except IndexError:\n            pdf_dest._objects.append(clone)\n            i = len(pdf_dest._objects)\n        clone.indirect_reference = IndirectObject(i, 0, pdf_dest)\n        return clone\n\n    def get_object(self) -> Optional[\"PdfObject\"]:\n        \"\"\"Resolve indirect references.\"\"\"\n        return self\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        raise NotImplementedError\n\n\nclass NullObject(PdfObject):\n    def clone(\n        self,\n        pdf_dest: PdfWriterProtocol,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"NullObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        return cast(\n            \"NullObject\", self._reference_clone(NullObject(), pdf_dest, force_duplicate)\n        )\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash((self.__class__,))\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(b\"null\")\n\n    @staticmethod\n    def read_from_stream(stream: StreamType) -> \"NullObject\":\n        nulltxt = stream.read(4)\n        if nulltxt != b\"null\":\n            raise PdfReadError(\"Could not read Null object\")\n        return NullObject()\n\n    def __repr__(self) -> str:\n        return \"NullObject\"\n\n    def __eq__(self, other: object) -> bool:\n        return isinstance(other, NullObject)\n\n    def __hash__(self) -> int:\n        return self.hash_bin()\n\n\nclass BooleanObject(PdfObject):\n    def __init__(self, value: Any) -> None:\n        self.value = value\n\n    def clone(\n        self,\n        pdf_dest: PdfWriterProtocol,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"BooleanObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        return cast(\n            \"BooleanObject\",\n            self._reference_clone(BooleanObject(self.value), pdf_dest, force_duplicate),\n        )\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash((self.__class__, self.value))\n\n    def __eq__(self, o: object, /) -> bool:\n        if isinstance(o, BooleanObject):\n            return self.value == o.value\n        if isinstance(o, bool):\n            return self.value == o\n        return False\n\n    def __hash__(self) -> int:\n        return self.hash_bin()\n\n    def __repr__(self) -> str:\n        return \"True\" if self.value else \"False\"\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        if self.value:\n            stream.write(b\"true\")\n        else:\n            stream.write(b\"false\")\n\n    @staticmethod\n    def read_from_stream(stream: StreamType) -> \"BooleanObject\":\n        word = stream.read(4)\n        if word == b\"true\":\n            return BooleanObject(True)\n        if word == b\"fals\":\n            stream.read(1)\n            return BooleanObject(False)\n        raise PdfReadError(\"Could not read Boolean object\")\n\n\nclass IndirectObject(PdfObject):\n    def __init__(self, idnum: int, generation: int, pdf: Any) -> None:  # PdfReader\n        self.idnum = idnum\n        self.generation = generation\n        self.pdf = pdf\n\n    def __hash__(self) -> int:\n        return hash((self.idnum, self.generation, id(self.pdf)))\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash((self.__class__, self.idnum, self.generation, id(self.pdf)))\n\n    def replicate(\n        self,\n        pdf_dest: PdfWriterProtocol,\n    ) -> \"PdfObject\":\n        return IndirectObject(self.idnum, self.generation, pdf_dest)\n\n    def clone(\n        self,\n        pdf_dest: PdfWriterProtocol,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"IndirectObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        if self.pdf == pdf_dest and not force_duplicate:\n            # Already duplicated and no extra duplication required\n            return self\n        if id(self.pdf) not in pdf_dest._id_translated:\n            pdf_dest._id_translated[id(self.pdf)] = {}\n            pdf_dest._id_translated[id(self.pdf)][\"PreventGC\"] = self.pdf  # type: ignore[index]\n\n        if self.idnum in pdf_dest._id_translated[id(self.pdf)]:\n            dup = pdf_dest.get_object(pdf_dest._id_translated[id(self.pdf)][self.idnum])\n            if force_duplicate:\n                assert dup is not None\n                assert dup.indirect_reference is not None\n                idref = dup.indirect_reference\n                return IndirectObject(idref.idnum, idref.generation, idref.pdf)\n        else:\n            obj = self.get_object()\n            # case observed : a pointed object can not be found\n            if obj is None:\n                # this normally\n                obj = NullObject()\n                assert isinstance(self, (IndirectObject,))\n                obj.indirect_reference = self\n            dup = pdf_dest._add_object(\n                obj.clone(pdf_dest, force_duplicate, ignore_fields)\n            )\n        assert dup is not None, \"mypy\"\n        assert dup.indirect_reference is not None, \"mypy\"\n        return dup.indirect_reference\n\n    @property\n    def indirect_reference(self) -> \"IndirectObject\":  # type: ignore[override]\n        return self\n\n    def get_object(self) -> Optional[\"PdfObject\"]:\n        return self.pdf.get_object(self)\n\n    def __deepcopy__(self, memo: Any) -> \"IndirectObject\":\n        return IndirectObject(self.idnum, self.generation, self.pdf)\n\n    def _get_object_with_check(self) -> Optional[\"PdfObject\"]:\n        o = self.get_object()\n        # the check is done here to not slow down get_object()\n        if isinstance(o, IndirectObject):\n            raise PdfStreamError(\n                f\"{self.__repr__()} references an IndirectObject {o.__repr__()}\"\n            )\n        return o\n\n    def __getattr__(self, name: str) -> Any:\n        # Attribute not found in object: look in pointed object\n        try:\n            return getattr(self._get_object_with_check(), name)\n        except AttributeError:\n            raise AttributeError(\n                f\"No attribute {name} found in IndirectObject or pointed object\"\n            )\n\n    def __getitem__(self, key: Any) -> Any:\n        # items should be extracted from pointed Object\n        return self._get_object_with_check()[key]  # type: ignore\n\n    def __contains__(self, key: Any) -> bool:\n        return key in self._get_object_with_check()  # type: ignore\n\n    def __iter__(self) -> Any:\n        return self._get_object_with_check().__iter__()  # type: ignore\n\n    def __float__(self) -> str:\n        # in this case we are looking for the pointed data\n        return self.get_object().__float__()  # type: ignore\n\n    def __int__(self) -> int:\n        # in this case we are looking for the pointed data\n        return self.get_object().__int__()  # type: ignore\n\n    def __str__(self) -> str:\n        # in this case we are looking for the pointed data\n        return self.get_object().__str__()\n\n    def __repr__(self) -> str:\n        return f\"IndirectObject({self.idnum!r}, {self.generation!r}, {id(self.pdf)})\"\n\n    def __eq__(self, other: object) -> bool:\n        return (\n            other is not None\n            and isinstance(other, IndirectObject)\n            and self.idnum == other.idnum\n            and self.generation == other.generation\n            and self.pdf is other.pdf\n        )\n\n    def __ne__(self, other: object) -> bool:\n        return not self.__eq__(other)\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(f\"{self.idnum} {self.generation} R\".encode())\n\n    @staticmethod\n    def read_from_stream(stream: StreamType, pdf: Any) -> \"IndirectObject\":  # PdfReader\n        idnum = b\"\"\n        while True:\n            tok = stream.read(1)\n            if not tok:\n                raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)\n            if tok.isspace():\n                break\n            idnum += tok\n        generation = b\"\"\n        while True:\n            tok = stream.read(1)\n            if not tok:\n                raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)\n            if tok.isspace():\n                if not generation:\n                    continue\n                break\n            generation += tok\n        r = read_non_whitespace(stream)\n        if r != b\"R\":\n            raise PdfReadError(\n                f\"Error reading indirect object reference at byte {hex(stream.tell())}\"\n            )\n        return IndirectObject(int(idnum), int(generation), pdf)\n\n\nFLOAT_WRITE_PRECISION = 8  # shall be min 5 digits max, allow user adj\n\n\nclass FloatObject(float, PdfObject):\n    def __new__(\n        cls, value: Any = \"0.0\", context: Optional[Any] = None\n    ) -> Self:\n        try:\n            value = float(value)\n            return float.__new__(cls, value)\n        except Exception as e:\n            # If this isn't a valid decimal (happens in malformed PDFs)\n            # fallback to 0\n            logger_warning(\n                f\"{e} : FloatObject ({value}) invalid; use 0.0 instead\", __name__\n            )\n            return float.__new__(cls, 0.0)\n\n    def clone(\n        self,\n        pdf_dest: Any,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"FloatObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        return cast(\n            \"FloatObject\",\n            self._reference_clone(FloatObject(self), pdf_dest, force_duplicate),\n        )\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash((self.__class__, self.as_numeric))\n\n    def myrepr(self) -> str:\n        if self == 0:\n            return \"0.0\"\n        nb = FLOAT_WRITE_PRECISION - int(log10(abs(self)))\n        return f\"{self:.{max(1, nb)}f}\".rstrip(\"0\").rstrip(\".\")\n\n    def __repr__(self) -> str:\n        return self.myrepr()  # repr(float(self))\n\n    def as_numeric(self) -> float:\n        return float(self)\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(self.myrepr().encode(\"utf8\"))\n\n\nclass NumberObject(int, PdfObject):\n    NumberPattern = re.compile(b\"[^+-.0-9]\")\n\n    def __new__(cls, value: Any) -> Self:\n        try:\n            return int.__new__(cls, int(value))\n        except ValueError:\n            logger_warning(f\"NumberObject({value}) invalid; use 0 instead\", __name__)\n            return int.__new__(cls, 0)\n\n    def clone(\n        self,\n        pdf_dest: Any,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"NumberObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        return cast(\n            \"NumberObject\",\n            self._reference_clone(NumberObject(self), pdf_dest, force_duplicate),\n        )\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash((self.__class__, self.as_numeric()))\n\n    def as_numeric(self) -> int:\n        return int(repr(self).encode(\"utf8\"))\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(repr(self).encode(\"utf8\"))\n\n    @staticmethod\n    def read_from_stream(stream: StreamType) -> Union[\"NumberObject\", \"FloatObject\"]:\n        num = read_until_regex(stream, NumberObject.NumberPattern)\n        if b\".\" in num:\n            return FloatObject(num)\n        return NumberObject(num)\n\n\nclass ByteStringObject(bytes, PdfObject):\n    \"\"\"\n    Represents a string object where the text encoding could not be determined.\n\n    This occurs quite often, as the PDF spec doesn't provide an alternate way to\n    represent strings -- for example, the encryption data stored in files (like\n    /O) is clearly not text, but is still stored in a \"String\" object.\n    \"\"\"\n\n    def clone(\n        self,\n        pdf_dest: Any,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"ByteStringObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        return cast(\n            \"ByteStringObject\",\n            self._reference_clone(\n                ByteStringObject(bytes(self)), pdf_dest, force_duplicate\n            ),\n        )\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash((self.__class__, bytes(self)))\n\n    @property\n    def original_bytes(self) -> bytes:\n        \"\"\"For compatibility with TextStringObject.original_bytes.\"\"\"\n        return self\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(b\"<\")\n        stream.write(binascii.hexlify(self))\n        stream.write(b\">\")\n\n    def __str__(self) -> str:\n        charset_to_try = [\"utf-16\", *list(NameObject.CHARSETS)]\n        for enc in charset_to_try:\n            try:\n                return self.decode(enc)\n            except UnicodeDecodeError:\n                pass\n        raise PdfReadError(\"Cannot decode ByteStringObject.\")\n\n\nclass TextStringObject(str, PdfObject):  # noqa: SLOT000\n    \"\"\"\n    A string object that has been decoded into a real unicode string.\n\n    If read from a PDF document, this string appeared to match the\n    PDFDocEncoding, or contained a UTF-16BE BOM mark to cause UTF-16 decoding\n    to occur.\n    \"\"\"\n\n    autodetect_pdfdocencoding: bool\n    autodetect_utf16: bool\n    utf16_bom: bytes\n    _original_bytes: Optional[bytes] = None\n\n    def __new__(cls, value: Any) -> Self:\n        original_bytes = None\n        if isinstance(value, bytes):\n            original_bytes = value\n            value = value.decode(\"charmap\")\n        text_string_object = str.__new__(cls, value)\n        text_string_object._original_bytes = original_bytes\n        text_string_object.autodetect_utf16 = False\n        text_string_object.autodetect_pdfdocencoding = False\n        text_string_object.utf16_bom = b\"\"\n        if original_bytes is not None and original_bytes[:2] in {codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE}:\n            # The value of `original_bytes` is only set for inputs being `bytes`.\n            # If this is UTF-16 data according to the BOM (first two characters),\n            # perform special handling. All other cases should not need any special conversion\n            # due to already being a string.\n            try:\n                text_string_object = str.__new__(cls, original_bytes.decode(\"utf-16\"))\n            except UnicodeDecodeError as exception:\n                logger_warning(\n                    f\"{exception!s}\\ninitial string:{exception.object!r}\",\n                    __name__,\n                )\n                text_string_object = str.__new__(cls, exception.object[: exception.start].decode(\"utf-16\"))\n            text_string_object._original_bytes = original_bytes\n            text_string_object.autodetect_utf16 = True\n            text_string_object.utf16_bom = original_bytes[:2]\n        else:\n            try:\n                encode_pdfdocencoding(text_string_object)\n                text_string_object.autodetect_pdfdocencoding = True\n            except UnicodeEncodeError:\n                text_string_object.autodetect_utf16 = True\n                text_string_object.utf16_bom = codecs.BOM_UTF16_BE\n        return text_string_object\n\n    def clone(\n        self,\n        pdf_dest: Any,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"TextStringObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        obj = TextStringObject(self)\n        obj._original_bytes = self._original_bytes\n        obj.autodetect_pdfdocencoding = self.autodetect_pdfdocencoding\n        obj.autodetect_utf16 = self.autodetect_utf16\n        obj.utf16_bom = self.utf16_bom\n        return cast(\n            \"TextStringObject\", self._reference_clone(obj, pdf_dest, force_duplicate)\n        )\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash((self.__class__, self.original_bytes))\n\n    @property\n    def original_bytes(self) -> bytes:\n        \"\"\"\n        It is occasionally possible that a text string object gets created where\n        a byte string object was expected due to the autodetection mechanism --\n        if that occurs, this \"original_bytes\" property can be used to\n        back-calculate what the original encoded bytes were.\n        \"\"\"\n        if self._original_bytes is not None:\n            return self._original_bytes\n        return self.get_original_bytes()\n\n    def get_original_bytes(self) -> bytes:\n        # We're a text string object, but the library is trying to get our raw\n        # bytes. This can happen if we auto-detected this string as text, but\n        # we were wrong. It's pretty common. Return the original bytes that\n        # would have been used to create this object, based upon the autodetect\n        # method.\n        if self.autodetect_utf16:\n            if self.utf16_bom == codecs.BOM_UTF16_LE:\n                return codecs.BOM_UTF16_LE + self.encode(\"utf-16le\")\n            if self.utf16_bom == codecs.BOM_UTF16_BE:\n                return codecs.BOM_UTF16_BE + self.encode(\"utf-16be\")\n            return self.encode(\"utf-16be\")\n        if self.autodetect_pdfdocencoding:\n            return encode_pdfdocencoding(self)\n        raise Exception(\"no information about original bytes\")  # pragma: no cover\n\n    def get_encoded_bytes(self) -> bytes:\n        # Try to write the string out as a PDFDocEncoding encoded string. It's\n        # nicer to look at in the PDF file. Sadly, we take a performance hit\n        # here for trying...\n        try:\n            if self._original_bytes is not None:\n                return self._original_bytes\n            if self.autodetect_utf16:\n                raise UnicodeEncodeError(\"\", \"forced\", -1, -1, \"\")\n            bytearr = encode_pdfdocencoding(self)\n        except UnicodeEncodeError:\n            if self.utf16_bom == codecs.BOM_UTF16_LE:\n                bytearr = codecs.BOM_UTF16_LE + self.encode(\"utf-16le\")\n            elif self.utf16_bom == codecs.BOM_UTF16_BE:\n                bytearr = codecs.BOM_UTF16_BE + self.encode(\"utf-16be\")\n            else:\n                bytearr = self.encode(\"utf-16be\")\n        return bytearr\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        bytearr = self.get_encoded_bytes()\n        stream.write(b\"(\")\n        for c_ in iter_unpack(\"c\", bytearr):\n            c = cast(bytes, c_[0])\n            if not c.isalnum() and c != b\" \":\n                # This:\n                #   stream.write(rf\"\\{c:0>3o}\".encode())\n                # gives\n                #   https://github.com/davidhalter/parso/issues/207\n                stream.write(b\"\\\\%03o\" % ord(c))\n            else:\n                stream.write(c)\n        stream.write(b\")\")\n\n\nclass NameObject(str, PdfObject):  # noqa: SLOT000\n    delimiter_pattern = re.compile(rb\"\\s+|[\\(\\)<>\\[\\]{}/%]\")\n    prefix = b\"/\"\n    renumber_table: ClassVar[dict[str, bytes]] = {\n        **{chr(i): f\"#{i:02X}\".encode() for i in b\"#()<>[]{}/%\"},\n        **{chr(i): f\"#{i:02X}\".encode() for i in range(33)},\n    }\n\n    def clone(\n        self,\n        pdf_dest: Any,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"NameObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        return cast(\n            \"NameObject\",\n            self._reference_clone(NameObject(self), pdf_dest, force_duplicate),\n        )\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash((self.__class__, self))\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(self.renumber())\n\n    def renumber(self) -> bytes:\n        out = self[0].encode(\"utf-8\")\n        if out != b\"/\":\n            deprecation_no_replacement(\n                f\"Incorrect first char in NameObject, should start with '/': ({self})\",\n                \"5.0.0\",\n            )\n        parts = [out]\n        for c in self[1:]:\n            if c > \"~\":\n                parts.extend(f\"#{x:02X}\".encode() for x in c.encode(\"utf-8\"))\n            else:\n                try:\n                    parts.append(self.renumber_table[c])\n                except KeyError:\n                    parts.append(c.encode(\"utf-8\"))\n        return b\"\".join(parts)\n\n    def _sanitize(self) -> \"NameObject\":\n        \"\"\"\n        Sanitize the NameObject's name to be a valid PDF name part\n        (alphanumeric, underscore, hyphen). The _sanitize method replaces\n        spaces and any non-alphanumeric/non-underscore/non-hyphen with\n        underscores.\n\n        Returns:\n            NameObject with sanitized name.\n        \"\"\"\n        name = str(self).removeprefix(\"/\")\n        name = re.sub(r\"\\ \", \"_\", name)\n        name = re.sub(r\"[^a-zA-Z0-9_-]\", \"_\", name)\n        return NameObject(\"/\" + name)\n\n    @classproperty\n    def surfix(cls) -> bytes:  # noqa: N805\n        deprecation_with_replacement(\"surfix\", \"prefix\", \"5.0.0\")\n        return b\"/\"\n\n    @staticmethod\n    def unnumber(sin: bytes) -> bytes:\n        result = bytearray()\n        i = 0\n        while i < len(sin):\n            if sin[i:i + 1] == b\"#\":\n                try:\n                    result.append(int(sin[i + 1 : i + 3], 16))\n                    i += 3\n                    continue\n                except (ValueError, IndexError):\n                    # if the 2 characters after # can not be converted to hex\n                    # we change nothing and carry on\n                    pass\n            result.append(sin[i])\n            i += 1\n        return bytes(result)\n\n    CHARSETS = (\"utf-8\", \"gbk\", \"latin1\")\n\n    @staticmethod\n    def read_from_stream(stream: StreamType, pdf: Any) -> \"NameObject\":  # PdfReader\n        name = stream.read(1)\n        if name != NameObject.prefix:\n            raise PdfReadError(\"Name read error\")\n        name += read_until_regex(stream, NameObject.delimiter_pattern)\n        try:\n            # Name objects should represent irregular characters\n            # with a '#' followed by the symbol's hex number\n            name = NameObject.unnumber(name)\n            for enc in NameObject.CHARSETS:\n                try:\n                    ret = name.decode(enc)\n                    return NameObject(ret)\n                except Exception:\n                    pass\n            raise UnicodeDecodeError(\"\", name, 0, 0, \"Code Not Found\")\n        except (UnicodeEncodeError, UnicodeDecodeError) as e:\n            if not pdf.strict:\n                logger_warning(\n                    f\"Illegal character in NameObject ({name!r}), \"\n                    \"you may need to adjust NameObject.CHARSETS\",\n                    __name__,\n                )\n                return NameObject(name.decode(\"charmap\"))\n            raise PdfReadError(\n                f\"Illegal character in NameObject ({name!r}). \"\n                \"You may need to adjust NameObject.CHARSETS.\",\n            ) from e\n\n\ndef encode_pdfdocencoding(unicode_string: str) -> bytes:\n    try:\n        return bytes([_pdfdoc_encoding_rev[k] for k in unicode_string])\n    except KeyError:\n        raise UnicodeEncodeError(\n            \"pdfdocencoding\",\n            unicode_string,\n            -1,\n            -1,\n            \"does not exist in translation table\",\n        )\n\n\ndef is_null_or_none(x: Any) -> TypeGuard[Union[None, NullObject, IndirectObject]]:\n    \"\"\"\n    Returns:\n        True if x is None or NullObject.\n\n    \"\"\"\n    return x is None or (\n        isinstance(x, PdfObject)\n        and (x.get_object() is None or isinstance(x.get_object(), NullObject))\n    )\n"
  },
  {
    "path": "pypdf/generic/_data_structures.py",
    "content": "# Copyright (c) 2006, Mathieu Fenniak\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\n\n__author__ = \"Mathieu Fenniak\"\n__author_email__ = \"biziqe@mathieu.fenniak.net\"\n\nimport logging\nimport re\nimport sys\nfrom collections.abc import Iterable, Sequence\nfrom io import BytesIO\nfrom math import ceil\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    Union,\n    cast,\n)\n\nfrom .._protocols import PdfReaderProtocol, PdfWriterProtocol, XmpInformationProtocol\nfrom .._utils import (\n    WHITESPACES,\n    StreamType,\n    deprecation_no_replacement,\n    logger_warning,\n    read_non_whitespace,\n    read_until_regex,\n    read_until_whitespace,\n    skip_over_comment,\n)\nfrom ..constants import (\n    CheckboxRadioButtonAttributes,\n    FieldDictionaryAttributes,\n    OutlineFontFlag,\n)\nfrom ..constants import FilterTypes as FT\nfrom ..constants import StreamAttributes as SA\nfrom ..constants import TypArguments as TA\nfrom ..constants import TypFitArguments as TF\nfrom ..errors import STREAM_TRUNCATED_PREMATURELY, LimitReachedError, PdfReadError, PdfStreamError\nfrom ._base import (\n    BooleanObject,\n    ByteStringObject,\n    FloatObject,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    PdfObject,\n    TextStringObject,\n    is_null_or_none,\n)\nfrom ._fit import Fit\nfrom ._image_inline import (\n    extract_inline__ascii85_decode,\n    extract_inline__ascii_hex_decode,\n    extract_inline__dct_decode,\n    extract_inline__run_length_decode,\n    extract_inline_default,\n)\nfrom ._utils import read_hex_string_from_stream, read_string_from_stream\n\nif sys.version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing_extensions import Self\n\nlogger = logging.getLogger(__name__)\n\nIndirectPattern = re.compile(rb\"[+-]?(\\d+)\\s+(\\d+)\\s+R[^a-zA-Z]\")\n\n\nclass ArrayObject(list[Any], PdfObject):\n    def replicate(\n        self,\n        pdf_dest: PdfWriterProtocol,\n    ) -> \"ArrayObject\":\n        arr = cast(\n            \"ArrayObject\",\n            self._reference_clone(ArrayObject(), pdf_dest, False),\n        )\n        for data in self:\n            if hasattr(data, \"replicate\"):\n                arr.append(data.replicate(pdf_dest))\n            else:\n                arr.append(data)\n        return arr\n\n    def clone(\n        self,\n        pdf_dest: PdfWriterProtocol,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"ArrayObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        try:\n            if self.indirect_reference.pdf == pdf_dest and not force_duplicate:  # type: ignore\n                return self\n        except Exception:\n            pass\n        arr = cast(\n            \"ArrayObject\",\n            self._reference_clone(ArrayObject(), pdf_dest, force_duplicate=True),\n        )\n        for data in self:\n            if isinstance(data, StreamObject):\n                dup = data._reference_clone(\n                    data.clone(pdf_dest, force_duplicate, ignore_fields),\n                    pdf_dest,\n                    force_duplicate,\n                )\n                arr.append(dup.indirect_reference)\n            elif isinstance(data, IndirectObject) and isinstance(resolved := data.get_object(), StreamObject):\n                dup = data._reference_clone(\n                    resolved.clone(pdf_dest, force_duplicate=True, ignore_fields=ignore_fields),\n                    pdf_dest,\n                    force_duplicate,\n                )\n                arr.append(dup.indirect_reference)\n            elif hasattr(data, \"clone\"):\n                arr.append(data.clone(pdf_dest, force_duplicate, ignore_fields))\n            else:\n                arr.append(data)\n        return arr\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash((self.__class__, tuple(x.hash_bin() for x in self)))\n\n    def items(self) -> Iterable[Any]:\n        \"\"\"Emulate DictionaryObject.items for a list (index, object).\"\"\"\n        return enumerate(self)\n\n    def _to_lst(self, lst: Any) -> list[Any]:\n        # Convert to list, internal\n        if isinstance(lst, (list, tuple, set)):\n            pass\n        elif isinstance(lst, PdfObject):\n            lst = [lst]\n        elif isinstance(lst, str):\n            if lst[0] == \"/\":\n                lst = [NameObject(lst)]\n            else:\n                lst = [TextStringObject(lst)]\n        elif isinstance(lst, bytes):\n            lst = [ByteStringObject(lst)]\n        else:  # for numbers,...\n            lst = [lst]\n        return lst\n\n    def __add__(self, lst: Any) -> \"ArrayObject\":\n        \"\"\"\n        Allow extension by adding list or add one element only\n\n        Args:\n            lst: any list, tuples are extended the list.\n            other types(numbers,...) will be appended.\n            if str is passed it will be converted into TextStringObject\n            or NameObject (if starting with \"/\")\n            if bytes is passed it will be converted into ByteStringObject\n\n        Returns:\n            ArrayObject with all elements\n\n        \"\"\"\n        temp = ArrayObject(self)\n        temp.extend(self._to_lst(lst))\n        return temp\n\n    def __iadd__(self, lst: Any) -> Self:\n        \"\"\"\n         Allow extension by adding list or add one element only\n\n        Args:\n            lst: any list, tuples are extended the list.\n            other types(numbers,...) will be appended.\n            if str is passed it will be converted into TextStringObject\n            or NameObject (if starting with \"/\")\n            if bytes is passed it will be converted into ByteStringObject\n\n        \"\"\"\n        self.extend(self._to_lst(lst))\n        return self\n\n    def __isub__(self, lst: Any) -> Self:\n        \"\"\"Allow to remove items\"\"\"\n        for x in self._to_lst(lst):\n            try:\n                index = self.index(x)\n                del self[index]\n            except ValueError:\n                pass\n        return self\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(b\"[\")\n        for data in self:\n            stream.write(b\" \")\n            data.write_to_stream(stream)\n        stream.write(b\" ]\")\n\n    @staticmethod\n    def read_from_stream(\n        stream: StreamType,\n        pdf: Optional[PdfReaderProtocol],\n        forced_encoding: Union[None, str, list[str], dict[int, str]] = None,\n    ) -> \"ArrayObject\":\n        arr = ArrayObject()\n        tmp = stream.read(1)\n        if tmp != b\"[\":\n            raise PdfReadError(\"Could not read array\")\n        while True:\n            # skip leading whitespace\n            tok = stream.read(1)\n            while tok.isspace():\n                tok = stream.read(1)\n            if tok == b\"\":\n                break\n            if tok == b\"%\":\n                stream.seek(-1, 1)\n                skip_over_comment(stream)\n                continue\n            stream.seek(-1, 1)\n            # check for array ending\n            peek_ahead = stream.read(1)\n            if peek_ahead == b\"]\":\n                break\n            stream.seek(-1, 1)\n            # read and append object\n            arr.append(read_object(stream, pdf, forced_encoding))\n        return arr\n\n\nclass DictionaryObject(dict[Any, Any], PdfObject):\n    def replicate(\n        self,\n        pdf_dest: PdfWriterProtocol,\n    ) -> \"DictionaryObject\":\n        d__ = cast(\n            \"DictionaryObject\",\n            self._reference_clone(self.__class__(), pdf_dest, False),\n        )\n        for k, v in self.items():\n            d__[k.replicate(pdf_dest)] = (\n                v.replicate(pdf_dest) if hasattr(v, \"replicate\") else v\n            )\n        return d__\n\n    def clone(\n        self,\n        pdf_dest: PdfWriterProtocol,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"DictionaryObject\":\n        \"\"\"Clone object into pdf_dest.\"\"\"\n        try:\n            if self.indirect_reference.pdf == pdf_dest and not force_duplicate:  # type: ignore\n                return self\n        except Exception:\n            pass\n\n        visited: set[tuple[int, int]] = set()  # (idnum, generation)\n        d__ = cast(\n            \"DictionaryObject\",\n            self._reference_clone(self.__class__(), pdf_dest, force_duplicate),\n        )\n        if ignore_fields is None:\n            ignore_fields = []\n        if len(d__.keys()) == 0:\n            d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)\n        return d__\n\n    def _clone(\n        self,\n        src: \"DictionaryObject\",\n        pdf_dest: PdfWriterProtocol,\n        force_duplicate: bool,\n        ignore_fields: Optional[Sequence[Union[str, int]]],\n        visited: set[tuple[int, int]],  # (idnum, generation)\n    ) -> None:\n        \"\"\"\n        Update the object from src.\n\n        Args:\n            src: \"DictionaryObject\":\n            pdf_dest:\n            force_duplicate:\n            ignore_fields:\n\n        \"\"\"\n        # First we remove the ignore_fields\n        # that are for a limited number of levels\n        assert ignore_fields is not None\n        ignore_fields = list(ignore_fields)\n        x = 0\n        while x < len(ignore_fields):\n            if isinstance(ignore_fields[x], int):\n                if cast(int, ignore_fields[x]) <= 0:\n                    del ignore_fields[x]\n                    del ignore_fields[x]\n                    continue\n                ignore_fields[x] -= 1  # type:ignore\n            x += 1\n        #  Check if this is a chain list, we need to loop to prevent recur\n        if any(\n            field not in ignore_fields\n            and field in src\n            and isinstance(src.raw_get(field), IndirectObject)\n            and isinstance(src[field], DictionaryObject)\n            and (\n                src.get(\"/Type\", None) is None\n                or cast(DictionaryObject, src[field]).get(\"/Type\", None) is None\n                or src.get(\"/Type\", None)\n                == cast(DictionaryObject, src[field]).get(\"/Type\", None)\n            )\n            for field in [\"/Next\", \"/Prev\", \"/N\", \"/V\"]\n        ):\n            ignore_fields = list(ignore_fields)\n            for lst in ((\"/Next\", \"/Prev\"), (\"/N\", \"/V\")):\n                for k in lst:\n                    objs = []\n                    if (\n                        k in src\n                        and k not in self\n                        and isinstance(src.raw_get(k), IndirectObject)\n                        and isinstance(src[k], DictionaryObject)\n                        # If need to go further the idea is to check\n                        # that the types are the same\n                        and (\n                            src.get(\"/Type\", None) is None\n                            or cast(DictionaryObject, src[k]).get(\"/Type\", None) is None\n                            or src.get(\"/Type\", None)\n                            == cast(DictionaryObject, src[k]).get(\"/Type\", None)\n                        )\n                    ):\n                        cur_obj: Optional[DictionaryObject] = cast(\n                            \"DictionaryObject\", src[k]\n                        )\n                        prev_obj: Optional[DictionaryObject] = self\n                        while cur_obj is not None:\n                            clon = cast(\n                                \"DictionaryObject\",\n                                cur_obj._reference_clone(\n                                    cur_obj.__class__(), pdf_dest, force_duplicate\n                                ),\n                            )\n                            # Check to see if we've previously processed our item\n                            if clon.indirect_reference is not None:\n                                idnum = clon.indirect_reference.idnum\n                                generation = clon.indirect_reference.generation\n                                if (idnum, generation) in visited:\n                                    cur_obj = None\n                                    break\n                                visited.add((idnum, generation))\n                            objs.append((cur_obj, clon))\n                            assert prev_obj is not None\n                            prev_obj[NameObject(k)] = clon.indirect_reference\n                            prev_obj = clon\n                            try:\n                                if cur_obj == src:\n                                    cur_obj = None\n                                else:\n                                    cur_obj = cast(\"DictionaryObject\", cur_obj[k])\n                            except Exception:\n                                cur_obj = None\n                        for s, c in objs:\n                            c._clone(\n                                s, pdf_dest, force_duplicate, ignore_fields, visited\n                            )\n\n        for k, v in src.items():\n            if k not in ignore_fields:\n                if isinstance(v, StreamObject):\n                    if not hasattr(v, \"indirect_reference\"):\n                        v.indirect_reference = None\n                    vv = v.clone(pdf_dest, force_duplicate, ignore_fields)\n                    assert vv.indirect_reference is not None\n                    self[k.clone(pdf_dest)] = vv.indirect_reference\n                elif k not in self:\n                    self[NameObject(k)] = (\n                        v.clone(pdf_dest, force_duplicate, ignore_fields)\n                        if hasattr(v, \"clone\")\n                        else v\n                    )\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        return hash(\n            (self.__class__, tuple(((k, v.hash_bin()) for k, v in self.items())))\n        )\n\n    def raw_get(self, key: Any) -> Any:\n        return dict.__getitem__(self, key)\n\n    def get_inherited(self, key: str, default: Any = None) -> Any:\n        \"\"\"\n        Returns the value of a key or from the parent if not found.\n        If not found returns default.\n\n        Args:\n            key: string identifying the field to return\n\n            default: default value to return\n\n        Returns:\n            Current key or inherited one, otherwise default value.\n\n        \"\"\"\n        if key in self:\n            return self[key]\n        try:\n            if \"/Parent\" not in self:\n                return default\n            raise KeyError(\"Not present\")\n        except KeyError:\n            return cast(\"DictionaryObject\", self[\"/Parent\"].get_object()).get_inherited(\n                key, default\n            )\n\n    def __setitem__(self, key: Any, value: Any) -> Any:\n        if not isinstance(key, PdfObject):\n            raise ValueError(\"Key must be a PdfObject\")\n        if not isinstance(value, PdfObject):\n            raise ValueError(\"Value must be a PdfObject\")\n        return dict.__setitem__(self, key, value)\n\n    def setdefault(self, key: Any, value: Optional[Any] = None) -> Any:\n        if not isinstance(key, PdfObject):\n            raise ValueError(\"Key must be a PdfObject\")\n        if not isinstance(value, PdfObject):\n            raise ValueError(\"Value must be a PdfObject\")\n        return dict.setdefault(self, key, value)\n\n    def __getitem__(self, key: Any) -> PdfObject:\n        return dict.__getitem__(self, key).get_object()\n\n    @property\n    def xmp_metadata(self) -> Optional[XmpInformationProtocol]:\n        \"\"\"\n        Retrieve XMP (Extensible Metadata Platform) data relevant to this\n        object, if available.\n\n        See Table 347 — Additional entries in a metadata stream dictionary.\n\n        Returns:\n          Returns a :class:`~pypdf.xmp.XmpInformation` instance\n          that can be used to access XMP metadata from the document. Can also\n          return None if no metadata was found on the document root.\n\n        \"\"\"\n        from ..xmp import XmpInformation  # noqa: PLC0415\n\n        metadata = self.get(\"/Metadata\", None)\n        if is_null_or_none(metadata):\n            return None\n        assert metadata is not None, \"mypy\"\n        metadata = metadata.get_object()\n        return XmpInformation(metadata)\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(b\"<<\\n\")\n        for key, value in self.items():\n            if len(key) > 2 and key[1] == \"%\" and key[-1] == \"%\":\n                continue\n            key.write_to_stream(stream, encryption_key)\n            stream.write(b\" \")\n            value.write_to_stream(stream)\n            stream.write(b\"\\n\")\n        stream.write(b\">>\")\n\n    @classmethod\n    def _get_next_object_position(\n            cls, position_before: int, position_end: int, generations: list[int], pdf: PdfReaderProtocol\n    ) -> int:\n        out = position_end\n        for generation in generations:\n            location = pdf.xref[generation]\n            values = [x for x in location.values() if position_before < x <= position_end]\n            if values:\n                out = min(out, *values)\n        return out\n\n    @classmethod\n    def _read_unsized_from_stream(\n            cls, stream: StreamType, pdf: PdfReaderProtocol\n    ) -> bytes:\n        object_position = cls._get_next_object_position(\n            position_before=stream.tell(), position_end=2 ** 32, generations=list(pdf.xref), pdf=pdf\n        ) - 1\n        current_position = stream.tell()\n        # Read until the next object position.\n        read_value = stream.read(object_position - stream.tell())\n        endstream_position = read_value.find(b\"endstream\")\n        if endstream_position < 0:\n            raise PdfReadError(\n                f\"Unable to find 'endstream' marker for obj starting at {current_position}.\"\n            )\n        # 9 = len(b\"endstream\")\n        stream.seek(current_position + endstream_position + 9)\n        return read_value[: endstream_position - 1]\n\n    @staticmethod\n    def read_from_stream(\n        stream: StreamType,\n        pdf: Optional[PdfReaderProtocol],\n        forced_encoding: Union[None, str, list[str], dict[int, str]] = None,\n    ) -> \"DictionaryObject\":\n        tmp = stream.read(2)\n        if tmp != b\"<<\":\n            raise PdfReadError(\n                f\"Dictionary read error at byte {hex(stream.tell())}: \"\n                \"stream must begin with '<<'\"\n            )\n        data: dict[Any, Any] = {}\n        while True:\n            tok = read_non_whitespace(stream)\n            if tok == b\"\\x00\":\n                continue\n            if tok == b\"%\":\n                stream.seek(-1, 1)\n                skip_over_comment(stream)\n                continue\n            if not tok:\n                raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)\n\n            if tok == b\">\":\n                stream.read(1)\n                break\n            stream.seek(-1, 1)\n            try:\n                try:\n                    key = read_object(stream, pdf)\n                    if isinstance(key, NullObject):\n                        break\n                    if not isinstance(key, NameObject):\n                        raise PdfReadError(\n                            f\"Expecting a NameObject for key but found {key!r}\"\n                        )\n                except PdfReadError as exc:\n                    if pdf is not None and pdf.strict:\n                        raise\n                    logger_warning(exc.__repr__(), __name__)\n                    continue\n                tok = read_non_whitespace(stream)\n                stream.seek(-1, 1)\n                value = read_object(stream, pdf, forced_encoding)\n            except Exception as exc:\n                if pdf is not None and pdf.strict:\n                    raise PdfReadError(exc.__repr__())\n                logger_warning(exc.__repr__(), __name__)\n                retval = DictionaryObject()\n                retval.update(data)\n                return retval  # return partial data\n\n            if not data.get(key):\n                data[key] = value\n            else:\n                # multiple definitions of key not permitted\n                msg = (\n                    f\"Multiple definitions in dictionary at byte \"\n                    f\"{hex(stream.tell())} for key {key}\"\n                )\n                if pdf is not None and pdf.strict:\n                    raise PdfReadError(msg)\n                logger_warning(msg, __name__)\n\n        pos = stream.tell()\n        s = read_non_whitespace(stream)\n        if s == b\"s\" and stream.read(5) == b\"tream\":\n            eol = stream.read(1)\n            # Occasional PDF file output has spaces after 'stream' keyword but before EOL.\n            # patch provided by Danial Sandler\n            while eol == b\" \":\n                eol = stream.read(1)\n            if eol not in (b\"\\n\", b\"\\r\"):\n                raise PdfStreamError(\"Stream data must be followed by a newline\")\n            if eol == b\"\\r\" and stream.read(1) != b\"\\n\":\n                stream.seek(-1, 1)\n            # this is a stream object, not a dictionary\n            if SA.LENGTH not in data:\n                if pdf is not None and pdf.strict:\n                    raise PdfStreamError(\"Stream length not defined\")\n                logger_warning(\n                    f\"Stream length not defined @pos={stream.tell()}\", __name__\n                )\n                data[NameObject(SA.LENGTH)] = NumberObject(-1)\n            length = data[SA.LENGTH]\n            if isinstance(length, IndirectObject):\n                t = stream.tell()\n                assert pdf is not None, \"mypy\"\n                length = pdf.get_object(length)\n                stream.seek(t, 0)\n            if length is None:  # if the PDF is damaged\n                length = -1\n            pstart = stream.tell()\n            if length >= 0:\n                from ..filters import MAX_DECLARED_STREAM_LENGTH  # noqa: PLC0415\n                if length > MAX_DECLARED_STREAM_LENGTH:\n                    raise LimitReachedError(f\"Declared stream length of {length} exceeds maximum allowed length.\")\n\n                data[\"__streamdata__\"] = stream.read(length)\n            else:\n                data[\"__streamdata__\"] = read_until_regex(\n                    stream, re.compile(b\"endstream\")\n                )\n            e = read_non_whitespace(stream)\n            ndstream = stream.read(8)\n            if (e + ndstream) != b\"endstream\":\n                # the odd PDF file has a length that is too long, so\n                # we need to read backwards to find the \"endstream\" ending.\n                # ReportLab (unknown version) generates files with this bug,\n                # and Python users into PDF files tend to be our audience.\n                # we need to do this to correct the streamdata and chop off\n                # an extra character.\n                pos = stream.tell()\n                stream.seek(-10, 1)\n                end = stream.read(9)\n                if end == b\"endstream\":\n                    # we found it by looking back one character further.\n                    data[\"__streamdata__\"] = data[\"__streamdata__\"][:-1]\n                elif pdf is not None and not pdf.strict:\n                    stream.seek(pstart, 0)\n                    data[\"__streamdata__\"] = DictionaryObject._read_unsized_from_stream(stream, pdf)\n                    pos = stream.tell()\n                else:\n                    stream.seek(pos, 0)\n                    raise PdfReadError(\n                        \"Unable to find 'endstream' marker after stream at byte \"\n                        f\"{hex(stream.tell())} (nd='{ndstream!r}', end='{end!r}').\"\n                    )\n        else:\n            stream.seek(pos, 0)\n        if \"__streamdata__\" in data:\n            return StreamObject.initialize_from_dictionary(data)\n        retval = DictionaryObject()\n        retval.update(data)\n        return retval\n\n\nclass TreeObject(DictionaryObject):\n    def __init__(self, dct: Optional[DictionaryObject] = None) -> None:\n        DictionaryObject.__init__(self)\n        if dct:\n            self.update(dct)\n\n    def has_children(self) -> bool:\n        return \"/First\" in self\n\n    def __iter__(self) -> Any:\n        return self.children()\n\n    def children(self) -> Iterable[Any]:\n        if not self.has_children():\n            return\n\n        child_ref = self[NameObject(\"/First\")]\n        last = self[NameObject(\"/Last\")]\n        child = child_ref.get_object()\n        visited: set[int] = set()\n        while True:\n            child_id = id(child)\n            if child_id in visited:\n                logger_warning(f\"Detected cycle in outline structure for {child}\", __name__)\n                return\n            visited.add(child_id)\n\n            yield child\n\n            if child == last:\n                return\n            child_ref = child.get(NameObject(\"/Next\"))  # type: ignore\n            if is_null_or_none(child_ref):\n                return\n            child = child_ref.get_object()\n\n    def add_child(self, child: Any, pdf: PdfWriterProtocol) -> None:\n        self.insert_child(child, None, pdf)\n\n    def inc_parent_counter_default(\n        self, parent: Union[None, IndirectObject, \"TreeObject\"], n: int\n    ) -> None:\n        if is_null_or_none(parent):\n            return\n        assert parent is not None, \"mypy\"\n        parent = cast(\"TreeObject\", parent.get_object())\n        if \"/Count\" in parent:\n            parent[NameObject(\"/Count\")] = NumberObject(\n                max(0, cast(int, parent[NameObject(\"/Count\")]) + n)\n            )\n            self.inc_parent_counter_default(parent.get(\"/Parent\", None), n)\n\n    def inc_parent_counter_outline(\n        self, parent: Union[None, IndirectObject, \"TreeObject\"], n: int\n    ) -> None:\n        if is_null_or_none(parent):\n            return\n        assert parent is not None, \"mypy\"\n        parent = cast(\"TreeObject\", parent.get_object())\n        #  BooleanObject requires comparison with == not is\n        opn = parent.get(\"/%is_open%\", True) == True  # noqa: E712\n        c = cast(int, parent.get(\"/Count\", 0))\n        if c < 0:\n            c = abs(c)\n        parent[NameObject(\"/Count\")] = NumberObject((c + n) * (1 if opn else -1))\n        if not opn:\n            return\n        self.inc_parent_counter_outline(parent.get(\"/Parent\", None), n)\n\n    def insert_child(\n        self,\n        child: Any,\n        before: Any,\n        pdf: PdfWriterProtocol,\n        inc_parent_counter: Optional[Callable[..., Any]] = None,\n    ) -> IndirectObject:\n        if inc_parent_counter is None:\n            inc_parent_counter = self.inc_parent_counter_default\n        child_obj = child.get_object()\n        child = child.indirect_reference  # get_reference(child_obj)\n\n        prev: Optional[DictionaryObject]\n        if \"/First\" not in self:  # no child yet\n            self[NameObject(\"/First\")] = child\n            self[NameObject(\"/Count\")] = NumberObject(0)\n            self[NameObject(\"/Last\")] = child\n            child_obj[NameObject(\"/Parent\")] = self.indirect_reference\n            inc_parent_counter(self, child_obj.get(\"/Count\", 1))\n            if \"/Next\" in child_obj:\n                del child_obj[\"/Next\"]\n            if \"/Prev\" in child_obj:\n                del child_obj[\"/Prev\"]\n            return child\n        prev = cast(\"DictionaryObject\", self[\"/Last\"])\n\n        while prev.indirect_reference != before:\n            if \"/Next\" in prev:\n                prev = cast(\"TreeObject\", prev[\"/Next\"])\n            else:  # append at the end\n                prev[NameObject(\"/Next\")] = cast(\"TreeObject\", child)\n                child_obj[NameObject(\"/Prev\")] = prev.indirect_reference\n                child_obj[NameObject(\"/Parent\")] = self.indirect_reference\n                if \"/Next\" in child_obj:\n                    del child_obj[\"/Next\"]\n                self[NameObject(\"/Last\")] = child\n                inc_parent_counter(self, child_obj.get(\"/Count\", 1))\n                return child\n        try:  # insert as first or in the middle\n            assert isinstance(prev[\"/Prev\"], DictionaryObject)\n            prev[\"/Prev\"][NameObject(\"/Next\")] = child\n            child_obj[NameObject(\"/Prev\")] = prev[\"/Prev\"]\n        except Exception:  # it means we are inserting in first position\n            del child_obj[\"/Next\"]\n        child_obj[NameObject(\"/Next\")] = prev\n        prev[NameObject(\"/Prev\")] = child\n        child_obj[NameObject(\"/Parent\")] = self.indirect_reference\n        inc_parent_counter(self, child_obj.get(\"/Count\", 1))\n        return child\n\n    def _remove_node_from_tree(\n        self, prev: Any, prev_ref: Any, cur: Any, last: Any\n    ) -> None:\n        \"\"\"\n        Adjust the pointers of the linked list and tree node count.\n\n        Args:\n            prev:\n            prev_ref:\n            cur:\n            last:\n\n        \"\"\"\n        next_ref = cur.get(NameObject(\"/Next\"), None)\n        if prev is None:\n            if next_ref:\n                # Removing first tree node\n                next_obj = next_ref.get_object()\n                del next_obj[NameObject(\"/Prev\")]\n                self[NameObject(\"/First\")] = next_ref\n                self[NameObject(\"/Count\")] = NumberObject(\n                    self[NameObject(\"/Count\")] - 1  # type: ignore\n                )\n\n            else:\n                # Removing only tree node\n                self[NameObject(\"/Count\")] = NumberObject(0)\n                del self[NameObject(\"/First\")]\n                if NameObject(\"/Last\") in self:\n                    del self[NameObject(\"/Last\")]\n        else:\n            if next_ref:\n                # Removing middle tree node\n                next_obj = next_ref.get_object()\n                next_obj[NameObject(\"/Prev\")] = prev_ref\n                prev[NameObject(\"/Next\")] = next_ref\n            else:\n                # Removing last tree node\n                assert cur == last\n                del prev[NameObject(\"/Next\")]\n                self[NameObject(\"/Last\")] = prev_ref\n            self[NameObject(\"/Count\")] = NumberObject(self[NameObject(\"/Count\")] - 1)  # type: ignore\n\n    def remove_child(self, child: Any) -> None:\n        child_obj = child.get_object()\n        child = child_obj.indirect_reference\n\n        if NameObject(\"/Parent\") not in child_obj:\n            raise ValueError(\"Removed child does not appear to be a tree item\")\n        if child_obj[NameObject(\"/Parent\")] != self:\n            raise ValueError(\"Removed child is not a member of this tree\")\n\n        found = False\n        prev_ref = None\n        prev = None\n        cur_ref: Optional[Any] = self[NameObject(\"/First\")]\n        cur: Optional[dict[str, Any]] = cur_ref.get_object()  # type: ignore\n        last_ref = self[NameObject(\"/Last\")]\n        last = last_ref.get_object()\n        while cur is not None:\n            if cur == child_obj:\n                self._remove_node_from_tree(prev, prev_ref, cur, last)\n                found = True\n                break\n\n            # Go to the next node\n            prev_ref = cur_ref\n            prev = cur\n            if NameObject(\"/Next\") in cur:\n                cur_ref = cur[NameObject(\"/Next\")]\n                cur = cur_ref.get_object()\n            else:\n                cur_ref = None\n                cur = None\n\n        if not found:\n            raise ValueError(\"Removal couldn't find item in tree\")\n\n        _reset_node_tree_relationship(child_obj)\n\n    def remove_from_tree(self) -> None:\n        \"\"\"Remove the object from the tree it is in.\"\"\"\n        if NameObject(\"/Parent\") not in self:\n            raise ValueError(\"Removed child does not appear to be a tree item\")\n        cast(\"TreeObject\", self[\"/Parent\"]).remove_child(self)\n\n    def empty_tree(self) -> None:\n        for child in self:\n            child_obj = child.get_object()\n            _reset_node_tree_relationship(child_obj)\n\n        if NameObject(\"/Count\") in self:\n            del self[NameObject(\"/Count\")]\n        if NameObject(\"/First\") in self:\n            del self[NameObject(\"/First\")]\n        if NameObject(\"/Last\") in self:\n            del self[NameObject(\"/Last\")]\n\n\ndef _reset_node_tree_relationship(child_obj: Any) -> None:\n    \"\"\"\n    Call this after a node has been removed from a tree.\n\n    This resets the nodes attributes in respect to that tree.\n\n    Args:\n        child_obj:\n\n    \"\"\"\n    del child_obj[NameObject(\"/Parent\")]\n    if NameObject(\"/Next\") in child_obj:\n        del child_obj[NameObject(\"/Next\")]\n    if NameObject(\"/Prev\") in child_obj:\n        del child_obj[NameObject(\"/Prev\")]\n\n\nclass StreamObject(DictionaryObject):\n    def __init__(self) -> None:\n        self._data: bytes = b\"\"\n        self.decoded_self: Optional[DecodedStreamObject] = None\n\n    def replicate(\n        self,\n        pdf_dest: PdfWriterProtocol,\n    ) -> \"StreamObject\":\n        d__ = cast(\n            \"StreamObject\",\n            self._reference_clone(self.__class__(), pdf_dest, False),\n        )\n        d__._data = self._data\n        try:\n            decoded_self = self.decoded_self\n            if decoded_self is None:\n                self.decoded_self = None\n            else:\n                self.decoded_self = cast(\n                    \"DecodedStreamObject\", decoded_self.replicate(pdf_dest)\n                )\n        except Exception:\n            pass\n        for k, v in self.items():\n            d__[k.replicate(pdf_dest)] = (\n                v.replicate(pdf_dest) if hasattr(v, \"replicate\") else v\n            )\n        return d__\n\n    def _clone(\n        self,\n        src: DictionaryObject,\n        pdf_dest: PdfWriterProtocol,\n        force_duplicate: bool,\n        ignore_fields: Optional[Sequence[Union[str, int]]],\n        visited: set[tuple[int, int]],\n    ) -> None:\n        \"\"\"\n        Update the object from src.\n\n        Args:\n            src:\n            pdf_dest:\n            force_duplicate:\n            ignore_fields:\n\n        \"\"\"\n        self._data = cast(\"StreamObject\", src)._data\n        try:\n            decoded_self = cast(\"StreamObject\", src).decoded_self\n            if decoded_self is None:\n                self.decoded_self = None\n            else:\n                self.decoded_self = cast(\n                    \"DecodedStreamObject\",\n                    decoded_self.clone(pdf_dest, force_duplicate, ignore_fields),\n                )\n        except Exception:\n            pass\n        super()._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)\n\n    def hash_bin(self) -> int:\n        \"\"\"\n        Used to detect modified object.\n\n        Returns:\n            Hash considering type and value.\n\n        \"\"\"\n        # Use _data to prevent errors on non-decoded streams.\n        return hash((super().hash_bin(), self._data))\n\n    def get_data(self) -> bytes:\n        return self._data\n\n    def set_data(self, data: bytes) -> None:\n        self._data = data\n\n    def hash_value_data(self) -> bytes:\n        data = super().hash_value_data()\n        data += self.get_data()\n        return data\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        self[NameObject(SA.LENGTH)] = NumberObject(len(self._data))\n        DictionaryObject.write_to_stream(self, stream)\n        del self[SA.LENGTH]\n        stream.write(b\"\\nstream\\n\")\n        stream.write(self._data)\n        stream.write(b\"\\nendstream\")\n\n    @staticmethod\n    def initialize_from_dictionary(\n        data: dict[str, Any]\n    ) -> Union[\"EncodedStreamObject\", \"DecodedStreamObject\"]:\n        retval: Union[EncodedStreamObject, DecodedStreamObject]\n        if SA.FILTER in data:\n            retval = EncodedStreamObject()\n        else:\n            retval = DecodedStreamObject()\n        retval._data = data[\"__streamdata__\"]\n        del data[\"__streamdata__\"]\n        if SA.LENGTH in data:\n            del data[SA.LENGTH]\n        retval.update(data)\n        return retval\n\n    def flate_encode(self, level: int = -1) -> \"EncodedStreamObject\":\n        from ..filters import FlateDecode  # noqa: PLC0415\n\n        if SA.FILTER in self:\n            f = self[SA.FILTER]\n            if isinstance(f, ArrayObject):\n                f = ArrayObject([NameObject(FT.FLATE_DECODE), *f])\n                try:\n                    params = ArrayObject(\n                        [NullObject(), *self.get(SA.DECODE_PARMS, ArrayObject())]\n                    )\n                except TypeError:\n                    # case of error where the * operator is not working (not an array\n                    params = ArrayObject(\n                        [NullObject(), self.get(SA.DECODE_PARMS, ArrayObject())]\n                    )\n            else:\n                f = ArrayObject([NameObject(FT.FLATE_DECODE), f])\n                params = ArrayObject(\n                    [NullObject(), self.get(SA.DECODE_PARMS, NullObject())]\n                )\n        else:\n            f = NameObject(FT.FLATE_DECODE)\n            params = None\n        retval = EncodedStreamObject()\n        retval.update(self)\n        retval[NameObject(SA.FILTER)] = f\n        if params is not None:\n            retval[NameObject(SA.DECODE_PARMS)] = params\n        retval._data = FlateDecode.encode(self._data, level)\n        return retval\n\n    def decode_as_image(self, pillow_parameters: Union[dict[str, Any], None] = None) -> Any:\n        \"\"\"\n        Try to decode the stream object as an image\n\n        Args:\n            pillow_parameters: parameters provided to Pillow Image.save() method,\n                cf. <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save>\n\n        Returns:\n            a PIL image if proper decoding has been found\n        Raises:\n            Exception: Errors during decoding will be reported.\n                It is recommended to catch exceptions to prevent\n                stops in your program.\n\n        \"\"\"\n        from ._image_xobject import _xobj_to_image  # noqa: PLC0415\n\n        if self.get(\"/Subtype\", \"\") != \"/Image\":\n            try:\n                msg = f\"{self.indirect_reference} does not seem to be an Image\"  # pragma: no cover\n            except AttributeError:\n                msg = f\"{self.__repr__()} object does not seem to be an Image\"  # pragma: no cover\n            logger_warning(msg, __name__)\n        extension, _, img = _xobj_to_image(self, pillow_parameters)\n        if extension is None:\n            return None  # pragma: no cover\n        return img\n\n\nclass DecodedStreamObject(StreamObject):\n    pass\n\n\nclass EncodedStreamObject(StreamObject):\n    def __init__(self) -> None:\n        self.decoded_self: Optional[DecodedStreamObject] = None\n\n    # This overrides the parent method\n    def get_data(self) -> bytes:\n        from ..filters import decode_stream_data  # noqa: PLC0415\n\n        if self.decoded_self is not None:\n            # Cached version of decoded object\n            return self.decoded_self.get_data()\n\n        # Create decoded object\n        decoded = DecodedStreamObject()\n        decoded.set_data(decode_stream_data(self))\n        for key, value in self.items():\n            if key not in (SA.LENGTH, SA.FILTER, SA.DECODE_PARMS):\n                decoded[key] = value\n        self.decoded_self = decoded\n        return decoded.get_data()\n\n    # This overrides the parent method:\n    def set_data(self, data: bytes) -> None:\n        from ..filters import FlateDecode  # noqa: PLC0415\n\n        if self.get(SA.FILTER, \"\") in (FT.FLATE_DECODE, [FT.FLATE_DECODE]):\n            if not isinstance(data, bytes):\n                raise TypeError(\"Data must be bytes\")\n            if self.decoded_self is None:\n                self.get_data()  # to create self.decoded_self\n            assert self.decoded_self is not None, \"mypy\"\n            self.decoded_self.set_data(data)\n            super().set_data(FlateDecode.encode(data))\n        else:\n            raise PdfReadError(\n                \"Streams encoded with a filter different from FlateDecode are not supported\"\n            )\n\n\nCONTENT_STREAM_ARRAY_MAX_LENGTH = 10_000\n\n\nclass ContentStream(DecodedStreamObject):\n    \"\"\"\n    In order to be fast, this data structure can contain either:\n\n    * raw data in ._data\n    * parsed stream operations in ._operations.\n\n    At any time, ContentStream object can either have both of those fields defined,\n    or one field defined and the other set to None.\n\n    These fields are \"rebuilt\" lazily, when accessed:\n\n    * when .get_data() is called, if ._data is None, it is rebuilt from ._operations.\n    * when .operations is called, if ._operations is None, it is rebuilt from ._data.\n\n    Conversely, these fields can be invalidated:\n\n    * when .set_data() is called, ._operations is set to None.\n    * when .operations is set, ._data is set to None.\n    \"\"\"\n\n    def __init__(\n        self,\n        stream: Any,\n        pdf: Any,\n        forced_encoding: Union[None, str, list[str], dict[int, str]] = None,\n    ) -> None:\n        self.pdf = pdf\n        self._operations: list[tuple[Any, bytes]] = []\n\n        # stream may be a StreamObject or an ArrayObject containing\n        # StreamObjects to be concatenated together.\n        if stream is None:\n            super().set_data(b\"\")\n        else:\n            stream = stream.get_object()\n            if isinstance(stream, ArrayObject):\n                from pypdf.filters import MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH  # noqa: PLC0415\n\n                if (stream_length := len(stream)) > CONTENT_STREAM_ARRAY_MAX_LENGTH:\n                    raise LimitReachedError(\n                        f\"Array-based stream has {stream_length} > {CONTENT_STREAM_ARRAY_MAX_LENGTH} elements.\"\n                    )\n                data = bytearray()\n                length = 0\n                for s in stream:\n                    s_resolved = s.get_object()\n                    if isinstance(s_resolved, NullObject):\n                        continue\n                    if not isinstance(s_resolved, StreamObject):\n                        # No need to emit an exception here for now - the PDF structure\n                        # seems to already be broken beforehand in these cases.\n                        logger_warning(\n                            f\"Expected StreamObject, got {type(s_resolved).__name__} instead. Data might be wrong.\",\n                            __name__\n                        )\n                    else:\n                        new_data = s_resolved.get_data()\n                        length += len(new_data)\n                        if length > MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH:\n                            raise LimitReachedError(\n                                f\"Array-based stream has at least {length} > \"\n                                f\"{MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH} output bytes.\"\n                            )\n                        data += new_data\n                    if len(data) == 0 or data[-1] != b\"\\n\":\n                        # There should be no direct need to check for a change of one byte.\n                        length += 1\n                        data += b\"\\n\"\n                super().set_data(bytes(data))\n            else:\n                stream_data = stream.get_data()\n                assert stream_data is not None\n                super().set_data(stream_data)\n        self.forced_encoding = forced_encoding\n\n    def replicate(\n        self,\n        pdf_dest: PdfWriterProtocol,\n    ) -> \"ContentStream\":\n        d__ = cast(\n            \"ContentStream\",\n            self._reference_clone(self.__class__(None, None), pdf_dest, False),\n        )\n        d__._data = self._data\n        try:\n            decoded_self = self.decoded_self\n            if decoded_self is None:\n                self.decoded_self = None\n            else:\n                self.decoded_self = cast(\n                    \"DecodedStreamObject\", decoded_self.replicate(pdf_dest)\n                )\n        except Exception:\n            pass\n        for k, v in self.items():\n            d__[k.replicate(pdf_dest)] = (\n                v.replicate(pdf_dest) if hasattr(v, \"replicate\") else v\n            )\n        return d__\n        d__.set_data(self._data)\n        d__.pdf = pdf_dest\n        d__._operations = list(self._operations)\n        d__.forced_encoding = self.forced_encoding\n        return d__\n\n    def clone(\n        self,\n        pdf_dest: Any,\n        force_duplicate: bool = False,\n        ignore_fields: Optional[Sequence[Union[str, int]]] = (),\n    ) -> \"ContentStream\":\n        \"\"\"\n        Clone object into pdf_dest.\n\n        Args:\n            pdf_dest:\n            force_duplicate:\n            ignore_fields:\n\n        Returns:\n            The cloned ContentStream\n\n        \"\"\"\n        try:\n            if self.indirect_reference.pdf == pdf_dest and not force_duplicate:  # type: ignore\n                return self\n        except Exception:\n            pass\n\n        visited: set[tuple[int, int]] = set()\n        d__ = cast(\n            \"ContentStream\",\n            self._reference_clone(\n                self.__class__(None, None), pdf_dest, force_duplicate\n            ),\n        )\n        if ignore_fields is None:\n            ignore_fields = []\n        d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited)\n        return d__\n\n    def _clone(\n        self,\n        src: DictionaryObject,\n        pdf_dest: PdfWriterProtocol,\n        force_duplicate: bool,\n        ignore_fields: Optional[Sequence[Union[str, int]]],\n        visited: set[tuple[int, int]],\n    ) -> None:\n        \"\"\"\n        Update the object from src.\n\n        Args:\n            src:\n            pdf_dest:\n            force_duplicate:\n            ignore_fields:\n\n        \"\"\"\n        src_cs = cast(\"ContentStream\", src)\n        super().set_data(src_cs._data)\n        self.pdf = pdf_dest\n        self._operations = list(src_cs._operations)\n        self.forced_encoding = src_cs.forced_encoding\n        # no need to call DictionaryObjection or anything\n        # like super(DictionaryObject,self)._clone(src, pdf_dest, force_duplicate, ignore_fields, visited)\n\n    def _parse_content_stream(self, stream: StreamType) -> None:\n        # 7.8.2 Content Streams\n        stream.seek(0, 0)\n        operands: list[Union[int, str, PdfObject]] = []\n        while True:\n            peek = read_non_whitespace(stream)\n            if peek in (b\"\", 0):\n                break\n            stream.seek(-1, 1)\n            if peek.isalpha() or peek in (b\"'\", b'\"'):\n                operator = read_until_regex(stream, NameObject.delimiter_pattern)\n                if operator == b\"BI\":\n                    # begin inline image - a completely different parsing\n                    # mechanism is required, of course... thanks buddy...\n                    assert operands == []\n                    ii = self._read_inline_image(stream)\n                    self._operations.append((ii, b\"INLINE IMAGE\"))\n                else:\n                    self._operations.append((operands, operator))\n                    operands = []\n            elif peek == b\"%\":\n                # If we encounter a comment in the content stream, we have to\n                # handle it here. Typically, read_object will handle\n                # encountering a comment -- but read_object assumes that\n                # following the comment must be the object we're trying to\n                # read. In this case, it could be an operator instead.\n                while peek not in (b\"\\r\", b\"\\n\", b\"\"):\n                    peek = stream.read(1)\n            else:\n                operands.append(read_object(stream, None, self.forced_encoding))\n\n    def _read_inline_image(self, stream: StreamType) -> dict[str, Any]:\n        # begin reading just after the \"BI\" - begin image\n        # first read the dictionary of settings.\n        settings = DictionaryObject()\n        while True:\n            tok = read_non_whitespace(stream)\n            stream.seek(-1, 1)\n            if tok == b\"I\":\n                # \"ID\" - begin of image data\n                break\n            key = read_object(stream, self.pdf)\n            tok = read_non_whitespace(stream)\n            stream.seek(-1, 1)\n            value = read_object(stream, self.pdf)\n            settings[key] = value\n        # left at beginning of ID\n        tmp = stream.read(3)\n        assert tmp[:2] == b\"ID\"\n        filtr = settings.get(\"/F\", settings.get(\"/Filter\", \"not set\"))\n        savpos = stream.tell()\n        if isinstance(filtr, list):\n            filtr = filtr[0]  # used forencoding\n        if \"AHx\" in filtr or \"ASCIIHexDecode\" in filtr:\n            data = extract_inline__ascii_hex_decode(stream)\n        elif \"A85\" in filtr or \"ASCII85Decode\" in filtr:\n            data = extract_inline__ascii85_decode(stream)\n        elif \"RL\" in filtr or \"RunLengthDecode\" in filtr:\n            data = extract_inline__run_length_decode(stream)\n        elif \"DCT\" in filtr or \"DCTDecode\" in filtr:\n            data = extract_inline__dct_decode(stream)\n        elif filtr == \"not set\":\n            cs = settings.get(\"/CS\", \"\")\n            if isinstance(cs, list):\n                cs = cs[0]\n            if \"RGB\" in cs:\n                lcs = 3\n            elif \"CMYK\" in cs:\n                lcs = 4\n            else:\n                bits = settings.get(\n                    \"/BPC\",\n                    8 if cs in {\"/I\", \"/G\", \"/Indexed\", \"/DeviceGray\"} else -1,\n                )\n                if bits > 0:\n                    lcs = bits / 8.0\n                else:\n                    data = extract_inline_default(stream)\n                    lcs = -1\n            if lcs > 0:\n                data = stream.read(\n                    ceil(cast(int, settings[\"/W\"]) * lcs) * cast(int, settings[\"/H\"])\n                )\n            # Move to the `EI` if possible.\n            ei = read_non_whitespace(stream)\n            stream.seek(-1, 1)\n        else:\n            data = extract_inline_default(stream)\n\n        ei = stream.read(3)\n        stream.seek(-1, 1)\n        if ei[:2] != b\"EI\" or ei[2:3] not in WHITESPACES:\n            # Deal with wrong/missing `EI` tags. Example: Wrong dimensions specified above.\n            stream.seek(savpos, 0)\n            data = extract_inline_default(stream)\n            ei = stream.read(3)\n            stream.seek(-1, 1)\n            if ei[:2] != b\"EI\" or ei[2:3] not in WHITESPACES:  # pragma: no cover\n                # Check the same condition again. This should never fail as\n                # edge cases are covered by `extract_inline_default` above,\n                # but check this ot make sure that we are behind the `EI` afterwards.\n                raise PdfStreamError(\n                    f\"Could not extract inline image, even using fallback. Expected 'EI', got {ei!r}\"\n                )\n        return {\"settings\": settings, \"data\": data}\n\n    # This overrides the parent method\n    def get_data(self) -> bytes:\n        if not self._data:\n            new_data = BytesIO()\n            for operands, operator in self._operations:\n                if operator == b\"INLINE IMAGE\":\n                    new_data.write(b\"BI\")\n                    dict_text = BytesIO()\n                    operands[\"settings\"].write_to_stream(dict_text)\n                    new_data.write(dict_text.getvalue()[2:-2])\n                    new_data.write(b\"ID \")\n                    new_data.write(operands[\"data\"])\n                    new_data.write(b\"EI\")\n                else:\n                    for op in operands:\n                        op.write_to_stream(new_data)\n                        new_data.write(b\" \")\n                    new_data.write(operator)\n                new_data.write(b\"\\n\")\n            self._data = new_data.getvalue()\n        return self._data\n\n    # This overrides the parent method\n    def set_data(self, data: bytes) -> None:\n        super().set_data(data)\n        self._operations = []\n\n    @property\n    def operations(self) -> list[tuple[Any, bytes]]:\n        if not self._operations and self._data:\n            self._parse_content_stream(BytesIO(self._data))\n            self._data = b\"\"\n        return self._operations\n\n    @operations.setter\n    def operations(self, operations: list[tuple[Any, bytes]]) -> None:\n        self._operations = operations\n        self._data = b\"\"\n\n    def isolate_graphics_state(self) -> None:\n        if self._operations:\n            self._operations.insert(0, ([], b\"q\"))\n            self._operations.append(([], b\"Q\"))\n        elif self._data:\n            self._data = b\"q\\n\" + self._data + b\"\\nQ\\n\"\n\n    # This overrides the parent method\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if not self._data and self._operations:\n            self.get_data()  # this ensures ._data is rebuilt\n        super().write_to_stream(stream, encryption_key)\n\n\ndef read_object(\n    stream: StreamType,\n    pdf: Optional[PdfReaderProtocol],\n    forced_encoding: Union[None, str, list[str], dict[int, str]] = None,\n) -> Union[PdfObject, int, str, ContentStream]:\n    tok = stream.read(1)\n    stream.seek(-1, 1)  # reset to start\n    if tok == b\"/\":\n        return NameObject.read_from_stream(stream, pdf)\n    if tok == b\"<\":\n        # hexadecimal string OR dictionary\n        peek = stream.read(2)\n        stream.seek(-2, 1)  # reset to start\n        if peek == b\"<<\":\n            return DictionaryObject.read_from_stream(stream, pdf, forced_encoding)\n        return read_hex_string_from_stream(stream, forced_encoding)\n    if tok == b\"[\":\n        return ArrayObject.read_from_stream(stream, pdf, forced_encoding)\n    if tok in (b\"t\", b\"f\"):\n        return BooleanObject.read_from_stream(stream)\n    if tok == b\"(\":\n        return read_string_from_stream(stream, forced_encoding)\n    if tok == b\"e\" and stream.read(6) == b\"endobj\":\n        return NullObject()\n    if tok == b\"n\":\n        return NullObject.read_from_stream(stream)\n    if tok == b\"%\":\n        # comment\n        skip_over_comment(stream)\n        tok = read_non_whitespace(stream)\n        stream.seek(-1, 1)\n        return read_object(stream, pdf, forced_encoding)\n    if tok in b\"0123456789+-.\":\n        # number object OR indirect reference\n        peek = stream.read(20)\n        stream.seek(-len(peek), 1)  # reset to start\n        if IndirectPattern.match(peek) is not None:\n            assert pdf is not None, \"mypy\"\n            return IndirectObject.read_from_stream(stream, pdf)\n        return NumberObject.read_from_stream(stream)\n    pos = stream.tell()\n    stream.seek(-20, 1)\n    stream_extract = stream.read(80)\n    stream.seek(pos)\n    read_until_whitespace(stream)\n    raise PdfReadError(\n        f\"Invalid Elementary Object starting with {tok!r} @{pos}: {stream_extract!r}\"\n    )\n\n\nclass Field(TreeObject):\n    \"\"\"\n    A class representing a field dictionary.\n\n    This class is accessed through\n    :meth:`get_fields()<pypdf.PdfReader.get_fields>`\n    \"\"\"\n\n    def __init__(self, data: DictionaryObject) -> None:\n        DictionaryObject.__init__(self)\n        field_attributes = (\n            FieldDictionaryAttributes.attributes()\n            + CheckboxRadioButtonAttributes.attributes()\n        )\n        self.indirect_reference = data.indirect_reference\n        for attr in field_attributes:\n            try:\n                self[NameObject(attr)] = data[attr]\n            except KeyError:\n                pass\n        if isinstance(self.get(\"/V\"), EncodedStreamObject):\n            d = cast(EncodedStreamObject, self[NameObject(\"/V\")]).get_data()\n            if isinstance(d, bytes):\n                d_str = d.decode()\n            elif d is None:\n                d_str = \"\"\n            else:\n                raise Exception(\"Should never happen\")\n            self[NameObject(\"/V\")] = TextStringObject(d_str)\n\n    # TABLE 8.69 Entries common to all field dictionaries\n    @property\n    def field_type(self) -> Optional[NameObject]:\n        \"\"\"Read-only property accessing the type of this field.\"\"\"\n        return self.get(FieldDictionaryAttributes.FT)\n\n    @property\n    def parent(self) -> Optional[DictionaryObject]:\n        \"\"\"Read-only property accessing the parent of this field.\"\"\"\n        return self.get(FieldDictionaryAttributes.Parent)\n\n    @property\n    def kids(self) -> Optional[\"ArrayObject\"]:\n        \"\"\"Read-only property accessing the kids of this field.\"\"\"\n        return self.get(FieldDictionaryAttributes.Kids)\n\n    @property\n    def name(self) -> Optional[str]:\n        \"\"\"Read-only property accessing the name of this field.\"\"\"\n        return self.get(FieldDictionaryAttributes.T)\n\n    @property\n    def alternate_name(self) -> Optional[str]:\n        \"\"\"Read-only property accessing the alternate name of this field.\"\"\"\n        return self.get(FieldDictionaryAttributes.TU)\n\n    @property\n    def mapping_name(self) -> Optional[str]:\n        \"\"\"\n        Read-only property accessing the mapping name of this field.\n\n        This name is used by pypdf as a key in the dictionary returned by\n        :meth:`get_fields()<pypdf.PdfReader.get_fields>`\n        \"\"\"\n        return self.get(FieldDictionaryAttributes.TM)\n\n    @property\n    def flags(self) -> Optional[int]:\n        \"\"\"\n        Read-only property accessing the field flags, specifying various\n        characteristics of the field (see Table 8.70 of the PDF 1.7 reference).\n        \"\"\"\n        return self.get(FieldDictionaryAttributes.Ff)\n\n    @property\n    def value(self) -> Optional[Any]:\n        \"\"\"\n        Read-only property accessing the value of this field.\n\n        Format varies based on field type.\n        \"\"\"\n        return self.get(FieldDictionaryAttributes.V)\n\n    @property\n    def default_value(self) -> Optional[Any]:\n        \"\"\"Read-only property accessing the default value of this field.\"\"\"\n        return self.get(FieldDictionaryAttributes.DV)\n\n    @property\n    def additional_actions(self) -> Optional[DictionaryObject]:\n        \"\"\"\n        Read-only property accessing the additional actions dictionary.\n\n        This dictionary defines the field's behavior in response to trigger\n        events. See Section 8.5.2 of the PDF 1.7 reference.\n        \"\"\"\n        return self.get(FieldDictionaryAttributes.AA)\n\n\nclass Destination(TreeObject):\n    \"\"\"\n    A class representing a destination within a PDF file.\n\n    See section 12.3.2 of the PDF 2.0 reference.\n\n    Args:\n        title: Title of this destination.\n        page: Reference to the page of this destination. Should\n            be an instance of :class:`IndirectObject<pypdf.generic.IndirectObject>`.\n        fit: How the destination is displayed.\n\n    Raises:\n        PdfReadError: If destination type is invalid.\n\n    \"\"\"\n\n    node: Optional[\n        DictionaryObject\n    ] = None  # node provide access to the original Object\n\n    def __init__(\n        self,\n        title: Union[str, bytes],\n        page: Union[NumberObject, IndirectObject, NullObject, DictionaryObject],\n        fit: Fit,\n    ) -> None:\n        self._filtered_children: list[Any] = []  # used in PdfWriter\n\n        typ = fit.fit_type\n        args = fit.fit_args\n\n        DictionaryObject.__init__(self)\n        self[NameObject(\"/Title\")] = TextStringObject(title)\n        self[NameObject(\"/Page\")] = page\n        self[NameObject(\"/Type\")] = typ\n\n        # from table 8.2 of the PDF 1.7 reference.\n        if typ == \"/XYZ\":\n            if len(args) < 1:  # left is missing : should never occur\n                args.append(NumberObject(0.0))\n            if len(args) < 2:  # top is missing\n                args.append(NumberObject(0.0))\n            if len(args) < 3:  # zoom is missing\n                args.append(NumberObject(0.0))\n            (\n                self[NameObject(TA.LEFT)],\n                self[NameObject(TA.TOP)],\n                self[NameObject(\"/Zoom\")],\n            ) = args\n        elif len(args) == 0:\n            pass\n        elif typ == TF.FIT_R:\n            (\n                self[NameObject(TA.LEFT)],\n                self[NameObject(TA.BOTTOM)],\n                self[NameObject(TA.RIGHT)],\n                self[NameObject(TA.TOP)],\n            ) = args\n        elif typ in [TF.FIT_H, TF.FIT_BH]:\n            try:  # Prefer to be more robust not only to null parameters\n                (self[NameObject(TA.TOP)],) = args\n            except Exception:\n                (self[NameObject(TA.TOP)],) = (NullObject(),)\n        elif typ in [TF.FIT_V, TF.FIT_BV]:\n            try:  # Prefer to be more robust not only to null parameters\n                (self[NameObject(TA.LEFT)],) = args\n            except Exception:\n                (self[NameObject(TA.LEFT)],) = (NullObject(),)\n        elif typ in [TF.FIT, TF.FIT_B]:\n            pass\n        else:\n            raise PdfReadError(f\"Unknown Destination Type: {typ!r}\")\n\n    @property\n    def dest_array(self) -> \"ArrayObject\":\n        return ArrayObject(\n            [self.raw_get(\"/Page\"), self[\"/Type\"]]\n            + [\n                self[x]\n                for x in [\"/Left\", \"/Bottom\", \"/Right\", \"/Top\", \"/Zoom\"]\n                if x in self\n            ]\n        )\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(b\"<<\\n\")\n        key = NameObject(\"/D\")\n        key.write_to_stream(stream)\n        stream.write(b\" \")\n        value = self.dest_array\n        value.write_to_stream(stream)\n\n        key = NameObject(\"/S\")\n        key.write_to_stream(stream)\n        stream.write(b\" \")\n        value_s = NameObject(\"/GoTo\")\n        value_s.write_to_stream(stream)\n\n        stream.write(b\"\\n\")\n        stream.write(b\">>\")\n\n    @property\n    def title(self) -> Optional[str]:\n        \"\"\"Read-only property accessing the destination title.\"\"\"\n        return self.get(\"/Title\")\n\n    @property\n    def page(self) -> Optional[IndirectObject]:\n        \"\"\"Read-only property accessing the IndirectObject of the destination page.\"\"\"\n        return self.get(\"/Page\")\n\n    @property\n    def typ(self) -> Optional[str]:\n        \"\"\"Read-only property accessing the destination type.\"\"\"\n        return self.get(\"/Type\")\n\n    @property\n    def zoom(self) -> Optional[int]:\n        \"\"\"Read-only property accessing the zoom factor.\"\"\"\n        return self.get(\"/Zoom\", None)\n\n    @property\n    def left(self) -> Optional[FloatObject]:\n        \"\"\"Read-only property accessing the left horizontal coordinate.\"\"\"\n        return self.get(\"/Left\", None)\n\n    @property\n    def right(self) -> Optional[FloatObject]:\n        \"\"\"Read-only property accessing the right horizontal coordinate.\"\"\"\n        return self.get(\"/Right\", None)\n\n    @property\n    def top(self) -> Optional[FloatObject]:\n        \"\"\"Read-only property accessing the top vertical coordinate.\"\"\"\n        return self.get(\"/Top\", None)\n\n    @property\n    def bottom(self) -> Optional[FloatObject]:\n        \"\"\"Read-only property accessing the bottom vertical coordinate.\"\"\"\n        return self.get(\"/Bottom\", None)\n\n    @property\n    def color(self) -> Optional[\"ArrayObject\"]:\n        \"\"\"Read-only property accessing the color in (R, G, B) with values 0.0-1.0.\"\"\"\n        return self.get(\n            \"/C\", ArrayObject([FloatObject(0), FloatObject(0), FloatObject(0)])\n        )\n\n    @property\n    def font_format(self) -> Optional[OutlineFontFlag]:\n        \"\"\"\n        Read-only property accessing the font type.\n\n        1=italic, 2=bold, 3=both\n        \"\"\"\n        return self.get(\"/F\", 0)\n\n    @property\n    def outline_count(self) -> Optional[int]:\n        \"\"\"\n        Read-only property accessing the outline count.\n\n        positive = expanded\n        negative = collapsed\n        absolute value = number of visible descendants at all levels\n        \"\"\"\n        return self.get(\"/Count\", None)\n"
  },
  {
    "path": "pypdf/generic/_files.py",
    "content": "from __future__ import annotations\n\nimport bisect\nfrom functools import cached_property\nfrom typing import TYPE_CHECKING, cast\n\nfrom pypdf._utils import format_iso8824_date, parse_iso8824_date\nfrom pypdf.constants import CatalogAttributes as CA\nfrom pypdf.constants import FileSpecificationDictionaryEntries\nfrom pypdf.constants import PageAttributes as PG\nfrom pypdf.errors import PdfReadError, PyPdfError\nfrom pypdf.generic import (\n    ArrayObject,\n    ByteStringObject,\n    DecodedStreamObject,\n    DictionaryObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    StreamObject,\n    TextStringObject,\n    is_null_or_none,\n)\n\nif TYPE_CHECKING:\n    import datetime\n    from collections.abc import Generator\n\n    from pypdf._writer import PdfWriter\n\n\nclass EmbeddedFile:\n    \"\"\"\n    Container holding the information on an embedded file.\n\n    Attributes are evaluated lazily if possible.\n\n    Further information on embedded files can be found in section 7.11 of the PDF 2.0 specification.\n    \"\"\"\n    def __init__(self, name: str, pdf_object: DictionaryObject, parent: ArrayObject | None = None) -> None:\n        \"\"\"\n        Args:\n            name: The (primary) name as provided in the name tree.\n            pdf_object: The corresponding PDF object to allow retrieving further data.\n            parent: The parent list.\n        \"\"\"\n        self._name = name\n        self.pdf_object = pdf_object\n        self._parent = parent\n\n    @property\n    def name(self) -> str:\n        \"\"\"The (primary) name of the embedded file as provided in the name tree.\"\"\"\n        return self._name\n\n    @classmethod\n    def _create_new(cls, writer: PdfWriter, name: str, content: str | bytes) -> EmbeddedFile:\n        \"\"\"\n        Create a new embedded file and add it to the PdfWriter.\n\n        Args:\n            writer: The PdfWriter instance to add the embedded file to.\n            name: The filename to display.\n            content: The data in the file.\n\n        Returns:\n            EmbeddedFile instance for the newly created embedded file.\n        \"\"\"\n        # Convert string content to bytes if needed\n        if isinstance(content, str):\n            content = content.encode(\"latin-1\")\n\n        # Create the file entry (the actual embedded file stream)\n        file_entry = DecodedStreamObject()\n        file_entry.set_data(content)\n        file_entry.update({NameObject(PG.TYPE): NameObject(\"/EmbeddedFile\")})\n\n        # Create the /EF entry\n        ef_entry = DictionaryObject()\n        ef_entry.update({NameObject(\"/F\"): writer._add_object(file_entry)})\n\n        # Create the filespec dictionary\n        from pypdf.generic import create_string_object  # noqa: PLC0415\n        filespec = DictionaryObject()\n        filespec_reference = writer._add_object(filespec)\n        name_object = cast(TextStringObject, create_string_object(name))\n        filespec.update(\n            {\n                NameObject(PG.TYPE): NameObject(\"/Filespec\"),\n                NameObject(FileSpecificationDictionaryEntries.F): name_object,\n                NameObject(FileSpecificationDictionaryEntries.EF): ef_entry,\n            }\n        )\n\n        # Add the name and filespec to the names array.\n        # We use the inverse order for insertion, as this allows us to re-use the\n        # same index.\n        names_array = cls._get_names_array(writer)\n        insertion_index = cls._get_insertion_index(names_array, name_object)\n        names_array.insert(insertion_index, filespec_reference)\n        names_array.insert(insertion_index, name_object)\n\n        # Return an EmbeddedFile instance\n        return cls(name=name, pdf_object=filespec, parent=names_array)\n\n    @classmethod\n    def _get_names_array(cls, writer: PdfWriter) -> ArrayObject:\n        \"\"\"Get the names array for embedded files, possibly creating and flattening it.\"\"\"\n        if CA.NAMES not in writer.root_object:\n            # Add the /Names entry to the catalog.\n            writer.root_object[NameObject(CA.NAMES)] = writer._add_object(DictionaryObject())\n\n        names_dict = cast(DictionaryObject, writer.root_object[CA.NAMES])\n        if \"/EmbeddedFiles\" not in names_dict:\n            # We do not yet have an entry for embedded files. Create and return it.\n            names = ArrayObject()\n            embedded_files_names_dictionary = DictionaryObject(\n                {NameObject(CA.NAMES): names}\n            )\n            names_dict[NameObject(\"/EmbeddedFiles\")] = writer._add_object(embedded_files_names_dictionary)\n            return names\n\n        # We have an existing embedded files entry.\n        embedded_files_names_tree = cast(DictionaryObject, names_dict[\"/EmbeddedFiles\"])\n        if \"/Names\" in embedded_files_names_tree:\n            # Simple case: We already have a flat list.\n            return cast(ArrayObject, embedded_files_names_tree[NameObject(CA.NAMES)])\n        if \"/Kids\" not in embedded_files_names_tree:\n            # Invalid case: This is no name tree.\n            raise PdfReadError(\"Got neither Names nor Kids in embedded files tree.\")\n\n        # Complex case: Convert a /Kids-based name tree to a /Names-based one.\n        # /Name-based ones are much easier to handle and allow us to simplify the\n        # actual insertion logic by only having to consider one case.\n        names = ArrayObject()\n        kids = cast(ArrayObject, embedded_files_names_tree[\"/Kids\"].get_object())\n        embedded_files_names_dictionary = DictionaryObject(\n            {NameObject(CA.NAMES): names}\n        )\n        names_dict[NameObject(\"/EmbeddedFiles\")] = writer._add_object(embedded_files_names_dictionary)\n        for kid in kids:\n            # Write the flattened file entries. As we do not change the actual files,\n            # this should not have any impact on references to them.\n            # There might be further (nested) kids here.\n            # Wait for an example before evaluating an implementation.\n            for name in kid.get_object().get(\"/Names\", []):\n                names.append(name)\n        return names\n\n    @classmethod\n    def _get_insertion_index(cls, names_array: ArrayObject, name: str) -> int:\n        keys = [names_array[i].encode(\"utf-8\") for i in range(0, len(names_array), 2)]\n        name_bytes = name.encode(\"utf-8\")\n\n        start = bisect.bisect_left(keys, name_bytes)\n        end = bisect.bisect_right(keys, name_bytes)\n\n        if start != end:\n            return end * 2\n        if start == 0:\n            return 0\n        if start == (key_count := len(keys)):\n            return key_count * 2\n        return end * 2\n\n    @property\n    def alternative_name(self) -> str | None:\n        \"\"\"Retrieve the alternative name (file specification).\"\"\"\n        for key in [FileSpecificationDictionaryEntries.UF, FileSpecificationDictionaryEntries.F]:\n            # PDF 2.0 reference, table 43:\n            #   > A PDF reader shall use the value of the UF key, when present, instead of the F key.\n            if key in self.pdf_object:\n                value = self.pdf_object[key].get_object()\n                if not is_null_or_none(value):\n                    return cast(str, value)\n        return None\n\n    @alternative_name.setter\n    def alternative_name(self, value: TextStringObject | None) -> None:\n        \"\"\"Set the alternative name (file specification).\"\"\"\n        if value is None:\n            if FileSpecificationDictionaryEntries.UF in self.pdf_object:\n                self.pdf_object[NameObject(FileSpecificationDictionaryEntries.UF)] = NullObject()\n            if FileSpecificationDictionaryEntries.F in self.pdf_object:\n                self.pdf_object[NameObject(FileSpecificationDictionaryEntries.F)] = NullObject()\n        else:\n            self.pdf_object[NameObject(FileSpecificationDictionaryEntries.UF)] = value\n            self.pdf_object[NameObject(FileSpecificationDictionaryEntries.F)] = value\n\n    @property\n    def description(self) -> str | None:\n        \"\"\"Retrieve the description.\"\"\"\n        value = self.pdf_object.get(FileSpecificationDictionaryEntries.DESC)\n        if is_null_or_none(value):\n            return None\n        return value\n\n    @description.setter\n    def description(self, value: TextStringObject | None) -> None:\n        \"\"\"Set the description.\"\"\"\n        if value is None:\n            self.pdf_object[NameObject(FileSpecificationDictionaryEntries.DESC)] = NullObject()\n        else:\n            self.pdf_object[NameObject(FileSpecificationDictionaryEntries.DESC)] = value\n\n    @property\n    def associated_file_relationship(self) -> str:\n        \"\"\"Retrieve the relationship of the referring document to this embedded file.\"\"\"\n        return self.pdf_object.get(\"/AFRelationship\", \"/Unspecified\")\n\n    @associated_file_relationship.setter\n    def associated_file_relationship(self, value: NameObject) -> None:\n        \"\"\"Set the relationship of the referring document to this embedded file.\"\"\"\n        self.pdf_object[NameObject(\"/AFRelationship\")] = value\n\n    @property\n    def _embedded_file(self) -> StreamObject:\n        \"\"\"Retrieve the actual embedded file stream.\"\"\"\n        if \"/EF\" not in self.pdf_object:\n            raise PdfReadError(f\"/EF entry not found: {self.pdf_object}\")\n        ef = cast(DictionaryObject, self.pdf_object[\"/EF\"])\n        for key in [FileSpecificationDictionaryEntries.UF, FileSpecificationDictionaryEntries.F]:\n            if key in ef:\n                return cast(StreamObject, ef[key].get_object())\n        raise PdfReadError(f\"No /(U)F key found in file dictionary: {ef}\")\n\n    @property\n    def _params(self) -> DictionaryObject:\n        \"\"\"Retrieve the file-specific parameters.\"\"\"\n        return self._embedded_file.get(\"/Params\", DictionaryObject()).get_object()\n\n    @cached_property\n    def _ensure_params(self) -> DictionaryObject:\n        \"\"\"Ensure the /Params dictionary exists and return it.\"\"\"\n        embedded_file = self._embedded_file\n        if \"/Params\" not in embedded_file:\n            embedded_file[NameObject(\"/Params\")] = DictionaryObject()\n        return cast(DictionaryObject, embedded_file[\"/Params\"])\n\n    @property\n    def subtype(self) -> str | None:\n        \"\"\"Retrieve the subtype. This is a MIME media type, prefixed by a slash.\"\"\"\n        value = self._embedded_file.get(\"/Subtype\")\n        if is_null_or_none(value):\n            return None\n        return value\n\n    @subtype.setter\n    def subtype(self, value: NameObject | None) -> None:\n        \"\"\"Set the subtype. This should be a MIME media type, prefixed by a slash.\"\"\"\n        embedded_file = self._embedded_file\n        if value is None:\n            embedded_file[NameObject(\"/Subtype\")] = NullObject()\n        else:\n            embedded_file[NameObject(\"/Subtype\")] = value\n\n    @property\n    def content(self) -> bytes:\n        \"\"\"Retrieve the actual file content.\"\"\"\n        return self._embedded_file.get_data()\n\n    @content.setter\n    def content(self, value: str | bytes) -> None:\n        \"\"\"Set the file content.\"\"\"\n        if isinstance(value, str):\n            value = value.encode(\"latin-1\")\n        self._embedded_file.set_data(value)\n\n    @property\n    def size(self) -> int | None:\n        \"\"\"Retrieve the size of the uncompressed file in bytes.\"\"\"\n        value = self._params.get(\"/Size\")\n        if is_null_or_none(value):\n            return None\n        return value\n\n    @size.setter\n    def size(self, value: NumberObject | None) -> None:\n        \"\"\"Set the size of the uncompressed file in bytes.\"\"\"\n        params = self._ensure_params\n        if value is None:\n            params[NameObject(\"/Size\")] = NullObject()\n        else:\n            params[NameObject(\"/Size\")] = value\n\n    @property\n    def creation_date(self) -> datetime.datetime | None:\n        \"\"\"Retrieve the file creation datetime.\"\"\"\n        return parse_iso8824_date(self._params.get(\"/CreationDate\"))\n\n    @creation_date.setter\n    def creation_date(self, value: datetime.datetime | None) -> None:\n        \"\"\"Set the file creation datetime.\"\"\"\n        params = self._ensure_params\n        if value is None:\n            params[NameObject(\"/CreationDate\")] = NullObject()\n        else:\n            date_str = format_iso8824_date(value)\n            params[NameObject(\"/CreationDate\")] = TextStringObject(date_str)\n\n    @property\n    def modification_date(self) -> datetime.datetime | None:\n        \"\"\"Retrieve the datetime of the last file modification.\"\"\"\n        return parse_iso8824_date(self._params.get(\"/ModDate\"))\n\n    @modification_date.setter\n    def modification_date(self, value: datetime.datetime | None) -> None:\n        \"\"\"Set the datetime of the last file modification.\"\"\"\n        params = self._ensure_params\n        if value is None:\n            params[NameObject(\"/ModDate\")] = NullObject()\n        else:\n            date_str = format_iso8824_date(value)\n            params[NameObject(\"/ModDate\")] = TextStringObject(date_str)\n\n    @property\n    def checksum(self) -> bytes | None:\n        \"\"\"Retrieve the MD5 checksum of the (uncompressed) file.\"\"\"\n        value = self._params.get(\"/CheckSum\")\n        if is_null_or_none(value):\n            return None\n        return value\n\n    @checksum.setter\n    def checksum(self, value: ByteStringObject | None) -> None:\n        \"\"\"Set the MD5 checksum of the (uncompressed) file.\"\"\"\n        params = self._ensure_params\n        if value is None:\n            params[NameObject(\"/CheckSum\")] = NullObject()\n        else:\n            params[NameObject(\"/CheckSum\")] = value\n\n    def delete(self) -> None:\n        \"\"\"Delete the file from the document.\"\"\"\n        if not self._parent:\n            raise PyPdfError(\"Parent required to delete file from document.\")\n        if self.pdf_object in self._parent:\n            index = self._parent.index(self.pdf_object)\n        elif (\n                (indirect_reference := getattr(self.pdf_object, \"indirect_reference\", None)) is not None\n                and indirect_reference in self._parent\n        ):\n            index = self._parent.index(indirect_reference)\n        else:\n            raise PyPdfError(\"File not found in parent object.\")\n        self._parent.pop(index)  # Reference.\n        self._parent.pop(index - 1)  # Name.\n        self.pdf_object = DictionaryObject()  # Invalidate.\n\n    def __repr__(self) -> str:\n        return f\"<{self.__class__.__name__} name={self.name!r}>\"\n\n    @classmethod\n    def _load_from_names(cls, names: ArrayObject) -> Generator[EmbeddedFile]:\n        \"\"\"\n        Convert the given name tree into class instances.\n\n        Args:\n            names: The name tree to load the data from.\n\n        Returns:\n            Iterable of class instances for the files found.\n        \"\"\"\n        # This is a name tree of the format [name_1, reference_1, name_2, reference_2, ...]\n        for i, name in enumerate(names):\n            if not isinstance(name, str):\n                # Skip plain strings and retrieve them as `direct_name` by index.\n                file_dictionary = name.get_object()\n                direct_name = names[i - 1].get_object()\n                yield EmbeddedFile(name=direct_name, pdf_object=file_dictionary, parent=names)\n\n    @classmethod\n    def _load(cls, catalog: DictionaryObject) -> Generator[EmbeddedFile]:\n        \"\"\"\n        Load the embedded files for the given document catalog.\n\n        This method and its signature are considered internal API and thus not exposed publicly for now.\n\n        Args:\n            catalog: The document catalog to load from.\n\n        Returns:\n            Iterable of class instances for the files found.\n        \"\"\"\n        try:\n            container = cast(\n                DictionaryObject,\n                cast(DictionaryObject, catalog[\"/Names\"])[\"/EmbeddedFiles\"],\n            )\n        except KeyError:\n            return\n\n        if \"/Kids\" in container:\n            for kid in cast(ArrayObject, container[\"/Kids\"].get_object()):\n                # There might be further (nested) kids here.\n                # Wait for an example before evaluating an implementation.\n                kid = kid.get_object()\n                if \"/Names\" in kid:\n                    yield from cls._load_from_names(cast(ArrayObject, kid[\"/Names\"]))\n        if \"/Names\" in container:\n            yield from cls._load_from_names(cast(ArrayObject, container[\"/Names\"]))\n"
  },
  {
    "path": "pypdf/generic/_fit.py",
    "content": "from typing import Any, Optional, Union\n\nfrom ._base import is_null_or_none\n\n\nclass Fit:\n    def __init__(\n        self, fit_type: str, fit_args: tuple[Union[None, float, Any], ...] = ()\n    ) -> None:\n        from ._base import FloatObject, NameObject, NullObject, NumberObject  # noqa: PLC0415\n\n        self.fit_type = NameObject(fit_type)\n        self.fit_args: list[Union[NullObject, FloatObject, NumberObject]] = [\n            NullObject() if is_null_or_none(a) else FloatObject(a) for a in fit_args\n        ]\n\n    @classmethod\n    def xyz(\n        cls,\n        left: Optional[float] = None,\n        top: Optional[float] = None,\n        zoom: Optional[float] = None,\n    ) -> \"Fit\":\n        \"\"\"\n        Display the page designated by page, with the coordinates (left, top)\n        positioned at the upper-left corner of the window and the contents\n        of the page magnified by the factor zoom.\n\n        A null value for any of the parameters left, top, or zoom specifies\n        that the current value of that parameter is to be retained unchanged.\n\n        A zoom value of 0 has the same meaning as a null value.\n\n        Args:\n            left:\n            top:\n            zoom:\n\n        Returns:\n            The created fit object.\n\n        \"\"\"\n        return Fit(fit_type=\"/XYZ\", fit_args=(left, top, zoom))\n\n    @classmethod\n    def fit(cls) -> \"Fit\":\n        \"\"\"\n        Display the page designated by page, with its contents magnified just\n        enough to fit the entire page within the window both horizontally and\n        vertically.\n\n        If the required horizontal and vertical magnification factors are\n        different, use the smaller of the two, centering the page within the\n        window in the other dimension.\n        \"\"\"\n        return Fit(fit_type=\"/Fit\")\n\n    @classmethod\n    def fit_horizontally(cls, top: Optional[float] = None) -> \"Fit\":\n        \"\"\"\n        Display the page designated by page, with the vertical coordinate top\n        positioned at the top edge of the window and the contents of the page\n        magnified just enough to fit the entire width of the page within the\n        window.\n\n        A null value for ``top`` specifies that the current value of that\n        parameter is to be retained unchanged.\n\n        Args:\n            top:\n\n        Returns:\n            The created fit object.\n\n        \"\"\"\n        return Fit(fit_type=\"/FitH\", fit_args=(top,))\n\n    @classmethod\n    def fit_vertically(cls, left: Optional[float] = None) -> \"Fit\":\n        return Fit(fit_type=\"/FitV\", fit_args=(left,))\n\n    @classmethod\n    def fit_rectangle(\n        cls,\n        left: Optional[float] = None,\n        bottom: Optional[float] = None,\n        right: Optional[float] = None,\n        top: Optional[float] = None,\n    ) -> \"Fit\":\n        \"\"\"\n        Display the page designated by page, with its contents magnified\n        just enough to fit the rectangle specified by the coordinates\n        left, bottom, right, and top entirely within the window\n        both horizontally and vertically.\n\n        If the required horizontal and vertical magnification factors are\n        different, use the smaller of the two, centering the rectangle within\n        the window in the other dimension.\n\n        A null value for any of the parameters may result in unpredictable\n        behavior.\n\n        Args:\n            left:\n            bottom:\n            right:\n            top:\n\n        Returns:\n            The created fit object.\n\n        \"\"\"\n        return Fit(fit_type=\"/FitR\", fit_args=(left, bottom, right, top))\n\n    @classmethod\n    def fit_box(cls) -> \"Fit\":\n        \"\"\"\n        Display the page designated by page, with its contents magnified just\n        enough to fit its bounding box entirely within the window both\n        horizontally and vertically.\n\n        If the required horizontal and vertical magnification factors are\n        different, use the smaller of the two, centering the bounding box\n        within the window in the other dimension.\n        \"\"\"\n        return Fit(fit_type=\"/FitB\")\n\n    @classmethod\n    def fit_box_horizontally(cls, top: Optional[float] = None) -> \"Fit\":\n        \"\"\"\n        Display the page designated by page, with the vertical coordinate top\n        positioned at the top edge of the window and the contents of the page\n        magnified just enough to fit the entire width of its bounding box\n        within the window.\n\n        A null value for top specifies that the current value of that parameter\n        is to be retained unchanged.\n\n        Args:\n            top:\n\n        Returns:\n            The created fit object.\n\n        \"\"\"\n        return Fit(fit_type=\"/FitBH\", fit_args=(top,))\n\n    @classmethod\n    def fit_box_vertically(cls, left: Optional[float] = None) -> \"Fit\":\n        \"\"\"\n        Display the page designated by page, with the horizontal coordinate\n        left positioned at the left edge of the window and the contents of the\n        page magnified just enough to fit the entire height of its bounding box\n        within the window.\n\n        A null value for left specifies that the current value of that\n        parameter is to be retained unchanged.\n\n        Args:\n            left:\n\n        Returns:\n            The created fit object.\n\n        \"\"\"\n        return Fit(fit_type=\"/FitBV\", fit_args=(left,))\n\n    def __str__(self) -> str:\n        if not self.fit_args:\n            return f\"Fit({self.fit_type})\"\n        return f\"Fit({self.fit_type}, {self.fit_args})\"\n\n\nDEFAULT_FIT = Fit.fit()\n"
  },
  {
    "path": "pypdf/generic/_image_inline.py",
    "content": "# Copyright (c) 2024, pypdf contributors\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nimport logging\nfrom io import BytesIO\nfrom typing import IO\n\nfrom .._utils import (\n    WHITESPACES,\n    WHITESPACES_AS_BYTES,\n    StreamType,\n    logger_warning,\n    read_non_whitespace,\n)\nfrom ..errors import PdfReadError\n\nlogger = logging.getLogger(__name__)\n\n# An inline image should be used only for small images (4096 bytes or less),\n# but allow twice this for cases where this has been exceeded.\nBUFFER_SIZE = 8192\n\n\ndef _check_end_image_marker(stream: StreamType) -> bool:\n    ei_tok = read_non_whitespace(stream)\n    ei_tok += stream.read(2)\n    stream.seek(-3, 1)\n    return ei_tok[:2] == b\"EI\" and (ei_tok[2:3] == b\"\" or ei_tok[2:3] in WHITESPACES)\n\n\ndef extract_inline__ascii_hex_decode(stream: StreamType) -> bytes:\n    \"\"\"\n    Extract HexEncoded stream from inline image.\n    The stream will be moved onto the EI.\n    \"\"\"\n    data_out: bytes = b\"\"\n    # Read data until delimiter > and EI as backup.\n    while True:\n        data_buffered = read_non_whitespace(stream) + stream.read(BUFFER_SIZE)\n        if not data_buffered:\n            raise PdfReadError(\"Unexpected end of stream\")\n        pos_tok = data_buffered.find(b\">\")\n        if pos_tok >= 0:  # found >\n            data_out += data_buffered[: pos_tok + 1]\n            stream.seek(-len(data_buffered) + pos_tok + 1, 1)\n            break\n        pos_ei = data_buffered.find(b\"EI\")\n        if pos_ei >= 0:  # found EI\n            stream.seek(-len(data_buffered) + pos_ei - 1, 1)\n            c = stream.read(1)\n            while c in WHITESPACES:\n                stream.seek(-2, 1)\n                c = stream.read(1)\n                pos_ei -= 1\n            data_out += data_buffered[:pos_ei]\n            break\n        if len(data_buffered) == 2:\n            data_out += data_buffered\n            raise PdfReadError(\"Unexpected end of stream\")\n        # Neither > nor EI found\n        data_out += data_buffered[:-2]\n        stream.seek(-2, 1)\n\n    if not _check_end_image_marker(stream):\n        raise PdfReadError(\"EI stream not found\")\n    return data_out\n\n\ndef extract_inline__ascii85_decode(stream: StreamType) -> bytes:\n    \"\"\"\n    Extract A85 stream from inline image.\n    The stream will be moved onto the EI.\n    \"\"\"\n    data_out: bytes = b\"\"\n    # Read data until delimiter ~>\n    while True:\n        data_buffered = read_non_whitespace(stream) + stream.read(BUFFER_SIZE)\n        if not data_buffered:\n            raise PdfReadError(\"Unexpected end of stream\")\n        pos_tok = data_buffered.find(b\"~>\")\n        if pos_tok >= 0:  # found!\n            data_out += data_buffered[: pos_tok + 2]\n            stream.seek(-len(data_buffered) + pos_tok + 2, 1)\n            break\n        if len(data_buffered) == 2:  # end of buffer\n            data_out += data_buffered\n            raise PdfReadError(\"Unexpected end of stream\")\n        data_out += data_buffered[\n            :-2\n        ]  # back by one char in case of in the middle of ~>\n        stream.seek(-2, 1)\n\n    if not _check_end_image_marker(stream):\n        raise PdfReadError(\"EI stream not found\")\n    return data_out\n\n\ndef extract_inline__run_length_decode(stream: StreamType) -> bytes:\n    \"\"\"\n    Extract RL (RunLengthDecode) stream from inline image.\n    The stream will be moved onto the EI.\n    \"\"\"\n    data_out: bytes = b\"\"\n    # Read data until delimiter 128\n    while True:\n        data_buffered = stream.read(BUFFER_SIZE)\n        if not data_buffered:\n            raise PdfReadError(\"Unexpected end of stream\")\n        pos_tok = data_buffered.find(b\"\\x80\")\n        if pos_tok >= 0:  # found\n            # Ideally, we could just use plain run-length decoding here, where 80_16 = 128_10\n            # marks the EOD. But there apparently are cases like in issue #3517, where we have\n            # an inline image with up to 51 EOD markers. In these cases, be resilient here and\n            # use the default `EI` marker detection instead. Please note that this fallback\n            # still omits special `EI` handling within the stream, but for now assume that having\n            # both of these cases occur at the same time is very unlikely (and the image stream\n            # is broken anyway).\n            # For now, do not skip over more than one whitespace character.\n            after_token = data_buffered[pos_tok + 1 : pos_tok + 4]\n            if after_token.startswith(b\"EI\") or after_token.endswith(b\"EI\"):\n                data_out += data_buffered[: pos_tok + 1]\n                stream.seek(-len(data_buffered) + pos_tok + 1, 1)\n            else:\n                logger_warning(\"Early EOD in RunLengthDecode of inline image, using fallback.\", __name__)\n                ei_marker = data_buffered.find(b\"EI\")\n                if ei_marker > 0:\n                    data_out += data_buffered[: ei_marker]\n                    stream.seek(-len(data_buffered) + ei_marker - 1, 1)\n            break\n        data_out += data_buffered\n\n    if not _check_end_image_marker(stream):\n        raise PdfReadError(\"EI stream not found\")\n    return data_out\n\n\ndef extract_inline__dct_decode(stream: StreamType) -> bytes:\n    \"\"\"\n    Extract DCT (JPEG) stream from inline image.\n    The stream will be moved onto the EI.\n    \"\"\"\n    def read(length: int) -> bytes:\n        # If 0 bytes are returned, and *size* was not 0, this indicates end of file.\n        # If the object is in non-blocking mode and no bytes are available, `None` is returned.\n        _result = stream.read(length)\n        if _result is None or len(_result) != length:\n            raise PdfReadError(\"Unexpected end of stream\")\n        return _result\n\n    data_out: bytes = b\"\"\n    # Read Blocks of data (ID/Size/data) up to ID=FF/D9\n    # https://www.digicamsoft.com/itu/itu-t81-36.html\n    not_first = False\n    while True:\n        c = read(1)\n        if not_first or (c == b\"\\xff\"):\n            data_out += c\n        if c != b\"\\xff\":\n            continue\n        not_first = True\n        c = read(1)\n        data_out += c\n        if c == b\"\\xff\":\n            stream.seek(-1, 1)  # pragma: no cover\n        elif c == b\"\\x00\":  # stuffing\n            pass\n        elif c == b\"\\xd9\":  # end\n            break\n        elif c in (\n            b\"\\xc0\\xc1\\xc2\\xc3\\xc4\\xc5\\xc6\\xc7\\xc9\\xca\\xcb\\xcc\\xcd\\xce\\xcf\"\n            b\"\\xda\\xdb\\xdc\\xdd\\xde\\xdf\"\n            b\"\\xe0\\xe1\\xe2\\xe3\\xe4\\xe5\\xe6\\xe7\\xe8\\xe9\\xea\\xeb\\xec\\xed\\xee\\xef\\xfe\"\n        ):\n            c = read(2)\n            data_out += c\n            sz = c[0] * 256 + c[1]\n            data_out += read(sz - 2)\n\n    if not _check_end_image_marker(stream):\n        raise PdfReadError(\"EI stream not found\")\n    return data_out\n\n\ndef extract_inline_default(stream: StreamType) -> bytes:\n    \"\"\"Legacy method, used by default\"\"\"\n    stream_out = BytesIO()\n    # Read the inline image, while checking for EI (End Image) operator.\n    while True:\n        data_buffered = stream.read(BUFFER_SIZE)\n        if not data_buffered:\n            raise PdfReadError(\"Unexpected end of stream\")\n        pos_ei = data_buffered.find(\n            b\"E\"\n        )  # We can not look straight for \"EI\" because it may not have been loaded in the buffer\n\n        if pos_ei == -1:\n            stream_out.write(data_buffered)\n        else:\n            # Write out everything including E (the one from EI to be removed)\n            stream_out.write(data_buffered[0 : pos_ei + 1])\n            sav_pos_ei = stream_out.tell() - 1\n            # Seek back in the stream to read the E next\n            stream.seek(pos_ei + 1 - len(data_buffered), 1)\n            saved_pos = stream.tell()\n            # Check for End Image\n            tok2 = stream.read(1)  # I of \"EI\"\n            if tok2 != b\"I\":\n                stream.seek(saved_pos, 0)\n                continue\n            tok3 = stream.read(1)  # possible space after \"EI\"\n            if tok3 not in WHITESPACES:\n                stream.seek(saved_pos, 0)\n                continue\n            while tok3 in WHITESPACES:\n                tok3 = stream.read(1)\n            if data_buffered[pos_ei - 1 : pos_ei] not in WHITESPACES and tok3 not in {\n                b\"Q\",\n                b\"E\",\n            }:  # for Q or EMC\n                stream.seek(saved_pos, 0)\n                continue\n            if is_followed_by_binary_data(stream):\n                # Inline image contains `EI ` sequence usually marking the end of it, but\n                # is followed by binary data which does not make sense for the actual end.\n                stream.seek(saved_pos, 0)\n                continue\n            # Data contains [\\s]EI[\\s](Q|EMC): 4 chars are sufficient\n            # remove E(I) wrongly inserted earlier\n            stream.seek(saved_pos - 1, 0)\n            stream_out.truncate(sav_pos_ei)\n            break\n\n    return stream_out.getvalue()\n\n\ndef is_followed_by_binary_data(stream: IO[bytes], length: int = 10) -> bool:\n    \"\"\"\n    Check if the next bytes of the stream look like binary image data or regular page content.\n\n    This is just some heuristics due to the PDF specification being too imprecise about\n    inline images containing the `EI` marker which would end an image. Starting with PDF 2.0,\n    we finally get a mandatory length field, but with (proper) PDF 2.0 support being very limited\n    everywhere, we should not expect to be able to remove such hacks in the near future - especially\n    considering legacy documents as well.\n\n    The actual implementation draws some inspiration from\n    https://github.com/itext/itext-java/blob/9.1.0/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/util/InlineImageParsingUtils.java\n    \"\"\"\n    position = stream.tell()\n    data = stream.read(length)\n    stream.seek(position)\n    if not data:\n        return False\n    operator_start = None\n    operator_end = None\n\n    for index, byte in enumerate(data):\n        if byte < 32 and byte not in WHITESPACES_AS_BYTES:\n            # This covers all characters not being displayable directly, although omitting whitespace\n            # to allow for operator detection.\n            return True\n        is_whitespace = byte in WHITESPACES_AS_BYTES\n        if operator_start is None and not is_whitespace:\n            # Interpret all other non-whitespace characters as the start of an operation.\n            operator_start = index\n        if operator_start is not None and is_whitespace:\n            # A whitespace stops an operation.\n            # Assume that having an inline image with tons of whitespace is rather unlikely.\n            operator_end = index\n            break\n\n    if operator_start is None:\n        # Inline images should not have tons of whitespaces, which would lead to no operator start.\n        return False\n    if operator_end is None:\n        # We probably are inside an operation.\n        operator_end = length\n    operator_length = operator_end - operator_start\n    operator = data[operator_start:operator_end]\n    if operator.startswith(b\"/\") and operator_length > 1:\n        # Name object.\n        return False\n    if operator.replace(b\".\", b\"\").isdigit():\n        # Graphics operator, for example a move. A number (integer or float).\n        return False\n    if operator_length > 3:  # noqa: SIM103\n        # Usually, the operators inside a content stream should not have more than three characters,\n        # especially after an inline image.\n        return True\n    return False\n"
  },
  {
    "path": "pypdf/generic/_image_xobject.py",
    "content": "\"\"\"Functions to convert an image XObject to an image\"\"\"\n\nimport sys\nfrom io import BytesIO\nfrom typing import Any, Literal, Optional, Union, cast\n\nfrom .._utils import check_if_whitespace_only, logger_warning\nfrom ..constants import ColorSpaces, StreamAttributes\nfrom ..constants import FilterTypes as FT\nfrom ..constants import ImageAttributes as IA\nfrom ..errors import EmptyImageDataError, PdfReadError\nfrom ..generic import (\n    ArrayObject,\n    DecodedStreamObject,\n    EncodedStreamObject,\n    NullObject,\n    TextStringObject,\n    is_null_or_none,\n)\n\nif sys.version_info[:2] >= (3, 10):\n    from typing import TypeAlias\nelse:\n    from typing_extensions import TypeAlias\n\n\ntry:\n    from PIL import Image, UnidentifiedImageError\nexcept ImportError:\n    raise ImportError(\n        \"pillow is required to do image extraction. \"\n        \"It can be installed via 'pip install pypdf[image]'\"\n    )\n\nmode_str_type: TypeAlias = Literal[\n    \"\", \"1\", \"RGB\", \"2bits\", \"4bits\", \"P\", \"L\", \"RGBA\", \"CMYK\"\n]\n\nMAX_IMAGE_MODE_NESTING_DEPTH: int = 10\n\n\ndef _get_image_mode(\n    color_space: Union[str, list[Any], Any],\n    color_components: int,\n    prev_mode: mode_str_type,\n    depth: int = 0,\n) -> tuple[mode_str_type, bool]:\n    \"\"\"\n    Returns:\n        Image mode, not taking into account mask (transparency).\n        ColorInversion is required (like for some DeviceCMYK).\n\n    \"\"\"\n    if depth > MAX_IMAGE_MODE_NESTING_DEPTH:\n        raise PdfReadError(\n            \"Color spaces nested too deeply. If required, consider increasing MAX_IMAGE_MODE_NESTING_DEPTH.\"\n        )\n    if is_null_or_none(color_space):\n        return \"\", False\n    color_space_str: str = \"\"\n    if isinstance(color_space, str):\n        color_space_str = color_space\n    elif not isinstance(color_space, list):\n        raise PdfReadError(\n            \"Cannot interpret color space\", color_space\n        )  # pragma: no cover\n    elif not color_space:\n        return \"\", False\n    elif color_space[0].startswith(\"/Cal\"):  # /CalRGB or /CalGray\n        color_space_str = \"/Device\" + color_space[0][4:]\n    elif color_space[0] == \"/ICCBased\":\n        icc_profile = color_space[1].get_object()\n        color_components = cast(int, icc_profile[\"/N\"])\n        color_space_str = icc_profile.get(\"/Alternate\", \"\")\n    elif color_space[0] == \"/Indexed\":\n        color_space_str = color_space[1].get_object()\n        mode, invert_color = _get_image_mode(\n            color_space_str, color_components, prev_mode, depth + 1\n        )\n        if mode in (\"RGB\", \"CMYK\"):\n            mode = \"P\"\n        return mode, invert_color\n    elif color_space[0] == \"/Separation\":\n        color_space_str = color_space[2].get_object()\n        mode, invert_color = _get_image_mode(\n            color_space_str, color_components, prev_mode, depth + 1\n        )\n        return mode, True\n    elif color_space[0] == \"/DeviceN\":\n        original_color_space = color_space\n        color_components = len(color_space[1])\n        color_space_str = color_space[2].get_object()\n        if color_space_str == \"/DeviceCMYK\" and color_components == 1:\n            if original_color_space[1][0] != \"/Black\":\n                logger_warning(\n                    f\"Color {original_color_space[1][0]} converted to Gray. Please share PDF with pypdf dev team\",\n                    __name__,\n                )\n            return \"L\", True\n        mode, invert_color = _get_image_mode(\n            color_space_str, color_components, prev_mode, depth + 1\n        )\n        return mode, invert_color\n\n    mode_map: dict[str, mode_str_type] = {\n        \"1bit\": \"1\",  # must be zeroth position: color_components may index the values\n        \"/DeviceGray\": \"L\",  # must be first position: color_components may index the values\n        \"palette\": \"P\",  # must be second position: color_components may index the values\n        \"/DeviceRGB\": \"RGB\",  # must be third position: color_components may index the values\n        \"/DeviceCMYK\": \"CMYK\",  # must be fourth position: color_components may index the values\n        \"2bit\": \"2bits\",\n        \"4bit\": \"4bits\",\n    }\n\n    mode = (\n        mode_map.get(color_space_str)\n        or list(mode_map.values())[color_components]\n        or prev_mode\n    )\n\n    return mode, mode == \"CMYK\"\n\n\ndef bits2byte(data: bytes, size: tuple[int, int], bits: int) -> bytes:\n    mask = (1 << bits) - 1\n    byte_buffer = bytearray(size[0] * size[1])\n    data_index = 0\n    bit = 8 - bits\n    for y in range(size[1]):\n        if bit != 8 - bits:\n            data_index += 1\n            bit = 8 - bits\n        for x in range(size[0]):\n            byte_buffer[x + y * size[0]] = (data[data_index] >> bit) & mask\n            bit -= bits\n            if bit < 0:\n                data_index += 1\n                bit = 8 - bits\n    return bytes(byte_buffer)\n\n\ndef _extended_image_from_bytes(\n    mode: str, size: tuple[int, int], data: bytes\n) -> Image.Image:\n    try:\n        img = Image.frombytes(mode, size, data)\n    except ValueError as exc:\n        nb_pix = size[0] * size[1]\n        data_length = len(data)\n        if data_length == 0:\n            raise EmptyImageDataError(\n                \"Data is 0 bytes, cannot process an image from empty data.\"\n            ) from exc\n        if data_length % nb_pix != 0:\n            raise exc\n        k = nb_pix * len(mode) / data_length\n        data = b\"\".join(bytes((x,) * int(k)) for x in data)\n        img = Image.frombytes(mode, size, data)\n    return img\n\n\ndef __handle_flate__indexed(color_space: ArrayObject) -> tuple[Any, Any, Any, Any]:\n    count = len(color_space)\n    if count == 4:\n        color_space, base, hival, lookup = (value.get_object() for value in color_space)\n        return color_space, base, hival, lookup\n\n    # Deal with strange AutoDesk files where `base` and `hival` look like this:\n    #   /DeviceRGB\\x00255\n    element1 = color_space[1]\n    element1 = element1 if isinstance(element1, str) else element1.get_object()\n    if count == 3 and \"\\x00\" in element1:\n        color_space, lookup = color_space[0].get_object(), color_space[2].get_object()\n        base, hival = element1.split(\"\\x00\")\n        hival = int(hival)\n        return color_space, base, hival, lookup\n    raise PdfReadError(f\"Expected color space with 4 values, got {count}: {color_space}\")\n\n\ndef _handle_flate(\n    size: tuple[int, int],\n    data: bytes,\n    mode: mode_str_type,\n    color_space: str,\n    colors: int,\n    obj_as_text: str,\n) -> tuple[Image.Image, str, str, bool]:\n    \"\"\"\n    Process image encoded in flateEncode\n    Returns img, image_format, extension, color inversion\n    \"\"\"\n    extension = \".png\"  # mime_type: \"image/png\"\n    image_format = \"PNG\"\n    lookup: Any\n    base: Any\n    hival: Any\n    if isinstance(color_space, ArrayObject) and color_space[0] == \"/Indexed\":\n        color_space, base, hival, lookup = __handle_flate__indexed(color_space)\n    if mode == \"2bits\":\n        mode = \"P\"\n        data = bits2byte(data, size, 2)\n    elif mode == \"4bits\":\n        mode = \"P\"\n        data = bits2byte(data, size, 4)\n    img = _extended_image_from_bytes(mode, size, data)\n    if color_space == \"/Indexed\":\n        if isinstance(lookup, (EncodedStreamObject, DecodedStreamObject)):\n            lookup = lookup.get_data()\n        if isinstance(lookup, TextStringObject):\n            lookup = lookup.original_bytes\n        if isinstance(lookup, str):\n            lookup = lookup.encode()\n        try:\n            nb, conv, mode = {  # type: ignore\n                \"1\": (0, \"\", \"\"),\n                \"L\": (1, \"P\", \"L\"),\n                \"P\": (0, \"\", \"\"),\n                \"RGB\": (3, \"P\", \"RGB\"),\n                \"CMYK\": (4, \"P\", \"CMYK\"),\n            }[_get_image_mode(base, 0, \"\")[0]]\n        except KeyError:  # pragma: no cover\n            logger_warning(\n                f\"Base {base} not coded please share the pdf file with pypdf dev team\",\n                __name__,\n            )\n            lookup = None\n        else:\n            if img.mode == \"1\":\n                # Two values (\"high\" and \"low\").\n                expected_count = 2 * nb\n                actual_count = len(lookup)\n                if actual_count != expected_count:\n                    if actual_count < expected_count:\n                        logger_warning(\n                            f\"Not enough lookup values: Expected {expected_count}, got {actual_count}.\",\n                            __name__\n                        )\n                        lookup += bytes([0] * (expected_count - actual_count))\n                    elif not check_if_whitespace_only(lookup[expected_count:]):\n                        logger_warning(\n                            f\"Too many lookup values: Expected {expected_count}, got {actual_count}.\",\n                            __name__\n                        )\n                    lookup = lookup[:expected_count]\n                colors_arr = [lookup[:nb], lookup[nb:]]\n                arr = b\"\".join(\n                    b\"\".join(\n                        colors_arr[1 if img.getpixel((x, y)) > 127 else 0]  # type: ignore[operator,unused-ignore]  # TODO: Remove unused-ignore on Python 3.10\n                        for x in range(img.size[0])\n                    )\n                    for y in range(img.size[1])\n                )\n                img = Image.frombytes(mode, img.size, arr)\n            else:\n                img = img.convert(conv)\n                if len(lookup) != (hival + 1) * nb:\n                    logger_warning(f\"Invalid Lookup Table in {obj_as_text}\", __name__)\n                    lookup = None\n                elif mode == \"L\":\n                    # gray lookup does not work: it is converted to a similar RGB lookup\n                    lookup = b\"\".join([bytes([b, b, b]) for b in lookup])\n                    mode = \"RGB\"\n                # TODO: https://github.com/py-pdf/pypdf/pull/2039\n                # this is a work around until PIL is able to process CMYK images\n                elif mode == \"CMYK\":\n                    _rgb = []\n                    for _c, _m, _y, _k in (\n                        lookup[n : n + 4] for n in range(0, 4 * (len(lookup) // 4), 4)\n                    ):\n                        _r = int(255 * (1 - _c / 255) * (1 - _k / 255))\n                        _g = int(255 * (1 - _m / 255) * (1 - _k / 255))\n                        _b = int(255 * (1 - _y / 255) * (1 - _k / 255))\n                        _rgb.append(bytes((_r, _g, _b)))\n                    lookup = b\"\".join(_rgb)\n                    mode = \"RGB\"\n                if lookup is not None:\n                    img.putpalette(lookup, rawmode=mode)\n            img = img.convert(\"L\" if base == ColorSpaces.DEVICE_GRAY else \"RGB\")\n    elif not is_null_or_none(color_space) and color_space[0] == \"/ICCBased\":\n        # Exclude pure black-and-white images.\n        # TODO: The remaining code still does not look correct. Shouldn't the proper way be\n        #       to use the original image and apply the ICC transformation on it?\n        #       For now, this just loads the original image with a different color space.\n        if mode != \"1\":\n            # Table 65 - Additional Entries Specific to an ICC Profile Stream Dictionary\n            mode2 = _get_image_mode(color_space, colors, mode)[0]\n            if mode != mode2:\n                img = Image.frombytes(mode, size, data)  # reloaded as mode may have changed\n    if mode == \"CMYK\":\n        extension = \".tif\"\n        image_format = \"TIFF\"\n    return img, image_format, extension, False\n\n\ndef _handle_jpx(\n    size: tuple[int, int],\n    data: bytes,\n    mode: mode_str_type,\n    color_space: str,\n    colors: int,\n) -> tuple[Image.Image, str, str, bool]:\n    \"\"\"\n    Process image encoded as JPX/JPEG2000\n    Returns img, image_format, extension, inversion\n    \"\"\"\n    extension = \".jp2\"  # mime_type: \"image/x-jp2\"\n    img1: Image.Image = Image.open(BytesIO(data), formats=(\"JPEG2000\",))\n    mode, invert_color = _get_image_mode(color_space, colors, mode)\n    if mode == \"\":\n        mode = cast(mode_str_type, img1.mode)\n        invert_color = mode == \"CMYK\"\n    if img1.mode == \"RGBA\" and mode == \"RGB\":\n        mode = \"RGBA\"\n    # we need to convert to the good mode\n    if img1.mode == mode or {img1.mode, mode} == {\"L\", \"P\"}:  # compare (unordered) sets\n        # L and P are indexed modes which should not be changed.\n        img = img1\n    elif {img1.mode, mode} == {\"RGBA\", \"CMYK\"}:\n        # RGBA / CMYK are 4bytes encoding where\n        # the encoding should be corrected\n        img = Image.frombytes(mode, img1.size, img1.tobytes())\n    else:  # pragma: no cover\n        img = img1.convert(mode)\n    # CMYK conversion\n    # https://stackverflow.com/questions/38855022/\n    if img.mode == \"CMYK\" and color_space == \"/ICCBased\":\n        img = img.convert(\"RGB\")\n    image_format = \"JPEG2000\"\n    return img, image_format, extension, invert_color\n\n\ndef _apply_decode(\n    img: Image.Image,\n    x_object_obj: dict[str, Any],\n    lfilters: FT,\n    color_space: Union[str, list[Any], Any],\n    invert_color: bool,\n) -> Image.Image:\n    # CMYK image and other color spaces without decode\n    # requires reverting scale (cf p243,2§ last sentence)\n    if IA.DECODE in x_object_obj:\n        decode = x_object_obj[IA.DECODE]\n        # if invert_color and lfilters == FT.DCT_DECODE:\n        #     decode = list(reversed(decode))\n    elif img.mode == \"CMYK\" and lfilters == FT.JPX_DECODE:\n        decode = [1.0, 0.0] if not invert_color else [0.0, 1.0]\n        decode = decode * len(img.getbands())\n    elif (img.mode == \"CMYK\" and lfilters == FT.DCT_DECODE) or (invert_color and img.mode == \"L\"):\n        decode = [1.0, 0.0] * len(img.getbands())\n    else:\n        decode = None\n\n    if (\n        isinstance(color_space, ArrayObject)\n        and color_space[0].get_object() == \"/Indexed\"\n    ):\n        decode = None  # decode is meaningless if Indexed\n    if (\n        isinstance(color_space, ArrayObject)\n        and color_space[0].get_object() == \"/Separation\"\n    ):\n        decode = [1.0, 0.0] * len(img.getbands())\n    if decode is not None and not all(decode[i] == i % 2 for i in range(len(decode))):\n        lut: list[int] = []\n        for i in range(0, len(decode), 2):\n            dmin = decode[i]\n            dmax = decode[i + 1]\n            lut.extend(\n                round(255.0 * (j / 255.0 * (dmax - dmin) + dmin)) for j in range(256)\n            )\n        img = img.point(lut)\n    return img\n\n\ndef _get_mode_and_invert_color(\n    x_object_obj: dict[str, Any], colors: int, color_space: Union[str, list[Any], Any]\n) -> tuple[mode_str_type, bool]:\n    if (\n        IA.COLOR_SPACE in x_object_obj\n        and x_object_obj[IA.COLOR_SPACE] == ColorSpaces.DEVICE_RGB\n    ):\n        # https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes\n        mode: mode_str_type = \"RGB\"\n    if x_object_obj.get(\"/BitsPerComponent\", 8) < 8:\n        mode, invert_color = _get_image_mode(\n            f\"{x_object_obj.get('/BitsPerComponent', 8)}bit\", 0, \"\"\n        )\n    else:\n        mode, invert_color = _get_image_mode(\n            color_space,\n            2\n            if (\n                colors == 1\n                and (\n                    not is_null_or_none(color_space)\n                    and \"Gray\" not in color_space\n                )\n            )\n            else colors,\n            \"\",\n        )\n    return mode, invert_color\n\n\ndef _xobj_to_image(\n        x_object: dict[str, Any],\n        pillow_parameters: Union[dict[str, Any], None] = None\n) -> tuple[Optional[str], bytes, Any]:\n    \"\"\"\n    Users need to have the pillow package installed.\n\n    It's unclear if pypdf will keep this function here, hence it's private.\n    It might get removed at any point.\n\n    Args:\n        x_object:\n        pillow_parameters: parameters provided to Pillow Image.save() method,\n            cf. <https://pillow.readthedocs.io/en/stable/reference/Image.html#PIL.Image.Image.save>\n\n    Returns:\n        Tuple[file extension, bytes, PIL.Image.Image]\n\n    \"\"\"\n    def _apply_alpha(\n        img: Image.Image,\n        x_object: dict[str, Any],\n        obj_as_text: str,\n        image_format: str,\n        extension: str,\n    ) -> tuple[Image.Image, str, str]:\n        alpha = None\n        if IA.S_MASK in x_object:  # add alpha channel\n            alpha = _xobj_to_image(x_object[IA.S_MASK])[2]\n            if img.size != alpha.size:\n                logger_warning(\n                    f\"image and mask size not matching: {obj_as_text}\", __name__\n                )\n            else:\n                # TODO: implement mask\n                if alpha.mode != \"L\":\n                    alpha = alpha.convert(\"L\")\n                if img.mode == \"P\":\n                    img = img.convert(\"RGB\")\n                elif img.mode == \"1\":\n                    img = img.convert(\"L\")\n                img.putalpha(alpha)\n            if \"JPEG\" in image_format:\n                image_format = \"JPEG2000\"\n                extension = \".jp2\"\n            else:\n                image_format = \"PNG\"\n                extension = \".png\"\n        return img, extension, image_format\n\n    # For error reporting\n    obj_as_text = (\n        x_object.indirect_reference.__repr__()\n        if x_object is None  # pragma: no cover\n        else x_object.__repr__()\n    )\n\n    # Get size and data\n    size = (cast(int, x_object[IA.WIDTH]), cast(int, x_object[IA.HEIGHT]))\n    data = x_object.get_data()  # type: ignore\n    if isinstance(data, str):  # pragma: no cover\n        data = data.encode()\n    if len(data) % (size[0] * size[1]) == 1 and data[-1] == 0x0A:  # ie. '\\n'\n        data = data[:-1]\n\n    # Get color properties\n    colors = x_object.get(\"/Colors\", 1)\n    color_space: Any = x_object.get(\"/ColorSpace\", NullObject()).get_object()\n    if isinstance(color_space, list) and len(color_space) == 1:\n        color_space = color_space[0].get_object()\n\n    mode, invert_color = _get_mode_and_invert_color(x_object, colors, color_space)\n\n    # Get filters\n    filters = x_object.get(StreamAttributes.FILTER, NullObject()).get_object()\n    lfilters = filters[-1] if isinstance(filters, list) else filters\n    decode_parms = x_object.get(StreamAttributes.DECODE_PARMS)\n    if decode_parms and isinstance(decode_parms, (tuple, list)):\n        decode_parms = decode_parms[0]\n    else:\n        decode_parms = {}\n    if not isinstance(decode_parms, dict):\n        decode_parms = {}\n\n    extension = None\n    if lfilters in (FT.FLATE_DECODE, FT.RUN_LENGTH_DECODE):\n        img, image_format, extension, _ = _handle_flate(\n            size,\n            data,\n            mode,\n            color_space,\n            colors,\n            obj_as_text,\n        )\n    elif lfilters in (FT.LZW_DECODE, FT.ASCII_85_DECODE):\n        # I'm not sure if the following logic is correct.\n        # There might not be any relationship between the filters and the\n        # extension\n        if lfilters == FT.LZW_DECODE:\n            image_format = \"TIFF\"\n            extension = \".tiff\"  # mime_type = \"image/tiff\"\n        else:\n            image_format = \"PNG\"\n            extension = \".png\"  # mime_type = \"image/png\"\n        try:\n            img = Image.open(BytesIO(data), formats=(\"TIFF\", \"PNG\"))\n        except UnidentifiedImageError:\n            img = _extended_image_from_bytes(mode, size, data)\n    elif lfilters == FT.DCT_DECODE:\n        img, image_format, extension = Image.open(BytesIO(data)), \"JPEG\", \".jpg\"\n        # invert_color kept unchanged\n    elif lfilters == FT.JPX_DECODE:\n        img, image_format, extension, invert_color = _handle_jpx(\n            size, data, mode, color_space, colors\n        )\n    elif lfilters == FT.CCITT_FAX_DECODE:\n        img, image_format, extension, invert_color = (\n            Image.open(BytesIO(data), formats=(\"TIFF\",)),\n            \"TIFF\",\n            \".tiff\",\n            False,\n        )\n    elif lfilters == FT.JBIG2_DECODE:\n        img, image_format, extension, invert_color = (\n            Image.open(BytesIO(data), formats=(\"PNG\", \"PPM\")),\n            \"PNG\",\n            \".png\",\n            False,\n        )\n    elif mode == \"CMYK\":\n        img, image_format, extension, invert_color = (\n            _extended_image_from_bytes(mode, size, data),\n            \"TIFF\",\n            \".tif\",\n            False,\n        )\n    elif mode == \"\":\n        raise PdfReadError(f\"ColorSpace field not found in {x_object}\")\n    else:\n        img, image_format, extension, invert_color = (\n            _extended_image_from_bytes(mode, size, data),\n            \"PNG\",\n            \".png\",\n            False,\n        )\n\n    img = _apply_decode(img, x_object, lfilters, color_space, invert_color)\n    img, extension, image_format = _apply_alpha(\n        img, x_object, obj_as_text, image_format, extension\n    )\n\n    if pillow_parameters is None:\n        pillow_parameters = {}\n    # Preserve JPEG image quality - see issue #3515.\n    if image_format == \"JPEG\":\n        # This prevents: Cannot use 'keep' when original image is not a JPEG:\n        # \"JPEG\" is the value of PIL.JpegImagePlugin.JpegImageFile.format\n        img.format = \"JPEG\"\n        if \"quality\" not in pillow_parameters:\n            pillow_parameters[\"quality\"] = \"keep\"\n\n    # Save image to bytes\n    img_byte_arr = BytesIO()\n    try:\n        img.save(img_byte_arr, format=image_format, **pillow_parameters)\n    except OSError:  # pragma: no cover  # covered with pillow 10.3\n        # in case of we convert to RGBA and then to PNG\n        img1 = img.convert(\"RGBA\")\n        image_format = \"PNG\"\n        extension = \".png\"\n        img_byte_arr = BytesIO()\n        img1.save(img_byte_arr, format=image_format)\n    data = img_byte_arr.getvalue()\n\n    try:  # temporary try/except until other fixes of images\n        img = Image.open(BytesIO(data))\n    except Exception as exception:\n        logger_warning(f\"Failed loading image: {exception}\", __name__)\n        img = None  # type: ignore[assignment,unused-ignore]  # TODO: Remove unused-ignore on Python 3.10\n    return extension, data, img\n"
  },
  {
    "path": "pypdf/generic/_link.py",
    "content": "# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\n\n# This module contains code used by _writer.py to track links in pages\n# being added to the writer until the links can be resolved.\n\nfrom typing import TYPE_CHECKING, Optional, Union, cast\n\nfrom .._utils import logger_warning\nfrom . import ArrayObject, DictionaryObject, IndirectObject, PdfObject, TextStringObject, is_null_or_none\n\nif TYPE_CHECKING:\n    from .._page import PageObject\n    from .._reader import PdfReader\n    from .._writer import PdfWriter\n\n\nclass NamedReferenceLink:\n    \"\"\"Named reference link being preserved until we can resolve it correctly.\"\"\"\n\n    def __init__(self, reference: TextStringObject, source_pdf: \"PdfReader\") -> None:\n        \"\"\"reference: TextStringObject with named reference\"\"\"\n        self._reference = reference\n        self._source_pdf = source_pdf\n\n    def find_referenced_page(self) -> Union[IndirectObject, None]:\n        destination = self._source_pdf.named_destinations.get(str(self._reference))\n        return destination.page if destination else None\n\n    def patch_reference(self, target_pdf: \"PdfWriter\", new_page: IndirectObject) -> None:\n        \"\"\"target_pdf: PdfWriter which the new link went into\"\"\"\n        # point named destination in new PDF to the new page\n        if str(self._reference) not in target_pdf.named_destinations:\n            target_pdf.add_named_destination(str(self._reference), new_page.page_number)\n\n\nclass DirectReferenceLink:\n    \"\"\"Direct reference link being preserved until we can resolve it correctly.\"\"\"\n\n    def __init__(self, reference: ArrayObject) -> None:\n        \"\"\"reference: an ArrayObject whose first element is the Page indirect object\"\"\"\n        self._reference = reference\n\n    def find_referenced_page(self) -> IndirectObject:\n        return self._reference[0]\n\n    def patch_reference(self, target_pdf: \"PdfWriter\", new_page: IndirectObject) -> None:\n        \"\"\"target_pdf: PdfWriter which the new link went into\"\"\"\n        self._reference[0] = new_page\n\n\nReferenceLink = Union[NamedReferenceLink, DirectReferenceLink]\n\n\ndef extract_links(new_page: \"PageObject\", old_page: \"PageObject\") -> list[tuple[ReferenceLink, ReferenceLink]]:\n    \"\"\"Extracts links from two pages on the assumption that the two pages are\n    the same. Produces one list of (new link, old link) tuples.\n    \"\"\"\n    new_annotations = new_page.get(\"/Annots\", ArrayObject()).get_object()\n    old_annotations = old_page.get(\"/Annots\", ArrayObject()).get_object()\n    if is_null_or_none(new_annotations):\n        new_annotations = ArrayObject()\n    if is_null_or_none(old_annotations):\n        old_annotations = ArrayObject()\n    if not isinstance(new_annotations, ArrayObject) or not isinstance(old_annotations, ArrayObject):\n        logger_warning(\n            f\"Expected annotation arrays: {old_annotations} {new_annotations}. Ignoring annotations.\",\n            __name__\n        )\n        return []\n    # TODO: Investigate in https://github.com/py-pdf/pypdf/issues/3667\n    # if len(new_annotations) != len(old_annotations):\n    #     logger_warning(f\"Annotation sizes differ: {old_annotations} vs. {new_annotations}\", __name__)\n\n    new_links = [_build_link(link, new_page) for link in new_annotations]\n    old_links = [_build_link(link, old_page) for link in old_annotations]\n\n    return [\n        (new_link, old_link) for (new_link, old_link)\n        in zip(new_links, old_links)\n        if new_link and old_link\n    ]\n\n\ndef _build_link(indirect_object: IndirectObject, page: \"PageObject\") -> Optional[ReferenceLink]:\n    src = cast(\"PdfReader\", page.pdf)\n    link = cast(DictionaryObject, indirect_object.get_object())\n    if (not isinstance(link, DictionaryObject)) or link.get(\"/Subtype\") != \"/Link\":\n        return None\n\n    if \"/A\" in link:\n        action = cast(DictionaryObject, link[\"/A\"])\n        if action.get(\"/S\") != \"/GoTo\":\n            return None\n\n        if \"/D\" not in action:\n            return None\n        return _create_link(action[\"/D\"], src)\n\n    if \"/Dest\" in link:\n        return _create_link(link[\"/Dest\"], src)\n\n    return None  # Nothing to do here\n\n\ndef _create_link(reference: PdfObject, source_pdf: \"PdfReader\") -> Optional[ReferenceLink]:\n    if isinstance(reference, TextStringObject):\n        return NamedReferenceLink(reference, source_pdf)\n    if isinstance(reference, ArrayObject):\n        return DirectReferenceLink(reference)\n    return None\n"
  },
  {
    "path": "pypdf/generic/_outline.py",
    "content": "from typing import Union\n\nfrom .._utils import StreamType, deprecation_no_replacement\nfrom ._base import NameObject\nfrom ._data_structures import Destination\n\n\nclass OutlineItem(Destination):\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        stream.write(b\"<<\\n\")\n        for key in [\n            NameObject(x)\n            for x in [\"/Title\", \"/Parent\", \"/First\", \"/Last\", \"/Next\", \"/Prev\"]\n            if x in self\n        ]:\n            key.write_to_stream(stream)\n            stream.write(b\" \")\n            value = self.raw_get(key)\n            value.write_to_stream(stream)\n            stream.write(b\"\\n\")\n        key = NameObject(\"/Dest\")\n        key.write_to_stream(stream)\n        stream.write(b\" \")\n        value = self.dest_array\n        value.write_to_stream(stream)\n        stream.write(b\"\\n\")\n        stream.write(b\">>\")\n"
  },
  {
    "path": "pypdf/generic/_rectangle.py",
    "content": "from typing import Any, Union\n\nfrom ._base import FloatObject, NumberObject\nfrom ._data_structures import ArrayObject\n\n\nclass RectangleObject(ArrayObject):\n    \"\"\"\n    This class is used to represent *page boxes* in pypdf.\n\n    These boxes include:\n\n    * :attr:`artbox <pypdf._page.PageObject.artbox>`\n    * :attr:`bleedbox <pypdf._page.PageObject.bleedbox>`\n    * :attr:`cropbox <pypdf._page.PageObject.cropbox>`\n    * :attr:`mediabox <pypdf._page.PageObject.mediabox>`\n    * :attr:`trimbox <pypdf._page.PageObject.trimbox>`\n    \"\"\"\n\n    def __init__(\n        self, arr: Union[\"RectangleObject\", tuple[float, float, float, float]]\n    ) -> None:\n        # must have four points\n        assert len(arr) == 4\n        # automatically convert arr[x] into NumberObject(arr[x]) if necessary\n        ArrayObject.__init__(self, [self._ensure_is_number(x) for x in arr])\n\n    def _ensure_is_number(self, value: Any) -> Union[FloatObject, NumberObject]:\n        if not isinstance(value, (FloatObject, NumberObject)):\n            value = FloatObject(value)\n        return value\n\n    def scale(self, sx: float, sy: float) -> \"RectangleObject\":\n        return RectangleObject(\n            (\n                float(self.left) * sx,\n                float(self.bottom) * sy,\n                float(self.right) * sx,\n                float(self.top) * sy,\n            )\n        )\n\n    def __repr__(self) -> str:\n        return f\"RectangleObject({list(self)!r})\"\n\n    @property\n    def left(self) -> FloatObject:\n        return self[0]\n\n    @left.setter\n    def left(self, f: float) -> None:\n        self[0] = FloatObject(f)\n\n    @property\n    def bottom(self) -> FloatObject:\n        return self[1]\n\n    @bottom.setter\n    def bottom(self, f: float) -> None:\n        self[1] = FloatObject(f)\n\n    @property\n    def right(self) -> FloatObject:\n        return self[2]\n\n    @right.setter\n    def right(self, f: float) -> None:\n        self[2] = FloatObject(f)\n\n    @property\n    def top(self) -> FloatObject:\n        return self[3]\n\n    @top.setter\n    def top(self, f: float) -> None:\n        self[3] = FloatObject(f)\n\n    @property\n    def lower_left(self) -> tuple[float, float]:\n        \"\"\"\n        Property to read and modify the lower left coordinate of this box\n        in (x,y) form.\n        \"\"\"\n        return self.left, self.bottom\n\n    @lower_left.setter\n    def lower_left(self, value: tuple[float, float]) -> None:\n        self[0], self[1] = (self._ensure_is_number(x) for x in value)\n\n    @property\n    def lower_right(self) -> tuple[float, float]:\n        \"\"\"\n        Property to read and modify the lower right coordinate of this box\n        in (x,y) form.\n        \"\"\"\n        return self.right, self.bottom\n\n    @lower_right.setter\n    def lower_right(self, value: tuple[float, float]) -> None:\n        self[2], self[1] = (self._ensure_is_number(x) for x in value)\n\n    @property\n    def upper_left(self) -> tuple[float, float]:\n        \"\"\"\n        Property to read and modify the upper left coordinate of this box\n        in (x,y) form.\n        \"\"\"\n        return self.left, self.top\n\n    @upper_left.setter\n    def upper_left(self, value: tuple[float, float]) -> None:\n        self[0], self[3] = (self._ensure_is_number(x) for x in value)\n\n    @property\n    def upper_right(self) -> tuple[float, float]:\n        \"\"\"\n        Property to read and modify the upper right coordinate of this box\n        in (x,y) form.\n        \"\"\"\n        return self.right, self.top\n\n    @upper_right.setter\n    def upper_right(self, value: tuple[float, float]) -> None:\n        self[2], self[3] = (self._ensure_is_number(x) for x in value)\n\n    @property\n    def width(self) -> float:\n        return self.right - self.left\n\n    @property\n    def height(self) -> float:\n        return self.top - self.bottom\n"
  },
  {
    "path": "pypdf/generic/_utils.py",
    "content": "import codecs\nfrom typing import Union\n\nfrom .._codecs import _pdfdoc_encoding\nfrom .._utils import StreamType, logger_warning, read_non_whitespace\nfrom ..errors import STREAM_TRUNCATED_PREMATURELY, PdfStreamError\nfrom ._base import ByteStringObject, TextStringObject\n\n\ndef hex_to_rgb(value: str) -> tuple[float, float, float]:\n    return tuple(int(value.lstrip(\"#\")[i : i + 2], 16) / 255.0 for i in (0, 2, 4))  # type: ignore\n\n\ndef read_hex_string_from_stream(\n    stream: StreamType,\n    forced_encoding: Union[None, str, list[str], dict[int, str]] = None,\n) -> Union[\"TextStringObject\", \"ByteStringObject\"]:\n    stream.read(1)\n    arr = []\n    x = b\"\"\n    while True:\n        tok = read_non_whitespace(stream)\n        if not tok:\n            raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)\n        if tok == b\">\":\n            break\n        x += tok\n        if len(x) == 2:\n            arr.append(int(x, base=16))\n            x = b\"\"\n    if len(x) == 1:\n        x += b\"0\"\n    if x != b\"\":\n        arr.append(int(x, base=16))\n    return create_string_object(bytes(arr), forced_encoding)\n\n\n__ESCAPE_DICT__ = {\n    b\"n\": ord(b\"\\n\"),\n    b\"r\": ord(b\"\\r\"),\n    b\"t\": ord(b\"\\t\"),\n    b\"b\": ord(b\"\\b\"),\n    b\"f\": ord(b\"\\f\"),\n    b\"(\": ord(b\"(\"),\n    b\")\": ord(b\")\"),\n    b\"/\": ord(b\"/\"),\n    b\"\\\\\": ord(b\"\\\\\"),\n    b\" \": ord(b\" \"),\n    b\"%\": ord(b\"%\"),\n    b\"<\": ord(b\"<\"),\n    b\">\": ord(b\">\"),\n    b\"[\": ord(b\"[\"),\n    b\"]\": ord(b\"]\"),\n    b\"#\": ord(b\"#\"),\n    b\"_\": ord(b\"_\"),\n    b\"&\": ord(b\"&\"),\n    b\"$\": ord(b\"$\"),\n}\n__BACKSLASH_CODE__ = 92\n\n\ndef read_string_from_stream(\n    stream: StreamType,\n    forced_encoding: Union[None, str, list[str], dict[int, str]] = None,\n) -> Union[\"TextStringObject\", \"ByteStringObject\"]:\n    tok = stream.read(1)\n    parens = 1\n    txt = []\n    while True:\n        tok = stream.read(1)\n        if not tok:\n            raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY)\n        if tok == b\"(\":\n            parens += 1\n        elif tok == b\")\":\n            parens -= 1\n            if parens == 0:\n                break\n        elif tok == b\"\\\\\":\n            tok = stream.read(1)\n            try:\n                txt.append(__ESCAPE_DICT__[tok])\n                continue\n            except KeyError:\n                if b\"0\" <= tok <= b\"7\":\n                    # \"The number ddd may consist of one, two, or three\n                    # octal digits; high-order overflow shall be ignored.\n                    # Three octal digits shall be used, with leading zeros\n                    # as needed, if the next character of the string is also\n                    # a digit.\" (PDF reference 7.3.4.2, p 16)\n                    sav = stream.tell() - 1\n                    for _ in range(2):\n                        ntok = stream.read(1)\n                        if b\"0\" <= ntok <= b\"7\":\n                            tok += ntok\n                        else:\n                            stream.seek(-1, 1)  # ntok has to be analyzed\n                            break\n                    i = int(tok, base=8)\n                    if i > 255:\n                        txt.append(__BACKSLASH_CODE__)\n                        stream.seek(sav)\n                    else:\n                        txt.append(i)\n                    continue\n                if tok in b\"\\n\\r\":\n                    # This case is hit when a backslash followed by a line\n                    # break occurs. If it's a multi-char EOL, consume the\n                    # second character:\n                    tok = stream.read(1)\n                    if tok not in b\"\\n\\r\":\n                        stream.seek(-1, 1)\n                    # Then don't add anything to the actual string, since this\n                    # line break was escaped:\n                    continue\n                msg = f\"Unexpected escaped string: {tok.decode('utf-8', 'ignore')}\"\n                logger_warning(msg, __name__)\n                txt.append(__BACKSLASH_CODE__)\n        txt.append(ord(tok))\n    return create_string_object(bytes(txt), forced_encoding)\n\n\ndef create_string_object(\n    string: Union[str, bytes],\n    forced_encoding: Union[None, str, list[str], dict[int, str]] = None,\n) -> Union[TextStringObject, ByteStringObject]:\n    \"\"\"\n    Create a ByteStringObject or a TextStringObject from a string to represent the string.\n\n    Args:\n        string: The data being used\n        forced_encoding: Typically None, or an encoding string\n\n    Returns:\n        A ByteStringObject\n\n    Raises:\n        TypeError: If string is not of type str or bytes.\n\n    \"\"\"\n    if isinstance(string, str):\n        return TextStringObject(string)\n    if isinstance(string, bytes):\n        if isinstance(forced_encoding, (list, dict)):\n            out = \"\"\n            for x in string:\n                try:\n                    out += forced_encoding[x]\n                except Exception:\n                    out += bytes((x,)).decode(\"charmap\")\n            obj = TextStringObject(out)\n            obj._original_bytes = string\n            return obj\n        if isinstance(forced_encoding, str):\n            if forced_encoding == \"bytes\":\n                return ByteStringObject(string)\n            obj = TextStringObject(string.decode(forced_encoding))\n            obj._original_bytes = string\n            return obj\n        try:\n            if string.startswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)):\n                retval = TextStringObject(string.decode(\"utf-16\"))\n                retval._original_bytes = string\n                retval.autodetect_utf16 = True\n                retval.utf16_bom = string[:2]\n                return retval\n            if string.startswith(b\"\\x00\"):\n                retval = TextStringObject(string.decode(\"utf-16be\"))\n                retval._original_bytes = string\n                retval.autodetect_utf16 = True\n                retval.utf16_bom = codecs.BOM_UTF16_BE\n                return retval\n            if string[1:2] == b\"\\x00\":\n                retval = TextStringObject(string.decode(\"utf-16le\"))\n                retval._original_bytes = string\n                retval.autodetect_utf16 = True\n                retval.utf16_bom = codecs.BOM_UTF16_LE\n                return retval\n\n            # This is probably a big performance hit here, but we need\n            # to convert string objects into the text/unicode-aware\n            # version if possible... and the only way to check if that's\n            # possible is to try.\n            # Some strings are strings, some are just byte arrays.\n            retval = TextStringObject(decode_pdfdocencoding(string))\n            retval._original_bytes = string\n            retval.autodetect_pdfdocencoding = True\n            return retval\n        except UnicodeDecodeError:\n            return ByteStringObject(string)\n    else:\n        raise TypeError(\"create_string_object should have str or unicode arg\")\n\n\ndef decode_pdfdocencoding(byte_array: bytes) -> str:\n    retval = \"\"\n    for b in byte_array:\n        c = _pdfdoc_encoding[b]\n        if c == \"\\u0000\":\n            raise UnicodeDecodeError(\n                \"pdfdocencoding\",\n                bytearray(b),\n                -1,\n                -1,\n                \"does not exist in translation table\",\n            )\n        retval += c\n    return retval\n"
  },
  {
    "path": "pypdf/generic/_viewerpref.py",
    "content": "# Copyright (c) 2023, Pubpub-ZZ\n#\n# All rights reserved.\n#\n# Redistribution and use in source and binary forms, with or without\n# modification, are permitted provided that the following conditions are\n# met:\n#\n# * Redistributions of source code must retain the above copyright notice,\n# this list of conditions and the following disclaimer.\n# * Redistributions in binary form must reproduce the above copyright notice,\n# this list of conditions and the following disclaimer in the documentation\n# and/or other materials provided with the distribution.\n# * The name of the author may not be used to endorse or promote products\n# derived from this software without specific prior written permission.\n#\n# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS \"AS IS\"\n# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE\n# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE\n# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE\n# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR\n# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF\n# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS\n# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN\n# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)\n# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE\n# POSSIBILITY OF SUCH DAMAGE.\n\nfrom typing import (\n    Any,\n    Optional,\n)\n\nfrom ._base import BooleanObject, NameObject, NumberObject, is_null_or_none\nfrom ._data_structures import ArrayObject, DictionaryObject\n\nf_obj = BooleanObject(False)\n\n\nclass ViewerPreferences(DictionaryObject):\n    def __init__(self, obj: Optional[DictionaryObject] = None) -> None:\n        super().__init__(self)\n        if not is_null_or_none(obj):\n            self.update(obj.items())  # type: ignore\n        try:\n            self.indirect_reference = obj.indirect_reference  # type: ignore\n        except AttributeError:\n            pass\n\n    def _get_bool(self, key: str, default: Optional[BooleanObject]) -> Optional[BooleanObject]:\n        return self.get(key, default)\n\n    def _set_bool(self, key: str, v: bool) -> None:\n        self[NameObject(key)] = BooleanObject(v is True)\n\n    def _get_name(self, key: str, default: Optional[NameObject]) -> Optional[NameObject]:\n        return self.get(key, default)\n\n    def _set_name(self, key: str, lst: list[str], v: NameObject) -> None:\n        if v[0] != \"/\":\n            raise ValueError(f\"{v} does not start with '/'\")\n        if lst != [] and v not in lst:\n            raise ValueError(f\"{v} is an unacceptable value\")\n        self[NameObject(key)] = NameObject(v)\n\n    def _get_arr(self, key: str, default: Optional[list[Any]]) -> Optional[ArrayObject]:\n        return self.get(key, None if default is None else ArrayObject(default))\n\n    def _set_arr(self, key: str, v: Optional[ArrayObject]) -> None:\n        if v is None:\n            try:\n                del self[NameObject(key)]\n            except KeyError:\n                pass\n            return\n        if not isinstance(v, ArrayObject):\n            raise ValueError(\"ArrayObject is expected\")\n        self[NameObject(key)] = v\n\n    def _get_int(self, key: str, default: Optional[NumberObject]) -> Optional[NumberObject]:\n        return self.get(key, default)\n\n    def _set_int(self, key: str, v: int) -> None:\n        self[NameObject(key)] = NumberObject(v)\n\n    @property\n    def PRINT_SCALING(self) -> NameObject:\n        return NameObject(\"/PrintScaling\")\n\n    def __new__(cls: Any, value: Any = None) -> \"ViewerPreferences\":  # noqa: PYI034\n        def _add_prop_bool(key: str, default: Optional[BooleanObject]) -> property:\n            return property(\n                lambda self: self._get_bool(key, default),\n                lambda self, v: self._set_bool(key, v),\n                None,\n                f\"\"\"\n            Returns/Modify the status of {key}, Returns {default} if not defined\n            \"\"\",\n            )\n\n        def _add_prop_name(\n            key: str, lst: list[str], default: Optional[NameObject]\n        ) -> property:\n            return property(\n                lambda self: self._get_name(key, default),\n                lambda self, v: self._set_name(key, lst, v),\n                None,\n                f\"\"\"\n            Returns/Modify the status of {key}, Returns {default} if not defined.\n            Acceptable values: {lst}\n            \"\"\",\n            )\n\n        def _add_prop_arr(key: str, default: Optional[ArrayObject]) -> property:\n            return property(\n                lambda self: self._get_arr(key, default),\n                lambda self, v: self._set_arr(key, v),\n                None,\n                f\"\"\"\n            Returns/Modify the status of {key}, Returns {default} if not defined\n            \"\"\",\n            )\n\n        def _add_prop_int(key: str, default: Optional[int]) -> property:\n            return property(\n                lambda self: self._get_int(key, default),\n                lambda self, v: self._set_int(key, v),\n                None,\n                f\"\"\"\n            Returns/Modify the status of {key}, Returns {default} if not defined\n            \"\"\",\n            )\n\n        cls.hide_toolbar = _add_prop_bool(\"/HideToolbar\", f_obj)\n        cls.hide_menubar = _add_prop_bool(\"/HideMenubar\", f_obj)\n        cls.hide_windowui = _add_prop_bool(\"/HideWindowUI\", f_obj)\n        cls.fit_window = _add_prop_bool(\"/FitWindow\", f_obj)\n        cls.center_window = _add_prop_bool(\"/CenterWindow\", f_obj)\n        cls.display_doctitle = _add_prop_bool(\"/DisplayDocTitle\", f_obj)\n\n        cls.non_fullscreen_pagemode = _add_prop_name(\n            \"/NonFullScreenPageMode\",\n            [\"/UseNone\", \"/UseOutlines\", \"/UseThumbs\", \"/UseOC\"],\n            NameObject(\"/UseNone\"),\n        )\n        cls.direction = _add_prop_name(\n            \"/Direction\", [\"/L2R\", \"/R2L\"], NameObject(\"/L2R\")\n        )\n        cls.view_area = _add_prop_name(\"/ViewArea\", [], None)\n        cls.view_clip = _add_prop_name(\"/ViewClip\", [], None)\n        cls.print_area = _add_prop_name(\"/PrintArea\", [], None)\n        cls.print_clip = _add_prop_name(\"/PrintClip\", [], None)\n        cls.print_scaling = _add_prop_name(\"/PrintScaling\", [], None)\n        cls.duplex = _add_prop_name(\n            \"/Duplex\", [\"/Simplex\", \"/DuplexFlipShortEdge\", \"/DuplexFlipLongEdge\"], None\n        )\n        cls.pick_tray_by_pdfsize = _add_prop_bool(\"/PickTrayByPDFSize\", None)\n        cls.print_pagerange = _add_prop_arr(\"/PrintPageRange\", None)\n        cls.num_copies = _add_prop_int(\"/NumCopies\", None)\n\n        cls.enforce = _add_prop_arr(\"/Enforce\", ArrayObject())\n\n        return DictionaryObject.__new__(cls)\n"
  },
  {
    "path": "pypdf/pagerange.py",
    "content": "\"\"\"\nRepresentation and utils for ranges of PDF file pages.\n\nCopyright (c) 2014, Steve Witham <switham_github@mac-guyver.com>.\nAll rights reserved. This software is available under a BSD license;\nsee https://github.com/py-pdf/pypdf/blob/main/LICENSE\n\"\"\"\n\nimport re\nfrom typing import Any, Union\n\nfrom .errors import ParseError\n\n_INT_RE = r\"(0|-?[1-9]\\d*)\"  # A decimal int, don't allow \"-0\".\nPAGE_RANGE_RE = f\"^({_INT_RE}|({_INT_RE}?(:{_INT_RE}?(:{_INT_RE}?)?)))$\"\n# groups:         12     34     5 6     7 8\n\n\nclass PageRange:\n    \"\"\"\n    A slice-like representation of a range of page indices.\n\n    For example, page numbers, only starting at zero.\n\n    The syntax is like what you would put between brackets [ ].\n    The slice is one of the few Python types that can't be subclassed,\n    but this class converts to and from slices, and allows similar use.\n\n      -  PageRange(str) parses a string representing a page range.\n      -  PageRange(slice) directly \"imports\" a slice.\n      -  to_slice() gives the equivalent slice.\n      -  str() and repr() allow printing.\n      -  indices(n) is like slice.indices(n).\n    \"\"\"\n\n    def __init__(self, arg: Union[slice, \"PageRange\", str]) -> None:\n        \"\"\"\n        Initialize with either a slice -- giving the equivalent page range,\n        or a PageRange object -- making a copy,\n        or a string like\n            \"int\", \"[int]:[int]\" or \"[int]:[int]:[int]\",\n            where the brackets indicate optional ints.\n        Remember, page indices start with zero.\n        Page range expression examples:\n\n            :     all pages.                   -1    last page.\n            22    just the 23rd page.          :-1   all but the last page.\n            0:3   the first three pages.       -2    second-to-last page.\n            :3    the first three pages.       -2:   last two pages.\n            5:    from the sixth page onward.  -3:-1 third & second to last.\n        The third, \"stride\" or \"step\" number is also recognized.\n            ::2       0 2 4 ... to the end.    3:0:-1    3 2 1 but not 0.\n            1:10:2    1 3 5 7 9                2::-1     2 1 0.\n            ::-1      all pages in reverse order.\n        Note the difference between this notation and arguments to slice():\n            slice(3) means the first three pages;\n            PageRange(\"3\") means the range of only the fourth page.\n            However PageRange(slice(3)) means the first three pages.\n        \"\"\"\n        if isinstance(arg, slice):\n            self._slice = arg\n            return\n\n        if isinstance(arg, PageRange):\n            self._slice = arg.to_slice()\n            return\n\n        m = isinstance(arg, str) and re.match(PAGE_RANGE_RE, arg)\n        if not m:\n            raise ParseError(arg)\n        if m.group(2):\n            # Special case: just an int means a range of one page.\n            start = int(m.group(2))\n            stop = start + 1 if start != -1 else None\n            self._slice = slice(start, stop)\n        else:\n            self._slice = slice(*[int(g) if g else None for g in m.group(4, 6, 8)])\n\n    @staticmethod\n    def valid(input: Any) -> bool:\n        \"\"\"\n        True if input is a valid initializer for a PageRange.\n\n        Args:\n            input: A possible PageRange string or a PageRange object.\n\n        Returns:\n            True, if the ``input`` is a valid PageRange.\n\n        \"\"\"\n        return isinstance(input, (slice, PageRange)) or (\n            isinstance(input, str) and bool(re.match(PAGE_RANGE_RE, input))\n        )\n\n    def to_slice(self) -> slice:\n        \"\"\"Return the slice equivalent of this page range.\"\"\"\n        return self._slice\n\n    def __str__(self) -> str:\n        \"\"\"A string like \"1:2:3\".\"\"\"\n        s = self._slice\n        indices: Union[tuple[int, int], tuple[int, int, int]]\n        if s.step is None:\n            if s.start is not None and s.stop == s.start + 1:\n                return str(s.start)\n\n            indices = s.start, s.stop\n        else:\n            indices = s.start, s.stop, s.step\n        return \":\".join(\"\" if i is None else str(i) for i in indices)\n\n    def __repr__(self) -> str:\n        \"\"\"A string like \"PageRange('1:2:3')\".\"\"\"\n        return \"PageRange(\" + repr(str(self)) + \")\"\n\n    def indices(self, n: int) -> tuple[int, int, int]:\n        \"\"\"\n        Assuming a sequence of length n, calculate the start and stop indices,\n        and the stride length of the PageRange.\n\n        See help(slice.indices).\n\n        Args:\n            n:  the length of the list of pages to choose from.\n\n        Returns:\n            Arguments for range().\n\n        \"\"\"\n        return self._slice.indices(n)\n\n    def __eq__(self, other: object) -> bool:\n        if not isinstance(other, PageRange):\n            return False\n        return self._slice == other._slice\n\n    def __hash__(self) -> int:\n        return hash((self.__class__, (self._slice.start, self._slice.stop, self._slice.step)))\n\n    def __add__(self, other: \"PageRange\") -> \"PageRange\":\n        if not isinstance(other, PageRange):\n            raise TypeError(f\"Can't add PageRange and {type(other)}\")\n        if self._slice.step is not None or other._slice.step is not None:\n            raise ValueError(\"Can't add PageRange with stride\")\n        a = self._slice.start, self._slice.stop\n        b = other._slice.start, other._slice.stop\n\n        if a[0] > b[0]:\n            a, b = b, a\n\n        # Now a[0] is the smallest\n        if b[0] > a[1]:\n            # There is a gap between a and b.\n            raise ValueError(\"Can't add PageRanges with gap\")\n        return PageRange(slice(a[0], max(a[1], b[1])))\n\n\nPAGE_RANGE_ALL = PageRange(\":\")  # The range of all pages.\n\n\ndef parse_filename_page_ranges(\n    args: list[Union[str, PageRange, None]]\n) -> list[tuple[str, PageRange]]:\n    \"\"\"\n    Given a list of filenames and page ranges, return a list of (filename, page_range) pairs.\n\n    Args:\n        args: A list where the first element is a filename. The other elements are\n            filenames, page-range expressions, slice objects, or PageRange objects.\n            A filename not followed by a page range indicates all pages of the file.\n\n    Returns:\n        A list of (filename, page_range) pairs.\n\n    \"\"\"\n    pairs: list[tuple[str, PageRange]] = []\n    pdf_filename: Union[str, None] = None\n    did_page_range = False\n    for arg in [*args, None]:\n        if PageRange.valid(arg):\n            if not pdf_filename:\n                raise ValueError(\n                    \"The first argument must be a filename, not a page range.\"\n                )\n\n            assert arg is not None\n            pairs.append((pdf_filename, PageRange(arg)))\n            did_page_range = True\n        else:\n            # New filename or end of list - use the complete previous file?\n            if pdf_filename and not did_page_range:\n                pairs.append((pdf_filename, PAGE_RANGE_ALL))\n\n            assert not isinstance(arg, PageRange), arg\n            pdf_filename = arg\n            did_page_range = False\n    return pairs\n\n\nPageRangeSpec = Union[str, PageRange, tuple[int, int], tuple[int, int, int], list[int]]\n"
  },
  {
    "path": "pypdf/papersizes.py",
    "content": "\"\"\"Helper to get paper sizes.\"\"\"\n\nfrom typing import NamedTuple\n\n\nclass Dimensions(NamedTuple):\n    width: int\n    height: int\n\n\nclass PaperSize:\n    \"\"\"(width, height) of the paper in portrait mode in pixels at 72 ppi.\"\"\"\n\n    # Notes of how to calculate it:\n    # 1. Get the size of the paper in millimeters\n    # 2. Convert it to inches (25.4 millimeters is equal to 1 inch)\n    # 3. Convert it to pixels at 72dpi (1 inch is equal to 72 pixels)\n\n    # All Din-A paper sizes follow this pattern:\n    # 2 x A(n - 1) = A(n)\n    # So the height of the next bigger one is the width of the smaller one\n    # The ratio is always approximately 1:2**0.5\n    # Additionally, A0 is defined to have an area of 1 m**2\n    # https://en.wikipedia.org/wiki/ISO_216\n    # Be aware of rounding issues!\n    A0 = Dimensions(2384, 3370)  # 841mm x 1189mm\n    A1 = Dimensions(1684, 2384)\n    A2 = Dimensions(1191, 1684)\n    A3 = Dimensions(842, 1191)\n    A4 = Dimensions(\n        595, 842\n    )  # Printer paper, documents - this is by far the most common\n    A5 = Dimensions(420, 595)  # Paperback books\n    A6 = Dimensions(298, 420)  # Postcards\n    A7 = Dimensions(210, 298)\n    A8 = Dimensions(147, 210)\n\n    # Envelopes\n    C4 = Dimensions(649, 918)\n\n\n_din_a = (\n    PaperSize.A0,\n    PaperSize.A1,\n    PaperSize.A2,\n    PaperSize.A3,\n    PaperSize.A4,\n    PaperSize.A5,\n    PaperSize.A6,\n    PaperSize.A7,\n    PaperSize.A8,\n)\n"
  },
  {
    "path": "pypdf/py.typed",
    "content": ""
  },
  {
    "path": "pypdf/types.py",
    "content": "\"\"\"Helpers for working with PDF types.\"\"\"\n\nimport sys\nfrom typing import Literal, Union\n\nif sys.version_info[:2] >= (3, 10):\n    # Python 3.10+: https://www.python.org/dev/peps/pep-0484\n    from typing import TypeAlias\nelse:\n    from typing_extensions import TypeAlias\n\nfrom .generic._base import NameObject, NullObject, NumberObject\nfrom .generic._data_structures import ArrayObject, Destination\nfrom .generic._outline import OutlineItem\n\nBorderArrayType: TypeAlias = list[Union[NameObject, NumberObject, ArrayObject]]\n\nOutlineItemType: TypeAlias = Union[OutlineItem, Destination]\n\nFitType: TypeAlias = Literal[\n    \"/XYZ\", \"/Fit\", \"/FitH\", \"/FitV\", \"/FitR\", \"/FitB\", \"/FitBH\", \"/FitBV\"\n]\n# These go with the FitType, they specify values for the fit\nZoomArgType: TypeAlias = Union[NumberObject, NullObject, float]\nZoomArgsType: TypeAlias = list[ZoomArgType]\n\n# Recursive types like the following are not yet supported by Sphinx:\n#    OutlineType = List[Union[Destination, \"OutlineType\"]]\n# Hence use this for the moment:\nOutlineType = list[Union[Destination, list[Union[Destination, list[Destination]]]]]\n\nLayoutType: TypeAlias = Literal[\n    \"/NoLayout\",\n    \"/SinglePage\",\n    \"/OneColumn\",\n    \"/TwoColumnLeft\",\n    \"/TwoColumnRight\",\n    \"/TwoPageLeft\",\n    \"/TwoPageRight\",\n]\n\nPagemodeType: TypeAlias = Literal[\n    \"/UseNone\",\n    \"/UseOutlines\",\n    \"/UseThumbs\",\n    \"/FullScreen\",\n    \"/UseOC\",\n    \"/UseAttachments\",\n]\n\nAnnotationSubtype: TypeAlias = Literal[\n    \"/Text\",\n    \"/Link\",\n    \"/FreeText\",\n    \"/Line\",\n    \"/Square\",\n    \"/Circle\",\n    \"/Polygon\",\n    \"/PolyLine\",\n    \"/Highlight\",\n    \"/Underline\",\n    \"/Squiggly\",\n    \"/StrikeOut\",\n    \"/Caret\",\n    \"/Stamp\",\n    \"/Ink\",\n    \"/Popup\",\n    \"/FileAttachment\",\n    \"/Sound\",\n    \"/Movie\",\n    \"/Screen\",\n    \"/Widget\",\n    \"/PrinterMark\",\n    \"/TrapNet\",\n    \"/Watermark\",\n    \"/3D\",\n    \"/Redact\",\n    \"/Projection\",\n    \"/RichMedia\",\n]\n"
  },
  {
    "path": "pypdf/xmp.py",
    "content": "\"\"\"\nAnything related to Extensible Metadata Platform (XMP) metadata.\n\nhttps://en.wikipedia.org/wiki/Extensible_Metadata_Platform\n\"\"\"\n\nimport datetime\nimport decimal\nimport re\nfrom collections.abc import Iterator\nfrom typing import (\n    Any,\n    Callable,\n    Optional,\n    TypeVar,\n    Union,\n)\nfrom xml.dom.minidom import Document, parseString\nfrom xml.dom.minidom import Element as XmlElement\nfrom xml.parsers.expat import ExpatError\n\nfrom ._protocols import XmpInformationProtocol\nfrom ._utils import StreamType, deprecate_with_replacement, deprecation_no_replacement\nfrom .errors import PdfReadError, XmpDocumentError\nfrom .generic import ContentStream, PdfObject\n\nRDF_NAMESPACE = \"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\nDC_NAMESPACE = \"http://purl.org/dc/elements/1.1/\"\nXMP_NAMESPACE = \"http://ns.adobe.com/xap/1.0/\"\nPDF_NAMESPACE = \"http://ns.adobe.com/pdf/1.3/\"\nXMPMM_NAMESPACE = \"http://ns.adobe.com/xap/1.0/mm/\"\n\n# What is the PDFX namespace, you might ask?\n# It's documented here: https://github.com/adobe/xmp-docs/raw/master/XMPSpecifications/XMPSpecificationPart3.pdf\n# This namespace is used to place \"custom metadata\"\n# properties, which are arbitrary metadata properties with no semantic or\n# documented meaning.\n#\n# Elements in the namespace are key/value-style storage,\n# where the element name is the key and the content is the value. The keys\n# are transformed into valid XML identifiers by substituting an invalid\n# identifier character with \\u2182 followed by the unicode hex ID of the\n# original character. A key like \"my car\" is therefore \"my\\u21820020car\".\n#\n# \\u2182 is the unicode character \\u{ROMAN NUMERAL TEN THOUSAND}\n#\n# The pdfx namespace should be avoided.\n# A custom data schema and sensical XML elements could be used instead, as is\n# suggested by Adobe's own documentation on XMP under \"Extensibility of\n# Schemas\".\nPDFX_NAMESPACE = \"http://ns.adobe.com/pdfx/1.3/\"\n\n# PDF/A\nPDFAID_NAMESPACE = \"http://www.aiim.org/pdfa/ns/id/\"\n\n# Internal mapping of namespace URI → prefix\n_NAMESPACE_PREFIX_MAP = {\n    DC_NAMESPACE: \"dc\",\n    XMP_NAMESPACE: \"xmp\",\n    PDF_NAMESPACE: \"pdf\",\n    XMPMM_NAMESPACE: \"xmpMM\",\n    PDFAID_NAMESPACE: \"pdfaid\",\n    PDFX_NAMESPACE: \"pdfx\",\n}\n\niso8601 = re.compile(\n    \"\"\"\n        (?P<year>[0-9]{4})\n        (-\n            (?P<month>[0-9]{2})\n            (-\n                (?P<day>[0-9]+)\n                (T\n                    (?P<hour>[0-9]{2}):\n                    (?P<minute>[0-9]{2})\n                    (:(?P<second>[0-9]{2}(.[0-9]+)?))?\n                    (?P<tzd>Z|[-+][0-9]{2}:[0-9]{2})\n                )?\n            )?\n        )?\n        \"\"\",\n    re.VERBOSE,\n)\n\n\nK = TypeVar(\"K\")\n\n# Minimal XMP template\n_MINIMAL_XMP = f\"\"\"<?xpacket begin=\"\\ufeff\" id=\"W5M0MpCehiHzreSzNTczkc9d\"?>\n<x:xmpmeta xmlns:x=\"adobe:ns:meta/\" x:xmptk=\"pypdf\">\n    <rdf:RDF xmlns:rdf=\"{RDF_NAMESPACE}\">\n        <rdf:Description rdf:about=\"\"\n            xmlns:dc=\"{DC_NAMESPACE}\"\n            xmlns:xmp=\"{XMP_NAMESPACE}\"\n            xmlns:pdf=\"{PDF_NAMESPACE}\"\n            xmlns:xmpMM=\"{XMPMM_NAMESPACE}\"\n            xmlns:pdfaid=\"{PDFAID_NAMESPACE}\"\n            xmlns:pdfx=\"{PDFX_NAMESPACE}\">\n        </rdf:Description>\n    </rdf:RDF>\n</x:xmpmeta>\n<?xpacket end=\"r\"?>\"\"\"\n\n\ndef _identity(value: K) -> K:\n    return value\n\n\ndef _converter_date(value: str) -> datetime.datetime:\n    matches = iso8601.match(value)\n    if matches is None:\n        raise ValueError(f\"Invalid date format: {value}\")\n    year = int(matches.group(\"year\"))\n    month = int(matches.group(\"month\") or \"1\")\n    day = int(matches.group(\"day\") or \"1\")\n    hour = int(matches.group(\"hour\") or \"0\")\n    minute = int(matches.group(\"minute\") or \"0\")\n    second = decimal.Decimal(matches.group(\"second\") or \"0\")\n    seconds_dec = second.to_integral(decimal.ROUND_FLOOR)\n    milliseconds_dec = (second - seconds_dec) * 1_000_000\n\n    seconds = int(seconds_dec)\n    milliseconds = int(milliseconds_dec)\n\n    tzd = matches.group(\"tzd\") or \"Z\"\n    dt = datetime.datetime(year, month, day, hour, minute, seconds, milliseconds)\n    if tzd != \"Z\":\n        tzd_hours, tzd_minutes = (int(x) for x in tzd.split(\":\"))\n        tzd_hours *= -1\n        if tzd_hours < 0:\n            tzd_minutes *= -1\n        dt = dt + datetime.timedelta(hours=tzd_hours, minutes=tzd_minutes)\n    return dt\n\n\ndef _format_datetime_utc(value: datetime.datetime) -> str:\n    \"\"\"Format a datetime as UTC with trailing 'Z'.\n\n    - If the input is timezone-aware, convert to UTC first.\n    - If naive, assume UTC.\n    \"\"\"\n    if value.tzinfo is not None and value.utcoffset() is not None:\n        value = value.astimezone(datetime.timezone.utc)\n\n    value = value.replace(tzinfo=None)\n    return value.strftime(\"%Y-%m-%dT%H:%M:%S.%fZ\")\n\n\ndef _generic_get(\n        element: XmlElement, self: \"XmpInformation\", list_type: str, converter: Callable[[Any], Any] = _identity\n) -> Optional[list[str]]:\n    containers = element.getElementsByTagNameNS(RDF_NAMESPACE, list_type)\n    retval: list[Any] = []\n    if len(containers):\n        for container in containers:\n            for item in container.getElementsByTagNameNS(RDF_NAMESPACE, \"li\"):\n                value = self._get_text(item)\n                value = converter(value)\n                retval.append(value)\n        return retval\n    return None\n\n\nclass XmpInformation(XmpInformationProtocol, PdfObject):\n    \"\"\"\n    An object that represents Extensible Metadata Platform (XMP) metadata.\n    Usually accessed by :py:attr:`xmp_metadata()<pypdf.PdfReader.xmp_metadata>`.\n\n    Raises:\n      PdfReadError: if XML is invalid\n\n    \"\"\"\n\n    def __init__(self, stream: ContentStream) -> None:\n        self.stream = stream\n        try:\n            data = self.stream.get_data()\n            doc_root: Document = parseString(data)  # noqa: S318\n        except (AttributeError, ExpatError) as e:\n            raise PdfReadError(f\"XML in XmpInformation was invalid: {e}\")\n        self.rdf_root: XmlElement = doc_root.getElementsByTagNameNS(\n            RDF_NAMESPACE, \"RDF\"\n        )[0]\n        self.cache: dict[Any, Any] = {}\n\n    @classmethod\n    def create(cls) -> \"XmpInformation\":\n        \"\"\"\n        Create a new XmpInformation object with minimal structure.\n\n        Returns:\n            A new XmpInformation instance with empty metadata fields.\n        \"\"\"\n        stream = ContentStream(None, None)\n        stream.set_data(_MINIMAL_XMP.encode(\"utf-8\"))\n        return cls(stream)\n\n    def write_to_stream(\n        self, stream: StreamType, encryption_key: Union[None, str, bytes] = None\n    ) -> None:\n        deprecate_with_replacement(\n            \"XmpInformation.write_to_stream\",\n            \"PdfWriter.xmp_metadata\",\n            \"6.0.0\"\n        )\n        if encryption_key is not None:  # deprecated\n            deprecation_no_replacement(\n                \"the encryption_key parameter of write_to_stream\", \"5.0.0\"\n            )\n        self.stream.write_to_stream(stream)\n\n    def get_element(self, about_uri: str, namespace: str, name: str) -> Iterator[Any]:\n        for desc in self.rdf_root.getElementsByTagNameNS(RDF_NAMESPACE, \"Description\"):\n            if desc.getAttributeNS(RDF_NAMESPACE, \"about\") == about_uri:\n                attr = desc.getAttributeNodeNS(namespace, name)\n                if attr is not None:\n                    yield attr\n                yield from desc.getElementsByTagNameNS(namespace, name)\n\n    def get_nodes_in_namespace(self, about_uri: str, namespace: str) -> Iterator[Any]:\n        for desc in self.rdf_root.getElementsByTagNameNS(RDF_NAMESPACE, \"Description\"):\n            if desc.getAttributeNS(RDF_NAMESPACE, \"about\") == about_uri:\n                for i in range(desc.attributes.length):\n                    attr = desc.attributes.item(i)\n                    if attr and attr.namespaceURI == namespace:\n                        yield attr\n                for child in desc.childNodes:\n                    if child.namespaceURI == namespace:\n                        yield child\n\n    def _get_text(self, element: XmlElement) -> str:\n        text = \"\"\n        for child in element.childNodes:\n            if child.nodeType == child.TEXT_NODE:\n                text += child.data\n        return text\n\n    def _get_single_value(\n        self,\n        namespace: str,\n        name: str,\n        converter: Callable[[str], Any] = _identity,\n    ) -> Optional[Any]:\n        cached = self.cache.get(namespace, {}).get(name)\n        if cached:\n            return cached\n        value = None\n        for element in self.get_element(\"\", namespace, name):\n            if element.nodeType == element.ATTRIBUTE_NODE:\n                value = element.nodeValue\n            else:\n                value = self._get_text(element)\n            break\n        if value is not None:\n            value = converter(value)\n        ns_cache = self.cache.setdefault(namespace, {})\n        ns_cache[name] = value\n        return value\n\n    def _getter_bag(self, namespace: str, name: str) -> Optional[list[str]]:\n        cached = self.cache.get(namespace, {}).get(name)\n        if cached:\n            return cached\n        retval: list[str] = []\n        for element in self.get_element(\"\", namespace, name):\n            if (bags := _generic_get(element, self, list_type=\"Bag\")) is not None:\n                retval.extend(bags)\n            else:\n                value = self._get_text(element)\n                retval.append(value)\n        ns_cache = self.cache.setdefault(namespace, {})\n        ns_cache[name] = retval\n        return retval\n\n    def _get_seq_values(\n        self,\n        namespace: str,\n        name: str,\n        converter: Callable[[Any], Any] = _identity,\n    ) -> Optional[list[Any]]:\n        cached = self.cache.get(namespace, {}).get(name)\n        if cached:\n            return cached\n        retval: list[Any] = []\n        for element in self.get_element(\"\", namespace, name):\n            if (seqs := _generic_get(element, self, list_type=\"Seq\", converter=converter)) is not None:\n                retval.extend(seqs)\n            elif (bags := _generic_get(element, self, list_type=\"Bag\")) is not None:\n                # See issue at https://github.com/py-pdf/pypdf/issues/3324\n                # Some applications violate the XMP metadata standard regarding `dc:creator` which should\n                # be an \"ordered array\" and thus a sequence, but use an unordered array (bag) instead.\n                # This seems to stem from the fact that the original Dublin Core specification does indeed\n                # use bags or direct values, while PDFs are expected to follow the XMP standard and ignore\n                # the plain Dublin Core variant. For this reason, add a fallback here to deal with such\n                # issues accordingly.\n                retval.extend(bags)\n            else:\n                value = converter(self._get_text(element))\n                retval.append(value)\n        ns_cache = self.cache.setdefault(namespace, {})\n        ns_cache[name] = retval\n        return retval\n\n    def _get_langalt_values(self, namespace: str, name: str) -> Optional[dict[Any, Any]]:\n        cached = self.cache.get(namespace, {}).get(name)\n        if cached:\n            return cached\n        retval: dict[Any, Any] = {}\n        for element in self.get_element(\"\", namespace, name):\n            alts = element.getElementsByTagNameNS(RDF_NAMESPACE, \"Alt\")\n            if len(alts):\n                for alt in alts:\n                    for item in alt.getElementsByTagNameNS(RDF_NAMESPACE, \"li\"):\n                        value = self._get_text(item)\n                        retval[item.getAttribute(\"xml:lang\")] = value\n            else:\n                retval[\"x-default\"] = self._get_text(element)\n        ns_cache = self.cache.setdefault(namespace, {})\n        ns_cache[name] = retval\n        return retval\n\n    @property\n    def dc_contributor(self) -> Optional[list[str]]:\n        \"\"\"Contributors to the resource (other than the authors).\"\"\"\n        return self._getter_bag(DC_NAMESPACE, \"contributor\")\n\n    @dc_contributor.setter\n    def dc_contributor(self, values: Optional[list[str]]) -> None:\n        self._set_bag_values(DC_NAMESPACE, \"contributor\", values)\n\n    @property\n    def dc_coverage(self) -> Optional[str]:\n        \"\"\"Text describing the extent or scope of the resource.\"\"\"\n        return self._get_single_value(DC_NAMESPACE, \"coverage\")\n\n    @dc_coverage.setter\n    def dc_coverage(self, value: Optional[str]) -> None:\n        self._set_single_value(DC_NAMESPACE, \"coverage\", value)\n\n    @property\n    def dc_creator(self) -> Optional[list[str]]:\n        \"\"\"A sorted array of names of the authors of the resource, listed in order of precedence.\"\"\"\n        return self._get_seq_values(DC_NAMESPACE, \"creator\")\n\n    @dc_creator.setter\n    def dc_creator(self, values: Optional[list[str]]) -> None:\n        self._set_seq_values(DC_NAMESPACE, \"creator\", values)\n\n    @property\n    def dc_date(self) -> Optional[list[datetime.datetime]]:\n        \"\"\"A sorted array of dates of significance to the resource. The dates and times are in UTC.\"\"\"\n        return self._get_seq_values(DC_NAMESPACE, \"date\", _converter_date)\n\n    @dc_date.setter\n    def dc_date(self, values: Optional[list[Union[str, datetime.datetime]]]) -> None:\n        if values is None:\n            self._set_seq_values(DC_NAMESPACE, \"date\", None)\n        else:\n            date_strings = []\n            for value in values:\n                if isinstance(value, datetime.datetime):\n                    date_strings.append(_format_datetime_utc(value))\n                else:\n                    date_strings.append(str(value))\n            self._set_seq_values(DC_NAMESPACE, \"date\", date_strings)\n\n    @property\n    def dc_description(self) -> Optional[dict[str, str]]:\n        \"\"\"A language-keyed dictionary of textual descriptions of the content of the resource.\"\"\"\n        return self._get_langalt_values(DC_NAMESPACE, \"description\")\n\n    @dc_description.setter\n    def dc_description(self, values: Optional[dict[str, str]]) -> None:\n        self._set_langalt_values(DC_NAMESPACE, \"description\", values)\n\n    @property\n    def dc_format(self) -> Optional[str]:\n        \"\"\"The mime-type of the resource.\"\"\"\n        return self._get_single_value(DC_NAMESPACE, \"format\")\n\n    @dc_format.setter\n    def dc_format(self, value: Optional[str]) -> None:\n        self._set_single_value(DC_NAMESPACE, \"format\", value)\n\n    @property\n    def dc_identifier(self) -> Optional[str]:\n        \"\"\"Unique identifier of the resource.\"\"\"\n        return self._get_single_value(DC_NAMESPACE, \"identifier\")\n\n    @dc_identifier.setter\n    def dc_identifier(self, value: Optional[str]) -> None:\n        self._set_single_value(DC_NAMESPACE, \"identifier\", value)\n\n    @property\n    def dc_language(self) -> Optional[list[str]]:\n        \"\"\"An unordered array specifying the languages used in the resource.\"\"\"\n        return self._getter_bag(DC_NAMESPACE, \"language\")\n\n    @dc_language.setter\n    def dc_language(self, values: Optional[list[str]]) -> None:\n        self._set_bag_values(DC_NAMESPACE, \"language\", values)\n\n    @property\n    def dc_publisher(self) -> Optional[list[str]]:\n        \"\"\"An unordered array of publisher names.\"\"\"\n        return self._getter_bag(DC_NAMESPACE, \"publisher\")\n\n    @dc_publisher.setter\n    def dc_publisher(self, values: Optional[list[str]]) -> None:\n        self._set_bag_values(DC_NAMESPACE, \"publisher\", values)\n\n    @property\n    def dc_relation(self) -> Optional[list[str]]:\n        \"\"\"An unordered array of text descriptions of relationships to other documents.\"\"\"\n        return self._getter_bag(DC_NAMESPACE, \"relation\")\n\n    @dc_relation.setter\n    def dc_relation(self, values: Optional[list[str]]) -> None:\n        self._set_bag_values(DC_NAMESPACE, \"relation\", values)\n\n    @property\n    def dc_rights(self) -> Optional[dict[str, str]]:\n        \"\"\"A language-keyed dictionary of textual descriptions of the rights the user has to this resource.\"\"\"\n        return self._get_langalt_values(DC_NAMESPACE, \"rights\")\n\n    @dc_rights.setter\n    def dc_rights(self, values: Optional[dict[str, str]]) -> None:\n        self._set_langalt_values(DC_NAMESPACE, \"rights\", values)\n\n    @property\n    def dc_source(self) -> Optional[str]:\n        \"\"\"Unique identifier of the work from which this resource was derived.\"\"\"\n        return self._get_single_value(DC_NAMESPACE, \"source\")\n\n    @dc_source.setter\n    def dc_source(self, value: Optional[str]) -> None:\n        self._set_single_value(DC_NAMESPACE, \"source\", value)\n\n    @property\n    def dc_subject(self) -> Optional[list[str]]:\n        \"\"\"An unordered array of descriptive phrases or keywords that specify the topic of the content.\"\"\"\n        return self._getter_bag(DC_NAMESPACE, \"subject\")\n\n    @dc_subject.setter\n    def dc_subject(self, values: Optional[list[str]]) -> None:\n        self._set_bag_values(DC_NAMESPACE, \"subject\", values)\n\n    @property\n    def dc_title(self) -> Optional[dict[str, str]]:\n        \"\"\"A language-keyed dictionary of the title of the resource.\"\"\"\n        return self._get_langalt_values(DC_NAMESPACE, \"title\")\n\n    @dc_title.setter\n    def dc_title(self, values: Optional[dict[str, str]]) -> None:\n        self._set_langalt_values(DC_NAMESPACE, \"title\", values)\n\n    @property\n    def dc_type(self) -> Optional[list[str]]:\n        \"\"\"An unordered array of textual descriptions of the document type.\"\"\"\n        return self._getter_bag(DC_NAMESPACE, \"type\")\n\n    @dc_type.setter\n    def dc_type(self, values: Optional[list[str]]) -> None:\n        self._set_bag_values(DC_NAMESPACE, \"type\", values)\n\n    @property\n    def pdf_keywords(self) -> Optional[str]:\n        \"\"\"An unformatted text string representing document keywords.\"\"\"\n        return self._get_single_value(PDF_NAMESPACE, \"Keywords\")\n\n    @pdf_keywords.setter\n    def pdf_keywords(self, value: Optional[str]) -> None:\n        self._set_single_value(PDF_NAMESPACE, \"Keywords\", value)\n\n    @property\n    def pdf_pdfversion(self) -> Optional[str]:\n        \"\"\"The PDF file version, for example 1.0 or 1.3.\"\"\"\n        return self._get_single_value(PDF_NAMESPACE, \"PDFVersion\")\n\n    @pdf_pdfversion.setter\n    def pdf_pdfversion(self, value: Optional[str]) -> None:\n        self._set_single_value(PDF_NAMESPACE, \"PDFVersion\", value)\n\n    @property\n    def pdf_producer(self) -> Optional[str]:\n        \"\"\"The name of the tool that saved the document as a PDF.\"\"\"\n        return self._get_single_value(PDF_NAMESPACE, \"Producer\")\n\n    @pdf_producer.setter\n    def pdf_producer(self, value: Optional[str]) -> None:\n        self._set_single_value(PDF_NAMESPACE, \"Producer\", value)\n\n    @property\n    def xmp_create_date(self) -> Optional[datetime.datetime]:\n        \"\"\"The date and time the resource was originally created. Returned as a UTC datetime object.\"\"\"\n        return self._get_single_value(XMP_NAMESPACE, \"CreateDate\", _converter_date)\n\n    @xmp_create_date.setter\n    def xmp_create_date(self, value: Optional[datetime.datetime]) -> None:\n        if value:\n            date_str = _format_datetime_utc(value)\n            self._set_single_value(XMP_NAMESPACE, \"CreateDate\", date_str)\n        else:\n            self._set_single_value(XMP_NAMESPACE, \"CreateDate\", None)\n\n    @property\n    def xmp_modify_date(self) -> Optional[datetime.datetime]:\n        \"\"\"The date and time the resource was last modified. Returned as a UTC datetime object.\"\"\"\n        return self._get_single_value(XMP_NAMESPACE, \"ModifyDate\", _converter_date)\n\n    @xmp_modify_date.setter\n    def xmp_modify_date(self, value: Optional[datetime.datetime]) -> None:\n        if value:\n            date_str = _format_datetime_utc(value)\n            self._set_single_value(XMP_NAMESPACE, \"ModifyDate\", date_str)\n        else:\n            self._set_single_value(XMP_NAMESPACE, \"ModifyDate\", None)\n\n    @property\n    def xmp_metadata_date(self) -> Optional[datetime.datetime]:\n        \"\"\"The date and time that any metadata for this resource was last changed. Returned as a UTC datetime object.\"\"\"\n        return self._get_single_value(XMP_NAMESPACE, \"MetadataDate\", _converter_date)\n\n    @xmp_metadata_date.setter\n    def xmp_metadata_date(self, value: Optional[datetime.datetime]) -> None:\n        if value:\n            date_str = _format_datetime_utc(value)\n            self._set_single_value(XMP_NAMESPACE, \"MetadataDate\", date_str)\n        else:\n            self._set_single_value(XMP_NAMESPACE, \"MetadataDate\", None)\n\n    @property\n    def xmp_creator_tool(self) -> Optional[str]:\n        \"\"\"The name of the first known tool used to create the resource.\"\"\"\n        return self._get_single_value(XMP_NAMESPACE, \"CreatorTool\")\n\n    @xmp_creator_tool.setter\n    def xmp_creator_tool(self, value: Optional[str]) -> None:\n        self._set_single_value(XMP_NAMESPACE, \"CreatorTool\", value)\n\n    @property\n    def xmpmm_document_id(self) -> Optional[str]:\n        \"\"\"The common identifier for all versions and renditions of this resource.\"\"\"\n        return self._get_single_value(XMPMM_NAMESPACE, \"DocumentID\")\n\n    @xmpmm_document_id.setter\n    def xmpmm_document_id(self, value: Optional[str]) -> None:\n        self._set_single_value(XMPMM_NAMESPACE, \"DocumentID\", value)\n\n    @property\n    def xmpmm_instance_id(self) -> Optional[str]:\n        \"\"\"An identifier for a specific incarnation of a document, updated each time a file is saved.\"\"\"\n        return self._get_single_value(XMPMM_NAMESPACE, \"InstanceID\")\n\n    @xmpmm_instance_id.setter\n    def xmpmm_instance_id(self, value: Optional[str]) -> None:\n        self._set_single_value(XMPMM_NAMESPACE, \"InstanceID\", value)\n\n    @property\n    def pdfaid_part(self) -> Optional[str]:\n        \"\"\"The part of the PDF/A standard that the document conforms to (e.g., 1, 2, 3).\"\"\"\n        return self._get_single_value(PDFAID_NAMESPACE, \"part\")\n\n    @pdfaid_part.setter\n    def pdfaid_part(self, value: Optional[str]) -> None:\n        self._set_single_value(PDFAID_NAMESPACE, \"part\", value)\n\n    @property\n    def pdfaid_conformance(self) -> Optional[str]:\n        \"\"\"The conformance level within the PDF/A standard (e.g., 'A', 'B', 'U').\"\"\"\n        return self._get_single_value(PDFAID_NAMESPACE, \"conformance\")\n\n    @pdfaid_conformance.setter\n    def pdfaid_conformance(self, value: Optional[str]) -> None:\n        self._set_single_value(PDFAID_NAMESPACE, \"conformance\", value)\n\n    @property\n    def custom_properties(self) -> dict[Any, Any]:\n        \"\"\"\n        Retrieve custom metadata properties defined in the undocumented pdfx\n        metadata schema.\n\n        Returns:\n            A dictionary of key/value items for custom metadata properties.\n\n        \"\"\"\n        if not hasattr(self, \"_custom_properties\"):\n            self._custom_properties = {}\n            for node in self.get_nodes_in_namespace(\"\", PDFX_NAMESPACE):\n                key = node.localName\n                while True:\n                    # see documentation about PDFX_NAMESPACE earlier in file\n                    idx = key.find(\"\\u2182\")\n                    if idx == -1:\n                        break\n                    key = (\n                        key[:idx]\n                        + chr(int(key[idx + 1 : idx + 5], base=16))\n                        + key[idx + 5 :]\n                    )\n                if node.nodeType == node.ATTRIBUTE_NODE:\n                    value = node.nodeValue\n                else:\n                    value = self._get_text(node)\n                self._custom_properties[key] = value\n        return self._custom_properties\n\n    def _get_or_create_description(self, about_uri: str = \"\") -> XmlElement:\n        \"\"\"Get or create an rdf:Description element with the given about URI.\"\"\"\n        for desc in self.rdf_root.getElementsByTagNameNS(RDF_NAMESPACE, \"Description\"):\n            if desc.getAttributeNS(RDF_NAMESPACE, \"about\") == about_uri:\n                return desc\n\n        doc = self.rdf_root.ownerDocument\n        if doc is None:\n            raise XmpDocumentError(\"XMP Document is None\")\n        desc = doc.createElementNS(RDF_NAMESPACE, \"rdf:Description\")\n        desc.setAttributeNS(RDF_NAMESPACE, \"rdf:about\", about_uri)\n        self.rdf_root.appendChild(desc)\n        return desc\n\n    def _clear_cache_entry(self, namespace: str, name: str) -> None:\n        \"\"\"Remove a cached value for a given namespace/name if present.\"\"\"\n        ns_cache = self.cache.get(namespace)\n        if ns_cache and name in ns_cache:\n            del ns_cache[name]\n\n    def _set_single_value(self, namespace: str, name: str, value: Optional[str]) -> None:\n        \"\"\"Set or remove a single metadata value.\"\"\"\n        self._clear_cache_entry(namespace, name)\n        desc = self._get_or_create_description()\n\n        existing_elements = list(desc.getElementsByTagNameNS(namespace, name))\n        for elem in existing_elements:\n            desc.removeChild(elem)\n\n        if existing_attr := desc.getAttributeNodeNS(namespace, name):\n            desc.removeAttributeNode(existing_attr)\n\n        if value is not None:\n            doc = self.rdf_root.ownerDocument\n            if doc is None:\n                raise XmpDocumentError(\"XMP Document is None\")\n            prefix = self._get_namespace_prefix(namespace)\n            elem = doc.createElementNS(namespace, f\"{prefix}:{name}\")\n            text_node = doc.createTextNode(str(value))\n            elem.appendChild(text_node)\n            desc.appendChild(elem)\n\n        self._update_stream()\n\n    def _set_bag_values(self, namespace: str, name: str, values: Optional[list[str]]) -> None:\n        \"\"\"Set or remove bag values (unordered array).\"\"\"\n        self._clear_cache_entry(namespace, name)\n        desc = self._get_or_create_description()\n\n        existing_elements = list(desc.getElementsByTagNameNS(namespace, name))\n        for elem in existing_elements:\n            desc.removeChild(elem)\n\n        if values:\n            doc = self.rdf_root.ownerDocument\n            if doc is None:\n                raise XmpDocumentError(\"XMP Document is None\")\n            prefix = self._get_namespace_prefix(namespace)\n            elem = doc.createElementNS(namespace, f\"{prefix}:{name}\")\n            bag = doc.createElementNS(RDF_NAMESPACE, \"rdf:Bag\")\n\n            for value in values:\n                li = doc.createElementNS(RDF_NAMESPACE, \"rdf:li\")\n                text_node = doc.createTextNode(str(value))\n                li.appendChild(text_node)\n                bag.appendChild(li)\n\n            elem.appendChild(bag)\n            desc.appendChild(elem)\n\n        self._update_stream()\n\n    def _set_seq_values(self, namespace: str, name: str, values: Optional[list[str]]) -> None:\n        \"\"\"Set or remove sequence values (ordered array).\"\"\"\n        self._clear_cache_entry(namespace, name)\n        desc = self._get_or_create_description()\n\n        existing_elements = list(desc.getElementsByTagNameNS(namespace, name))\n        for elem in existing_elements:\n            desc.removeChild(elem)\n\n        if values:\n            doc = self.rdf_root.ownerDocument\n            if doc is None:\n                raise XmpDocumentError(\"XMP Document is None\")\n            prefix = self._get_namespace_prefix(namespace)\n            elem = doc.createElementNS(namespace, f\"{prefix}:{name}\")\n            seq = doc.createElementNS(RDF_NAMESPACE, \"rdf:Seq\")\n\n            for value in values:\n                li = doc.createElementNS(RDF_NAMESPACE, \"rdf:li\")\n                text_node = doc.createTextNode(str(value))\n                li.appendChild(text_node)\n                seq.appendChild(li)\n\n            elem.appendChild(seq)\n            desc.appendChild(elem)\n\n        self._update_stream()\n\n    def _set_langalt_values(self, namespace: str, name: str, values: Optional[dict[str, str]]) -> None:\n        \"\"\"Set or remove language alternative values.\"\"\"\n        self._clear_cache_entry(namespace, name)\n        desc = self._get_or_create_description()\n\n        existing_elements = list(desc.getElementsByTagNameNS(namespace, name))\n        for elem in existing_elements:\n            desc.removeChild(elem)\n\n        if values:\n            doc = self.rdf_root.ownerDocument\n            if doc is None:\n                raise XmpDocumentError(\"XMP Document is None\")\n            prefix = self._get_namespace_prefix(namespace)\n            elem = doc.createElementNS(namespace, f\"{prefix}:{name}\")\n            alt = doc.createElementNS(RDF_NAMESPACE, \"rdf:Alt\")\n\n            for lang, value in values.items():\n                li = doc.createElementNS(RDF_NAMESPACE, \"rdf:li\")\n                li.setAttribute(\"xml:lang\", lang)\n                text_node = doc.createTextNode(str(value))\n                li.appendChild(text_node)\n                alt.appendChild(li)\n\n            elem.appendChild(alt)\n            desc.appendChild(elem)\n\n        self._update_stream()\n\n    def _get_namespace_prefix(self, namespace: str) -> str:\n        \"\"\"Get the appropriate namespace prefix for a given namespace URI.\"\"\"\n        return _NAMESPACE_PREFIX_MAP.get(namespace, \"unknown\")\n\n    def _update_stream(self) -> None:\n        \"\"\"Update the stream with the current XML content.\"\"\"\n        doc = self.rdf_root.ownerDocument\n        if doc is None:\n            raise XmpDocumentError(\"XMP Document is None\")\n\n        xml_data = doc.toxml(encoding=\"utf-8\")\n        self.stream.set_data(xml_data)\n"
  },
  {
    "path": "pyproject.toml",
    "content": "[build-system]\nrequires = [\"flit_core >=3.11,<4\"]\nbuild-backend = \"flit_core.buildapi\"\n\n[project]\nname = \"pypdf\"\nauthors = [{ name = \"Mathieu Fenniak\", email = \"biziqe@mathieu.fenniak.net\" }]\nmaintainers = [{ name = \"stefan6419846\" }, { name = \"Martin Thoma\", email = \"info@martin-thoma.de\" }]\ndescription = \"A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files\"\nreadme = \"README.md\"\ndynamic = [\"version\"]\nlicense = \"BSD-3-Clause\"\nlicense-files = [\"LICENSE\"]\nrequires-python = \">=3.9\"\nclassifiers = [\n    \"Development Status :: 5 - Production/Stable\",\n    \"Intended Audience :: Developers\",\n    \"Programming Language :: Python :: 3\",\n    \"Programming Language :: Python :: 3 :: Only\",\n    \"Programming Language :: Python :: 3.9\",\n    \"Programming Language :: Python :: 3.10\",\n    \"Programming Language :: Python :: 3.11\",\n    \"Programming Language :: Python :: 3.12\",\n    \"Programming Language :: Python :: 3.13\",\n    \"Programming Language :: Python :: 3.14\",\n    \"Operating System :: OS Independent\",\n    \"Topic :: Software Development :: Libraries :: Python Modules\",\n    \"Typing :: Typed\",\n]\n\ndependencies = [\n    \"typing_extensions >= 4.0; python_version < '3.11'\",\n]\n\n[project.urls]\nChangelog = \"https://pypdf.readthedocs.io/en/latest/meta/CHANGELOG.html\"\nDocumentation = \"https://pypdf.readthedocs.io/en/latest/\"\nSource = \"https://github.com/py-pdf/pypdf\"\n\"Bug Reports\" = \"https://github.com/py-pdf/pypdf/issues\"\n\n[project.optional-dependencies]\ncrypto = [\"cryptography\"]\ncryptodome = [\"PyCryptodome\"]\nimage = [\"Pillow>=8.0.0\"]\nfull = [\n    \"cryptography\",\n    \"Pillow>=8.0.0\"\n]\ndev = [\n    \"flit\",\n    \"pip-tools\",\n    \"pre-commit\",\n    \"pytest-cov\",\n    \"pytest-socket\",\n    \"pytest-timeout\",\n    \"pytest-xdist\",\n    \"wheel\"\n]\ndocs = [\n    \"myst_parser\",\n    \"sphinx\",\n    \"sphinx_rtd_theme\"\n]\n\n[tool.check-wheel-contents]\npackage = \"./pypdf\"\n\n[tool.flit.sdist]\nexclude = [\n    \".gitblame-ignore-revs\",\n    \".github/*\",\n    \".gitignore\",\n    \".gitmodules\",\n    \".pre-commit-config.yaml\",\n    \"docs/*\",\n    \"make_release.py\",\n    \"Makefile\",\n    \"requirements/*\",\n    \"sample-files/.github/*\",\n    \"sample-files/.gitignore\",\n    \"sample-files/.pre-commit-config.yaml\",\n    \"tests/pdf_cache/*\",\n]\ninclude = [\"resources/\", \"tests/\", \"CHANGELOG.md\"]\n\n[tool.pytest.ini_options]\naddopts = \"--disable-socket\"\nfilterwarnings = [\"error\"]\nmarkers = [\n    \"slow: Test which require more than a second\",\n    \"samples: Tests which use files from https://github.com/py-pdf/sample-files\",\n    \"enable_socket: Tests which need to download files\"\n]\ntestpaths = [\"tests\"]\nnorecursedirs = [\"tests/pdf_cache\"]\n\n[tool.isort]\nline_length = 79\nindent = '    '\nmulti_line_output = 3\ninclude_trailing_comma = true\nknown_third_party = [\"pytest\"]\n\n[tool.coverage.run]\nsource = [\"pypdf\"]\nbranch = true\npatch = [\n    \"subprocess\",\n]\nparallel = true\n\n[tool.coverage.report]\n# Regexes for lines to exclude from consideration\nexclude_lines = [\n    # Have to re-enable the standard pragma\n    \"pragma: no cover\",\n    \"@overload\",\n    \"deprecated\",\n\n    # Don't complain about type-checking code not being hit by unit tests\n    \"if TYPE_CHECKING\",\n\n    # Don't complain about missing debug-only code:\n    \"def __repr__\",\n    \"def __str__\",\n    \"if self\\\\.debug\",\n\n    # Don't complain if tests don't hit defensive assertion code:\n    \"raise AssertionError\",\n    \"raise NotImplementedError\",\n\n    # Don't complain if non-runnable code isn't run:\n    \"if __name__ == .__main__.:\",\n]\n\n[tool.ruff]\nline-length = 120\nexclude = [\n    \"sample-files/\",\n]\n\n[tool.ruff.lint]\nselect = [\"ALL\"]\nignore = [\n    \"A001\",    # Variable is shadowing a Python builtin\n    \"A002\",    # Function argument is shadowing a Python builtin\n    \"ANN401\",  # Dynamically typed expressions (typing.Any) are disallowed\n    \"ARG001\",  # Unused function argument\n    \"ARG002\",  # Unused method argument\n    \"ARG004\",  # Unused static method argument\n    \"B904\",    # Within an `except` clause, raise exceptions with\n    \"B905\",    # `zip()` without an explicit `strict=` parameter\n    \"BLE001\",  # Do not catch blind exception: `Exception`\n    \"COM812\",  # Yes, they make the diff smaller\n    \"D101\",    # Missing docstring in public class\n    \"D102\",    # Missing docstring in public method\n    \"D105\",    # Missing docstring in magic method\n    \"D106\",    # Missing docstring in public nested class\n    \"D107\",    # Missing docstring in `__init__`\n    \"D205\",    # One blank line required between summary line and description\n    \"D212\",    # I want multiline-docstrings to start at the second line\n    \"D401\",    # First line of docstring should be in imperative mood - false positives\n    \"D415\",    # First line should end with a period\n    \"D417\",    # Missing argument descriptions in the docstring\n    \"DTZ001\",  # The use of `datetime.datetime()` without `tzinfo` is necessary\n    \"EM101\",   # Exception must not use a string literal, assign to variable first\n    \"EM102\",   # Exception must not use an f-string literal, assign to variable first\n    \"ERA001\",  # Found commented-out code\n    \"FA100\",   # Missing `from __future__ import annotations`, but uses `typing.Dict`\n    \"FA102\",   # Missing `from __future__ import annotations`, but uses PEP 604 union\n    \"FBT001\",  # Boolean positional arg in function definition\n    \"FBT002\",  # Boolean default value in function definition\n    \"FBT003\",  # Boolean positional value in function call\n    \"FIX002\",  # TODOs should typically not be in the code, but sometimes are ok\n    \"G004\",    # f-string in logging statement\n    \"N806\",    # non-lowercase-variable-in-function\n    \"N814\",    # Camelcase `PageAttributes` imported as constant `PG`\n    \"N817\",    # CamelCase `PagesAttributes` imported as acronym `PA`\n    \"PERF203\", # `try`-`except` within a loop incurs performance overhead\n    \"PGH003\",  # Use specific rule codes when ignoring type issues\n    \"PLW1510\", # `subprocess.run` without explicit `check` argument\n    \"PLW2901\", # `with` statement variable `img` overwritten by assignment target\n    \"PT011\",   # `pytest.raises(ValueError)` is too broad, set the `match`\n    \"PT012\",   # `pytest.raises()` block should contain a single simple statement\n    \"PT014\",   # Ruff bug: Duplicate of test case at index 1 in `@pytest_mark.parametrize`\n    \"PTH123\",  # `open()` should be replaced by `Path.open()`\n    \"PYI042\",  # Type alias `mode_str_type` should be CamelCase\n    \"RUF001\",  # Detect confusable Unicode-to-Unicode units. Introduces bugs\n    \"RUF002\",  # Detect confusable Unicode-to-Unicode units. Introduces bugs\n    \"S101\",    # Use of `assert` detected\n    \"S110\",    # `try`-`except`-`pass` detected, consider logging the exception\n    \"SIM105\",  # contextlib.suppress\n    \"SIM108\",  # Don't enforce ternary operators\n    \"SLF001\",  # Private member accessed\n    \"TC006\",   # To discuss: Add quotes to type expression in `typing.cast()`\n    \"TD002\",   # Authors of TODOs can be found via git\n    \"TD003\",   # For the moment, fix it later: Missing issue link on the line following this TODO\n    \"TID252\",  # We want relative imports\n    \"TRY002\",  # Create your own exception\n    \"TRY003\",  # Avoid specifying long messages outside the exception class\n    \"TRY004\",  # Prefer `TypeError` exception for invalid type\n    \"TRY201\",  # Use `raise` without specifying exception name\n    \"TRY300\",  # Consider moving this statement to an `else` block\n    \"TRY301\",  # Abstract `raise` to an inner function\n    \"UP006\",   # Non-PEP 585 annotation. As long as we are not on Python 3.11+\n    \"UP007\",   # Non-PEP 604 annotation. As long as we are not on Python 3.11+\n]\n\n[tool.ruff.lint.mccabe]\nmax-complexity = 30  # Recommended: 10\n\n[tool.ruff.lint.per-file-ignores]\n\"_cryptography.py\" = [\"S304\", \"S305\"]  # Use of insecure cipher / modes, aka RC4 and AES-ECB\n\"_encryption.py\" = [\"S324\"]\n\"_writer.py\" = [\"S324\"]\n\"pypdf/_codecs/symbol.py\" = [\"A005\"]  # Module shadows a Python standard-library module\n\"types.py\" = [\"A005\"]  # Module shadows a Python standard-library module\n\"pypdf/_text_extraction/__init__.py\" = [\"PLW0603\"]  # Using the global statement to update is discouraged\n\"docs/conf.py\" = [\"INP001\", \"PTH100\"]\n\"json_consistency.py\" = [\"T201\"]\n\"make_release.py\" = [\"S603\", \"S607\", \"T201\"]\n\"pypdf/*\" = [\"N802\", \"N803\"]  # We first need to deprecate old stuff\n\"tests/*\" = [\"ANN001\", \"ANN201\", \"B017\", \"B018\", \"D103\", \"D104\", \"S105\", \"S106\"]\n\"tests/test_workflows.py\" =  [\"T201\"]\n\n[tool.ruff.lint.pydocstyle]\nconvention = \"google\"\n\n[tool.ruff.lint.pylint]\nallow-magic-value-types = [\"bytes\", \"float\", \"int\", \"str\"]\nmax-args = 12  # Recommended: 5\nmax-branches = 36  # Recommended: 12\nmax-returns = 11  # Recommended: 6\nmax-statements = 176  # Recommended: 50\n\n[tool.docformatter]\npre-summary-newline = true\nwrap-summaries = 0\nwrap-descriptions = 0\n\n[tool.mypy]\nshow_error_codes = true\nignore_missing_imports = true\ncheck_untyped_defs = true\ndisallow_any_generics = true\ndisallow_untyped_defs = true\ndisallow_incomplete_defs = true\nwarn_redundant_casts = true\nwarn_unused_ignores = true\nwarn_unused_configs = true\nexclude = ['venv', '.venv', 'tests', 'make_release.py']\n"
  },
  {
    "path": "requirements/ci-3.11.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.11\n# by the following command:\n#\n#    pip-compile --output-file=requirements/ci-3.11.txt requirements/ci.in\n#\ncffi==2.0.0\n    # via cryptography\ncoverage[toml]==7.13.0\n    # via\n    #   -r requirements/ci.in\n    #   pytest-cov\ncryptography==46.0.5\n    # via -r requirements/ci.in\ndefusedxml==0.7.1\n    # via fpdf2\nexceptiongroup==1.2.2\n    # via pytest\nexecnet==2.1.1\n    # via pytest-xdist\nfonttools==4.61.0\n    # via fpdf2\nfpdf2==2.8.1\n    # via -r requirements/ci.in\niniconfig==2.0.0\n    # via pytest\nmypy==1.17.0\n    # via -r requirements/ci.in\nmypy-extensions==1.0.0\n    # via mypy\npackaging==24.1\n    # via pytest\npillow==12.1.1\n    # via\n    #   -r requirements/ci.in\n    #   fpdf2\npluggy==1.5.0\n    # via pytest\npy-cpuinfo==9.0.0\n    # via pytest-benchmark\npycparser==2.22\n    # via cffi\npytest==8.3.3\n    # via\n    #   -r requirements/ci.in\n    #   pytest-benchmark\n    #   pytest-cov\n    #   pytest-socket\n    #   pytest-timeout\n    #   pytest-xdist\npytest-benchmark==4.0.0\n    # via -r requirements/ci.in\npytest-cov==5.0.0\n    # via -r requirements/ci.in\npytest-socket==0.7.0\n    # via -r requirements/ci.in\npytest-timeout==2.3.1\n    # via -r requirements/ci.in\npytest-xdist==3.6.1\n    # via -r requirements/ci.in\npyyaml==6.0.2\n    # via -r requirements/ci.in\nruff==0.15.0\n    # via -r requirements/ci.in\ntomli==2.0.2\n    # via\n    #   coverage\n    #   mypy\n    #   pytest\ntypeguard==4.3.0\n    # via -r requirements/ci.in\ntyping-extensions==4.12.2\n    # via\n    #   mypy\n    #   typeguard\n"
  },
  {
    "path": "requirements/ci.in",
    "content": "coverage\nfpdf2\nmypy\npillow\ncryptography\npytest\npytest-benchmark\npytest-socket\npytest-timeout\npytest-xdist\npytest-cov\n# ruff  # only take this for 3.11\ntypeguard\npyyaml\n"
  },
  {
    "path": "requirements/ci.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.9\n# by the following command:\n#\n#    pip-compile requirements/ci.in\n#\ncffi==2.0.0\n    # via cryptography\ncoverage[toml]==7.10.7\n    # via\n    #   -r requirements/ci.in\n    #   pytest-cov\ncryptography==46.0.5\n    # via -r requirements/ci.in\nexceptiongroup==1.2.2\n    # via pytest\nexecnet==2.1.1\n    # via pytest-xdist\nimportlib-metadata==8.5.0\n    # via typeguard\niniconfig==2.0.0\n    # via pytest\nmypy==1.13.0\n    # via -r requirements/ci.in\nmypy-extensions==1.0.0\n    # via mypy\npackaging==24.1\n    # via pytest\npillow==10.4.0\n    # via\n    #   -r requirements/ci.in\n    #   fpdf2\npluggy==1.5.0\n    # via pytest\npy-cpuinfo==9.0.0\n    # via pytest-benchmark\npycparser==2.22\n    # via cffi\npytest==8.3.3\n    # via\n    #   -r requirements/ci.in\n    #   pytest-benchmark\n    #   pytest-cov\n    #   pytest-socket\n    #   pytest-timeout\n    #   pytest-xdist\npytest-benchmark==4.0.0\n    # via -r requirements/ci.in\npytest-cov==5.0.0\n    # via -r requirements/ci.in\npytest-socket==0.7.0\n    # via -r requirements/ci.in\npytest-timeout==2.3.1\n    # via -r requirements/ci.in\npytest-xdist==3.6.1\n    # via -r requirements/ci.in\npyyaml==6.0.2\n    # via -r requirements/ci.in\ntomli==2.0.2\n    # via\n    #   coverage\n    #   mypy\n    #   pytest\ntypeguard==4.3.0\n    # via -r requirements/ci.in\ntyping-extensions==4.13.2\n    # via\n    #   mypy\n    #   typeguard\nzipp==3.20.2\n    # via importlib-metadata\n"
  },
  {
    "path": "requirements/dev.in",
    "content": "pillow\npip-tools\npre-commit\npytest-cov\nflit\nwheel\n"
  },
  {
    "path": "requirements/dev.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.9\n# by the following command:\n#\n#    pip-compile requirements/dev.in\n#\nbuild==1.2.2.post1\n    # via pip-tools\ncertifi==2024.8.30\n    # via requests\ncfgv==3.4.0\n    # via pre-commit\ncharset-normalizer==3.4.0\n    # via requests\nclick==8.1.7\n    # via pip-tools\ncoverage[toml]==7.6.1\n    # via pytest-cov\ndistlib==0.3.9\n    # via virtualenv\ndocutils==0.20.1\n    # via flit\nexceptiongroup==1.2.2\n    # via pytest\nfilelock==3.20.3\n    # via virtualenv\nflit==3.11.0\n    # via -r dev.in\nflit-core==3.11.0\n    # via flit\nidentify==2.6.1\n    # via pre-commit\nidna==3.10\n    # via requests\nimportlib-metadata==8.5.0\n    # via build\niniconfig==2.0.0\n    # via pytest\nnodeenv==1.9.1\n    # via pre-commit\npackaging==24.1\n    # via\n    #   build\n    #   pytest\n    #   wheel\npillow==12.1.1\n    # via -r dev.in\npip-tools==7.4.1\n    # via -r dev.in\nplatformdirs==4.3.6\n    # via virtualenv\npluggy==1.5.0\n    # via pytest\npre-commit==3.5.0\n    # via -r dev.in\npyproject-hooks==1.2.0\n    # via\n    #   build\n    #   pip-tools\npytest==8.3.3\n    # via pytest-cov\npytest-cov==5.0.0\n    # via -r dev.in\npyyaml==6.0.2\n    # via pre-commit\nrequests==2.32.4\n    # via flit\ntomli==2.0.2\n    # via\n    #   build\n    #   coverage\n    #   pip-tools\n    #   pytest\ntomli-w==1.0.0\n    # via flit\ntyping-extensions==4.15.0\n    # via virtualenv\nurllib3==2.6.3\n    # via requests\nvirtualenv==20.36.1\n    # via pre-commit\nwheel==0.46.2\n    # via\n    #   -r dev.in\n    #   pip-tools\nzipp==3.20.2\n    # via importlib-metadata\n\n# The following packages are considered to be unsafe in a requirements file:\n# pip\n# setuptools\n"
  },
  {
    "path": "requirements/docs.in",
    "content": "sphinx\nsphinx_rtd_theme\nmyst_parser\n"
  },
  {
    "path": "requirements/docs.txt",
    "content": "#\n# This file is autogenerated by pip-compile with Python 3.10\n# by the following command:\n#\n#    pip-compile requirements/docs.in\n#\nalabaster==1.0.0\n    # via sphinx\nbabel==2.16.0\n    # via sphinx\ncertifi==2024.8.30\n    # via requests\ncharset-normalizer==3.4.0\n    # via requests\ndocutils==0.21.2\n    # via\n    #   myst-parser\n    #   sphinx\n    #   sphinx-rtd-theme\nidna==3.10\n    # via requests\nimagesize==1.4.1\n    # via sphinx\njinja2==3.1.6\n    # via\n    #   myst-parser\n    #   sphinx\nmarkdown-it-py==3.0.0\n    # via\n    #   mdit-py-plugins\n    #   myst-parser\nmarkupsafe==3.0.1\n    # via jinja2\nmdit-py-plugins==0.4.2\n    # via myst-parser\nmdurl==0.1.2\n    # via markdown-it-py\nmyst-parser==4.0.0\n    # via -r requirements/docs.in\npackaging==24.1\n    # via sphinx\npygments==2.18.0\n    # via sphinx\npyyaml==6.0.2\n    # via myst-parser\nrequests==2.32.4\n    # via sphinx\nsnowballstemmer==2.2.0\n    # via sphinx\nsphinx==8.1.3\n    # via\n    #   -r requirements/docs.in\n    #   myst-parser\n    #   sphinx-rtd-theme\n    #   sphinxcontrib-jquery\nsphinx-rtd-theme==3.0.1\n    # via -r requirements/docs.in\nsphinxcontrib-applehelp==2.0.0\n    # via sphinx\nsphinxcontrib-devhelp==2.0.0\n    # via sphinx\nsphinxcontrib-htmlhelp==2.1.0\n    # via sphinx\nsphinxcontrib-jquery==4.1\n    # via sphinx-rtd-theme\nsphinxcontrib-jsmath==1.0.1\n    # via sphinx\nsphinxcontrib-qthelp==2.0.0\n    # via sphinx\nsphinxcontrib-serializinghtml==2.0.0\n    # via sphinx\ntomli==2.0.2\n    # via sphinx\nurllib3==2.6.3\n    # via requests\n"
  },
  {
    "path": "resources/010-pdflatex-forms.txt",
    "content": "Name\n\nCheck\n\nSubmit\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n                                 1"
  },
  {
    "path": "resources/AEO.1172.layout.rot180.txt",
    "content": "9  1of    Page                                                                                                                                                                   2022 AEO Management Co. All Rights Reserved. Proprietary and Confidential AEO Business Information. Subject to Legal Action if Disclosed Without Authorization from AEO.Date Printed: 17/Nov/2022\n                                                                                                                                         PRODUCT SUMMARY\n                                                                                                                                                                                                            Fit / Other:\n                                                                                                                                                                                1172 KNIT SHORTIE           Style Desc:\n                                                                                                                                                                                SUMMER-B 2023               Season:\n                                                                                                                                                                                50 / 170                    Division / Dept:\n                                                                                                                                                                                AMERICAN EAGLE OUTFITTERSCompany:\n                             SUMMER-B 2023                                                                                               1172 KNIT SHORTIE                                                                                      STYLE: 1172\n                                                                                                                                    STATUS: FNL\n"
  },
  {
    "path": "resources/AEO.1172.layout.txt",
    "content": "                                                                                                            STATUS: FNL\nSTYLE: 1172                                                                                            1172 KNIT SHORTIE                                                                                          SUMMER-B 2023\n                                    Company:                    AMERICAN EAGLE OUTFITTERS\n                                    Division / Dept:            50 / 170\n                                    Season:                     SUMMER-B 2023\n                                    Style Desc:                 1172 KNIT SHORTIE\n                                    Fit / Other:\n                                                                                                       PRODUCT SUMMARY\nDate Printed: 17/Nov/2022                                     2022 AEO Management Co. All Rights Reserved. Proprietary and Confidential AEO Business Information. Subject to Legal Action if Disclosed Without Authorization from AEO.Page 1of 9\n"
  },
  {
    "path": "resources/Claim Maker Alerts Guide_pg2.layout.txt",
    "content": " Updated System Responses for Common Scenarios\n\n\n  Scenario                                 Before Change             After                           Why?\n\n  An On Hold / Missing                     New doc info was          Leave state as On               Batches can be released early\n  Documents case receives its              logged but no             Hold and update state           and coders can code all they can\n  first documentation set after            further automated         reason to Ready To              and then leave the batch in In\n  coding operations have                   action was taken.         Code.                           Progress. When docs come in,\n  already begun for the batch                                                                        the case is picked up by the\n  (batch state = In Progress).                                                                       normal On Hold process due to\n                                                                                                     the assignment of the Ready to\n                                                                                                     Code state reason.\n\n  An “incomplete” case (not                All documents             All manually attached           Ensures that ALL info that has\n  Code Completed or Ignored)               were “overwritten”        PDFs are preserved              arrived for the case remains\n  in an “in flight” batch (state =         with data from the        in place and all                visible to users. Specifically\n  Reconciled, Assigned, or In              new documents.            “extracted”                     addresses split labor / C-section\n  Progress) receives new                                             documents are                   cases, allowing a coder to refer\n  documents.                                                         aggregated under a              back to the “Superseded”\n                                                                     SUPERSEDED ON                   documents to make sure a newly\n                                                                     [DATE] text doc with            extracted “C-section only”\n                                                                     type Complete                   document wasn’t really a Labor\n                                                                     Record.                         to C-section case.\n\n  New documents are received               New doc info was          Existing documents              Prompts the coder to review the\n  for a Code Completed or                  logged but no             are “superseded”                new documentation set while\n  Ignored case in an “in flight”           further automated         (see previous) and              retaining all previously applied\n  batch.                                   action was taken.         the case is set back            codes.  If no significant change is\n                                                                     to On Hold / Ready to           noted, the case can simply be set\n                                                                     Code.                           back to Code Completed.\n\n  Documentation for an                     New case info             The case is added to            Ensures proper review of any\n  “uncoded” (aka not Code                  was logged but            a new batch with the            additional documentation\n  Completed) case or a new                 no further                same date of service.           received for a previously\n  patient is received for a                automated action          Set state to Ignored            completed batch as well as\n  Complete or Charges Entered              was taken.                on the original case (if        documentation for brand new\n  batch.                                                             it exists) and add              cases after a batch has already\n                                                                     notes to both the               been Completed. Notes on the\n                                                                     original and new                original and duplicate case\n                                                                     cases indicating the            ensure that users are aware of\n                                                                     link between the two.           actions taken by the system.\n\n  Documentation for a Code                 New doc info was          Existing case                   The status of the new document\n  Completed case in a                      logged but no             documents are left in           is clearly indicated as arriving\n  Complete or Charges Entered              further automated         place and the new               AFTER the associated case was\n  batch is received.                       action was taken.         documentation is                coded avoiding potential\n                                                                     added as a PDF                  confusion regarding which\n                                                                     attachment with type            documentation was utilized at the\n                                                                     “complete record” and           time of coding while also\n                                                                     title POSTED LATE -             providing access to the new info\n                                                                     [DATE].                         and allowing the end user to\n                                                                                                     determine the correct course of\n                                                                                                     action."
  },
  {
    "path": "resources/Epic.Page.layout.txt",
    "content": "All Postprocedure Notes\n   Last edited 10/11/23 0919 by Danny Chaung, DO\n   Date of Service 10/11/23 0918\n   Status: Signed\nAnesthesia Post Evaluation\n\nProcedure Summary\n\n   Date: 10/11/23                                                Room / Location: EHMC ENDOSCOPY\n   Anesthesia Start: 0852                                        Anesthesia Stop: 0918\n   Procedure: COLONOSCOPY                                        Diagnosis: Cancer screening\n   Scheduled Providers: Walter A Klein, MD; Danny Chaung,        Responsible Provider: Danny Chaung, DO\n   DO\n   Anesthesia Type: general                                      ASA Status: 2\n\n\nPatient location during evaluation: PACU\nPost op Vital Signs: stable\n\nLevel of consciousness: awake and alert\nPain management: adequate analgesia\nAirway patency: patent\nAnesthetic complications: no\nRespiratory status: unassisted\nHydration status: continuing\nPost-op Complications: No\n\n\n\nAssessment: Nausea and Vomiting: absent\n\n\n\n\nMIPS Measure #404 - Smoking Abstinence\nIs the patient a current smoker? No (XX404)\n\n\n\n"
  },
  {
    "path": "resources/afm_to_dataclass.py",
    "content": "# ruff: noqa: T201, INP001, D100\n# Use this file to generate Font dataclasses for the 14 Adobe Core fonts.\nimport re\nimport textwrap\nimport urllib.request\nfrom io import BytesIO\nfrom typing import cast\nfrom zipfile import ZipFile\n\nfrom pypdf._codecs.adobe_glyphs import adobe_glyphs\nfrom pypdf.constants import FontFlags\n\n# FONT_LOC = \"web.archive.org/web/20110531171921if_/http://www.adobe.com/content/dam/Adobe/en/devnet/font/pdfs/Core14_AFMs.zip\"\nFONT_LOC = \"download.macromedia.com/pub/developer/opentype/tech-notes/Core14_AFMs.zip\"\nPROTOCOL = \"https://\"\nFONT_URL = PROTOCOL + FONT_LOC\n\nclass Parser:\n    def __init__(self) -> None:\n        self.license_information = \"\"\n        self.files: dict[str, str] = {}\n\n    def get_fonts(self) -> None:\n        with urllib.request.urlopen(\n            f\"https://{FONT_LOC}\"\n        ) as connection, ZipFile(BytesIO(\n            connection.read())\n        ) as font_zip:\n            for filename in font_zip.namelist():\n                if filename.lower().endswith(\".afm\"):\n                    with font_zip.open(filename, mode=\"r\") as afm_font_file:\n                        self.files[filename] = afm_font_file.read().decode(\"utf-8\")\n                else:\n                    with font_zip.open(filename, mode=\"r\") as afm_font_file:\n                        self.license_information = afm_font_file.read().decode(\"utf-8\")\n\n    def get_disclaimer(self, width: int = 95) -> str:\n        pre = (\n            \"# This file is based upon the 14 core AFM files provided by Adobe/Macromedia at\\n# \" +\n            FONT_URL +\n            \"\\n# The original copyright follows:\\n#\\n# \" +\n            \"-\" * width +\n            \"\\n\"\n        )\n        title = \"# \" + self.license_information.split(\"<title>\")[1].split(\"</title>\")[0]\n        text = self.license_information.split('<td width=\"300\">')[1].split(\"<font color\")[0]\n        post = \"\\n# \" + \"-\" * width + \"\\n\\n\"\n        return pre + title + \"\\n#\\n# \" + \"\\n# \".join(textwrap.wrap(text=text, width=width)) + post\n\n    def _handle_font(self, file_name: str, font_data: str) -> list[str]:  # noqa: C901\n        # AFM specification: https://adobe-type-tools.github.io/font-tech-notes/pdfs/5004.AFM_Spec.pdf\n        copyrights: list[str] = []\n        name: str = \"\"\n        family: str = \"\"\n        weight: str = \"\"\n        ascent: float = 0.0\n        descent: float = 0.0\n        cap_height: float = 0.0\n        x_height: float = 0.0\n        italic_angle: float = 0.0\n        flags: int = 0\n        bbox: tuple[float, float, float, float] = (0, 0, 0, 0)\n        character_widths: dict[str, int] = {}\n\n        for line in font_data.splitlines(keepends=False):\n            if not line.strip():\n                continue\n\n            if \" \" not in line:\n                continue\n            key, value = line.split(\" \", maxsplit=1)\n            if not key:\n                continue\n\n            if key == \"FontName\":\n                name = value\n                if \"Times\" in value:\n                    flags |= FontFlags.SERIF\n            if key == \"Weight\":\n                weight = value\n            if key == \"FamilyName\":\n                family = value\n\n            if key == \"Ascender\":\n                ascent = cast(float, value)\n            if key == \"Descender\":\n                descent = cast(float, value)\n            if key == \"CapHeight\":\n                cap_height = cast(float, value)\n            if key == \"XHeight\":\n                x_height = cast(float, value)\n            if key == \"ItalicAngle\":\n                italic_angle = cast(float, value)\n                if value != \"0\":\n                    flags |= FontFlags.ITALIC\n            if key == \"IsFixedPitch\" and value.lower() == \"true\":\n                flags |= FontFlags.FIXED_PITCH\n            if key == \"FontBBox\":\n                bbox = tuple(map(float, value.split(\" \")[:4]))  # type: ignore\n            if key == \"EncodingScheme\":\n                if value == \"FontSpecific\":\n                    flags |= FontFlags.SYMBOLIC\n                else:\n                    flags |= FontFlags.NONSYMBOLIC\n\n            # Add copyright information. This is available in two fields: \"Comment\" and \"Notice\".\n            # However, all information available in \"Comment\" is also available in \"Notice\", and\n            # the information under \"Notice\" is more complete. Ignore \"Comment\" and only copy\n            # information from \"Notice\", to avoid adding the same information twice.\n            if key == \"Notice\" and value.startswith(\"Copyright\"):\n                copyrights.append(re.sub(r\"\\.([A-Z])\", r\".  \\1\", value))  # Take care of missing space after period.\n\n            if key == \"C\":\n                # C integer ; WX number ; N name; We're ignoring C.\n                key_value_pairs = line.split(\";\")\n                character_width_x = -1\n                character_name = \"dummy\"\n                for pair in key_value_pairs:\n                    if not pair.strip():\n                        continue\n                    key_of_pair, value_of_pair = pair.strip().split(\" \", maxsplit=1)\n                    if key_of_pair == \"WX\":\n                        character_width_x = int(value_of_pair)\n                    if key_of_pair == \"N\":\n                        character_name = value_of_pair\n                glyph = adobe_glyphs[f\"/{character_name}\"]\n                character_widths[glyph.encode(\"unicode_escape\").decode(\"utf-8\")] = character_width_x\n            if key == \"CH\":\n                raise NotImplementedError(name, line)\n            # Add default width\n            try:\n                if (flags & FontFlags.FIXED_PITCH) == FontFlags.FIXED_PITCH:\n                    character_widths[\"default\"] = character_widths[\" \"]\n                else:\n                    character_widths[\"default\"] = 2 * character_widths[\" \"]\n            except KeyError:\n                pass\n\n        result = [\n            f\"    # Generated from {file_name}\"\n        ]\n        for copyright_entry in sorted(set(copyrights)):\n            result.extend(f\"    # {line}\" for line in textwrap.wrap(text=copyright_entry, width=95))\n        result.append(f'    \"{name}\": CoreFontMetrics(')\n        result.append(\"        font_descriptor=FontDescriptor(\")\n        result.append(f'            name=\"{name}\",')\n        result.append(f'            family=\"{family}\",')\n        result.append(f'            weight=\"{weight}\",')\n        result.append(f\"            ascent={ascent},\")\n        result.append(f\"            descent={descent},\")\n        result.append(f\"            cap_height={cap_height},\")\n        result.append(f\"            x_height={x_height},\")\n        result.append(f\"            italic_angle={italic_angle},\")\n        result.append(f\"            flags={flags},\")\n        result.append(f\"            bbox=({', '.join(map(str, bbox))}),\")\n        result.append(\"        ),\")\n        result.append(\"        character_widths={\")\n        for character, width in character_widths.items():\n            d = '\"'\n            try:\n                if ord(character) == 34:  # Double quotation mark\n                    d = \"'\"\n            except TypeError:\n                pass\n            result.append(f\"            {d}{character}{d}: {width},\")\n        result.append(\"        },\")\n        result.append(\"    ),\")\n        return result\n\n    def get_font_data(self) -> str:\n        data = [\n            \"from pypdf._font import CoreFontMetrics, FontDescriptor\\n\\n\"\n            \"CORE_FONT_METRICS: dict[str, CoreFontMetrics] = {\",\n        ]\n        for name, font_data in self.files.items():\n            data.extend(self._handle_font(name, font_data))\n        data.append(\"}\\n\")\n        return \"\\n\".join(data)\n\n\nparser = Parser()\nparser.get_fonts()\n\nprint(parser.get_disclaimer())\nprint(parser.get_font_data())\n"
  },
  {
    "path": "resources/crazyones.txt",
    "content": "The Crazy Ones\nOctober 14, 1998\nHeres to the crazy ones. The misﬁts. The rebels. The troublemakers.\nThe round pegs in the square holes.\nThe ones who see things diﬀerently. Theyre not fond of rules. And\nthey have no respect for the status quo. You can quote them,\ndisagree with them, glorify or vilify them.\nAbout the only thing you cant do is ignore them. Because they change\nthings. They invent. They imagine. They heal. They explore. They\ncreate. They inspire. They push the human race forward.\nMaybe they have to be crazy.\nHow else can you stare at an empty canvas and see a work of art? Or\nsit in silence and hear a song thats never been written? Or gaze at\na red planet and see a laboratory on wheels?\nWe make tools for these kinds of people.\nWhile some see them as the crazy ones, we see genius. Because the\npeople who are crazy enough to think they can change the world,\nare the ones who do."
  },
  {
    "path": "resources/crazyones_layout_vertical_space.txt",
    "content": "The Crazy Ones\nOctober 14, 1998\n\n   Heres to the crazy ones. The misﬁts. The rebels. The troublemakers.\n       The round pegs in the square holes.\n   The ones who see things diﬀerently. Theyre not fond of rules. And\n       they have no respect for the status quo. You can quote them,\n       disagree with them, glorify or vilify them.\n   About the only thing you cant do is ignore them. Because they change\n       things. They invent. They imagine. They heal. They explore. They\n       create. They inspire. They push the human race forward.\n   Maybe they have to be crazy.\n   How else can you stare at an empty canvas and see a work of art? Or\n       sit in silence and hear a song thats never been written? Or gaze at\n       a red planet and see a laboratory on wheels?\n   We make tools for these kinds of people.\n   While some see them as the crazy ones, we see genius. Because the\n       people who are crazy enough to think they can change the world,\n       are the ones who do."
  },
  {
    "path": "resources/crazyones_layout_vertical_space_font_height_weight.txt",
    "content": "The Crazy Ones\nOctober 14, 1998\n\n   Heres to the crazy ones. The misﬁts. The rebels. The troublemakers.\n       The round pegs in the square holes.\n\n   The ones who see things diﬀerently. Theyre not fond of rules. And\n       they have no respect for the status quo. You can quote them,\n       disagree with them, glorify or vilify them.\n\n   About the only thing you cant do is ignore them. Because they change\n       things. They invent. They imagine. They heal. They explore. They\n       create. They inspire. They push the human race forward.\n\n   Maybe they have to be crazy.\n\n   How else can you stare at an empty canvas and see a work of art? Or\n       sit in silence and hear a song thats never been written? Or gaze at\n       a red planet and see a laboratory on wheels?\n\n   We make tools for these kinds of people.\n\n   While some see them as the crazy ones, we see genius. Because the\n       people who are crazy enough to think they can change the world,\n       are the ones who do."
  },
  {
    "path": "resources/jpeg.txt",
    "content": "ffd8ffe000104a46494600010100000100010000ffdb0043000302020302020303030304030304050805050404050a070706080c0a0c0c0b0a0b0b0d0e12100d0e110e0b0b1016101113141515150c0f171816141812141514ffdb00430103040405040509050509140d0b0d1414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414141414ffc20011080258032003012200021101031101ffc4001d000100010501010100000000000000000000050203040607010809ffc4001c0101000203010101000000000000000000000304010205060708ffda000c03010002100310000001f950000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000cab1ae2a6365f550e977ba07be8a1d12eefb9672fc7e978d1e79ad3d121b8926a8918ef236020c800000000000000000007b7f5d71d9f9fa4702cfc2df7a46db800000000000000000000000000000000002fef8b12d3131efaae14eef3d87af1fceb81db78f48c5f7cc6f4f064db8ff63cdea3cf60cf96f3b1e1cc3416d719c99355a64b03e736e81c990000000000000000af6e86bead3bb7e673793aec84fe651e6c24d64e4d7abe5491a3cdd3795fd33edab9f27bb472bf4bebe2c5de88000000000000000000000000000000d8afe91fb35d7d528decfc2f2675acce296a0cecdab62474b897cad5f2a0cecd7b5fccecc720b17a1ce458bf6a7c58f2f79be21a1b708be349a8f92f11f2ebc1ccdc0000000000013135bff2f8d19277ee70fcddbab325659e0aacbc1860cac98ec9df79cdbb4bedd2cdcc63ba7f27a1cdaade1c9435f89689f5670aef7a6d207a0f50000000000000000000000000000b956e1de8ac491f54a2f3df6c63cf2f55066cd19d174b68c8cccc3e5ef6fcabde4c9e5da6425c5ebd876ba5a4f65c24cfa18ae48741eb9c097e54c7ebbc9bb91476b3b9e0f377d295d1f21e806000000000cbc6b6fa4484cf9ff314dff2aa5cef7da33f5d3a06e393b1fa1f57c3746e95cc39dc9b1662e2ea50df379e419b1c5f44f32d52c4d676891d467b95c4ca8c9eb55aa70bd27e92e11ec3de420ebf740000000000000000000000017acedbd68f2336fd3f57a365ef9775ab271aaad9b96ed4b53da3b23bd6d1e6a6f9161bade87576b193294f4b4c08fcf839b164317368d4e6ae63ec3ea5c87b07ceadf2ef95fe91f9c7d9d78db52b67d8d7d320b7bd23e6376d8f2938000000a8bfd63036cf3fe5e8aaba79fcab757a8e25cb5e1d5a7f86e374fb3b073eb51d1c35506db65e4d32ba690fe64531c3eeeda6f528a1bb87b46a7479d662a564659be5db3d67937aef7416ae8000000000000000000000197bf68d39eeaaec78b8befad82ea8f7a1a54f3130af7ae57ef979feb9e53ccad71a4af371f63edc76b5494d7f47b47be67018cf976d3674ae8df3ecc7563d9e323b2ed624ac5cc78d15acecd85536d29553f21e80600000369d5bb4f3f97396b3e57cf794d66ade3579ed46e3fb83a47974c561997116ed4f67d79eef2012fb2e97b0e359684dde220adadf5ce45b061f4e729da22a7b3cf33706df278539f307d15cfaff004f960f59ee00000000000000000000000d96fcdc9fd739faee4ccd57b11989b659ab9d371b68898f346545c6e1278f8793d2d26a5a0aef1a489c7f69800cbcf7c05c2995c05bc4fdcc7bde8e0c9b567c8f2b77a8dda945ecdacfc8ef870650000243b8f27ebde6fc94bf60e49dbb48f1f8c76ff9ff00b7e8b49c3c2b9c5f3d3dbdec1d5fa7d8f99745fa1386691c47be5da5428f7a0c9cf63956565e0435fb359d8b65af57e6cbf2bad69a741cdd6a66ad1aad64d31c58fe5ff34d3e7bc1ea9cafd9fd002dde000000000000000000000eb92dad6c7f53a3730ba4e6d2db915dced9bb11e931bd679ed6da063f7394e4efc7bcd9a07bb1d6a6c51da8f0c6078cb268da77c45f6687fa83cfcdc6f81fdf7f2af3f7e3927ab4a7d1e9e7fb1be753491f7065e5c60e8bd3b9c7ce6de38f0b680000dc7abf1eeabe6bc849ecda4e250e6743e5f2917358d6e468ae7b1f4574ef94773ebf7a4b834fe9f5eafbba693d3e283bfe06efabf67d0fcf5a2eeba8f1f81f42efbccb69e7f2b48e4bdaf9e56abafed5a7ec38d6664e224638b6dd5fb046f4bafc5be7cfa5392edb73a1e9bd80000000000000000000006dfb972be9ff0047a7b46ebc9e7e9ed3f85660ad6bd6357c291f3f345f41e41b3e16b9cf66e6f2e35988db627d2c3abaaa707819bb6e95b0eee97f55fc37d578527d43f1eed5f3ed5da1b2f173bd3439b8bb1c7fa386d4b62e576e3b5a0740e79f3cb7647cd6e0000199daf8475fe3f0767b176be1f9ca31a4bcd34d5312720ed5c88a6aa2d5db7e57e6fbf931135631df6238ff009358cacc8798af53a4ecbcff0067a5cfb5096e361af6bdc5b3b6db75d8592ad5376dcb8f665bbd91a4ec7621aff38afd8f6ff460ce40000000000000000000742e7b2bd98fa45ca6bfa8d1b8f2b9176f635c851f76ac1e1c9b3c3625ee748c191a2deba2e2cc43cf81e63345cb5e54ce5dbb55d8c7b4d77a7c62ca64dbea4797663323a5a4de458bdd4d3179ded3ab7c7fa02ef94b169b149d7ec694deae457b4175dcd87a5c576ede71e3a73d5dabbc2f97557ac55af9fb11bb86f77fa5f3c4776be4b73a11a20ac07a78c5d908992db7d92a838c2e46dacd924f26eec954a5e6558ab48f33222bcaf5253c84c2d34e5560f6bf430ce4000000000000000000003a34f72ceabf51a2afcbdde8a8c8b556b990819c8fe249177ed5ca5b5dae9bbae60751e85a34d8c3f3df375bf2e4c57cc35eef51f1b92cc6557eb20f632431fa1a43e565e5d0de8c096caf3fe879a4eee177e3bef62b67c2eb7cff00458bb6cf697d1f19571f9484a5e9727222a5ea7a185b925996391834ec5090f46d7985469ccecbd67e2ceffdaf95e5f00ed7c1f1e4e1c50e61ee56566e4c24975bb12319bc95f95cde91e3cedebd4b9cabca99bd4556b18a54d51c546a7b8f25bdd1851e8bd580000000000000000000000e87cf32ba91f61928597faa5258db65b97bf39a65ec74f487f36782e349897eddee7ef569bb8c7498e7abf66d63cef7c57e97833dba137cd73c1d9f9135cdab5efac57c5af3673cbfb48495c97cd7e83e53579c2edc75fbdb1cb435ca7e879dbbe6be4a77de0315ff0021acebcc4b6f7cba567e5fdb7b7fc67876b85f4a70fd57cabdebd5e05fa5e9756c4db75c9f97b5476b9b1c7f2188f73b0f3e4ecdec3b534fb061c564e76a6e65ed1a471b3db566c9368f66522a973bdf482b79e55eb1e548dce62f98e4e37a5f5e166e0000000000000000000000006d5d2f85746f7b57a76e3cd6627c4fc3c9c0e71bfeaf6e6bcccfa56f3aace5fd2175aea9aa7435e3d81bce8bda8a7b77d1f23d243f54e95c9257e31f43899eccfa13c97a7d02466b9d6fc8c0d2e8b153bf5dea7c8af57d4795e6ef0f57d3755d5731c9ea7731f31625bcfc99b930cd8b2b38d6762ae312cde0c759c36b908dcd8ef5d55e55ef69b6372d52d79cdba3a89ea5f02d3f0f758a9a3d7e46ff00a5fe91cf67b48faa6b1ac614f62e6394f9e1a4635dde592e578d63d07a80bbd1000000000000000000000000015523a6ed9c23a1fd169efd463dff005f06549e0e4f1e4c4c8b345bd76343e5f9e961393761e3d7b5a247569fe2763a0e6e853bf1ff00b5cf7b4d50def69c5c1ce92b5c1646d1cc75ae25d1ed79feeb6fe74e71d1f11d8b8b624d55f418f973187ad9ca8bd729da1ccc579893df016ae799d7629185ea91dbd1abebda8ef5b4dc6cab5174759df75d95adf2f93879bc3a5f2f87f2559db0a46e578d71ac67624f656f22d16ad61739b36e6f493bfea0269c00000000000000000000000000000365e89c59e8a1fa2b2be7fd9fd145d8a3f4fcce9c7b3d5af61f435bfa0664351daeef105b0fc8fe9f5554fbc1f4397133146f535a92be8ba397abf45e77a4d6f13dc2b3c4c8cbc49292a48cfc646e24c981a81e1917738b4eb19f3f2b89fb251715ddfbea8e1bf4fdef2da945eff19678bf24e0ccc379afb7798b9789a72e7fdb1721fcd97acd56e1ad72e47e4e9a576a9d427b3b9e8da645767bd7ec1d6ee8672000000000000000000000000000000000000001d1e6a226783f57f445d0b8a19d5ae6c91bbd58bf306f494f13cb5724a7952713218df363bda7597df2437d92af326e3a76b23ba70afb467e5e5e3eef8bd1f1dc27e6dfb2fe34a3ea37efb7bf3a3adc57bec0e7dcbb9ab6b1e3ce57bf5357873ebbee99daf8b744f79d31e7b728a824b3dfb059b619c8000000000000000000000000000000000000000007469ed4f6ce17d51ed1ec5d0abd7b8dd6ee666d06a517f46f15ea783d42abd62bf66fe462646b2e4deb3d271276a9ddbfdebfcef9ff00cd9f617ce9075b8d7d63f2748d2f4dfa11e7ce90f7bcac9fce3936a87ab4ec16743d3da7cf7ca7e94053550c6bfccfb0f20ebfcee91d1f1a00000000000000000000000000000000000000000000006c9d0792f5ae57bfa55297a4c8b59746f5f1ef5a6963a041e548f7be4dc37065b0287adb39f1f271ddbbd63956e7a4ff006e55f3b74aeaf83dd7e27dcf8a55ee9e2a77803c0f7ca32de987934bd2d6f3dd65f28aace71739375be7577cceae3b5f3200000000000000000000000000000000000000000000002aebdc7fa7d3f4930ae9e47d0aed3458b3c5c8f7371abf5eecf6b5aa5bf3f459abd92945ca60674777dd835ec7c49b8e1ebd676af72edbaf5b1ef8f7593c7a3c78614d546712f3da5ef115e558f7f4b2b17ad696ebd2f75d627e4f3a1ddf9500000000000000000000000000000000000000000000000dd34b938ee75db176df07eb1e5bbb4e63df698f96ec7cdf43d7b6cd7b97efa13df2ab5e7f12b918bd2cdec7cbaa3b3837b2aac2cfb79a4d6aaad8cd36efda6b47977dcb1ed65e1ef1d33faf48e77d96ff004d9e9b9dc568db354e7fadb509390d678fcc476fe5e000000000000000000000000000000000000000000000001d733b51dbf83f587af62bd21b66893b7fc953ab6c507075b56b945e9f8fb5697baeb1acd45db9454ec514fb7718b74e5dac6d8deafed1def24ac2dc65a96e973f178d6276be3d3d68deefc2feb49a86fd6f6fb577ca726f987ebff8f391f42c883dab49afd9d4c77fe4800000000000000000000000000000000000000000000000137d4b89760e67b7929bb1b6c76f9dfb231f57bfe535dbc49aa5db155ef253b67132239efe1db57ede3dec762be559a33d88eb9d035d9a847d8c445d1fa47a24bcbf77e65a3718fa578beb6be67fa0b8ee6f0fea5f7579f1f43dff0025b9f35a25f97eeed721ddf9cf4bc5d23a9e0c000000000000000000000000000000000000000000000001bbe91950f43b96d7a7ec1cdf6d7a0368d5731d319916a8fa981c09b8ae8f8ebd971d9da661733072a2bb53c476333ea3e35f5774bc8e9d19d1636f79df8d20fac6a1c2fab7d51d0fe27eb577cbf79f983dd360eb6af81b2c573bd8dec1ae8ce9277e9d0a4ad1513ef9e83e3a1bc000000000000000000000000000000000000000000000000006f7d1780f62e47d0f6c8fb7e53f455d5896a3b98713b05ab5c3d7a9dce2e7e569f564c3631354dfcd8ba5b6fda7f01f71b9c2fa2e334fe47bf3f5b8c381f5cc4a323037af2be5aab4b34e267626d144466369fd1f1b37add0eaf8209290000000000000000000000000000000000000000000000000003aaf2ae97cc9b74f362c3f3ff4a8faae48dcf431945fb6cd7911d5cdcdd5346de345b7c09b93d5645bccca51374bd555579e56e955e53eb2c0ceb5b53a6fe7741e6f97d73649fb7caf23f206b3be687f45e105cd400000000000000000000000000000000000000000000000000006ff00a06ebcfdbaa6d5cd76cf136a623e62333dec5c3b15f5bd7d56aab96ee691a96d5aff0043ca6064534e61ca9bd733e29363af176fa18d7765de6e712088dd79d6c973ce336760f97891b98b97c197e63e71d3b98fd5a8874b400000000000000000000000000000000000000000000000000001b66a7b055cf4f979792f0f661ed4be1c3985b37a1ec6332d62513ef9317969e5c2bb7d98ee487bb54f06b8dda2b7c456cfcf25b8d24b4eeb937067abe9b2b816f58f90c0a3cecdc079a6f9a1fd3a9074350000000000000000000000000000000000000000000000000000176d30fadf64e7fd37e796f578dcfb442d9dd6bf4f0738d77adf3ea9b412f51476b7e55e65b0740e47b05dd7a36bf15812e35fc79fbfc8923b6a8c92a5b6c7b668bbe4b8c7d5f73d360dbe65d6367d63e81502ce000000000000000000000000000000000000000000000000000000379fa7fe27fa5bcc4db1dccbf7c9cf231f6a2af6b8b0d97667c62db97918d019fb6667a9874bc7df203832c4d797e7224c6b977d628a2e51b2a9dd6aee5b2c4e1c5ecf9cb18fa4530c8000000000000000000000000000000000000000000000000000001b2eb48df64d3c7bb17cd6e62634a62d2da2e2e6706de2c4879976f590cad7d3624e328b55f376e63e74b8f32b3f2fd0c50d19b2eb3c692d635fa39fbdbe43b9f12f470da1e9e1000000000000000000000000000000000000000000000000000000000c9fa8be54d839127d558f1f21f3cb545fa657a9a6164e5627a3861e3b3b0fcbcf6bdb88f34d7e7994bdc845cd72e36eda62ceb507cafafa5dc03d4c01b0000000000000000000000000000000000000000000000000000000000093eb9c41cfdbec3c8f91f7af2d3fd0185ccf2a8edb9dbd4b0776f7e685adcd8ec1e70d81bbafd1daaf04a2fe9d479f47bada05cd4000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003ffc40032100001040201030303030402020300000002000103040511120610131420212231600730501523324016332434357090ffda0008010100010502ff00ee4f4d2ec68ce69b1765d7f47b5a2c6d9067ad2b310b8fe36358c9474db941000bad2015f66374e8c06469b1d1934b4658bfde66db8d790d0519e45fd12e6ca859017171fe52be3ca5186a08a870f24a9f0251b156f1268d3b6979348a5da77edada36d2936a58f922171ff6a184a7283162a3a62286a6d47534a18f8b0ab38e83230dae8aad2bdcc55aa127f1f0c2539d6a410b047ccb0984f3bc1838abc798920846d4cc47e564669dddd37716532e3b5342a40443c5ff00d611737ab86d20a4c2c359990c08604d132105ad287685d70172ca74641236471b3e2ec7f1956af99e3008d0b285f81e2f30359aef546c6f654a7792c7c8d9f9697685f7df926747f2b49c7934d029634eda7ff528634efbd4a215a318d78d30f6674ce85d548bcae18afa26aef1391697954f0437a2cf74bc98d7fe2aae3db8ebb33a725e42446ee8cf48cd73d21990d84361348ce874b69fb9372696153c1f1fe9e3b024682261661ed142f2a6c6bea5afe35b4c4849519bc7362a509a3cbd06656be8367f98d11f16ea1c37a59bf8708ca42ab486bfbb86d18e9a5745df92127dc65a5ea1467cbb0839bb63a6d4911469db68e356e1f117fa10c2760e9e12085985337611e4588c5f35363580325598548da223e282cfc84cb1398784ace5c658af4ec5246ec85d9949f28e0e4f9fc03e3ff8686379a4af5c6b87766dae09856f4d348a474ebeeb83a18dc9c61e2ce5da1241f52c0613ce87a6c7c7d4183f4e89b8bbb6d4b173121702fdead564b5250a034c75db7da9ff00df8311e378599b304ccf6256679a75e57dc563e06d38a6ca9e8af72515bda80f9260da31d2b910d9832f8d2c65afe0e288a63ab59abc7dd932725c9712913616795ade2ce04103bbc5513d443598558f8ee0fa7c67d763a56a3387859756d6660bc3ab09d964a0d3feec15cecc98fa234e161f681f03c565fc637338c4194c97379ec6dddf7da174edf1b75cdd4523f2a24eb969a4916f6b27870ca559622825fe0b1f51e216044ddd9d725b546815b930fd26dc2ce32bd38ba86787c916b62c8e46152d9f890f93f7c7cde39ba43241c1e6061eafca03a99fcb23448e3d29e3630901e23fdb66727c2637d242c3ef1b0f129b266ed3d9737fbf7ae09e1db144e2fc5461fdcc5d2f235aa0e0130b838a89759e18f97f035a3f2cc2ec9df489fda72715d39918e1793ac638a0cc7561d9534a531d689dd48fe209a6722dfb40b8be1b365511f596e2bb962b842e994aa459187f73a7e9958b7c112d3a76d76224c48e6d296c239797b6b3a89b93495b6d2c5c5c7e1fa76d8a92b858af94a9e32dfcc66a4e13c36ab9d4b1fc0566d20b1a4d3ed7919725bed24ba5211128865140331227667841423c46f4ff00b109a17db28dd6d484894c1ce376717fdac355f494c476a0c73ca8b17c06d47e3452239114c8e42744efee8a4e2aad8faa21e63720d22fa5f1b6bc478ccc7f6b2b65a646df2c5a4d22eaea62507f015e2d42d5ddd0d7d26817a6f8281d91ed937d4a38c53943134d6f9a050a924e31cc7ccfde2fa78a45b4cfa5e44e7bececafc7c65fd9a15ded5b8db4d483c92e2e8338deacc23986e0f2cfa22976a8d37b45174ffd190c67894a1e33efaed58b5362a2f20652ab82b1f495693e6b592063b447d9d93329231963bb54a95aff007ebd5fa1abe9b5a4c81db460cea5aec8ab2900c53b3b3a075112b127d3ef18c8d156905a22d3c7f2daf612bf1f38ff0067a74395f1658f2e1362ac0f1ca58060ce5a67233d9c40e65d378a1d4948423cdc0dac80ea4400f2163f02f20d9c1f8c6e55f13c1ff6f4f45c87318f630ca527028cb815693b332715c7b75653f257ff007e87cd5d6dbd01c8a5ac712675a72442fa315c76a6aa8e3e2e2e80d4a5bf7042e4a0c73cc784e95f20643a4b50e671ef4a48255cbe392dae4beea683c913b69ff63a69ff00f20097978153cbf05772de41bf2bc84c1f3037197a72c070b328bc5d4168595d979cbb5d3b4bd44d8cc58857c9556e39a8599e01dcfd35f036b44198aac4ad41c0ea48a22d8b3aa80d213633616e9bc0f2c632c76ea9d2b1fef626467a5117d58e6026bb8f131b757c278da63235dc43305baaf13d3c6f9dece09e30b94dc1fc2fc8e3e0cefed887915781b581a8324d88a22104d558a3eb3aa20f19f17693e1cd79503ed4428b5c676d4ffb1d3e5aba048d73767e5b69c130e9cdbe3159a7aea4ea6dc593caf9dddf6eba4a5112a320955cbc82019db0ce548794f8697c4076f935d3636bb5f6b878e4ad276a5378e5c648134797c7b6ad45e33eadada2ff007b0528b0b16953b9c541739b5f8d9d52b5e278ed34c393acceb1d2b42432c73c797c7327afc4a4876a68b5edadf787fc7a7a768ec6225692ad995a283ae320252289f6bc7b670f984137c313ab3ff7fec54b0f56787e5b8edbc49a353448e3d29117df93fb31b71e9cf47a9f88653a81e456ac3ce741b4f4e7d3797e89e7f994f929436a1f840fb658ac9bc4f6724d2c5902e679aa9eaf1dfef5395a0b480f8bd7b7a53dbe4c32e8eb5ad2b365881e6e12d4b8a736962ba1a974ad027f87ef19f1286c28af7864c4759fa71ccf5bbcb0e46f15d99561db8c5f49836e36ec4a6ffbbf67116dadd7675c931226db4c2a545f75aedaecd213273724cabbf154a4db9d86109ad33395c647676867f98664d26d312f567a33e6e42c633c7e19bfdec4daf515937c2dbbf61371453bbb48fb786c383faffa253f2169580d8ccda2f60cae2bcceeb6fdf5b507d29ec699a4e483ecad1f8e2fdae9fb651cccfd996d483b5252335354305f6f78a89405c14f73e2c5b7777989d47cc9431ba16d262764d2a6993cc9a6f99cda49bfdec75cf4761b44dec61e4a4896b49bb98edadc7af737708b920abf120f14ea141f6e5a595b1f4768e2399e1c0da9545d36ee9fa7e38c63c542ca0c3d4e058fa8bd152276c2541766d7b28d47b0f1e17fb795c5f06bb0f8cfddcfe63953d95358daff00278abed435f49875ecf94e6ead484357f80c358f354f647f738d88260d3b26eda572246dc4bd8cc981d4706d450696b4d28ed3c4a1816b8b4b2715204d7a68ba6a4750e0aa44abd6398a8742deb4c1d055e11b9d394ab85f8c02587e40abf312ac60f17367dadadaf2332e9d08e662aec30e6819872dfe7ec68ddd7851c7a4c4ecb9ba6672786b28a1e3d9930a76efc57504de2adfc050b4f56cf6e1d9be1049f137cad264ddac86c2c8e8bbe3a83dc931fd23ca2c974d7a7450784b69dd12664df09a0925418d615e95c1d88d90c8ceba4f3b8ec5c32f5ee3c1af75e94aaee5ad5e23f91ab61a27f3724efb50d1b1617f40bfab54ec545c96d7f57b18fb9d3bd7f16522ce5e676c81f293bc21b431368b42a676ec11f25057411f1eecb9277f665720f7ac7f0384ba56636fbc6db628dd7174df089714ec993276e4d7e0d3f7e8dc5b12ad41a38f398f12ab9987c53ebb34446a3a0e82b847dca42120998970174f13271b0c724a51304e4616724bd713ae98c54f9ebf8be89c763d18c75c32d9da75472f997c89228f9058ac50133b8bd5cc1da8ed06fbbba8a5d2f51f073a2772411727af53682ae98878fec66f20f521fe0ab593a92d6986c451baa90b48a7c6b70b159e378e0f2a2c5bb0cb1bc6fc769a1364ceaf47cc661e26cdb50c3f3d16e2cc3fe39e95a3a79a91a4b1c5451ec84599bd840c4b11d256f30f8fe80a9585f0b46a0e627a9542dcc3666b9776cefb5b543256719347d7d97269f3d72e212e488f487992fa99492f319a178d7d9e097d546703b398bb27edb7401b410ede9639cd4189766929346369b8fbee6421a236272b337f0784c935636552c7050db631bb0312accd1491bc724791a2ceaad7d491d0138ef63b839c1b6c856e2f047b401c56072af4e4abd5a2d0e77a88ed8c9ca53c6f4e5dc9bd3fd3b3012e90a900e62856a43cd3cc85fe1e4658cfd403a3566fd449cdaef555fb8af5eda966724ecb8ae3da382499e0c71448ad41033e41c9eb9b908ba76dae02436eb3d692bcbe097834832c08eba285042863d2807eac6388b3da161bb736a53e6fedbb763a315dba77a6fe170b9569a302d2827d22b3c82493ebaf61d9148d24723f03ad6dd98f538598389656ba093c72472726555a475f24b1875e3bb0f55e1e18ee75d5216c87574f694f24b39f89c9344ccde35e164154dd5813ac335c39138ed3b218c8d35395d478b98d34352b23be4ec44e4eff0029bef4fe636f8ef34632059aaf0ac658e61c392920470ae1aed0a8a728d15e91d14845eec9e5c6934f39d997f8612717c4e65adb09e934ce9df6a3274337d3216de23504da52fd6b2306e3b70f19427f1a1b5f152519615af66dbb08b99627a0649c6b74de3a80daf1c23d45d4d540423daf1ed45519d340310cb948c14d6649fda4a83fd04c87b3a76626707a36a16e4c61f1282f1af128e3d211440b5d9fb492842393cf736fe259dc5f159869d850fdc5971461a4c84f49a459093fb57cb7228a3d450b4d0bc5912db1b3f692761452917688f83c79bf45603f562f30ddfd4ccb586c866efe4de2075146eea1aaad64e3aca6b125a266d7b7ec9fe5b1a4b19d3a5607fa05761ca609eb022653c3ea21c4cfceb11271da78d30ae299912d7ce93ab97e2a4190cacb7dff008cc6e6a4a45572b56cb09a134edb626d3b3a7361590b7f13b737a95fcb60231064c854d5e40379a507a7e4b44740c95981e296210679a667919ddd032841440c037324f32e2def343f6e8cc77f51cb853d315453d3621bd07a6b9da0fec5deeff099fb12d277601c8f51844734f2583fe3eb64ed54683ab660287acab701ea2a33306629984d97aeadde12457878e2b5246dde3f953ff918b18e2ee044f213461349e69663d3321513287e15bb6f3bfb6288e7928744ec2c745d770cae366c65805fa5602791681140a581662469b29dac37d31c8d207625cbe45d58b11d68ef752c211dec94f903fe6713ff00c7b771271445c9d6463e32bdc9658d9485c8d90a894f37d3eee81c1b141e911d45d63896b1870fbf46e71b039d8e4194175767a3c4d2fbf776db4f7ace36e37545943d58dcbfe531bc45d486e32e6ee4af24f24cff00cde2beac7fbaf8728173d0264280b4b7da9509f212c7d13270c8f4cd9a21dba36b8ffc73d3a3aebaa8460c10fdd74ef56dfc555b3fa85909a39ec496a5f675043f3f80e14f950f718f36b18bb7523fba76d3b265cbb43114f361b011e2e9bd352d45d518cfe9b945fa61990b7874edb5fa9d9f8f88b69d634fe3dd998bc957f01e9f27f0a74deca9ff00b4d7ddc3378e1a16e41f84c87e7b742e3bd4e742b7c15752d75fa920216963f2363156e8feadb3459afd54b36a2924298fb513e161fdd683c9010f12fc03046e3713a6431f26d7688b84ab3157d4d24edc5d90b6900f33e9eb638cca36b4a506e3d63976cc66fde25c0d9f90fb75b6c845e2b5f805493c567b328dfe256f9ed426f2c2edb6983c734cc9beea07d4cceba77af7d046fd758568fabff50e5c98fec12a07e5acefddfecddb3b1f19ff00006771780fcf076074526dfb4133c06397aaf1ca7e4949b6cdf7ed0db63053d8189b7ccff61d6325e1297c38bfb197508fe05d3d2b4b41db4fd9bef0539acb1038129c78cfda56d1f6dfcf98d3bed0b6bd9af6ba02e06cfe7899f5dc937cace8ee0fc07a66df09e66fabb6962ec157af9610ba0af8ea5ed61be03fc53ad2105a5a5a5a5a45f0b4b4b48bb50b5e34c43221f9644c85d66bff005ff01c6cfe9ef99727ef8e3dc4acc5e29b223f43762a8670c3f23a5c1715a5af69326ee6b6a8c2766ce33a523aa078789c72f8af4088d10e9653fc7f02a537a8a9de94de2956487ebbadb85937df5f0c1e3b0c2b4b4b4b8ad7666da08d144b8ebb13fc71dbafd38c434ccd511d559fa0d2e3064215ea496489f5f8174f58e517b2aded35db0d31596fed7d9f4a07f241783c77b827f85b4cdd8bb46a36f89155a92df9eaf46c42177a361902ed49684c47b5fa5e23274ff859140ba9e41a387fbaaf578b66a6e47f81622d7a6b8a28de53b5859eac5de61fec17dc3e5a94bc0b3317f685988246ecceb9277ec05a432e94b22e8fc1b56c6fa352545d7d8d61ab5ebf98fa2f350606c09318c920c41d7dd49fd5ac54afb56e4f0c17a4e72fe078eb3eaaad5061986c18b5d87c737690bff1a66d482fda79de4a94e6dd632edb5b54e84f90903a365e390c0d9c78ed37d4f8cacde8bd3a92bafd460d626031ac3ea6395aa750dfa11647a8b257da19ce526d8acc5cf822727fc0fa6a76e71fd062fb6b31b1878c5e49dda2197e20b208546eb4a12f14bbef56b1dcb18ac1478eaaf4d58a2c639ba1fd3722cdc9743e6432b85521080754661b2d7ed3b7976abc9138d91613a11686e596af1deb0e67f8256b075278a509e3a92ed8db60e3f320ba9bfc4fe51c7a76519ed5bfeddaeffa7f59ac7500d6f82aca5aebace93499b1a2cca85a9b0b660fd47b3e3cbf545ccbb13a96b01a9aaf06ac02724d1b0ca538d4832175d7dff06e99bac2e25c5fd5bb8691eb50c0d2c76ab3c49d6bb5d7d9573e43dba0ee352ea4d2d2ba71d583256defde53288b4fd89b69ddd94903a79783dbc8edc8dcdff0612702c4dc7bd476bcaca4179998258822825b0bd36a46a719358a5e25918b8c719f028a092c26a91409ecbae9afd47f0407d798a68f3fd533e6dfb4adb1fb267e4da5a56240886f65b829ed14df85748f12ab3c442410b0ad271765c886311110066528f10ce46c35954bc7182fba82142da6f618f1785d0bf90ea74cd9b29b014a8b752d46a39cfc2ba5dfc708719466a8f1a1fbb331338f07d693277775d40fff008e9be157b3c15701918078adadf6d27760668e6b6ab619f950863acde6465c975bc0307517e158191fd2d5b1b509b1b4b45897078de664ecb5db2e5e590a3d2fb273676199c24feaf2090e6a25fd661516424b44183c9ca38cc4d2aee528945e1faa3f843f3dbafe330ea1fc2b004ef1452717ab610cff001624da39c99c66675cc59d859d5eac5099b2765a4c2eea3a33ccabf4fc922a982a30aa9622ac366c3cac53bc458c95e677abf12b78e48c932fd438bc79efc2ba77eab035ddd431383348ec8cb6893ba775b66464c48a9c2efe8a04d5e1140fc109484dc645e530786cede23621b41b2c37c1beb8dff828cd01afd40b052e7bf0ac09f0cac5590d5455f4a4894df09cdd727f755afcd4543e0293ab94f8b485e292bddd27979bd13e07eab6d607c89e3e09a4d2eb6363ea1fc2a290a1930cc56e9bc1c54eda721da38369a93129b1ccc881c0bd98e974abccdc7d530abd719dac97291b6a090956251caea08f9c7660d0bfc3f536bfaefe17fa6b91f5b8fb65a7b0fb789b920aece9ab833db8435763fab82e2b4b48798bc16e415eadc9a4d9af4c9ab21afa51b69464a94ade29cd9c64ff003ea532933bf85f45e53fa4f50dc2faa46e4a11d381b32b122b16113791de05e976869a0a6ce9e832f49c5dabaf0b2e0c98596bb0baaf678095ae4a59c628ec98c963f0ce94ccbe631bc57f8a23533393490edd878a16da8419470b218019e5005337cfb1fb7d9f9ae6b2d686ae1ff0dc0664b077a29c2c444e9f69d48c8fe1c1d467c5e29d93caca495116d6d33261401b470a20d390a74c4bab725ea2cfe1dd0d971765a462a404516d4702f0271e2b6b7de2f9402ca360653b8a98be7922f959eca7f4c82490a693f0ead664a93e1733066aa224c1c97a567415348a0d35814fdb4b484b8bc761914cca59368fe57d96673418c82f5b2bd6bf10c366a7c2d8c56561cb552fb87dc3eceda52c8fa98f7fb1a52ca104791eb18e3566cc9727fc4b1d919f176b0fd6b14e0c98dd93caea437745edda2904066eaac744577ad5c8e5c85a9c3f16ad92b749e1ebcb8317fcfea78e2eb5c74aff00f2dc6139f5951647d67480a5eb89dded7575d9cace6af5b444e65ffed8ff00ffc4003d110001030204030604040404070000000001000203041105122131101341061420225051324061b1233071a1334291f0151624345262728081c1e1ffda0008010301013f01f5e240dd3a6017783ecb9ce5cf284c3aa041dbf3c9037466886ee0839aed8fccec9d2f46a0c2ed4a7b40e21aac9aeb26baff00992cec84799495ae7fc3a27484f54fd538b987337754d8c4f0e927982a6af82aff008675f6ebf2ce706ee9cf2fe19ca1aa2c56e17e0d726bb37e55557867922dd3dee71b94ccce1e545ae0754ed06aa9e364c2c54f46637a7b397a858662666fc19f7e87dfe51efc889277e16e0df0109b1dd38654c7e53f904868b95575ce90e58f409a0754eb1365491343157583bcab217ee9a0c2fb853cc6409f7ea83b5d161958e999cb94f9be49eeca11b9d780574d8fdd1d3c0533652ebc2275c5bc75b5265772dbb04e6d95d0dd32b0b5b9423779b9e0e1c1f173765c8e53bccb98e87cccdd5254b6aa1120fecfc8c86e51e2d1645eaf7f0dc857e0d394f8b10a8e445a6e546e321b052d3c8d172a360ea8b3314d65b89e0d7f29d7553cb959991d45961137226319d9df2278857e03c247823376f871792f2867b2c35adcd72aa4b72d964b2a7a7045caa988336e0d81c45d3865d13911982b906c9c2c55faaa3a8ef3087f5f90eab22b5f4562d56ba22de160ba70447187dbc389479a72999a1d90a973ceab3282701aa793985462ee4db06aa8233294d828e5f7528ea131de61752d2874799ab0a98c53728f5f909058dd31fee89b146ce09853878586c9ce478c1bf8714668d90279b85184ddd05757b2ef0eb5913752eba042072e5bc0d51690a2aa2c6e529b296c824081bea3f3dedcc38dd0d15efe3278c42c1005c6c1458357cdab623ff9d3ee99d9dac3f1587f7f44dec9d465bba468feaaa7b23513c45b148d72acc0313a13f8d09fbfd96ad3aaa681d31ba92131f832a1105a354925f6595b6d53e31d1362cc6c80b69f21236c7f24b4aba2500a8b01acadd436cdf72a93b39490ff00b8bbbf61fb290330f80ba8e1b9f60a6abc6ea0d83720586d15546d32554a5d7e89c79b1e5b26e1c2239f36509b5b4d7e5b6504feab10c330fc45b967841faf5feaaa7051417301bb3f75552b5da0e207073c353e42e4d013b85045cc96fd07c8bdb9820b22d50f034ea8eca3a692a1d9626dcac3fb272cfe6a97651f4dd51e0b4545fc366bee5594d27756991eeb354fda5a36e8c6977ecb0dc49d5cd27965a3dcaacc40b7f0e32856cd948075526175b54733e5cca8b09a6a1b4d2eae51b9b2ebd154d364d59b2c7f09eecfef707c2771ec86a805b2be6d02ee65c2e53e20c57574d6b9e6cd54b00a78f2f5f92959fcc131fd0a75815a10989e2cb75b1583f679f58c135568de81565f0b688a8a0bfd931b8f55ff00c83fa7ff00552b6be38f94e7171f729b054b0e6bdcaa8c3fbe0b556c9b4386d2eac6027faaa8aa7c9a0d022dbae5a60c86e139eecdaa9dee89cdcba28a5cec0557411bb342762ab699f4552e81dd3ec84964e7dd326c86e9f5ae76c9cf2ee11c6e95d958a9691b4fa9d5df292476d5bc05c2bd917dc20b0ca2ef73f9b60a0aa6e401da26398fd5a6e9f335ba26cfaecb16acabd194247d4a761f573bb35449fbdd51534548db052381fd10713b713aaac9a18e95b2c8561f8dd349680dc1faaaf61b87aed7511e4c75adfd0a0f2b3942ee4c1ee9c47454f48f9cdfa2861640dcacf967461cb94e08b0ab1401581537269f39ddca56e7610a9ae0a0f6b5c03ceea7823eaaa4b5a796c1c77e1b276274cd765ba63c3ce66ecb1595cf9f21d9bc221cc81bccf60b16a06d661f2530f6d10664d1c9fbad428a29a6f802a6c3037cd36bf44001a0f9ca11fe9e3ffa47db8582ab66788aa5abfe590a2ecc7314384d550c1a48e514cc98668cdd62b2164161d7861249611f555585bab9d9a23e6541d9e90481f55b0e9c718a08a3ac782dfaa387537fc3fb94ca4819b35016dbe7b0d7f328e33f4fb68afadb84cec91b9e7a2a6ab131ca7429a784afe5c6e7fb2738bce676eb0a796d4651d555d3f798b22186d49765b2a7a76d3c618140ec9283c4aed245695927bfa0e012e6a731fb1fbab6a992de42d29ed0f6969ea9ec7d24d94eed4d2988b43da5a7aa968a689f96d7586d1987f164dfc1b2130e5730a6b83c5c271b15da1873d267f6f41c0e731cc59efc26bc7502dd507b5c6c0ac5a8a39b2c8775b2893cd82ba60b0f0151b8be9dccf6540fbddaaa9d923cc156bbbcd049efe83472f26763d3764f682a49a4a2ae73c6f753913d3676febc213aa73332115b756564ed9593f40a9e611bb31d9371ca6a798e4692107c75f4dcc84dc14d6bb952b0fa150cfde2999270c670d74c79f08b9eaa8637b685ac937b2b26795e15acad745a80b94e8c06aa9ac8697479d5475b154fc0abdc5b01038766b3f25fed758b3b914ce73bafa1767aa3307539fd47fed62788cb433b58068a195b3b048cd8af759ace21145e1cc050364f94345dc5475d4f23b2b5daa74848b29a432c85ee5401c6a58d66e50c3217d398a5dcaff2c4c1fac832aa1a58a9630c8c681769ab448e10b7d0a82abb9d4b26e837fd17686939f0b6a63d6df65d9c9dae89d13fa153e80953b2cecc146ee8544ed32f0c62625e22e9c282964a8a7648f3b858860733273c8d41584609242ee6c9bfd93e27353b56aafaf65041941d54f33a790bdde87805789a3ee526e36fd3dbfbe8a1a4a5a73e46d9171276d154d337e31a0429f3fc250cd0c96720d73f61a2c530e7cc44b0ea7aaa1c0aaaaa41cc6e56f5253182368637609fe57dd66f652683313655f8d4507962d4aa9ab96a9d779f4485ee8de1ed3a8586e2acaaf24da3fee9ceb3ac9cd0f6dca10c6d398055a7f1953565bf0e554f0dfccb3376babaaa9a285b9a57594fda3631b9606dcfd554d7d4d59bcae4edfd15bba0553e2f34360ff305498bd2cc329758fd5492306b752de4797159509e4885b3582ff14e56a0dd4d8e55cba35d65248f90e679ba69e0ff0088fa28dfc0d95ecf84aef539fe646690eee45c8395f80d1029df11f461a8e04a1c48ba0d564139351dfd1a33d15916ab5b85d375f0ede8fb269b8f0655b2bacc5037e0f3d3d218ecbc1c802871caac8bfdbd2838b50902ccd5982ce17302e622e27fec43ffc400391100010302040403050801040300000000010002030411051221311013224132505106142040d12330334261718191a1157280b1c1e1f0ffda0008010201013f01f3e869e5a976589b7549ecc54d47e23837fca6fb214c3c729ff0a4f65e85ba073bfb1f447d93a722e2423fa553ecc4ecb981e1dfe0fd14d04b4eec92b6c7efc90374658c6ee41c0edf32c63a5706305c9587fb3ad60e6571fe3eaaab13828dbcb845bf6586574b2baee1a22f73b64d1adca9aac30d9365e66cab6844a3a85d57503a94e61e1fbc9256c43a93ea9cef0e89cfba72712d399aa0c4e68b47f5050564351e03afa7cb5150cd5f272e11f4587e1d0e1cce91777aa7e791d65fe990976672a89194ccb3028310be8e4da86386e9ec648991868d16555944d91a4762abe89d4725bb1dbeeaa2b037a63dd39c49b94dcce1a221c0a768140c6ca2c54b4a58e4e6e4d42a0c40c9f652efebf2985e14fc45d73a306e7e8a9e9e2a5672e2161c1d2b1a8485cefd155c99cdb8465fd936578eea9a4ced5598e329a4c81525536ae3cc16298736b212d1bf644169b1f8c9005caa8ab2f3666c9a11d4d95346d0d55760ed165cdba6831bae14d21784fbf740aa0a932b7249bfc9619446ba70c3e1eea1e5c2d10c6341c250e3b2e5068ccf5598c323e889412ba717726b02b01c2090b6ed588b1e273758139d132ce59c15ed051f267e7b3677fdfc7553991d91bb22db2ba0995240b047a8dcf0770747ccd972b967559dd17531534e2a230f1f2383c6628330fcca08edd4e59c14f7860b955b399c646aa7c2b3393e99b4e2df0036374ca582a7a9c13e95ad1d0afd96214a2ae99d19fe3f7445b43f0d6cdca8f4ee984bce8a485e05d31a3ba2db941b6e27835dcb3753e491b74750b0d97932643b3be46189b1b1acf456d1465d9ac54f258d9ca386376a9b66e81543b349f0d34f95d64d3982e58ba2c58dd37bbd63bd1dafc3893ef206aa10dba9ed956550422d72a78f2f010b88ba22c8a3a857b1b222caea9a6e7c41ff0020db985920f40998b81272dea5a98e26734a33c758cbb10a910bad754f52254ffc43c5adbac567745d2d587d53f3d8aa5a8cc2c5660bb2f6963be497f8f86b9979936f16c84ee71d566514a0354afce983a90b00a6f1290e89922907709875d5494e0b333561d272e5e59eff0021815409a9794efcab12c3339e644a0a5927a6c8f54cc9a927c9d96270b9afe63561d339c8756bc5af016274c67ea6aa0a2746733d4370fd130e6d131a0357b58472a303d7e1af6ecf09c6e1461350e17b2e73ad644a935d1729cb238044151d4168ca50932bc3c21aebf7f8555fba5482e3d274283504f8987a88550c6c9a151c2d8bc2986dc0aba6ea106176ca9e980d4a6b407279b3345ed0d489aa4463f2ff00e56c8d444dee8d5c7d97bfb3b052d532461639aacaf650446437524593e0ca84616813df7595bdd3a31d9363b9b2dbe4305acf7aa6ca776e9f45b276c88bbac9e32bb834f070cba954f55149272d182c06450c391b72a790df455ded152d0332976677a0553884b3c864f542f2bad2393594ccfd54d230e8c6af09bae6e6d2cb96fdc8593d55316b058aa8901d07103839d64e7dd342770a48f3c97f4f91c36b4d0ce1ff0097ba97ae30f66c86231b4f2dc98d8c0e629df1c87a166ca8390d557c0f14f995397098109b89b29e10e99d655dedb4510cb4accc7d4ecab71baeaf3f68fd3d069c1a33e81368e43b9534222eea38afa95cb6dd09e3668059493be5e91b22084c7df74f6907304100b657be817bb122e53a3caafc034b8d8282110b2ddfe4b00c505bdca73fedfa7d162f8638fda46a879b35398de87369aa32bb6588b646012316172b2a4653ba686c32752e5c7531e5ecb18ada6c3e731526ae1b9f45cd7d63b3cef47dd63fd53f944dc2ccc2992f2fc08cb33f729ad01665991d559345c6a88b14dea6ea8ddaeb20fb22fba6cb90a7d5128b89e0c63a4395aa9e9843af7f94c231a1503ddaacf5763ebffbff00bfdf76c6d66c14f1c32c9d41490472c79145869a6973314e40d5cb10c664a2a7223d09d027b09374411ba0d256550363de55cf8dbe00a47ba43aa03e18dae2fb052d33c75288f6551ea1071598add307aa2428299d37eca389b10b37e5a8b1ca9a36e43d4dfd5331da3947582d3fdfff007f4a9f11a57e8240b9d09d43c7f6aa24a6683cc78fed63552d9ea4b587a5a81b14f56d344d714c17d4fc5c97a22ca00036fc0e8e3647ab74d6db74edd6ca38e497c0141401bd52ea80b6df39278cf18cd9c9ecf456b716b1ced91696eea0177709f74c9b943552d58b599c628e39580b82f7183d1369a16ecd5b7cf4c2d21e2d17364f8f2f168b9b202da29c74a8df90dd73989efce6e9c2e3e0a377491e4354db3afc0b74ba06c6e810f6a28ad8dd364690a6933683e1cbad96dc291d67dbc86a5b76df83756ab150485ba70721c0fc2747dd4a3ba60b9518c928f219066691c0141a248c04de97db8395ec8bd6657415d3754e6dc2f767bdba956313ece5f981f2295b91e470a79837a5ca5239970ae8ea38df804d89d26c84259ba60bbf8565b3050753ec3c8aadb6b3d430895a4a734b4d8f022e102ad63c036fb2313da2e4209a328b29080d24a33bb3e60bdf5b6db552bcbcdcaa38ac331f22959cc616aa47e57642ab1a736609a9a74b27b7ba70efc295ba66e123db1b880a2aa696f52a8a90e19420e057750c46575d35b945bc8eae2ca79813a47bf7e0c7f659adba2330d165b6ea0903742a5aa630686e513737286a380f4515397eae4c6066de49887e028ea03ba4a01036598a8fc28b7d13dfd958f00a7ad8e3db55356ba46900aa3767a68ddfa0f25ae17854d1f70a0ab923e976a10998e4dd7643416e0ec8357296aa067eaa4af75ed1b6c993195a1a555c39023a2c38de8e2fdbc96a85e17232665a26b2e9ad2dd8a0f78ee8bddeaa2a6e6ea54b439429204d3cb7053fdb37452c56365878cb4910fd0792eea78cc12b98531a1e5414cccaaa2311ecafc21a9e5296b83859194294872a376a41556066b854df80cfd87936314fa09c7f2b9a46ca1ae7b549506551bb33aca3a7616aac6f2ce8b31574d17562c7688b4cae0d40585bc9a463656963b62aae98d2ca632815182ed908decd6cbdf1e3445e653d4991b49b28e8e22d553072cf4a37eeb0ba624f39dfc79462345ef71f4f882735d1b8b5dbaa291ac3d4a49e073148466d15d0714caa7b519cbd5261cf91d9e61609a034651e5353430d58eb1afaa9704a867e19bff85ee558ddd850a3a93f90a661d52ffca9983cc7c44051e0d18fc475d43490c1e06ffc10ffc4003f1000010203040607060406030101000000010002031121101231510420223041611323324252607140508191b1f0143362a105247282c1e14353d17090ffda0008010100063f02ff00ec92e89f3fe954847e345d8fdc29f47f0bc112611a29986471aaa823d7cb784bd5554c31a0e72d7939a1c39a177608585f1fa7dbe8c284a19ae744d943bd3c9c1173b478a00a92585544b8d7de9789ba3eabab629c94e5bbae39aafb549a1031093c82d8601aa61c660764ee2df45d43dd0bd6b2c7e7c0704d6468575e4175d0e0e32ce9ef0bad409179f9a92145b520881bcafb449a0b8e410744abbc3c10a6e43a42f0a4d07e864c123187dabdf338fdd17431c00ec448ce633f76ccd1a382935a1b6042aa57d1afb8cc8dc60e2800d17a4017018ee6606a7451e1b62b3272e9747bf1f469578b99ebcb9fbaef4518f77571553ee36c4d228cff00af8a9012032d49ea8b0908ea44d220868807b83bbee89344cabd39bbd8a404d4e4b685b3181f61b90da5eee4a1b9c0ba28919cf8ea0010a2c11d5133afd3c107a0388f07b99ac1c5506d115de61ae28b04ea295841441a11bfbb0db3e7c02201267c380d56a6d851d4a2c759d0a209b1c8b2a611ab1e78fb92eb7152e271dc5029c9555770d0536c253ad1107a1df5c86dbce418daf1279eb029b558a35d496a0d588d0d636391b311cdc13e1bc49ec3748e7ee3bceed3b865b8025441ce6a3390443772d299557af0922d6944da5a7028b4e23780013278057dfdb898b72dc62b1dc84160ab69fe22c20b241b11b412e7cfefe1ee168226de3b917949b20886ba6a6ea9dd0aa95faa333aa220f8ef3a4ba0b19c5c38fb23415346d7c28826c78bae1c94482fed30cbdc25db9a192da79526e39d9352dfb8291a1ddb1bb533521c703c6cc161ad5dd8574a28dacd28025ed3709184befebee16f313dcd549b41ec97b3dd438633aa0820648d11d4eca3452d66a08db43aae63c4dae12214480ec5871cfdc0d12972d5c352bbcd913532ddc4f8b7753c86765536a8d51b00cd368b046d0d1895392c1609bea9b444847710f486b7f2e8e74f87b8209fd02ca2a8b6a2da6ec043654eea9cb5dcdcf7513d2c0562b146c61e69b5b0d6dbc7d10a2344534734d52475b0b1d0de26d709109f06276db97b7c39114a192084d12db02ecdb31455dd0f54289c3927d370ff005dc9c88dc4af497691acd4ec68e20ac51450435c584816418e063b2e33f97f9f6f88c96d4e73cfeffcdb22a7648a2a4a5208c957735cd0927927827069aee1febb96c413a70071dce3a80f02a57911791b029a3ac01b63325370179b49d47b7c379a006b6e3ad8eec38200bd4ba445c4ee1fea7741dc703bdc554eee62d2d70041a1054487e1716d7dbeee0e87b3f0dcca7ec8f3c409eedd07161ad06075a9bcc6cc563b888e680017120012fdbdbe67b0ea39022a0f1f6c10fc58db286c73ce4d134661b0e5e328f49165fd2109bdcf77c82ac2fdca6ce0368813a342a7e8087f2f0a9fa420e84d74278c1ed7198d69c91a7b2c620c8861a8f4f7086f187b38fb1e1b9d8639e277448511e92335b95d135561886739bcab9061de393420e89284d2bac885c51e1cca774756d996b8ba4195851f6410bbcf3970f70b1d7a4c9ed7a7b0f243657614b568b0a299a95b2aa2c947675b3ed2d9bce5d4419732bac7d3258c94899f3b7ab82f7fa053fc3b87aaeb61965a1fa3452c2d4347d28745a40c327232f6535ea9a7607b89cc88fbd11b9e56d372751b308001132aaa5b40b69506a5560a8bb3b2ab545d76415d01488a2106038018b89e01073d9f888be288b65ad6019511bd19ae3936aa4d1761d844e45570cd4c508421463378c0e7ec9d1b2f08913070e1ee31121991faa6c46998395b416cd48aa2ecd93b426208cf75386db90bc6ec10fc43dd18aeaf47604ebc193c93881761ab8cc2de97468ae84fcdaa474d7aebb4988ffee539cd52aa73b2eb997829c8ddb2bda1b8c161b99c4757c0314e8aeed3b2f7218515c7a377649346db236554b50d110752486d221a68a725d54237732a71e306f20aae738ad832765666ab64380ed15af0c129b5757a3359ea519c5e8db93519ba67352141abb0d2e538d103464a4d9b951a00449b4b5c26d2a58b78141ca638eb0437379e6bdd6e68bdd41c1b3a0f737451e275a302eef7fbd7a6a4d116ccba8aaa1bb4913820d64a50e2060ca4baa0f888884d10c22e79bc79aa95458d9b309c7d029c486f1fdaa43645b46cd761545c1995b47a677252860436f2532666d3a92709a9e2d5d19c46a61ab53ac58cda8d97018628c4886f3ce27dce083223884d851691fd3b5bcc6c691ad8d81ad1327804d89a6c4e89bff5b715b1a334bbc4fa947b2c013e14170d2221a53016f655e790c6f35282dbc7c456dbbe1bbba704d3dd437979eeba1747a29207189eea041911c426c38ce9469e27bdbac6c68152a6d98404467cada54ac6c993209916056230cc12b6b4380e39cca943e8607f4b668fe27498914653a6a5d85d644cf80537ba7b8210891a6d69c1aa5d10462429968c45a471e0a47b4da1dd92f70bf2a338952fcb85e007ebeedbb10ba2c1c2ecf04d2c8ad04d2eb8c8cf5e5601c1506a4d910adafdd600346255225df822c2f2e5b7323923d18bace13d4bcea008b21ecc3faee83089c366d39616104514685e175b2eec4faee49719018928b3460224a85cec3e1fbabf11e5eee7ef022146201ceabac810dedc9bb28749062b5dc43644227a4b9fd6394d5e1a443f8992a46867fb94ef897aacd3a27396a85228e8f1361d3a1cd173a80273f352cf56e8a431fbeb358c6973dd40020ed29fb5e062eacbd8ef59ae8a28f477036699e2e8c7d753497370be6dbc316d5070e3ad7e2bc31b9945ba34e23c8ed60029c576cf81bd9f7d42f8fd75c3b34d86f792d161d4b83e3aeed39edda71bacf4b62bfbf076c1b20c67fe4bf6227a26bd8439aea822c7436ba7a544126b72e7a9244b5e4b3c0e336a33870b96284f4690e327ff00a46701c227013a7cd194101dc0ce8bf36e0c9945d64473f8ed19fbf217c7ebaf3cac3afd1c065f3c792eb34801d935b345edeba18c6588b74097fd76e9cf761d19b58c85103e18ee44aabb0d90e09f1013462457988f38b9daad7f909a3c248d72dcd08917477b219c1c4535d909957bdc1a13213057bceccd982706894389b6db3f04e775fa39c336da3f85c175e776a291c395ae6eb9e5e428ada5d0e9f3d785ea8c38ac6c584ea39a57553e81f564f87253d5d1defc1b370f96a6863bd75d63349d16218515981080d334126278a13b1f8230b4081f8507fe4799bbe09cf7b8b9ee3324f1b7d75de11197904b67473709ebb1d91b1de266d0b25680b458a7b0d749de982a616138051624333830fab67a6e1a503ac538675f20c2764e1ab3b403da6a927b723253b5b63745d35ae8905b46c56f69aaf7e289fd221ba69da1e840e8fa39ed38f69dff009ba9711ae0f90410644714c8b295e1396a4adbc15fe90097778a7bfc467ab2ef5999533babbc0a96b34f90ae7186658fc7549870cbe59293848e46c70d7ed6f0141e3c90f807bf56fafdfd355b728ba6bb76334565deb01ced050f6091c14c6afcfc8501ff00aa584f565958e09a6d27d85b0a18bce720628e92271c94ba31f257c7e59fd949a27633c8709f3bc4b6a79f1d4e46c69d40a2339fb046d31c267b0db74804776769bde1f21c4824f64cc57efeceadd7fcd00dc0236b4a61f18ddf4505b79c5031897bb9608f424c37f09e09d0a2b64e0a4153b42219dba4c475366ed979c9d2e34f21b66fb90dd47656068c4ae93b6de5a8e36dd3814d883ba5039ee9919cdeb636d1f4b61e900541ba5068f8afc3c5373478bdec8a0e69983c422e7b835a3894347d1eba2c3ef788abc512a59791213dd8915f54082889cc1e051bbd936bbd751f0dc27440654d5b90597b33c02da8c01e415f22fc3f136c00a812c2e0fa5b0e1f17c45202aa4ea2bba369ae0cf09aafe63497446e5c14a53b25c1aa66a7c89120388aed342054d578293705218a60cd4ec958f66a43830c4def3209b0983d4e7616b9b307828b07ba2a3d2c84dbd3d2200b8f16173c86b4624a3d1fe443a37ff519597620973526ba615e5fa8a2d9faf9159159da6e69b121baf31d81574a2a8aa9ba9253f8ea36627721976a197804ec11b458861bfeab6f4584e7e6090ae3dc2141f032d98aa93e815d18213c72539ed9f23bb45762e379bfe5514acaa2ec4298ab751a7929656e8ce7761fd59394ed7c6886eb1a264a8d1cf7cd3705c1735437ca9b8ccf91c1064460426c4719c41b2e90b688c8d85aee1661319a6bb9a9ad814cd758fe91d905260b83926e8ffc498e89768d8ccc7e2af35f1221f0862b92e87470690c1c7d771b46a8b45397159372f25446feb52fdf50dd59b8e3682077ace88bb63777218311f9354e3bc68ecc855c85c877dfe27d56950439cfa875e74a64913e1ebe4b26b22ee2a454c545925246d863f55b27f654da6635a64c975101f1078e526aebe27f6314a1b036dd22eb8baf86b8ccce54f5fbf4f25fa388b24a6da1522a7a81bc1b6b69eaa7066cf8a01ec556382ecb95dd1f467c477241d18b3456658b95e883a7899c4aabad000e48ea3cb9ce21d0da5b7b80e5f19f92e2b692067ab2e164a6aae47bcd3c757661b8aeb223618f9a9bc18c7f52bb098d863f48dc34df73afc16ba4e3d9a914f9792e2b7f4e1b9c5779764fcd7e505b2c68f86a0dc163990c746c01ae69a918d6b9cfc970364baf1bb413df60b04662cc75e3c8ce4d683b2070fdfef2f25b2230c9ed37815062b800e7344e584f972d4c161648ee0ea8d4d32e96117bfe312187d7c9913467340768c40986c8169c3e38a3bb98d5c358ad2cb8106f4aad970f2668f11d1190a0bfab885e642e9e72ce48ea62b1b70582c3752b22458864c602e71e4a2b9a5ee697120c4ed1f5e7e4d9457dfd220c9aea1c3819ceb391f66d3223dbd236e5dbb9ce9fe7c9c6386078730c333e1cd322c337a1c468734e63758eed9a3437de870bb52f17dfd4f93e2683167d293d235ee7622405dfdbee5ec9b37ba678d93c07dfde29cf799b9c664f93d91a0bcc388c330e0ba585b2f147c338b4fb151c0c6346b653af3f98f9a7c77e2ef289890aad7483d9989cff00d7c532342f08bc3c2729fdfefbf2f88f6c360c5ce320a5a1b1b1bf5be63f64e8d19d7a23b1329794dba468eebaf1c38386454283a6cd91e774c5ee1e672dd973886b45493c139bd317913ec34fc933f0d09cc635e092e226f1c47245913498d118716b9e48f2bb4c0d222439700ea6787c4fcd4a2c2644781473693a77bfd4937f978d7e931490ceab6ba585fd6cff00cfbaafce70c6a587eeaa4de91c66409890f5f4fb92126c57b4b419b4607228f47a34368952f126aa70ee4096176b4e73a7ec8f49a4c4208ba5ad37411e8117389738d493c7ff00db2fffc4002d10010002020103030304030101010100000100112131411051617181912060a13050b1f040c1d1e1f17090ffda0008010100013f21ff00f64cac0652d3547fb73eb2c312b8ddf9969077e07e65426e9a47e2b7b8ac103829a2adaf722f4ef615e3edca4a19a76858a7405f4bccbd0b98a7304285c2e3a9666fd6670f272fc7f9c8005ae009be7c6ab9a8f0b96ab1fca2c07ac6b5d5e738eddafda0c1ea080efa8dd33a282b0967e3f74d89051d9bbf88bd08bcedf980e5ccb4635a550229d2ad8b05d0bb58972ce50857f94cb3775d1283a61d5ea46fb0ebdffeb13685001536cc1c42201ef2e347c4bc0ae32ae6969f202e956abba0339483684d1873e1edfb801dce6dd10416fb68f48c1dd2d89ccbb6012aaefc45b5316e59cc4207a06662889532abc4cb42351c71fe39d69d05b3d74155c3528ca068ed3889e285cc14108b530699611456245720d597ec7c10512f36ce1c14bdb4e3b998137710b901f0ef3fb6aed41687308eccc1b96c044a7c8fac7a91c658f95cb369df4351aa97084a8152e4c53b9cd530a4661dff008ae2382b173e3fbcc039c00e3cb071880db00e3ab64c50298cae0c758c74d8318acf05d35567672e4cca328b24c02dc0f57e5e7f6a75d2e96aaf58c054a8e9a4e190ab47d65497c1a9e6957305e7ae9408afa8c36c45d1926bfc34402176bb7aeab86121e206012981516c1322232984a7a19e69848c5c243f12c6f3158139c969dc2e1b3868ed7fcea8fda2e5fb4442c94ef8319f5fa2fa175c2e9593125d4111d0a6d9835d75d928878623544333c32b6eff006ff8235638137b777a3c358bef3174d8c4ad5862f6a9e954670971084a6970dacc11a3de5cf789763bc66e692160ee499126a02f42adbc8b79f6edfb36d216fb430000b8adbf42c1874112dd2d8a53a1d1542a6ea711d34c49032b896ef84af2835a8f4615cd6a2aad92a42d69861f207f5c3312d3fc87d9800ccbb8ceb9e79570768418eaae6d4762dc63d418b7e65371894888a84adbd226f2c04a64a1982f1318417ad8d9870e430e3bce13cae9ff004d7e79fd90d9daf83cca13399f9ff9f41b6003ad30d9992012df294f1301886f105b488287d0f4cd0dc6aa1b2e024ab464b947357d60a01f93f58bb10ba05fefaca605dd669bfc4aa2743a573c333a8a0f674b2fb31de7a5830935732994e88ac2c5895513a621a85c346eaf60ebde5ddb6d5d0693f637e541b19feb1d0a3ad1186d0b8b28613df335a42595bed3279979059dc7d379df4c7137c54b1bdc432e19e4365e6a769094c153d98cfd476cea02d58f6cd42857899e7bff006ea26a32a6a5c49a3c2303f945598b6cf5babcf4dd3310bca3d598f9318f745608c598244e26909c4fc0672638fd887900e5588ea8225afd2423751afe61a44a989c3da5b85388e10a2ee98b79fa6a62c2e5441e06ae2f26f172e9acd26d2941e3febfbe3f506ef3b89e064cf9cd7c43945505e22c6a51d1013cd196e5dfd0188073295160a87926a295b538e23e3fbcc22999d09b1568a482cd3dd5d3e4be1dfec2c03bd46da00964f54af79604e2455a2259e01d0f59b51ca06e149758fd37b4c3198a66c364bb3de5607b0fe9e51405c2c0a1e2ee52a881b469ed389429d2a26b0635689f5599702e67a6435d2b32e1a138e605fca7cf4bfd00574ca6d73eff00d63f61f2a0dae29186a3aebad20f72cf339152d2b3da3c86f3721d52c1fd268b12f431e913a4fb547f3fa5cf6d925d06ff00acaa76864ed1e5d4917605ceea618292abb1d93ed15298e3e8b767a379914b504c25a9954188014cabeb97bcfbd563863bb743c1b1f729fd81866103943d573c553162343a51ba2666b0c11a527307ac199a56935a2fd7b97d12ec48d4388a16230ea3c5e4bfd2c7397b3f1cff001f89921f6907885456a4211b8ed53baf54ec49e21baa621b0c06aefd3b448436937de667b633d52a70e33e81030931264f110389604332c223e65aaeb1d184556106dac5673e717ace3fcf16c16ab457040d1360f47aa8680b3781eb2b88392025c2748a194feb5cc5dcb8b6c6a130cea25b0a6998a6715652a3e70133c688883b31fa3562e198152823a8a42e4eff00c4405b993098d021317307226bbc3b14b2e81de5b422456eb7033627989afc252c6351b3686308cc84d63d0aa94a665bb238428b96a86bd5625302f05a6d63c8fc7f9e36ca780fc1fdefb9be9869728a22e128ba864972c00f6e233c3536579ed0c0384760ccd422077fac4cc33030c29822d02ad3182b3c740be12e7aa59982e664a8e77dfe8a12f46bcff6ff00af4acd3d007b99632d651080ac3898238f78c0f62e3b2e7a7fee412b95f499c480a3191c10f2673115426b52d74b2afa64da800c6a70452ce1a3da5ade300bc7a3fcfa0218a362bf82a1d5c6a6666c8c20f31a9bd4b41770d44be25a1909550f89660cf479fd5eaa82228b7062559b282f2d79e8d84d98c69334782629f96e6ff46d61ca853b4642b963765730cb9359be3b6d6f98f5283fe885871aef090b5f98a4ea52be65706652c75cca86ae3bcad834dc48352ef64144dca23442ed947975effe7bace5076d3d12e23d639f966f130333632ea664d32e09bcb4e205c69899b9a86d3e9230dcba149ccc7857762f3f062024be798f4f291499a953467f4ddff48cb7569763f01aad74cac16134a6fa77f557410701953743eb05b3158416cdf63c6c8ee485cba4487b4a91292514ff00711161e06a0b13b4572ed8a0386b85fe5ff3c8980441cd30fe3f12a36398c3a45562d937a85e07b3ea4fb17a1259e696bb63051a67312ff48220bb23e3f4df023e63605fa7f719979d154318fac01431f5ab88aa77f5ed02d453b8c49e53d197b2aa72630e995f8982512e05ca2a38182f166bd2ff00cf60ec467077f6ff00b1a580b03848102109425046ce90812fe5e7ab1fa01154d8a8c70457b9b74da2286ae59706bf3fc757a465d855ed0376aaee7c5c1a8aac64cd7fdb9521f7549edffb111be9bcff00d25c1d612d5d5efbeff8ed2c80b5e038acf78a1caaca2e2bde2606bfc98b3f1d38e3ade2b10f240b30963f1f52c01044ac8b6cc57bd198b001885aca862398a34cb91e73b1291b7ec20e50d6326b87c7fe4a8421d2b17d0084ca5e2cb43a31254e4ac12c651edd20522744bccbdd4add74876cd567314b9c3fe66b99eec357f9a94282a2cfb51454b5c557bc5ccec8a8ee12f4d47dd207ce898d51d6d87b459286c423d0c399733136859a8431c74c287abfa13dcbc5f995a97710440a82024abab76089a562b4ce1d6c3e7f61b30635cd3a643756ca81052aa2da68425a1387404bb7e83a83c0bb562591e3da29743d25db2921496c30d63050b66bebdcc53e423691859770b89176e1b98c57c421215cab7e10d97ea314d4f11da6e2c7c802d4bf148d20778c369c9315387d6070365fc12ef46bbe8e442452e2675472c67ac4d38863e8604bfa1d28eb38ab8c67579abcfec4a71b6616d7f3ff00a74860ed2e2464a8583d14cc23b82a779ac750b6889e74e181193dad4bca017d15312ac765578e906a36cf1a986153729186c5454603965f81e89842796369b31027842ba8bfc481ee3e6f825541b8009e750d6626abb81db02e01170d9c46e66b5df09ba8b12500452c225952aba7e9a79a5fcc463ea8f4c44d517aae5f40e86e96e1079cf7fe37dbf63a283b1d0ecc175cbb579d873e2572b6c9908324311dcaec6505a82742d9c9254e70f445f44588eb799903310a1a483483988eab7328038b8080afa77e4b16ba93af647a0664d1161e41332a8e3885c1784ea70c1e08aade835a6ac94f77d633f2584aff002995f0b4b16a7a4bcc4640a778b61270b0524693998df2a20c4e36598b52ee5e9a88a881fe90a69321dc14840952a540e99b8c6a95f9aed0dd04b44d7e57f642a4a0c859e381bdc751d99a97a370d53995082584cb05c3a53dd9a3912843412235094112cb71c33131a2aa5325733647bc0f7d71510cb0e63ebeb2c596f05a2610d197626f402571006a7e3e6333b337329961c35300adb4dacb5e8e3e13d32aa563d120879eb298fa3a854f9f4fe2525f0f6085233a48e6d767be451b5cfa4a1d42e2d6a276cc499654e8d8426375702d91979cfd4b0f71ee3ad7fec6bdb7791f1f1fb30a20f5894e33cff59ccb72d998003da57623a665cda9918345c77e05d5640041e65c38cc0818c9001d4d11722b88aee0c0a29e358b043d2a14abe7989453950edbec4ab0a8b60e4567e6e163b2be54b888d92db2b9b4bd04d1a8ed90ed62f90f8d278e55bcb52ef2c14980e90ed2efa22008fd32f3da5c2767d3a18752a6766d01bd41cc33170188fd4536df573b8e43a71300938038ae3f6746ceb1291898708986377e1c45e52957115c2442917c928252ab953d272fcc140fb25d86622238c9d32815d1cca787cc11d31603a80b563e56cd77ef7887eadc3fcd1ad5c960083121a67eb32cdb2aaa29793e6505ce551117dbc916ca9d983a5f514dcbd4e523b3a6445036b89615bb87c4b46c7a4b17a7d13b2835288d1958ea7201173e057f032c3821c5e1b012ce33e3f6a76ceb129196d768051d9ad76f89a417d19725ab98b1bab4d02665127638945e778b6e2e3909731306c70ec740f71b313a8f6cb943f05314003cdafccb111ce9f0313b4e985baf785a47f45e65d73e0e0f695be95a758145657cafacff00ef29c8113c74e7276479843dfc8986e5332660095c7328d219bb88bbb95e8637379515a8f9eefdb4e501932d725f60d6a211207a654efda0c27981d302a8165991048d5ced032cc6a3cf4a241644d90cd5d4a56de12c08078cdef6576de5983bcd54b3ee12b66c597cbea2643395855bef733e09552fe9788ee31102a7f0408306a58111491d8e63a054566857d1a984e81c182e6f0a21ad1404626d82df1c1c9ff119aa72b5e0ec7ee1488cb01a946ef8a3d88d5a861557eadc4f8ba57ba9fc4b93af143fd2cf53c966821d17be1cca6fb208b5176f283c0bc04433567fb534e8626021f82280b9b6276ea1b6a56b1d2e57317dff000eae88416e0399e3083f97d481939258cad6e4c1eac6471a5a2722592d3e22dc116b47d2082024df89be6d5d5abb9a441302e5c18e15da586e2e27e5df34777c4d6252b6739c994c62abccc680aab81e6bbef3fbd7f6bca1c7573112c7a50fc66b80a3fefa2c1c6baab246fad0d728bcd0dbf3fc431d4c5a954ebe0db3e3a1fc11d2e7da0716b2b13a152a21b1ca8aa55b5e7a932d32be4d057c1c7b43174d680a6f9ce799547b72b50f48bada30951e7873c4c90dc8d05f6f4f33005c41862b7bfccb927792e541cf80f83f7cba71a182b940a3eabee59c351bae421d0e54b8d9bdbd2f6f6ae07758fb50b821f5b23ec642341e9d6d67097dddfe652b53c531300b75970749d4c041aaa1e9da5b063369f98d3eed16bf4330de37f61163b84f5bff007d16a0dfd1852e953baeb15ef1295118628e70e86a501eead1002b5bab3de98b50d182af351dbb9f3d1212d2272ae13d35f1d036820d68e63b3d7ccb4e9776ff00532ff4a8e3ec27ab0033b267f83f3f5f5cd27f0a8f091164f24ee5e92eee108a0b1d3b0eef7453b098753c3015ce7d2ce8c27b2f6767b9e2174c64f5ee6be606b2d353fe07e659590f6aeb5a7184c1afa582c9626a333b54fd8358822d52ebb1cbbfcc3a378ccdc68e7a78e060d90ab97ffb32a5cf42b4f31b314bef864fe65c1c9644897068402d5e22f9f7c8dbeedcbfa6fa7815943f27d0b52ee34dde19dafb044354c8a6b179cf44b20a954e27775b71c0c06591c3386efcbd6612754b9757bf469a9f601d939256edaffe0260cb6ec7db1a7d37f4b3a635543acaa11cd1a7ec172c8b06c653c89d9c75a278bf5dbeee7788530657e1531455b5f565b9311e1e834dc26da18e8a69ec92e9917f47597ae39bd92c2ba31299d92a3567e7ec20f88d46d4e0f8da7b4be3a2d116f0e95b42461e10e9ebd5f5b7f39977d1590d4c15688b2dca97d6a5a53f4dc1d8c2da2b32e4bbe98a4748982aeb9fb081db477629bc79398aba1d405cb6072631a358e07fde9e3f75f46622bf4ba0a6099cafd1ce988cba5eb5777608ac462cf4b894b00ec18cbdbec2624ba08b60e1c1cd332dd2a5665bf3512ca753b03764b3b0669d0cc717517b309cb8813eb1961c448916610bbd7404baa664e9ed2f8afc45c2fbab306d5f731d5ea623cfd87a0e582bb3f2bfa28ade07a7aeca96be3a754c6bd54f07aaf4ea58cba5af4b92de2668d4f4aa0da7132e7894431b1e3bc30d4c3a9a7e21e2a69597b57739b3269fb0f72193e0ee8f0fd0201f0ea3379c9de5d2ae87bb814ced0d0faebfe42843193d20e961b6043a5867f0004e747c551888db2e1ae7e7f9854d12af4a710a2126a3c19a1e5639510ff0507094b07b7d87e67b36c357efcca9b0f2a0c41caf1eaec964d4b9cc583921bbd7f3cab79fcf007a03049cf4d8be2cb1286e5b31102c78e04c0d4c1a8090b857114e6dca562502ee3bf0f0e7096302225ab412cbd9c83def4976bc43d1e289fd0f2fd883706ab71630bf274e66381530270aca1d12e59e7a449568fcc276344fa4efd4af8bd5bfb6783d4626fdbb691952b7c5ebd03842c2d7e3d76a470d4f04a62e86bc4c87f258d2efa21c3ad2d25684f7d7c2106044355443b79e9dd88decdbf621ab3cc395e6b19c573c7acc01aa653f74ba98aa486d9145dd55cbe871153395ed086f251b75a8a194d12cd795de63d41f06a57328fc27bad4454457906d671a7a15d1b55011995f17befd408f5c7459390731c53d8212426d1dfb7d8ae197594b1ee43604b099f326a557c40b5e98179f996cd28268b5349bedc17f7d40d9675d5d0b1ce0ff0070f84c1a9e18c1c060acb396fbad0ec91a0346b0be917fa318fabccc34189c0d31f50819a0c96e3742b887ae4d2763bc552adaf2fd8d87cb586dac1f60fcc4b153149994ab95188be31494bf88bb08dc7c26a335bc2645be85a8056e5b4702fbd40110c0b6f22774043c1c743a980e1e97006372c04bf594b01ed0ed5c215a0be704bb8f27d8e819d68a461287384c9ff94fbc28746aa2a0e353c45d0bae6e18abbaa536abcc5cfd0a004f64abf94d8de5d4cdfc490e522d435497654ec3fdcc198b143f9956e4961e0f57b24bda38b2dd16ca3b399ccdf9bfe4c1fd9502e2af25ce68d7698070c05dd83e2119254b15866f9ca5ea9b3cdca2e2cdc00c138f13702d44d530dd6c8a254b19812202662bb877bd59ff00239f7cce652a566109e81b5c7d96f5074a94f931eded306dcdf91c22f68aae34d36e65d6ee094b64a7bbb7e3a2db712963dfda0f277882653a37d15c43bb013ce143dddfb4696eef80f983fd1e4124c7e6602615f48c6cd786aaf060bf657f238f61ff0072a16c02b231f7d1214ccbea6e3c91156731330f6815ef2e6255a03615a1cc3de8bca03163798d3e2331a70afa41ee4d50b8e1d17000797f6101979a344b8d33d454950097bd2a8aac9c517195c72fd95a28a339b4cff07e630e66233d050672e46e0a97183b41943d9895b58fa234302cd72774c4587b00ca56f9058f880fb01444799b2621816fccce6a2762609913b8ab01a0ec36aeebdfecb0da19bed7bf8e7fb98c4c32611e9c976c186ee0db312c070ed1735f28172fac3167bb987b7788ede814c42336ae8d0c13187bc393c41426ae6283880b0bb76981b022ae069e4afb2ea5451178b37e2b6bdae3a6a2d6a170801a85b54b9514e7ea7dcdb88fcd71c531c208947f128040592a52154b95077185cb93390d12b576799973c687d974b0ba5748d91151de333e56d258948a370f8c34250d746f249980af48cb71f49d078e9bbad743ada008977302c8b86319a677331532e70c4db90a5f2ed77e6f5afb32a9040ac2e9bb17c76db70fc6e88107e49ff5103855d710db07bc5917d0aed2ac52085ee5f5c0b0b3a831675d2e725447c6353f2e1a858f4400d5bc0679de2ebecccf68faf92cac171aca16c6efc74469899619aa47d65d03c12ce2bcc991db47810a5102e276d11da574d46d338b98fee85d02dd78885de7b25c2ff2fb35eef17734a64ad61719bc72cd11bb89b046cb9f58d830219b26dd4d52006a8866074a9529edd0b2e5810bf333433d858c691abca9bafb3aa0522ba34d0b2e906acbc965d982e02a5859b974349dac1fa0415a705e9ad4ca2317b4aba829a9644b49620779562250de699e1a3e3ecf1b45908a2baf3805781d401181b8953904278989888e997ef185b84aed1e5a45350a3538706c102c048082cef9dd6f03c5d525c73bbe5cbf67f630113fbda16d256bff00687879f5b07a742146599cac03dba298741bc50e662726e70d965330dee7713002ec56392f0699854abd70017acd1d8f43ed1657261c11f40d0da9a144672a18a3ef11695f14e85ece855137082eaf5f5b544020e36cb861c500e3b0b335b3238aabc782615a8a30783ed3d0e4abd40727f772e9d5081d8c9fc2b9c1a08f4911d136fa2fa09f8a9d00dab28e176029c1d37c3ac6e0778c1f0b4e5dc789b6b818f65fb5f191f4ac0503a25a538cbbcf2892d58c337947c3da5ac8bf570c2d9acd60bf129d4eeafc0b793dabd9e682442f03c3468f51f9a712e86c5696a93bb5f02e1ecb3aa429b4bb25358c8a67c4d0da874b6f158ae3def894d45370ab719168ce5daad942f7c204a0771ff8a9dabbbfff006cbfffda000c03010002000300000010f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cffc0ae93edbcf3cf3cf3cf3cf3cf3cf37e3df3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cee10d0fd2b6b23bf3cf3cf3cf3cf3cf34030438e79f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3b66af9e1d8d847d9f3cf3cf3cf3cf3c8153d2af8280ff00cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3d611c95047440e1e2f6f3cf3cf3ce464b0683f41b8765cf3cf3cf3cf3cf3cf3cf3cf3cf3dbb68e945009cf48456696f3cf3cf386ec92789777447cb2057cf3cf3cf3cf3cf3cf3cf3cf165b3fd0852c15eac9688cbd3cf3cf3b76c40bf3ff00bdc4238d37cf3cf3cf3cf3cf3cf3cf3cf0791a4a1ead9fedbd20316e7bcf3cf369c010fb7a19f71ea0d4dff3cf3cf3cf3cf3cf3cf3cf03f437334887ff00ada206f8a47cf3cf356e516ccedcaf8362fa1b0f3cf3cf3cf3cf3cf3cf3cf25c61f38cb6fdfc930d49d8d3cf3cf1bf829ba33fca4eb434a924dbcf3cf3cf3cf3cf3cf3cf28f50ad82577ee400fa52d373fdfbcba203a8eff00fed22bd98fa0a7bcf3cf3cf3cf3cf3cf3cf3c761dbab6bc7ee356fd06e56c176ba83d02eaff18eb10d7c23bff3cf3cf3cf3cf3cf3cf3cf3cbde2b8fbe3e0b3300f0ffa8e42fb1ce62e142d5dbb2dfdb127df3cf3cf3cf3cf3cf3cf3cf3cf3ed25581d3b14f73414b801d7cfb45f9dbcd2698a76dd7a77cf3cf3cf3cf3cf3cf3cf3cf3cf2e8ce99cafaaec0daa187b454ef9ee09cf8f64ca3d837fef3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf1df9fb7f55d1c87b17cafcef7c2dcd74be441a9b5f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3c5de36a8422976b38f0667900c557ce7bcf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf88e8b1e8a9b250121de9c415b4f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3c632567f85716b00bfbbe81cb0b1f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3c717a7b2ae83407debaae5aaa729f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf212fbc0f6e7840823b3b9adff008f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3dbdff004f0ed08149435ca605ccf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf2ddf671de301701811e2fe555df3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3bc63482c07f8d57e681438b77f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3ca07cc0542510c9dd2d36df3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3ce80fd6b8d82b67192257ef3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf4ed7a5af2a2fb67e883ef3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf794ea5ed07d17dec1cb5f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf90cab769713d68b57735f3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf095d43215041934ec09cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3d6f3f8eeddbd0aea47cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf26dc3092db8bd67fcf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3f10f43fd957bcf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cf3cffc4002b1101000201020503040301010100000000010011213141105161719181a1b14050d1f020c1e130f180ffda0008010301013f10fbf0b6a68e5c76441f509d123b45406d7fdf2e822b42f526993d9fa9505b1df963164328801c2ccc693650f175ff00a5c9e5d0e73058bdfcc66db30a3306ba0d1226fa96b79dfc5d8e98a6ecaaee98f0dfd17e985b8633a428250510af3391164148a759710610bff95ba5f36c7e7e3bc52f6b30c25560ccdb5a2be5311e9344c31613f31a3d7973efafd198ade3b6b809940415c10944a98a6e2aa8d64d7f9b87a096a29dcbb7bfe23330c621b6a68e803829693cc289ae904ec0d1e675ea794ceb6fd164f7962dc104b38223980688b2f80bcc448162554cc753f96919aff73fe6de653b65f4262178d00eaa6f4c1a92bd4da81170f432465cb750db71fbb7d0e07947b129817354cac83978eb1c520dd7856bfc929e44edbf10a0005c6884331e06c8eb51c364621aca4dcb93e89a79d3c7d0a557821506e4511b61a3f812e2254bc4195dd3f8a6c03ddfd2320649b385442e1e0e2ab4c1b84b50bab899225841865c7d0f73e2f5aebf40ea22eac835162998af8234e1a4d2164c3a954a66f16bfc4eae61f112db95c71ba40c334482370408752c250d41b22434c29361a7f334f259d5afa0efa94620ed23b30929844d2135897344c2704d11c45ccb59fe2807a58bf1fdcd1a5ce758ae9341081ab2329acb1ae4bcd4680462259ea3223e201341ff00be93acb8c1691aa2b544fe0cbae1ad90d66779c3076bb1ac00007f4d0ca3f3d5f827bc4fbb8efb1fef48c3647168d9a561cdf503d6a28a61b853cda384299ca086f10b83611b635660e020f687b7151cf88001b7d066f6781aceb06ce0f033085c39a58cbb11b1fca2bc1ab0aabbebee2fdf6ef072f2105eabab5eb17a3a057bb98dc018d875b778956c25303ba9cd22868b5015f788d53c943b0a7de28f173d1df99d6640e0661cba8644238b62342329ebbaf7dbdfe3e8705bcc1a6592c8e550219957c4f184b4bace91293a732f2d0f78788a3a8fbc45543260dd6a65d9ccc3cb9f69449344b1e86081ea776133f185d9e78d652b17aad7a3550cb8392f67a06fe61d7179202737c7fc8b419bb8773a3ecf78883881835c105c4198608c032b5e5046e657aff9feeff4597c90a19926ac8870c67646c625b744096c86a9cde47bc2e869a861deb2bde2342fa248cc80ce0f42f35ef3dc61a7994aa55bcae3dc87801beaf7b0807a610364397132837e2202ac8c4e95b7789a889160d9e9d1dbf137dc61e6b23e236b8a6ef8788a8c93c0615afee7a41358d5d8e87e757a69f49d0d15758830c12b982674410a5e77af23d6553b85748052ec6e214cb01034256cb2f216742f1eb97b4cb85f34bf1ef1f22aeabbff004476eea11a347120a95234c1cde81fbd6377560694bcac5af5863e9a429dd4e8e9e1bf31e663441921b9518d41e4573fe39fc4a41ddddeff004cb5e8c741985da51a91ba108cf7fd36fcc5035813b4ace9817cf95f588aaaaf57e2e0e11595dfb5c1b844351c1416e93281ea18fdec41e96b46047c001df2fef4828d910a6d6cef45fbc14355ea327bcbc9526194c097948c83a73dbce9131adc9a1df9fc77808141f5953edfd2e01361994fdcc9e9191bd4617a6df88afa8e62845e80f2caf82e51c8fdd7943cfae9ed57c149a18789410034744efb311a06ba377df6af9811aa806e969eb9dab7b8b6ff0047ac5ecfd73f370050a3eb992d81fa76e38c85816bb1158f23affb2c20b8a8801745f047e96b2b0768047d0b21b5a9d47acbc2a73b2bf3ed3382995e6cef9579952e2c4afcd09e3ff7ec36d7f965f23159c7e72c4d1282798c70a7efa243c26f08e6685e11ece23c0c6c858fe3d62ce0580e5cefaff05459b4085c5402f6329061102d63e70fd86c3e0dd753fc5836591eda43f98f0ca6a5e48ae10c29b9b5c0a011d8cd1b8350e15c344066b97a450bb663bea95284e83e3ec28235483d9c3eccb86e3a2975163c15f51dbd49ab2101f3c33088710b28aca4c70858b9aa658daddda5cabc594781d7d6a72981cc4d939ca1358af7afb115ce529ee61f73804da00d5e49cfac30aacc3a9ad7b42325b30ab300a085cc4402b02321b196028db36758acea83c287e4577acff510d64ffe7d899769e0703b5d3dd61ba596f5ceced12cb0b21cdcbfa9927382cc6d033b92d46f50376191bea2fb5f089accac2e2d2aba567da229873d1da9e8c6afac1775db4f78e73e67758bde0d7d3fdaaecfd8ae4d4f261ef8c9d427712d66d6e74998bd0ecede650528a8d0c5a83296dc06ee05bd57f1fdc146c95ee01eae3fb8e87555b97b72ed2cad6df9679c0e96c204711be839e97fdbb13702f63ec6e06c56da3b9752fd7b32ad24b7eb2e194085de4b9f10b9deb1f94ccf778e0ff7d26396287c273eb0214dc0a6ba0e57db9c2f68003a1823652c58997504b77fd7dbe7a4718dcbf75f5fb2504b209b4aea761dbb3af4872ebe0a9a656c42b87223513068f2e9da055b640b012ce84a7baf37fad59dc58c0f1abed2c2539183c10533afd9a61b25d1a39ea1dff33b5d587be904a04e9113d2b1718e71baba3c4aa008e47fe10c683a50fb46eb5d5b9643ecc1d0953316a6a19d9489297e66ae7963de596c21c9326780efecc2a18b51ee3b95c52908c208b135becdbae016124a22ee28a9519a92f57d9c5564cb70658ca328d115510654cf0db7da2c6748225902e900c18952a2185210542de7ed3a4456b8966f3ab11de3b117b13507ff00843fffc4002b1101000201020405050101010100000000010011213141105161718191a1b1d1204050c1f0e13080f1ffda0008010201013f10fcf5f3be869ddd0f188616ce6bae9463be7a6b1cc93a03dc76e9f107b77f87dbfec5b836e2cf91b6de37b41460347238c1af36a91da8b9fe9d1ee7fdf5554c001e24d4c3f737d44006eb065cb34ed8dc564e4639a92a141a000f22367119b58886b65b24ecd17941fef38832dbc4efe3a7cffd2f8b3ca7253d623b5b65d33032526e441f10d7cfdf5e99bb26bb861feed7f6db00655c0eed3aec52be0ca816e332f6cb58d87305a690d5b604a152bd9302872d94e8a2532805853e32c5ce65faee7f72ff00958a5f36c7cfb47c96b31065259ba89352749cc6c09b91117a39ba3d7dfbebf677dac56e5d5d6cbc977a0de74824e3cb9baaf37ff9a450d65a45c9883e408bca5bca0240e595cae079d21072b93f0e8f9ed1c1d2611d47eb449412fd57a980e58c0104b5c0a31c063454148a5a21d4b3a3ccf9f7d79fd90b4872cd8f1dd706aef543156a2a0e18f9565c1184ef812a5980288c301d48845cc1e3431c395c5cfe46ff00333defeb75a1f5654b65b4260dbc1f5a4e906f306b2bd4de9316094999a9dba9c9dcfb1a694ab7b183e7c586eccc15432c8c59a732557029d6134e0e4210644288aa85286b3542abe81a7c766224293e9c1eac3e65222d482cc68d0f8c591d6a28ec9467594aa5cfc4269e7a797d8f2ba0f23c3da2ca0cbd8728e624c359d1113e850cb0aa38839a03784911c1b1f1d7d6ff00b07d09b007bc75d8dd23462a00cce3233a6546e1242b0e25cb2586cd65c1ae8f73e75f1fb06b1769cf6251a82b20cb2d99a2666e598ef897652b4415b84101758e2e1333339f06a7efe9767a11b5c52946c92b8cbd8848b8616384a14a30c260353440d054d2f79f3a77afb0a185baf0722fa9d6b9dc6dc2313c9a4b386d0f6e4dd32441e6e389673278024082a2d6649021ea9f23fdfa5c0dd17dbf734a96398b351541992ce10ce589c106da340c1313c1b5408c40068ffdce80f24d9f069ed7370c8ca8504bbc8822188f2896aa6b15114b71b92620462ecc34b0ac51d0d34f7a3ed5edb4505b3189f0cfb41ee7fbac6ca57ca2622fde286165f0f138f596601b8d398380410b866628b9a0500fb002dbd76dde457718a6a9938a90883c3658e710c2d887ed1c83589bcc15718a0bdd3cdda069052bbb97afc467a0736096b0b862b781a88b1a91a948769563485326700b85c04c6705cb11b70aeae32f8f5fb142adb00dceda29a9058ad022688e47c66e365b4e22155c42a581a91d711619aaadc2f443563cf091e4d5f48e571741e92dbb65b0ede933807ac402abc881871a8a62171af30c624d668354da24430634209c19492a98625c2c76b086f65eff00e7d930d0373e2a9fe6a72229bf09f2211e57660918e01454d1146b9433c756c2e4734f220d5bae6fb6c45d479ff91d380e444b5544b57ae60503ca67dcb05b104c54a6003109b5434236008513546133103a26b3c0c85ac0ed9e7f8f9fb3146c8401b13068c2be47b2197aa007292d7311f3e2180db12af26737c3de35bddf9cc00a9908d4bb835b3a102d7a352c5b255de256bc48cce5c9a4e902950d1bc622f598a039c08d21121aae7f8e72affcbdfed88545a16b3b3cba367297ced8a7999f46bbeb3d7e9af7a944507a3e62a0f17a2ebb6af86b2c05804d177654328914a469111442b970d386b062ea20a759596af07df59aa6e80284ae04b7446a9539ede72812dcb6ff00600a147de3f35f7e16e92826f98500478654c628d41b5db8100f48741c44bd577e20d574f28ab7ef7e62361e39f7b800a0c7df557afbe65638508de21b1b224ef28f998001a406eedc0491771154b93892ee45fc0d4f39ed2f12822200da58cd1208a0a03696aba83c9fd0671158e22a98164ef23f0265cbc28f7b440b497e749ac105b2a2b7e8210b9a515054b3a4ff00023d123ac4348dcd2a16e76e0317031c88a9689b9722b46a8d6245031a4a92294bf0889176368a7a371a4cd8ab16a1666d1e63d671cf81c814e1773aa1ab63f057837c3fa8d47311eb1394a5a94618d8225c6542d9a70414c311b4d220801a24bb070bd72c6fc1573c1d3bcbec37ef0437099a12eb22184cc734a8237ab8e054da3096949b11f7e00622d231ef0007e0f23e1d7bf3fede0595c0035cc4c3cb1da409a9a99e5bc15c644fd203f711b519a0955accda170ed020d43f097b539b2663a7de585c4588a94b0e32acc5784b32928de5cced11436f225752f9625b0daf9d45fe16e5e49f1fb8c70082b51a7ac1ae084234cd401de0e96ae9f3a44508e6e5f8f48a1db33b0aa5d3a7d0afee7afe154e725f2cfea692216e55b994a2688fce2f57e6f0205950cd23b188794650403fd8bdff00b963f0a80a749b453e9a9e950c5b1c7247e00cd650a594d31cdc32c850eb2172dfe83f0cc87a63cd87d6bc8e049d2c12d6382845b2120e59bc532dc2c21459b431ae5a80036fc3093614ccb0d68f32649821713bc03a4bb28754b5d9c1cad151b07ab9fe20eee8b6be63fae4f760974308f06d8019886c5f6816c60f571ba72b0050734eaf73639de7a6f058d014763f13e148309aefcb3a3888d99f37938f59596874cfb4c67a49ef16c57ba12adf5c7d31eb328bd807cfea0544699d5c757ff087ffc4002d1001000202010303020603010101000000010011213141516171108191a1c1205060b1d1f03040e1f17090ffda0008010100013f10ff00ec8291400da0dd295a69f8657e4af434abe1d1e735a689ce0b15cac28adb379ad3cd08a0150d0415974a6dd3786eb1757a006550704a30c9619e8d36ec8650163346eaebd1ad8f1c85855be889e47f4d8b6b1c5297da05e5b6687e332e3d946178dd5f329750695e446ab372d0986bbc120cd0e46aae9e63262201195ecdaef37c9c0118cb9a0adcef6305f264cffbc899c016aba0224d50d958bc85bd1df4a7a3040810d420dd8adf3bd9825b5f09213658614a5aa285bb3e9f86916a5a000e6196d221204cf0a11e447f34097b5a72a083a32c6f363551bec365c1cadac18d4360c5a4abbd7de1b501437a658a355bb86324ec53ab256a18cd562be22182295eb32add1a80ecbae8e7559dea8c76e2e52cae8991ff006b4c0abd42bb5feb863994b88072adb7d2c480555008d5006de682f6de6e165d2fa46c5ddf30500717a97a9fba1072b83b5a76aece1a08963847502e476c71c303b9118715b935620096a2bacafcbea7c255b406d41ae9e52138b915a61c1d556f7bd6a12f6aa2a9b0e1abc7fec37755e42cc11ac60ac36c6dad0d6e5c405cc9ab4714be200cb99772e812ccd62862fe95e26f20760160f4f111a32dba9febd8c02c931780ed1c99a8050bc919b4dddf4c43021001a7f4891574ed2f4e9e60c300957d20841d4b203476c130148f1312c722162b48360574e7b04a5e0ed0c4a959002c82b02178cda5a0c82d403a83f2dbe764209c3578291d8de4c6494b620aa106addbcee13ed13ed23c4ee713048ff0b547f7b4aeac9e55c4425b778c0403e5dee662ee5985468f694388fde651a962cb84810e0d8915d5b70a5d4d293a7faadbe45e285a0614a2f2553ac12d094d04f22abbe618a0c1d20f07c44f16f306a20ca9b820970179aa9aae5ac22f2c1d710134ea5a9bc4b35bb46d2b7baa8405e121a5d2da112b180800192987f2b3e396748f5a237db8e73a3ce371d18c4128b78699f020620f234bc06e25b3e88ac2be52e9f7c04249de604dacc612d443d1c09999155914aec87688a44a4d8ffa6972185b842e385c02f46a0fde0e00140070054b36938b28ae1ab4dccb9af78a0a4a8874e18180c379dc0bca41199764553032a6ff00ae38833948257f586d32c34a371ec00e1ee058ba2025620b3f282cdfc3745d5bd0c99712963828cc0a51545b7270725b131e96956ea25828956aa602b1ce920b46a7250b05c23f031421a8a8b55f43ea0d60c1e62a283587f881d16e78b9404cc3b4ca080b0ad983cbbfd9ff0046993757a8b0b5d05a5ad07315882aa1d381440500bd5750ceae7500dc011700063cc25514ca7cc7e50eaa94352a560c6639ce6059e7d61aa96222f0cb80ec14d2070893aab8828a32ae62f040a25ad74d13282e72f30b00b781d0913c10035cadf92ab5549e86d765d02d76850d13423297acaf4dfe0d04a2cc21c912bccba972cb9c422e72c62065129c41d6a1582bcc3387b92d5bbccd6584ada01d565b0ed4aa657ac46aa2c4e42882b5b20f783bd2d303b843aace912c226755701ff382f42c728b7c4527569a1712bc02981906b5b616802b84ed85f425547082acbcb778c41305ae0b6e6930099e095924d42026a177028ad3df11489dae5aef55b622177bbe61a0f625a06c7ac22a093952c97e88852a03d0d08a15541144086380632c531636d71401f926cd315d3ca783fbb9c1ad82dd0106868dd5f56527aeda5fb100d46bcc7872f217172e6ac7103b2c72c559034f2412cfa600631d0353c6063d693a32961700757c4bedadba948afa409ee555695cd40385384a7a476dadc05c5c0a0ae13abd7b0747fccdab834ceb4342d16a0b2d0b609294e81068ad5f03ace504a494dc0b0c119e68bee73089acc2387c4460b966f70aadb2f313aecbd27d12e186b18494b0a6a50fbcb8302f104dc698a44ab9e603333d39ecc1f5d52926b36092e86d3a718dbf5aac0a3489635f91d4d9f5c45c5df39262ab3a9766a098efd11c5a8fca5f2519398301616063a40cca30d590a092a50cf3d3de1294652b5aa37131701610acde2fd56a9963228ba6076a7ec53eff0011ad54bb067b79c9f32d2ad94ec38859d4b88305762d5c84016744be4730cd06a6d63c89edfe43ad070834006d5e22e5d2d900b48badb9553835714fb6b96298443e86a12cb251da0d1d2395a672da648b7de3d8adf524b7b444a36461b412a4a66b38c213efd6438990851947689b0473894d533008315090140cac3202154cb5171fc88a0843f7ca7e39edb05a567a4501b97df81c198f6998dd456bd8dc3e3ead60feea5ed050b3516b42ecd9ef169371b5a0d67b439a81dcdd27dff02dc750d661f3114af4ff00b50219c0baf1adea3c4e56d93acbb6e562f0955c16e6247a66fa2c7b5aff009178b62c80129d51454c14b816871cb77e3311504777f88a644f312972f304cac5f4fbcaec3ea8d295759659cfe0ad9170f3105c65e269549d270125ca798b2deb9944362faabff6305aaf14ef11d2487cb51675feed56044b14b1bef0472840a5c54142828588f3f9093c2992b559fef89556482ad9d44039a542ed1ef1b85a2094b32cc9e90eb52f4b8244eede1a0f6d71de346e24fb4605cacb68305e63b465ef16d566fd57d352842e093799752f6d08de035b8ee9467be00e1c5e2fbb100c6c693dbfc772081da69001083395cb2a0e9e023fdf2c6388ed509ca4adb4a8282621db9a8388e61990382a22003549a8b70dfe0c1b83ace09bf595a0b13701e04561d2079d0f785ceb02dcd408c2670dc0f791686c23b42d7de5ea2380752745301c668de9f9052aac05aa5e7a637afdf6e1230e64f799839bda4a26a6e3ea588e8e6de651162675f80acc0135f1dd8ac2b6c4429469ae232ca8317f1a5972dacccd711bb514a5c6b6632dc044413c446044b514062aeab46aef0f15fe2a10012a7292223828052a0ee3a3b05b7cf3145a635c30985a1c730a0051d2130c52c51ac447f7408a96b225f8fef58f9e35659a8669416420d16c9f505402d7820f74d4d38e669ef29a56d4c662618e157c46a58ce09445d4204a774ec9721bac5cb5d5b5e599ac3d0ca9131448588962e4ccb1c69a05a14c5ab28bc5d393f20b10b32a14555e2f5b8a1655694f022aec22cb7b39224b480a812a67f720c09d499a55d55b2865020654b506bde58af2fe05f56d29fa312923de78806e4348ec611663b19ee7a567d2803cc6e556816956589fdd74bff120d0b7055076d1be2526d6a20731bbac1c4396a5585d7796f0078f681b6b1c5c1642df797294b80dbb3b55d5bb88a45260eadbfdbeb098468545ca8d3c4ae9917a03d6d452e7d886b4c9465ed72fc2b3a222e415c563996d6ba6cbc730b3ec8b4432761a0a8fa316cafa3fbda5856af238859f0944d33888c6057688e5e5575894c5cb7202e05c8478ad9cacfe401140910b5b0183c12a20b812ef5d4a1bc6e9a1c3fdfde2eb3e97841511740a97179e91f048f58c8ace91261ae92a65266729645bf55a205b46e55b92f8a85fc5cab997cc2858e2bac0bc86e0bfa12a6d02715d1dd55e3de1205cc459588aaa57619d21ed395372c230b9aba786b9ac3013a451dcff000a1ed97cc03295d6dbbe987b5a3ac5eb1ab0728912056f074f31141e8739888130d60c5cc1aa5aef8961e93977062f1c16e2139617cc0a288375d3d04334046b39bf8fa4b52517c200d703c43d073e212eb1e174e7db705f8b36537e7fbc4772588207371f6dde7714b191801ca4a55046a5ccc6c3db349348b55fde84cae671640b292c5c90004d6b4801e8a1cd39c8367fbe1a9aca616d10a1cdb8bcdddac342c5dc146d625e93161b8806534ac1d21b5c0d99fa44159b11000f78953046944381170166b297dee03532d731c3ea55b6f746444a79bfc0b72acab08c3831d2293882de3ccd4595e22ba3b2cb0c7684060d13bab51073447a47de16fa3b452570c451bc90f365840b6155632aa50671972e76fcff0086de45b2a8d8450e00c9c1757605d1314ebb45dc09862f2e69946531cb3355a9484e77d67c4ca66b2d38e2f1d3bc3552e189c9e974db3158cab7d2e8166d5d2cf3da14366cf66ee58e4d3cc1b7ce63147911c5bfd660a146ae3a480e1b350259712e0d97b38840ac6986323676897149b3ccbdf647f1c438ec153fbe235528bc8f58b962bc102ed050f00b55cb9ff79e20a05d861a31c85ab0614b36388d9b9ef00aed9ccd701a41fceeb0cde08108b5775729216348ff007ccb5860c9157a16ef0afeb18601d2587118e155eabd1961eb0cc60cc1c8b75a806868e36d9bfda1e62556e7247786004b5e37d32d4c8c77cb72f80965ca9394c26ae05425eef40e0840c468b0b2e72ff8689c78cef8a3871c78a6909b7f12a8459a39836d5ca008d7796342e5db530f4e1283d88ad997a11eb0d4e5cba9fb7fe4bfd0387684c158a2d89990b5db9b962edb66374b08b04d622361f32a04b23011e95a866bb48841a46c6235154d38738ef09ed19ea5c288c9d6398d80eee0c0ee81b99cf7a7fde3063e58c534d94b605a691a485d1f173b4d3b9dc44376782b3d59bda577964b364155afde8c3c13bcb9b4d177894f46cd45710d3919c131955316bd6adbde2031040bbff28b50015c0f1fdf12f9586db07ce7bfd0966066ddab5777aafde6f1c1ae60eea5d5465a48291a5c390335d6aa3b4c675366dfe27382eb506b222ecc0335c4fd982b3a85d3309435421abf40971f7424800c247685ff0071da1e86bc294040a11de9b70742022b822dbb8cb30881814f46337ce59856e219f647b45588c329b2ab30b0b9e158be5e1922943844c54e56f845b20038e03a2eff00dfbcd9235819b96c25b9b62c25ee296291252a411d29c47696aaa28b43d9545c9dd6af72d64439eb0b152c9ad4bd7ac5bf4544cd2841b950cc5af3a765e3550dd131b7a147d3d2a1431d2c554c08c3b30ef5bb799a909861166451d963fc79d8b9b09ab361070d50e168c4f0c6dc9190dbed2ea5cb9a5dc642f05c70622eaaa38041b1fc742d131c76861c250b0aef0b1b2f2c5cd3b06e2bfd812551ca15b6a13b978b6d030b2a0a45200832c0006494100020e02ad5bbff007d46ba1cd2cd436e5c3841bb878582946444d8c1f4841150e91d1a8b427e0550ba94c648695f5d660cdce1e89546e166a5175e52100d5116d666671cc2ba3092f4172e1564b6ea0f914fbbd6c6a08c582e838b4cf7855d438d628d070136d0889637355a454146554b0a2a8b01b2e8564cba41bc18298c30275864b5db04f312d2b312a464e16ec6c4a360046115a05b600a14b86cba7609a703845b64050a5c365d3b0420427641e0682588ab1620abb3bc2c18305885b5b477eac71b2a5d26a0dec2f35158601b1f7fc37441186adc42c6c97e087620b4b5cc54543f163a30700c9cc1dc48cc4a86e4993b5a8eea040482269377dbf21262901b5050550da56b2aa301020822a8604cd665b21a835322bd228a7998a371783bc7a0b3d212c64427635a98643a348ec52e1f6352be09a61f1305fb2302774439a865890a740397b40eec590038b25392ae2f75347eaee55de9e3addbaacd7859152aa95861a46edbb3108f2aca429ae5555e55973c5d4afbe0fac6436676fe0947d0ca9ac79c6b89675142455c5f683161d0cc28cec601b945c60ba8679260b6877980567292a5200368f8bc47b0976f696b95b80c297a3cfe00b40db3b2471c9f319a987b4128d3880d32605bde5d160229045004b1970487623b9b2a6b88b1221475acd39014b5ad323f90988916acc4ec021bcf108b9a8c2e5d532d106b4ec97c225c0acc63e7d2a70b4de223df5a7d16218e70c532b7bcce9af2c3fbfc44d510ef4a8334063138d15cf596d48693d822b68d022ded9083c813808359439097658e570bdaf24e397167eed476afd083f9ae616cb692df862fc26fa2600a607f6daa70c41b2f06e28db3387750f96a16662f81336af4b2475dabd589b2b035700f3774399575d595e65efb43424a6bbc75936b6de3d6a59b05ae3c410b44bfc5bc47704c875702d2ee879301f14f4f4260dc20bec160618226e1181480da9a74d2ebf2207545d001aca11b5f2370149464cc71576711c08eb92a3dccbaef2c02fa256cc1da28f20a8b5289d388b563b31e8a416b8a8b620a6b9577fdef014d94b0dc3594f7318861b4706c38978b23c3879ac444afe88414af56002828e84ce0c45020f74a122ef1706cea476d8cb0e20517036512890c51cfb7f58dc1b9b605b09ec8fa8ebbb80e5e08bc0b3841f848feb71a43da5281307f8588b16e11e35fe258a8e7920dddd06159835ff504c912a44e6235b42c90e7bce735d972d73066097986e96c6254552d604bd0882f50a16a85ac4f1312a512f1724b38832c45b297d10a975440082961194628cdb236fc8b44f790cd9723fc2520c231974641a0584025a2eaea5b330b20dd5ac76b697cca440e21a02ef1538185d916215a166e59c74089614ce4d454020d23029978a7110d482bc4040b886341cf98354550f6fbf3356313c246163793d3e9297d2e893a50518823c783d56bd05e6eb293ed889f7793f986b58bb9fda6608da153d5651c02893ed514015aefc134163904296b05ce487df16a2a747ac34e98540fb32d67bb1be8205f4dda663a0fd4e221a174c08b406fb730db4522a7cf98ed5585fa5f58819560e180ae2747a9d628185dc2db4d74991b8cf74cd0bbde25145621167a45d1b0b2d559b22ac7a8f69588d44e610bf4d3e994272a1aa5127072d18abba1273a710d062c06357468a003f23b33e8a89496c2321a42f1698d0cad48449856618a14bc732a8583a7994f435056a86c413b47c75ba8607655051151042c710e07552c704cc1f67961d79982b08934ff007f98eed04344b8c00d4170f189697d5d8cc0be509f2f31369c8adaf39942156bdd7fe7c798a569a4b2fb426be5974d5c410b34bc4b5aa9642fe5b38bd39fe9c0d913221f6889414696bcc01b3aaf910f029df2f99b6f305d2f68aadbde2ceaa5a9dd463e5c4dad5d0e4f2fda5c2dd4d2f762354ed6d1f01471548841a7da2f4e4e6a00ec23fa9de7084c7c0f72688ad1ebca227120a8c5fb206dc60ce4982a370f3950464bcebb45240d0bda2f15055a99f89421874fde5dee8b7728d7e1369b5d69d814345d0a28b2f64765138ea0a0e940a816e68d7e4c1920f09e816702d56d29cad1c788b982a2716ecdca860cec99d38620b0d5372b8a8be23315748b2838c9d3bcaeba86a10aa4c0cce34b45e3ac1d48f4635888f08c29bf34f98e0bf39453a15f8b3920f96a0482bccaaabc6b7df339540732b84af22a399bd8ca105d637b84fb9da879c5e7ec90e87e407cd473b17ab1f5cdb5cc7e11caf7b21b81e5088acf2060e5a22ef69abfb7d595fbb417f2fe0891336a597873178cc1ee48b4b1a07afa31c3ab35dc9997adcfb18f858bbf31d89459efd434a35c45aba1da6ea87ed144caee555d83ad4642a2f37a88146f60ccbe97eb57141b4b494c0738b144e19d49e0ca0300341c7e4e1da4e026c44d23ccd95a515090a280563474dd0c42733d90476d75839657796e2cbd2ddb994c0ea2b2c3a42dc660e4386602565996c462fd587116f9539879f452b5830b5306ea58cd9de05c0f86121f3e4b401959aac28298ab716ce297598f1af48fef017256d04eec5e041466af43e0b970465bf131933d618f24672e2042ed6caaa58a91f06dfa4ec21e27d8f47d5854abdf495305a5e3d0de40364a54218798bd8f8822e158f9837c530055ef17fce259b36f12e438954bd216c7bcc25d92b1832e3be96ec94a0ca88a2da1968daa85ca1cb06d1155d5fca4e841c00d889a479815503b695a50118a357a2ad092482f501ab871be0622aab139ca63d65cc051758632a4e5d4ba81963329cf937d25cf9a6b4f923eeb8a55ef503128f4655d23114bf6089371e011576c7c72ddb71c81200234d7308101492af54196dc22c09e5a7d23b2eba88f0554f68b4ba17cc001d31294df2a0edd86d69dd36ec7ccc97ee81f406081573cb16a2dfa9d8ea0307925a27487b863145d7a3c4c114adb7f55cd9ba6727a9dbd16e04b9594ef1d50512e71a7e20b4d2768e98c719263539811440c88548d4720861865ccfaf6fdaa28151458a19928c03d5b1528bd825945145dafe595eee04da2d0b480b99710d0dc4bc18b5ab42ec2e0595cb698a150a398583537188e1c8174434ae354cc850cb501f15f240478067d05241a6cb2254bc4bf5f717fc46c7fb8f7e601555c0efa07598bebf43e4b8bc8abd6b2fac16f37ad1f231f316382f2af76e2d95f797896f0830c2ce809c1d3460bbf43b7cccd700341447f0b0ceb287b443b6f5883f5186400074856fb61eb6906c664611fc5d9f449b29c90ef5cc439b3a714fe62db96e09e112961945788cc2c135138c19ae23d7f1408b557001cb2dabf65ef5559555d17614336aa6dea5ad1ab168282f07e618500d0c150694305603415a3121db14a852af15c99c65c15df2b1aa4dc53a6d335685f1b6d5bb282eeab0740bb62e2d5478dacd63dccf88027103373f32cd509a0d0b2c2f39131d23e454b030b9ae98802f6381500ca19c970e6a9e48ecc0b8ad2943300238ca0a5298b2e98340160ceb81e8ea061b21058a5c1d0e0f894a9d6edfd7de6d0e497317ec15ab41093a9f23abec7a2d45bf532c91ed5c137d2f8c3d14b5f18f30b4a39438b194b183ee23ee389884497285de4bfb40b6047140c0a85675631bb06b7cebd781958de37f48421284991bc458a196000d2183f0962b4d0b5343740d0b5ac12cc965f78052e45b607c272ca11104a35bcb22b9abaa0fce45fa5c6bd2c4f1d2267b7d2b863279263406cdf4b735de250b95e3834f045a8f2422140b434a1df43d17f0e464b13503e9763c409fc702df6ca9325838466b2853f11e476889d02d577cd7aa07e61b72de88b11f458b064565038c6bbc7208b536ac5a8b704cb04621209e062eae5869bcaef34a40e5f56350c8bd2a956651bc52a57c825c651016b8b2fa912d6f15266815d03d19ce2f38ad9a00d4574ba6735c460e51482cd565c972a9a4a42b348f64590da734efa0707e78db81d0850260378cbb5cb96623d02fd42a014652fdb99a99693588fd08b314ca4b7d908cc9b32afa062f0061cd5ac07d5e2e007bdd794227b10532a9faa39d9dc62d45b95bd7ff22fd46300a9823407d88503666d3599bbc436c99f451a2e8ac797359d47a6a0a5bca5a8f899ff007823ede08b37e8e89416f0a38079f9af9fd0461c9bb2b6edb0a71d29559bb080330859e815e9612b603962b2ba4bb1d05f786ab2336847d66895a30654eb9f4cbe496a983e520f461b93dafd8382111124225226e502cc0516c16780bee7a02d12e11a0e44d9efd5e8050b8c188a9132c9ee1c62258d7a5e0ed67a2fe079948a22735321ee9fa089ab820d058bb0aa9a2f93580b9b54c14815e810db0201a7a9923fa5e48f641daba8ac795cbd5c88f59403b9e23a953285efe806299138d3ecb7ed0412044a9fb25801400c0eabf71fafa5904984e43d21856186254046e5a7f722e88e7baf429f597aa39043455b5576beb633430155c3f856580599b458ca1fbf56ac6bf40900949d88468c1c3142af14e9373d112d1e088c609e89d673c5e6002223913995aa05b9c187b972a88e4788a871af1167bccbbb676d6239a1adc02abc09f6897c008588e981b17193196001b57820e4215284b49dd5e123177eb71f4282d56c1d78d6bafe0336ce482ad504ccb94b317855bfa8beff00a04ceae46da0164012c6d0a72d419acdc7b2ee5410c06b68112ca9950c1bb4e1846d341e465c23876d844c28d618be72302ba80c14bbc4a29c395250c715014d875c06f10021bd2925ef6d63792cfdf35cf3aa4e4157afe05afc2c821e418ea618e65ab393afa1b515769a1329842ae2dc98f039ff009fa04d031d40362269215778545264c8597a6a92930902a04d38b8d679efe815090d02afc0e9104097d5ce1cb38e92deb3b12c6beb05fe4c7989338721e8841a46c610725addcb1cddc14153032f12ab5f4b97f8162dfa40870d65c4ba852a05ef4c849858d32836e58b936a707eff003dbf410b4238148a4d85075be78194e1f4474dc6a72b82ac6d0b8cb770047c4a9d2e68f781025d2b18fef0204d398b2f508708ea57a3f32c489e56e641b65afa59e276221c46fd75236b40666baa2b28c71307a64a053c99212719aa2f065f7abafd0543be0520d414a1cad8d0ce20127240a86d4106aad8f8c2e8db059828013971a67fec0a94260cbe6040a9a1ed6f796dec8ea229514c1716cf86125388767a9c5098ae308710535e8ca6c111e2000a1eb0c2f0dfa7b1caf18086c5cb83960fd05bb9d01c7646c505e6b0ea547026e695058426dfb286c168a49467da595499a2e641eb021ec558e6d648826ec91d6037134a10ed8094128e92e16e89921a898be8892f75a95350f48ff7a7a043ba3216ce84282df407e619059cd97463d79a5bb4c0168ef514afe83b87a8104d1470d31d3102a05ca940aa40882363c9280942af370fad564c4480cfab251c069fda58e50c01d58fa242788108441ac413e91298b54ac3946b62640470d4b2ef01da88af7817531770d6d3a0d30a3bdd7c403f6c1163000651d5958f1ff6282a57231a036e23cc5ccaab27e83c9118af758e002d4c5f0e42fd02a0a6e26680aa2fe65b58dccd994cd9301840079f0248649444ad883e86308604b2004017378617b581b614836988b729af295e08b8b97413d8e63a89c89777371df2d3d0701e8ca33442f1b628b162b20b63ef9feec14c46081949f7609aaf72e678aae8db068dbb1413752405a194f7acf17fa0f9352ff50f4553b05c82c50c1fecc2f44aa24e63e640b8105340c05884e8110ac408624a5d10c1b17274ff00a089188df72e32d4ba900800d4eba58ce7f1d626084177312244653e2c67de534fe20da6b963592b27d65d928784219bf088c105d9d30f970201d448aaac30075580a8eb4ba70e69588a184d0c5fa228778a28d1553742fb557d7f420fc0031682300a257098352b1e9c3153cf1048db5d7385d9e20545a51699964e9fc23a4e412563ad09541330a015de5a391fb42712f578d9f465d732d6158a63acaa5c0ebc12d0097783caa43dfaf5aceac6c359ef2c1861a8688f8b80a54e9da10a034fd9281db005946d5f67eb03da2ab661db7bbb6e13fbd075c67306ae281f48c7fec769376423ec9a9403654397017e6a2394da73fa108100a714a23a048d8c812c3bb805978a847360b8bae195c72e33965179aace2275278c056e0f898674ee08d9876969498788c768551dc7f88feb4fc63755dbd8dc14b64f90d423f862f431b6078942907e73e1750a0ddb573357cfb056fba43e9e8ca0a482dab02015b3062fc9c76877d573e629dacbba5806a2b76f332ef668c3004aba1de5e4d96979b69d269f3fa1409558008887445314e708e6710085a7ee236239111c90299d673059d2e19a8d75a881b5e18ab686538854391c444739ba7848005a3a5e6291afda53fb30892c4b1f566a777b060be73aff009083ed847f0c3a66962f38d34d7bd4a37c0413d28ddf39106904689b7b57cbb880c379fdc5700bacb0af30057bb258779023f942bde65a714dcb642e0ed7f895d88cd9f13e87de98e5956a5abd7f4364f6d4b4e62f194638b3a203a8ca605c4164662320a731e9152b98462c6c4d9138318e8f680ab1a487eecbf5a7fecccd78fdb88b0366a533f1e8cd235408040080e419c64eee02fe65a902b1a703e03d33399ffa0936c74d887060e1394460a6e900d9c996e5423e2ae7c5a7edfb62346ae56b3747433afd0e13fcd026c44c88f312dc8a1bb0d6ad6d58b555a329af328c5da70660c691db2f4ab2235946c712b059d2814aee8998d971c1e658881b3a5977f48a164d0ea4a9bccb7083ccb850e78ec1ca2d349d332b24800c4a3969d0f923951b843d1681f31237380516c516d3ad1eb99370340d99f4c02710eb62429161caf69b00cd3a055f42239e357125aad62dda72bcfed83cfe8ac97e5eb3b5814aac765be0a519af854055dd596007c45b61cdc5ce6735018728ea2885182ad060296432042d771843a1634c59e2fa4c65bbcdf58f436c454908014fa5d44b98c969880ac712d6f1a8fa568977deb07bc1578986879d7082597aed9ce4a21db73981ad06800028afd176cdbe8201c669c2ca5bd6487061d2ec983d7c86a1a2f6466222749c266bc4218e07f11b92f9831a1c3c42e61f4e8bf98350d600aaa6059ca83bfe4472ff0096ebf88270110e6e2dd6267ba09ed1f9fed6a185929402b7c0f76e207ab48f28ecc0507350be5db04979af9952cd2ef247b641b0d25a58082c0297e8b807b03292c790a195828ed6aa40e5c665645433132fa2625a08d5c125b37322179088214653221d2afdd2dbfb406e3d18205331b55374298d7d62f8c8094dbde36020ae171a59f002dfb4af7a029fcca8ee08cbbd18f784cb08b8ba5687de654af7a04766b730632843d8313006de08c2464e253ab9399bc4a97323a74b66963dd15fa2f4316a41a8b176154359e4d60a7aa506e0f301168958b87916c6433f0c1c19cb8c6576d302a04c173f32e7ae219af332b326a78400306a8cc5c56734f9331c257f038c4548e74b7e8c4c0be942ff001b9920d971896139be66d08da5588e552a0332183ae6e209276f585d4facf972cfe83f45100a8cd7512e8d0e4e725618198c3d61452d7585d0d76883239ccd1103aa84201f24c2af740d3b148c8fd22a2c734009476d3c3fb4be279bd1120dc2cae1be18ce6ab9c712fd579eb50c1205d830ac73e61a14b4965ea8ca510aaa91b78a9dbf1e6559ba8c45ef116b0e83fa64442f0ad8ff0045a8c402094ade8a814838588a057e25e1af88abb774980d2a144234f1069289370d8b7f873797dc15ff00b1372c6707d6674c0e798d53b5d891da281de076848695705846e026acddce2c23822d461a8d6941b19419a85d43de01af5c1318bc6c3fa2b1d6285312911a430950fc8186b40d2bb16a8002c6345858a86e16b97dc93ad4c8d06ed0fef32ff52802bde887296d29ad0fdcf45c7a3a8cd9175a94d928318f885bb1c585cce20bef8bb80f38738c66016064423b304c660b066e01dd973041ec978a852dee341aa7ace52007a2d0c0a1de4b44d3f45ae63d9d84b72652cbaa2802346001a95af5e22a0e734cc8cf152cad0c34bb1a2cc26269aa19afeb38c611d4592d85afc7c4ba02ef38e618f794baefe2240b9cd27d25d9bae906e48adb46d98df64104f7479a2f748ee0acc15d65e9488e91dd3d5b867c214dbdcb0505f1400a3f458ad1bf105d82d4268581722dcece46f9ebcf98954eb70c58a6e60a1dd96f4a75491576decdfda66b67de1a620bc127897bdcba9903be9550584be4986d3c406502763ec4779405638831c7e2351a2aa0148966c8a4c3cff788c905869e267890ab52505683805c6213f9eaca818508a36dfe8d64b8b477139c160a951abb0f9950a33da355141f30167bd4716d31d11f50c4da24a2018d2da9fda99853dad95870618150e96408da78c3352c6f1a9a4e08bb0710d469eb016768e5416ef8000a16016acd7e8e37072ed600bd6e802c44b4a304049404b11a4139251a0f05cbca2ae2eb5f5864c2dea5b08d7c4bc1bf7950d6392528a791d46962bc5cb2eaccb96d7b2c242e0852ec90ea6af7e22db17f58caec878e0c2ae202d69fb30aa8938ba0cec3010b40b761fa3a822b56140b278166a016ae20a5cb16a5a27ed15259784ed2b285933548fcf129a53fbc6bcaaf336b76c658978c6b843164acf48257bb3009027351c36bce3304a609b80d0241381f64a2821a9579aae81794a319575fa3c3f115cad387088a284451114887e29ab0d0eb09440038040688456b0c2c85b0d545f79a143bdccf50e3304c35d8620b99b5e9d425c82d6fac3420355cc0acf745496dee0b1771b1c468071538c4add81400485c61db450e900502a02dd0507e90cb0b962282ed2134a305b2c3a27a06f40b5619b5626314e9300dd1181639802d0f11d673554cb358b589e8a8bdbd514ab423dd28157955416805a87948da068a50b2e7208376450a7b727a03100501838fd27ad025562650b68c611044021535a3d564d16b4348bb22473825667116867de6cda965ae7d6e34223ac040d2055a180015583863aab605112ed0ee5165d0f4a88996bd8d5c403cd14983c2ea25a06907c87e97c650cfbb6268c4a6d32b24120ca9ad52968dabadd9269b6b0aee49a156b0039a8b9d46a101de5a12b9ba0a1467a8da857658e0b8b00e92b5338ae7230b9a52cb5229b0c91ea4410660b53b2e28c505356e5ca602296058ce6266e1a927a10a70037064b005d80b16151530e023e0692ab6a3955555ff00f6cbffd9"
  },
  {
    "path": "resources/multicolumn-lorem-ipsum.txt",
    "content": "                        Two-Column Document with Lorem Ipsum\n\n                                                         Your Name\n                                                     January 3, 2024\n\nAbstract                                                          pellentesque ante.  Phasellus adipiscing semper elit.\n                                                                  Proin fermentum massa ac quam.  Sed diam turpis,\nThis is a sample document with two columns ﬁlled                  molestie vitae, placerat a, molestie nec, leo.  Maece-\nwith Lorem Ipsum text.                                            nas lacinia. Nam ipsum ligula, eleifend at, accumsan\n  Lorem ipsum dolor sit amet,  consectetuer adip-                 nec, suscipit a, ipsum.  Morbi blandit ligula feugiat\niscing elit.   Ut purus elit, vestibulum ut, placerat             magna.  Nunc eleifend consequat lorem.  Sed lacinia\nac, adipiscing vitae, felis. Curabitur dictum gravida             nulla vitae enim.   Pellentesque tincidunt purus vel\nmauris.     Nam  arcu  libero,  nonummy  eget,  con-              magna.  Integer non enim.  Praesent euismod nunc\nsectetuer id, vulputate a, magna.   Donec vehicula                eu purus.  Donec bibendum quam in tellus.  Nullam\naugue eu neque.   Pellentesque habitant morbi tris-               cursus pulvinar lectus.  Donec et mi.  Nam vulpu-\ntique senectus et netus et malesuada fames ac turpis              tate metus eu enim. Vestibulum pellentesque felis eu\negestas.   Mauris ut leo.   Cras viverra metus rhon-              massa.\ncus sem.   Nulla et lectus vestibulum urna fringilla                 Quisque ullamcorper placerat ipsum.  Cras nibh.\nultrices.  Phasellus eu tellus sit amet tortor gravida            Morbi vel justo vitae lacus tincidunt ultrices. Lorem\nplacerat. Integer sapien est, iaculis in, pretium quis,           ipsum dolor sit amet, consectetuer adipiscing elit. In\nviverra ac, nunc.   Praesent eget sem vel leo ultri-              hachabitasseplateadictumst. Integertempusconva-\nces bibendum.  Aenean faucibus.  Morbi dolor nulla,               llis augue. Etiam facilisis. Nunc elementum fermen-\nmalesuada eu, pulvinar at, mollis ac, nulla.   Cur-               tum wisi.  Aenean placerat.  Ut imperdiet, enim sed\nabitur auctor semper nulla.  Donec varius orci eget               gravida sollicitudin, felis odio placerat quam, ac pul-\nrisus.  Duis nibh mi, congue eu, accumsan eleifend,               vinar elit purus eget enim. Nunc vitae tortor. Proin\nsagittis quis, diam. Duis eget orci sit amet orci dig-            tempus nibh sit amet nisl. Vivamus quis tortor vitae\nnissim rutrum.                                                    risus porta vehicula.\n  Nam dui ligula, fringilla a, euismod sodales, sollic-\nitudin vel, wisi. Morbi auctor lorem non justo. Nam                  Fusce mauris.  Vestibulum luctus nibh at lectus.\nlacus libero, pretium at, lobortis vitae, ultricies et,           Sed bibendum, nulla a faucibus semper, leo velit ul-\ntellus.   Donec aliquet, tortor sed accumsan biben-               tricies tellus, ac venenatis arcu wisi vel nisl. Vestibu-\ndum,  erat ligula aliquet magna,  vitae ornare odio               lum diam. Aliquam pellentesque, augue quis sagittis\nmetus a mi.  Morbi ac orci et nisl hendrerit mollis.              posuere, turpis lacus congue quam, in hendrerit risus\nSuspendisse ut massa.  Cras nec ante.  Pellentesque               eros eget felis.  Maecenas eget erat in sapien mattis\na nulla. Cum sociis natoque penatibus et magnis dis               porttitor.  Vestibulum porttitor.  Nulla facilisi.  Sed\nparturient montes, nascetur ridiculus mus. Aliquam                a turpis eu lacus commodo facilisis.  Morbi fringilla,\ntincidunturna. Nullaullamcorpervestibulumturpis.                  wisi in dignissim interdum, justo lectus sagittis dui,\nPellentesque cursus luctus mauris.                                et vehicula libero dui cursus dui.   Mauris tempor\n  Nulla malesuada porttitor diam. Donec felis erat,               ligula sed lacus.  Duis cursus enim ut augue.  Cras\ncongue non, volutpat at, tincidunt tristique, libero.             ac magna.  Cras nulla.  Nulla egestas.  Curabitur a\nVivamus viverra fermentum felis.  Donec nonummy                   leo.   Quisque egestas wisi eget nunc.   Nam feugiat\n\n\n                                                                1"
  },
  {
    "path": "resources/toy.layout.txt",
    "content": "AWAY again1\n   AWAY again2\n\n\n    Something[cited]\n\n              Single quote operator\n              Double quote operator\n              Last Txt"
  },
  {
    "path": "tests/__init__.py",
    "content": "import concurrent.futures\nimport os\nimport ssl\nimport sys\nimport urllib.request\nfrom pathlib import Path\nfrom typing import Optional\nfrom urllib.error import HTTPError\n\nif sys.version_info >= (3, 11):\n    from typing import Self\nelse:\n    from typing_extensions import Self\n\nimport yaml\n\nTESTS_ROOT = Path(__file__).parent.resolve()\nPROJECT_ROOT = TESTS_ROOT.parent\nRESOURCE_ROOT = PROJECT_ROOT / \"resources\"\nSAMPLE_ROOT = Path(PROJECT_ROOT) / \"sample-files\"\n\n\ndef _get_data_from_url(url: str) -> bytes:\n    ssl._create_default_https_context = ssl._create_unverified_context\n    attempts = 0\n    while attempts < 3:\n        try:\n            with urllib.request.urlopen(  # noqa: S310\n                    url\n            ) as response:\n                return response.read()\n        except HTTPError as e:\n            if attempts < 3:\n                attempts += 1\n            else:\n                raise e\n    raise ValueError(f\"Unknown error handling {url}\")\n\n\n# TODO: Make keyword-only and drop name being optional.\ndef get_data_from_url(url: Optional[str] = None, name: Optional[str] = None) -> bytes:\n    \"\"\"\n    Download a File from a URL and return its contents.\n\n    This function makes sure the PDF is not downloaded too often.\n    This function is a last resort for PDF files where we are uncertain if\n    we may add it for testing purposes to https://github.com/py-pdf/sample-files\n\n    Args:\n        url: location of the PDF file\n        name: unique name across all files\n\n    Returns:\n        Read File as bytes\n\n    \"\"\"\n    if name is None:\n        raise ValueError(\"A name must always be specified\")\n\n    if os.getenv(\"GITHUB_JOB\", None) is not None:\n        cache_dir = Path(\"tests\", \"pdf_cache\").resolve()\n    else:\n        cache_dir = Path(__file__).parent / \"pdf_cache\"\n    if not cache_dir.exists():\n        cache_dir.mkdir()\n    cache_path = cache_dir / name\n\n    if url is not None:\n        if url.startswith(\"file://\"):\n            path = Path(url[7:].replace(\"\\\\\", \"/\"))\n            return path.read_bytes()\n        if not cache_path.exists():\n            cache_path.write_bytes(_get_data_from_url(url))\n    return cache_path.read_bytes()\n\n\ndef _strip_position(line: str) -> str:\n    \"\"\"\n    Remove the location information.\n\n    The message\n        WARNING  pypdf._reader:_utils.py:364 Xref table not zero-indexed.\n\n    becomes\n        Xref table not zero-indexed.\n\n    Args:\n        line: the original line\n\n    Returns:\n        A line with stripped position\n\n    \"\"\"\n    line = \".py\".join(line.split(\".py:\")[1:])\n    return \" \".join(line.split(\" \")[1:])\n\n\ndef normalize_warnings(caplog_text: str) -> list[str]:\n    return [_strip_position(line) for line in caplog_text.strip().split(\"\\n\")]\n\n\ndef is_sublist(child_list, parent_list):\n    \"\"\"\n    Check if child_list is a sublist of parent_list, with respect to\n    * elements order\n    * elements repetition\n\n    Elements are compared using `==`\n    \"\"\"\n    if len(child_list) == 0:\n        return True\n    if len(parent_list) == 0:\n        return False\n    if parent_list[0] == child_list[0]:\n        return is_sublist(child_list[1:], parent_list[1:])\n    return is_sublist(child_list, parent_list[1:])\n\n\ndef read_yaml_to_list_of_dicts(yaml_file: Path) -> list[dict[str, str]]:\n    with open(yaml_file) as yaml_input:\n        return yaml.safe_load(yaml_input)\n\n\ndef download_test_pdfs():\n    \"\"\"\n    Run this before the tests are executed to ensure you have everything locally.\n\n    This is especially important to avoid pytest timeouts.\n    \"\"\"\n    pdfs = read_yaml_to_list_of_dicts(Path(__file__).parent / \"example_files.yaml\")\n\n    with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:\n        futures = [\n            executor.submit(get_data_from_url, pdf[\"url\"], name=pdf[\"local_filename\"])\n            for pdf in pdfs\n        ]\n        concurrent.futures.wait(futures)\n\n\nclass PILContext:\n    \"\"\"Allow changing the PIL/Pillow configuration for some limited scope.\"\"\"\n\n    def __init__(self) -> None:\n        self._saved_load_truncated_images = False\n\n    def __enter__(self) -> Self:\n        # Allow loading incomplete images.\n        from PIL import ImageFile  # noqa: PLC0415\n        self._saved_load_truncated_images = ImageFile.LOAD_TRUNCATED_IMAGES\n        ImageFile.LOAD_TRUNCATED_IMAGES = True\n        return self\n\n    def __exit__(self, type_, value, traceback) -> Optional[bool]:\n        from PIL import ImageFile  # noqa: PLC0415\n        ImageFile.LOAD_TRUNCATED_IMAGES = self._saved_load_truncated_images\n        if type_:\n            # Error.\n            return None\n        return True\n"
  },
  {
    "path": "tests/bench.py",
    "content": "\"\"\"\nBenchmark the speed of pypdf.\n\nThe results are on https://py-pdf.github.io/pypdf/dev/bench/\nPlease keep in mind that the variance is high.\n\"\"\"\nfrom io import BytesIO\nfrom tempfile import NamedTemporaryFile\n\nimport pytest\n\nimport pypdf\nfrom pypdf import PageObject, PdfReader, PdfWriter, Transformation\nfrom pypdf.generic import Destination, read_string_from_stream\n\nfrom . import RESOURCE_ROOT, SAMPLE_ROOT, get_data_from_url\n\n\ndef page_ops(pdf_path, password):\n    pdf_path = RESOURCE_ROOT / pdf_path\n\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter()\n\n    if password:\n        reader.decrypt(password)\n\n    page = reader.pages[0]\n    page = writer.add_page(page)\n\n    op = Transformation().rotate(90).scale(1.2)\n    page.add_transformation(op)\n    page.merge_page(page)\n\n    op = Transformation().scale(1).translate(tx=1, ty=1)\n    page.add_transformation(op)\n    page.merge_page(page)\n\n    op = Transformation().rotate(90).scale(1).translate(tx=1, ty=1)\n    page.add_transformation(op)\n    page.merge_page(page)\n\n    page.add_transformation((1, 0, 0, 0, 0, 0))\n    page.scale(2, 2)\n    page.scale_by(0.5)\n    page.scale_to(100, 100)\n\n    page = writer.pages[0]\n    page.compress_content_streams()\n    page.extract_text()\n\n\ndef test_page_operations(benchmark):\n    \"\"\"\n    Apply various page operations.\n\n    Rotation, scaling, translation, content stream compression, text extraction\n    \"\"\"\n    benchmark(page_ops, \"libreoffice-writer-password.pdf\", \"openpassword\")\n\n\ndef merge():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    outline = RESOURCE_ROOT / \"pdflatex-outline.pdf\"\n    pdf_forms = RESOURCE_ROOT / \"pdflatex-forms.pdf\"\n    pdf_pw = RESOURCE_ROOT / \"libreoffice-writer-password.pdf\"\n\n    writer = PdfWriter()\n\n    # string path:\n    writer.append(pdf_path)\n    writer.append(outline)\n    writer.append(pdf_path, pages=pypdf.pagerange.PageRange(slice(0, 0)))\n    writer.append(pdf_forms)\n\n    # Merging an encrypted file\n    reader = PdfReader(pdf_pw)\n    reader.decrypt(\"openpassword\")\n    writer.append(reader)\n\n    # PdfReader object:\n    writer.append(PdfReader(pdf_path), outline_item=\"True\")\n\n    # File handle\n    with open(pdf_path, \"rb\") as fh:\n        writer.append(fh)\n\n    outline_item = writer.add_outline_item(\"An outline item\", 0)\n    writer.add_outline_item(\"deeper\", 0, parent=outline_item)\n    writer.add_metadata({\"/Author\": \"Martin Thoma\"})\n    writer.add_named_destination(\"title\", 0)\n    writer.set_page_layout(\"/SinglePage\")\n    writer.page_mode = \"/UseThumbs\"\n\n    with NamedTemporaryFile(suffix=\".pdf\") as target_file:\n        write_path = target_file.name\n        writer.write(write_path)\n        writer.close()\n\n        # Check if outline is correct\n        reader = PdfReader(write_path)\n        assert [\n            el.title for el in reader.outline if isinstance(el, Destination)\n        ] == [\n            \"Foo\",\n            \"Bar\",\n            \"Baz\",\n            \"Foo\",\n            \"Bar\",\n            \"Baz\",\n            \"Foo\",\n            \"Bar\",\n            \"Baz\",\n            \"True\",\n            \"An outline item\",\n        ]\n\n\ndef test_merge(benchmark):\n    \"\"\"\n    Apply various page operations.\n\n    Rotation, scaling, translation, content stream compression, text extraction\n    \"\"\"\n    benchmark(merge)\n\n\ndef text_extraction(pdf_path):\n    with open(pdf_path, mode=\"rb\") as fd:\n        reader = PdfReader(fd)\n        text = \"\"\n        for page in reader.pages:\n            text += page.extract_text()\n    return text\n\n\ndef test_text_extraction(benchmark):\n    file_path = SAMPLE_ROOT / \"009-pdflatex-geotopo/GeoTopo.pdf\"\n    benchmark(text_extraction, file_path)\n\n\ndef read_string_from_stream_performance():\n    stream = BytesIO(b\"(\" + b\"\".join([b\"x\"] * 1024 * 256) + b\")\")\n    assert read_string_from_stream(stream)\n\n\ndef test_read_string_from_stream_performance(benchmark):\n    \"\"\"\n    This test simulates reading an embedded base64 image of 256kb.\n    It should be faster than a second, even on ancient machines.\n\n    Runs < 100ms on a 2019 notebook. Takes 10 seconds prior to #1350.\n    \"\"\"\n    benchmark(read_string_from_stream_performance)\n\n\ndef image_new_property(data):\n    reader = PdfReader(data)\n    assert reader.pages[0].images.keys() == [\n        \"/I0\",\n        \"/I1\",\n        \"/I2\",\n        \"/I3\",\n        \"/I4\",\n        \"/I5\",\n        \"/I6\",\n        \"/I7\",\n        \"/I8\",\n        \"/I9\",\n        [\"/TPL1\", \"/Image5\"],\n        [\"/TPL2\", \"/Image53\"],\n        [\"/TPL2\", \"/Image37\"],\n        [\"/TPL2\", \"/Image49\"],\n        [\"/TPL2\", \"/Image51\"],\n        [\"/TPL2\", \"/Image39\"],\n        [\"/TPL2\", \"/Image57\"],\n        [\"/TPL2\", \"/Image55\"],\n        [\"/TPL2\", \"/Image43\"],\n        [\"/TPL2\", \"/Image30\"],\n        [\"/TPL2\", \"/Image22\"],\n        [\"/TPL2\", \"/Image41\"],\n        [\"/TPL2\", \"/Image47\"],\n        [\"/TPL2\", \"/Image45\"],\n        [\"/TPL3\", \"/Image65\"],\n        [\"/TPL3\", \"/Image30\"],\n        [\"/TPL3\", \"/Image61\"],\n        [\"/TPL4\", \"/Image30\"],\n        [\"/TPL5\", \"/Image30\"],\n        [\"/TPL6\", \"/Image30\"],\n        [\"/TPL7\", \"/Image30\"],\n        [\"/TPL8\", \"/Image30\"],\n        [\"/TPL9\", \"/Image30\"],\n        [\"/TPL10\", \"/Image30\"],\n        [\"/TPL11\", \"/Image30\"],\n        [\"/TPL12\", \"/Image30\"],\n    ]\n    assert len(reader.pages[0].images.items()) == 36\n    assert reader.pages[0].images[0].name == \"I0.png\"\n    assert len(reader.pages[0].images[-1].data) > 10000\n    assert reader.pages[0].images[\"/TPL1\", \"/Image5\"].image.format == \"JPEG\"\n    assert (\n        reader.pages[0].images[\"/I0\"].indirect_reference.get_object()\n        == reader.pages[0][\"/Resources\"][\"/XObject\"][\"/I0\"]\n    )\n    list(reader.pages[0].images[0:2])\n    with pytest.raises(TypeError):\n        reader.pages[0].images[b\"0\"]\n    with pytest.raises(IndexError):\n        reader.pages[0].images[9999]\n    # just for test coverage:\n    with pytest.raises(KeyError):\n        reader.pages[0]._get_image([\"test\"], reader.pages[0])\n    assert list(PageObject(None, None).images) == []\n\n\n@pytest.mark.enable_socket\ndef test_image_new_property_performance(benchmark):\n    url = \"https://github.com/py-pdf/pypdf/files/11219022/pdf_font_garbled.pdf\"\n    name = \"pdf_font_garbled.pdf\"\n    data = BytesIO(get_data_from_url(url, name=name))\n\n    benchmark(image_new_property, data)\n\n\ndef image_extraction(data):\n    reader = PdfReader(data)\n    list(reader.pages[0].images)\n\n\n@pytest.mark.enable_socket\ndef test_large_compressed_image_performance(benchmark):\n    url = \"https://github.com/py-pdf/pypdf/files/15306199/file_with_large_compressed_image.pdf\"\n    data = BytesIO(get_data_from_url(url, name=\"file_with_large_compressed_image.pdf\"))\n    benchmark(image_extraction, data)\n"
  },
  {
    "path": "tests/conftest.py",
    "content": "\"\"\"Fixtures that are available automatically for all tests.\"\"\"\n\nimport uuid\n\nimport pytest\n\n\n@pytest.fixture(scope=\"session\")\ndef pdf_file_path(tmp_path_factory):\n    return tmp_path_factory.mktemp(\"pypdf-data\") / f\"{uuid.uuid4()}.pdf\"\n\n\n@pytest.fixture(scope=\"session\")\ndef txt_file_path(tmp_path_factory):\n    return tmp_path_factory.mktemp(\"pypdf-data\") / f\"{uuid.uuid4()}.txt\"\n"
  },
  {
    "path": "tests/example_files.yaml",
    "content": "- local_filename: 2201.00214.pdf\n  url: https://arxiv.org/pdf/2201.00214.pdf\n- local_filename: ASurveyofImageClassificationBasedTechniques.pdf\n  url: https://raw.githubusercontent.com/xyegithub/myBlog/12127c712ac2008782616c743224b187a4069477/posts/c94b2364/paper_pdfs/ImageClassification/2007%2CASurveyofImageClassificationBasedTechniques.pdf\n- local_filename: Giacalone.pdf\n  url: https://github.com/yxj-HGNwmb5kdp8ewr/yxj-HGNwmb5kdp8ewr.github.io/raw/master/files/Giacalone%20Llobell%20Jaeger%20(2022)%20Food%20Qual%20Prefer.pdf\n- local_filename: iss1718.pdf\n  url: https://github.com/py-pdf/pypdf/files/10983477/Ballinasloe_WS.pdf\n- local_filename: iss2077.pdf\n  url: https://github.com/py-pdf/pypdf/files/12309492/example_134.pdf\n- local_filename: pdf_font_garbled.pdf\n  url: https://github.com/py-pdf/pypdf/files/11219022/pdf_font_garbled.pdf\n- local_filename: The%20lean%20times%20in%20the%20Peruvian%20economy.pdf\n  url: https://github.com/alexanderquispe/1REI05/raw/main/reports/report_1/The%20lean%20times%20in%20the%20Peruvian%20economy.pdf\n- local_filename: tika-908104.pdf\n  url: https://github.com/user-attachments/files/18382273/tika-908104.pdf\n- local_filename: tika-923406.pdf\n  url: https://github.com/user-attachments/files/18382274/tika-923406.pdf\n- local_filename: tika-955562.pdf\n  url: https://github.com/user-attachments/files/18382288/tika-955562.pdf\n- local_filename: tika-959173.pdf\n  url: https://github.com/user-attachments/files/18382295/tika-959173.pdf\n- local_filename: waarom-meisjes-het-beter-doen-op-HAVO-en-VWO-ROA.pdf\n  url: https://github.com/py-pdf/pypdf/files/10773829/waarom-meisjes-het-beter-doen-op-HAVO-en-VWO-ROA.pdf\n- local_filename: tika-957144.pdf\n  url: https://github.com/user-attachments/files/18382302/tika-957144.pdf\n- local_filename: ascii charset.pdf\n  url: https://github.com/py-pdf/pypdf/files/9472500/main.pdf\n- local_filename: cmap1370.pdf\n  url: https://github.com/py-pdf/pypdf/files/9667138/cmap1370.pdf\n- local_filename: 02voc.pdf\n  url: https://github.com/py-pdf/pypdf/files/9712729/02voc.pdf\n- local_filename: iss1533.pdf\n  url: https://github.com/py-pdf/pypdf/files/10376149/iss1533.pdf\n- local_filename: tstUCS2.pdf\n  url: https://github.com/py-pdf/pypdf/files/11190189/pdf_font_garbled.pdf\n- local_filename: tst-GBK_EUC.pdf\n  url: https://github.com/py-pdf/pypdf/files/11315397/3.pdf\n- local_filename: math_latex.pdf\n  url: https://github.com/py-pdf/pypdf/files/12163370/math-in-text-created-via-latex.pdf\n- local_filename: unixxx_glyphs.pdf\n  url: https://arxiv.org/pdf/2201.00021.pdf\n- local_filename: TextAttack_paper.pdf\n  url: https://arxiv.org/pdf/2005.05909.pdf\n- local_filename: iss2173.pdf\n  url: https://github.com/py-pdf/pypdf/files/12552700/tt.pdf\n- local_filename: iss2290.pdf\n  url: https://github.com/py-pdf/pypdf/files/13452885/example.pdf\n- local_filename: NewJersey.pdf\n  url: https://github.com/py-pdf/pypdf/files/12090692/New.Jersey.Coinbase.staking.securities.charges.2023-0606_Coinbase-Penalty-and-C-D.pdf\n- local_filename: tika-952445.pdf\n  url: https://github.com/user-attachments/files/18382348/tika-952445.pdf\n- local_filename: tika-921632.pdf\n  url: https://github.com/user-attachments/files/18382354/tika-921632.pdf\n- local_filename: tika-976970.pdf\n  url: https://github.com/user-attachments/files/18382397/tika-976970.pdf\n- local_filename: tika-914102.pdf\n  url: https://github.com/user-attachments/files/18381687/tika-914102.pdf\n- local_filename: iss1737.pdf\n  url: https://github.com/py-pdf/pypdf/files/11068604/tt1.pdf\n- local_filename: issue-1801.pdf\n  url: https://github.com/py-pdf/pypdf/files/11250359/test_img.pdf\n- local_filename: tika-924546.pdf\n  url: https://github.com/user-attachments/files/18381697/tika-924546.pdf\n- local_filename: issue-1801.png\n  url: https://user-images.githubusercontent.com/1658117/232842886-9d1b0726-3a5b-430d-8464-595d919c266c.png\n- local_filename: grimm10\n  url: https://github.com/py-pdf/pypdf/files/11336817/grimm10.pdf\n- local_filename: labeled-edges-center-image.png\n  url: https://user-images.githubusercontent.com/4083478/236685544-a1940b06-fb42-4bb1-b589-1e4ad429d68e.png\n- local_filename: watermark1.png\n  url: https://user-images.githubusercontent.com/4083478/236793172-09340aef-3440-4c8a-af85-a91cdad27d46.png\n- local_filename: tika-977609.pdf\n  url: https://github.com/user-attachments/files/18381754/tika-977609.pdf\n- local_filename: tifimage.png\n  url: https://user-images.githubusercontent.com/4083478/236793166-288b4b59-dee3-49fd-a04e-410aab06199a.png\n- local_filename: tika-972174.pdf\n  url: https://github.com/user-attachments/files/18381744/tika-972174.pdf\n- local_filename: tika-972174_p0-im0.png\n  url: https://user-images.githubusercontent.com/4083478/238288207-b77dd38c-34b4-4f4f-810a-bf9db7ca0414.png\n- local_filename: Vitocal.pdf\n  url: https://github.com/py-pdf/pypdf/files/11962229/DB-5368770_Vitocal_200-G.pdf\n- local_filename: VitocalImage.png\n  url: https://user-images.githubusercontent.com/4083478/251283945-38c5b92c-cf94-473c-bb57-a51b74fc39be.jpg\n- local_filename: cmyk_deflate.pdf\n  url: https://github.com/py-pdf/pypdf/files/12078533/cmyk2.pdf\n- local_filename: cmyk_deflate.tif\n  url: https://github.com/py-pdf/pypdf/files/12078556/cmyk.tif.txt\n- local_filename: o1whh9b3.pdf\n  url: https://github.com/py-pdf/pypdf/files/11578953/USC.EMBA.-.Pre-Season.and.Theme.I.pdf\n- local_filename: selbst.72916.pdf\n  url: https://github.com/py-pdf/pypdf/files/14395695/selbst.72916.pdf\n- local_filename: iss1912.pdf\n  url: https://github.com/py-pdf/pypdf/files/11845099/GeoTopo-komprimiert.pdf\n- local_filename: calRGB.pdf\n  url: https://github.com/py-pdf/pypdf/files/12061061/tt.pdf\n- local_filename: 2023USDC.pdf\n  url: https://github.com/py-pdf/pypdf/files/12090523/2023.USDC_Circle.Examination.Report.May.2023.pdf\n- local_filename: iss1982_im1.png\n  url: https://github.com/py-pdf/pypdf/files/12144094/im1.png.txt\n- local_filename: iss1982_im2.png\n  url: https://github.com/py-pdf/pypdf/files/12144093/im2.png.txt\n- local_filename: usa.png\n  url: https://github.com/py-pdf/pypdf/assets/4083478/56c93021-33cd-4387-ae13-5cbe7e673f42\n- local_filename: paid.pdf\n  url: https://github.com/py-pdf/pypdf/files/12050253/tt.pdf\n- local_filename: Pesquisa-de-Precos-Combustiveis-novembro-2023.pdf\n  url: https://www.joinville.sc.gov.br/wp-content/uploads/2023/11/Pesquisa-de-Precos-Combustiveis-novembro-2023.pdf\n- local_filename: iss2138.pdf\n  url: https://github.com/py-pdf/pypdf/files/12483807/AEO.1172.pdf\n- local_filename: iss3268.pdf\n  url: https://github.com/user-attachments/files/20060394/broken.pdf\n- local_filename: direct-link.pdf\n  url: https://github.com/user-attachments/files/20348304/tst.pdf\n- local_filename: named-reference.pdf\n  url: https://github.com/user-attachments/files/20455804/MinimalJob.pdf\n- local_filename: large_lzw_example_encoded.dat\n  url: https://github.com/user-attachments/files/20923310/large_lzw_example_encoded.dat.txt\n- local_filename: issue-3419.pdf\n  url: https://github.com/user-attachments/files/21578875/layout-parser-paper-with-empty-pages.pdf\n- local_filename: issue-3429.pdf\n  url: https://github.com/user-attachments/files/21711469/bomb.pdf\n- local_filename: issue-3508.pdf\n  url: https://github.com/user-attachments/files/23211824/repair-manual-thermo-230-300-350-2012-en.pdf\n- local_filename: issue-3553.pdf\n  url: https://github.com/user-attachments/files/23996861/ATOLCertificate.pdf\n- local_filename: issue-3633.pdf\n  url: https://github.com/user-attachments/files/25212719/minimal_signature.pdf\n"
  },
  {
    "path": "tests/generic/__init__.py",
    "content": ""
  },
  {
    "path": "tests/generic/test_base.py",
    "content": "\"\"\"Test the pypdf.generic._base module.\"\"\"\nfrom io import BytesIO\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.generic import read_hex_string_from_stream\nfrom tests import get_data_from_url\n\n\n@pytest.mark.parametrize(\n    (\"source\", \"expected\"),\n    [\n        (b\"<00FE00FF>\", \"\\xfe\\xff\"),\n        (b\"<00FE00FF00D6>\", \"\\xfe\\xff\\xd6\"),\n    ]\n)\ndef test_text_string_object__looks_like_bom(source: bytes, expected: str) -> None:\n    stream = BytesIO(source)\n    result = read_hex_string_from_stream(stream)\n    assert result == expected\n\n\n@pytest.mark.enable_socket\ndef test_text_string_object__wrongly_detected_bom():\n    url = \"https://github.com/user-attachments/files/24401507/minimal.pdf\"\n    name = \"issue3587.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader_page = reader.pages[0]\n\n    writer = PdfWriter()\n    for page in reader.pages:\n        writer_page = writer.add_blank_page(reader_page.mediabox.width, reader_page.mediabox.height)\n        writer_page.merge_page(page)\n\n        assert writer_page.extract_text() == (\n            \"无译形带 r的参 z慧队手行 c要枪互工先调 uC一在你 k该方导最 xT况 M味政没出 v大同团\\n\"\n            \"想急压游这体构主 m基重张预另做内已织程术并 U种规被中应 s过小立就公测和 F更为 BS\\n\"\n            \"把强型 w利 qfJ现能您关文）己个言 VW是 Z亲社 y。说准密令 K络通自力 i诸旦明量放及 I\\n\"\n            \"成战康养 d都蜂多开 pE次提朋动比台有培愿 A确 l充计标去人如么 b灵 N它 g弃语看 X；j\\n\"\n            \"轮 HG采共由地友入（器 Y果感建切理情从集德翻 a单第识任 Q模 eh目经相哪受起时着 DR\\n\"\n            \"用好 o备划付信、度解效作协读 O讨高具击始者意群治扩到 P才兰网认 t马倒来本整 L们 n\\n\"\n            \"系可论，步各之但\\n\"\n            \"12\"\n        )\n"
  },
  {
    "path": "tests/generic/test_data_structures.py",
    "content": "\"\"\"Test the pypdf.generic._data_structures module.\"\"\"\nimport os\nimport subprocess\nimport sys\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import Callable\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.errors import LimitReachedError\nfrom pypdf.generic import (\n    ArrayObject,\n    ContentStream,\n    DictionaryObject,\n    NameObject,\n    NullObject,\n    RectangleObject,\n    StreamObject,\n    TreeObject,\n)\nfrom tests import RESOURCE_ROOT, get_data_from_url\n\ntry:\n    import resource\nexcept ImportError:\n    resource = None\n\n\ndef test_dictionary_object__get_next_object_position():\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n\n    # reader.xref = {0: {7: 15, 9: 10245, 12: 939, 14: 2999, 16: 4982, 18: 9949, 22: 11160}}\n    assert DictionaryObject._get_next_object_position(\n        position_before=12345, position_end=999999, generations=list(reader.xref), pdf=reader\n    ) == 999999  # No value after 12345 in dictionary\n    assert DictionaryObject._get_next_object_position(\n        position_before=11111, position_end=999999, generations=list(reader.xref), pdf=reader\n    ) == 11160  # First value after 11111 in dictionary.\n    assert DictionaryObject._get_next_object_position(\n        position_before=42, position_end=999999, generations=list(reader.xref), pdf=reader\n    ) == 939  # First value after 42 in dictionary.\n\n    # New generation.\n    reader.xref[1] = {7: 42, 24: 15000}\n    assert DictionaryObject._get_next_object_position(\n        position_before=10, position_end=999999, generations=list(reader.xref), pdf=reader\n    ) == 15\n\n\ndef test_tree_object__cyclic_reference(caplog):\n    writer = PdfWriter()\n    child1 = writer._add_object(DictionaryObject())\n    child2 = writer._add_object(DictionaryObject({NameObject(\"/Next\"): child1}))\n    child3 = writer._add_object(DictionaryObject({NameObject(\"/Next\"): child2}))\n    child1.get_object()[NameObject(\"/Next\")] = child3\n    tree = TreeObject()\n    tree[NameObject(\"/First\")] = child2\n    tree[NameObject(\"/Last\")] = writer._add_object(DictionaryObject())\n\n    assert list(tree.children()) == [child2.get_object(), child1.get_object(), child3.get_object()]\n    assert \"Detected cycle in outline structure for \" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_array_object__clone_same_object_multiple_times(caplog):\n    url = \"https://github.com/user-attachments/files/25412858/Draft_OSMF_financial_statement_2013.pdf\"\n    name = \"issue2991.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))\n\n    writer = PdfWriter()\n    for page in reader.pages:\n        page2 = writer.add_page(page)\n        assert page2.mediabox == RectangleObject((0, 0, 595, 841))\n    assert caplog.messages == []\n\n\ndef test_array_object__clone_same_stream_multiple_times():\n    writer = PdfWriter()\n\n    # Unique streams.\n    stream1 = StreamObject()\n    stream1.set_data(b\"Hello World!\")\n    stream2 = StreamObject()\n    stream2.set_data(b\"Lorem ipsum!\")\n\n    # Shared streams.\n    shared_streams = [StreamObject() for _ in range(3)]\n    [shared_stream.set_data(f\"Shared stream {index}\".encode()) for index, shared_stream in enumerate(shared_streams)]\n\n    # Add to writer.\n    writer._add_object(stream1)\n    writer._add_object(stream2)\n    shared_references = [writer._add_object(shared_stream) for shared_stream in shared_streams]\n\n    # Arrays.\n    array1 = ArrayObject([stream1.indirect_reference, *shared_references])\n    array2 = ArrayObject([stream2.indirect_reference, *shared_references])\n\n    # Cloned.\n    cloned1 = array1.clone(pdf_dest=writer)\n    cloned2 = array2.clone(pdf_dest=writer)\n\n    # Nullify one shared object.\n    writer._replace_object(shared_references[1].indirect_reference, NullObject())\n\n    # The first entry is always different. The remaining shared entries should be dedicated copies.\n    assert cloned1[1:] != cloned2[1:]\n\n    assert ContentStream(stream=array1, pdf=None).get_data() == b\"Hello World!\\nShared stream 0\\nShared stream 2\\n\"\n    assert ContentStream(stream=array2, pdf=None).get_data() == b\"Lorem ipsum!\\nShared stream 0\\nShared stream 2\\n\"\n    assert (\n        ContentStream(stream=cloned1, pdf=None).get_data() ==\n        b\"Hello World!\\nShared stream 0\\nShared stream 1\\nShared stream 2\\n\"\n    )\n    assert (\n        ContentStream(stream=cloned2, pdf=None).get_data() ==\n        b\"Lorem ipsum!\\nShared stream 0\\nShared stream 1\\nShared stream 2\\n\"\n    )\n\n\n@pytest.mark.enable_socket\ndef test_dictionary_object__read_from_stream__limit():\n    name = \"read_from_stream__length_2gb.pdf\"\n    url = \"https://github.com/user-attachments/files/25842437/read_from_stream__length_2gb.pdf\"\n\n    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))\n    page = reader.pages[0]\n\n    with pytest.raises(\n            expected_exception=LimitReachedError,\n            match=r\"^Declared stream length of 2147483647 exceeds maximum allowed length\\.$\"\n    ):\n        page.extract_text()\n\n\ndef _prepare_test_dictionary_object__read_from_stream__no_limit(\n        path: Path\n) -> tuple[str, dict[str, str], Callable[[], None]]:\n    env = os.environ.copy()\n    env[\"COVERAGE_PROCESS_START\"] = \"pyproject.toml\"\n\n    name = \"read_from_stream__length_2gb.pdf\"\n    url = \"https://github.com/user-attachments/files/25842437/read_from_stream__length_2gb.pdf\"\n    data = get_data_from_url(url=url, name=name)\n    pdf_path = path / name\n    pdf_path.write_bytes(data)\n    pdf_path_str = pdf_path.resolve().as_posix()\n\n    try:\n        env[\"PYTHONPATH\"] = \".\" + os.pathsep + env[\"PYTHONPATH\"]\n    except KeyError:\n        env[\"PYTHONPATH\"] = \".\"\n\n    def limit_virtual_memory() -> None:\n        limit_kb = 1_000_000\n        limit_bytes = limit_kb * 1024\n        resource.setrlimit(resource.RLIMIT_AS, (limit_bytes, limit_bytes))\n\n    return pdf_path_str, env, limit_virtual_memory\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(condition=resource is None, reason=\"Does not have 'resource' module.\")\n@pytest.mark.skipif(sys.platform == \"darwin\", reason=\"RLIMIT_AS is unreliable.\")\ndef test_dictionary_object__read_from_stream__no_limit(tmp_path):\n    pdf_path_str, env, limit_virtual_memory = _prepare_test_dictionary_object__read_from_stream__no_limit(tmp_path)\n\n    source_file = tmp_path / \"script.py\"\n    source_file.write_text(\n        f\"\"\"\nimport sys\nfrom pypdf import filters, PdfReader\n\nfilters.MAX_DECLARED_STREAM_LENGTH = sys.maxsize\n\nwith open({pdf_path_str!r}, mode=\"rb\") as fd:\n    reader = PdfReader(fd)\n    print(reader.pages[0].extract_text())\n\"\"\"\n    )\n\n    result = subprocess.run(  # noqa: S603  # We have the control here.\n        [sys.executable, source_file],\n        capture_output=True,\n        env=env,\n        text=True,\n        preexec_fn=limit_virtual_memory,\n    )\n    assert result.returncode == 1\n    assert result.stdout == \"\"\n    assert result.stderr.replace(\"\\r\", \"\").endswith(\"\\nMemoryError\\n\")\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(condition=resource is None, reason=\"Does not have 'resource' module.\")\n@pytest.mark.skipif(sys.platform == \"darwin\", reason=\"RLIMIT_AS is unreliable.\")\ndef test_dictionary_object__read_from_stream__no_limit__path(tmp_path):\n    pdf_path_str, env, limit_virtual_memory = _prepare_test_dictionary_object__read_from_stream__no_limit(tmp_path)\n\n    source_file = tmp_path / \"script.py\"\n    source_file.write_text(\n        f\"\"\"\nimport sys\nfrom pypdf import filters, PdfReader\n\nfilters.MAX_DECLARED_STREAM_LENGTH = sys.maxsize\n\nreader = PdfReader({pdf_path_str!r})\nprint(reader.pages[0].extract_text())\n\"\"\"\n    )\n\n    result = subprocess.run(  # noqa: S603  # We have the control here.\n        [sys.executable, source_file],\n        capture_output=True,\n        env=env,\n        text=True,\n        preexec_fn=limit_virtual_memory,\n    )\n    assert result.returncode == 0\n    assert result.stdout.replace(\"\\r\", \"\") == \"Hello from pypdf\\n\"\n    assert result.stderr == \"\"\n\n\ndef _get_array_based_buffer(stream_count: int, chunk_bytes: int) -> BytesIO:\n    writer = PdfWriter()\n    page = writer.add_blank_page(width=10, height=10)\n\n    streams = [ContentStream(stream=None, pdf=writer) for _ in range(stream_count)]\n    chunk = b\"q\\n\" + (b\"A\" * chunk_bytes) + b\"\\nQ\\n\"\n    [stream.set_data(chunk) for stream in streams]\n    contents = ArrayObject([writer._add_object(stream) for stream in streams])\n    page[NameObject(\"/Contents\")] = contents\n\n    buffer = BytesIO()\n    writer.write(buffer)\n    buffer.flush()\n    return buffer\n\n\n@pytest.mark.timeout(10)\ndef test_content_stream__array_based__performance():\n    buffer = _get_array_based_buffer(stream_count=10_000, chunk_bytes=7000)\n    reader = PdfReader(buffer)\n    _ = reader.pages[0].get_contents()\n\n\ndef test_content_stream__array_based__length():\n    buffer = _get_array_based_buffer(stream_count=11_000, chunk_bytes=1)\n    reader = PdfReader(buffer)\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Array\\-based stream has 11000 > 10000 elements\\.$\"\n    ):\n        _ = reader.pages[0].get_contents()\n\n\n@pytest.mark.timeout(10)\ndef test_content_stream__array_based__output_length():\n    buffer = _get_array_based_buffer(stream_count=10_000, chunk_bytes=8192)\n    reader = PdfReader(buffer)\n    with pytest.raises(\n            expected_exception=LimitReachedError,\n            match=r\"^Array\\-based stream has at least 75003501 > 75000000 output bytes\\.$\"\n    ):\n        _ = reader.pages[0].get_contents()\n"
  },
  {
    "path": "tests/generic/test_files.py",
    "content": "\"\"\"Test the pypdf.generic._files module.\"\"\"\nimport datetime\nimport shutil\nimport subprocess\nfrom io import BytesIO\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.constants import AFRelationship\nfrom pypdf.errors import PdfReadError, PyPdfError\nfrom pypdf.generic import (\n    ArrayObject,\n    ByteStringObject,\n    DictionaryObject,\n    EmbeddedFile,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    TextStringObject,\n    create_string_object,\n)\nfrom tests import SAMPLE_ROOT, get_data_from_url\n\nPDFATTACH_BINARY = shutil.which(\"pdfattach\")\n\n\n@pytest.mark.skipif(PDFATTACH_BINARY is None, reason=\"Requires poppler-utils\")\ndef test_embedded_file__basic(tmpdir):\n    clean_path = SAMPLE_ROOT / \"002-trivial-libre-office-writer\" / \"002-trivial-libre-office-writer.pdf\"\n    attached_path = tmpdir / \"attached.pdf\"\n    file_path = tmpdir / \"test.txt\"\n    file_path.write_binary(b\"Hello World\\n\")\n    subprocess.run([PDFATTACH_BINARY, clean_path, file_path, attached_path])  # noqa: S603\n    with PdfReader(str(attached_path)) as reader:\n        attachment = next(iter(EmbeddedFile._load(reader.root_object)))\n\n        assert attachment.name == \"test.txt\"\n        assert attachment.alternative_name == \"test.txt\"\n        assert attachment.description is None\n        assert attachment.associated_file_relationship == AFRelationship.UNSPECIFIED\n        assert attachment.subtype is None\n        assert attachment.content == b\"Hello World\\n\"\n        assert attachment.size == 12\n        assert attachment.creation_date is None\n        assert attachment.modification_date is None\n        assert attachment.checksum is None\n        assert repr(attachment) == \"<EmbeddedFile name='test.txt'>\"\n\n\ndef test_embedded_file__artificial():\n    # No alternative name.\n    pdf_object = DictionaryObject(answer=42)\n    attachment = EmbeddedFile(name=\"dummy\", pdf_object=pdf_object)\n    assert attachment.alternative_name is None\n\n    # No /EF.\n    with pytest.raises(PdfReadError, match=f\"/EF entry not found: {pdf_object}\"):\n        _ = attachment._embedded_file\n\n    # Empty /EF dictionary.\n    pdf_object = DictionaryObject()\n    pdf_object[NameObject(\"/EF\")] = DictionaryObject()\n    attachment = EmbeddedFile(name=\"dummy\", pdf_object=pdf_object)\n    with pytest.raises(PdfReadError, match=r\"No /\\(U\\)F key found in file dictionary: {}\"):\n        _ = attachment._embedded_file\n\n    # Missing /Params key.\n    pdf_object[NameObject(\"/EF\")] = DictionaryObject()\n    pdf_object[NameObject(\"/EF\")][NameObject(\"/F\")] = DictionaryObject(answer=42)\n    assert attachment._params == DictionaryObject()\n\n    # An actual checksum is set.\n    # Generated using `hashlib.md5(b\"Hello World!\\n\").digest()`\n    params = DictionaryObject()\n    params[NameObject(\"/CheckSum\")] = ByteStringObject(b\"\\x8d\\xdd\\x8b\\xe4\\xb1y\\xa5)\\xaf\\xa5\\xf2\\xff\\xaeK\\x98X\")\n    pdf_object[NameObject(\"/EF\")][NameObject(\"/F\")][NameObject(\"/Params\")] = params\n    assert attachment.checksum == b\"\\x8d\\xdd\\x8b\\xe4\\xb1y\\xa5)\\xaf\\xa5\\xf2\\xff\\xaeK\\x98X\"\n\n\n@pytest.mark.enable_socket\ndef test_embedded_file__kids():\n    # Generated using the instructions available from\n    # https://medium.com/@pymupdf/zugferd-and-ghostscript-how-to-create-industry-standard-and-compliant-pdf-e-invoices-83c9fde31ee5\n    # Notes:\n    #   * Yes, we need the full paths. Otherwise, the output file will only have an empty page.\n    #   * The XML file has been a custom basic text file.\n    #   * The input PDF file has been the `002-trivial-libre-office-writer.pdf` file.\n    url = \"https://github.com/user-attachments/files/18691309/embedded_files_kids.pdf\"\n    name = \"embedded_files_kids.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    attachments = list(EmbeddedFile._load(reader.root_object))\n    assert len(attachments) == 1\n    attachment = attachments[0]\n\n    assert attachment.name == \"factur-x.xml\"\n    assert attachment.alternative_name == \"factur-x.xml\"\n    assert attachment.description == \"ZUGFeRD electronic invoice\"\n    assert attachment.associated_file_relationship == AFRelationship.ALTERNATIVE\n    assert attachment.subtype == \"/text/xml\"\n    assert attachment.content.startswith(b\"Hello World!\\n\\nLorem ipsum dolor sit amet, \")\n    assert attachment.content.endswith(b\"\\ntakimata sanctus est Lorem ipsum dolor sit amet.\\n\")\n    assert attachment.size == 606\n    assert attachment.creation_date is None\n    assert attachment.modification_date == datetime.datetime(\n        2013, 1, 21, 8, 14, 33, tzinfo=datetime.timezone(datetime.timedelta(hours=1))\n    )\n    assert attachment.checksum is None\n    assert repr(attachment) == \"<EmbeddedFile name='factur-x.xml'>\"\n\n    # No /Names in /Kids.\n    del (\n        reader.root_object[NameObject(\"/Names\")][NameObject(\"/EmbeddedFiles\")][NameObject(\"/Kids\")][0]\n        .get_object()[NameObject(\"/Names\")]\n    )\n    attachments = list(EmbeddedFile._load(reader.root_object))\n    assert attachments == []\n\n\n@pytest.mark.enable_socket\ndef test_embedded_file__ensure_params__existing_params():\n    url = \"https://github.com/user-attachments/files/18691309/embedded_files_kids.pdf\"\n    name = \"embedded_files_kids.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    attachments = list(EmbeddedFile._load(reader.root_object))\n    assert len(attachments) == 1\n    attachment = attachments[0]\n\n    assert \"/Params\" in attachment._embedded_file\n    params_dict = attachment._ensure_params\n\n    assert isinstance(params_dict, DictionaryObject)\n\n    assert NameObject(\"/ModDate\") in params_dict\n\n    original_mod_date = params_dict.get(NameObject(\"/ModDate\"))\n    params_dict[NameObject(\"/TestParam\")] = TextStringObject(\"test_value\")\n\n    assert params_dict[NameObject(\"/TestParam\")] == TextStringObject(\"test_value\")\n    assert params_dict[NameObject(\"/ModDate\")] == original_mod_date\n\n    params_dict2 = attachment._ensure_params\n    assert params_dict is params_dict2\n    assert params_dict2[NameObject(\"/TestParam\")] == TextStringObject(\"test_value\")\n\n\ndef test_embedded_file__name_is_read_only():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    assert embedded_file.name == \"test.txt\"\n\n    with pytest.raises(AttributeError):\n        embedded_file.name = \"new_name.txt\"\n\n\ndef test_embedded_file__alternative_name_setter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file.alternative_name = TextStringObject(\"Alternative Name\")\n    assert embedded_file.alternative_name == \"Alternative Name\"\n\n    embedded_file.alternative_name = None\n    if NameObject(\"/UF\") in embedded_file.pdf_object:\n        assert embedded_file.pdf_object[NameObject(\"/UF\")] == NullObject()\n    if NameObject(\"/F\") in embedded_file.pdf_object:\n        assert embedded_file.pdf_object[NameObject(\"/F\")] == NullObject()\n    assert embedded_file.alternative_name is None\n\n    pdf_string = TextStringObject(\"PDF String\")\n    embedded_file.alternative_name = pdf_string\n    assert embedded_file.alternative_name == \"PDF String\"\n\n\ndef test_embedded_file__alternative_name__uf_key_only():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file.pdf_object[NameObject(\"/UF\")] = create_string_object(\"original_uf\")\n    del embedded_file.pdf_object[NameObject(\"/F\")]\n\n    assert NameObject(\"/UF\") in embedded_file.pdf_object\n    assert NameObject(\"/F\") not in embedded_file.pdf_object\n\n    embedded_file.alternative_name = None\n    assert embedded_file.pdf_object[NameObject(\"/UF\")] == NullObject()\n    assert NameObject(\"/F\") not in embedded_file.pdf_object\n\n    embedded_file.alternative_name = TextStringObject(\"new_uf\")\n    assert embedded_file.pdf_object[NameObject(\"/UF\")] == create_string_object(\"new_uf\")\n    assert embedded_file.pdf_object[NameObject(\"/F\")] == create_string_object(\"new_uf\")\n\n\ndef test_embedded_file__alternative_name__f_key_only():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file.pdf_object[NameObject(\"/F\")] = create_string_object(\"original_f\")\n    if NameObject(\"/UF\") in embedded_file.pdf_object:\n        del embedded_file.pdf_object[NameObject(\"/UF\")]\n\n    assert NameObject(\"/F\") in embedded_file.pdf_object\n    assert NameObject(\"/UF\") not in embedded_file.pdf_object\n\n    embedded_file.alternative_name = None\n    assert embedded_file.pdf_object[NameObject(\"/F\")] == NullObject()\n    assert NameObject(\"/UF\") not in embedded_file.pdf_object\n\n    embedded_file.alternative_name = TextStringObject(\"new_f\")\n    assert embedded_file.pdf_object[NameObject(\"/F\")] == create_string_object(\"new_f\")\n    assert embedded_file.pdf_object[NameObject(\"/UF\")] == create_string_object(\"new_f\")\n\n\ndef test_embedded_file__alternative_name__both_f_and_uf():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file.pdf_object[NameObject(\"/F\")] = create_string_object(\"original_f\")\n    embedded_file.pdf_object[NameObject(\"/UF\")] = create_string_object(\"original_uf\")\n\n    embedded_file.alternative_name = TextStringObject(\"new_name\")\n    assert embedded_file.pdf_object[NameObject(\"/F\")] == create_string_object(\"new_name\")\n    assert embedded_file.pdf_object[NameObject(\"/UF\")] == create_string_object(\"new_name\")\n    assert embedded_file.alternative_name == \"new_name\"\n\n    embedded_file.alternative_name = None\n    assert embedded_file.pdf_object[NameObject(\"/F\")] == NullObject()\n    assert embedded_file.pdf_object[NameObject(\"/UF\")] == NullObject()\n    assert embedded_file.alternative_name is None\n\n\ndef test_embedded_file__description_setter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file.description = TextStringObject(\"Test Description\")\n    assert embedded_file.description == \"Test Description\"\n\n    embedded_file.description = None\n    assert embedded_file.pdf_object[NameObject(\"/Desc\")] == NullObject()\n\n    pdf_string = TextStringObject(\"PDF Description\")\n    embedded_file.description = pdf_string\n    assert embedded_file.description == \"PDF Description\"\n\n\ndef test_embedded_file__subtype_setter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file.subtype = NameObject(\"/text/plain\")\n    assert embedded_file.subtype == \"/text/plain\"\n\n    embedded_file.subtype = None\n    assert embedded_file._embedded_file[NameObject(\"/Subtype\")] == NullObject()\n\n    name_obj = NameObject(\"/application#2Fjson\")\n    embedded_file.subtype = name_obj\n    assert embedded_file.subtype == \"/application#2Fjson\"\n\n\ndef test_embedded_file__content_setter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n    assert embedded_file.content == b\"content\"\n\n    embedded_file.content = b\"Hello World!\"\n    assert embedded_file.content == b\"Hello World!\"\n\n    embedded_file.content = \"Lorem ipsum dolor sit amet\"\n    assert embedded_file.content == b\"Lorem ipsum dolor sit amet\"\n\n\ndef test_embedded_file__size_setter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file.size = NumberObject(1024)\n    assert embedded_file.size == 1024\n\n    embedded_file.size = None\n    assert embedded_file._ensure_params[NameObject(\"/Size\")] == NullObject()\n\n    num_obj = NumberObject(2048)\n    embedded_file.size = num_obj\n    assert embedded_file.size == 2048\n\n\ndef test_embedded_file__size_getter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file._ensure_params[NameObject(\"/Size\")] = NullObject()\n    assert embedded_file.size is None\n\n    embedded_file._ensure_params[NameObject(\"/Size\")] = NumberObject(4096)\n    retrieved_size = embedded_file.size\n    assert retrieved_size == 4096\n\n\ndef test_embedded_file__creation_date_setter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    test_date = datetime.datetime(2023, 1, 1, 12, 0, 0)\n    embedded_file.creation_date = test_date\n    assert embedded_file.creation_date == test_date\n\n    embedded_file.creation_date = None\n    assert embedded_file._ensure_params[NameObject(\"/CreationDate\")] == NullObject()\n\n\ndef test_embedded_file__modification_date_setter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    test_date = datetime.datetime(2023, 1, 2, 12, 0, 0)\n    embedded_file.modification_date = test_date\n    assert embedded_file.modification_date == test_date\n\n    embedded_file.modification_date = None\n    assert embedded_file._ensure_params[NameObject(\"/ModDate\")] == NullObject()\n\n\ndef test_embedded_file__checksum_setter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    checksum_bytes = ByteStringObject(b\"checksum_value\")\n    embedded_file.checksum = checksum_bytes\n    assert embedded_file.checksum == b\"checksum_value\"\n\n    embedded_file.checksum = None\n    assert embedded_file._ensure_params[NameObject(\"/CheckSum\")] == NullObject()\n\n    byte_string = ByteStringObject(b\"pdf_checksum\")\n    embedded_file.checksum = byte_string\n    assert embedded_file.checksum == b\"pdf_checksum\"\n\n\ndef test_embedded_file__associated_file_relationship_setter():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file.associated_file_relationship = NameObject(\"/Data\")\n    assert embedded_file.associated_file_relationship == \"/Data\"\n\n\ndef test_embedded_file__setters_integration():\n    writer = PdfWriter()\n    writer.add_blank_page(100, 100)\n\n    embedded_file = writer.add_attachment(\"test.txt\", b\"Hello, World!\")\n    embedded_file.alternative_name = TextStringObject(\"Alternative Name\")\n    embedded_file.description = TextStringObject(\"Test Description\")\n    embedded_file.subtype = NameObject(\"/text/plain\")\n    embedded_file.size = NumberObject(13)\n    creation_date = datetime.datetime(2023, 1, 1, 12, 0, 0)\n    embedded_file.creation_date = creation_date\n    modification_date = datetime.datetime(2023, 1, 2, 12, 0, 0)\n    embedded_file.modification_date = modification_date\n    embedded_file.checksum = ByteStringObject(b\"checksum123\")\n    embedded_file.associated_file_relationship = NameObject(AFRelationship.DATA)\n\n    # Make sure that this is an indirect object for PDF/A-3 compliance.\n    assert embedded_file.pdf_object.indirect_reference == IndirectObject(6, 0, writer)\n\n    pdf_bytes = BytesIO()\n    writer.write(pdf_bytes)\n\n    reader = PdfReader(pdf_bytes)\n    assert \"test.txt\" in reader.attachments\n\n\ndef test_embedded_file__null_object_handling():\n    writer = PdfWriter()\n    embedded_file = writer.add_attachment(\"test.txt\", b\"content\")\n\n    embedded_file.alternative_name = TextStringObject(\"Name\")\n    embedded_file.description = TextStringObject(\"Description\")\n    embedded_file.subtype = NameObject(\"/text/plain\")\n    embedded_file.size = NumberObject(1024)\n    embedded_file.checksum = ByteStringObject(b\"checksum\")\n\n    embedded_file.alternative_name = None\n    embedded_file.description = None\n    embedded_file.subtype = None\n    embedded_file.size = None\n    embedded_file.checksum = None\n\n    assert embedded_file.alternative_name is None\n    assert embedded_file.description is None\n    assert embedded_file.subtype is None\n    assert embedded_file.size is None\n    assert embedded_file.checksum is None\n\n\ndef test_embedded_file__delete_without_parent():\n    attachment = EmbeddedFile(name=\"test.txt\", pdf_object=DictionaryObject())\n    with pytest.raises(PyPdfError, match=r\"^Parent required to delete file from document\\.$\"):\n        attachment.delete()\n\n\ndef test_embedded_file__delete_known():\n    writer = PdfWriter()\n    writer.add_blank_page(100, 100)\n    attachment = writer.add_attachment(\"test.txt\", b\"content\")\n    writer.add_attachment(\"test2.txt\", b\"content2\")\n\n    attachments = list(writer.attachment_list)\n    assert len(attachments) == 2\n    attachment.delete()\n    with pytest.raises(PdfReadError, match=r\"^/EF entry not found: {}$\"):\n        _ = attachment.content\n\n    attachments = list(writer.attachment_list)\n    assert len(attachments) == 1\n    assert attachments[0].name == \"test2.txt\"\n\n    # Delete second time.\n    with pytest.raises(PyPdfError, match=r\"^File not found in parent object\\.$\"):\n        attachment.delete()\n\n\ndef test_embedded_file__delete__no_indirect_reference():\n    writer = PdfWriter()\n    writer.add_blank_page(100, 100)\n\n    # Add an attachment and replace the indirect reference in the name tree\n    # by the dictionary itself. This is how pypdf <= 6.1.0 would embed files\n    # and thus should be supported as well.\n    embedded_file = writer.add_attachment(\"test.txt\", b\"Hello, World!\")\n    assert embedded_file.pdf_object.indirect_reference == IndirectObject(6, 0, writer)\n    embedded_file._parent[-1] = embedded_file.pdf_object.get_object()\n\n    embedded_file.delete()\n    attachments = list(writer.attachment_list)\n    assert len(attachments) == 0\n\n\n@pytest.mark.enable_socket\ndef test_embedded_file__create__kids_based_name_tree():\n    \"\"\"Test for issue #3473.\"\"\"\n    url = \"https://github.com/user-attachments/files/18691309/embedded_files_kids.pdf\"\n    name = \"embedded_files_kids.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n\n    writer.add_attachment(\"test.pdf\", b\"content\")\n\n    assert dict(writer.attachments) == {\n        \"factur-x.xml\": [\n            (\n                b\"Hello World!\\n\\nLorem ipsum dolor sit amet, consetetur sad\"\n                b\"ipscing elitr, sed diam nonumy eirmod tempor\\ninvidunt ut\"\n                b\" labore et dolore magna aliquyam erat, sed diam voluptua\"\n                b\". At vero eos et accusam\\net justo duo dolores et ea rebu\"\n                b\"m. Stet clita kasd gubergren, no sea takimata sanctus es\"\n                b\"t Lorem\\nipsum dolor sit amet. Lorem ipsum dolor sit amet\"\n                b\", consetetur sadipscing elitr, sed diam\\nnonumy eirmod te\"\n                b\"mpor invidunt ut labore et dolore magna aliquyam erat, s\"\n                b\"ed diam voluptua.\\nAt vero eos et accusam et justo duo do\"\n                b\"lores et ea rebum. Stet clita kasd gubergren, no sea\\ntak\"\n                b\"imata sanctus est Lorem ipsum dolor sit amet.\\n\"\n            )\n        ],\n        \"test.pdf\": [b\"content\"]\n    }\n\n    attachments = list(writer.attachment_list)\n    assert len(attachments) == 2\n    assert writer.root_object[\"/Names\"][\"/EmbeddedFiles\"][\"/Names\"] == [\n        \"factur-x.xml\", attachments[0].pdf_object.indirect_reference,\n        \"test.pdf\", attachments[1].pdf_object.indirect_reference,\n    ]\n\n\ndef test_embedded_file__create__neither_kids_nor_names():\n    writer = PdfWriter()\n    writer.add_blank_page(100, 100)\n\n    # Add an attachment and remove the corresponding /Names key.\n    writer.add_attachment(\"test.txt\", b\"Hello, World!\")\n    del writer.root_object[\"/Names\"][\"/EmbeddedFiles\"][\"/Names\"]\n\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^Got neither Names nor Kids in embedded files tree\\.$\"):\n        writer.add_attachment(\"test2.txt\", b\"content2\")\n\n\ndef test_embedded_file__get_insertion_index():\n    # Empty list.\n    assert EmbeddedFile._get_insertion_index(ArrayObject(), \"test.txt\") == 0\n\n    # One mismatching entry.\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"dummy.txt\"), NullObject()]),\n        \"test.txt\"\n    ) == 2\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"xxx.txt\"), NullObject()]),\n        \"test.txt\"\n    ) == 0\n\n    # Multiple entries.\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"dummy.txt\"), NullObject(), TextStringObject(\"xxx.txt\"), NullObject()]),\n        \"test.txt\"\n    ) == 2\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"xxx.txt\"), NullObject(), TextStringObject(\"yyy.txt\"), NullObject()]),\n        \"test.txt\"\n    ) == 0\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"aaa.txt\"), NullObject(), TextStringObject(\"bbb.txt\"), NullObject()]),\n        \"test.txt\"\n    ) == 4\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([\n            TextStringObject(\"aaa.txt\"), NullObject(),\n            TextStringObject(\"test.txt\"), NullObject(),\n            TextStringObject(\"zzz.txt\"), NullObject()\n        ]),\n        \"test.txt\"\n    ) == 4\n\n    # Length.\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"a\"), NullObject()]),\n        \"aa\"\n    ) == 2\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"a\"), NullObject()]),\n        \"a\"\n    ) == 2\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"aaa\"), NullObject()]),\n        \"aa\"\n    ) == 0\n\n    # Special characters.\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"café\"), NullObject()]),\n        \"cafe\"\n    ) == 0\n    assert EmbeddedFile._get_insertion_index(\n        ArrayObject([TextStringObject(\"Tun\"), NullObject()]),\n        \"Tür\"\n    ) == 2\n\n\ndef test_embedded_file__order():\n    writer = PdfWriter()\n    writer.add_blank_page(100, 100)\n\n    attachment1 = writer.add_attachment(\"test.txt\", \"content\")\n    attachment2 = writer.add_attachment(\"abc.txt\", \"content\")\n    attachment3 = writer.add_attachment(\"xyz.txt\", \"content\")\n    attachment4 = writer.add_attachment(\"test.txt\", \"content2\")\n\n    assert dict(writer.attachments) == {\n        \"abc.txt\": [b\"content\"],\n        \"test.txt\": [b\"content\", b\"content2\"],\n        \"xyz.txt\": [b\"content\"]\n    }\n\n    assert writer.root_object[\"/Names\"][\"/EmbeddedFiles\"][\"/Names\"] == [\n        \"abc.txt\", attachment2.pdf_object.indirect_reference,\n        \"test.txt\", attachment1.pdf_object.indirect_reference,\n        \"test.txt\", attachment4.pdf_object.indirect_reference,\n        \"xyz.txt\", attachment3.pdf_object.indirect_reference,\n    ]\n"
  },
  {
    "path": "tests/generic/test_image_inline.py",
    "content": "\"\"\"Test the pypdf.generic._image_inline module.\"\"\"\nfrom io import BytesIO\n\nimport pytest\n\nfrom pypdf import PdfReader\nfrom pypdf.errors import PdfReadError\nfrom pypdf.generic._image_inline import is_followed_by_binary_data\nfrom tests import get_data_from_url\n\n\ndef test_is_followed_by_binary_data():\n    # Empty/too short stream.\n    stream = BytesIO()\n    assert not is_followed_by_binary_data(stream)\n\n    stream = BytesIO(b\" q\\n\")\n    assert not is_followed_by_binary_data(stream)\n\n    # byte < 32 and no whitespace.\n    stream = BytesIO(b\"\\x00\\x11\\x13\\x37\")\n    assert is_followed_by_binary_data(stream)\n    assert stream.read(1) == b\"\\x00\"\n    assert is_followed_by_binary_data(stream)\n    assert stream.read(1) == b\"\\x11\"\n    assert is_followed_by_binary_data(stream)\n    assert stream.read() == b\"\\x13\\x37\"\n\n    # byte < 32, but whitespace.\n    stream = BytesIO(b\" q\\n\")\n    assert not is_followed_by_binary_data(stream)\n\n    # Whitespace only.\n    stream = BytesIO(b\" \\n\\n\\n  \\n\")\n    assert not is_followed_by_binary_data(stream)\n\n    # No `operator_end`.\n    stream = BytesIO(b\"\\n\\n\\n\\n\\n\\n\\n\\nBT\\n\")\n    assert not is_followed_by_binary_data(stream)\n\n    # Operator length is <= 3.\n    stream = BytesIO(b\"\\n\\n\\n\\n\\n\\n\\nBT\\n\")\n    assert not is_followed_by_binary_data(stream)\n\n    # Operator length is > 3.\n    stream = BytesIO(b\"\\n\\n\\n\\n\\nTEST\\n\")\n    assert is_followed_by_binary_data(stream)\n\n    # Just characters.\n    stream = BytesIO(b\" ABCDEF\")\n    assert is_followed_by_binary_data(stream)\n\n    # No `operator_start`.\n    stream = BytesIO(b\"ABCDEFG\")\n    assert is_followed_by_binary_data(stream)\n\n    # Name object.\n    stream = BytesIO(b\"/R10 gs\\n/R12 cs\\n\")\n    assert not is_followed_by_binary_data(stream)\n\n    # Numbers.\n    stream = BytesIO(b\"1337 42 m\\n\")\n    assert not is_followed_by_binary_data(stream)\n\n    stream = BytesIO(b\"1234.56 42 13 37 10 20 c\\n\")\n    assert not is_followed_by_binary_data(stream)\n\n\n@pytest.mark.enable_socket\ndef test_extract_inline_dct__early_end_of_file():\n    url = \"https://github.com/user-attachments/files/23056988/inline_dct__early_eof.pdf\"\n    name = \"inline_dct__early_eof.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[0]\n\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^Unexpected end of stream$\"):\n        page.images[0].image.load()\n\n\n@pytest.mark.enable_socket\ndef test_extract_inline_dct__multiple_eod():\n    url = \"https://github.com/user-attachments/files/23900687/cedolini_esempio-1.pdf\"\n    name = \"issue3517.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    for page in reader.pages:\n        for image in page.images:\n            _ = image.image.load()\n"
  },
  {
    "path": "tests/generic/test_image_xobject.py",
    "content": "\"\"\"Test the pypdf.generic._image_xobject module.\"\"\"\nfrom io import BytesIO\n\nimport pytest\nfrom PIL import Image\n\nfrom pypdf import PdfReader\nfrom pypdf._utils import Version\nfrom pypdf.constants import FilterTypes, ImageAttributes, StreamAttributes\nfrom pypdf.errors import EmptyImageDataError, PdfReadError\nfrom pypdf.generic import ArrayObject, DecodedStreamObject, NameObject, NumberObject, StreamObject, TextStringObject\nfrom pypdf.generic._image_xobject import _extended_image_from_bytes, _handle_flate, _xobj_to_image\n\nfrom .. import RESOURCE_ROOT, get_data_from_url\nfrom ..utils import get_image_data\n\n\n@pytest.mark.enable_socket\ndef test_get_imagemode_recursion_depth():\n    \"\"\"Avoid infinite recursion for nested color spaces.\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12814018/out1.pdf\"\n    name = \"issue2240.pdf\"\n    # Simple example: Just let the color space object reference itself.\n    # The alternative would be to generate a chain of referencing objects.\n    content = get_data_from_url(url, name=name)\n    source = b\"\\n10 0 obj\\n[ /DeviceN [ /HKS#2044#20K /Magenta /Yellow /Black ] 7 0 R 11 0 R 12 0 R ]\\nendobj\\n\"\n    target = b\"\\n10 0 obj\\n[ /DeviceN [ /HKS#2044#20K /Magenta /Yellow /Black ] 10 0 R 11 0 R 12 0 R ]\\nendobj\\n\"\n    reader = PdfReader(BytesIO(content.replace(source, target)))\n    with pytest.raises(\n        PdfReadError,\n        match=r\"Color spaces nested too deeply\\. If required, consider increasing MAX_IMAGE_MODE_NESTING_DEPTH\\.\",\n    ):\n        reader.pages[0].images[0]\n\n\ndef test_handle_flate__image_mode_1(caplog):\n    data = b\"\\x00\\xe0\\x00\"\n    lookup = DecodedStreamObject()\n    expected_data = (\n        (66, 66, 66),\n        (66, 66, 66),\n        (66, 66, 66),\n        (0, 19, 55),\n        (0, 19, 55),\n        (0, 19, 55),\n        (66, 66, 66),\n        (66, 66, 66),\n        (66, 66, 66),\n    )\n\n    # No trailing data.\n    lookup.set_data(b\"\\x42\\x42\\x42\\x00\\x13\\x37\")\n    result = _handle_flate(\n        size=(3, 3),\n        data=data,\n        mode=\"1\",\n        color_space=ArrayObject(\n            [NameObject(\"/Indexed\"), NameObject(\"/DeviceRGB\"), NumberObject(1), lookup]\n        ),\n        colors=2,\n        obj_as_text=\"dummy\",\n    )\n    assert expected_data == get_image_data(result[0])\n    assert not caplog.text\n\n    # Trailing whitespace.\n    lookup.set_data(b\"\\x42\\x42\\x42\\x00\\x13\\x37  \\x0a\")\n    result = _handle_flate(\n        size=(3, 3),\n        data=data,\n        mode=\"1\",\n        color_space=ArrayObject(\n            [NameObject(\"/Indexed\"), NameObject(\"/DeviceRGB\"), NumberObject(1), lookup]\n        ),\n        colors=2,\n        obj_as_text=\"dummy\",\n    )\n    assert expected_data == get_image_data(result[0])\n    assert not caplog.text\n\n    # Trailing non-whitespace character.\n    lookup.set_data(b\"\\x42\\x42\\x42\\x00\\x13\\x37\\x12\")\n    result = _handle_flate(\n        size=(3, 3),\n        data=data,\n        mode=\"1\",\n        color_space=ArrayObject(\n            [\n                NameObject(\"/Indexed\"),\n                NameObject(\"/DeviceRGB\"),\n                NumberObject(1),\n                lookup,\n            ]\n        ),\n        colors=2,\n        obj_as_text=\"dummy\",\n    )\n    assert expected_data == get_image_data(result[0])\n    assert \"Too many lookup values: Expected 6, got 7.\" in caplog.text\n\n    # Not enough lookup data.\n    # `\\xe0` of the original input (the middle part) does not use `0x37 = 55` for the lookup\n    # here, but received a custom padding of `0`.\n    lookup.set_data(b\"\\x42\\x42\\x42\\x00\\x13\")\n    caplog.clear()\n    expected_short_data = tuple([entry if entry[0] == 66 else (0, 19, 0) for entry in expected_data])\n    result = _handle_flate(\n        size=(3, 3),\n        data=data,\n        mode=\"1\",\n        color_space=ArrayObject(\n            [\n                NameObject(\"/Indexed\"),\n                NameObject(\"/DeviceRGB\"),\n                NumberObject(1),\n                lookup,\n            ]\n        ),\n        colors=2,\n        obj_as_text=\"dummy\",\n    )\n    assert expected_short_data == get_image_data(result[0])\n    assert \"Not enough lookup values: Expected 6, got 5.\" in caplog.text\n\n\ndef test_extended_image_frombytes_zero_data():\n    mode = \"RGB\"\n    size = (1, 1)\n    data = b\"\"\n\n    with pytest.raises(EmptyImageDataError, match=r\"Data is 0 bytes, cannot process an image from empty data\\.\"):\n        _extended_image_from_bytes(mode, size, data)\n\n\ndef test_handle_flate__autodesk_indexed():\n    reader = PdfReader(RESOURCE_ROOT / \"AutoCad_Diagram.pdf\")\n    page = reader.pages[0]\n    for name, image in page.images.items():\n        assert name.startswith(\"/\")\n        image.image.load()\n\n    data = RESOURCE_ROOT.joinpath(\"AutoCad_Diagram.pdf\").read_bytes()\n    data = data.replace(b\"/DeviceRGB\\x00255\", b\"/DeviceRGB\")\n    reader = PdfReader(BytesIO(data))\n    page = reader.pages[0]\n    with pytest.raises(\n            PdfReadError,\n            match=r\"^Expected color space with 4 values, got 3: \\['/Indexed', '/DeviceRGB', '\\\\x00\\\\x80\\\\x00\\\\x80\\\\x80耀\"  # noqa: E501\n    ):\n        for name, _image in page.images.items():  # noqa: PERF102\n            assert name.startswith(\"/\")\n\n\n@pytest.mark.enable_socket\ndef test_get_mode_and_invert_color():\n    url = \"https://github.com/user-attachments/files/18381726/tika-957721.pdf\"\n    name = \"tika-957721.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[12]\n    for _name, image in page.images.items():  # noqa: PERF102\n        image.image.load()\n\n\n@pytest.mark.enable_socket\ndef test_get_imagemode__empty_array():\n    url = \"https://github.com/user-attachments/files/23050451/poc.pdf\"\n    name = \"issue3499.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[0]\n\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^ColorSpace field not found in .+\"):\n        page.images[0].image.load()\n\n\ndef test_p_image_with_alpha_mask():\n    # Generate the base image. Use TIFF as this is easy to do on the fly.\n    image = Image.new(mode=\"P\", size=(10, 10), color=0)\n    image_data = BytesIO()\n    image.save(image_data, format=\"tiff\")\n\n    # Set the common values.\n    x_object = StreamObject()\n    mask_object = StreamObject()\n    for obj in [x_object, mask_object]:\n        obj[NameObject(ImageAttributes.WIDTH)] = NumberObject(image.width)\n        obj[NameObject(ImageAttributes.HEIGHT)] = NumberObject(image.height)\n        obj[NameObject(StreamAttributes.FILTER)] = NameObject(FilterTypes.CCITT_FAX_DECODE)\n\n    # Set the basic image data.\n    x_object.set_data(image_data.getvalue())\n    x_object[NameObject(ImageAttributes.COLOR_SPACE)] = TextStringObject(\"palette\")\n\n    # Generate the mask image. Will be a diagonal white stripe.\n    image = Image.new(mode=\"1\", size=(image.width, image.height))\n    [image.putpixel((i, i), 1) for i in range(10)]\n    image_data = BytesIO()\n    image.save(image_data, format=\"tiff\")\n\n    # Set the mask data.\n    mask_object.set_data(image_data.getvalue())\n    mask_object[NameObject(ImageAttributes.COLOR_SPACE)] = TextStringObject(\"1bit\")\n\n    # Add the mask to the image.\n    x_object[NameObject(\"/SMask\")] = mask_object\n\n    # Generate the output image and make sure that the diagonal stripe is present.\n    extension, data, image = _xobj_to_image(x_object)\n    assert extension == \".png\"\n    assert data.startswith(b\"\\x89PNG\")\n    for i in range(10):\n        for j in range(10):\n            assert image.getpixel((i, j)) == (0, 0, 0, 255 * (i == j))\n\n\n@pytest.mark.enable_socket\ndef test_handle_flate__icc_based__image_mode_1():\n    url = \"https://github.com/user-attachments/files/23756943/pypdf_bug_3534_iccbased.pdf\"\n    name = \"issue3534.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[0]\n\n    image = page.images[0].image\n    assert image is not None\n    image.load()\n    assert image.size == (64, 64)\n    assert image.mode == \"1\"\n\n    for y in range(64):\n        for x in range(64):\n            # Determine which chess square this pixel belongs to\n            square_x = x // 8\n            square_y = y // 8\n            is_black_square = (square_x + square_y) % 2 == 1\n            assert image.getpixel((x, y)) == 255 * int(not is_black_square)\n\n\n@pytest.mark.skipif(\n    condition=Version(Image.__version__) < Version(\"12.1.0\"),\n    reason=\"Unsuitable Pillow version.\"\n)\ndef test_handle_jpx__explicit_decode():\n    stream = StreamObject()\n    stream[NameObject(\"/BitsPerComponent\")] = NumberObject(8)\n    stream[NameObject(\"/ColorSpace\")] = NameObject(\"/DeviceCMYK\")\n    stream[NameObject(\"/Decode\")] = ArrayObject([1, 0, 1, 0, 1, 0, 1, 0])\n    stream[NameObject(\"/Filter\")] = NameObject(\"/JPXDecode\")\n    stream[NameObject(\"/Height\")] = NumberObject(16)\n    stream[NameObject(\"/Width\")] = NumberObject(16)\n\n    image = Image.new(mode=\"CMYK\", size=(16, 16))\n    [image.putpixel((i, i), 255) for i in range(16)]\n    image_data = BytesIO()\n    image.save(image_data, format=\"JPEG2000\")\n    stream.set_data(image_data.getvalue())\n    image.save(image_data, format=\"JPEG2000\")\n\n    result = _xobj_to_image(x_object=stream)[2]\n    for y in range(16):\n        for x in range(16):\n            assert result.getpixel((x, y)) == (255 * (x != y), 255, 255, 255), (x, y)\n            assert image.getpixel((x, y)) == (255 * (x == y), 0, 0, 0), (x, y)\n"
  },
  {
    "path": "tests/generic/test_link.py",
    "content": "\"\"\"Test the pypdf.generic._link module.\"\"\"\nfrom io import BytesIO\n\nimport pytest\n\nfrom pypdf import PageObject, PdfReader, PdfWriter\nfrom pypdf.generic import ArrayObject, NameObject, NullObject, extract_links\nfrom tests import get_data_from_url\n\n\n@pytest.mark.enable_socket\ndef test_extract_links__null_object_in_old_page():\n    url = \"https://github.com/user-attachments/files/25507697/sample.pdf\"\n    name = \"issue3656.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))\n\n    writer = PdfWriter()\n    writer.append(reader)\n\n\ndef test_extract_links(caplog):\n    page1 = PageObject()\n    page2 = PageObject()\n\n    # No annotations.\n    assert extract_links(page1, page2) == []\n    assert caplog.messages == []\n\n    # Only old annotations.\n    page1[NameObject(\"/Annots\")] = NullObject()\n    assert extract_links(page1, page2) == []\n    assert caplog.messages == []\n    caplog.clear()\n\n    page1[NameObject(\"/Annots\")] = ArrayObject([NullObject()])\n    assert extract_links(page1, page2) == []\n    assert caplog.messages == []\n    caplog.clear()\n\n    # Both old and new annotations.\n    page2[NameObject(\"/Annots\")] = ArrayObject([NullObject()])\n    assert extract_links(page1, page2) == []\n    assert caplog.messages == []  # Same size.\n    caplog.clear()\n\n    page2[NameObject(\"/Annots\")] = NullObject()\n    assert extract_links(page1, page2) == []\n    assert caplog.messages == []\n    caplog.clear()\n\n    # Only new annotations.\n    del page1[NameObject(\"/Annots\")]\n    page2[NameObject(\"/Annots\")] = ArrayObject([NullObject()])\n    assert extract_links(page1, page2) == []\n    assert caplog.messages == []\n"
  },
  {
    "path": "tests/scripts/__init__.py",
    "content": ""
  },
  {
    "path": "tests/scripts/data/commits__version_4_0_1.json",
    "content": "[\n  {\n    \"sha\": \"b7bfd0d7eddfd0865a94cc9e7027df6596242cf7\",\n    \"node_id\": \"C_kwDOAC-ZndoAKGI3YmZkMGQ3ZWRkZmQwODY1YTk0Y2M5ZTcwMjdkZjY1OTYyNDJjZjc\",\n    \"commit\": {\n      \"author\": {\n        \"name\": \"rsinger417\",\n        \"email\": \"159086296+rsinger417@users.noreply.github.com\",\n        \"date\": \"2024-02-13T21:42:56Z\"\n      },\n      \"committer\": {\n        \"name\": \"GitHub\",\n        \"email\": \"noreply@github.com\",\n        \"date\": \"2024-02-13T21:42:56Z\"\n      },\n      \"message\": \"BUG: Use NumberObject for /Border elements of annotations (#2451)\\n\\nAs defined in Table 164 – Entries common to all annotation dictionaries, the /Border Array consists of NumberObjects. Previously, pypdf used NameObject which is wrong.\\r\\n\\r\\nThe previous version caused a warning in the class NameObject: \\\"Incorrect first char in NameObject:({self})\\\".\\r\\n\\r\\nFixes #2444\",\n      \"tree\": {\n        \"sha\": \"e75b96ca1bb3e60a696bd57c5bb5aac9e7c5651b\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/trees/e75b96ca1bb3e60a696bd57c5bb5aac9e7c5651b\"\n      },\n      \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/commits/b7bfd0d7eddfd0865a94cc9e7027df6596242cf7\",\n      \"comment_count\": 0,\n      \"verification\": {\n        \"verified\": true,\n        \"reason\": \"valid\",\n        \"signature\": \"-----BEGIN PGP SIGNATURE-----\\n\\nwsFcBAABCAAQBQJly+JgCRC1aQ7uu5UhlAAA56IQAKmAJws3vIKAR1dTx6e8fgp+\\nWFMSZub7HGMi0Wz6MKF+hp1fxTNheBwlWVvVHuIGggtg9QSFkpHmi5Qqn+IUHJq1\\nI5Jst6Il3lCF2UXUboyN+XbS/lo6rXHriz+Yi7Xwgj+JulHnruFvFEU40AdKnI1w\\n88Wh94KXEJqQ6nyP4R2qpDLLlhQ0/4FTIZCWfw8XK1vPmTQwP0ZroL5N7s1pq2s9\\nBBDtvcxTE1EbWIyyMzAiNByxdaTakqNLRMq80saiArR4t6f1H4v8dgYep/6R5dxU\\n2GGXjh6JOS6xNObrSNvuFanrgAxZoft255OGsU5Y/2yxryp+Bs1QO/PXYXch2ERN\\nXyYQKxp886PRcL1vGukksqx5t8Oc781z7RHV/QCIJ5Ry66vC7zDmkk2+Eq6gwWMr\\nHzTg3eQ2DL+I4CsNIezb470UOKdIWu9SdQmOrGeUAnQ0rB0V7VOe9n1buPmMP/e0\\ngXcq/BNFaNWTCyIHv1XgB6G516k4zM1F5j1BF0GCrhrdX8lXMZUB+WI3V8CsRObI\\naKdnE9aGBJPZCFN5O+92ntKt7tUQQLmLNPDgYZktzBg73ejRlpQ4zOBRBFiSNfPj\\nRrNzBn1LFtSLxc7/MsP1lLl5NtI0oLWrZMjym3CcAJQYmqsZCv22b94R4hSorAwW\\nGr2/wRH3JQVIToDU1/W4\\n=JYMh\\n-----END PGP SIGNATURE-----\\n\",\n        \"payload\": \"tree e75b96ca1bb3e60a696bd57c5bb5aac9e7c5651b\\nparent 8cacb0fc8fee9920b0515d1289e6ee8191eb3f21\\nauthor rsinger417 <159086296+rsinger417@users.noreply.github.com> 1707860576 -0600\\ncommitter GitHub <noreply@github.com> 1707860576 +0100\\n\\nBUG: Use NumberObject for /Border elements of annotations (#2451)\\n\\nAs defined in Table 164 – Entries common to all annotation dictionaries, the /Border Array consists of NumberObjects. Previously, pypdf used NameObject which is wrong.\\r\\n\\r\\nThe previous version caused a warning in the class NameObject: \\\"Incorrect first char in NameObject:({self})\\\".\\r\\n\\r\\nFixes #2444\"\n      }\n    },\n    \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/b7bfd0d7eddfd0865a94cc9e7027df6596242cf7\",\n    \"html_url\": \"https://github.com/py-pdf/pypdf/commit/b7bfd0d7eddfd0865a94cc9e7027df6596242cf7\",\n    \"comments_url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/b7bfd0d7eddfd0865a94cc9e7027df6596242cf7/comments\",\n    \"author\": {\n      \"login\": \"rsinger417\",\n      \"id\": 159086296,\n      \"node_id\": \"U_kgDOCXt22A\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/159086296?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/rsinger417\",\n      \"html_url\": \"https://github.com/rsinger417\",\n      \"followers_url\": \"https://api.github.com/users/rsinger417/followers\",\n      \"following_url\": \"https://api.github.com/users/rsinger417/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/rsinger417/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/rsinger417/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/rsinger417/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/rsinger417/orgs\",\n      \"repos_url\": \"https://api.github.com/users/rsinger417/repos\",\n      \"events_url\": \"https://api.github.com/users/rsinger417/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/rsinger417/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"committer\": {\n      \"login\": \"web-flow\",\n      \"id\": 19864447,\n      \"node_id\": \"MDQ6VXNlcjE5ODY0NDQ3\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/19864447?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/web-flow\",\n      \"html_url\": \"https://github.com/web-flow\",\n      \"followers_url\": \"https://api.github.com/users/web-flow/followers\",\n      \"following_url\": \"https://api.github.com/users/web-flow/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/web-flow/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/web-flow/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/web-flow/orgs\",\n      \"repos_url\": \"https://api.github.com/users/web-flow/repos\",\n      \"events_url\": \"https://api.github.com/users/web-flow/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/web-flow/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"parents\": [\n      {\n        \"sha\": \"8cacb0fc8fee9920b0515d1289e6ee8191eb3f21\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/8cacb0fc8fee9920b0515d1289e6ee8191eb3f21\",\n        \"html_url\": \"https://github.com/py-pdf/pypdf/commit/8cacb0fc8fee9920b0515d1289e6ee8191eb3f21\"\n      }\n    ]\n  },\n  {\n    \"sha\": \"8cacb0fc8fee9920b0515d1289e6ee8191eb3f21\",\n    \"node_id\": \"C_kwDOAC-ZndoAKDhjYWNiMGZjOGZlZTk5MjBiMDUxNWQxMjg5ZTZlZTgxOTFlYjNmMjE\",\n    \"commit\": {\n      \"author\": {\n        \"name\": \"Stefan\",\n        \"email\": \"96178532+stefan6419846@users.noreply.github.com\",\n        \"date\": \"2024-02-13T21:33:37Z\"\n      },\n      \"committer\": {\n        \"name\": \"GitHub\",\n        \"email\": \"noreply@github.com\",\n        \"date\": \"2024-02-13T21:33:37Z\"\n      },\n      \"message\": \"DOC: Document easier way to update metadata (#2454)\",\n      \"tree\": {\n        \"sha\": \"79408055102933a8d62a3d1ec49df9f25fd5e963\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/trees/79408055102933a8d62a3d1ec49df9f25fd5e963\"\n      },\n      \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/commits/8cacb0fc8fee9920b0515d1289e6ee8191eb3f21\",\n      \"comment_count\": 0,\n      \"verification\": {\n        \"verified\": true,\n        \"reason\": \"valid\",\n        \"signature\": \"-----BEGIN PGP SIGNATURE-----\\n\\nwsFcBAABCAAQBQJly+AxCRC1aQ7uu5UhlAAAd2kQAB8venK8xBYafzASXTRV2ye/\\nOkGIVobepYja0lKIgZpipPlmDbDnHB2UptWRMpAd7rNiL9iYnSqBNxOmCvfux99/\\nqx0h9XuYzSZ1KJ6cK43ab1ErSsrjvLpO/LsMmtakzZR7BrFUjO6mIE3YuU0GmhKM\\nNUPngT+A6/Lxz6Z+UwqkeylkcDj+90gNAPiKY2yr+mKmg99RI5Xqvm7j++vT3bPF\\nJQmr46w0aiGW30Von0JAtu/IvprGksrfHWALFIYMHnJCaXJdv2mPJ8mwiLew/o4L\\n0uicPmwnDvS7VdCObi6EKbEP4ptgierco8pAMVRpkUpnmu8ObgT7ZzPLT6iay6U1\\n2Gtc0zYXlcVSo4JQW9iE9zrGMk91m+BmIOZAhJsgfdz4DewCWCBxmz4+u0wlIlzN\\n6JwwZQsW3Yq/P/gJ9qxBUKPe3SAcs3jz2VG3fiOt/HzAA6YLAUPUDxnhwvWhju5i\\nLiQEApEnIri4OeNhqYmOjsEI3aV/3s6jE2fEiGPDkQW61yMAAiSVgZk3BcnFwZzL\\nHrf+JWTRnosPFOhkRoTH3AOzmOWOKUCCUmVdC8nKn4Sp0tp+31HIH/h3LmVflBLy\\nXHwPT/6OwW1yBzueYM6LWwovNlk3AS2g19fgylOmokIkrnlmi4nCwD30hM8plEFk\\ni7hsSGE/rfsjTt5lTBip\\n=1RO9\\n-----END PGP SIGNATURE-----\\n\",\n        \"payload\": \"tree 79408055102933a8d62a3d1ec49df9f25fd5e963\\nparent 3fb63f7e3839ce39ac98978c996f3086ba230a20\\nauthor Stefan <96178532+stefan6419846@users.noreply.github.com> 1707860017 +0100\\ncommitter GitHub <noreply@github.com> 1707860017 +0100\\n\\nDOC: Document easier way to update metadata (#2454)\\n\\n\"\n      }\n    },\n    \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/8cacb0fc8fee9920b0515d1289e6ee8191eb3f21\",\n    \"html_url\": \"https://github.com/py-pdf/pypdf/commit/8cacb0fc8fee9920b0515d1289e6ee8191eb3f21\",\n    \"comments_url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/8cacb0fc8fee9920b0515d1289e6ee8191eb3f21/comments\",\n    \"author\": {\n      \"login\": \"stefan6419846\",\n      \"id\": 96178532,\n      \"node_id\": \"U_kgDOBbuRZA\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/96178532?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/stefan6419846\",\n      \"html_url\": \"https://github.com/stefan6419846\",\n      \"followers_url\": \"https://api.github.com/users/stefan6419846/followers\",\n      \"following_url\": \"https://api.github.com/users/stefan6419846/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/stefan6419846/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/stefan6419846/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/stefan6419846/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/stefan6419846/orgs\",\n      \"repos_url\": \"https://api.github.com/users/stefan6419846/repos\",\n      \"events_url\": \"https://api.github.com/users/stefan6419846/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/stefan6419846/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"committer\": {\n      \"login\": \"web-flow\",\n      \"id\": 19864447,\n      \"node_id\": \"MDQ6VXNlcjE5ODY0NDQ3\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/19864447?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/web-flow\",\n      \"html_url\": \"https://github.com/web-flow\",\n      \"followers_url\": \"https://api.github.com/users/web-flow/followers\",\n      \"following_url\": \"https://api.github.com/users/web-flow/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/web-flow/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/web-flow/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/web-flow/orgs\",\n      \"repos_url\": \"https://api.github.com/users/web-flow/repos\",\n      \"events_url\": \"https://api.github.com/users/web-flow/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/web-flow/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"parents\": [\n      {\n        \"sha\": \"3fb63f7e3839ce39ac98978c996f3086ba230a20\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/3fb63f7e3839ce39ac98978c996f3086ba230a20\",\n        \"html_url\": \"https://github.com/py-pdf/pypdf/commit/3fb63f7e3839ce39ac98978c996f3086ba230a20\"\n      }\n    ]\n  },\n  {\n    \"sha\": \"3fb63f7e3839ce39ac98978c996f3086ba230a20\",\n    \"node_id\": \"C_kwDOAC-ZndoAKDNmYjYzZjdlMzgzOWNlMzlhYzk4OTc4Yzk5NmYzMDg2YmEyMzBhMjA\",\n    \"commit\": {\n      \"author\": {\n        \"name\": \"Stefan\",\n        \"email\": \"96178532+stefan6419846@users.noreply.github.com\",\n        \"date\": \"2024-02-04T20:32:49Z\"\n      },\n      \"committer\": {\n        \"name\": \"GitHub\",\n        \"email\": \"noreply@github.com\",\n        \"date\": \"2024-02-04T20:32:49Z\"\n      },\n      \"message\": \"TST: Avoid catching not emitted warnings (#2429)\\n\\nFix compatibility with pytest==8. \\r\\n\\r\\nRelevant upstream change: pytest-dev/pytest#9288\\r\\n\\r\\nFixes #2427\",\n      \"tree\": {\n        \"sha\": \"c96cab2f682f6db4c84440e26869b4d9de6a2bab\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/trees/c96cab2f682f6db4c84440e26869b4d9de6a2bab\"\n      },\n      \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/commits/3fb63f7e3839ce39ac98978c996f3086ba230a20\",\n      \"comment_count\": 0,\n      \"verification\": {\n        \"verified\": true,\n        \"reason\": \"valid\",\n        \"signature\": \"-----BEGIN PGP SIGNATURE-----\\n\\nwsFcBAABCAAQBQJlv/RxCRC1aQ7uu5UhlAAATI8QAB/yRz+hoeJVtjW/CePJo2Jv\\n451gPAo66s7JMG+PwcCiI8KAAUEusDbrJAmdq8rfqnShSB83h/7g/s5oFr/1lFyh\\noKkwoeMt6hGKtwEkTpa877gAWJ4ssRb1ymJoy7quPNlbFYtKngMC60Vc5TNEY1ZX\\nQ1FdIG5rVRBsA5H7fP7k0q2QC6w/Ns6nftpPFIf3JSVnillJ/RKDLhEfPw6/PMi0\\nnIJ2moTgTs6uyc4R0blR44BoElPd46ot/SQDcnHEwIQlWpfa2RIpulhF8qkO9fe3\\neCRBQ7TZXjedsG+Da71QKxRWRFdwPqO+HI4u5EHNLIaw8z9450jtbz5H1NhNIB1s\\nkIDTMgFXxGVuFKXfneduA6TAxrrJ12ONHcrUkN30y9AQ7Qe/B8LJ50iXQvo81SwZ\\nqTFBluB6WiVuMMMT0pHgNCjsAEPvaagPa10qvjVokXh1rXlzQiNwqBWCbwj2b6f4\\n8i3Vf9ufrK5p2WhsfO1aCW7Yc2C620sq66ic2Ck5cT2HLJA+cF1j7d7PT3/N0veo\\ncnpPpAFeUs2A6R/zL0yJSoPV+BLM0BdahxfsBlT9pdrdqvBA7JIGOC9c3msSWBZY\\n6GmfmsmWp0xdwYDJEzUL06shKjH6GlzhWvkjYuYH3myJBCoAjlUWCsJCvXWOD3iX\\nPID6Cv+BtDfu80muR94A\\n=bK1N\\n-----END PGP SIGNATURE-----\\n\",\n        \"payload\": \"tree c96cab2f682f6db4c84440e26869b4d9de6a2bab\\nparent 61b73d49778e8f0fb172d5323e67677c9974e420\\nauthor Stefan <96178532+stefan6419846@users.noreply.github.com> 1707078769 +0100\\ncommitter GitHub <noreply@github.com> 1707078769 +0100\\n\\nTST: Avoid catching not emitted warnings (#2429)\\n\\nFix compatibility with pytest==8. \\r\\n\\r\\nRelevant upstream change: pytest-dev/pytest#9288\\r\\n\\r\\nFixes #2427\"\n      }\n    },\n    \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/3fb63f7e3839ce39ac98978c996f3086ba230a20\",\n    \"html_url\": \"https://github.com/py-pdf/pypdf/commit/3fb63f7e3839ce39ac98978c996f3086ba230a20\",\n    \"comments_url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/3fb63f7e3839ce39ac98978c996f3086ba230a20/comments\",\n    \"author\": {\n      \"login\": \"stefan6419846\",\n      \"id\": 96178532,\n      \"node_id\": \"U_kgDOBbuRZA\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/96178532?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/stefan6419846\",\n      \"html_url\": \"https://github.com/stefan6419846\",\n      \"followers_url\": \"https://api.github.com/users/stefan6419846/followers\",\n      \"following_url\": \"https://api.github.com/users/stefan6419846/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/stefan6419846/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/stefan6419846/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/stefan6419846/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/stefan6419846/orgs\",\n      \"repos_url\": \"https://api.github.com/users/stefan6419846/repos\",\n      \"events_url\": \"https://api.github.com/users/stefan6419846/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/stefan6419846/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"committer\": {\n      \"login\": \"web-flow\",\n      \"id\": 19864447,\n      \"node_id\": \"MDQ6VXNlcjE5ODY0NDQ3\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/19864447?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/web-flow\",\n      \"html_url\": \"https://github.com/web-flow\",\n      \"followers_url\": \"https://api.github.com/users/web-flow/followers\",\n      \"following_url\": \"https://api.github.com/users/web-flow/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/web-flow/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/web-flow/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/web-flow/orgs\",\n      \"repos_url\": \"https://api.github.com/users/web-flow/repos\",\n      \"events_url\": \"https://api.github.com/users/web-flow/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/web-flow/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"parents\": [\n      {\n        \"sha\": \"61b73d49778e8f0fb172d5323e67677c9974e420\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/61b73d49778e8f0fb172d5323e67677c9974e420\",\n        \"html_url\": \"https://github.com/py-pdf/pypdf/commit/61b73d49778e8f0fb172d5323e67677c9974e420\"\n      }\n    ]\n  },\n  {\n    \"sha\": \"61b73d49778e8f0fb172d5323e67677c9974e420\",\n    \"node_id\": \"C_kwDOAC-ZndoAKDYxYjczZDQ5Nzc4ZThmMGZiMTcyZDUzMjNlNjc2NzdjOTk3NGU0MjA\",\n    \"commit\": {\n      \"author\": {\n        \"name\": \"CWKSC\",\n        \"email\": \"cwksc.person@gmail.com\",\n        \"date\": \"2024-02-03T08:02:35Z\"\n      },\n      \"committer\": {\n        \"name\": \"GitHub\",\n        \"email\": \"noreply@github.com\",\n        \"date\": \"2024-02-03T08:02:35Z\"\n      },\n      \"message\": \"DOC: Typo `Polyline` → `PolyLine` in adding-pdf-annotations.md (#2426)\",\n      \"tree\": {\n        \"sha\": \"9fb79466999d9d73c6ba15afdc76ce4d6f59c470\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/trees/9fb79466999d9d73c6ba15afdc76ce4d6f59c470\"\n      },\n      \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/commits/61b73d49778e8f0fb172d5323e67677c9974e420\",\n      \"comment_count\": 0,\n      \"verification\": {\n        \"verified\": true,\n        \"reason\": \"valid\",\n        \"signature\": \"-----BEGIN PGP SIGNATURE-----\\n\\nwsFcBAABCAAQBQJlvfMbCRC1aQ7uu5UhlAAA1X0QADKiCwRr4WJNPYlwgJKp/I4l\\nO/6H/uQ5XO6fSvkLNchzU+017kgwEfaPoEunTvb0rpAVfwjJknytCCaR5duQQ7np\\naP23J6gIViawM15qp20C53q+5r6NUZnerOIrMKMGLaRtsDMIePYT6zd5Q9KTnx5/\\nhF6X+LMx5zKDuXHmRV8Jhmii+8IQA4Ekgv/t+UNmkqpVQig603/IzPTVnUkY+Gcu\\nNEHb1W66bS5/BvMyrqwDx//Z0kpxJltNAoaVNAAz1+KgUm/NncBJcuR95U7ffGkO\\neoi9UqlF06YO4mkA7ZbAUfgujWEDsbCsnFuVsKe5RJLeRvidHQl7YJQg36mWV+He\\nNTMttZX2UJOiFLDeWeEoJ+DixBmXO5EbYsZlFDhGFizNAtY14zW/7RUioBao20DZ\\ny8RmYmmJW5p39h4gEvDD6+62lYqz+2SIPPSQdPNmANn2OOge43KArfyNYHbg4M13\\n6yLzMZuY61B5arfV0JdDlBdLncws3C7JjKljOfSCYCJ0/Bq8fKL5206k60U3jyru\\nRCoTtHFIWn1vzHgOf9cJMiIPWTa8HxH2+2mvZbDxmT+p4J5qgfRJ0BUw1i/klWqt\\n1OfmSgMgdkgPxczSjHd2gnnasClNy4yyrWsdDjRKaTEOMSIsb7DUm8UnD3oDs+nC\\nKMudDi6gn5ASiZf+ZsA5\\n=MAdq\\n-----END PGP SIGNATURE-----\\n\",\n        \"payload\": \"tree 9fb79466999d9d73c6ba15afdc76ce4d6f59c470\\nparent f851a532a5ec23b572d86bd7185b327a3fac6b58\\nauthor CWKSC <cwksc.person@gmail.com> 1706947355 +0800\\ncommitter GitHub <noreply@github.com> 1706947355 +0100\\n\\nDOC: Typo `Polyline` → `PolyLine` in adding-pdf-annotations.md (#2426)\\n\\n\"\n      }\n    },\n    \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/61b73d49778e8f0fb172d5323e67677c9974e420\",\n    \"html_url\": \"https://github.com/py-pdf/pypdf/commit/61b73d49778e8f0fb172d5323e67677c9974e420\",\n    \"comments_url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/61b73d49778e8f0fb172d5323e67677c9974e420/comments\",\n    \"author\": {\n      \"login\": \"CWKSC\",\n      \"id\": 53114952,\n      \"node_id\": \"MDQ6VXNlcjUzMTE0OTUy\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/53114952?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/CWKSC\",\n      \"html_url\": \"https://github.com/CWKSC\",\n      \"followers_url\": \"https://api.github.com/users/CWKSC/followers\",\n      \"following_url\": \"https://api.github.com/users/CWKSC/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/CWKSC/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/CWKSC/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/CWKSC/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/CWKSC/orgs\",\n      \"repos_url\": \"https://api.github.com/users/CWKSC/repos\",\n      \"events_url\": \"https://api.github.com/users/CWKSC/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/CWKSC/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"committer\": {\n      \"login\": \"web-flow\",\n      \"id\": 19864447,\n      \"node_id\": \"MDQ6VXNlcjE5ODY0NDQ3\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/19864447?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/web-flow\",\n      \"html_url\": \"https://github.com/web-flow\",\n      \"followers_url\": \"https://api.github.com/users/web-flow/followers\",\n      \"following_url\": \"https://api.github.com/users/web-flow/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/web-flow/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/web-flow/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/web-flow/orgs\",\n      \"repos_url\": \"https://api.github.com/users/web-flow/repos\",\n      \"events_url\": \"https://api.github.com/users/web-flow/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/web-flow/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"parents\": [\n      {\n        \"sha\": \"f851a532a5ec23b572d86bd7185b327a3fac6b58\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/f851a532a5ec23b572d86bd7185b327a3fac6b58\",\n        \"html_url\": \"https://github.com/py-pdf/pypdf/commit/f851a532a5ec23b572d86bd7185b327a3fac6b58\"\n      }\n    ]\n  },\n  {\n    \"sha\": \"f851a532a5ec23b572d86bd7185b327a3fac6b58\",\n    \"node_id\": \"C_kwDOAC-ZndoAKGY4NTFhNTMyYTVlYzIzYjU3MmQ4NmJkNzE4NWIzMjdhM2ZhYzZiNTg\",\n    \"commit\": {\n      \"author\": {\n        \"name\": \"dependabot[bot]\",\n        \"email\": \"49699333+dependabot[bot]@users.noreply.github.com\",\n        \"date\": \"2024-02-03T08:00:35Z\"\n      },\n      \"committer\": {\n        \"name\": \"GitHub\",\n        \"email\": \"noreply@github.com\",\n        \"date\": \"2024-02-03T08:00:35Z\"\n      },\n      \"message\": \"DEV: Bump codecov/codecov-action from 3 to 4 (#2430)\\n\\nBumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.\\r\\n- [Release notes](https://github.com/codecov/codecov-action/releases)\\r\\n- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)\\r\\n- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)\\r\\n\\r\\n---\\r\\nupdated-dependencies:\\r\\n- dependency-name: codecov/codecov-action\\r\\n  dependency-type: direct:production\\r\\n  update-type: version-update:semver-major\\r\\n...\\r\\n\\r\\nSigned-off-by: dependabot[bot] <support@github.com>\\r\\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\",\n      \"tree\": {\n        \"sha\": \"fb40fe05c5f1a6679bc1e7a24b0f9fc55c150c88\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/trees/fb40fe05c5f1a6679bc1e7a24b0f9fc55c150c88\"\n      },\n      \"url\": \"https://api.github.com/repos/py-pdf/pypdf/git/commits/f851a532a5ec23b572d86bd7185b327a3fac6b58\",\n      \"comment_count\": 0,\n      \"verification\": {\n        \"verified\": true,\n        \"reason\": \"valid\",\n        \"signature\": \"-----BEGIN PGP SIGNATURE-----\\n\\nwsFcBAABCAAQBQJlvfKjCRC1aQ7uu5UhlAAA9NUQAGTOt3JzejSo6o5fHUrLreus\\nv8TScA1B4nuWsJLH0nvGArZ8y8L/9JqG2fUTs3WGjY3PL9Dgn9fhmO+3dMcUDEav\\nEtBXdNHsodAUvNHKh1d9ZwCK+jSzbO9tSKiY4enxqUHnr+0m0q3XQHkYLf9eUklE\\n9/vi/OCV8JSptRkiS+VOsSrqfO+zqNUfnOxpNy6UNLPaNDwZyom6WROZE6yXLm1W\\nE0rsG10rBEXyvhjF2E4znoEcN/5+OIJr87h1Jys7y3qMXOo61my6bEpHY+gZpBRQ\\nN3xo3ptu4BhP0a4oI8iDjnQMQLS4cLN++LeMuUbWIEpKtiKkF5q/bGP3s1wniLTD\\nSYh14z0jIaJ7QPdkOEK2/Fv9lx5tno66bFe4vKC4DSmX3itcqh/XOiPFPkgRAalj\\nAd5g6hs1QlJErAwQShe6lzNDRnIDGoD6ZOaTMdxlbRNdwInr83Qz4Gt92D+dX4eQ\\njln9Welx4xTuPnYv6Qhmdc69Kk2nyhRuTnCsI0jaoqDRSLQxlzCuuQMn7u5XyqSS\\npSkWUYOw8zjrJd7ItPVe3YII5JIiRLEkHrDzTwGZAcy2E6GPMDLeXsx4K6GUhsfC\\nXenOpPuoo6BDk/bhrkWb7klyYG09JQtum31bCpDp1qxafXh5jh9Y0mztZJ4gWjaF\\n0NawJ3AozsNrioHxf6xz\\n=0OMP\\n-----END PGP SIGNATURE-----\\n\",\n        \"payload\": \"tree fb40fe05c5f1a6679bc1e7a24b0f9fc55c150c88\\nparent 757932944f54ba661b89e0629ed3fc9d8345dbab\\nauthor dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> 1706947235 +0100\\ncommitter GitHub <noreply@github.com> 1706947235 +0100\\n\\nDEV: Bump codecov/codecov-action from 3 to 4 (#2430)\\n\\nBumps [codecov/codecov-action](https://github.com/codecov/codecov-action) from 3 to 4.\\r\\n- [Release notes](https://github.com/codecov/codecov-action/releases)\\r\\n- [Changelog](https://github.com/codecov/codecov-action/blob/main/CHANGELOG.md)\\r\\n- [Commits](https://github.com/codecov/codecov-action/compare/v3...v4)\\r\\n\\r\\n---\\r\\nupdated-dependencies:\\r\\n- dependency-name: codecov/codecov-action\\r\\n  dependency-type: direct:production\\r\\n  update-type: version-update:semver-major\\r\\n...\\r\\n\\r\\nSigned-off-by: dependabot[bot] <support@github.com>\\r\\nCo-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>\"\n      }\n    },\n    \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/f851a532a5ec23b572d86bd7185b327a3fac6b58\",\n    \"html_url\": \"https://github.com/py-pdf/pypdf/commit/f851a532a5ec23b572d86bd7185b327a3fac6b58\",\n    \"comments_url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/f851a532a5ec23b572d86bd7185b327a3fac6b58/comments\",\n    \"author\": {\n      \"login\": \"dependabot[bot]\",\n      \"id\": 49699333,\n      \"node_id\": \"MDM6Qm90NDk2OTkzMzM=\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/in/29110?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/dependabot%5Bbot%5D\",\n      \"html_url\": \"https://github.com/apps/dependabot\",\n      \"followers_url\": \"https://api.github.com/users/dependabot%5Bbot%5D/followers\",\n      \"following_url\": \"https://api.github.com/users/dependabot%5Bbot%5D/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/dependabot%5Bbot%5D/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/dependabot%5Bbot%5D/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/dependabot%5Bbot%5D/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/dependabot%5Bbot%5D/orgs\",\n      \"repos_url\": \"https://api.github.com/users/dependabot%5Bbot%5D/repos\",\n      \"events_url\": \"https://api.github.com/users/dependabot%5Bbot%5D/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/dependabot%5Bbot%5D/received_events\",\n      \"type\": \"Bot\",\n      \"site_admin\": false\n    },\n    \"committer\": {\n      \"login\": \"web-flow\",\n      \"id\": 19864447,\n      \"node_id\": \"MDQ6VXNlcjE5ODY0NDQ3\",\n      \"avatar_url\": \"https://avatars.githubusercontent.com/u/19864447?v=4\",\n      \"gravatar_id\": \"\",\n      \"url\": \"https://api.github.com/users/web-flow\",\n      \"html_url\": \"https://github.com/web-flow\",\n      \"followers_url\": \"https://api.github.com/users/web-flow/followers\",\n      \"following_url\": \"https://api.github.com/users/web-flow/following{/other_user}\",\n      \"gists_url\": \"https://api.github.com/users/web-flow/gists{/gist_id}\",\n      \"starred_url\": \"https://api.github.com/users/web-flow/starred{/owner}{/repo}\",\n      \"subscriptions_url\": \"https://api.github.com/users/web-flow/subscriptions\",\n      \"organizations_url\": \"https://api.github.com/users/web-flow/orgs\",\n      \"repos_url\": \"https://api.github.com/users/web-flow/repos\",\n      \"events_url\": \"https://api.github.com/users/web-flow/events{/privacy}\",\n      \"received_events_url\": \"https://api.github.com/users/web-flow/received_events\",\n      \"type\": \"User\",\n      \"site_admin\": false\n    },\n    \"parents\": [\n      {\n        \"sha\": \"757932944f54ba661b89e0629ed3fc9d8345dbab\",\n        \"url\": \"https://api.github.com/repos/py-pdf/pypdf/commits/757932944f54ba661b89e0629ed3fc9d8345dbab\",\n        \"html_url\": \"https://github.com/py-pdf/pypdf/commit/757932944f54ba661b89e0629ed3fc9d8345dbab\"\n      }\n    ]\n  }\n]\n"
  },
  {
    "path": "tests/scripts/test_example_files.py",
    "content": "\"\"\"Tests related to the example files.\"\"\"\nfrom operator import itemgetter\nfrom pathlib import Path\n\nfrom tests import read_yaml_to_list_of_dicts\n\n\ndef test_consistency():\n    pdfs = read_yaml_to_list_of_dicts(Path(__file__).parent.parent / \"example_files.yaml\")\n\n    # Ensure the names are unique\n    assert len(pdfs) == len(set(map(itemgetter(\"local_filename\"), pdfs)))\n\n    # Ensure the urls are unique\n    assert len(pdfs) == len(set(map(itemgetter(\"url\"), pdfs)))\n"
  },
  {
    "path": "tests/scripts/test_make_release.py",
    "content": "\"\"\"Test the `make_release.py` script.\"\"\"\nimport sys\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\n\nDATA_PATH = Path(__file__).parent.resolve() / \"data\"\n\n# line starting with \\ and ending with \" have been observed on Windows\nGIT_LOG__VERSION_4_0_1 = \"\"\"\nb7bfd0d7eddfd0865a94cc9e7027df6596242cf7:::BUG: Use NumberObject for /Border elements of annotations (#2451):::rsinger417\n8cacb0fc8fee9920b0515d1289e6ee8191eb3f21:::DOC: Document easier way to update metadata (#2454):::Stefan\n3fb63f7e3839ce39ac98978c996f3086ba230a20:::TST: Avoid catching not emitted warnings (#2429):::Stefan\n\\\\61b73d49778e8f0fb172d5323e67677c9974e420:::DOC: Typo `Polyline` → `PolyLine` in adding-pdf-annotations.md (#2426):::CWKSC\"\nf851a532a5ec23b572d86bd7185b327a3fac6b58:::DEV: Bump codecov/codecov-action from 3 to 4 (#2430):::dependabot[bot]\"\"\".encode()  # noqa: E501\n\nCOMMITS__VERSION_4_0_1 = DATA_PATH.joinpath(\"commits__version_4_0_1.json\")\nVERSION_3_9_PLUS = sys.version_info[:2] >= (3, 9)\n\n\n@pytest.mark.skipif(not VERSION_3_9_PLUS, reason=\"Function uses method removeprefix added in Python 3.9\")\n@pytest.mark.parametrize(\n    (\"data\", \"expected\"),\n    [\n        (\"\", \"\"),\n        (\"# CHANGELOG\", \"\"),\n        (\"# CHANGELOG \", \"\"),\n        (\"# CHANGELOG  \", \"\"),\n        (\"## CHANGELOG\", \"## CHANGELOG\"),\n        (\"CHANGELOG\", \"CHANGELOG\"),\n        (\"# CHANGELOG #\", \"#\"),\n    ]\n)\ndef test_strip_header(data, expected):\n    \"\"\"Removal of the 'CHANGELOG' header.\"\"\"\n    make_release = pytest.importorskip(\"make_release\")\n    assert make_release.strip_header(data) == expected\n\n\ndef test_get_git_commits_since_tag():\n    make_release = pytest.importorskip(\"make_release\")\n\n    with open(COMMITS__VERSION_4_0_1, mode=\"rb\") as commits, mock.patch(\n        \"urllib.request.urlopen\", side_effect=lambda _: commits\n    ), mock.patch(\"subprocess.check_output\", return_value=GIT_LOG__VERSION_4_0_1):\n        commits = make_release.get_git_commits_since_tag(\"4.0.1\")\n    assert commits == [\n        make_release.Change(\n            commit_hash=\"b7bfd0d7eddfd0865a94cc9e7027df6596242cf7\",\n            prefix=\"BUG\",\n            message=\"Use NumberObject for /Border elements of annotations (#2451)\",\n            author=\"rsinger417\",\n            author_login=\"rsinger417\",\n        ),\n        make_release.Change(\n            commit_hash=\"8cacb0fc8fee9920b0515d1289e6ee8191eb3f21\",\n            prefix=\"DOC\",\n            message=\"Document easier way to update metadata (#2454)\",\n            author=\"Stefan\",\n            author_login=\"stefan6419846\",\n        ),\n        make_release.Change(\n            commit_hash=\"3fb63f7e3839ce39ac98978c996f3086ba230a20\",\n            prefix=\"TST\",\n            message=\"Avoid catching not emitted warnings (#2429)\",\n            author=\"Stefan\",\n            author_login=\"stefan6419846\",\n        ),\n        make_release.Change(\n            commit_hash=\"61b73d49778e8f0fb172d5323e67677c9974e420\",\n            prefix=\"DOC\",\n            message=\"Typo `Polyline` → `PolyLine` in adding-pdf-annotations.md (#2426)\",\n            author=\"CWKSC\",\n            author_login=\"CWKSC\",\n        ),\n        make_release.Change(\n            commit_hash=\"f851a532a5ec23b572d86bd7185b327a3fac6b58\",\n            prefix=\"DEV\",\n            message=\"Bump codecov/codecov-action from 3 to 4 (#2430)\",\n            author=\"dependabot[bot]\",\n            author_login=\"dependabot[bot]\",\n        ),\n    ]\n\n\ndef test_get_formatted_changes():\n    make_release = pytest.importorskip(\"make_release\")\n\n    with open(COMMITS__VERSION_4_0_1, mode=\"rb\") as commits, mock.patch(\n        \"urllib.request.urlopen\", side_effect=lambda _: commits\n    ), mock.patch(\"subprocess.check_output\", return_value=GIT_LOG__VERSION_4_0_1):\n        output, output_with_user = make_release.get_formatted_changes(\"4.0.1\")\n\n    assert (\n        output\n        == \"\"\"\n### Bug Fixes (BUG)\n- Use NumberObject for /Border elements of annotations (#2451)\n\n### Documentation (DOC)\n- Document easier way to update metadata (#2454)\n- Typo `Polyline` → `PolyLine` in adding-pdf-annotations.md (#2426)\n\n### Developer Experience (DEV)\n- Bump codecov/codecov-action from 3 to 4 (#2430)\n\n### Testing (TST)\n- Avoid catching not emitted warnings (#2429)\n\"\"\"\n    )\n    assert (\n        output_with_user\n        == \"\"\"\n### Bug Fixes (BUG)\n- Use NumberObject for /Border elements of annotations (#2451) by @rsinger417\n\n### Documentation (DOC)\n- Document easier way to update metadata (#2454) by @stefan6419846\n- Typo `Polyline` → `PolyLine` in adding-pdf-annotations.md (#2426) by @CWKSC\n\n### Developer Experience (DEV)\n- Bump codecov/codecov-action from 3 to 4 (#2430) by @dependabot[bot]\n\n### Testing (TST)\n- Avoid catching not emitted warnings (#2429) by @stefan6419846\n\"\"\"\n    )\n\n\ndef test_get_formatted_changes__other():\n    make_release = pytest.importorskip(\"make_release\")\n\n    changes = [\n        make_release.Change(\n            commit_hash=\"f20c36eabd59ea661f30c5da35af7c9e435c7de9\",\n            prefix=\"\",\n            message=\"Improve lossless compression example (#2488)\",\n            author=\"j-t-1\",\n            author_login=\"j-t-1\",\n        ),\n        make_release.Change(\n            commit_hash=\"afbee382f8fd2b39588db6470b9b2b2c82905318\",\n            prefix=\"ENH\",\n            message=\"Add reattach_fields function (#2480)\",\n            author=\"pubpub-zz\",\n            author_login=\"pubpub-zz\",\n        ),\n        make_release.Change(\n            commit_hash=\"cd705f959064d8125397ddf4f7bdd2ea296f889f\",\n            prefix=\"FIX\",\n            message=\"Broken test due to expired test file URL (#2468)\",\n            author=\"pubpub-zz\",\n            author_login=\"pubpub-zz\",\n        ),\n    ]\n    with mock.patch.object(\n        make_release, \"get_git_commits_since_tag\", return_value=changes\n    ):\n        output, output_with_user = make_release.get_formatted_changes(\"dummy\")\n\n    assert (\n        output\n        == \"\"\"\n### New Features (ENH)\n- Add reattach_fields function (#2480)\n\n### Other\n- : Improve lossless compression example (#2488)\n- FIX: Broken test due to expired test file URL (#2468)\n\"\"\"\n    )\n\n    assert (\n        output_with_user\n        == \"\"\"\n### New Features (ENH)\n- Add reattach_fields function (#2480) by @pubpub-zz\n\n### Other\n- : Improve lossless compression example (#2488) by @j-t-1\n- FIX: Broken test due to expired test file URL (#2468) by @pubpub-zz\n\"\"\"\n    )\n"
  },
  {
    "path": "tests/test_annotations.py",
    "content": "\"\"\"Test the pypdf.annotations submodule.\"\"\"\n\nfrom io import BytesIO\nfrom pathlib import Path\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.annotations import (\n    AnnotationDictionary,\n    Ellipse,\n    FreeText,\n    Highlight,\n    Line,\n    Link,\n    Polygon,\n    PolyLine,\n    Popup,\n    Rectangle,\n    Text,\n)\nfrom pypdf.errors import PdfReadError\nfrom pypdf.generic import ArrayObject, FloatObject, NumberObject\n\nfrom . import RESOURCE_ROOT, get_data_from_url\n\n\ndef test_ellipse(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    # Act\n    ellipse_annotation = Ellipse(\n        rect=(50, 550, 500, 650),\n        interior_color=\"ff0000\",\n    )\n    writer.add_annotation(0, ellipse_annotation)\n\n    # Assert: You need to inspect the file manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\ndef test_text(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"outline-without-title.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    # Act\n    text_annotation = Text(\n        text=\"Hello World\\nThis is the second line!\",\n        rect=(50, 550, 500, 650),\n        open=True,\n    )\n    writer.add_annotation(0, text_annotation)\n\n    # Assert: You need to inspect the file manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\ndef test_free_text(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    # Act\n    free_text_annotation = FreeText(\n        text=\"Hello World - bold and italic\\nThis is the second line!\",\n        rect=(50, 550, 200, 650),\n        font=\"Arial\",\n        bold=True,\n        italic=True,\n        font_size=\"20pt\",\n        font_color=\"00ff00\",\n        border_color=None,\n        background_color=None,\n    )\n    writer.add_annotation(0, free_text_annotation)\n\n    free_text_annotation = FreeText(\n        text=\"Another free text annotation (not bold, not italic)\",\n        rect=(500, 550, 200, 650),\n        font=\"Arial\",\n        bold=False,\n        italic=False,\n        font_size=\"20pt\",\n        font_color=\"00ff00\",\n        border_color=\"0000ff\",\n        background_color=\"cdcdcd\",\n    )\n    writer.add_annotation(0, free_text_annotation)\n\n    # Assert: You need to inspect the file manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\ndef test_free_text__font_specifier():\n    free_text_annotation = FreeText(\n        text=\"Hello World\",\n        rect=(0, 0, 0, 0),\n    )\n    assert free_text_annotation[\"/DS\"] == \"font: normal normal 14pt Helvetica;text-align:left;color:#000000\"\n    free_text_annotation = FreeText(\n        text=\"Hello World\",\n        rect=(50, 550, 200, 650),\n        font=\"Arial\",\n        bold=True,\n        italic=True,\n        font_size=\"20pt\",\n        font_color=\"00ff00\",\n        border_color=None,\n        background_color=None,\n    )\n    assert free_text_annotation[\"/DS\"] == \"font: italic bold 20pt Arial;text-align:left;color:#00ff00\"\n\n\ndef test_annotation_dictionary():\n    a = AnnotationDictionary()\n    a.flags = 123\n    assert a.flags == 123\n\n\ndef test_polygon(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    with pytest.raises(ValueError):\n        Polygon(\n            vertices=[],\n        )\n\n    annotation = Polygon(\n        vertices=[(50, 550), (200, 650), (70, 750), (50, 700)],\n    )\n    writer.add_annotation(0, annotation)\n\n    # Assert: You need to inspect the file manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\ndef test_polyline(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    with pytest.raises(\n            ValueError,\n            match=r\"A polyline needs at least 1 vertex with two coordinates\",\n    ):\n        PolyLine(\n            vertices=[],\n        )\n\n    annotation = PolyLine(\n        vertices=[(50, 550), (200, 650), (70, 750), (50, 700)],\n    )\n    writer.add_annotation(0, annotation)\n\n    # Assert: You need to inspect the file manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\ndef test_line(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    # Act\n    line_annotation = Line(\n        text=\"Hello World\\nLine2\",\n        rect=(50, 550, 200, 650),\n        p1=(50, 550),\n        p2=(200, 650),\n    )\n    writer.add_annotation(0, line_annotation)\n\n    # Assert: You need to inspect the file manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\ndef test_rectangle(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    # Act\n    square_annotation = Rectangle(\n        rect=(50, 550, 200, 650), interior_color=\"ff0000\"\n    )\n    writer.add_annotation(0, square_annotation)\n\n    square_annotation = Rectangle(rect=(40, 400, 150, 450))\n    writer.add_annotation(0, square_annotation)\n\n    # Assert: You need to inspect the file manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\ndef test_highlight(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    # Act\n    highlight_annotation = Highlight(\n        rect=(95.79332, 704.31777, 138.55779, 724.6855),\n        highlight_color=\"ff0000\",\n        quad_points=ArrayObject(\n            [\n                FloatObject(100.060779),\n                FloatObject(723.55398),\n                FloatObject(134.29033),\n                FloatObject(723.55398),\n                FloatObject(100.060779),\n                FloatObject(705.4493),\n                FloatObject(134.29033),\n                FloatObject(705.4493),\n            ]\n        ),\n        printing=False,\n    )\n    writer.add_annotation(0, highlight_annotation)\n    for annot in writer.pages[0][\"/Annots\"]:\n        obj = annot.get_object()\n        subtype = obj[\"/Subtype\"]\n        if subtype == \"/Highlight\":\n            assert \"/F\" not in obj or obj[\"/F\"] == NumberObject(0)\n\n    writer.add_page(page)\n    # Act\n    highlight_annotation = Highlight(\n        rect=(95.79332, 704.31777, 138.55779, 724.6855),\n        highlight_color=\"ff0000\",\n        quad_points=ArrayObject(\n            [\n                FloatObject(100.060779),\n                FloatObject(723.55398),\n                FloatObject(134.29033),\n                FloatObject(723.55398),\n                FloatObject(100.060779),\n                FloatObject(705.4493),\n                FloatObject(134.29033),\n                FloatObject(705.4493),\n            ]\n        ),\n        printing=True,\n    )\n    writer.add_annotation(1, highlight_annotation)\n    for annot in writer.pages[1][\"/Annots\"]:\n        obj = annot.get_object()\n        subtype = obj[\"/Subtype\"]\n        if subtype == \"/Highlight\":\n            assert obj[\"/F\"] == NumberObject(4)\n\n    # Assert: You need to inspect the file manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\ndef test_link(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"outline-without-title.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    # Act\n    # Part 1: Too many args\n    with pytest.raises(ValueError):\n        Link(\n            rect=(50, 550, 200, 650),\n            url=\"https://martin-thoma.com/\",\n            target_page_index=3,\n        )\n\n    # Part 2: Too few args\n    with pytest.raises(ValueError):\n        Link(\n            rect=(50, 550, 200, 650),\n        )\n\n    # Part 3: External Link\n    link_annotation = Link(\n        rect=(50, 50, 100, 100),\n        url=\"https://martin-thoma.com/\",\n        border=[1, 0, 6, [3, 2]],\n    )\n    writer.add_annotation(0, link_annotation)\n\n    # Part 4: Internal Link\n    link_annotation = Link(\n        rect=(100, 100, 300, 200),\n        target_page_index=1,\n        border=[50, 10, 4],\n    )\n    writer.add_annotation(0, link_annotation)\n\n    for page in reader.pages[1:]:\n        writer.add_page(page)\n\n    # Assert: You need to inspect the file manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\ndef test_popup(caplog):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"outline-without-title.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    # Act\n    text_annotation = Text(\n        title_bar=\"hello world\",\n        text=\"Hello World\\nThis is the second line!\",\n        rect=(50, 550, 200, 650),\n        open=True,\n    )\n    ta = writer.add_annotation(0, text_annotation)\n    popup_annotation = Popup(\n        rect=(50, 550, 200, 650),\n        open=True,\n        parent=ta,  # prefer to use for evolutivity\n    )\n    writer.add_annotation(writer.pages[0], popup_annotation)\n\n    Popup(\n        rect=(50, 550, 200, 650),\n        open=True,\n        parent=True,  # broken parameter  # type: ignore\n    )\n    assert \"Unregistered Parent object : No Parent field set\" in caplog.text\n\n    target = \"annotated-pdf-popup.pdf\"\n    writer.write(target)\n    Path(target).unlink()  # comment this out for manual inspection\n\n\ndef test_markup_annotation_in_reply_to():\n    \"\"\"Test that a reply annotation gets /IRT, /RT, and /NM after a write/read cycle.\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n\n    parent = Text(\n        text=\"Parent comment\",\n        rect=(50, 550, 200, 650),\n        open=True,\n    )\n    parent_ref = writer.add_annotation(0, parent)\n\n    reply = Text(\n        text=\"Reply to parent\",\n        rect=(50, 550, 200, 650),\n        in_reply_to=parent_ref,\n    )\n    writer.add_annotation(0, reply)\n\n    assert \"/IRT\" in reply\n    assert reply[\"/IRT\"].get_object() is parent_ref\n    assert reply[\"/RT\"] == \"/R\"\n    assert \"/NM\" in reply\n\n    assert \"/NM\" not in parent_ref\n\n    buf = BytesIO()\n    writer.write(buf)\n\n    reader2 = PdfReader(buf)\n    annots = reader2.pages[0][\"/Annots\"]\n    assert len(annots) == 2\n\n    reply_obj = annots[1].get_object()\n    assert reply_obj[\"/IRT\"].get_object()[\"/Contents\"] == \"Parent comment\"\n    assert reply_obj[\"/NM\"] == reply[\"/NM\"]\n\n\ndef test_markup_annotation_in_reply_to_group_type():\n    \"\"\"Test that a grouped annotation sets /RT to /Group.\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n\n    parent = Text(\n        text=\"Parent\",\n        rect=(50, 550, 200, 650),\n    )\n    parent_ref = writer.add_annotation(0, parent)\n\n    grouped = Text(\n        text=\"Grouped with parent\",\n        rect=(50, 550, 200, 650),\n        in_reply_to=parent_ref,\n        reply_type=\"Group\",\n    )\n    writer.add_annotation(0, grouped)\n\n    assert grouped[\"/RT\"] == \"/Group\"\n    assert \"/IRT\" in grouped\n    assert \"/NM\" in grouped\n\n\ndef test_markup_annotation_name_without_reply():\n    \"\"\"Test that annotation_name without in_reply_to raises ValueError.\"\"\"\n    with pytest.raises(ValueError, match=\"annotation_name is only supported when in_reply_to is set\"):\n        Text(\n            text=\"Named but not a reply\",\n            rect=(50, 550, 200, 650),\n            annotation_name=\"my-unique-name\",\n        )\n\n\ndef test_markup_annotation_reply_type_without_reply():\n    \"\"\"Test that non-default reply_type without in_reply_to raises ValueError.\"\"\"\n    with pytest.raises(ValueError, match=\"reply_type is only meaningful when in_reply_to is set\"):\n        Text(\n            text=\"Grouped but not a reply\",\n            rect=(50, 550, 200, 650),\n            reply_type=\"Group\",\n        )\n\n\ndef test_markup_annotation_in_reply_to_custom_name():\n    \"\"\"Test explicit annotation_name with in_reply_to.\"\"\"\n    writer = PdfWriter()\n    writer.add_blank_page(width=200, height=200)\n\n    parent = Text(text=\"Parent\", rect=(0, 0, 100, 100))\n    parent_ref = writer.add_annotation(0, parent)\n\n    reply = Text(\n        text=\"Reply\",\n        rect=(0, 0, 100, 100),\n        in_reply_to=parent_ref,\n        annotation_name=\"custom-reply-name\",\n    )\n    writer.add_annotation(0, reply)\n\n    assert reply[\"/NM\"] == \"custom-reply-name\"\n    assert \"/IRT\" in reply\n\n\ndef test_markup_annotation_in_reply_to_unregistered():\n    \"\"\"Test that an unregistered parent raises ValueError.\"\"\"\n    unregistered = Text(text=\"Not added to writer\", rect=(0, 0, 100, 100))\n    with pytest.raises(ValueError, match=\"in_reply_to must be a registered annotation\"):\n        Text(\n            text=\"Reply\",\n            rect=(0, 0, 100, 100),\n            in_reply_to=unregistered,\n        )\n\n\ndef test_markup_annotation_in_reply_to_indirect_object():\n    \"\"\"Test passing an IndirectObject directly as in_reply_to.\"\"\"\n    writer = PdfWriter()\n    writer.add_blank_page(width=200, height=200)\n\n    parent = Text(text=\"Parent\", rect=(0, 0, 100, 100))\n    parent_ref = writer.add_annotation(0, parent)\n    indirect_ref = parent_ref.indirect_reference\n\n    reply = Text(\n        text=\"Reply via IndirectObject\",\n        rect=(0, 0, 100, 100),\n        in_reply_to=indirect_ref,\n    )\n    writer.add_annotation(0, reply)\n\n    assert \"/IRT\" in reply\n    assert reply[\"/RT\"] == \"/R\"\n    assert \"/NM\" in reply\n\n    buf = BytesIO()\n    writer.write(buf)\n\n    reader = PdfReader(buf)\n    annots = reader.pages[0][\"/Annots\"]\n    assert len(annots) == 2\n    reply_obj = annots[1].get_object()\n    assert reply_obj[\"/IRT\"].get_object()[\"/Contents\"] == \"Parent\"\n    assert reply_obj[\"/NM\"] == reply[\"/NM\"]\n\n\n@pytest.mark.enable_socket\ndef test_outline_action_without_d_lenient():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss3268.pdf\")))\n    assert len(reader.outline) == 2\n\n\n@pytest.mark.enable_socket\ndef test_outline_action_without_d_strict(pdf_file_path):\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss3268.pdf\")))\n    reader.strict = True\n    with pytest.raises(PdfReadError) as e:\n        assert len(reader.outline) == 2\n    assert \"Outline Action Missing /D\" in str(e)\n"
  },
  {
    "path": "tests/test_appearance_stream.py",
    "content": "\"\"\"Test the pypdf.generic._appearance_stream module.\"\"\"\n\nfrom pypdf.generic._appearance_stream import BaseStreamConfig, TextStreamAppearance\n\n\ndef test_comb():\n    layout=BaseStreamConfig(rectangle=(0.0, 0.0, 197.285, 18.455))\n    font_size = 10.0\n    text = \"01234567\"\n    max_length = 10\n    is_comb = True\n    appearance_stream = TextStreamAppearance(\n        layout=layout, text=text, font_size=font_size, is_comb=is_comb, max_length=max_length\n    )\n    assert appearance_stream.get_data() == (\n        b\"q\\n/Tx BMC \\nq\\n2 1 193.285 16.455 re\\nW\\nBT\\n/Helv 10.0 Tf 0 g\\n\"\n        b\"7.084250000000001 5.637499999999999 Td\\n(0) Tj\\n\"\n        b\"19.7285 0.0 Td\\n(1) Tj\\n\"\n        b\"19.728500000000004 0.0 Td\\n(2) Tj\\n\"\n        b\"19.728499999999997 0.0 Td\\n(3) Tj\\n\"\n        b\"19.728499999999997 0.0 Td\\n(4) Tj\\n\"\n        b\"19.728499999999997 0.0 Td\\n(5) Tj\\n\"\n        b\"19.72850000000001 0.0 Td\\n(6) Tj\\n\"\n        b\"19.728499999999997 0.0 Td\\n(7) Tj\\nET\\nQ\\nEMC\\nQ\\n\"\n    )\n\n    layout.rectangle = (0.0, 0.0, 20.852, 20.84)\n    text = \"AA\"\n    max_length = 1\n    appearance_stream = TextStreamAppearance(\n        layout=layout, text=text, font_size=font_size, is_comb=is_comb, max_length=max_length\n    )\n    assert appearance_stream.get_data() == (\n        b\"q\\n/Tx BMC \\nq\\n2 1 16.852 18.84 re\\nW\\nBT\\n/Helv 10.0 Tf 0 g\\n7.091 6.83 Td\\n(A) Tj\\nET\\nQ\\nEMC\\nQ\\n\"\n    )\n\n\ndef test_scale_text():\n    layout=BaseStreamConfig(rectangle=(0, 0, 9.1, 55.4))\n    font_size = 10.1\n    text = \"Hello World\"\n    is_multiline = False\n    appearance_stream = TextStreamAppearance(\n        layout=layout, text=text, font_size=font_size, is_multiline=is_multiline\n    )\n    assert b\"10.1 Tf\" in appearance_stream.get_data()\n\n    text = \"This is a very very long sentence that probably will scale below the minimum font size\"\n    font_size = 0.0\n    appearance_stream = TextStreamAppearance(\n        layout=layout, text=text, font_size=font_size, is_multiline=is_multiline\n    )\n    assert b\"4.0 Tf\" in appearance_stream.get_data()\n\n    layout.rectangle = (0, 0, 160, 360)\n    font_size = 0.0\n    text = \"\"\"Welcome to pypdf\npypdf is a free and open source pure-python PDF library capable of splitting, merging, cropping, and\ntransforming the pages of PDF files. It can also add custom data, viewing options, and passwords to PDF\nfiles. pypdf can retrieve text and metadata from PDFs as well.\n\nSee pdfly for a CLI application that uses pypdf to interact with PDFs.\n    \"\"\"\n    is_multiline = True\n    appearance_stream = TextStreamAppearance(\n        layout=layout, text=text, font_size=font_size, is_multiline=is_multiline\n    )\n    assert b\"12 Tf\" in appearance_stream.get_data()\n    assert b\"pypdf is a free and open\" in appearance_stream.get_data()\n\n    layout.rectangle = (0, 0, 160, 160)\n    appearance_stream = TextStreamAppearance(\n        layout=layout, text=text, font_size=font_size, is_multiline=is_multiline\n    )\n    assert b\"9.8 Tf\" in appearance_stream.get_data()\n\n    layout.rectangle = (0, 0, 160, 12)\n    appearance_stream = TextStreamAppearance(\n        layout=layout, text=text, font_size=font_size, is_multiline=is_multiline\n    )\n    text = \"\"\"Option A\nOption B\nOption C\nOption D\n\"\"\"\n    selection = \"Option A\"\n    assert b\"4.0 Tf\" in appearance_stream.get_data()\n\n    text = \"pneumonoultramicroscopicsilicovolcanoconiosis\"\n    appearance_stream = TextStreamAppearance(\n        layout=layout, text=text, selection=selection, font_size=font_size, is_multiline=is_multiline\n    )\n    assert b\"7.3 Tf\" in appearance_stream.get_data()\n\n    layout.rectangle = (0, 0, 10, 100)\n    text = \"OneWord\"\n    appearance_stream = TextStreamAppearance(\n        layout=layout, text=text, font_size=font_size, is_multiline=is_multiline\n    )\n    assert b\"OneWord\" in appearance_stream.get_data()\n"
  },
  {
    "path": "tests/test_cmap.py",
    "content": "\"\"\"Test the pypdf_cmap module.\"\"\"\nfrom io import BytesIO\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf._cmap import get_encoding, parse_bfchar, parse_bfrange\nfrom pypdf._codecs import charset_encoding\nfrom pypdf._font import Font\nfrom pypdf.errors import LimitReachedError\nfrom pypdf.generic import ArrayObject, DictionaryObject, IndirectObject, NameObject, NullObject, StreamObject\n\nfrom . import RESOURCE_ROOT, get_data_from_url\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"strict\"),\n    [\n        # compute_space_width:\n        (\n            None,\n            \"tika-923406.pdf\",\n            False,\n        ),\n        # _parse_to_unicode_process_rg:\n        (\n            None,\n            \"tika-959173.pdf\",\n            False,\n        ),\n        (\n            None,\n            \"tika-959173.pdf\",\n            True,\n        ),\n        # issue #1718:\n        (\n            None,\n            \"iss1718.pdf\",\n            False,\n        ),\n    ],\n)\ndef test_text_extraction_slow(caplog, url: str, name: str, strict: bool):\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)), strict=strict)\n    for page in reader.pages:\n        page.extract_text()\n    assert caplog.text == \"\"\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"strict\"),\n    [\n        # bfchar_on_2_chars: issue #1293\n        (\n            None,\n            \"ASurveyofImageClassificationBasedTechniques.pdf\",\n            False,\n        ),\n        # L40, get_font_width_from_default\n        (\n            None,\n            \"tika-908104.pdf\",\n            False,\n        ),\n        # multiline_bfrange / regression test for issue #1285:\n        (\n            None,\n            \"The%20lean%20times%20in%20the%20Peruvian%20economy.pdf\",\n            False,\n        ),\n        (\n            None,\n            \"Giacalone.pdf\",\n            False,\n        ),\n    ],\n)\ndef test_text_extraction_fast(caplog, url: str, name: str, strict: bool):\n    \"\"\"Text extraction runs without exceptions or warnings\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)), strict=strict)\n    for page in reader.pages:\n        page.extract_text()\n    assert caplog.text == \"\"\n\n\n@pytest.mark.enable_socket\ndef test_parse_encoding_advanced_encoding_not_implemented(caplog):\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"tika-957144.pdf\")))\n    for page in reader.pages:\n        page.extract_text()\n    # The correctly spelled encoding is /WinAnsiEncoding\n    assert \"Advanced encoding /WinAnsEncoding not implemented yet\" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_ascii_charset():\n    # Issue #1312\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"ascii charset.pdf\")))\n    assert \"/a\" not in reader.pages[0].extract_text()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"page_nb\", \"within_text\"),\n    [\n        (\n            None,\n            \"cmap1370.pdf\",\n            0,\n            \"\",\n        ),\n        (\n            None,\n            \"02voc.pdf\",\n            2,\n            \"Document delineation and character sequence decoding\",\n        ),\n    ],\n    ids=[\"iss1370\", \"iss1379\"],\n)\ndef test_text_extraction_of_specific_pages(\n    url: str, name: str, page_nb: int, within_text\n):\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert within_text in reader.pages[page_nb].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_iss1533():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss1533.pdf\")))\n    reader.pages[0].extract_text()  # no error\n    font = Font.from_font_resource(reader.pages[0][\"/Resources\"][\"/Font\"][\"/F\"])\n    assert font.character_map[\"\\x01\"] == \"Ü\"\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"page_index\", \"within_text\", \"caplog_text\"),\n    [\n        (\n            None,\n            \"tstUCS2.pdf\",\n            1,\n            [\"2 / 12\", \"S0490520090001\", \"于博\"],\n            \"\",\n        ),\n        (\n            None,\n            \"tst-GBK_EUC.pdf\",\n            0,\n            [\"NJA\", \"中华男科学杂志\"],\n            \"Multiple definitions in dictionary at byte 0x5cb42 for key /MediaBox\\n\",\n        ),\n    ],\n)\ndef test_cmap_encodings(caplog, url, name, page_index, within_text, caplog_text):\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    extracted = reader.pages[page_index].extract_text()  # no error\n    for contained in within_text:\n        assert contained in extracted\n    assert caplog_text in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_latex():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"math_latex.pdf\")))\n    txt = reader.pages[0].extract_text()  # no error\n    for pat in (\"α\", \"β\", \"γ\", \"ϕ\", \"φ\", \"ℏ\", \"∫\", \"∂\", \"·\", \"×\"):\n        assert pat in txt\n    # actually the ϕ and φ seems to be crossed in latex\n\n\n@pytest.mark.enable_socket\ndef test_unixxx_glyphs():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"unixxx_glyphs.pdf\")))\n    txt = reader.pages[0].extract_text()  # no error\n    for pat in (\"闫耀庭\", \"龚龑\", \"张江水\", \"1′′.2\"):\n        assert pat in txt\n\n\n@pytest.mark.enable_socket\ndef test_cmap_compute_space_width():\n    # issue 2137\n    # original file URL:\n    # url = \"https://arxiv.org/pdf/2005.05909.pdf\"\n    # URL from github issue is too long to pass code type check, use original arxiv URL instead\n    # url = \"https://github.com/py-pdf/pypdf/files/12489914/Morris.et.al.-.2020.-.TextAttack.A.Framework.for.Adversarial.Attacks.Data.Augmentation.and.Adversarial.Training.in.NLP.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"TextAttack_paper.pdf\")))\n    reader.pages[0].extract_text()  # no error\n\n\n@pytest.mark.enable_socket\ndef test_tabs_in_cmap():\n    \"\"\"Issue #2173\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss2173.pdf\")))\n    reader.pages[0].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_ignoring_non_put_entries():\n    \"\"\"Issue #2290\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss2290.pdf\")))\n    reader.pages[0].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_eten_b5():\n    \"\"\"Issue #2356\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss2290.pdf\")))\n    reader.pages[0].extract_text().startswith(\"1/7 \\n富邦新終身壽險\")\n\n\ndef test_missing_entries_in_cmap():\n    \"\"\"\n    Issue #2702: this issue is observed on damaged pdfs\n    use of this file in test has been discarded as too slow/long\n    we will create the same error from crazyones\n    \"\"\"\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    p = reader.pages[0]\n    p[\"/Resources\"][\"/Font\"][\"/F1\"][NameObject(\"/ToUnicode\")] = IndirectObject(\n        99999999, 0, reader\n    )\n    p.extract_text()\n\n\ndef test_null_missing_width():\n    \"\"\"For coverage of #2792\"\"\"\n    writer = PdfWriter(RESOURCE_ROOT / \"crazyones.pdf\")\n    page = writer.pages[0]\n    ft = page[\"/Resources\"][\"/Font\"][\"/F1\"]\n    ft[NameObject(\"/Widths\")] = ArrayObject()\n    ft[\"/FontDescriptor\"][NameObject(\"/MissingWidth\")] = NullObject()\n    page.extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_unigb_utf16():\n    \"\"\"Cf #2812\"\"\"\n    url = (\n        \"https://github.com/user-attachments/files/16767536/W020240105322424121296.pdf\"\n    )\n    name = \"iss2812.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert \"《中国能源展望 2060（2024 年版）》编写委员会\" in reader.pages[1].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_too_many_differences():\n    \"\"\"Cf #2836\"\"\"\n    url = (\n        \"https://github.com/user-attachments/files/16911741/dumb_extract_text_crash.pdf\"\n    )\n    name = \"iss2836.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert reader.pages[0].extract_text() == \"\"\n\n\n@pytest.mark.enable_socket\ndef test_iss2925():\n    url = (\n        \"https://github.com/user-attachments/files/17621508/2305.09315.pdf\"\n    )\n    name = \"iss2925.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert \"slicing on the PDG to extract the relevant contextual\" in reader.pages[3].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_iss2966():\n    \"\"\"Regression test for issue #2966: indirect objects in fonts\"\"\"\n    url = (\n        \"https://github.com/user-attachments/files/17904233/repro_out.pdf\"\n    )\n    name = \"iss2966.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert \"Lorem ipsum dolor sit amet\" in reader.pages[0].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_binascii_odd_length_string(caplog):\n    \"\"\"Tests for #2216\"\"\"\n    url = \"https://github.com/user-attachments/files/18199642/iss2216.pdf\"\n    name = \"iss2216.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    page = reader.pages[0]\n    assert \"\\n(Many other theorems may\\n\" in page.extract_text()\n    assert \"Skipping broken line b'143f   143f   10300': Odd-length string\\n\" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_standard_encoding(caplog):\n    \"\"\"Tests for #3156\"\"\"\n    url = \"https://github.com/user-attachments/files/18983503/standard-encoding.pdf\"\n    name = \"issue3156.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    page = reader.pages[0]\n    assert page.extract_text() == \"Lorem ipsum\"\n    assert \"Advanced encoding\" not in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_function_in_font_widths(caplog):\n    \"\"\"Tests for #3153\"\"\"\n    url = \"https://github.com/user-attachments/files/18945709/Marseille_pypdf_level_0.2._compressed.pdf\"\n    name = \"issue3153.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    page = reader.pages[455]\n    assert \"La vulnérabilité correspond aux conséquences potentielles\" in page.extract_text()\n    assert \"Expected numeric value for width, got {'/Bounds': [0.25, 0.25],\" in caplog.text\n\n\ndef test_get_encoding__encoding_value_is_none():\n    ft = DictionaryObject()\n    ft[NameObject(\"/Encoding\")] = NullObject()\n    assert get_encoding(ft) == (\n        dict(zip(range(256), charset_encoding[\"/StandardEncoding\"])),\n        {}\n    )\n\n\ndef test_parse_bfchar(caplog):\n    map_dict = {}\n    int_entry = []\n    parse_bfchar(line=b\"057e   1337\", map_dict=map_dict, int_entry=int_entry)\n    parse_bfchar(line=b\"056e   1f310\", map_dict=map_dict, int_entry=int_entry)\n\n    assert map_dict == {-1: 2, \"ծ\": \"\", \"վ\": \"ጷ\"}\n    assert int_entry == [1406, 1390]\n    assert caplog.messages == [\"Got invalid hex string: Odd-length string (b'1f310')\"]\n\n\ndef test_parse_bfrange__iteration_limit():\n    writer = PdfWriter()\n\n    to_unicode = StreamObject()\n    to_unicode.set_data(\n        b\"beginbfrange\\n\"\n        b\"<00000000> <001FFFFF> <00000000>\\n\"\n        b\"endbfrange\\n\"\n    )\n    font = writer._add_object(DictionaryObject({\n        NameObject(\"/Type\"): NameObject(\"/Font\"),\n        NameObject(\"/Subtype\"): NameObject(\"/Type1\"),\n        NameObject(\"/BaseFont\"): NameObject(\"/Helvetica\"),\n        NameObject(\"/ToUnicode\"): to_unicode,\n    }))\n\n    page = writer.add_blank_page(width=100, height=100)\n    page[NameObject(\"/Resources\")] = DictionaryObject({\n        NameObject(\"/Font\"): DictionaryObject({\n            NameObject(\"/F1\"): font.indirect_reference,\n        })\n    })\n\n    # Case without list, exceeding list directly.\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Maximum /ToUnicode size limit reached: 2097152 > 100000\\.$\"\n    ):\n        _ = page.extract_text()\n\n    # Use a pre-filled dummy list to simulate multiple calls where the upper bound does\n    # not overflow, but the overall size does. Case without list.\n    int_entry = [0] * 99_999\n    map_dict = {}\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Maximum /ToUnicode size limit reached: 165535 > 100000\\.$\"\n    ):\n        _ = parse_bfrange(line=b\"0000 FFFF 0000\", map_dict=map_dict, int_entry=int_entry, multiline_rg=None)\n    assert map_dict == {-1: 2}\n\n    # Exceeding from previous call.\n    int_entry.append(1)\n    map_dict = {}\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Maximum /ToUnicode size limit reached: 100001 > 100000\\.$\"\n    ):\n        _ = parse_bfrange(line=b\"00000000 00000000 00000000\", map_dict=map_dict, int_entry=int_entry, multiline_rg=None)\n    assert map_dict == {-1: 4}\n\n    # multiline_rg\n    int_entry = [0] * 99_995\n    map_dict = {-1: 1}\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Maximum /ToUnicode size limit reached: 100001 > 100000\\.$\"\n    ):\n        _ = parse_bfrange(\n            line=b\"0020  0021  0022  0023  0024  0025  0026  2019\",\n            map_dict=map_dict, int_entry=int_entry, multiline_rg=(32, 251)\n        )\n    assert map_dict == {-1: 1, \" \": \" \", \"!\": \"!\", '\"': '\"', \"#\": \"#\", \"$\": \"$\"}\n\n    # No multiline_rg, but list.\n    int_entry = [0] * 99_995\n    map_dict = {}\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Maximum /ToUnicode size limit reached: 100001 > 100000\\.$\"\n    ):\n        _ = parse_bfrange(\n            line=b\"01 8A [ FFFD FFFD FFFD FFFF FFAB AAAA BBBB\",\n            map_dict=map_dict, int_entry=int_entry, multiline_rg=None\n        )\n    assert map_dict == {-1: 1, \"\\x01\": \"�\", \"\\x02\": \"�\", \"\\x03\": \"�\", \"\\x04\": \"\\uffff\", \"\\x05\": \"ﾫ\"}\n\n\ndef test_parse_bfchar__iteration_limit():\n    int_entry = [0] * 99_995\n    map_dict = {}\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Maximum /ToUnicode size limit reached: 100002 > 100000\\.$\"\n    ):\n        parse_bfchar(\n            line=b\"0003   0020   0008   0025   0009   0026   000A   0027   000B   0028   000C   0029   000D   002A\",\n            map_dict=map_dict, int_entry=int_entry,\n        )\n    assert map_dict == {}\n"
  },
  {
    "path": "tests/test_codecs.py",
    "content": "\"\"\"Test LZW-related code.\"\"\"\nfrom io import BytesIO\n\nimport pytest\n\nfrom pypdf import PdfReader\nfrom pypdf._codecs._codecs import LzwCodec\nfrom pypdf.errors import LimitReachedError\n\nfrom . import RESOURCE_ROOT, get_data_from_url\n\ntest_cases = [\n    pytest.param(b\"\", id=\"Empty input\"),\n    pytest.param(b\"A\", id=\"Single character\"),\n    pytest.param(b\"AAAAAA\", id=\"Repeating character\"),\n    pytest.param(b\"Hello, World!\", id=\"Simple text\"),\n    pytest.param(b\"ABABABABABAB\", id=\"Repeating pattern\"),\n    pytest.param(b\"The quick brown fox jumps over the lazy dog\", id=\"Longer text\"),\n    pytest.param(b\"\\x00\\xFF\\x00\\xFF\", id=\"Binary data\"),\n    pytest.param(\n        b\"BBBCBDBEBFBGBHBIBJBKBLBMBNBOBPBQBRBSBTBUBVBWBXBYBZB[B\\\\B]B^B_B`BaBbBcBdBeBfBgBhBiBjBkBlBmBnBoBpBqBrBsBtBuBvBwBxByCBCCCDCECFCGCHCICJCKCLCMCNCOCPCQCRCSCTCUCVCWCXCYCZC[C\\\\C]C^C_C`CaCbCcCdCeCfCgChCiCjCkClCmCnCoCpCqCrCsCtCuCvCwCxCyDBDCDDDEDFDGDHDIDJDKDLDMDNDODPDQDRDSDTDUDVDWDXDYDZD[D\\\\D]D^D_D`DaDbDcDdDeDfDgDhDiDjDkDlDmDnDoDpDqDrDsDtDuDvDwDxDyEBECEDEEEFEGEHEIEJEKELEMENEOEPEQERESETEUEVEWEXEYEZE[E\\\\E]E^E_E`EaEbEcEdEeEfEgEhEiEjEkElEmEnEoEpEqErEsEtEuEvEwExEyFBFCFDFEFFFGFHFIFJFKFLFMFNFOFPFQFRFSFTFUFVFWFXFYFZF[F\\\\F]F^F_F`FaFbFcFdFeFfFgFhFiFjFkFlFmFnFoFpFqFrFsFtFuFvFwFxFyGBGCGDGEGFGGGHGIGJGKGLGMGNGOGPGQGRGSGTGUGVGWGXGYGZG[G\\\\G]G^G_G`GaGbGcGdGeGfGgGhGiGjGkGlGmGnGoGpGqGrGsGtGuGvGwGxGyHBHCHDHEHFHGHHHIHJHKHLHMHNHOHPHQHRHSHTHUHVHWHXHYHZH[H\\\\H]H^H_H`HaHbHcHdHeHfHgHhHiHjHkHlHmHnHoHpHqHrHsHtHuHvHwHxHyIBICIDIEIFIGIHIIIJIKILIMINIOIPIQIRISITIUIVIWIXIYIZI[I\\\\I]I^I_I`IaIbIcIdIeIfIgIhIiIjIkIlImInIoIpIqIrIsItIuIvIwIxIyJBJCJDJEJFJGJHJIJJJKJLJMJNJOJPJQJRJSJTJUJVJWJXJYJZJ[J\\\\J]J^J_J`JaJbJcJdJeJfJgJhJiJjJkJlJmJnJoJpJqJrJsJtJuJvJwJxJyKBKCKDKEKFKGKHKIKJKKKLKMKNKOKPKQKRKSKTKUKVKWKXKYKZK[K\\\\K]K^K_K`KaKbKcKdKeKfKgKhKiKjKkKlKmKnKoKpKqKrKsKtKuKvKwKxKyLBLCLDLELFLGLHLILJLKLLLMLNLOLPLQLRLSLTLULVLWLXLYLZL[L\\\\L]L^L_L`LaLbLcLdLeLfLgLhLiLjLkLlLmLnLoLpLqLrLsLtLuLvLwLxLyMBMCMDMEMFMGMHMIMJMKMLMMMNMOMPMQMRMSMTMUMVMWMXMYMZM[M\\\\M]M^M_M`MaMbMcMdMeMfMgMhMiMjMkMlMmMnMoMpMqMrMsMtMuMvMwMxMyNBNCNDNENFNGNHNINJNKNLNMNNNONPNQNRNSNTNUNVNWNXNYNZN[N\\\\N]N^N_N`NaNbNcNdNeNfNgNhNiNjNkNlNmNnNoNpNqNrNsNtNuNvNwNxNyOBOCODOEOFOGOHOIOJOKOLOMONOOOPOQOROSOTOUOVOWOXOYOZO[O\\\\O]O^O_O`OaObOcOdOeOfOgOhOiOjOkOlOmOnOoOpOqOrOsOtOuOvOwOxOyPBPCPDPEPFPGPHPIPJPKPLPMPNPOPPPQPRPSPTPUPVPWPXPYPZP[P\\\\P]P^P_P`PaPbPcPdPePfPgPhPiPjPkPlPmPnPoPpPqPrPsPtPuPvPwPxPyQBQCQDQEQFQGQHQIQJQKQLQMQNQOQPQQQRQSQTQUQVQWQXQYQZQ[Q\\\\Q]Q^Q_Q`QaQbQcQdQeQfQgQhQiQjQkQlQmQnQoQpQqQrQsQtQuQvQwQxQyRBRCRDRERFRGRHRIRJRKRLRMRNRORPRQRRRSRTRURVRWRXRYRZR[R\\\\R]R^R_R`RaRbRcRdReRfRgRhRiRjRkRlRmRnRoRpRqRrRsRtRuRvRwRxRySBSCSDSESFSGSHSISJSKSLSMSNSOSPSQSRSSSTSUSVSWSXSYSZS[S\\\\S]S^S_S`SaSbScSdSeSfSgShSiSjSkSlSmSnSoSpSqSrSsStSuSvSwSxSyTBTCTDTETFTGTHTITJTKTLTMTNTOTPTQTRTSTTTUTVTWTXTYTZT[T\\\\T]T^T_T`TaTbTcTdTeTfTgThTiTjTkTlTmTnToTpTqTrTsTtTuTvTwTxTyUBUCUDUEUFUGUHUIUJUKULUMUNUOUPUQURUSUTUUUVUWUXUYUZU[U\\\\U]U^U_U`UaUbUcUdUeUfUgUhUiUjUkUlUmUnUoUpUqUrUsUtUuUvUwUxUyVBVCVDVEVFVGVHVIVJVKVLVMVNVOVPVQVRVSVTVUVVVWVXVYVZV[V\\\\V]V^V_V`VaVbVcVdVeVfVgVhViVjVkVlVmVnVoVpVqVrVsVtVuVvVwVxVyWBWCWDWEWFWGWHWIWJWKWLWMWNWOWPWQWRWSWTWUWVWWWXWYWZW[W\\\\W]W^W_W`WaWbWcWdWeWfWgWhWiWjWkWlWmWnWoWpWqWrWsWtWuWvWwWxWyXBXCXDXEXFXGXHXIXJXKXLXMXNXOXPXQXRXSXTXUXVXWXXXYXZX[X\\\\X]X^X_X`XaXbXcXdXeXfXgXhXiXjXkXlXmXnXoXpXqXrXsXtXuXvXwXxXyYBYCYDYEYFYGYHYIYJYKYLYMYNYOYPYQYRYSYTYUYVYWYXYYYZY[Y\\\\Y]Y^Y_Y`YaYbYcYdYeYfYgYhYiYjYkYlYmYnYoYpYqYrYsYtYuYvYwYxYyZBZCZDZEZFZGZHZIZJZKZLZMZNZOZPZQZRZSZTZUZVZWZXZYZZZ[Z\\\\Z]Z^Z_Z`ZaZbZcZdZeZfZgZhZiZjZkZlZmZnZoZpZqZrZsZtZuZvZwZxZy[B[C[D[E[F[G[H[I[J[K[L[M[N[O[P[Q[R[S[T[U[V[W[X[Y[Z[[[\\\\[][^[_[`[a[b[c[d[e[f[g[h[i[j[k[l[m[n[o[p[q[r[s[t[u[v[w[x[y\\\\B\\\\C\\\\D\\\\E\\\\F\\\\G\\\\H\\\\I\\\\J\\\\K\\\\L\\\\M\\\\N\\\\O\\\\P\\\\Q\\\\R\\\\S\\\\T\\\\U\\\\V\\\\W\\\\X\\\\Y\\\\Z\\\\[\\\\\\\\\\\\]\\\\^\\\\_\\\\`\\\\a\\\\b\\\\c\\\\d\\\\e\\\\f\\\\g\\\\h\\\\i\\\\j\\\\k\\\\l\\\\m\\\\n\\\\o\\\\p\\\\q\\\\r\\\\s\\\\t\\\\u\\\\v\\\\w\\\\x\\\\y]B]C]D]E]F]G]H]I]J]K]L]M]N]O]P]Q]R]S]T]U]V]W]X]Y]Z][]\\\\]]]^]_]`]a]b]c]d]e]f]g]h]i]j]k]l]m]n]o]p]q]r]s]t]u]v]w]x]y^B^C^D^E^F^G^H^I^J^K^L^M^N^O^P^Q^R^S^T^U^V^W^X^Y^Z^[^\\\\^]^^^_^`^a^b^c^d^e^f^g^h^i^j^k^l^m^n^o^p^q^r^s^t^u^v^w^x^y_B_C_D_E_F_G_H_I_J_K_L_M_N_O_P_Q_R_S_T_U_V_W_X_Y_Z_[_\\\\_]_^___`_a_b_c_d_e_f_g_h_i_j_k_l_m_n_o_p_q_r_s_t_u_v_w_x_y`B`C`D`E`F`G`H`I`J`K`L`M`N`O`P`Q`R`S`T`U`V`W`X`Y`Z`[`\\\\`]`^`_```a`b`c`d`e`f`g`h`i`j`k`l`m`n`o`p`q`r`s`t`u`v`w`x`yaBaCaDaEaFaGaHaIaJaKaLaMaNaOaPaQaRaSaTaUaVaWaXaYaZa[a\\\\a]a^a_a`aaabacadaeafagahaiajakalamanaoapaqarasatauavawaxaybBbCbDbEbFbGbHbIbJbKbLbMbNbObPbQbRbSbTbUbVbWbXbYbZb[b\\\\b]b^b_b`babbbcbdbebfbgbhbibjbkblbmbnbobpbqbrbsbtbubvbwbxbycBcCcDcEcFcGcHcIcJcKcLcMcNcOcPcQcRcScTcUcVcWcXcYcZc[c\\\\c]c^c_c`cacbcccdcecfcgchcicjckclcmcncocpcqcrcsctcucvcwcxcydBdCdDdEdFdGdHdIdJdKdLdMdNdOdPdQdRdSdTdUdVdWdXdYdZd[d\\\\d]d^d_d`dadbdcdddedfdgdhdidjdkdldmdndodpdqdrdsdtdudvdwdxdyeBeCeDeEeFeGeHeIeJeKeLeMeNeOePeQeReSeTeUeVeWeXeYeZe[e\\\\e]e^e_e`eaebecedeeefegeheiejekelemeneoepeqereseteuevewexeyfBfCfDfEfFfGfHfIfJfKfLfMfNfOfPfQfRfSfTfUfVfWfXfYfZf[f\\\\f]f^f_f`fafbfcfdfefffgfhfifjfkflfmfnfofpfqfrfsftfufvfwfxfygBgCgDgEgFgGgHgIgJgKgLgMgNgOgPgQgRgSgTgUgVgWgXgYgZg[g\\\\g]g^g_g`gagbgcgdgegfggghgigjgkglgmgngogpgqgrgsgtgugvgwgxgyhBhChDhEhFhGhHhIhJhKhLhMhNhOhPhQhRhShThUhVhWhXhYhZh[h\\\\h]h^h_h`hahbhchdhehfhghhhihjhkhlhmhnhohphqhrhshthuhvhwhxhyiBiCiDiEiFiGiHiIiJiKiLiMiNiOiPiQiRiSiTiUiViWiXiYiZi[i\\\\i]i^i_i`iaibicidieifigihiiijikiliminioipiqirisitiuiviwixiyjBjCjDjEjFjGjHjIjJjKjLjMjNjOjPjQjRjSjTjUjVjWjXjYjZj[j\\\\j]j^j_j`jajbjcjdjejfjgjhjijjjkjljmjnjojpjqjrjsjtjujvjwjxjykBkCkDkEkFkGkHkIkJkKkLkMkNkOkPkQkRkSkTkUkVkWkXkYkZk[k\\\\k]k^k_k`kakbkckdkekfkgkhkikjkkklkmknkokpkqkrksktkukvkwkxkylBlClDlElFlGlHlIlJlKlLlMlNlOlPlQlRlSlTlUlVlWlXlYlZl[l\\\\l]l^l_l`lalblcldlelflglhliljlklllmlnlolplqlrlsltlulvlwlxlymBmCmDmEmFmGmHmImJmKmLmMmNmOmPmQmRmSmTmUmVmWmXmYmZm[m\\\\m]m^m_m`mambmcmdmemfmgmhmimjmkmlmmmnmompmqmrmsmtmumvmwmxmynBnCnDnEnFnGnHnInJnKnLnMnNnOnPnQnRnSnTnUnVnWnXnYnZn[n\\\\n]n^n_n`nanbncndnenfngnhninjnknlnmnnnonpnqnrnsntnunvnwnxnyoBoCoDoEoFoGoHoIoJoKoLoMoNoOoPoQoRoSoToUoVoWoXoYoZo[o\\\\o]o^o_o`oaobocodoeofogohoiojokolomonooopoqorosotouovowoxoypBpCpDpEpFpGpHpIpJpKpLpMpNpOpPpQpRpSpTpUpVpWpXpYpZp[p\\\\p]p^p_p`papbpcpdpepfpgphpipjpkplpmpnpopppqprpsptpupvpwpxpyqBqCqDqEqFqGqHqIqJqKqLqMqNqOqPqQqRqSqTqUqVqWqXqYqZq[q\\\\q]q^q_q`qaqbqcqdqeqfqgqhqiqjqkqlqmqnqoqpqqqrqsqtquqvqwqxqyrBrCrDrErFrGrHrIrJrKrLrMrNrOrPrQrRrSrTrUrVrWrXrYrZr[r\\\\r]r^r_r`rarbrcrdrerfrgrhrirjrkrlrmrnrorprqrrrsrtrurvrwrxrysBsCsDsEsFsGsHsIsJsKsLsMsNsOsPsQsRsSsTsUsVsWsXsYsZs[s\\\\s]s^s_s`sasbscsdsesfsgshsisjskslsmsnsospsqsrssstsusvswsxsytBtCtDtEtFtGtHtItJtKtLtMtNtOtPtQtRtStTtUtVtWtXtYtZt[t\\\\t]t^t_t`tatbtctdtetftgthtitjtktltmtntotptqtrtstttutvtwtxtyuBuCuDuEuFuGuHuIuJuKuLuMuNuOuPuQuRuSuTuUuVuWuXuYuZu[u\\\\u]u^u_u`uaubucudueufuguhuiujukulumunuoupuqurusutuuuvuwuxuyvBvCvDvEvFvGvHvIvJvKvLvMvNvOvPvQvRvSvTvUvVvWvXvYvZv[v\\\\v]v^v_v`vavbvcvdvevfvgvhvivjvkvlvmvnvovpvqvrvsvtvuvvvwvxvywBwCwDwEwFwGwHwIwJwKwLwMwNwOwPwQwRwSwTwUwVwWwXwYwZw[w\\\\w]w^w_w`wawbwcwdwewfwgwhwiwjwkwlwmwnwowpwqwrwswtwuwvwwwxwyxBxCxDxExFxGxHxIxJxKxLxMxNxOxPxQxRxSxTxUxVxWxXxYxZx[x\\\\x]x^x_x`xaxbxcxdxexfxgxhxixjxkxlxmxnxoxpxqxrxsxtxuxvxwxxxyyByCyDyEyFyGyHyIyJyKyLyMyNyOyPyQyRySyTyUyVyWyXyYyZy[y\\\\y]y^y_y`yaybycydyeyfygyhyiyjykylymynyoypyqyrysytyuyvywyxyyBBBBBCBBDBBEBBFBBGBBHBBIBBJBBKBBLBBMBBNBBOBBPBBQBBRBBSBBTBBUBBVBBWBBXBBYBBZBB[BB\\\\BB]BB^BB_BB`BBaBBbBBcBBdBBeBBfBBgBBhBBiBBjBBkBBlBBmBBnBBoBBpBBqBBrBBsBBtBBuBBvBBwBBxBByBCBBCCBCDBCEBCFBCGBCHBCIBCJBCKBCLBCMBCNBCOBCPBCQBCRBCSBCTBCUBCVBCWBCXBCYBCZBC[BC\\\\BC]BC^BC_BC`BCaBCbBCcBCdBCeBCfBCgBChBCiBCjBCkBClBCmBCnBCoBCpBCqBCrBCsBCtBCuBCvBCwBCxBCyBDBBDCBDDBDEBDFBDGBDHBDIBDJBDKBDLBDMBDNBDOBDPBDQBDRBDSBDTBDUBDVBDWBDXBDYBDZBD[BD\\\\BD]BD^BD_BD`BDaBDbBDcBDdBDeBDfBDgBDhBDiBDjBDkBDlBDmBDnBDoBDpBDqBDrBDsBDtBDuBDvBDwBDxBDyBEBBECBEDBEEBEFBEGBEHBEIBEJBEKBELBEMBENBEOBEPBEQBERBESBETBEUBEVBEWBEXBEYBEZBE[BE\\\\BE]BE^BE_BE`BEaBEbBEcBEdBEeBEfBEgBEhBEiBEjBEkBElBEmBEnBEoBEpBEqBErBEsBEtBEuBEvBEwBExBEyBFBBFCBFDBFEBFFBFGBFHBFIBFJBFKBFLBFMBFNBFOBFPBFQBFRBFSBFTBFUBFVBFWBFXBFYBFZBF[BF\\\\BF]BF^BF_BF`BFaBFbBFcBFdBFeBFfBFgBFhBFiBFjBFkBFlBFmBFnBFoBFpBFqBFrBFsBFtBFuBFvBFwBFxBFyBGBBGCBGDBGEBGFBGGBGHBGIBGJBGKBGLBGMBGNBGOBGPBGQBGRBGSBGTBGUBGVBGWBGXBGYBGZBG[BG\\\\BG]BG^BG_BG`BGaBGbBGcBGdBGeBGfBGgBGhBGiBGjBGkBGlBGmBGnBGoBGpBGqBGrBGsBGtBGuBGvBGwBGxBGyBHBBHCBHDBHEBHFBHGBHHBHIBHJBHKBHLBHMBHNBHOBHPBHQBHRBHSBHTBHUBHVBHWBHXBHYBHZBH[BH\\\\BH]BH^BH_BH`BHaBHbBHcBHdBHeBHfBHgBHhBHiBHjBHkBHlBHmBHnBHoBHpBHqBHrBHsBHtBHuBHvBHwBHxBHyBIBBICBIDBIEBIFBIGBIHBIIBIJBIKBILBIMBINBIOBIPBIQBIRBISBITBIUBIVBIWBIXBIYBIZBI[BI\\\\BI]BI^BI_BI`BIaBIbBIcBIdBIeBIfBIgBIhBIiBIjBIkBIlBImBInBIoBIpBIqBIrBIsBItBIuBIvBIwBIxBIyBJBBJCBJDBJEBJFBJGBJHBJIBJJBJKBJLBJMBJNBJOBJPBJQBJRBJSBJTBJUBJVBJWBJXBJYBJZBJ[BJ\\\\BJ]BJ^BJ_BJ`BJaBJbBJcBJdBJeBJfBJgBJhBJiBJjBJkBJlBJmBJnBJoBJpBJqBJrBJsBJtBJuBJvBJwBJxBJyBKBBKCBKDBKEBKFBKGBKHBKIBKJBKKBKLBKMBKNBKOBKPBKQBKRBKSBKTBKUBKVBKWBKXBKYBKZBK[BK\\\\BK]BK^BK_BK`BKaBKbBKcBKdBKeBKfBKgBKhBKiBKjBKkBKlBKmBKnBKoBKpBKqBKrBKsBKtBKuBKvBKwBKxBKyBLBBLCBLDBLEBLFBLGBLHBLIBLJBLKBLLBLMBLNBLOBLPBLQBLRBLSBLTBLUBLVBLWBLXBLYBLZBL[BL\\\\BL]BL^BL_BL`BLaBLbBLcBLdBLeBLfBLgBLhBLiBLjBLkBLlBLmBLnBLoBLpBLqBLrBLsBLtBLuBLvBLwBLxBLyBMBBMCBMDBMEBMFBMGBMHBMIBMJBMKBMLBMMBMNBMOBMPBMQBMRBMSBMTBMUBMVBMWBMXBMYBMZBM[BM\\\\BM]BM^BM_BM`BMaBMbBMcBMdBMeBMfBMgBMhBMiBMjBMkBMlBMmBMnBMoBMpBMqBMrBMsBMtBMuBMvBMwBMxBMyBNBBNCBNDBNEBNFBNGBNHBNIBNJBNKBNLBNMBNNBNOBNPBNQBNRBNSBNTBNUBNVBNWBNXBNYBNZBN[BN\\\\BN]BN^BN_\",\n        id=\"Table overflow\",\n    ),\n]\n\n\n@pytest.mark.parametrize(\"data\", test_cases)\ndef test_encode_decode(data):\n    \"\"\"Decoder and encoder match.\"\"\"\n    codec = LzwCodec()\n    compressed_data = codec.encode(data)\n    decoded = codec.decode(compressed_data)\n    assert decoded == data\n\n\n@pytest.mark.parametrize(\n    (\"plain\", \"expected_encoded\"),\n    [\n        (b\"\", b\"\\x80@@\"),\n        (b\"A\", b\"\\x80\\x10` \"),\n        (b\"AAAAAA\", b\"\\x80\\x10`P8\\x08\"),\n        (b\"Hello, World!\", b\"\\x80\\x12\\x0c\\xa6\\xc3a\\xbcX +\\x9b\\xceF\\xc3 \\x86\\x02\"),\n    ],\n)\ndef test_encode_lzw(plain, expected_encoded):\n    codec = LzwCodec()\n    actual_encoded = codec.encode(plain)\n    assert actual_encoded == expected_encoded\n\n\n@pytest.mark.parametrize(\n    (\"encoded\", \"expected_decoded\"),\n    [\n        # _pack_codes_into_bytes([256, 65, 66, 67, 68, 256, 256, 69, 70, 71, 72, 257])\n        (b\"\\x80\\x10HD2$\\x02\\x00E#\\x11\\xc9\\x10\\x10\", b\"ABCDEFGH\"),  # Clear twice.\n        # _pack_codes_into_bytes([65, 66, 67, 68, 257])\n        (b\" \\x90\\x88dH\\x08\", b\"ABCD\"),  # No explicit initial clear marker.\n    ],\n)\ndef test_decode_lzw(encoded, expected_decoded):\n    codec = LzwCodec()\n    actual_decoded = codec.decode(encoded)\n    assert actual_decoded == expected_decoded\n\n\ndef test_lzw_decoder_table_overflow(caplog):\n    path = RESOURCE_ROOT / \"lzw_decoder_table_overflow.bin\"\n    codec = LzwCodec()\n    assert codec.decode(path.read_bytes()).startswith(\n        b'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!\"#$%&\\'()*+,-./:;<=>?@'\n    )\n    assert len(codec.decoding_table) == 4096\n    assert \"Ignoring too large LZW table index.\" in caplog.text\n\n\n@pytest.mark.enable_socket\n@pytest.mark.timeout(timeout=15, method=\"thread\")\ndef test_lzw_decoder_large_stream_performance(caplog):\n    LzwCodec().decode(get_data_from_url(name=\"large_lzw_example_encoded.dat\"))\n\n\n@pytest.mark.enable_socket\ndef test_lzw_decoder__output_limit():\n    url = \"https://github.com/user-attachments/files/23057035/lzw__output_limit.pdf\"\n    name = \"lzw__output_limit.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[0]\n\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Limit reached while decompressing: 75000828 > 75000000$\"\n    ):\n        page.images[0].image.load()\n"
  },
  {
    "path": "tests/test_constants.py",
    "content": "\"\"\"Test the pypdf.constants module.\"\"\"\nimport re\nfrom typing import Callable\n\nimport pytest\n\nfrom pypdf.constants import PDF_KEYS, GraphicsStateParameters, UserAccessPermissions\n\n\ndef test_slash_prefix():\n    \"\"\"\n    Naming conventions of PDF_KEYS (constant names) are followed.\n\n    This test function validates if PDF key names follow the required pattern:\n    - Starts with a slash \"/\"\n    - Followed by an uppercase letter\n    - Contains alphanumeric characters (letters and digits)\n    - The attribute name should be a case-insensitive match, with underscores removed\n    \"\"\"\n    pattern = re.compile(r\"^\\/[A-Z]+[a-zA-Z0-9]*$\")\n    for cls in PDF_KEYS:\n        for attr in dir(cls):\n            # Skip magic methods\n            if attr.startswith(\"__\") and attr.endswith(\"__\"):\n                continue\n\n            # Skip methods\n            constant_value = getattr(cls, attr)\n            if isinstance(constant_value, Callable):\n                continue\n\n            assert constant_value.startswith(\"/\")\n            assert attr.replace(\"_\", \"\").casefold() == constant_value[1:].casefold()\n\n            # There are a few exceptions that may be lowercase\n            if cls == GraphicsStateParameters and attr in [\"ca\", \"op\"]:\n                continue\n\n            assert pattern.match(constant_value)\n\n\ndef test_user_access_permissions__dict_handling():\n    # Value is mix of configurable and reserved bits.\n    # Reserved bits should not be part of the dictionary.\n    as_dict = UserAccessPermissions(512 + 64 + 8).to_dict()\n    assert as_dict == {\n        \"add_or_modify\": False,\n        \"assemble_doc\": False,\n        \"extract\": False,\n        \"extract_text_and_graphics\": True,\n        \"fill_form_fields\": False,\n        \"modify\": True,\n        \"print\": False,\n        \"print_to_representation\": False,\n    }\n\n    # Convert the dictionary back to an integer.\n    # This should add the reserved bits automatically.\n    permissions = UserAccessPermissions.from_dict(as_dict)\n    assert permissions == 4294963912\n\n    # Roundtrip for valid dictionary.\n    data = {\n        \"add_or_modify\": True,\n        \"assemble_doc\": False,\n        \"extract\": False,\n        \"extract_text_and_graphics\": True,\n        \"fill_form_fields\": False,\n        \"modify\": True,\n        \"print\": False,\n        \"print_to_representation\": True,\n    }\n    assert UserAccessPermissions.from_dict(data).to_dict() == data\n\n    # Empty inputs.\n    assert UserAccessPermissions.from_dict({}) == 4294963392  # Reserved bits.\n    assert UserAccessPermissions(0).to_dict() == {\n        \"add_or_modify\": False,\n        \"assemble_doc\": False,\n        \"extract\": False,\n        \"extract_text_and_graphics\": False,\n        \"fill_form_fields\": False,\n        \"modify\": False,\n        \"print\": False,\n        \"print_to_representation\": False,\n    }\n\n    # Unknown dictionary keys.\n    data = {\n        \"add_or_modify\": True,\n        \"key1\": False,\n        \"key2\": True,\n    }\n    unknown = {\n        \"key1\": False,\n        \"key2\": True,\n    }\n    with pytest.raises(\n        ValueError,\n        match=f\"Unknown dictionary keys: {unknown!r}\"\n    ):\n        UserAccessPermissions.from_dict(data)\n\n\ndef test_user_access_permissions__all():\n    all_permissions = UserAccessPermissions.all()\n    all_int = int(all_permissions)\n    all_string = bin(all_permissions)\n\n    assert all_string.startswith(\"0b\")\n    assert len(all_string[2:]) == 32  # 32-bit integer\n\n    assert all_int & UserAccessPermissions.R1 == 0\n    assert all_int & UserAccessPermissions.R2 == 0\n    assert all_int & UserAccessPermissions.PRINT == UserAccessPermissions.PRINT\n    assert all_int & UserAccessPermissions.R7 == UserAccessPermissions.R7\n    assert all_int & UserAccessPermissions.R31 == UserAccessPermissions.R31\n"
  },
  {
    "path": "tests/test_doc_common.py",
    "content": "\"\"\"Test the pypdf._doc_common module.\"\"\"\nimport itertools\nimport re\nimport shutil\nimport subprocess\nfrom io import BytesIO\nfrom operator import itemgetter\nfrom pathlib import Path\nfrom unittest import mock\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.errors import LimitReachedError, PdfReadError\nfrom pypdf.filters import FlateDecode\nfrom pypdf.generic import (\n    ArrayObject,\n    DictionaryObject,\n    EmbeddedFile,\n    EncodedStreamObject,\n    NameObject,\n    NullObject,\n    TextStringObject,\n    ViewerPreferences,\n)\nfrom tests import RESOURCE_ROOT, SAMPLE_ROOT, get_data_from_url\n\nPDFATTACH_BINARY = shutil.which(\"pdfattach\")\n\n\n@pytest.mark.skipif(PDFATTACH_BINARY is None, reason=\"Requires poppler-utils\")\ndef test_attachments(tmpdir):\n    tmpdir = Path(tmpdir)\n\n    # No attachments.\n    clean_path = SAMPLE_ROOT / \"002-trivial-libre-office-writer\" / \"002-trivial-libre-office-writer.pdf\"\n    with PdfReader(clean_path) as pdf:\n        assert pdf._list_attachments() == []\n        assert list(pdf.attachment_list) == []\n\n    # UF = name.\n    attached_path = tmpdir / \"attached.pdf\"\n    file_path = tmpdir / \"test.txt\"\n    file_path.write_bytes(b\"Hello World\\n\")\n    subprocess.run([PDFATTACH_BINARY, clean_path, file_path, attached_path])  # noqa: S603\n    with PdfReader(attached_path) as pdf:\n        assert pdf._list_attachments() == [\"test.txt\"]\n        assert pdf._get_attachments(\"test.txt\") == {\"test.txt\": b\"Hello World\\n\"}\n        assert [(x.name, x.content) for x in pdf.attachment_list] == [(\"test.txt\", b\"Hello World\\n\")]\n        assert next(pdf.attachment_list).alternative_name == \"test.txt\"\n\n    # UF != name.\n    different_path = tmpdir / \"different.pdf\"\n    different_path.write_bytes(re.sub(rb\" /UF [^/]+ /\", b\" /UF(my-file.txt) /\", attached_path.read_bytes()))\n    with PdfReader(different_path) as pdf:\n        assert pdf._list_attachments() == [\"test.txt\", \"my-file.txt\"]\n        assert pdf._get_attachments(\"test.txt\") == {\"test.txt\": b\"Hello World\\n\"}\n        assert pdf._get_attachments(\"my-file.txt\") == {\"my-file.txt\": b\"Hello World\\n\"}\n        assert [(x.name, x.content) for x in pdf.attachment_list] == [(\"test.txt\", b\"Hello World\\n\")]\n        assert next(pdf.attachment_list).alternative_name == \"my-file.txt\"\n\n    # Only name.\n    no_f_path = tmpdir / \"no-f.pdf\"\n    no_f_path.write_bytes(re.sub(rb\" /UF [^/]+ /\", b\" /\", attached_path.read_bytes()))\n    with PdfReader(no_f_path) as pdf:\n        assert pdf._list_attachments() == [\"test.txt\"]\n        assert pdf._get_attachments(\"test.txt\") == {\"test.txt\": b\"Hello World\\n\"}\n        assert [(x.name, x.content) for x in pdf.attachment_list] == [(\"test.txt\", b\"Hello World\\n\")]\n        assert next(pdf.attachment_list).alternative_name is None\n\n    # UF and F.\n    uf_f_path = tmpdir / \"uf-f.pdf\"\n    uf_f_path.write_bytes(attached_path.read_bytes().replace(b\" /UF \", b\"/F(file.txt) /UF \"))\n    with PdfReader(uf_f_path) as pdf:\n        assert pdf._list_attachments() == [\"test.txt\"]\n        assert pdf._get_attachments(\"test.txt\") == {\"test.txt\": b\"Hello World\\n\"}\n        assert [(x.name, x.content) for x in pdf.attachment_list] == [(\"test.txt\", b\"Hello World\\n\")]\n        assert next(pdf.attachment_list).alternative_name == \"test.txt\"\n\n    # Only F.\n    only_f_path = tmpdir / \"f.pdf\"\n    only_f_path.write_bytes(attached_path.read_bytes().replace(b\" /UF \", b\" /F \"))\n    with PdfReader(only_f_path) as pdf:\n        assert pdf._list_attachments() == [\"test.txt\"]\n        assert pdf._get_attachments(\"test.txt\") == {\"test.txt\": b\"Hello World\\n\"}\n        assert [(x.name, x.content) for x in pdf.attachment_list] == [(\"test.txt\", b\"Hello World\\n\")]\n        assert next(pdf.attachment_list).alternative_name == \"test.txt\"\n\n\ndef test_get_attachments__same_attachment_more_than_twice():\n    writer = PdfWriter()\n    writer.add_blank_page(100, 100)\n    for i in range(5):\n        writer.add_attachment(\"test.txt\", f\"content{i}\")\n    assert writer._get_attachments(\"test.txt\") == {\n        \"test.txt\": [b\"content0\", b\"content1\", b\"content2\", b\"content3\", b\"content4\"]\n    }\n    assert [(x.name, x.content) for x in writer.attachment_list] == [\n        (\"test.txt\", b\"content0\"),\n        (\"test.txt\", b\"content1\"),\n        (\"test.txt\", b\"content2\"),\n        (\"test.txt\", b\"content3\"),\n        (\"test.txt\", b\"content4\"),\n    ]\n\n\ndef test_get_attachments__alternative_name_is_none():\n    writer = PdfWriter()\n    attachment = EmbeddedFile(name=\"test.txt\", pdf_object=writer.root_object)\n    assert attachment.alternative_name is None\n    with mock.patch(\n            \"pypdf._writer.PdfWriter.attachment_list\",\n            new_callable=mock.PropertyMock(return_value=[attachment])\n    ), mock.patch(\n            \"pypdf.generic._files.EmbeddedFile.content\",\n            new_callable=mock.PropertyMock(return_value=b\"content\")\n    ):\n        assert writer._get_attachments() == {\"test.txt\": b\"content\"}\n\n\n@pytest.mark.enable_socket\ndef test_byte_encoded_named_destinations():\n    url = \"https://github.com/user-attachments/files/19820164/pypdf_issue.pdf\"\n    name = \"issue3261.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))\n\n    page = reader.pages[0]\n    for annotation in page.annotations:\n        if annotation.get(\"/Subtype\") == \"/Link\":\n            action = annotation[\"/A\"]\n            if action[\"/S\"] == \"/GoTo\":\n                named_dest = action[\"/D\"]\n                assert str(named_dest) in reader.named_destinations\n                assert TextStringObject(named_dest) in reader.named_destinations\n\n    assert reader.named_destinations == {\n        \"Doc-Start\": {\n            \"/Title\": \"Doc-Start\",\n            \"/Page\": page.indirect_reference,\n            \"/Type\": \"/XYZ\",\n            \"/Left\": 133.768,\n            \"/Top\": 667.198,\n            \"/Zoom\": NullObject()\n        },\n        \"cite.dacÃ\\xadk2025racerflightweightstaticdata\": {\n            \"/Title\": \"cite.dacÃ\\xadk2025racerflightweightstaticdata\",\n            \"/Page\": page.indirect_reference,\n            \"/Type\": \"/XYZ\",\n            \"/Left\": 133.768,\n            \"/Top\": 614.424,\n            \"/Zoom\": NullObject()\n        },\n        # This is the same as the previous entry, but with `str(name)` instead of the title.\n        \"楣整搮捡귃㉫㈰爵捡牥汦杩瑨敷杩瑨瑳瑡捩慤慴\": {\n            \"/Left\": 133.768,\n            \"/Page\": page.indirect_reference,\n            \"/Title\": \"cite.dacÃ\\xadk2025racerflightweightstaticdata\",\n            \"/Top\": 614.424,\n            \"/Type\": \"/XYZ\",\n            \"/Zoom\": NullObject()\n        },\n        \"page.1\": {\n            \"/Title\": \"page.1\",\n            \"/Page\": page.indirect_reference,\n            \"/Type\": \"/XYZ\",\n            \"/Left\": 132.768,\n            \"/Top\": 705.06,\n            \"/Zoom\": NullObject()\n        },\n        \"section*.1\": {\n            \"/Title\": \"section*.1\",\n            \"/Page\": page.indirect_reference,\n            \"/Type\": \"/XYZ\",\n            \"/Left\": 133.768,\n            \"/Top\": 642.222,\n            \"/Zoom\": NullObject()\n        }\n    }\n\n\ndef test_viewer_preferences__indirect_reference():\n    input_path = RESOURCE_ROOT / \"git.pdf\"\n    reader = PdfReader(input_path)\n    assert (0, 24) not in reader.resolved_objects\n    viewer_preferences = reader.viewer_preferences\n    assert isinstance(viewer_preferences, ViewerPreferences)\n    assert viewer_preferences == {\"/DisplayDocTitle\": True}\n    assert (0, 24) in reader.resolved_objects\n    assert id(viewer_preferences) == id(reader.viewer_preferences)\n    assert id(viewer_preferences) == id(reader.resolved_objects[(0, 24)])\n\n\n@pytest.mark.enable_socket\ndef test_named_destinations__tree_is_null_object():\n    url = \"https://github.com/user-attachments/files/20885216/test.pdf\"\n    name = \"issue3330.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))\n\n    assert reader.named_destinations == {}\n\n\n@pytest.mark.enable_socket\ndef test_outline__issue3462():\n    url = \"https://github.com/user-attachments/files/22293402/e371fffe0b_a7cccde95a.pdf\"\n    name = \"issue3462.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))\n\n    outline_flat = list(\n        itertools.chain.from_iterable(\n            entry if isinstance(entry, list) else [entry] for entry in reader.outline\n        )\n    )\n    assert list(map(itemgetter(\"/Title\"), outline_flat)) == [\n        \"AR 2021 - Daftar Isi\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"AR 2021 Book 001 (Highlights - Ikhtisar Saham)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"AR 2021 Book 002 (Laporan Manajemen)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"Page 9\",\n        \"AR 2021 Book 003-1 (Profil Perusahaan)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"Page 9\",\n        \"Page 10\",\n        \"Page 11\",\n        \"Page 12\",\n        \"Page 13\",\n        \"Page 14\",\n        \"Page 15\",\n        \"Page 16\",\n        \"Page 17\",\n        \"Page 18\",\n        \"Page 19\",\n        \"Page 20\",\n        \"Page 21\",\n        \"Page 22\",\n        \"Page 23\",\n        \"Page 24\",\n        \"Page 25\",\n        \"Page 26\",\n        \"Page 27\",\n        \"Page 28\",\n        \"Page 29\",\n        \"Page 30\",\n        \"Page 31\",\n        \"Page 32\",\n        \"Page 33\",\n        \"Page 34\",\n        \"Page 35\",\n        \"Page 36\",\n        \"Page 37\",\n        \"Page 38\",\n        \"Page 39\",\n        \"Page 40\",\n        \"Page 41\",\n        \"Page 42\",\n        \"Page 43\",\n        \"Page 44\",\n        \"Page 45\",\n        \"Page 46\",\n        \"Page 47\",\n        \"AR 2021 Book 003-2 (Sumber Daya Manusia)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"Page 9\",\n        \"Page 10\",\n        \"Page 11\",\n        \"Page 12\",\n        \"AR 2021 Book 003-3 (Komposisi pemegang saham)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"AR 2021 Book 003-4 (Kronologis Pencatatan Saham)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"AR 2021 Book 003-5 (Akuntan Publik Independen)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"AR 2021 Book 004 (Analisa dan Pembahasan Manajemen)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"Page 9\",\n        \"Page 10\",\n        \"Page 11\",\n        \"Page 12\",\n        \"Page 13\",\n        \"Page 14\",\n        \"Page 15\",\n        \"Page 16\",\n        \"Page 17\",\n        \"Page 18\",\n        \"Page 19\",\n        \"Page 20\",\n        \"Page 21\",\n        \"AR 2021 Book 005-1 (Tata Kelola Perusahaan)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"Page 9\",\n        \"Page 10\",\n        \"Page 11\",\n        \"Page 12\",\n        \"AR 2021 Book 005-2 (Direksi-Komisaris)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"Page 9\",\n        \"Page 10\",\n        \"Page 11\",\n        \"Page 12\",\n        \"Page 13\",\n        \"Page 14\",\n        \"Page 15\",\n        \"Page 16\",\n        \"Page 17\",\n        \"Page 18\",\n        \"Page 19\",\n        \"Page 20\",\n        \"Page 21\",\n        \"Page 22\",\n        \"Page 23\",\n        \"Page 24\",\n        \"Page 25\",\n        \"Page 26\",\n        \"Page 27\",\n        \"Page 28\",\n        \"Page 29\",\n        \"Page 30\",\n        \"Page 31\",\n        \"Page 32\",\n        \"Page 33\",\n        \"Page 34\",\n        \"Page 35\",\n        \"Page 36\",\n        \"Page 37\",\n        \"Page 38\",\n        \"AR 2021 Book 005-3 (Komite Audit)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"Page 9\",\n        \"AR 2021 Book 005-4 (Sekretaris Perusahaan)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"Page 9\",\n        \"Page 10\",\n        \"AR 2021 Book 005-5 (Unit Audit Internal)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"AR 2021 Book 005-6 (Sistem Pengendalian Internal)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"AR 2021 Book 005-7 (Program Saham)\",\n        \"Page 1\",\n        \"AR 2021 Book 005-8 ( Whistleblowing)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"Page 3\",\n        \"Page 4\",\n        \"Page 5\",\n        \"Page 6\",\n        \"Page 7\",\n        \"Page 8\",\n        \"Page 9\",\n        \"Page 10\",\n        \"Page 11\",\n        \"Page 12\",\n        \"Page 13\",\n        \"Page 14\",\n        \"Page 15\",\n        \"Page 16\",\n        \"Page 17\",\n        \"Page 18\",\n        \"Page 19\",\n        \"Page 20\",\n        \"Page 21\",\n        \"Page 22\",\n        \"Page 23\",\n        \"Page 24\",\n        \"Page 25\",\n        \"AR 2021 Book 006 (Tanggung Jawab Sosial - CSR)\",\n        \"Page 1\",\n        \"Page 2\",\n        \"AR 2021 Book 007-1 (LAPORAN KEUANGAN KONSOLIDASIAN)\",\n        \"Page 1\",\n        \"AR 2021 Book 007-2 (Isi Laporan Keuangan)\",\n        \"AR 2021 Book 008 (Tanggung Jawab Atas Laporan Tahunan)\",\n        \"Page 1\",\n        \"Page 2\"\n    ]\n\n\ndef test_flatten__cyclic_references():\n    path = RESOURCE_ROOT / \"crazyones.pdf\"\n\n    reader = PdfReader(path)\n    assert len(reader.pages) == 1\n    reader._flatten()\n\n    # Make the first child point to the object itself.\n    pages_object = reader.get_object(10)\n    pages_object[NameObject(\"/Kids\")][0].indirect_reference.idnum = 10\n    reader.resolved_objects[(10, 0)] = pages_object\n\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^Detected cyclic page references\\.$\"):\n        reader._flatten()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.timeout(10)\ndef test_get_outline__cyclic_references(caplog):\n    url = \"https://github.com/user-attachments/files/24859044/circular_outline.pdf\"\n    name = \"circular_outline.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))\n\n    assert reader.outline == [\n        {\n            \"/%is_open%\": True,\n            \"/Page\": reader.pages[0].indirect_reference,\n            \"/Title\": \"Bookmark A\",\n            \"/Type\": \"/Fit\"\n        },\n        {\n            \"/%is_open%\": True,\n            \"/Page\": reader.pages[0].indirect_reference,\n            \"/Title\": \"Bookmark B\",\n            \"/Type\": \"/Fit\"\n        }\n    ]\n    assert caplog.messages[0].startswith(\"Detected cycle in outline structure for {\")\n\n\n@pytest.mark.enable_socket\n@pytest.mark.timeout(10)\ndef test_get_outline__cyclic_references__nested_handling(caplog):\n    url = \"https://github.com/user-attachments/files/24859044/circular_outline.pdf\"\n    name = \"circular_outline.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url=url, name=name)))\n\n    nested_outline = DictionaryObject()\n    writer._add_object(nested_outline)\n    nested_outline.update({\n        NameObject(\"/Title\"): TextStringObject(\"Nested entry\"),\n        NameObject(\"/Parent\"): writer.get_object(5),\n        NameObject(\"/Dest\"): ArrayObject([writer.pages[0].indirect_reference, NameObject(\"/Fit\")]),\n        NameObject(\"/Next\"): writer.get_object(6),\n    })\n    writer.get_object(5)[NameObject(\"/First\")] = nested_outline.indirect_reference\n    writer.get_object(6)[NameObject(\"/First\")] = nested_outline.indirect_reference\n\n    assert writer.outline == [\n        {\n            \"/%is_open%\": True,\n            \"/Page\": writer.pages[0].indirect_reference,\n            \"/Title\": \"Bookmark A\",\n            \"/Type\": \"/Fit\"\n        },\n        [\n            {\n                \"/%is_open%\": True,\n                \"/Page\": writer.pages[0].indirect_reference,\n                \"/Title\": \"Nested entry\",\n                \"/Type\": \"/Fit\"\n            },\n            {\n                \"/%is_open%\": True,\n                \"/Page\": writer.pages[0].indirect_reference,\n                \"/Title\": \"Bookmark B\",\n                \"/Type\": \"/Fit\"\n            }\n        ],\n        {\n            \"/%is_open%\": True,\n            \"/Page\": writer.pages[0].indirect_reference,\n            \"/Title\": \"Bookmark B\",\n            \"/Type\": \"/Fit\"\n        },\n        [\n            {\n                \"/%is_open%\": True,\n                \"/Page\": writer.pages[0].indirect_reference,\n                \"/Title\": \"Nested entry\",\n                \"/Type\": \"/Fit\"\n            }\n        ]\n    ]\n    assert caplog.messages[0].startswith(\"Detected cycle in outline structure for {\")\n\n\ndef test_xfa__decompression_limit():\n    payload = b\"A\" * 100_0000\n    compressed = FlateDecode.encode(payload, 9)\n\n    writer = PdfWriter()\n    writer.add_blank_page(width=72, height=72)\n\n    stream = EncodedStreamObject()\n    stream._data = compressed\n    stream[NameObject(\"/Filter\")] = NameObject(\"/FlateDecode\")\n    stream_reference = writer._add_object(stream)\n\n    acro = DictionaryObject()\n    acro[NameObject(\"/XFA\")] = ArrayObject([TextStringObject(\"datasets\"), stream_reference])\n    writer.root_object[NameObject(\"/AcroForm\")] = writer._add_object(acro)\n\n    data = BytesIO()\n    writer.write(data)\n    data.flush()\n\n    reader = PdfReader(data)\n    with mock.patch(\"pypdf.filters.ZLIB_MAX_OUTPUT_LENGTH\", 75_000), pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Limit reached while decompressing. 902 bytes remaining.$\"\n    ):\n        _ = reader.xfa\n"
  },
  {
    "path": "tests/test_encryption.py",
    "content": "\"\"\"Test the pypdf._encryption module.\"\"\"\nimport secrets\nfrom io import BytesIO\n\nimport pytest\n\nimport pypdf\nfrom pypdf import PasswordType, PdfReader, PdfWriter\nfrom pypdf._crypt_providers import crypt_provider\nfrom pypdf._crypt_providers._fallback import _DEPENDENCY_ERROR_STR\nfrom pypdf._encryption import AlgV5, CryptAES, CryptRC4\nfrom pypdf.errors import DependencyError, PdfReadError\nfrom tests import RESOURCE_ROOT, SAMPLE_ROOT\n\nUSE_CRYPTOGRAPHY = crypt_provider[0] == \"cryptography\"\nUSE_PYCRYPTODOME = crypt_provider[0] == \"pycryptodome\"\nHAS_AES = USE_CRYPTOGRAPHY or USE_PYCRYPTODOME\n\n\n@pytest.mark.parametrize(\n    (\"name\", \"requires_aes\"),\n    [\n        # unencrypted pdf\n        (\"unencrypted.pdf\", False),\n        # created by:\n        # qpdf --encrypt \"\" \"\" 40 -- unencrypted.pdf r2-empty-password.pdf\n        (\"r2-empty-password.pdf\", False),\n        # created by:\n        # qpdf --encrypt \"\" \"\" 128 -- unencrypted.pdf r3-empty-password.pdf\n        (\"r3-empty-password.pdf\", False),\n        # created by:\n        # qpdf --encrypt \"asdfzxcv\" \"\" 40 -- unencrypted.pdf r2-user-password.pdf\n        (\"r2-user-password.pdf\", False),\n        # created by:\n        # qpdf --encrypt \"\" \"asdfzxcv\" 40 -- unencrypted.pdf r2-owner-password.pdf\n        (\"r2-owner-password.pdf\", False),\n        # created by:\n        # qpdf --encrypt \"asdfzxcv\" \"\" 128 -- unencrypted.pdf r3-user-password.pdf\n        (\"r3-user-password.pdf\", False),\n        # created by:\n        # qpdf --encrypt \"asdfzxcv\" \"\" 128 --force-V4 -- unencrypted.pdf r4-user-password.pdf\n        (\"r4-user-password.pdf\", False),\n        # created by:\n        # qpdf --encrypt \"\" \"asdfzxcv\" 128 --force-V4 -- unencrypted.pdf r4-owner-password.pdf\n        (\"r4-owner-password.pdf\", False),\n        # created by:\n        # qpdf --encrypt \"asdfzxcv\" \"\" 128 --use-aes=y -- unencrypted.pdf r4-aes-user-password.pdf\n        (\"r4-aes-user-password.pdf\", True),\n        # created by:\n        # qpdf --encrypt \"\" \"\" 256 --force-R5 -- unencrypted.pdf r5-empty-password.pdf\n        (\"r5-empty-password.pdf\", True),\n        # created by:\n        # qpdf --encrypt \"asdfzxcv\" \"\" 256 --force-R5 -- unencrypted.pdf r5-user-password.pdf\n        (\"r5-user-password.pdf\", True),\n        # created by:\n        # qpdf --encrypt \"\" \"asdfzxcv\" 256 --force-R5 -- unencrypted.pdf r5-owner-password.pdf\n        (\"r5-owner-password.pdf\", True),\n        # created by:\n        # qpdf --encrypt \"\" \"\" 256 -- unencrypted.pdf r6-empty-password.pdf\n        (\"r6-empty-password.pdf\", True),\n        # created by:\n        # qpdf --encrypt \"asdfzxcv\" \"\" 256 -- unencrypted.pdf r6-user-password.pdf\n        (\"r6-user-password.pdf\", True),\n        # created by:\n        # qpdf --encrypt \"\" \"asdfzxcv\" 256 -- unencrypted.pdf r6-owner-password.pdf\n        (\"r6-owner-password.pdf\", True),\n    ],\n)\ndef test_encryption(name, requires_aes):\n    \"\"\"\n    Encrypted PDFs are handled correctly.\n\n    This test function ensures that:\n    - If PyCryptodome or cryptography is not available and required, a DependencyError is raised\n    - Encrypted PDFs are identified correctly\n    - Decryption works for encrypted PDFs\n    - Metadata is properly extracted from the decrypted PDF\n    \"\"\"\n    inputfile = RESOURCE_ROOT / \"encryption\" / name\n    if requires_aes and not HAS_AES:\n        with pytest.raises(DependencyError) as exc:\n            ipdf = pypdf.PdfReader(inputfile)\n            ipdf.decrypt(\"asdfzxcv\")\n            dd = dict(ipdf.metadata)\n        assert exc.value.args[0] == _DEPENDENCY_ERROR_STR\n        return\n    ipdf = pypdf.PdfReader(inputfile)\n    if str(inputfile).endswith(\"unencrypted.pdf\"):\n        assert not ipdf.is_encrypted\n    else:\n        assert ipdf.is_encrypted\n        ipdf.decrypt(\"asdfzxcv\")\n    assert len(ipdf.pages) == 1\n    dd = dict(ipdf.metadata)\n    # remove empty value entry\n    dd = {x[0]: x[1] for x in dd.items() if x[1]}\n    assert dd == {\n        \"/Author\": \"cheng\",\n        \"/CreationDate\": \"D:20220414132421+05'24'\",\n        \"/Creator\": \"WPS Writer\",\n        \"/ModDate\": \"D:20220414132421+05'24'\",\n        \"/SourceModified\": \"D:20220414132421+05'24'\",\n        \"/Trapped\": \"/False\",\n    }\n\n\n@pytest.mark.parametrize(\n    (\"name\", \"user_passwd\", \"owner_passwd\"),\n    [\n        # created by\n        # qpdf --encrypt \"foo\" \"bar\" 256 -- unencrypted.pdf r6-both-passwords.pdf\n        (\"r6-both-passwords.pdf\", \"foo\", \"bar\"),\n    ],\n)\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_pdf_with_both_passwords(name, user_passwd, owner_passwd):\n    \"\"\"\n    PDFs with both user and owner passwords are handled correctly.\n\n    This test function ensures that:\n    - Encrypted PDFs with both user and owner passwords are identified correctly\n    - Decryption works for both user and owner passwords\n    - The correct password type is returned after decryption\n    - The number of pages is correctly identified after decryption\n    \"\"\"\n    inputfile = RESOURCE_ROOT / \"encryption\" / name\n    ipdf = pypdf.PdfReader(inputfile)\n    assert ipdf.is_encrypted\n    assert ipdf.decrypt(user_passwd) == PasswordType.USER_PASSWORD\n    assert ipdf.decrypt(owner_passwd) == PasswordType.OWNER_PASSWORD\n    assert len(ipdf.pages) == 1\n\n\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_aesv2_without_length_in_encrypt_dict():\n    \"\"\"\n    AESV2-encrypted PDF without /Length in encrypt dict decrypts correctly.\n\n    Some PDFs omit /Length in the main encrypt dict (defaulting to 40 bits),\n    but AESV2 requires 128 bits. The key length should be read from the\n    crypt filter dict instead.\n    \"\"\"\n    inputfile = RESOURCE_ROOT / \"encryption\" / \"r4-aes-v2-no-key-length.pdf\"\n    reader = PdfReader(inputfile)\n    assert reader.is_encrypted\n    result = reader.decrypt(\"\")\n    assert result in (PasswordType.USER_PASSWORD, PasswordType.OWNER_PASSWORD)\n    assert len(reader.pages) == 1\n\n\n@pytest.mark.parametrize(\n    (\"pdffile\", \"password\"),\n    [\n        (\"crazyones-encrypted-256.pdf\", \"password\"),\n        (\"crazyones-encrypted-256.pdf\", b\"password\"),\n    ],\n)\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_read_page_from_encrypted_file_aes_256(pdffile, password):\n    \"\"\"\n    A page can be read from an encrypted.\n\n    This is a regression test for issue 327:\n    IndexError for get_page() of decrypted file\n    \"\"\"\n    path = RESOURCE_ROOT / pdffile\n    pypdf.PdfReader(path, password=password).pages[0]\n\n\n@pytest.mark.parametrize(\n    \"names\",\n    [\n        (\n            [\n                \"unencrypted.pdf\",\n                \"r3-user-password.pdf\",\n                \"r4-aes-user-password.pdf\",\n                \"r5-user-password.pdf\",\n            ]\n        ),\n    ],\n)\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_merge_encrypted_pdfs(names):\n    \"\"\"Encrypted PDFs can be merged after decryption.\"\"\"\n    merger = pypdf.PdfWriter()\n    files = [RESOURCE_ROOT / \"encryption\" / x for x in names]\n    pdfs = [pypdf.PdfReader(x) for x in files]\n    for pdf in pdfs:\n        if pdf.is_encrypted:\n            pdf.decrypt(\"asdfzxcv\")\n        merger.append(pdf)\n    # no need to write to file\n    merger.close()\n\n\n@pytest.mark.skipif(\n    USE_CRYPTOGRAPHY,\n    reason=\"Limitations of cryptography. see https://github.com/pyca/cryptography/issues/2494\",\n)\n@pytest.mark.parametrize(\n    \"cryptcls\",\n    [\n        CryptRC4,\n    ],\n)\ndef test_encrypt_decrypt_with_cipher_class(cryptcls):\n    \"\"\"Encryption and decryption using a cipher class work as expected.\"\"\"\n    message = b\"Hello World\"\n    key = bytes(0 for _ in range(128))  # b\"secret key\"\n    crypt = cryptcls(key)\n    assert crypt.decrypt(crypt.encrypt(message)) == message\n\n\ndef test_attempt_decrypt_unencrypted_pdf():\n    \"\"\"Attempting to decrypt an unencrypted PDF raises a PdfReadError.\"\"\"\n    path = RESOURCE_ROOT / \"crazyones.pdf\"\n    with pytest.raises(PdfReadError) as exc:\n        PdfReader(path, password=\"nonexistent\")\n    assert exc.value.args[0] == \"Not an encrypted file\"\n\n\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_alg_v5_generate_values():\n    \"\"\"\n    Algorithm V5 values are generated without raising exceptions.\n\n    This test function checks if there is an exception during the value generation.\n    It does not verify that the content is correct.\n    \"\"\"\n    key = b\"0123456789123451\"\n    values = AlgV5.generate_values(\n        R=5,\n        user_password=b\"foo\",\n        owner_password=b\"bar\",\n        key=key,\n        p=0,\n        metadata_encrypted=True,\n    )\n    assert values == {\n        \"/U\": values[\"/U\"],\n        \"/UE\": values[\"/UE\"],\n        \"/O\": values[\"/O\"],\n        \"/OE\": values[\"/OE\"],\n        \"/Perms\": values[\"/Perms\"],\n    }\n\n\n@pytest.mark.parametrize(\n    (\"alg\", \"requires_aes\"),\n    [\n        (\"RC4-40\", False),\n        (\"RC4-128\", False),\n        (\"AES-128\", True),\n        (\"AES-256-R5\", True),\n        (\"AES-256\", True),\n        (\"ABCD\", False),\n    ],\n)\ndef test_pdf_encrypt(pdf_file_path, alg, requires_aes):\n    user_password = secrets.token_urlsafe(10)\n    owner_password = secrets.token_urlsafe(10)\n\n    reader = PdfReader(RESOURCE_ROOT / \"encryption\" / \"unencrypted.pdf\")\n    page = reader.pages[0]\n    text0 = page.extract_text()\n\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    # test with invalid algorithm name\n    if alg == \"ABCD\":\n        with pytest.raises(ValueError) as exc:\n            writer.encrypt(\n                user_password=user_password,\n                owner_password=owner_password,\n                algorithm=alg,\n            )\n        assert exc.value.args[0] == \"Algorithm 'ABCD' NOT supported\"\n        return\n\n    if requires_aes and not HAS_AES:\n        with pytest.raises(DependencyError) as exc:\n            writer.encrypt(\n                user_password=user_password,\n                owner_password=owner_password,\n                algorithm=alg,\n            )\n            with open(pdf_file_path, \"wb\") as output_stream:\n                writer.write(output_stream)\n        assert exc.value.args[0] == _DEPENDENCY_ERROR_STR\n        return\n\n    writer.encrypt(\n        user_password=user_password, owner_password=owner_password, algorithm=alg\n    )\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n    reader = PdfReader(pdf_file_path)\n    assert reader.is_encrypted\n    assert reader.decrypt(owner_password) == PasswordType.OWNER_PASSWORD\n    assert reader.decrypt(user_password) == PasswordType.USER_PASSWORD\n\n    page = reader.pages[0]\n    text1 = page.extract_text()\n    assert text0 == text1\n\n\n@pytest.mark.parametrize(\n    \"count\",\n    [1, 2, 3, 4, 5, 10],\n)\ndef test_pdf_encrypt_multiple(pdf_file_path, count):\n    user_password = secrets.token_urlsafe(10)\n    owner_password = secrets.token_urlsafe(10)\n\n    reader = PdfReader(RESOURCE_ROOT / \"encryption\" / \"unencrypted.pdf\")\n    page = reader.pages[0]\n    text0 = page.extract_text()\n\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    if count == 1:\n        owner_password = None\n\n    for _i in range(count):\n        writer.encrypt(\n            user_password=user_password,\n            owner_password=owner_password,\n            algorithm=\"RC4-128\",\n        )\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n    reader = PdfReader(pdf_file_path)\n    assert reader.is_encrypted\n    if owner_password is None:\n        # NOTICE: owner_password will set to user_password if it's None\n        assert reader.decrypt(user_password) == PasswordType.OWNER_PASSWORD\n    else:\n        assert reader.decrypt(owner_password) == PasswordType.OWNER_PASSWORD\n        assert reader.decrypt(user_password) == PasswordType.USER_PASSWORD\n\n    page = reader.pages[0]\n    text1 = page.extract_text()\n    assert text0 == text1\n\n\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_aes_decrypt_corrupted_data():\n    \"\"\"Just for robustness\"\"\"\n    aes = CryptAES(secrets.token_bytes(16))\n    for num in [0, 17, 32]:\n        aes.decrypt(secrets.token_bytes(num))\n\n\n@pytest.mark.samples\ndef test_encrypt_stream_dictionary(pdf_file_path):\n    user_password = secrets.token_urlsafe(10)\n\n    reader = PdfReader(SAMPLE_ROOT / \"023-cmyk-image/cmyk-image.pdf\")\n    page = reader.pages[0]\n    original_image_obj = reader.get_object(page.images[\"/I\"].indirect_reference)\n\n    writer = PdfWriter()\n    writer.add_page(reader.pages[0])\n    writer.encrypt(\n        user_password=user_password,\n        owner_password=None,\n        algorithm=\"RC4-128\",\n    )\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n    reader = PdfReader(pdf_file_path)\n    assert reader.is_encrypted\n    assert reader.decrypt(user_password) == PasswordType.OWNER_PASSWORD\n    page = reader.pages[0]\n    decrypted_image_obj = reader.get_object(page.images[\"/I\"].indirect_reference)\n\n    assert decrypted_image_obj[\"/ColorSpace\"][3] == original_image_obj[\"/ColorSpace\"][3]\n\n\ndef test_are_permissions_valid_none_for_unencrypted():\n    \"\"\"are_permissions_valid is None for unencrypted documents.\"\"\"\n    reader = PdfReader(RESOURCE_ROOT / \"encryption\" / \"unencrypted.pdf\")\n    assert reader.are_permissions_valid is None\n\n\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_are_permissions_valid_none_before_decrypt():\n    \"\"\"are_permissions_valid is None for encrypted documents before decrypt().\"\"\"\n    reader = PdfReader(RESOURCE_ROOT / \"encryption\" / \"r6-both-passwords.pdf\")\n    assert reader.are_permissions_valid is None\n\n\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_are_permissions_valid_true_for_valid_r6():\n    \"\"\"are_permissions_valid is True when /Perms integrity check passes.\"\"\"\n    reader = PdfReader(RESOURCE_ROOT / \"encryption\" / \"r6-owner-password.pdf\")\n    reader.decrypt(\"usersecret\")\n    assert reader.are_permissions_valid is True\n\n\ndef test_are_permissions_valid_true_for_v4():\n    \"\"\"are_permissions_valid defaults to True for V4 encryption (no /Perms field).\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"encryption\" / \"unencrypted.pdf\")\n    writer.encrypt(user_password=\"user\", owner_password=\"owner\", algorithm=\"RC4-128\")\n    output = BytesIO()\n    writer.write(output)\n    reader = PdfReader(output)\n    reader.decrypt(\"user\")\n    assert reader.are_permissions_valid is True\n\n\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_are_permissions_valid_false_when_tampered():\n    \"\"\"are_permissions_valid is False when /Perms has been tampered with.\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"encryption\" / \"unencrypted.pdf\")\n    writer.encrypt(user_password=\"user\", owner_password=\"owner\", algorithm=\"AES-256\")\n    output = BytesIO()\n    writer.write(output)\n\n    # Tamper with /Perms by modifying the raw bytes\n    data = bytearray(output.getvalue())\n    perms_marker = b\"/Perms \"\n    idx = data.find(perms_marker)\n    assert idx != -1, \"/Perms not found in PDF\"\n    # Find the hex string value after /Perms and corrupt a byte\n    start = data.index(b\"<\", idx)\n    data[start + 2] ^= 0xFF  # flip bits in the first byte of the hex string\n    tampered = BytesIO(bytes(data))\n\n    reader = PdfReader(tampered)\n    reader.decrypt(\"user\")\n    assert reader.are_permissions_valid is False\n"
  },
  {
    "path": "tests/test_filters.py",
    "content": "\"\"\"Test the pypdf.filters module.\"\"\"\nimport os\nimport string\nimport subprocess\nimport sys\nimport zlib\nfrom io import BytesIO\nfrom itertools import product as cartesian_product\nfrom pathlib import Path\nfrom typing import cast\nfrom unittest import mock\n\nimport pytest\nfrom PIL import Image, ImageOps\n\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.errors import DependencyError, DeprecationError, LimitReachedError, PdfReadError, PdfStreamError\nfrom pypdf.filters import (\n    ASCII85Decode,\n    ASCIIHexDecode,\n    CCITParameters,\n    CCITTFaxDecode,\n    CCITTParameters,\n    FlateDecode,\n    JBIG2Decode,\n    RunLengthDecode,\n    decode_stream_data,\n    decompress,\n)\nfrom pypdf.generic import (\n    ArrayObject,\n    BooleanObject,\n    ContentStream,\n    DictionaryObject,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    StreamObject,\n    TextStringObject,\n)\n\nfrom . import RESOURCE_ROOT, PILContext, get_data_from_url\nfrom .test_encryption import HAS_AES\nfrom .test_images import image_similarity\nfrom .utils import get_image_data\n\nfilter_inputs = (\n    string.ascii_letters,\n    string.ascii_lowercase,\n    string.ascii_uppercase,\n    string.digits,\n    string.hexdigits,\n    string.octdigits,\n    string.punctuation,\n    string.printable,\n    string.whitespace,  # Add more\n)\n\n\n@pytest.mark.parametrize(\n    (\"predictor\", \"s\"), list(cartesian_product([1], filter_inputs))\n)\ndef test_flate_decode_encode(predictor, s):\n    \"\"\"FlateDecode encode() and decode() methods work as expected.\"\"\"\n    codec = FlateDecode()\n    s = s.encode()\n    encoded = codec.encode(s)\n    assert codec.decode(encoded, DictionaryObject({\"/Predictor\": predictor})) == s\n\n\ndef test_flatedecode_unsupported_predictor():\n    \"\"\"\n    FlateDecode raises PdfReadError for unsupported predictors.\n\n    Predictor values outside the ranges [1, 2] and [10, 15] are not supported.\n\n    Checks that a PdfReadError is raised when decoding with unsupported predictors.\n    \"\"\"\n    codec = FlateDecode()\n    predictors = (-10, -1, 0, 3, 9, 16, 20, 100)\n\n    for predictor, s in cartesian_product(predictors, filter_inputs):\n        s = s.encode()\n        with pytest.raises(PdfReadError):\n            codec.decode(codec.encode(s), DictionaryObject({NameObject(\"/Predictor\"): NumberObject(predictor)}))\n\n\n@pytest.mark.parametrize(\n    (\"data\", \"expected\"),\n    [\n        (\">\", b\"\"),\n        (\n            \"6162636465666768696a6b6c6d6e6f707172737475767778797a>\",\n            string.ascii_lowercase.encode(),\n        ),\n        (\n            \"4142434445464748494a4b4c4d4e4f505152535455565758595a>\",\n            string.ascii_uppercase.encode(),\n        ),\n        (\n            \"6162636465666768696a6b6c6d6e6f707172737475767778797a4142434445464748494a4b4c4d4e4f505152535455565758595a>\",\n            string.ascii_letters.encode(),\n        ),\n        (\"30313233343536373839>\", string.digits.encode()),\n        (\n            \"3  031323334353637   3839>\",\n            string.digits.encode(),\n        ),  # Same as previous, but whitespaced\n        (\"30313233343536373839616263646566414243444546>\", string.hexdigits.encode()),\n        (\"20090a0d0b0c>\", string.whitespace.encode()),\n        # Odd number of hexadecimal digits behaves as if a 0 (zero) followed the last digit\n        (\"3938373635343332313>\", string.digits[::-1].encode()),\n    ],\n    ids=[\n        \"empty\",\n        \"ascii_lowercase\",\n        \"ascii_uppercase\",\n        \"ascii_letters\",\n        \"digits\",\n        \"digits_whitespace\",\n        \"hexdigits\",\n        \"whitespace\",\n        \"odd_number\",\n    ],\n)\ndef test_ascii_hex_decode_method(data, expected):\n    \"\"\"\n    Feeds a bunch of values to ASCIIHexDecode.decode() and ensures the\n    correct output is returned.\n    \"\"\"\n    assert ASCIIHexDecode.decode(data) == expected\n\n\ndef test_ascii_hex_decode_missing_eod(caplog):\n    \"\"\"ASCIIHexDecode.decode() logs warning when no EOD character is present.\"\"\"\n    ASCIIHexDecode.decode(\"\")\n    assert \"missing EOD in ASCIIHexDecode, check if output is OK\" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_decode_ahx():\n    \"\"\"\n    See #1979\n    Gray Image in CMYK : requiring reverse\n    \"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"NewJersey.pdf\")))\n    for p in reader.pages:\n        _ = list(p.images.keys())\n\n\ndef test_ascii85decode_with_overflow():\n    inputs = (\n        v + \"~>\"\n        for v in \"\\x01\\x02\\x03\\x04\\x05\\x06\\x07\\x08\\x0e\\x0f\"\n        \"\\x10\\x11\\x12\\x13\\x14\\x15\\x16\\x17\\x18\\x19\\x1a\"\n        \"\\x1b\\x1c\\x1d\\x1e\\x1fvwxy{|}~\\x7f\\x80\\x81\\x82\"\n        \"\\x83\\x84\\x85\\x86\\x87\\x88\\x89\\x8a\\x8b\\x8c\\x8d\"\n        \"\\x8e\\x8f\\x90\\x91\\x92\\x93\\x94\\x95\\x96\\x97\\x98\"\n        \"\\x99\\x9a\\x9b\\x9c\\x9d\\x9e\\x9f\\xa0¡¢£¤¥¦§¨©ª«¬\"\n        \"\\xad®¯°±²³´µ¶·¸¹º»¼½¾¿ÀÁÂÃÄÅÆÇ\"\n    )\n\n    for i in inputs:\n        with pytest.raises(ValueError):\n            ASCII85Decode.decode(i)\n\n\ndef test_ascii85decode_five_zero_bytes():\n    \"\"\"\n    ASCII85Decode handles the special case of five zero bytes correctly.\n\n    ISO 32000-1:2008 §7.4.3:\n\n    «As a special case, if all five bytes are 0, they shall be represented by\n    the character with code 122 (z) instead of by five exclamation points\n    (!!!!!).»\n    \"\"\"\n    inputs = (\"z\", \"zz\", \"zzz\")\n    exp_outputs = (\n        b\"\\x00\\x00\\x00\\x00\",\n        b\"\\x00\\x00\\x00\\x00\" * 2,\n        b\"\\x00\\x00\\x00\\x00\" * 3,\n    )\n\n    assert ASCII85Decode.decode(\"!!!!!~>\") == ASCII85Decode.decode(\"z~>\")\n\n    for expected, i in zip(exp_outputs, inputs):\n        assert ASCII85Decode.decode(i + \"~>\") == expected\n\n\ndef test_ccitparameters():\n    with pytest.raises(\n        DeprecationError,\n        match=r\"CCITParameters is deprecated and was removed in pypdf 6\\.0\\.0\\. Use CCITTParameters instead\",\n    ):\n        CCITParameters()\n\n\ndef test_ccittparameters():\n    params = CCITTParameters()\n    assert params.K == 0  # zero is the default according to page 78\n    assert params.BlackIs1 is False\n    assert params.group == 3\n\n\n@pytest.mark.parametrize(\n    (\"parameters\", \"expected_k\", \"expected_black_is_1\"),\n    [\n        (None, 0, False),\n        (\n            ArrayObject([{\"/K\": NumberObject(1)}, {\"/Columns\": NumberObject(13)}, {\"/BlackIs1\": BooleanObject(True)}]),\n            1, True\n        ),\n    ],\n)\ndef test_ccitt_get_parameters(parameters, expected_k, expected_black_is_1):\n    parameters = CCITTFaxDecode._get_parameters(parameters=parameters, rows=0)\n    assert parameters.K == expected_k  # noqa: SIM300\n    assert parameters.BlackIs1 == expected_black_is_1\n\n\ndef test_ccitt_get_parameters__indirect_object():\n    class Pdf:\n        def get_object(self, reference) -> NumberObject:\n            return NumberObject(42)\n\n    parameters = CCITTFaxDecode._get_parameters(\n        parameters=None, rows=IndirectObject(13, 1, Pdf())\n    )\n    assert parameters.rows == 42\n\n\ndef test_ccitt_fax_decode():\n    data = b\"\"\n    parameters = DictionaryObject(\n        {\"/K\": NumberObject(-1), \"/Columns\": NumberObject(17)}\n    )\n\n    # This is the header of an empty TIFF image.\n    assert CCITTFaxDecode.decode(data, parameters) == (\n        b\"II*\\x00\\x08\\x00\\x00\\x00\\x08\\x00\\x00\\x01\\x04\\x00\\x01\\x00\\x00\\x00\\x11\\x00\"\n        b\"\\x00\\x00\\x01\\x01\\x04\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x01\"\n        b\"\\x03\\x00\\x01\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x03\\x01\\x03\\x00\\x01\\x00\"\n        b\"\\x00\\x00\\x04\\x00\\x00\\x00\\x06\\x01\\x03\\x00\\x01\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x11\\x01\\x04\\x00\\x01\\x00\\x00\\x00l\\x00\\x00\\x00\\x16\\x01\"\n        b\"\\x04\\x00\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x17\\x01\\x04\\x00\\x01\\x00\"\n        b\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n    )\n\n\n@pytest.mark.enable_socket\ndef test_decompress_zlib_error(caplog):\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"tika-952445.pdf\")))\n    for page in reader.pages:\n        page.extract_text()\n    assert \"incorrect startxref pointer(3)\" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_lzw_decode_neg1():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"tika-921632.pdf\")))\n    page = reader.pages[47]\n    assert page.extract_text().startswith(\"Chapter 2\")\n\n\n@pytest.mark.enable_socket\ndef test_issue_399():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"tika-976970.pdf\")))\n    reader.pages[1].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_image_without_pillow(tmp_path):\n    env = os.environ.copy()\n    env[\"COVERAGE_PROCESS_START\"] = \"pyproject.toml\"\n\n    name = \"tika-914102.pdf\"\n    pdf_path = Path(__file__).parent / \"pdf_cache\" / name\n    pdf_path_str = pdf_path.resolve().as_posix()\n\n    source_file = tmp_path / \"script.py\"\n    source_file.write_text(\n        f\"\"\"\nimport sys\nfrom pypdf import PdfReader\n\nimport pytest\n\n\nsys.modules[\"PIL\"] = None\nreader = PdfReader(\"{pdf_path_str}\", strict=True)\n\nfor page in reader.pages:\n    with pytest.raises(ImportError) as exc:\n        page.images[0]\n    assert exc.value.args[0] == (\n        \"pillow is required to do image extraction. \"\n        \"It can be installed via 'pip install pypdf[image]'\"\n    ), exc.value.args[0]\n\"\"\"\n    )\n\n    try:\n        env[\"PYTHONPATH\"] = \".\" + os.pathsep + env[\"PYTHONPATH\"]\n    except KeyError:\n        env[\"PYTHONPATH\"] = \".\"\n    result = subprocess.run(  # noqa: S603  # We have the control here.\n        [sys.executable, source_file],\n        capture_output=True,\n        env=env,\n    )\n    assert result.returncode == 0\n    assert result.stdout == b\"\"\n    assert (\n        result.stderr.replace(b\"\\r\", b\"\")\n        == b\"Superfluous whitespace found in object header b'4' b'0'\\n\"\n    )\n\n\n@pytest.mark.enable_socket\ndef test_issue_1737():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss1737.pdf\")))\n    reader.pages[0][\"/Resources\"][\"/XObject\"][\"/Im0\"].get_data()\n    reader.pages[0][\"/Resources\"][\"/XObject\"][\"/Im1\"].get_data()\n    reader.pages[0][\"/Resources\"][\"/XObject\"][\"/Im2\"].get_data()\n\n\n@pytest.mark.enable_socket\ndef test_pa_image_extraction():\n    \"\"\"\n    PNG images with PA mode can be extracted.\n\n    This is a regression test for issue #1801\n    \"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"issue-1801.pdf\")))\n\n    page0 = reader.pages[0]\n    images = page0.images\n    assert len(images) == 1\n    assert images[0].name == \"Im1.png\"\n\n    # Ensure visual appearance\n    expected_data = BytesIO(get_data_from_url(name=\"issue-1801.png\"))\n    assert image_similarity(expected_data, images[0].image) == 1\n\n\n@pytest.mark.enable_socket\ndef test_1bit_image_extraction():\n    \"\"\"Cf issue #1814\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"grimm10\")))\n    for p in reader.pages:\n        p.images\n\n\n@pytest.mark.enable_socket\ndef test_png_transparency_reverse():\n    \"\"\"Cf issue #1599\"\"\"\n    pdf_path = RESOURCE_ROOT / \"labeled-edges-center-image.pdf\"\n    reader = PdfReader(pdf_path)\n    refimg = Image.open(\n        BytesIO(get_data_from_url(name=\"labeled-edges-center-image.png\"))\n    )\n    data = reader.pages[0].images[0]\n    img = Image.open(BytesIO(data.data))\n    assert \".jp2\" in data.name\n    assert get_image_data(img) == get_image_data(refimg)\n\n\n@pytest.mark.enable_socket\ndef test_iss1787():\n    \"\"\"Cf issue #1787\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"pdf_font_garbled.pdf\")))\n    refimg = Image.open(BytesIO(get_data_from_url(name=\"watermark1.png\")))\n    data = reader.pages[0].images[0]\n    img = Image.open(BytesIO(data.data))\n    assert \".png\" in data.name\n    assert get_image_data(img) == get_image_data(refimg)\n    obj = data.indirect_reference.get_object()\n    obj[\"/DecodeParms\"][NameObject(\"/Columns\")] = NumberObject(1000)\n    obj.decoded_self = None\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^Unsupported PNG filter 244$\"):\n        _ = reader.pages[0].images[0]\n\n\n@pytest.mark.enable_socket\ndef test_tiff_predictor():\n    \"\"\"Decode Tiff Predictor 2 Images\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"tika-977609.pdf\")))\n    refimg = Image.open(BytesIO(get_data_from_url(name=\"tifimage.png\")))\n    data = reader.pages[0].images[0]\n    img = Image.open(BytesIO(data.data))\n    assert \".png\" in data.name\n    assert get_image_data(img) == get_image_data(refimg)\n\n\n@pytest.mark.enable_socket\ndef test_rgba():\n    \"\"\"Decode RGB with transparency\"\"\"\n    with PILContext():\n        reader = PdfReader(BytesIO(get_data_from_url(name=\"tika-972174.pdf\")))\n        data = reader.pages[0].images[0]\n        assert \".jp2\" in data.name\n        similarity = image_similarity(\n            data.image, BytesIO(get_data_from_url(name=\"tika-972174_p0-im0.png\"))\n        )\n        assert similarity > 0.99\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_cmyk():\n    \"\"\"Decode CMYK\"\"\"\n    # JPEG compression\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"Vitocal.pdf\")))\n    refimg = BytesIO(get_data_from_url(name=\"VitocalImage.png\"))\n    data = reader.pages[1].images[0]\n    assert data.image.mode == \"CMYK\"\n    assert \".jpg\" in data.name\n    assert image_similarity(data.image, refimg) > 0.99\n    # deflate\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"cmyk_deflate.pdf\")))\n    refimg = BytesIO(get_data_from_url(name=\"cmyk_deflate.tif\"))\n    data = reader.pages[0].images[0]\n    assert data.image.mode == \"CMYK\"\n    assert \".tif\" in data.name\n    assert image_similarity(data.image, refimg) > 0.999  # lossless compression expected\n\n\n@pytest.mark.enable_socket\ndef test_iss1863():\n    \"\"\"Test doc from iss1863\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"o1whh9b3.pdf\")))\n    for p in reader.pages:\n        for i in p.images:\n            i.name\n\n\n@pytest.mark.enable_socket\ndef test_read_images():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"selbst.72916.pdf\")))\n    page = reader.pages[0]\n    for _ in page.images:\n        pass\n\n\n@pytest.mark.enable_socket\ndef test_cascaded_filters_images():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss1912.pdf\")))\n    # for focus, analyse the page 23\n    for p in reader.pages:\n        for i in p.images:\n            _ = i.name, i.image\n\n\n@pytest.mark.enable_socket\ndef test_calrgb():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"calRGB.pdf\")))\n    reader.pages[0].images[0]\n\n\n@pytest.mark.enable_socket\ndef test_index_lookup():\n    \"\"\"The lookup is provided as an str and bytes\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"2023USDC.pdf\")))\n    # TextStringObject Lookup\n    refimg = BytesIO(get_data_from_url(name=\"iss1982_im1.png\"))\n    data = reader.pages[0].images[-1]\n    assert data.image.mode == \"RGB\"\n    assert image_similarity(data.image, refimg) > 0.999\n    # ByteStringObject Lookup\n    refimg = BytesIO(get_data_from_url(name=\"iss1982_im2.png\"))\n    data = reader.pages[-1].images[-1]\n    assert data.image.mode == \"RGB\"\n    assert image_similarity(data.image, refimg) > 0.999\n    # indexed CMYK images\n    # currently with a TODO as we convert the palette to RGB\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"tika-972174.pdf\")))\n    refimg = Image.open(BytesIO(get_data_from_url(name=\"usa.png\")))\n    data = reader.pages[0].images[\"/Im3\"]\n    # assert data.image.mode == \"PA\" but currently \"RGBA\"\n    assert image_similarity(data.image, refimg) > 0.999\n\n\n@pytest.mark.enable_socket\ndef test_2bits_image():\n    \"\"\"From #1954, test with 2bits image. TODO: 4bits also\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"paid.pdf\")))\n    url_png = \"https://user-images.githubusercontent.com/4083478/253568117-ca95cc85-9dea-4145-a5e0-032f1c1aa322.png\"\n    name_png = \"Paid.png\"\n    refimg = BytesIO(get_data_from_url(url_png, name=name_png))\n    data = reader.pages[0].images[0]\n    assert image_similarity(data.image, refimg) > 0.99\n\n\n@pytest.mark.enable_socket\ndef test_gray_devicen_cmyk():\n    \"\"\"\n    Cf #1979\n    Gray Image in CMYK : requiring reverse\n    \"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12080338/example_121.pdf\"\n    name = \"gray_cmyk.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url_png = \"https://user-images.githubusercontent.com/4083478/254545494-42df4949-1557-4f2d-acca-6be6e8de1122.png\"\n    name_png = \"velo.png\"\n    refimg = BytesIO(get_data_from_url(url_png, name=name_png))\n    data = reader.pages[0].images[0]\n    assert data.image.mode == \"L\"\n    assert image_similarity(data.image, refimg) > 0.999\n\n\n@pytest.mark.enable_socket\ndef test_runlengthdecode():\n    \"\"\"From #1954, test with 2bits image. TODO: 4bits also\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12159941/out.pdf\"\n    name = \"RunLengthDecode.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url_png = \"https://user-images.githubusercontent.com/4083478/255940800-6d63972e-a3d6-4cf9-aa6f-0793af24cded.png\"\n    name_png = \"RunLengthDecode.png\"\n    refimg = BytesIO(get_data_from_url(url_png, name=name_png))\n    data = reader.pages[0].images[0]\n    assert image_similarity(data.image, refimg) > 0.999\n    url = \"https://github.com/py-pdf/pypdf/files/12162905/out.pdf\"\n    name = \"FailedRLE1.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].images[0]\n    url = \"https://github.com/py-pdf/pypdf/files/12162926/out.pdf\"\n    name = \"FailedRLE2.pdf\"\n    reader.pages[0].images[0]\n\n\n@pytest.mark.enable_socket\ndef test_gray_separation_cmyk():\n    \"\"\"\n    Cf #1955\n    Gray Image in Separation/RGB : requiring reverse\n    \"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12143372/tt.pdf\"\n    name = \"TestWithSeparationBlack.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url_png = \"https://user-images.githubusercontent.com/4083478/254545494-42df4949-1557-4f2d-acca-6be6e8de1122.png\"\n    name_png = \"velo.png\"  # reused\n    refimg = BytesIO(get_data_from_url(url_png, name=name_png))\n    data = reader.pages[0].images[0]\n    assert data.image.mode == \"L\"\n    assert image_similarity(data.image, refimg) > 0.999\n\n\n@pytest.mark.enable_socket\ndef test_singleton_device():\n    \"\"\"From #2023\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12177287/tt.pdf\"\n    name = \"pypdf_with_arr_deviceRGB.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].images[0]\n\n\n@pytest.mark.enable_socket\ndef test_jpx_no_spacecode():\n    \"\"\"From #2061\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12253581/tt2.pdf\"\n    name = \"jpx_no_spacecode.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    im = reader.pages[0].images[0]\n    # create an object without filter and without colorspace\n    # just for coverage\n    del im.indirect_reference.get_object()[\"/Filter\"]\n    with pytest.raises(PdfReadError) as exc:\n        reader.pages[0].images[0]\n    assert exc.value.args[0].startswith(\"ColorSpace field not found\")\n\n\n@pytest.mark.enable_socket\ndef test_encodedstream_lookup():\n    \"\"\"From #2124\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12455580/10.pdf\"\n    name = \"iss2124.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[12].images[0]\n\n\n@pytest.mark.enable_socket\ndef test_convert_1_to_la():\n    \"\"\"From #2165\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12543290/whitepaper.WBT.token.blockchain.whitepaper.pdf\"\n    name = \"iss2165.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    for i in reader.pages[13].images:\n        _ = i\n\n\n@pytest.mark.enable_socket\ndef test_nested_device_n_color_space():\n    \"\"\"From #2240\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12814018/out1.pdf\"\n    name = \"issue2240.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].images[0]\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_flate_decode_with_image_mode_1():\n    \"\"\"From #2248\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12847339/Prototype-Declaration-VDE4110-HYD-5000-20000-ZSS-DE.pdf\"\n    name = \"issue2248.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    for image in reader.pages[7].images:\n        _ = image\n\n\n@pytest.mark.enable_socket\ndef test_flate_decode_with_image_mode_1__whitespace_at_end_of_lookup():\n    \"\"\"From #2331\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/13611048/out1.pdf\"\n    name = \"issue2331.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].images[0]\n\n\n@pytest.mark.enable_socket\ndef test_ascii85decode__invalid_end__recoverable(caplog):\n    \"\"\"From #2996\"\"\"\n    url = \"https://github.com/user-attachments/files/18050808/1af7d56a-5c8c-4914-85b3-b2536a5525cd.pdf\"\n    name = \"issue2996.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    page = reader.pages[1]\n    assert page.extract_text() == \"\"\n    assert \"Ignoring missing Ascii85 end marker.\" in caplog.text\n\n\ndef test_ascii85decode__non_recoverable(caplog):\n    # Without our custom handling, this would complain about the final `~>` being missing.\n    data = \"äöüß\"\n    with pytest.raises(ValueError, match=\"Non-Ascii85 digit found: Ã\"):\n        ASCII85Decode.decode(data)\n    assert \"Ignoring missing Ascii85 end marker.\" in caplog.text\n    caplog.clear()\n\n    data += \"~>\"\n    with pytest.raises(ValueError, match=\"Non-Ascii85 digit found: Ã\"):\n        ASCII85Decode.decode(data)\n    assert caplog.text == \"\"\n\n\ndef test_ascii85decode__ignore_whitespaces(caplog):\n    \"\"\"Whitespace characters must be silently ignored\"\"\"\n    data = b\"Cqa;:3k~\\n>\"\n    result = ASCII85Decode.decode(data)\n    assert result == b\"l\\xbe`\\x8d:\"\n\n\n@pytest.mark.enable_socket\ndef test_ccitt_fax_decode__black_is_1():\n    url = \"https://github.com/user-attachments/files/19288881/imagemagick-CCITTFaxDecode_BlackIs1-true.pdf\"\n    name = \"issue3193.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    other_reader = PdfReader(RESOURCE_ROOT / \"imagemagick-CCITTFaxDecode.pdf\")\n\n    actual_image = reader.pages[0].images[0].image\n    expected_image_inverted = other_reader.pages[0].images[0].image\n    expected_pixels = get_image_data(ImageOps.invert(expected_image_inverted))\n    actual_pixels = get_image_data(actual_image)\n    assert expected_pixels == actual_pixels\n\n    # AttributeError: 'NullObject' object has no attribute 'get'\n    data_modified = get_data_from_url(url, name=name).replace(\n        b\"/DecodeParms [ << /K -1 /BlackIs1 true /Columns 16 /Rows 16 >> ]\",\n        b\"/DecodeParms [ null ]\"\n    )\n    reader = PdfReader(BytesIO(data_modified))\n    _ = reader.pages[0].images[0].image\n\n\n@pytest.mark.enable_socket\ndef test_flate_decode__image_is_none_due_to_size_limit(caplog):\n    url = \"https://github.com/user-attachments/files/19464256/file.pdf\"\n    name = \"issue3220.pdf\"\n\n    with mock.patch(\"pypdf.filters.ZLIB_MAX_OUTPUT_LENGTH\", 0):\n        reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n        images = reader.pages[0].images\n        assert len(images) == 1\n        image = images[0]\n        assert image.name == \"Im0.png\"\n        assert image.image is None\n\n    assert (\n        \"Failed loading image: Image size (180000000 pixels) exceeds limit of \"\n        \"178956970 pixels, could be decompression bomb DOS attack.\"\n    ) in caplog.messages\n\n\n@pytest.mark.enable_socket\ndef test_flate_decode__not_rectangular(caplog):\n    url = \"https://github.com/user-attachments/files/19663603/issue3241_compressed.txt\"\n    name = \"issue3241.txt\"\n    data = get_data_from_url(url, name=name)\n    decode_parms = DictionaryObject()\n    decode_parms[NameObject(\"/Predictor\")] = NumberObject(15)\n    decode_parms[NameObject(\"/Columns\")] = NumberObject(4881)\n    actual = FlateDecode.decode(data=data, decode_parms=decode_parms)\n    actual_image = Image.frombytes(mode=\"1\", size=(4881, 81), data=actual)\n\n    url = \"https://github.com/user-attachments/assets/c5695850-c076-4255-ab72-7c86851a4a04\"\n    name = \"issue3241.png\"\n    expected_data = BytesIO(get_data_from_url(url, name=name))\n    assert image_similarity(expected_data, actual_image) == 1\n    assert caplog.messages == [\"Image data is not rectangular. Adding padding.\"]\n\n\ndef test_jbig2decode__binary_errors():\n    with mock.patch(\"pypdf.filters.JBIG2DEC_BINARY\", None), \\\n            pytest.raises(DependencyError, match=r\"jbig2dec binary is not available\\.\"):\n        JBIG2Decode.decode(b\"dummy\")\n\n    result = subprocess.CompletedProcess(\n        args=[\"dummy\"], returncode=0, stdout=b\"\",\n        stderr=(\n            b\"jbig2dec: unrecognized option '--embedded'\\n\"\n            b\"Usage: jbig2dec [options] <file.jbig2>\\n\"\n            b\"   or  jbig2dec [options] <global_stream> <page_stream>\\n\"\n        )\n    )\n    with mock.patch(\"pypdf.filters.subprocess.run\", return_value=result), \\\n            mock.patch(\"pypdf.filters.JBIG2DEC_BINARY\", \"/usr/bin/jbig2dec\"), \\\n            pytest.raises(DependencyError, match=r\"jbig2dec>=0.19 is required\\.\"):\n        JBIG2Decode.decode(b\"dummy\")\n\n    result = subprocess.CompletedProcess(\n        args=[\"dummy\"], returncode=0, stdout=b\"\",\n        stderr=(\n            b\"jbig2dec: unrecognized option '-M'\\n\"\n            b\"Usage: jbig2dec [options] <file.jbig2>\\n\"\n            b\"   or  jbig2dec [options] <global_stream> <page_stream>\\n\"\n        )\n    )\n    with mock.patch(\"pypdf.filters.subprocess.run\", return_value=result), \\\n            mock.patch(\"pypdf.filters.JBIG2DEC_BINARY\", \"/usr/bin/jbig2dec\"), \\\n            pytest.raises(DependencyError, match=r\"jbig2dec>=0.19 is required\\.\"):\n        JBIG2Decode.decode(b\"dummy\")\n\n\n@pytest.mark.skipif(condition=not JBIG2Decode._is_binary_compatible(), reason=\"Requires recent jbig2dec\")\ndef test_jbig2decode__edge_cases(caplog):\n    image_data = (\n        b'\\x00\\x00\\x00\\x010\\x00\\x01\\x00\\x00\\x00\\x13\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x02\\x06\"'\n        b'\\x00\\x01\\x00\\x00\\x00\\x1c\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x05\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x9f\\xa8_\\xff\\xac'\n\n    )\n    jbig2_globals = b\"\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x00\\x18\\x00\\x00\\x03\\xff\\xfd\\xff\\x02\\xfe\\xfe\\xfe\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x01R\\xd0u7\\xff\\xac\"  # noqa: E501\n\n    # Validation: Is our image data valid?\n    content_stream = ContentStream(stream=None, pdf=None)\n    content_stream.set_data(jbig2_globals)\n    result = JBIG2Decode.decode(image_data, decode_parms=DictionaryObject({\"/JBIG2Globals\": content_stream}))\n    image = Image.open(BytesIO(result), formats=(\"PNG\", \"PPM\"))\n    for x in range(5):\n        for y in range(5):\n            assert image.getpixel((x, y)) == (255 if x < 3 else 0), (x, y)\n    assert caplog.messages == []\n\n    # No decode_params. Completely white image.\n    result = JBIG2Decode.decode(image_data)\n    image = Image.open(BytesIO(result), formats=(\"PNG\", \"PPM\"))\n    for x in range(5):\n        for y in range(5):\n            assert image.getpixel((x, y)) == 255, (x, y)\n    assert caplog.messages == [\n        \"jbig2dec WARNING text region refers to no symbol dictionaries (segment 0x00000002)\",\n        \"jbig2dec WARNING ignoring out of range symbol ID (0/0) (segment 0x00000002)\"\n    ]\n    caplog.clear()\n\n    # JBIG2Globals is NULL. Completely white image.\n    result = JBIG2Decode.decode(image_data, decode_parms=DictionaryObject({\"/JBIG2Globals\": NullObject()}))\n    image = Image.open(BytesIO(result), formats=(\"PNG\", \"PPM\"))\n    for x in range(5):\n        for y in range(5):\n            assert image.getpixel((x, y)) == 255, (x, y)\n    assert caplog.messages == [\n        \"jbig2dec WARNING text region refers to no symbol dictionaries (segment 0x00000002)\",\n        \"jbig2dec WARNING ignoring out of range symbol ID (0/0) (segment 0x00000002)\"\n    ]\n    caplog.clear()\n\n    # JBIG2Globals is DictionaryObject. Completely white image.\n    result = JBIG2Decode.decode(image_data, decode_parms=DictionaryObject({\"/JBIG2Globals\": DictionaryObject()}))\n    image = Image.open(BytesIO(result), formats=(\"PNG\", \"PPM\"))\n    for x in range(5):\n        for y in range(5):\n            assert image.getpixel((x, y)) == 255, (x, y)\n    assert caplog.messages == [\n        \"jbig2dec WARNING text region refers to no symbol dictionaries (segment 0x00000002)\",\n        \"jbig2dec WARNING ignoring out of range symbol ID (0/0) (segment 0x00000002)\"\n    ]\n    caplog.clear()\n\n    # Invalid input.\n    with pytest.raises(PdfStreamError, match=r\"Unable to decode JBIG2 data\\. Exit code: 1\"):\n        JBIG2Decode.decode(b\"aaaaaa\")\n    assert caplog.messages == [\n        \"jbig2dec FATAL ERROR page has no image, cannot be completed\",\n        \"jbig2dec WARNING unable to complete page\"\n    ]\n\n\n@pytest.mark.timeout(timeout=30, method=\"thread\")\n@pytest.mark.enable_socket\ndef test_flate_decode_stream_with_faulty_tail_bytes():\n    \"\"\"\n    Test for #3332\n\n    The test ensures two things:\n        1. stream can be decoded at all\n        2. decoding doesn't falls through to last fallback in try-except blocks\n           that is too slow and takes ages for this stream\n    \"\"\"\n    data = get_data_from_url(\n        url=\"https://github.com/user-attachments/files/20901522/faulty_stream_tail_example.1.pdf\",\n        name=\"faulty_stream_tail_example.1.pdf\"\n    )\n    expected = get_data_from_url(\n        url=\"https://github.com/user-attachments/files/20941717/decoded.dat.txt\",\n        name=\"faulty_stream_tail_example.1.decoded.dat\"\n    )\n    reader = PdfReader(BytesIO(data))\n    obj = reader.get_object(IndirectObject(182, 0, reader))\n    assert cast(StreamObject, obj).get_data() == expected\n\n\n@pytest.mark.enable_socket\ndef test_rle_decode_with_faulty_tail_byte_in_multi_encoded_stream(caplog):\n    \"\"\"\n    Test for #3355\n\n    The test ensures that the inner RLE encoded stream can be decoded,\n    because this stream contains an extra faulty newline byte in the\n    end that can be ignored during decoding.\n    \"\"\"\n    data = get_data_from_url(\n        url=\"https://github.com/user-attachments/files/21038398/test_data_rle.txt\",\n        name=\"multi_decoding_example_with_faulty_tail_byte.pdf\"\n    )\n    reader = PdfReader(BytesIO(data))\n    obj = reader.get_object(IndirectObject(60, 0, reader))\n    cast(StreamObject, obj).get_data()\n    assert \"Found trailing newline in stream data, check if output is OK\" in caplog.messages\n\n\n@pytest.mark.enable_socket\ndef test_rle_decode_exception_with_corrupted_stream(caplog):\n    \"\"\"\n    Additional Test to #3355\n\n    This test must report the EOD warning during RLE decoding and ensures\n    that we do not fail during code coverage analyses in the git PR pipeline.\n    \"\"\"\n    data = get_data_from_url(\n        url=\"https://github.com/user-attachments/files/21052626/rle_stream_with_error.txt\",\n        name=\"rle_stream_with_error.txt\"\n    )\n    decoded = RunLengthDecode.decode(data)\n    assert decoded.startswith(b\"\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x02\\x02\\x02\\x02\\x02\\x02\\x02\\x03\\x03\")\n    assert decoded.endswith(b\"\\x87\\x83\\x83\\x83\\x83\\x83\\x83\\x83]]]]]]]RRRRRRRX\\xa5\")\n    assert len(decoded) == 1048576\n    assert caplog.messages == [\"Early EOD in RunLengthDecode, check if output is OK\"]\n\n\ndef test_decompress():\n    data = string.printable.encode(\"utf-8\") + string.printable[::-1].encode(\"utf-8\")\n    compressed = FlateDecode.encode(data)\n\n    # Decompress regularly.\n    decompressed = decompress(compressed)\n    assert decompressed == data\n\n    # Decompress byte-wise.\n    with mock.patch(\"pypdf.filters._decompress_with_limit\", side_effect=zlib.error):\n        decompressed = decompress(compressed)\n        assert decompressed == data\n\n    # Decompress byte-wise with very low output limit.\n    with mock.patch(\"pypdf.filters._decompress_with_limit\", side_effect=zlib.error), \\\n            mock.patch(\"pypdf.filters.ZLIB_MAX_OUTPUT_LENGTH\", len(compressed) - 13), \\\n            pytest.raises(\n                LimitReachedError, match=r\"^Limit reached while decompressing\\. 12 bytes remaining\\.$\"\n            ):\n        decompress(compressed)\n\n    # Decompress byte-wise with input limit.\n    with mock.patch(\"pypdf.filters.ZLIB_MAX_RECOVERY_INPUT_LENGTH\", 1000), \\\n            pytest.raises(\n                LimitReachedError, match=r\"^Recovery limit reached while decompressing\\. 336 bytes remaining\\.$\"\n            ):\n        decompress(b\"A\" * 1337)\n\n\ndef test_decompress__logging_on_invalid_data(caplog):\n    \"\"\"We do not like suddenly getting empty outputs for non-empty inputs without a warning.\"\"\"\n    codec = FlateDecode()\n    encoded = codec.encode(b\"My test string\")\n    assert len(encoded) > 5\n    assert codec.decode(encoded[5:]) == b\"\"\n    assert caplog.messages == [\"Error -3 while decompressing data: incorrect header check\"]\n\n\ndef test_ccittfaxdecode__ccf_inline():\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"jpeg.pdf\")\n    page = writer.pages[0]\n    writer.remove_images()\n\n    image_data = (\n        b\"\\nBI\\n  /W 16\\n  /H 16\\n  /CS /G\\n  /BPC 1\\n  /F [/CCF]\\n\"\n        b\"  /DP [ << /K -1 /BlackIs1 false /Columns 16 /Rows 16 >> ]\\nID\\n\"\n        b\"&\\xa0\\xbf\\xcc9\\x14|G#\\x1f\\xff\\xf1\\xcc9\\x18\\xfe\\xbbX\\xfc\\x00@\\x04\"\n        b\"\\nEI\\n\"\n    )\n    content_stream = page.get_contents()\n    content_stream.set_data(\n        content_stream.get_data().replace(b\"/Im4 Do\", b\"\").replace(b\"\\nET\", image_data)\n    )\n    page.replace_contents(content_stream)\n\n    expected = PdfReader(RESOURCE_ROOT / \"imagemagick-CCITTFaxDecode.pdf\").pages[0].images[0].image\n    assert get_image_data(expected) == get_image_data(page.images[0].image)\n\n\ndef test_dctdecode__dct_inline():\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"jpeg.pdf\")\n    page = writer.pages[0]\n    writer.remove_images()\n\n    image_data = (\n        b\"\\nBI\\n  /W 16\\n  /H 16\\n  /CS /G\\n  /BPC 8\\n  /F [/DCT]\\nID\\n\"\n        b\"\\xff\\xd8\\xff\\xe0\\x00\\x10JFIF\\x00\\x01\\x01\\x01\\x01,\\x01,\\x00\\x00\\xff\\xfe\\x00\\x13Created with GIMP\\xff\\xe2\"\n        b\"\\x02\\xb0ICC_PROFILE\\x00\\x01\\x01\\x00\\x00\\x02\\xa0lcms\\x040\\x00\\x00mntrRGB XYZ \\x07\\xe6\\x00\\x04\\x00\\x0f\\x00\"\n        b\"\\t\\x00\\x1d\\x007acspAPPL\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\xd6\\x00\\x01\\x00\\x00\\x00\\x00\\xd3-lcms\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\rdesc\\x00\\x00\\x01 \\x00\\x00\\x00@cprt\\x00\\x00\\x01`\"\n        b\"\\x00\\x00\\x006wtpt\\x00\\x00\\x01\\x98\\x00\\x00\\x00\\x14chad\\x00\\x00\\x01\\xac\\x00\\x00\\x00,rXYZ\\x00\\x00\\x01\\xd8\"\n        b\"\\x00\\x00\\x00\\x14bXYZ\\x00\\x00\\x01\\xec\\x00\\x00\\x00\\x14gXYZ\\x00\\x00\\x02\\x00\\x00\\x00\\x00\\x14rTRC\\x00\\x00\"\n        b\"\\x02\\x14\\x00\\x00\\x00 gTRC\\x00\\x00\\x02\\x14\\x00\\x00\\x00 bTRC\\x00\\x00\\x02\\x14\\x00\\x00\\x00 chrm\\x00\\x00\"\n        b\"\\x024\\x00\\x00\\x00$dmnd\\x00\\x00\\x02X\\x00\\x00\\x00$dmdd\\x00\\x00\\x02|\\x00\\x00\\x00$mluc\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x00\\x01\\x00\\x00\\x00\\x0cenUS\\x00\\x00\\x00$\\x00\\x00\\x00\\x1c\\x00G\\x00I\\x00M\\x00P\\x00 \\x00b\\x00\"\n        b\"u\\x00i\\x00l\\x00t\\x00-\\x00i\\x00n\\x00 \\x00s\\x00R\\x00G\\x00Bmluc\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\"\n        b\"\\x00\\x0cenUS\\x00\\x00\\x00\\x1a\\x00\\x00\\x00\\x1c\\x00P\\x00u\\x00b\\x00l\\x00i\\x00c\\x00 \\x00D\\x00o\\x00m\\x00a\"\n        b\"\\x00i\\x00n\\x00\\x00XYZ \\x00\\x00\\x00\\x00\\x00\\x00\\xf6\\xd6\\x00\\x01\\x00\\x00\\x00\\x00\\xd3-sf32\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x01\\x0cB\\x00\\x00\\x05\\xde\\xff\\xff\\xf3%\\x00\\x00\\x07\\x93\\x00\\x00\\xfd\\x90\\xff\\xff\\xfb\\xa1\\xff\"\n        b\"\\xff\\xfd\\xa2\\x00\\x00\\x03\\xdc\\x00\\x00\\xc0nXYZ \\x00\\x00\\x00\\x00\\x00\\x00o\\xa0\\x00\\x008\\xf5\\x00\\x00\\x03\"\n        b\"\\x90XYZ \\x00\\x00\\x00\\x00\\x00\\x00$\\x9f\\x00\\x00\\x0f\\x84\\x00\\x00\\xb6\\xc4XYZ \\x00\\x00\\x00\\x00\\x00\\x00b\"\n        b\"\\x97\\x00\\x00\\xb7\\x87\\x00\\x00\\x18\\xd9para\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x02ff\\x00\\x00\\xf2\\xa7\"\n        b\"\\x00\\x00\\rY\\x00\\x00\\x13\\xd0\\x00\\x00\\n[chrm\\x00\\x00\\x00\\x00\\x00\\x03\\x00\\x00\\x00\\x00\\xa3\\xd7\\x00\\x00T|\"\n        b\"\\x00\\x00L\\xcd\\x00\\x00\\x99\\x9a\\x00\\x00&g\\x00\\x00\\x0f\\\\mluc\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x01\\x00\\x00\"\n        b\"\\x00\\x0cenUS\\x00\\x00\\x00\\x08\\x00\\x00\\x00\\x1c\\x00G\\x00I\\x00M\\x00Pmluc\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x01\\x00\\x00\\x00\\x0cenUS\\x00\\x00\\x00\\x08\\x00\\x00\\x00\\x1c\\x00s\\x00R\\x00G\\x00B\\xff\\xdb\\x00C\\x00\\x01\"\n        b\"\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\"\n        b\"\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\"\n        b\"\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\x01\\xff\\xc0\\x00\\x0b\\x08\\x00\\x10\\x00\\x10\"\n        b\"\\x01\\x01\\x11\\x00\\xff\\xc4\\x00\\x17\\x00\\x00\\x03\\x01\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x06\\x07\\x08\\n\\xff\\xc4\\x00\\x1d\\x10\\x00\\x03\\x00\\x03\\x01\\x01\\x01\\x01\\x00\\x00\\x00\\x00\\x00\\x00\"\n        b\"\\x00\\x00\\x05\\x06\\x07\\x01\\x04\\x08\\x02\\x03\\x13\\x15\\xff\\xda\\x00\\x08\\x01\\x01\\x00\\x00?\\x00\\xc4D\\x0eA\"\n        b\"\\x8e\\x91\\xa8\\xf3\\xcf5N\\xb5\\x7f\\x87k\\xbc_\\x96\\xe3\\x83]\\x9c\\\\\\xff\\x00\\x19f1^=:A\\x98jm.\\x03\\x9f\\x10\"\n        b\"mW\\xc2\\xcbYF\\xd2T\\x06\\xef,OXfX`^\\x18\\x0ez\\xb4U \\x91\\x17\\xd4\\xf6\\xbe\\xc2\\xb7\\x85s:{\\xa1\\x8f\\xec;}\"\n        b\"\\x8f-l/1|\\x19\\x86|\\x14\\xc5+j\\x8cm\\xf0\\xde\\x10\\xba\\x7f\\xa5=\\xe2\\x86\\xd8\\x18\\r\\xed$o\\xab2h\\xbc\\xad\"\n        b\"\\x8cS\\x18\\xba\\xd8,\\xb2\\xa3\\xbf\\xd9\\xd8I\\x84+\\x07\\x9d\\x1ay\\x1cr\\xba\\x81\\nu\\x0f\\xa7yk\\xa0%5\\xf2\\xf4\"\n        b\"\\xf4\\x9e\\x8d\\xe6\\x19\\x90+s;P\\xfd\\xd1\\xb3\\x8f\\xac\\xf8\\x0e@5\\xf5\\x8f(i\\xc3\\x0e\\xf3\\xd3\\xbc\\xf5\\xa5\"\n        b\"\\xed:\\x85<$\\xee\\xd1@%i\\xde\\x1ao\\xdaF$\\t?Vq\\xce\\x92\\xde\\xe1\\xbd\\x14H\\x8a'\\\"\\x8d\\xbf75\\xaef\\x90\\xc3|\"\n        b\"\\xe8~\\x82\\x04\\xab+3O.\\xdeX&\\xac\\xf2t\\x89\\xcf\\xd3\\xfa\\x85\\xbdFu=\\x8e*\\xa9\\xfb!\\x96\\xed\\xfa\\xe3S\\xe5A\"\n        b\"\\xf2\\xa8\\xf5\\xe8\\xd7\\x85\\xa5\\x05\\t\\xf8a\\xff\\x00\\xff\\xd9\"\n        b\"\\nEI\\n\"\n    )\n    content_stream = page.get_contents()\n    content_stream.set_data(\n        content_stream.get_data().replace(b\"/Im4 Do\", b\"\").replace(b\"\\nET\", image_data)\n    )\n    page.replace_contents(content_stream)\n\n    expected = PdfReader(RESOURCE_ROOT / \"imagemagick-images.pdf\").pages[3].images[0].image\n    assert get_image_data(expected) == get_image_data(page.images[0].image)\n\n\ndef test_deprecate_inline_image_filters():\n    stream = ContentStream(stream=None, pdf=None)\n    stream.set_data(b\"&\\xa0\\xbf\\xcc9\\x14|G#\\x1f\\xff\\xf1\\xcc9\\x18\\xfe\\xbbX\\xfc\\x00@\\x04\")\n\n    # The abbreviations do not work here, which is one of the reasons for the deprecation.\n    stream[NameObject(\"/Width\")] = NumberObject(16)\n    stream[NameObject(\"/Height\")] = NumberObject(16)\n    stream[NameObject(\"/ColorSpace\")] = NameObject(\"/DeviceGray\")\n    stream[NameObject(\"/BitsPerComponent\")] = NumberObject(1)\n    stream[NameObject(\"/Filter\")] = NameObject(\"/CCF\")\n    stream[NameObject(\"/DecodeParams\")] = ArrayObject(\n        [\n            DictionaryObject(\n                {\n                    NameObject(\"/K\"): NumberObject(-1),\n                    NameObject(\"/BlackIs1\"): TextStringObject(\"false\"),\n                    NameObject(\"/Columns\"): NumberObject(16),\n                    NameObject(\"/Rows\"): NumberObject(16),\n                }\n            )\n        ]\n    )\n\n    with pytest.warns(\n            expected_warning=DeprecationWarning,\n            match=r\"^The filter name /CCF is deprecated and will be removed in pypdf 7\\.0\\.0\\. Use /CCITTFaxDecode instead\\.$\"  # noqa: E501\n    ):\n        decode_stream_data(stream)\n\n    stream[NameObject(\"/Filter\")] = NameObject(\"/CCITTFaxDecode\")\n    assert decode_stream_data(stream).startswith(b\"II*\")\n\n\ndef test_flatedecode__columns_is_zero():\n    codec = FlateDecode()\n    data = b\"Hello World!\"\n    parameters = DictionaryObject({\n        NameObject(\"/Predictor\"): NumberObject(13),\n        NameObject(\"/Columns\"): NumberObject(0)\n    })\n\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^Expected positive number for /Columns, got 0!$\"):\n        codec.decode(codec.encode(data), parameters)\n\n\ndef test_runlengthdecode__decode_limit():\n    uncompressed_size = 76 * 1024 * 1024  # 76 MB target\n    runs = uncompressed_size // 128\n    encoded = (b\"\\x81A\" * runs) + b\"\\x80\"\n\n    with pytest.raises(expected_exception=LimitReachedError, match=r\"^Limit reached while decompressing\\.$\"):\n        RunLengthDecode.decode(encoded)\n\n    uncompressed_size = 5 * 1024\n    runs = uncompressed_size // 128\n    encoded = (b\"\\x81A\" * runs) + b\"\\x80\"\n\n    # Use a very low limit for this exact comparison, otherwise *pytest* takes ages to render a failure diff.\n    with mock.patch(\"pypdf.filters.RUN_LENGTH_MAX_OUTPUT_LENGTH\", uncompressed_size):\n        assert RunLengthDecode.decode(encoded) == b\"A\" * uncompressed_size\n\n\n@pytest.mark.timeout(10)\ndef test_asciihexdecode__speed():\n    encoded = (b\"41\" * 1_200_000) + b\">\"\n    ASCIIHexDecode.decode(encoded)\n"
  },
  {
    "path": "tests/test_font.py",
    "content": "\"\"\"Test font-related functionality.\"\"\"\n\nfrom pypdf._font import Font\nfrom pypdf.generic import DictionaryObject, NameObject\n\n\ndef test_font_descriptor():\n    font_res = DictionaryObject({\n        NameObject(\"/BaseFont\"): NameObject(\"/Helvetica\"),\n        NameObject(\"/Subtype\"): NameObject(\"/Type1\")\n    })\n    my_font = Font.from_font_resource(font_res)\n    assert my_font.font_descriptor.family == \"Helvetica\"\n    assert my_font.font_descriptor.weight == \"Medium\"\n    assert my_font.font_descriptor.ascent == 718\n    assert my_font.font_descriptor.descent == -207\n\n    test_string = \"This is a long sentence. !@%%^€€€. çûįö¶´\"\n    charwidth = my_font.text_width(test_string)\n    assert charwidth == 19251\n\n    font_res[NameObject(\"/BaseFont\")] = NameObject(\"/Palatino\")\n    my_font = Font.from_font_resource(font_res)\n    assert my_font.font_descriptor.weight == \"Unknown\"\n\n    font_res[NameObject(\"/BaseFont\")] = NameObject(\"/Courier-Bold\")\n    my_font = Font.from_font_resource(font_res)\n    assert my_font.font_descriptor.italic_angle == 0\n    assert my_font.font_descriptor.flags == 33\n    assert my_font.font_descriptor.bbox == (-113.0, -250.0, 749.0, 801.0)\n"
  },
  {
    "path": "tests/test_forms.py",
    "content": "\"\"\"Test form-related functionality. Separate file to keep overview.\"\"\"\n\nfrom io import BytesIO\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom tests import get_data_from_url\n\n\n@pytest.mark.enable_socket\ndef test_form_button__v_value_should_be_name_object():\n    url = \"https://github.com/user-attachments/files/18736500/blank-form.pdf\"\n    name = \"issue3115.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter(clone_from=reader)\n    writer.update_page_form_field_values(\n        writer.pages[0],\n        {\"Other\": \"/On\"},\n        auto_regenerate=False,\n    )\n    stream = BytesIO()\n    writer.write(stream)\n\n    # Wrong: `/V (/On)`.\n    assert b\"\\n/V /On\\n\" in stream.getvalue()\n"
  },
  {
    "path": "tests/test_generic.py",
    "content": "\"\"\"Test the pypdf.generic module.\"\"\"\n\nimport codecs\nimport gc\nimport weakref\nfrom base64 import a85encode\nfrom copy import deepcopy\nfrom io import BytesIO\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.constants import CheckboxRadioButtonAttributes\nfrom pypdf.errors import DeprecationError, PdfReadError, PdfStreamError\nfrom pypdf.generic import (\n    ArrayObject,\n    BooleanObject,\n    ByteStringObject,\n    ContentStream,\n    DecodedStreamObject,\n    Destination,\n    DictionaryObject,\n    Fit,\n    FloatObject,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    OutlineItem,\n    PdfObject,\n    RectangleObject,\n    StreamObject,\n    TextStringObject,\n    TreeObject,\n    create_string_object,\n    encode_pdfdocencoding,\n    is_null_or_none,\n    read_hex_string_from_stream,\n    read_object,\n    read_string_from_stream,\n)\nfrom pypdf.generic._image_inline import (\n    extract_inline__ascii85_decode,\n    extract_inline__ascii_hex_decode,\n    extract_inline__dct_decode,\n    extract_inline__run_length_decode,\n)\n\nfrom . import RESOURCE_ROOT, get_data_from_url\nfrom .utils import ReaderDummy\n\n\nclass ChildDummy(DictionaryObject):\n    @property\n    def indirect_reference(self):\n        return self\n\n\ndef test_float_object_exception(caplog):\n    assert FloatObject(\"abc\") == 0\n    assert caplog.text != \"\"\n\n\ndef test_number_object_exception(caplog):\n    assert NumberObject(\"0,0\") == 0\n    assert caplog.text != \"\"\n\n\ndef test_number_object_no_exception():\n    NumberObject(2**100_000_000)\n\n\ndef test_create_string_object_exception():\n    with pytest.raises(TypeError) as exc:\n        create_string_object(123)\n    assert (  # typeguard is not running\n        exc.value.args[0] == \"create_string_object should have str or unicode arg\"\n    ) or (  # typeguard is enabled\n        'type of argument \"string\" must be one of (str, bytes); got int instead'\n        in exc.value.args[0]\n    )\n\n\n@pytest.mark.parametrize(\n    (\"value\", \"expected\", \"tell\"), [(b\"true\", b\"true\", 4), (b\"false\", b\"false\", 5)]\n)\ndef test_boolean_object(value, expected, tell):\n    stream = BytesIO(value)\n    assert BooleanObject.read_from_stream(stream).value == (expected == b\"true\")\n    stream.seek(0, 0)\n    assert stream.read() == expected\n    assert stream.tell() == tell\n\n\ndef test_boolean_object_write():\n    stream = BytesIO()\n    boolobj = BooleanObject(None)\n    boolobj.write_to_stream(stream)\n    stream.seek(0, 0)\n    assert stream.read() == b\"false\"\n\n\ndef test_boolean_eq():\n    boolobj = BooleanObject(True)\n    assert (boolobj == True) is True  # noqa: E712\n    assert (boolobj == False) is False  # noqa: E712\n    assert (boolobj == \"True\") is False\n    hash1 = hash(boolobj)\n    assert hash1 == hash(boolobj)\n\n    boolobj = BooleanObject(False)\n    assert (boolobj == True) is False  # noqa: E712\n    assert (boolobj == False) is True  # noqa: E712\n    assert (boolobj == \"True\") is False\n    assert hash1 != hash(boolobj)\n\n\ndef test_boolean_object_exception():\n    stream = BytesIO(b\"False\")\n    with pytest.raises(PdfReadError) as exc:\n        BooleanObject.read_from_stream(stream)\n    assert exc.value.args[0] == \"Could not read Boolean object\"\n\n\ndef test_array_object_exception():\n    stream = BytesIO(b\"False\")\n    with pytest.raises(PdfReadError) as exc:\n        ArrayObject.read_from_stream(stream, None)\n    assert exc.value.args[0] == \"Could not read array\"\n\n\ndef test_null_object_exception():\n    stream = BytesIO(b\"notnull\")\n    with pytest.raises(PdfReadError) as exc:\n        NullObject.read_from_stream(stream)\n    assert exc.value.args[0] == \"Could not read Null object\"\n\n\n@pytest.mark.parametrize(\"value\", [b\"\", b\"False\", b\"foo \", b\"foo  \", b\"foo bar\"])\ndef test_indirect_object_premature(value):\n    stream = BytesIO(value)\n    with pytest.raises(PdfStreamError) as exc:\n        IndirectObject.read_from_stream(stream, None)\n    assert exc.value.args[0] == \"Stream has ended unexpectedly\"\n\n\ndef test_read_hex_string_from_stream():\n    stream = BytesIO(b\"a1>\")\n    assert read_hex_string_from_stream(stream) == \"\\x10\"\n\n\ndef test_read_hex_string_from_stream_exception():\n    stream = BytesIO(b\"\")\n    with pytest.raises(PdfStreamError) as exc:\n        read_hex_string_from_stream(stream)\n    assert exc.value.args[0] == \"Stream has ended unexpectedly\"\n\n\ndef test_read_string_from_stream_exception():\n    stream = BytesIO(b\"x\")\n    with pytest.raises(PdfStreamError) as exc:\n        read_string_from_stream(stream)\n    assert exc.value.args[0] == \"Stream has ended unexpectedly\"\n\n\ndef test_read_string_from_stream_not_in_escapedict_no_digit():\n    stream = BytesIO(b\"x\\\\y\")\n    with pytest.raises(PdfReadError) as exc:\n        read_string_from_stream(stream)\n    assert exc.value.args[0] == \"Stream has ended unexpectedly\"\n\n\ndef test_read_string_from_stream_multichar_eol():\n    stream = BytesIO(b\"x\\\\\\n )\")\n    assert read_string_from_stream(stream) == \" \"\n\n\ndef test_read_string_from_stream_multichar_eol2():\n    stream = BytesIO(b\"x\\\\\\n\\n)\")\n    assert read_string_from_stream(stream) == \"\"\n\n\ndef test_read_string_from_stream_excape_digit():\n    stream = BytesIO(b\"x\\\\1a )\")\n    assert read_string_from_stream(stream) == \"\\x01a \"\n\n\ndef test_read_string_from_stream_excape_digit2():\n    stream = BytesIO(b\"(hello \\\\1\\\\2\\\\3\\\\4)\")\n    assert read_string_from_stream(stream) == \"hello \\x01\\x02\\x03\\x04\"\n\n\ndef test_name_object(caplog):\n    stream = BytesIO(b\"x\")\n    with pytest.raises(PdfReadError) as exc:\n        NameObject.read_from_stream(stream, None)\n    assert exc.value.args[0] == \"Name read error\"\n\n    with pytest.raises(\n        DeprecationError,\n        match=r\"surfix is deprecated and was removed in pypdf 5\\.0\\.0\\. Use prefix instead\\.\",\n    ):\n        _ = NameObject.surfix\n\n    assert (\n        NameObject.read_from_stream(\n            BytesIO(b\"/A;Name_With-Various***Characters?\"), None\n        )\n        == \"/A;Name_With-Various***Characters?\"\n    )\n\n    assert (\n        NameObject.read_from_stream(BytesIO(b\"/paired#28#29parentheses\"), None)\n        == \"/paired()parentheses\"\n    )\n\n    assert NameObject.read_from_stream(BytesIO(b\"/A#42\"), None) == \"/AB\"\n\n    assert (\n        NameObject.read_from_stream(\n            BytesIO(b\"/#f1j#d4#aa#0c#ce#87#b4#b3#b0#23J#86#fe#2a#b2jYJ#94\"),\n            ReaderDummy(),\n        )\n        == \"/ñjÔª\\x0cÎ\\x87´³°#J\\x86þ*²jYJ\\x94\"\n    )\n\n    assert (NameObject.read_from_stream(BytesIO(b\"/#JA#231f\"), None)) == \"/#JA#1f\"\n\n    assert (\n        NameObject.read_from_stream(\n            BytesIO(b\"/#e4#bd#a0#e5#a5#bd#e4#b8#96#e7#95#8c\"), None\n        )\n    ) == \"/你好世界\"\n\n    # test PDFDocEncoding (latin-1)\n    assert (\n        NameObject.read_from_stream(BytesIO(b\"/DocuSign\\xae\"), None)\n    ) == \"/DocuSign®\"\n\n    # test write\n    b = BytesIO()\n    NameObject(\"/hello\").write_to_stream(b)\n    assert bytes(b.getbuffer()) == b\"/hello\"\n\n    caplog.clear()\n    b = BytesIO()\n    with pytest.raises(\n            expected_exception=DeprecationError,\n            match=r\"Incorrect first char in NameObject, should start with '/': \\(hello\\) is deprecated and was\"\n    ):\n        NameObject(\"hello\").write_to_stream(b)\n\n    caplog.clear()\n    b = BytesIO()\n    NameObject(\"/DIJMAC+Arial Black#1\").write_to_stream(b)\n    assert bytes(b.getbuffer()) == b\"/DIJMAC+Arial#20Black#231\"\n    assert caplog.text == \"\"\n\n    caplog.clear()\n    b = BytesIO()\n    NameObject(\"/你好世界 (%)\").write_to_stream(b)\n    assert bytes(b.getbuffer()) == b\"/#E4#BD#A0#E5#A5#BD#E4#B8#96#E7#95#8C#20#28#25#29\"\n    assert caplog.text == \"\"\n\n    caplog.clear()\n    b = BytesIO()\n    NameObject(\"/{foo}<bar>(baz)[qux]#/%\").write_to_stream(b)\n    assert bytes(b.getbuffer()) == b\"/#7Bfoo#7D#3Cbar#3E#28baz#29#5Bqux#5D#23#2F#25\"\n    assert caplog.text == \"\"\n\n\ndef test_destination_fit_r():\n    d = Destination(\n        TextStringObject(\"title\"), NullObject(), Fit.fit_rectangle(0, 0, 0, 0)\n    )\n    assert d.title == NameObject(\"title\")\n    assert d.typ == \"/FitR\"\n    assert d.zoom is None\n    assert d.left == FloatObject(0)\n    assert d.right == FloatObject(0)\n    assert d.top == FloatObject(0)\n    assert d.bottom == FloatObject(0)\n    assert list(d) == []\n    d.empty_tree()\n\n\ndef test_destination_fit_v():\n    d = Destination(NameObject(\"title\"), NullObject(), Fit.fit_vertically(left=0))\n\n    writer = PdfWriter()\n    writer.add_named_destination_object(d)\n\n    # Trigger Exception\n    Destination(NameObject(\"title\"), NullObject(), Fit.fit_vertically(left=None))\n\n\ndef test_outline_item_write_to_stream():\n    stream = BytesIO()\n    oi = OutlineItem(NameObject(\"title\"), NullObject(), Fit.fit_vertically(left=0))\n    oi.write_to_stream(stream)\n    stream.seek(0, 0)\n    assert stream.read() == b\"<<\\n/Title (title)\\n/Dest [ null /FitV 0.0 ]\\n>>\"\n\n\ndef test_encode_pdfdocencoding_keyerror():\n    with pytest.raises(UnicodeEncodeError) as exc:\n        encode_pdfdocencoding(\"😀\")\n    assert exc.value.args[0] == \"pdfdocencoding\"\n\n\n@pytest.mark.parametrize(\"test_input\", [\"\", \"data\"])\ndef test_encode_pdfdocencoding_returns_bytes(test_input):\n    \"\"\"\n    Test that encode_pdfdocencoding() always returns bytes because bytearray\n    is duck type compatible with bytes in mypy\n    \"\"\"\n    out = encode_pdfdocencoding(test_input)\n    assert isinstance(out, bytes)\n\n\ndef test_read_object_comment_exception():\n    stream = BytesIO(b\"% foobar\")\n    pdf = None\n    with pytest.raises(PdfStreamError) as exc:\n        read_object(stream, pdf)\n    assert exc.value.args[0] == \"File ended unexpectedly.\"\n\n\ndef test_read_object_empty():\n    stream = BytesIO(b\"endobj\")\n    pdf = None\n    assert isinstance(read_object(stream, pdf), NullObject)\n\n\ndef test_read_object_empty_in_array():\n    stream = BytesIO(b\"[endobj\")\n    pdf = None\n    result = read_object(stream, pdf)\n    assert isinstance(result, ArrayObject)\n    assert len(result) == 1\n    assert isinstance(result[0], NullObject)\n\n\ndef test_read_object_invalid():\n    stream = BytesIO(b\"hello\")\n    pdf = None\n    with pytest.raises(PdfReadError) as exc:\n        read_object(stream, pdf)\n    assert \"hello\" in exc.value.args[0]\n\n\ndef test_read_object_comment():\n    stream = BytesIO(b\"% foobar\\n1 \")\n    pdf = None\n    out = read_object(stream, pdf)\n    assert out == 1\n\n\ndef test_bytestringobject():\n    bo = ByteStringObject(\"stream\", encoding=\"utf-8\")\n    stream = BytesIO(b\"\")\n    bo.write_to_stream(stream)\n    stream.seek(0, 0)\n    assert stream.read() == b\"<73747265616d>\"  # TODO: how can we verify this?\n\n\ndef test_dictionaryobject_key_is_no_pdfobject():\n    do = DictionaryObject({NameObject(\"/S\"): NameObject(\"/GoTo\")})\n    with pytest.raises(ValueError) as exc:\n        do[\"foo\"] = NameObject(\"/GoTo\")\n    assert exc.value.args[0] == \"Key must be a PdfObject\"\n\n\ndef test_dictionaryobject_xmp_meta():\n    do = DictionaryObject({NameObject(\"/S\"): NameObject(\"/GoTo\")})\n    assert do.xmp_metadata is None\n\n\ndef test_dictionaryobject_value_is_no_pdfobject():\n    do = DictionaryObject({NameObject(\"/S\"): NameObject(\"/GoTo\")})\n    with pytest.raises(ValueError) as exc:\n        do[NameObject(\"/S\")] = \"/GoTo\"\n    assert exc.value.args[0] == \"Value must be a PdfObject\"\n\n\ndef test_dictionaryobject_setdefault_key_is_no_pdfobject():\n    do = DictionaryObject({NameObject(\"/S\"): NameObject(\"/GoTo\")})\n    with pytest.raises(ValueError) as exc:\n        do.setdefault(\"foo\", NameObject(\"/GoTo\"))\n    assert exc.value.args[0] == \"Key must be a PdfObject\"\n\n\ndef test_dictionaryobject_setdefault_value_is_no_pdfobject():\n    do = DictionaryObject({NameObject(\"/S\"): NameObject(\"/GoTo\")})\n    with pytest.raises(ValueError) as exc:\n        do.setdefault(NameObject(\"/S\"), \"/GoTo\")\n    assert exc.value.args[0] == \"Value must be a PdfObject\"\n\n\ndef test_dictionaryobject_setdefault_value():\n    do = DictionaryObject({NameObject(\"/S\"): NameObject(\"/GoTo\")})\n    do.setdefault(NameObject(\"/S\"), NameObject(\"/GoTo\"))\n\n\ndef test_dictionaryobject_read_from_stream():\n    stream = BytesIO(b\"<< /S /GoTo >>\")\n    pdf = None\n    out = DictionaryObject.read_from_stream(stream, pdf)\n    assert out.get_object() == {NameObject(\"/S\"): NameObject(\"/GoTo\")}\n\n\ndef test_dictionaryobject_read_from_stream_broken():\n    stream = BytesIO(b\"< /S /GoTo >>\")\n    pdf = None\n    with pytest.raises(PdfReadError) as exc:\n        DictionaryObject.read_from_stream(stream, pdf)\n    assert (\n        exc.value.args[0]\n        == \"Dictionary read error at byte 0x2: stream must begin with '<<'\"\n    )\n\n\ndef test_dictionaryobject_read_from_stream_unexpected_end():\n    stream = BytesIO(b\"<< \\x00/S /GoTo\")\n    pdf = None\n    with pytest.raises(PdfStreamError) as exc:\n        DictionaryObject.read_from_stream(stream, pdf)\n    assert exc.value.args[0] == \"Stream has ended unexpectedly\"\n\n\ndef test_dictionaryobject_read_from_stream_stream_no_newline():\n    stream = BytesIO(b\"<< /S /GoTo >>stream\")\n    pdf = None\n    with pytest.raises(PdfReadError) as exc:\n        DictionaryObject.read_from_stream(stream, pdf)\n    assert exc.value.args[0] == \"Stream data must be followed by a newline\"\n\n\n@pytest.mark.parametrize((\"strict\"), [(True), (False)])\ndef test_dictionaryobject_read_from_stream_stream_no_stream_length(strict, caplog):\n    stream = BytesIO(b\"<< /S /GoTo >>stream\\n123456789endstream abcd\")\n\n    class Tst:  # to replace pdf\n        strict = False\n\n    pdf = Tst()\n    pdf.strict = strict\n    if strict:\n        with pytest.raises(PdfReadError) as exc:\n            DictionaryObject.read_from_stream(stream, pdf)\n        assert exc.value.args[0] == \"Stream length not defined\"\n    else:\n        o = DictionaryObject.read_from_stream(stream, pdf)\n        assert \"Stream length not defined\" in caplog.text\n        assert o.get_data() == b\"123456789\"\n\n\n@pytest.mark.parametrize(\n    (\"strict\", \"length\", \"should_fail\"),\n    [\n        (True, 6, False),\n        (True, 10, False),\n        (True, 4, True),\n        (False, 6, False),\n        (False, 10, False),\n    ],\n)\ndef test_dictionaryobject_read_from_stream_stream_stream_valid(\n    strict, length, should_fail\n):\n    stream = BytesIO(b\"<< /S /GoTo /Length %d >>stream\\nBT /F1\\nendstream\\n\" % length)\n\n    class Tst:  # to replace pdf\n        strict = True\n\n    pdf = Tst()\n    pdf.strict = strict\n    with pytest.raises(PdfReadError) as exc:\n        do = DictionaryObject.read_from_stream(stream, pdf)\n        # TODO: What should happen with the stream?\n        assert do == {\"/S\": \"/GoTo\"}\n        if length in (6, 10):\n            assert b\"BT /F1\" in do.get_data()\n        raise PdfReadError(\"__ALLGOOD__\")\n    assert should_fail ^ (exc.value.args[0] == \"__ALLGOOD__\")\n\n\ndef test_rectangleobject():\n    ro = RectangleObject((1, 2, 3, 4))\n    assert ro.lower_left == (1, 2)\n    assert ro.lower_right == (3, 2)\n    assert ro.upper_left == (1, 4)\n    assert ro.upper_right == (3, 4)\n\n    ro.lower_left = (5, 6)\n    assert ro.lower_left == (5, 6)\n\n    ro.bottom -= 2\n    ro.left -= 2\n    assert ro.lower_left == (3, 4)\n\n    ro.lower_right = (7, 8)\n    assert ro.lower_right == (7, 8)\n\n    ro.upper_left = (9, 11)\n    assert ro.upper_left == (9, 11)\n\n    ro.upper_right = (13, 17)\n    assert ro.upper_right == (13, 17)\n    ro.top += 1\n    ro.right += 1\n    assert ro.upper_right == (14, 18)\n\n\ndef test_textstringobject_exc():\n    tso = TextStringObject(\"foo\")\n    assert tso.get_original_bytes() == b\"foo\"\n\n\ndef test_textstringobject_autodetect_utf16():\n    tso = TextStringObject(\"foo\")\n    tso.autodetect_utf16 = True\n    tso.utf16_bom = codecs.BOM_UTF16_BE\n    assert tso.get_original_bytes() == b\"\\xfe\\xff\\x00f\\x00o\\x00o\"\n    tso.utf16_bom = codecs.BOM_UTF16_LE\n    assert tso.get_original_bytes() == b\"\\xff\\xfef\\x00o\\x00o\\x00\"\n    assert tso.get_encoded_bytes() == b\"\\xff\\xfef\\x00o\\x00o\\x00\"\n\n\ndef test_textstringobject__numbers_as_input():\n    _ = TextStringObject(42)\n    _ = TextStringObject(13.37)\n\n\ndef test_remove_child_not_in_tree():\n    tree = TreeObject()\n    with pytest.raises(ValueError) as exc:\n        tree.remove_child(ChildDummy())\n    assert exc.value.args[0] == \"Removed child does not appear to be a tree item\"\n\n\ndef test_remove_child_not_in_that_tree():\n    tree = TreeObject()\n    tree.indirect_reference = NullObject()\n    child = TreeObject()\n    child.indirect_reference = NullObject()\n    with pytest.raises(ValueError) as exc:\n        child.remove_from_tree()\n    assert exc.value.args[0] == \"Removed child does not appear to be a tree item\"\n    tree.add_child(child, ReaderDummy())\n    with pytest.raises(ValueError) as exc:\n        tree.remove_child(child)\n    assert exc.value.args[0] == \"Removed child is not a member of this tree\"\n\n\ndef test_remove_child_not_found_in_tree():\n    class ChildDummy(DictionaryObject):\n        @property\n        def indirect_reference(self) -> \"ChildDummy\":\n            return self\n\n    tree = TreeObject()\n    tree.indirect_reference = NullObject()\n    child = ChildDummy(TreeObject())\n    tree.add_child(child, ReaderDummy())\n    child2 = ChildDummy(TreeObject())\n    child2[NameObject(\"/Parent\")] = tree\n    with pytest.raises(ValueError) as exc:\n        tree.remove_child(child2)\n    assert exc.value.args[0] == \"Removal couldn't find item in tree\"\n\n\ndef test_remove_child_found_in_tree():\n    writer = PdfWriter()\n\n    # Add Tree\n    tree = TreeObject()\n    writer._add_object(tree)\n\n    # Add first child\n    # It's important to set a value, otherwise the writer.get_reference will\n    # return the same object when a second child is added.\n    child1 = TreeObject()\n    child1[NameObject(\"/Foo\")] = TextStringObject(\"bar\")\n    child1_ref = writer._add_object(child1)\n    tree.add_child(child1_ref, writer)\n    assert tree[NameObject(\"/Count\")] == 1\n    assert len(list(tree.children())) == 1\n\n    # Add second child\n    child2 = TreeObject()\n    child2[NameObject(\"/Foo\")] = TextStringObject(\"baz\")\n    child2_ref = writer._add_object(child2)\n    tree.add_child(child2_ref, writer)\n    assert tree[NameObject(\"/Count\")] == 2\n    assert len(list(tree.children())) == 2\n\n    # Remove last child\n    tree.remove_child(child2_ref)\n    assert tree[NameObject(\"/Count\")] == 1\n    assert len(list(tree.children())) == 1\n\n    # Add new child\n    child3 = TreeObject()\n    child3[NameObject(\"/Foo\")] = TextStringObject(\"3\")\n    child3_ref = writer._add_object(child3)\n    tree.add_child(child3_ref, writer)\n    assert tree[NameObject(\"/Count\")] == 2\n    assert len(list(tree.children())) == 2\n\n    # Remove first child\n    child1 = tree[NameObject(\"/First\")]\n    tree.remove_child(child1)\n    assert tree[NameObject(\"/Count\")] == 1\n    assert len(list(tree.children())) == 1\n\n    child4 = TreeObject()\n    child4[NameObject(\"/Foo\")] = TextStringObject(\"4\")\n    child4_ref = writer._add_object(child4)\n    tree.add_child(child4_ref, writer)\n    assert tree[NameObject(\"/Count\")] == 2\n    assert len(list(tree.children())) == 2\n\n    child5 = TreeObject()\n    child5[NameObject(\"/Foo\")] = TextStringObject(\"5\")\n    child5_ref = writer._add_object(child5)\n    tree.add_child(child5_ref, writer)\n    assert tree[NameObject(\"/Count\")] == 3\n    assert len(list(tree.children())) == 3\n\n    # Remove middle child\n    child4.remove_from_tree()\n    assert tree[NameObject(\"/Count\")] == 2\n    assert len(list(tree.children())) == 2\n\n    tree.empty_tree()\n\n\ndef test_remove_child_in_tree():\n    pdf = RESOURCE_ROOT / \"form.pdf\"\n\n    tree = TreeObject()\n    reader = PdfReader(pdf)\n    writer = PdfWriter()\n    writer._add_object(tree)\n    writer.add_page(reader.pages[0])\n    writer.add_outline_item(\"foo\", page_number=0)\n    obj = writer._objects[-1]\n    tree.add_child(obj, writer)\n    tree.remove_child(obj)\n    tree.add_child(obj, writer)\n    tree.empty_tree()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"caplog_content\"),\n    [\n        (  # parse_content_stream_peek_percentage\n            \"https://github.com/user-attachments/files/18381763/tika-985770.pdf\",\n            \"tika-985770.pdf\",\n            \"\",\n        ),\n        (  # read_inline_image_no_has_q\n            \"https://github.com/user-attachments/files/18381775/tika-998719.pdf\",\n            \"tika-998719.pdf\",\n            \"\",\n        ),\n        (  # read_inline_image_loc_neg_1\n            \"https://github.com/user-attachments/files/18381706/tika-935066.pdf\",\n            \"tika-935066.pdf\",\n            \"\",\n        ),\n        (  # object_read_from_stream_unicode_error\n            \"https://github.com/user-attachments/files/18381750/tika-974966.pdf\",\n            \"tika-974966.pdf\",\n            \"\",\n        ),\n        (  # dict_read_from_stream\n            \"https://github.com/user-attachments/files/18381762/tika-984877.pdf\",\n            \"tika-984877.pdf\",\n            \"Multiple definitions in dictionary at byte 0x1084 for key /Length\",\n        ),\n    ],\n    ids=[\n        \"parse_content_stream_peek_percentage\",\n        \"read_inline_image_no_has_q\",\n        \"read_inline_image_loc_neg_1\",\n        \"object_read_from_stream_unicode_error\",\n        \"dict_read_from_stream\",\n    ],\n)\ndef test_extract_text(caplog, url: str, name: str, caplog_content: str):\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    for page in reader.pages:\n        page.extract_text()\n    if caplog_content == \"\":\n        assert caplog_content == caplog.text\n    else:\n        assert caplog_content in caplog.text\n\n\n@pytest.mark.slow\n@pytest.mark.enable_socket\ndef test_text_string_write_to_stream():\n    url = \"https://github.com/user-attachments/files/18381698/tika-924562.pdf\"\n    name = \"tika-924562.pdf\"\n\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    for page in writer.pages:\n        page.compress_content_streams()\n\n\n@pytest.mark.enable_socket\ndef test_bool_repr(tmp_path):\n    url = \"https://github.com/user-attachments/files/18381703/tika-932449.pdf\"\n    name = \"tika-932449.pdf\"\n\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    write_path = tmp_path / \"tmp-fields-report.txt\"\n    with open(write_path, \"w\") as fp:\n        fields = reader.get_fields(fileobj=fp)\n    assert fields\n    assert list(fields.keys()) == [\"USGPOSignature\"]\n    with open(write_path) as fp:\n        data = fp.read()\n    assert data.startswith(\n        \"Field Name: USGPOSignature\\nField Type: Signature\\nField Flags: 1\\n\"\n        \"Value: {'/Type': '/Sig', '/Filter': '/Adobe.PPKLite', \"\n        \"'/SubFilter':\"\n    )\n\n\n@pytest.mark.enable_socket\ndef test_issue_997(pdf_file_path):\n    url = (\n        \"https://github.com/py-pdf/pypdf/files/8908874/\"\n        \"Exhibit_A-2_930_Enterprise_Zone_Tax_Credits_final.pdf\"\n    )\n    name = \"gh-issue-997.pdf\"\n\n    merger = PdfWriter()\n    merger.append(BytesIO(get_data_from_url(url, name=name)))  # here the error raises\n    with open(pdf_file_path, \"wb\") as f:\n        merger.write(f)\n    merger.close()\n\n    # Strict\n    merger = PdfWriter()\n    merger.append(BytesIO(get_data_from_url(url, name=name)))  # here the error raises\n    with open(pdf_file_path, \"wb\") as f:\n        merger.write(f)\n    merger.close()\n\n\ndef test_checkboxradiobuttonattributes_opt():\n    assert \"/Opt\" in CheckboxRadioButtonAttributes.attributes_dict()\n\n\ndef test_name_object_invalid_decode():\n    charsets = deepcopy(NameObject.CHARSETS)\n    try:\n        NameObject.CHARSETS = (\"utf-8\",)\n        stream = BytesIO(b\"/\\x80\\x02\\x03\")\n        # strict:\n        with pytest.raises(PdfReadError) as exc:\n            NameObject.read_from_stream(stream, ReaderDummy(strict=True))\n        assert \"Illegal character in NameObject \" in exc.value.args[0]\n\n        # non-strict:\n        stream.seek(0)\n        NameObject.read_from_stream(stream, ReaderDummy(strict=False))\n    finally:\n        NameObject.CHARSETS = charsets\n\n\ndef test_indirect_object_invalid_read():\n    stream = BytesIO(b\"0 1 s\")\n    with pytest.raises(PdfReadError) as exc:\n        IndirectObject.read_from_stream(stream, ReaderDummy())\n    assert exc.value.args[0] == \"Error reading indirect object reference at byte 0x5\"\n\n\ndef test_create_string_object_utf16_bom():\n    # utf16-be\n    result = create_string_object(\n        b\"\\xfe\\xff\\x00P\\x00a\\x00p\\x00e\\x00r\\x00P\\x00o\\x00r\\x00t\\x00 \\x001\\x004\\x00\\x00\"\n    )\n    assert result == \"PaperPort 14\\x00\"\n    assert result.autodetect_utf16 is True\n    assert result.utf16_bom == b\"\\xfe\\xff\"\n    assert (\n        result.get_encoded_bytes()\n        == b\"\\xfe\\xff\\x00P\\x00a\\x00p\\x00e\\x00r\\x00P\\x00o\\x00r\\x00t\\x00 \\x001\\x004\\x00\\x00\"\n    )\n\n    # utf16-le\n    result = create_string_object(\n        b\"\\xff\\xfeP\\x00a\\x00p\\x00e\\x00r\\x00P\\x00o\\x00r\\x00t\\x00 \\x001\\x004\\x00\\x00\\x00\"\n    )\n    assert result == \"PaperPort 14\\x00\"\n    assert result.autodetect_utf16 is True\n    assert result.utf16_bom == b\"\\xff\\xfe\"\n    assert (\n        result.get_encoded_bytes()\n        == b\"\\xff\\xfeP\\x00a\\x00p\\x00e\\x00r\\x00P\\x00o\\x00r\\x00t\\x00 \\x001\\x004\\x00\\x00\\x00\"\n    )\n    result = TextStringObject(\n        b\"\\xff\\xfeP\\x00a\\x00p\\x00e\\x00r\\x00P\\x00o\\x00r\\x00t\\x00 \\x001\\x004\\x00\\x00\\x00\"\n    )\n    assert result == \"PaperPort 14\\x00\"\n    assert result.autodetect_utf16 is True\n    assert result.utf16_bom == b\"\\xff\\xfe\"\n    assert (\n        result.get_encoded_bytes()\n        == b\"\\xff\\xfeP\\x00a\\x00p\\x00e\\x00r\\x00P\\x00o\\x00r\\x00t\\x00 \\x001\\x004\\x00\\x00\\x00\"\n    )\n\n    # utf16-be without bom\n    result = TextStringObject(\"ÿ\")\n    result.autodetect_utf16 = True\n    result.utf16_bom = b\"\"\n    assert result.get_encoded_bytes() == b\"\\x00\\xFF\"\n    assert result.original_bytes == b\"\\x00\\xFF\"\n\n\ndef test_create_string_object_force():\n    assert create_string_object(b\"Hello World\", []) == \"Hello World\"\n    assert create_string_object(b\"Hello World\", {72: \"A\"}) == \"Aello World\"\n    assert create_string_object(b\"Hello World\", \"utf8\") == \"Hello World\"\n\n\n@pytest.mark.parametrize(\n    (\"value\", \"expected\"),\n    [\n        (\"0.000000\", \"0.0\"),\n        (\"0.0\", \"0.0\"),\n        (\"1.0\", \"1\"),\n        (\"0.123000\", \"0.123\"),\n        (\"0.000123000\", \"0.000123\"),\n        (\"0.0\", \"0.0\"),\n        (\"0\", \"0.0\"),\n        (\"1\", \"1\"),\n        (\"1.0\", \"1\"),\n        (\"1.01\", \"1.01\"),\n        (\"1.010\", \"1.01\"),\n        (\"0000.0000\", \"0.0\"),\n        (\"0.10101010\", \"0.1010101\"),\n        (\"50000000000\", \"50000000000\"),\n        (\"99900000000000000123\", \"99900000000000000000\"),\n        (\"99900000000000000123.456000\", \"99900000000000000000\"),\n        (\"0.00000000000000000000123\", \"0.00000000000000000000123\"),\n        (\"0.00000123\", \"0.00000123\"),\n        (\"0.00000000000000000000123000\", \"0.00000000000000000000123\"),\n        (\"-4.6\", \"-4.6\"),  # from #1910\n        # (\n        #    \"50032481330523882508234.00000000000000000000123000\",\n        #    \"50032481330523882508234.00000000000000000000123\",\n        # ),\n        # (\n        #    \"928457298572093487502198745102973402987412908743.75249875981374981237498213740000\",\n        #    \"928457298572093487502198745102973402987412908743.7524987598137498123749821374\",\n        # ),\n    ],\n)\ndef test_float_object_decimal_to_string(value, expected):\n    assert repr(FloatObject(value)) == expected\n\n\ndef test_cloning(caplog):\n    writer = PdfWriter()\n    with pytest.raises(Exception) as exc:\n        PdfObject().clone(writer)\n    assert \"PdfObject does not implement .clone so far\" in exc.value.args[0]\n\n    obj1 = DictionaryObject()\n    obj1.indirect_reference = None\n    n = len(writer._objects)\n    obj2 = obj1.clone(writer)\n    assert len(writer._objects) == n + 1\n    obj3 = obj2.clone(writer)\n    assert len(writer._objects) == n + 1\n    assert obj2.indirect_reference == obj3.indirect_reference\n    obj3 = obj2.indirect_reference.clone(writer)\n    assert len(writer._objects) == n + 1\n    assert obj2.indirect_reference == obj3.indirect_reference\n    assert (\n        obj2.indirect_reference\n        == obj2._reference_clone(obj2, writer).indirect_reference\n    )\n    assert len(writer._objects) == n + 1\n    assert obj2.indirect_reference == obj3.indirect_reference\n\n    obj3 = obj2.indirect_reference.clone(writer, True)\n    assert len(writer._objects) == n + 2\n    assert obj2.indirect_reference != obj3.indirect_reference\n\n    arr1 = ArrayObject([obj2])\n    arr2 = arr1.clone(writer)\n    arr3 = arr2.clone(writer)\n    assert arr2 == arr3\n    obj10 = StreamObject()\n    arr1 = ArrayObject([obj10])\n    obj11 = obj10.clone(writer)\n    assert arr1[0] == obj11\n\n    obj20 = DictionaryObject(\n        {NameObject(\"/Test\"): NumberObject(1), NameObject(\"/Test2\"): StreamObject()}\n    )\n    obj21 = obj20.clone(writer, ignore_fields=None)\n    assert \"/Test\" in obj21\n    assert isinstance(obj21.get(\"/Test2\"), IndirectObject)\n\n\ndef test_cloning_indirect_obj_keeps_hard_reference():\n    \"\"\"\n    Reported in #3450\n\n    Ensure that cloning an IndirectObject keeps a hard reference to\n    the underlying object, preventing its deallocation, which could allow\n    `id(obj)` to return the same value for different objects.\n    \"\"\"\n    writer1 = PdfWriter()\n    indirect_object = IndirectObject(1, 0, writer1)\n\n    # Create a weak reference to the underlying object to test later\n    # if it is still alive in memory or not\n    obj_weakref = weakref.ref(indirect_object.pdf)\n    assert obj_weakref() is not None\n\n    writer2 = PdfWriter()\n    indirect_object.clone(writer2)\n\n    # Mimic indirect_object/writer1 going out of scope and being\n    # garbage collected. Clone should have kept a hard reference to\n    # it, preventing its deallocation.\n    del indirect_object\n    del writer1\n    gc.collect()\n    assert obj_weakref() is not None\n\n\ndef test_cloning_null_obj_keeps_hard_reference():\n    \"\"\"\n    Ensure that cloning a NullObject keeps a hard reference to\n    the underlying object, preventing its deallocation, which could allow\n    `id(obj)` to return the same value for different objects.\n    \"\"\"\n    writer1 = PdfWriter()\n    indirect_object = IndirectObject(1, 0, writer1)\n    null_obj = NullObject()\n    null_obj.indirect_reference = indirect_object\n\n    # Create a weak reference to the underlying object to test later\n    # if it is still alive in memory or not\n    obj_weakref = weakref.ref(indirect_object.pdf)\n    assert obj_weakref() is not None\n\n    writer2 = PdfWriter()\n    null_obj.clone(writer2)\n\n    # Mimic indirect_object/writer1 going out of scope and being\n    # garbage collected. Clone should have kept a hard reference to\n    # it, preventing its deallocation.\n    del indirect_object\n    del writer1\n    del null_obj\n    gc.collect()\n    assert obj_weakref() is not None\n\n\n@pytest.mark.enable_socket\ndef test_append_with_indirectobject_not_pointing(caplog):\n    \"\"\"\n    Reported in #1631\n    the object 43 0 is not invalid\n    \"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/10729142/document.pdf\"\n    name = \"tst_iss1631.pdf\"\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data, strict=False)\n    writer = PdfWriter()\n    writer.append(reader)\n    assert \"Object 43 0 not defined.\" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_iss1615_1673():\n    \"\"\"\n    Test cases where /N is not indicating chains of objects\n    test also where /N,... are not part of chains\n    \"\"\"\n    # #1615\n    url = \"https://github.com/py-pdf/pypdf/files/10671366/graph_letter.pdf\"\n    name = \"graph_letter.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader)\n    assert (\n        \"/N\"\n        in writer.pages[0][\"/Annots\"][0]\n        .get_object()[\"/AP\"][\"/N\"][\"/Resources\"][\"/ColorSpace\"][\"/Cs1\"][1]\n        .get_object()\n    )\n    # #1673\n    url = \"https://github.com/py-pdf/pypdf/files/10848750/budgeting-loan-form-sf500.pdf\"\n    name = \"budgeting-loan-form-sf500.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n\n\n@pytest.mark.enable_socket\ndef test_destination_withoutzoom():\n    \"\"\"Cf issue #1832\"\"\"\n    url = \"https://github.com/user-attachments/files/15605648/2021_book_security.pdf\"\n    name = \"2021_book_security.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.outline\n\n    out = BytesIO()\n    writer = PdfWriter(clone_from=reader)\n    writer.write(out)\n\n\ndef test_encodedstream_set_data():\n    \"\"\"\n    EncodedStreamObject.set_data to extend data stream works.\n\n    Checks also the flate_encode.\n    \"\"\"\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    co = reader.pages[0][\"/Contents\"][0].get_object()\n    co.set_data(b\"%hello\\n\" + co.get_data())\n    assert b\"hello\" in co.get_data()\n    b = BytesIO()\n    co.write_to_stream(b)\n    b.seek(0)\n    aa = read_object(b, None)\n    assert b\"hello\" in aa.get_data()\n    assert aa[\"/Filter\"] == \"/FlateDecode\"\n    assert \"/DecodeParms\" not in aa\n    bb = aa.flate_encode()\n    assert b\"hello\" in bb.get_data()\n    assert bb[\"/Filter\"] == [\"/FlateDecode\", \"/FlateDecode\"]\n    assert str(bb[\"/DecodeParms\"]) == \"[NullObject, NullObject]\"\n    bb[NameObject(\"/Test\")] = NameObject(\"/MyTest\")\n    cc = bb.flate_encode()\n    assert bb[\"/Filter\"] == [\"/FlateDecode\", \"/FlateDecode\"]\n    assert b\"hello\" in cc.get_data()\n    assert cc[\"/Filter\"] == [\"/FlateDecode\", \"/FlateDecode\", \"/FlateDecode\"]\n    assert str(cc[\"/DecodeParms\"]) == \"[NullObject, NullObject, NullObject]\"\n    assert cc[NameObject(\"/Test\")] == \"/MyTest\"\n\n    with pytest.raises(TypeError):\n        aa.set_data(\"toto\")\n\n    aa[NameObject(\"/Filter\")] = NameObject(\"/JPXEncode\")\n    with pytest.raises(PdfReadError):\n        aa.set_data(b\"toto\")\n\n\n@pytest.mark.enable_socket\ndef test_set_data_2():\n    \"\"\"\n    Modify a stream not yet loaded and\n    where the filter is [\"/FlateDecode\"]\n    \"\"\"\n    url = \"https://github.com/user-attachments/files/16796095/f5471sm-2.pdf\"\n    name = \"iss2780.pdf\"\n    writer = PdfWriter(BytesIO(get_data_from_url(url, name=name)))\n    writer.root_object[\"/AcroForm\"][\"/XFA\"][7].set_data(b\"test\")\n    assert writer.root_object[\"/AcroForm\"][\"/XFA\"][7].get_object()[\"/Filter\"] == [\n        \"/FlateDecode\"\n    ]\n    assert writer.root_object[\"/AcroForm\"][\"/XFA\"][7].get_object().get_data() == b\"test\"\n\n\n@pytest.mark.enable_socket\ndef test_calling_indirect_objects():\n    \"\"\"Cope with cases where attributes/items are called from indirectObject\"\"\"\n    url = \"https://github.com/user-attachments/files/15605648/2021_book_security.pdf\"\n    name = \"2021_book_security.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.trailer.get(\"/Info\")[\"/Creator\"]\n    reader.pages[0][\"/Contents\"][0].get_data()\n    writer = PdfWriter(clone_from=reader)\n    ind = writer._add_object(writer)\n    assert ind.fileobj == writer.fileobj\n    with pytest.raises(AttributeError):\n        ind.not_existing_attribute\n    # create an IndirectObject referencing an IndirectObject.\n    writer._objects.append(writer.pages[0].indirect_reference)\n    ind = IndirectObject(len(writer._objects), 0, writer)\n    with pytest.raises(PdfStreamError):\n        ind[\"/Type\"]\n\n\n@pytest.mark.enable_socket\ndef test_indirect_object_page_dimensions():\n    url = \"https://github.com/py-pdf/pypdf/files/13302338/Zymeworks_Corporate.Presentation_FINAL1101.pdf.pdf\"\n    name = \"issue2287.pdf\"\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data, strict=False)\n    mediabox = reader.pages[0].mediabox\n    assert mediabox == RectangleObject((0, 0, 792, 612))\n\n\ndef test_indirect_object_contains():\n    writer = PdfWriter()\n    indirect_object = IndirectObject(1, 0, writer)\n    assert \"foo\" not in indirect_object\n    assert \"/Producer\" in indirect_object\n\n\ndef test_indirect_object_iter():\n    writer = PdfWriter()\n    indirect_object = IndirectObject(1, 0, writer)\n    assert \"foo\" not in list(indirect_object)\n    assert \"/Producer\" in list(indirect_object)\n\n\ndef test_array_operators():\n    a = ArrayObject(\n        [\n            NumberObject(1),\n            NumberObject(2),\n            NumberObject(3),\n            NumberObject(4),\n        ]\n    )\n    b = a + 5\n    assert isinstance(b, ArrayObject)\n    assert b == [1, 2, 3, 4, 5]\n    assert a == [1, 2, 3, 4]\n    a -= 2\n    a += \"abc\"\n    a -= (3, 4)\n    a += [\"d\", \"e\"]\n    a += BooleanObject(True)\n    assert a == [1, \"abc\", \"d\", \"e\", True]\n    a += \"/toto\"\n    assert isinstance(a[-1], NameObject)\n    assert isinstance(a[1], TextStringObject)\n    a += b\"1234\"\n    assert a[-1] == ByteStringObject(b\"1234\")\n    la = len(a)\n    a -= 300\n    assert len(a) == la\n\n\ndef test_unitary_extract_inline_buffer_invalid():\n    with pytest.raises(PdfReadError):\n        extract_inline__ascii_hex_decode(BytesIO())\n    with pytest.raises(PdfReadError):\n        extract_inline__ascii_hex_decode(BytesIO(4095 * b\"00\" + b\"   \"))\n    with pytest.raises(PdfReadError):\n        extract_inline__ascii_hex_decode(BytesIO(b\"00\"))\n    with pytest.raises(PdfReadError):\n        extract_inline__ascii85_decode(BytesIO())\n    with pytest.raises(PdfReadError):\n        extract_inline__ascii85_decode(BytesIO(a85encode(b\"1\")))\n    with pytest.raises(PdfReadError):\n        extract_inline__ascii85_decode(BytesIO(a85encode(b\"1\") + b\"~> Q\"))\n    with pytest.raises(PdfReadError):\n        extract_inline__ascii85_decode(BytesIO(a85encode(b\"1234578\" * 990)))\n    with pytest.raises(PdfReadError):\n        extract_inline__run_length_decode(BytesIO())\n    with pytest.raises(PdfReadError):\n        extract_inline__run_length_decode(BytesIO(b\"\\x01\\x01\\x80\"))\n    with pytest.raises(PdfReadError):\n        extract_inline__dct_decode(BytesIO(b\"\\xFF\\xD9\"))\n\n\ndef test_unitary_extract_inline():\n    # AHx\n    b = 16000 * b\"00\"\n    assert len(extract_inline__ascii_hex_decode(BytesIO(b + b\" EI\"))) == len(b)\n    with pytest.raises(PdfReadError):\n        extract_inline__ascii_hex_decode(BytesIO(b + b\"> \"))\n    # RL\n    b = 8200 * b\"\\x00\\xAB\" + b\"\\x80\"\n    assert len(extract_inline__run_length_decode(BytesIO(b + b\" EI\"))) == len(b)\n\n    # default\n    # EIDD instead of EI; using A85\n    b = b\"\"\"1 0 0 1 0 0 cm  BT /F1 12 Tf 14.4 TL ET\\nq 100 0 0 100 100 100 cm\nBI\\n/W 16 /H 16 /BPC 8 /CS /RGB /F [/A85 /Fl]\\nID\nGar8O(o6*is8QV#;;JAuTq2lQ8J;%6#\\'d5b\"Q[+ZD?\\'\\\\+CGj9~>\nEIDD\nQ\\nBT 1 0 0 1 200 100 Tm (Test) Tj T* ET\\n \\n\"\"\"\n    ec = DecodedStreamObject()\n    ec.set_data(b)\n    co = ContentStream(ec, None)\n    with pytest.raises(PdfReadError) as exc:\n        co.operations\n    assert \"EI stream not found\" in exc.value.args[0]\n    # EIDD instead of EI; using /Fl (default extraction)\n    b = b\"\"\"1 0 0 1 0 0 cm  BT /F1 12 Tf 14.4 TL ET\\nq 100 0 0 100 100 100 cm\nBI\\n/W 16 /H 16 /BPC 8 /CS /RGB /F /Fl \\nID\nGar8O(o6*is8QV#;;JAuTq2lQ8J;%6#\\'d5b\"Q[+ZD?\\'\\\\+CGj9~>\nEIDD\nQ\\nBT 1 0 0 1 200 100 Tm (Test) Tj T* ET\\n \\n\"\"\"\n    ec = DecodedStreamObject()\n    ec.set_data(b)\n    co = ContentStream(ec, None)\n    with pytest.raises(PdfReadError) as exc:\n        co.operations\n    assert \"Unexpected end of stream\" in exc.value.args[0]\n\n    b = b\"\"\"1 0 0 1 0 0 cm  BT /F1 12 Tf 14.4 TL ET\\nq 100 0 0 100 100 100 cm\nBI\\n/W 16 /H 16 /BPC 8 /CS /RGB /F /Fl \\nID\nGar8O(o6*is8QV#;;JAuTq2lQ8J;%6#\\'d5b\"Q[+ZD?\\'\\\\+CGj9~>EI\nBT\\nQ\\nBT 1 0 0 1 200 100 Tm (Test) Tj T* ET\\n \\n\"\"\"\n    ec = DecodedStreamObject()\n    ec.set_data(b)\n    co = ContentStream(ec, None)\n    with pytest.raises(PdfReadError) as exc:\n        co.operations\n    assert \"Unexpected end of stream\" in exc.value.args[0]\n\n    b = b\"\"\"1 0 0 1 0 0 cm  BT /F1 12 Tf 14.4 TL ET\\nq 100 0 0 100 100 100 cm\nBI\\n/W 4 /H 4 /CS /G \\nID\nabcdefghijklmnopEI\nQ\\nQ\\nBT 1 0 0 1 200 100 Tm (Test) Tj T* ET\\n \\n\"\"\"\n    ec = DecodedStreamObject()\n    ec.set_data(b)\n    co = ContentStream(ec, None)\n    assert co.operations[7][0][\"data\"] == b\"abcdefghijklmnop\"\n\n    b = b\"\"\"1 0 0 1 0 0 cm  BT /F1 12 Tf 14.4 TL ET\\nq 100 0 0 100 100 100 cm\nBI\\n/W 4 /H 4 \\nID\nabcdefghijklmnopEI\nQ\\nQ\\nBT 1 0 0 1 200 100 Tm (Test) Tj T* ET\\n \\n\"\"\"\n    ec = DecodedStreamObject()\n    ec.set_data(b)\n    co = ContentStream(ec, None)\n    assert co.operations[7][0][\"data\"] == b\"abcdefghijklmnop\"\n\n\ndef test_missing_hashbin():\n    assert NullObject().hash_bin() == hash((NullObject,))\n    assert hash(NullObject()) == NullObject().hash_bin()\n    t = ByteStringObject(b\"123\")\n    assert t.hash_bin() == hash((ByteStringObject, b\"123\"))\n\n\ndef test_is_null_or_none():\n    assert is_null_or_none(NullObject())\n    assert not is_null_or_none(PdfObject())\n\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    # used with get\n    assert is_null_or_none(reader.root_object.get(\"/do_no_exist\"))\n    # object unknown...\n    assert is_null_or_none(IndirectObject(99999, 0, reader).get_object())\n    # ... or which has been replaced with NullObject\n    writer = PdfWriter(reader)\n    writer.pages[0][\"/Contents\"].append(writer._add_object(NullObject()))\n    assert is_null_or_none(writer.pages[0][\"/Contents\"][-1])\n\n\ndef test_coverage_arrayobject():\n    writer = PdfWriter()\n    a = ArrayObject([1])\n    assert isinstance(a.replicate(writer)[0], int)\n    assert isinstance(a.clone(writer)[0], int)\n    a.indirect_reference = IndirectObject(1, 0, writer)\n    assert isinstance(a.clone(writer)[0], int)\n    r = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    a = ArrayObject([r.pages[0][\"/Contents\"][0].get_object()])\n    aa = a.clone(writer)\n    assert isinstance(aa[0], IndirectObject)\n    for k, v in aa.items():\n        assert isinstance(k, int)\n        assert isinstance(v, PdfObject)\n\n\ndef test_coverage_streamobject():\n    writer = PdfWriter()\n    s = StreamObject()\n    del s.decoded_self\n    s.replicate(writer)\n    s.clone(writer)\n\n    co = ContentStream(None, None)\n    co.replicate(writer)\n    co.clone(writer, False, None)\n    co.indirect_reference = IndirectObject(1, 0, writer)\n    assert co == co.clone(writer)\n\n    r = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    co = r.pages[0].get_contents()\n    co[NameObject(\"/testkey\")] = NameObject(\"/test\")\n    co.decoded_self = None\n    assert \"/testkey\" in co.replicate(writer)\n    co = r.pages[0].get_contents()\n    co[NameObject(\"/testkey\")] = NameObject(\"/test\")\n    co.decoded_self = DecodedStreamObject()\n    assert \"/testkey\" in co.replicate(writer)\n\n\ndef test_contentstream_arrayobject_containing_nullobject(caplog):\n    stream_object = DecodedStreamObject()\n    stream_object.set_data(b\"Hello World!\")\n\n    input_stream = ArrayObject([NullObject(), stream_object])\n    content_stream = ContentStream(stream=input_stream, pdf=None)\n    assert content_stream.get_data() == b\"Hello World!\\n\"\n    assert caplog.text == \"\"\n\n\n@pytest.mark.enable_socket\ndef test_build_link__go_to_action_without_destination():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"issue-3419.pdf\")))\n    writer = PdfWriter()\n    for page in reader.pages:\n        writer.add_page(page)\n    assert len(writer.pages) == len(reader.pages)\n\n\n@pytest.mark.enable_socket\ndef test_dictionaryobject__length_0_stream():\n    \"\"\"Test for issue #3052.\"\"\"\n    url = \"https://github.com/user-attachments/files/18734105/correct.pdf\"\n    name = \"issue3052.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n    output = BytesIO()\n    writer.write(output)\n    assert b\"\\n8 0 obj\\n<<\\n/Length 0\\n>>\\nstream\\n\\nendstream\\nendobj\\n\" in output.getvalue()\n"
  },
  {
    "path": "tests/test_images.py",
    "content": "\"\"\"\nTests which ensure that image extraction works properly go here.\n\nTypically, tests in here should compare the extracted images count, names,\nand/or the actual image data with the expected value.\n\"\"\"\n\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import Union\nfrom unittest import mock\nfrom zipfile import ZipFile\n\nimport pytest\nfrom PIL import Image, ImageChops, ImageDraw\n\nfrom pypdf import PageObject, PdfReader, PdfWriter\nfrom pypdf.errors import LimitReachedError\nfrom pypdf.filters import JBIG2Decode\nfrom pypdf.generic import ContentStream, NameObject, NullObject\n\nfrom . import RESOURCE_ROOT, SAMPLE_ROOT, get_data_from_url\nfrom .utils import get_image_data\n\n\ndef open_image(path: Union[Path, Image.Image, BytesIO]) -> Image.Image:\n    if isinstance(path, Image.Image):\n        img = path\n    else:\n        if isinstance(path, Path):\n            assert path.exists()\n        with Image.open(path) as img:\n            img = (\n                img.copy()\n            )  # Opened image should be copied to avoid issues with file closing\n    return img\n\n\ndef image_size(image: Image.Image):\n    buffer = BytesIO()\n    image.save(buffer, format=image.format)\n    return buffer.tell()\n\n\ndef image_similarity(\n    path1: Union[Path, Image.Image, BytesIO], path2: Union[Path, Image.Image, BytesIO]\n) -> float:\n    \"\"\"\n    Check image similarity.\n\n    A value of \"0\" means the images are different. A value of 1 means they are\n    identical. A value above 0.9 means they are almost the same.\n\n    This can be used to ensure visual similarity.\n    \"\"\"\n    # Open the images using Pillow\n    image1 = open_image(path1)\n    image2 = open_image(path2)\n\n    # Check if the images have the same dimensions\n    if image1.size != image2.size:\n        return 0\n\n    # Check if the color modes are the same\n    if image1.mode != image2.mode:\n        return 0\n\n    # Calculate the Mean Squared Error (MSE)\n    diff = ImageChops.difference(image1, image2)\n    pixels = get_image_data(diff)\n\n    if isinstance(pixels[0], tuple):\n        mse = sum(sum((c / 255.0) ** 2 for c in p) for p in pixels) / (\n            len(pixels) * len(pixels[0])\n        )\n    else:\n        mse = sum((p / 255.0) ** 2 for p in pixels) / len(pixels)\n\n    return 1 - mse\n\n\n@pytest.mark.samples\ndef test_image_similarity_one():\n    path_a = SAMPLE_ROOT / \"018-base64-image/page-0-QuickPDFImd32aa1ab.png\"\n    path_b = path_a\n    assert image_similarity(path_a, path_b) == 1\n\n\n@pytest.mark.samples\ndef test_image_similarity_zero():\n    path_a = SAMPLE_ROOT / \"018-base64-image/page-0-QuickPDFImd32aa1ab.png\"\n    path_b = SAMPLE_ROOT / \"009-pdflatex-geotopo/page-23-Im2.png\"\n    assert image_similarity(path_a, path_b) == 0\n\n\n@pytest.mark.samples\ndef test_image_similarity_mid():\n    path_a = SAMPLE_ROOT / \"018-base64-image/page-0-QuickPDFImd32aa1ab.png\"\n    img_b = Image.open(path_a)\n    draw = ImageDraw.Draw(img_b)\n\n    # Fill the rectangle with black color\n    draw.rectangle([0, 0, 100, 100], fill=(0, 0, 0))\n    sim1 = image_similarity(path_a, img_b)\n    assert sim1 > 0.9\n    assert sim1 > 0\n    assert sim1 < 1\n\n    draw.rectangle([0, 0, 200, 200], fill=(0, 0, 0))\n    sim2 = image_similarity(path_a, img_b)\n    assert sim2 < sim1\n    assert sim2 > 0\n\n\n@pytest.mark.enable_socket\ndef test_image_new_property():\n    name = \"pdf_font_garbled.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=name)))\n    assert reader.pages[0].images.keys() == [\n        \"/I0\",\n        \"/I1\",\n        \"/I2\",\n        \"/I3\",\n        \"/I4\",\n        \"/I5\",\n        \"/I6\",\n        \"/I7\",\n        \"/I8\",\n        \"/I9\",\n        [\"/TPL1\", \"/Image5\"],\n        [\"/TPL2\", \"/Image53\"],\n        [\"/TPL2\", \"/Image37\"],\n        [\"/TPL2\", \"/Image49\"],\n        [\"/TPL2\", \"/Image51\"],\n        [\"/TPL2\", \"/Image39\"],\n        [\"/TPL2\", \"/Image57\"],\n        [\"/TPL2\", \"/Image55\"],\n        [\"/TPL2\", \"/Image43\"],\n        [\"/TPL2\", \"/Image30\"],\n        [\"/TPL2\", \"/Image22\"],\n        [\"/TPL2\", \"/Image41\"],\n        [\"/TPL2\", \"/Image47\"],\n        [\"/TPL2\", \"/Image45\"],\n        [\"/TPL3\", \"/Image65\"],\n        [\"/TPL3\", \"/Image30\"],\n        [\"/TPL3\", \"/Image61\"],\n        [\"/TPL4\", \"/Image30\"],\n        [\"/TPL5\", \"/Image30\"],\n        [\"/TPL6\", \"/Image30\"],\n        [\"/TPL7\", \"/Image30\"],\n        [\"/TPL8\", \"/Image30\"],\n        [\"/TPL9\", \"/Image30\"],\n        [\"/TPL10\", \"/Image30\"],\n        [\"/TPL11\", \"/Image30\"],\n        [\"/TPL12\", \"/Image30\"],\n    ]\n    assert len(reader.pages[0].images.items()) == 36\n    assert reader.pages[0].images[0].name == \"I0.png\"\n\n    expected_image_url = \"https://github.com/user-attachments/assets/3bf25760-2113-4e25-b4c2-fc1d3a84a263\"\n    expected_image_name = \"pdf_font_garbled_image30.png\"\n    expected_image_data = BytesIO(get_data_from_url(url=expected_image_url, name=expected_image_name))\n    assert image_similarity(\n        expected_image_data,\n        reader.pages[0].images[-1].image\n    ) == 1\n\n    assert reader.pages[0].images[\"/TPL1\", \"/Image5\"].image.format == \"JPEG\"\n    assert (\n        reader.pages[0].images[\"/I0\"].indirect_reference.get_object()\n        == reader.pages[0][\"/Resources\"][\"/XObject\"][\"/I0\"]\n    )\n    list(reader.pages[0].images[0:2])\n    with pytest.raises(TypeError):\n        reader.pages[0].images[b\"0\"]\n    with pytest.raises(IndexError):\n        reader.pages[0].images[9999]\n    # just for test coverage:\n    with pytest.raises(KeyError):\n        reader.pages[0]._get_image([\"test\"], reader.pages[0])\n    assert list(PageObject(None, None).images) == []\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"page_index\", \"image_key\", \"expected\"),\n    [\n        (\n            SAMPLE_ROOT / \"009-pdflatex-geotopo/GeoTopo.pdf\",\n            23,\n            \"/Im2\",\n            SAMPLE_ROOT / \"009-pdflatex-geotopo/page-23-Im2.png\",\n        ),\n        (\n            SAMPLE_ROOT / \"003-pdflatex-image/pdflatex-image.pdf\",\n            0,\n            \"/Im1\",\n            SAMPLE_ROOT / \"003-pdflatex-image/page-0-Im1.jpg\",\n        ),\n        (\n            SAMPLE_ROOT / \"018-base64-image/base64image.pdf\",\n            0,\n            \"/QuickPDFImd32aa1ab\",\n            SAMPLE_ROOT / \"018-base64-image/page-0-QuickPDFImd32aa1ab.png\",\n        ),\n        (\n            SAMPLE_ROOT / \"019-grayscale-image/grayscale-image.pdf\",\n            0,\n            \"/X0\",\n            SAMPLE_ROOT / \"019-grayscale-image/page-0-X0.png\",\n        ),\n    ],\n    ids=[\n        \"009-pdflatex-geotopo/page-23-Im2.png\",\n        \"003-pdflatex-image/page-0-Im1.jpg\",\n        \"018-base64-image/page-0-QuickPDFImd32aa1ab.png\",\n        \"019-grayscale-image/page-0-X0.png\",\n    ],\n)\n@pytest.mark.samples\ndef test_image_extraction(src, page_index, image_key, expected):\n    reader = PdfReader(src)\n    actual_image = reader.pages[page_index].images[image_key]\n    if not expected.exists():\n        # A little helper for test generation\n        with open(f\"page-{page_index}-{actual_image.name}\", \"wb\") as fp:\n            fp.write(actual_image.data)\n    assert image_similarity(BytesIO(actual_image.data), expected) >= 0.99\n\n\ndef test_get_inline_image_without_xobject_resources():\n    page = PageObject(None, None)\n    inline_image = object()\n\n    with mock.patch.object(page, \"_get_inline_images\", return_value={\"~0~\": inline_image}):\n        assert page._get_image(\"~0~\") is inline_image\n\n\ndef test_get_inline_image_without_xobject_resources_raises_when_missing():\n    page = PageObject(None, None)\n\n    with (\n        mock.patch.object(page, \"_get_inline_images\", return_value=None),\n        pytest.raises(KeyError, match=\"No inline image can be found\"),\n    ):\n        page._get_image(\"~0~\")\n\n\ndef test_get_xobject_image_without_xobject_resources_raises():\n    page = PageObject(None, None)\n\n    with pytest.raises(\n        KeyError,\n        match=\"Cannot access image object /Im0 without XObject resources\",\n    ):\n        page._get_image(\"/Im0\")\n\n\n@pytest.mark.enable_socket\n@pytest.mark.timeout(30)\ndef test_loop_in_image_keys():\n    \"\"\"Cf #2077\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss2077.pdf\")))\n    reader.pages[0][\"/Resources\"][\"/XObject\"][NameObject(\"/toto\")] = NullObject()\n    reader.pages[0].images.keys()\n\n\n@pytest.mark.enable_socket\ndef test_devicen_cmyk_black_only():\n    \"\"\"Cf #2321\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/13501846/Addressing_Adversarial_Attacks.pdf\"\n    name = \"iss2321.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/py-pdf/pypdf/assets/4083478/cc2dabc1-86e6-4179-a8a4-2b0efea124be\"\n    name = \"iss2321_img0.pdf\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    assert image_similarity(reader.pages[5].images[0].image, img) >= 0.99\n    url = \"https://github.com/py-pdf/pypdf/assets/4083478/6b64a949-42be-40d5-9eea-95707f350d89\"\n    name = \"iss2321_img1.pdf\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    assert image_similarity(reader.pages[10].images[0].image, img) >= 0.99\n\n\n@pytest.mark.enable_socket\ndef test_bi_in_text():\n    \"\"\"Cf #2456\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/14322910/BI_text_with_one_image.pdf\"\n    name = \"BI_text_with_one_image.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert reader.pages[0].images.keys() == [\"~0~\"]\n    assert reader.pages[0].images[0].name == \"~0~.png\"\n\n\n@pytest.mark.enable_socket\ndef test_cmyk_no_filter():\n    \"\"\"Cf #2522\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/14614887/out3.pdf\"\n    name = \"iss2522.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].images[0].image\n\n\n@pytest.mark.enable_socket\ndef test_separation_1byte_to_rgb_inverted():\n    \"\"\"Cf #2343\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/13679585/test2_P038-038.pdf\"\n    name = \"iss2343.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/py-pdf/pypdf/assets/4083478/b7f41897-96ef-4ea6-b165-5ef307a92b87\"\n    name = \"iss2343.png\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    assert image_similarity(reader.pages[0].images[0].image, img) >= 0.99\n    obj = reader.pages[0].images[0].indirect_reference.get_object()\n    obj.set_data(obj.get_data() + b\"\\x00\")\n    with pytest.raises(ValueError):\n        reader.pages[0].images[0]\n\n\n@pytest.mark.enable_socket\ndef test_data_with_lf():\n    \"\"\"Cf #2343\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/13946477/panda.pdf\"\n    name = \"iss2343b.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/py-pdf/pypdf/assets/4083478/1120b0cf-a67a-403f-aa1a-9a191cbc087f\"\n    name = \"iss2343b0.png\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    assert image_similarity(reader.pages[8].images[9].image, img) == 1.0\n\n\n@pytest.mark.enable_socket\ndef test_oserror():\n    \"\"\"Cf #2265\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/13127130/Binance.discovery.responses.2.gov.uscourts.dcd.256060.140.1.pdf\"\n    name = \"iss2265.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[2].images[1]\n    # Due to errors in translation in pillow we may not get\n    # the correct image. Therefore we cannot use `image_similarity`.\n\n\n@pytest.mark.parametrize(\n    (\"pdf\", \"pdf_name\", \"images\", \"images_name\", \"filtr\"),\n    [\n        (\n            \"https://github.com/py-pdf/pypdf/files/13127197/FTX.Claim.SC30.01072023101624File595287144.pdf\",\n            \"iss2266a.pdf\",\n            \"https://github.com/py-pdf/pypdf/files/14967061/iss2266a_images.zip\",\n            \"iss2266a_images.zip\",\n            ((0, 0), (1, 0), (4, 0), (9, 0)),  # random pick-up to speed up test\n        ),\n        (\n            \"https://github.com/py-pdf/pypdf/files/13127242/FTX.Claim.Skybridge.Capital.30062023113350File971325116.pdf\",\n            \"iss2266b.pdf\",\n            \"https://github.com/py-pdf/pypdf/files/14967099/iss2266b_images.zip\",\n            \"iss2266b_images.zip\",\n            ((0, 0), (1, 0), (4, 0), (9, 0)),  # random pick-up to speed up test\n        ),\n    ],\n)\n@pytest.mark.enable_socket\ndef test_corrupted_jpeg_iss2266(pdf, pdf_name, images, images_name, filtr):\n    \"\"\"\n    Code to create zipfile:\n    import pypdf;zipfile\n\n    with pypdf.PdfReader(\"____inputfile___\") as r:\n     with zipfile.ZipFile(\"__outputzip___\",\"w\") as z:\n      for p in r.pages:\n       for ii,i in enumerate(p.images):\n        print(i.name)\n        b=BytesIO()\n        i.image.save(b,\"JPEG\")\n        z.writestr(f\"image_{p.page_number}_{ii}_{i.name}\",b.getbuffer())\n    \"\"\"\n    url = pdf\n    name = pdf_name\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = images\n    name = images_name\n    print(pdf_name, images_name)  # noqa: T201\n    with ZipFile(BytesIO(get_data_from_url(url, name=name)), \"r\") as zf:\n        for fn in zf.namelist():\n            sp = fn.split(\"_\")\n            p, i = int(sp[1]), int(sp[2])\n            if filtr is not None and (p, i) not in filtr:\n                continue\n            print(fn)  # noqa: T201\n            img = Image.open(BytesIO(zf.read(fn)))\n            assert image_similarity(reader.pages[p].images[i].image, img) >= 0.99\n\n\n@pytest.mark.enable_socket\n@pytest.mark.timeout(30)\ndef test_large_compressed_image():\n    url = \"https://github.com/py-pdf/pypdf/files/15306199/file_with_large_compressed_image.pdf\"\n    reader = PdfReader(\n        BytesIO(get_data_from_url(url, name=\"file_with_large_compressed_image.pdf\"))\n    )\n    list(reader.pages[0].images)\n\n\n@pytest.mark.enable_socket\ndef test_ff_fe_starting_lut():\n    \"\"\"Cf issue #2660\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/15385628/original_before_merge.pdf\"\n    name = \"iss2660.pdf\"\n    writer = PdfWriter(BytesIO(get_data_from_url(url, name=name)))\n    b = BytesIO()\n    writer.write(b)\n    reader = PdfReader(b)\n    url = \"https://github.com/py-pdf/pypdf/assets/4083478/6150700d-87fd-43a2-8695-c2c05a44838c\"\n    name = \"iss2660.png\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    assert image_similarity(writer.pages[1].images[0].image, img) == 1.0\n    assert image_similarity(reader.pages[1].images[0].image, img) == 1.0\n\n\n@pytest.mark.enable_socket\ndef test_inline_image_extraction():\n    \"\"\"Cf #2598\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/14982414/lebo102.pdf\"\n    name = \"iss2598.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    # there is no error because images are correctly extracted\n    reader.pages[1].extract_text()\n    reader.pages[2].extract_text()\n    reader.pages[3].extract_text()\n\n    url = \"https://github.com/py-pdf/pypdf/files/15210011/Pages.62.73.from.0560-22_WSP.Plan_July.2022_Version.1.pdf\"\n    name = \"iss2598a.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].extract_text()\n    reader.pages[1].extract_text()\n\n    url = \"https://github.com/mozilla/pdf.js/raw/master/test/pdfs/issue14256.pdf\"\n    name = \"iss2598b.pdf\"\n    writer = PdfWriter(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/py-pdf/pypdf/assets/4083478/71bc5053-cfc7-44ba-b7be-8e2333e2c749\"\n    name = \"iss2598b.png\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    for i in range(8):\n        assert image_similarity(writer.pages[0].images[i].image, img) == 1\n    writer.pages[0].extract_text()\n    # check recalculation of inline images\n    assert writer.pages[0].inline_images is not None\n    writer.pages[0].merge_scaled_page(writer.pages[0], 0.25)\n    assert writer.pages[0].inline_images is None\n    reader = PdfReader(RESOURCE_ROOT / \"imagemagick-ASCII85Decode.pdf\")\n    writer.pages[0].merge_page(reader.pages[0])\n    assert list(writer.pages[0].images.keys()) == [\n        \"/Im0\",\n        \"~0~\",\n        \"~1~\",\n        \"~2~\",\n        \"~3~\",\n        \"~4~\",\n        \"~5~\",\n        \"~6~\",\n        \"~7~\",\n        \"~8~\",\n        \"~9~\",\n        \"~10~\",\n        \"~11~\",\n        \"~12~\",\n        \"~13~\",\n        \"~14~\",\n        \"~15~\",\n    ]\n\n    url = \"https://github.com/py-pdf/pypdf/files/15233597/bug1065245.pdf\"\n    name = \"iss2598c.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/py-pdf/pypdf/assets/4083478/bfb221be-11bd-46fe-8129-55a58088a4b6\"\n    name = \"iss2598c.jpg\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    assert image_similarity(reader.pages[0].images[0].image, img) >= 0.99\n\n    url = \"https://github.com/py-pdf/pypdf/files/15282904/tt.pdf\"\n    name = \"iss2598d.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/py-pdf/pypdf/assets/4083478/1a770e1b-9ad2-4125-89ae-6069992dda23\"\n    name = \"iss2598d.png\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    assert image_similarity(reader.pages[0].images[0].image, img) == 1\n\n\n@pytest.mark.enable_socket\ndef test_extract_image_from_object(caplog):\n    url = \"https://github.com/py-pdf/pypdf/files/15176076/B2.pdf\"\n    name = \"iss2613.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    image = reader.pages[0][\"/Resources\"][\"/Pattern\"][\"/P1\"][\"/Resources\"][\"/XObject\"][\n        \"/X1\"\n    ].decode_as_image()\n    assert isinstance(image, Image.Image)\n    with pytest.raises(Exception):\n        co = reader.pages[0].get_contents()\n        co.decode_as_image()\n    assert \"does not seem to be an Image\" in caplog.text\n    caplog.clear()\n    co.indirect_reference = \"for_test\"\n    with pytest.raises(Exception):\n        co = reader.pages[0].get_contents()\n        co.decode_as_image()\n    assert \"does not seem to be an Image\" in caplog.text\n\n\ndef test_extract_jpeg_with_explicit_quality():\n    reader = PdfReader(RESOURCE_ROOT / \"side-by-side-subfig.pdf\")\n    page = reader.pages[0]\n    x_object = page[\"/Resources\"][\"/XObject\"][\"/Im1\"]\n    assert x_object[\"/Filter\"] == \"/DCTDecode\"\n    image = x_object.decode_as_image()\n    assert isinstance(image, Image.Image)\n    assert image.format == \"JPEG\"\n    small_image = x_object.decode_as_image(pillow_parameters={\"quality\": 75})\n    assert image_size(small_image) < image_size(image)\n\n\n@pytest.mark.enable_socket\ndef test_4bits_images(caplog):\n    url = \"https://github.com/user-attachments/files/16624406/tt.pdf\"\n    name = \"iss2411.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/user-attachments/assets/53058564-9a28-4e4a-818f-a6528013d7dc\"\n    name = \"iss2411.png\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    assert image_similarity(reader.pages[0].images[1].image, img) == 1.0\n\n\n@pytest.mark.enable_socket\ndef test_no_filter_with_colorspace_as_list():\n    \"\"\"Tests for #2998\"\"\"\n    url = \"https://github.com/user-attachments/files/18058571/9bf7a2e2-72c8-4ac1-b8ae-164df16c8cef.pdf\"\n    name = \"iss2998.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    page = reader.pages[0]\n    page.images.items()\n\n\ndef test_contentstream__read_inline_image__fallback_is_successful():\n    stream = ContentStream(stream=None, pdf=None)\n    stream.set_data(\n        b\"\"\"Q\nq 9.6 0 0 4.8 5523.6 1031 cm\nBI\n/CS /RGB\n/W 2\n/H 1\n/BPC 8\nID \\x8b\\x8b\\x8b\\xfe\\xfe\\xfe\nEI Q\n/R413 gs\n        \"\"\"\n    )\n    page = PageObject(pdf=None)\n    with mock.patch.object(page, \"get_contents\", return_value=stream):\n        images = page._get_inline_images()\n        assert list(images) == [\"~0~\"]\n        assert images[\"~0~\"].data == (\n            b\"\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x00\\x02\\x00\\x00\\x00\\x01\\x08\\x02\\x00\\x00\\x00{@\\xe8\\xdd\\x00\\x00\\x00\\x0f\"\n            b\"IDATx\\x9cc\\xe8\\xee\\xee\\xfe\\xf7\\xef\\x1f\\x00\\x0e \\x04\\x9cpr_\\x96\\x00\\x00\\x00\\x00IEND\\xaeB`\\x82\"\n        )\n\n\n@pytest.mark.enable_socket\ndef test_inline_image_containing_ei_in_body():\n    \"\"\"Tests for #3107\"\"\"\n    expected = \"\"\"\\nID ><8d>£^H<8e><8b>¢AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA¡^BêMEI E^N^^<8a>^AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA\nAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA^D\n<8b>²: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA5>^D\né^EAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD<98>AAAAAA<8d><82>\nAAAAAAAA^B\nEI\\nQ\\n\"\"\".encode(\"latin1\")  # noqa: E501\n    url = \"https://github.com/user-attachments/files/18943249/testing.pdf\"\n    name = \"issue3107.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter(clone_from=reader)\n    for page in writer.pages:\n        page.transfer_rotation_to_content()\n    output = BytesIO()\n    writer.write(output)\n    assert expected in output.getvalue()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(condition=not JBIG2Decode._is_binary_compatible(), reason=\"Requires recent jbig2dec\")\ndef test_jbig2decode():\n    url = \"https://github.com/py-pdf/pypdf/files/12090692/New.Jersey.Coinbase.staking.securities.charges.2023-0606_Coinbase-Penalty-and-C-D.pdf\"\n    name = \"jbig2.pdf\"\n\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[0]\n    image = next(iter(page.images))\n    assert image.image.size == (5138, 6630)\n    assert image.image.mode == \"1\"\n    assert image.image.format == \"PNG\"\n\n    url = \"https://github.com/user-attachments/assets/d6f88c80-a2e0-4ea9-b1e0-34442041d004\"\n    name = \"jbig2.png\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n\n    assert image_similarity(image.image, img) >= 0.999\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(condition=not JBIG2Decode._is_binary_compatible(), reason=\"Requires recent jbig2dec\")\ndef test_jbig2decode__jbig2globals():\n    url = \"https://github.com/user-attachments/files/20119148/out.pdf\"\n    name = \"jbig2_globals.pdf\"\n\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[0]\n    image = next(iter(page.images))\n    assert image.image.size == (1067, 1067)\n    assert image.image.mode == \"1\"\n    assert image.image.format == \"PNG\"\n\n    url = \"https://github.com/user-attachments/assets/7ac41ee3-9c13-44cf-aa74-8f106287e354\"\n    name = \"jbig2_globals.png\"\n    img = Image.open(BytesIO(get_data_from_url(url, name=name)))\n\n    # Wrong image: 0.9618265964800714\n    assert image_similarity(image.image, img) >= 0.999\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(condition=not JBIG2Decode._is_binary_compatible(), reason=\"Requires recent jbig2dec\")\ndef test_jbig2decode__memory_limit():\n    url = \"https://github.com/py-pdf/pypdf/files/12090692/New.Jersey.Coinbase.staking.securities.charges.2023-0606_Coinbase-Penalty-and-C-D.pdf\"\n    name = \"jbig2.pdf\"\n    error_messages = [\n        # Version 0.20\n        (\n            r\"^Memory limit reached while reading JBIG2 data:\\n\"\n            r\"jbig2dec FATAL ERROR memory: limit reached: limit: 5000000 \\(4 Mbyte\\) used: 4329386 \\(4 Mbyte\\) allocation: 4263106 \\(4 Mbyte\\)\\n\"  # noqa: E501\n            r\"jbig2dec FATAL ERROR failed to allocate image data buffer \\(stride=643, height=6630\\)\"\n        ),\n        # Version 0.19\n        (\n            r\"^Memory limit reached while reading JBIG2 data:\\n\"\n            r\"jbig2dec FATAL ERROR failed to allocate image data buffer \\(stride=643, height=6630\\)\"\n        ),\n    ]\n\n    with mock.patch(\"pypdf.filters.JBIG2_MAX_OUTPUT_LENGTH\", 5_000_000):\n        reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n        page = reader.pages[0]\n        with pytest.raises(expected_exception=LimitReachedError, match=rf\"({'|'.join(error_messages)})\"):\n            _ = next(iter(page.images))\n\n\n@pytest.mark.enable_socket\ndef test_get_ids_image__resources_is_none():\n    url = \"https://github.com/user-attachments/files/18381726/tika-957721.pdf\"\n    name = \"tika-957721.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[2]\n    assert list(page.images.items()) == []\n"
  },
  {
    "path": "tests/test_javascript.py",
    "content": "\"\"\"Test topics around the usage of JavaScript in PDF documents.\"\"\"\nfrom typing import Any\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom tests import RESOURCE_ROOT\n\n\n@pytest.fixture\ndef pdf_file_writer():\n    reader = PdfReader(RESOURCE_ROOT / \"issue-604.pdf\")\n    writer = PdfWriter()\n    writer.append_pages_from_reader(reader)\n    return writer\n\n\ndef test_add_js(pdf_file_writer):\n    pdf_file_writer.add_js(\"this.print({bUI:true,bSilent:false,bShrinkToFit:true});\")\n\n    assert (\n        \"/Names\" in pdf_file_writer._root_object\n    ), \"add_js should add a name catalog in the root object.\"\n    assert (\n        \"/JavaScript\" in pdf_file_writer._root_object[\"/Names\"]\n    ), \"add_js should add a JavaScript name tree under the name catalog.\"\n\n\ndef test_added_js(pdf_file_writer):\n    def get_javascript_name() -> Any:\n        assert \"/Names\" in pdf_file_writer._root_object\n        assert \"/JavaScript\" in pdf_file_writer._root_object[\"/Names\"]\n        assert \"/Names\" in pdf_file_writer._root_object[\"/Names\"][\"/JavaScript\"]\n        return pdf_file_writer._root_object[\"/Names\"][\"/JavaScript\"][\"/Names\"][\n            -2\n        ]  # return -2 in order to get the latest javascript\n\n    pdf_file_writer.add_js(\"this.print({bUI:true,bSilent:false,bShrinkToFit:true});\")\n    first_js = get_javascript_name()\n\n    pdf_file_writer.add_js(\"this.print({bUI:true,bSilent:false,bShrinkToFit:true});\")\n    second_js = get_javascript_name()\n\n    assert (\n        first_js != second_js\n    ), \"add_js should add to the previous script in the catalog.\"\n"
  },
  {
    "path": "tests/test_merger.py",
    "content": "\"\"\"Test merging PDF functionality.\"\"\"\nfrom io import BytesIO\nfrom pathlib import Path\n\nimport pytest\n\nimport pypdf\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.generic import ArrayObject, Destination, DictionaryObject, Fit, NameObject, NullObject\n\nfrom . import RESOURCE_ROOT, get_data_from_url\nfrom .test_encryption import HAS_AES\n\n\ndef merger_operate(merger):\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    outline = RESOURCE_ROOT / \"pdflatex-outline.pdf\"\n    pdf_forms = RESOURCE_ROOT / \"pdflatex-forms.pdf\"\n    pdf_pw = RESOURCE_ROOT / \"libreoffice-writer-password.pdf\"\n\n    merger.append(pdf_path)\n    merger.append(outline)\n    merger.append(pdf_path, pages=pypdf.pagerange.PageRange(slice(0, 0)))\n    merger.append(pdf_forms)\n    merger.merge(0, pdf_path, import_outline=False)\n    with pytest.raises(NotImplementedError) as exc:\n        with open(pdf_path, \"rb\") as fp:\n            data = fp.read()\n        merger.append(data)\n    assert exc.value.args[0].startswith(\n        \"Merging requires an object that PdfReader can parse. \"\n        \"Typically, that is a Path\"\n    )\n\n    # Merging an encrypted file\n    reader = pypdf.PdfReader(pdf_pw)\n    reader.decrypt(\"openpassword\")\n    merger.append(reader)\n\n    # PdfReader object:\n    r = pypdf.PdfReader(pdf_path)\n    merger.append(r, outline_item=\"foo\", pages=list(range(len(r.pages))))\n\n    # File handle\n    with open(pdf_path, \"rb\") as fh:\n        merger.append(fh)\n\n    # to force to build outlines and ensure the add_outline_item is\n    # at end of the list\n    merger.write(BytesIO())\n    outline_item = merger.add_outline_item(\"An outline item\", 0)\n    oi2 = merger.add_outline_item(\n        \"deeper\", 0, parent=outline_item, italic=True, bold=True\n    )\n    merger.add_outline_item(\n        \"Let's see\", 2, oi2, (255, 255, 0), True, True, Fit.fit_box_vertically(left=12)\n    )\n    merger.add_outline_item(\n        \"The XYZ fit\",\n        0,\n        outline_item,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.xyz(left=10, top=20, zoom=3),\n    )\n    merger.add_outline_item(\n        \"The FitH fit\",\n        0,\n        outline_item,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit_horizontally(top=10),\n    )\n    merger.add_outline_item(\n        \"The FitV fit\",\n        0,\n        outline_item,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit_vertically(left=10),\n    )\n    merger.add_outline_item(\n        \"The FitR fit\",\n        0,\n        outline_item,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit_rectangle(left=10, bottom=20, right=30, top=40),\n    )\n    merger.add_outline_item(\n        \"The FitB fit\", 0, outline_item, (255, 0, 15), True, True, Fit.fit_box()\n    )\n    merger.add_outline_item(\n        \"The FitBH fit\",\n        0,\n        outline_item,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit_box_horizontally(top=10),\n    )\n    merger.add_outline_item(\n        \"The FitBV fit\",\n        0,\n        outline_item,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit_box_vertically(left=10),\n    )\n\n    found_oi = merger.find_outline_item(\"nothing here\")\n    assert found_oi is None\n\n    found_oi = merger.find_outline_item(\"foo\")\n    assert found_oi == [9]\n\n    merger.add_metadata({\"/Author\": \"Martin Thoma\"})\n    merger.add_named_destination(\"/Title\", 0)\n    merger.set_page_layout(\"/SinglePage\")\n    merger.page_mode = \"/UseThumbs\"\n\n\ndef check_outline(tmp_path):\n    # Check if outline is correct\n    reader = pypdf.PdfReader(tmp_path)\n    assert [el.title for el in reader.outline if isinstance(el, Destination)] == [\n        \"Foo\",\n        \"Bar\",\n        \"Baz\",\n        \"Foo\",\n        \"Bar\",\n        \"Baz\",\n        \"Foo\",\n        \"Bar\",\n        \"Baz\",\n        \"foo\",\n        \"An outline item\",  # this has been moved to end normal???\n    ]\n\n    # TODO: There seem to be no destinations for those links?\n\n\ntmp_filename = \"dont_commit_merged.pdf\"\n\n\ndef test_merger_operations_by_traditional_usage_with_writer(tmp_path):\n    # Arrange\n    merger = PdfWriter()\n    merger_operate(merger)\n    path = tmp_path / tmp_filename\n\n    # Act\n    merger.write(path)\n    merger.close()\n    # Assert\n    check_outline(path)\n\n\ndef test_merger_operations_by_semi_traditional_usage_with_writer(tmp_path):\n    path = tmp_path / tmp_filename\n\n    with PdfWriter() as merger:\n        merger_operate(merger)\n        merger.write(path)  # Act\n\n    # Assert\n    assert Path(path).is_file()\n    check_outline(path)\n\n\ndef test_merger_operation_by_new_usage_with_writer(tmp_path):\n    path = tmp_path / tmp_filename\n    with PdfWriter(fileobj=path) as merger:\n        merger_operate(merger)\n\n    # Assert\n    assert Path(path).is_file()\n    check_outline(path)\n\n\ndef test_merge_page_exception_with_writer():\n    merger = pypdf.PdfWriter()\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    with pytest.raises(TypeError) as exc:\n        merger.merge(0, pdf_path, pages=\"a:b\")\n    assert (\n        exc.value.args[0]\n        == '\"pages\" must be a tuple of (start, stop[, step]) or a list'\n    )\n    merger.close()\n\n\ndef test_merge_page_tuple_with_writer():\n    merger = pypdf.PdfWriter()\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    merger.merge(0, pdf_path, pages=(0, 1))\n    merger.close()\n\n\ndef test_merge_write_closed_fh_with_writer(pdf_file_path):\n    merger = pypdf.PdfWriter()\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    merger.append(pdf_path)\n\n    merger.close()\n    merger.write(pdf_file_path)\n    merger.add_metadata({\"author\": \"Martin Thoma\"})\n    merger.set_page_layout(\"/SinglePage\")\n    merger.page_mode = \"/UseNone\"\n    merger.add_outline_item(\"An outline item\", 0)\n\n\n@pytest.mark.enable_socket\ndef test_trim_outline_list_with_writer(pdf_file_path):\n    url = \"https://github.com/user-attachments/files/18381771/tika-995175.pdf\"\n    name = \"tika-995175.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.add_outline_item_dict(merger.outline[0])\n    merger.write(pdf_file_path)\n    merger.close()\n\n\n@pytest.mark.enable_socket\ndef test_zoom_with_writer(pdf_file_path):\n    url = \"https://github.com/user-attachments/files/18381769/tika-994759.pdf\"\n    name = \"tika-994759.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n    merger.close()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_zoom_xyz_no_left_with_add_page(pdf_file_path):\n    url = \"https://github.com/user-attachments/files/18381704/tika-933322.pdf\"\n    name = \"tika-933322.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    for p in reader.pages:\n        merger.add_page(p)\n    merger.write(pdf_file_path)\n    merger.close()\n\n\n@pytest.mark.enable_socket\ndef test_zoom_xyz_no_left_with_writer(pdf_file_path):\n    url = \"https://github.com/user-attachments/files/18381704/tika-933322.pdf\"\n    name = \"tika-933322.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n    merger.close()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\ndef test_outline_item_with_writer(pdf_file_path):\n    url = \"https://github.com/user-attachments/files/18381773/tika-997511.pdf\"\n    name = \"tika-997511.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n    merger.close()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\ndef test_trim_outline_with_writer(pdf_file_path):\n    url = \"https://github.com/user-attachments/files/18381759/tika-982336.pdf\"\n    name = \"tika-982336.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n    merger.close()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\ndef test1_with_writer(pdf_file_path):\n    url = \"https://github.com/user-attachments/files/18381696/tika-923621.pdf\"\n    name = \"tika-923621.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n    merger.close()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\ndef test_sweep_recursion1_with_writer(pdf_file_path):\n    # TODO: This test looks like an infinite loop.\n    url = \"https://github.com/user-attachments/files/18381697/tika-924546.pdf\"\n    name = \"tika-924546.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n    merger.close()\n\n    reader2 = PdfReader(pdf_file_path)\n    reader2.pages\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            # TODO: This test looks like an infinite loop.\n            \"https://github.com/user-attachments/files/18381700/tika-924794.pdf\",\n            \"tika-924794.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381697/tika-924546.pdf\",\n            \"tika-924546.pdf\",\n        ),\n    ],\n)\ndef test_sweep_recursion2_with_writer(url, name, pdf_file_path):\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n    merger.close()\n\n    reader2 = PdfReader(pdf_file_path)\n    reader2.pages\n\n\n@pytest.mark.enable_socket\ndef test_sweep_indirect_list_newobj_is_none_with_writer(caplog, pdf_file_path):\n    url = \"https://github.com/user-attachments/files/18381681/tika-906769.pdf\"\n    name = \"tika-906769.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n    merger.close()\n    # used to be: assert \"Object 21 0 not defined.\" in caplog.text\n\n    reader2 = PdfReader(pdf_file_path)\n    reader2.pages\n\n\n@pytest.mark.enable_socket\ndef test_iss1145_with_writer():\n    # issue with FitH destination with null param\n    url = \"https://github.com/py-pdf/pypdf/files/9164743/file-0.pdf\"\n    name = \"iss1145.pdf\"\n    merger = PdfWriter()\n    merger.append(PdfReader(BytesIO(get_data_from_url(url, name=name))))\n    merger.close()\n\n\n@pytest.mark.enable_socket\ndef test_iss1344_with_writer(caplog):\n    url = \"https://github.com/py-pdf/pypdf/files/9549001/input.pdf\"\n    name = \"iss1344.pdf\"\n    m = PdfWriter()\n    m.append(PdfReader(BytesIO(get_data_from_url(url, name=name))))\n    b = BytesIO()\n    m.write(b)\n    p = PdfReader(b).pages[0]\n    assert \"/DIJMAC+Arial Black\" in p._debug_for_extract()\n    assert \"adresse où le malade peut être visité\" in p.extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_articles_with_writer(caplog):\n    url = \"https://github.com/user-attachments/files/18381699/tika-924666.pdf\"\n    name = \"924666.pdf\"\n    m = PdfWriter()\n    m.append(PdfReader(BytesIO(get_data_from_url(url, name=name))), (2, 10))\n    b = BytesIO()\n    m.write(b)\n    r = PdfReader(b)\n    assert len(r.threads) == 4\n    assert r.threads[0].get_object()[\"/F\"][\"/P\"] == r.pages[0]\n\n\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\n@pytest.mark.enable_socket\ndef test_null_articles_with_writer():\n    data = get_data_from_url(name=\"issue-3508.pdf\")\n    merger = PdfWriter()\n    merger.append(BytesIO(data))\n    assert len(merger.pages) == 98\n\n\ndef test_get_reference():\n    writer = PdfWriter(RESOURCE_ROOT / \"crazyones.pdf\")\n    assert writer.get_reference(writer.pages[0]) == writer.pages[0].indirect_reference\n\n\n@pytest.mark.enable_socket\ndef test_direct_link_preserved(pdf_file_path):\n    # this could be any PDF -- we don't care which\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss3268.pdf\")))\n    writer = PdfWriter(clone_from=reader)\n\n    # this PDF has a direct link from p1 to p2\n    merger = PdfReader(BytesIO(get_data_from_url(name=\"direct-link.pdf\")))\n    for p in merger.pages:\n        writer.add_page(p)\n\n    writer.write(pdf_file_path)\n\n    check = PdfReader(pdf_file_path)\n    page3 = check.pages[2]\n    link = page3[\"/Annots\"][0].get_object()\n    assert link[\"/Subtype\"] == \"/Link\"\n    dest = link[\"/Dest\"][0]  # indirect reference of page referred to\n\n    page4 = check.flattened_pages[3]\n    assert dest == page4.indirect_reference, \"Link from page 3 to page 4 is broken\"\n\n\n@pytest.mark.enable_socket\ndef test_direct_link_preserved_reordering(pdf_file_path):\n    # this could be any PDF -- we don't care which\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss3268.pdf\")))\n    writer = PdfWriter(clone_from=reader)\n\n    # this PDF has a direct link from p1 to p2\n    merger = PdfReader(BytesIO(get_data_from_url(name=\"direct-link.pdf\")))\n    for p in merger.pages:\n        writer.add_page(p)\n\n    # let's insert a page to mess up the page order\n    writer.insert_page(reader.pages[0], 3)\n\n    writer.write(pdf_file_path)\n\n    check = PdfReader(pdf_file_path)\n    page3 = check.pages[2]\n    link = page3[\"/Annots\"][0].get_object()\n    assert link[\"/Subtype\"] == \"/Link\"\n    dest = link[\"/Dest\"][0]  # indirect reference of page referred to\n\n    page5 = check.flattened_pages[4]  # it moved one out\n    assert dest == page5.indirect_reference, \"Link from page 3 to page 5 is broken\"\n\n\n@pytest.mark.enable_socket\ndef test_direct_link_page_missing(pdf_file_path):\n    # this could be any PDF -- we don't care which\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss3268.pdf\")))\n    writer = PdfWriter(clone_from=reader)\n\n    # this PDF has a direct link from p1 to p2\n    merger = PdfReader(BytesIO(get_data_from_url(name=\"direct-link.pdf\")))\n    writer.add_page(merger.pages[0])\n    # but we're not adding page 2\n\n    writer.write(pdf_file_path)  # verify nothing crashes\n\n\n@pytest.mark.enable_socket\ndef test_named_reference_preserved(pdf_file_path):\n    # this could be any PDF -- we don't care which\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss3268.pdf\")))\n    writer = PdfWriter(clone_from=reader)\n\n    # this PDF has a named reference from from p3 to p5\n    merger = PdfReader(BytesIO(get_data_from_url(name=\"named-reference.pdf\")))\n    for p in merger.pages:\n        writer.add_page(p)\n\n    writer.write(pdf_file_path)\n\n    check = PdfReader(pdf_file_path)\n    page5 = check.pages[4]\n    page7 = check.flattened_pages[6]\n    for link in page5[\"/Annots\"]:\n        action = link[\"/A\"]\n        assert action.get(\"/S\") == \"/GoTo\"\n        dest = str(action[\"/D\"])\n        assert dest in check.named_destinations\n        pref = check.named_destinations[dest].page\n\n        assert pref == page7.indirect_reference, \"Link from page 5 to page 7 is broken\"\n\n\n@pytest.mark.enable_socket\ndef test_named_ref_to_page_that_is_gone(pdf_file_path):\n    source = PdfReader(BytesIO(get_data_from_url(name=\"named-reference.pdf\")))\n    buf = BytesIO()\n    tmp = PdfWriter()\n    tmp.add_page(source.pages[2])  # we add only the page with the reference\n    tmp.write(buf)\n\n    source = PdfReader(buf)\n\n    writer = PdfWriter()\n    writer.add_page(source.pages[0])  # now references to non-existent page\n    writer.write(pdf_file_path)  # don't crash\n\n\ndef test_merge__null_destination():\n    \"\"\"Tests for issue #3444.\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n    writer2 = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n\n    annotation = DictionaryObject()\n    annotation[NameObject(\"/Subtype\")] = NameObject(\"/Link\")\n    a = DictionaryObject()\n    annotation[NameObject(\"/A\")] = a\n    a[NameObject(\"/S\")] = NameObject(\"/GoTo\")\n\n    target = NullObject()\n    a[NameObject(\"/D\")] = writer._add_object(target)\n\n    annots = ArrayObject([annotation])\n    page = writer2.pages[0]\n    page[NameObject(\"/Annots\")] = annots\n\n    data = BytesIO()\n    writer2.write(data)\n    data.seek(0)\n\n    writer.merge(position=1, fileobj=data)\n    assert writer.pages[0].annotations is None\n"
  },
  {
    "path": "tests/test_page.py",
    "content": "\"\"\"Test the pypdf._page module.\"\"\"\nimport json\nimport math\nimport os\nimport re\nimport shutil\nimport subprocess\nimport sys\nfrom copy import deepcopy\nfrom io import BytesIO\nfrom pathlib import Path\nfrom random import shuffle\nfrom typing import Any\nfrom unittest import mock\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter, Transformation\nfrom pypdf._page import PageObject\nfrom pypdf.constants import PageAttributes\nfrom pypdf.constants import PageAttributes as PG\nfrom pypdf.errors import PdfReadError, PdfReadWarning, PyPdfError\nfrom pypdf.generic import (\n    ArrayObject,\n    ContentStream,\n    DictionaryObject,\n    FloatObject,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    RectangleObject,\n    TextStringObject,\n)\n\nfrom . import RESOURCE_ROOT, SAMPLE_ROOT, get_data_from_url, normalize_warnings\nfrom .test_images import image_similarity\nfrom .utils import extract_cell_text, extract_table, extract_text_and_rectangles\n\nGHOSTSCRIPT_BINARY = shutil.which(\"gs\")\n\n\ndef get_all_sample_files():\n    meta_file = SAMPLE_ROOT / \"files.json\"\n    if not Path(meta_file).is_file():\n        return {\"data\": []}\n    with open(meta_file) as fp:\n        data = fp.read()\n    return json.loads(data)\n\n\nall_files_meta = get_all_sample_files()\n\n\n@pytest.mark.samples\n@pytest.mark.parametrize(\n    \"meta\",\n    [m for m in all_files_meta[\"data\"] if not m[\"encrypted\"]],\n    ids=[m[\"path\"] for m in all_files_meta[\"data\"] if not m[\"encrypted\"]],\n)\n@pytest.mark.filterwarnings(\"ignore::pypdf.errors.PdfReadWarning\")\ndef test_read(meta):\n    pdf_path = SAMPLE_ROOT / meta[\"path\"]\n    reader = PdfReader(pdf_path)\n    try:\n        reader.pages[0]\n    except Exception:\n        return\n    assert len(reader.pages) == meta[\"pages\"]\n\n\n@pytest.mark.samples\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"pdf_path\", \"password\"),\n    [\n        (\"crazyones.pdf\", None),\n        (\"attachment.pdf\", None),\n        (\n            \"libreoffice-writer-password.pdf\",\n            \"openpassword\",\n        ),\n        (\"imagemagick-images.pdf\", None),\n        (\"imagemagick-lzw.pdf\", None),\n        (\"reportlab-inline-image.pdf\", None),\n        (\"https://arxiv.org/pdf/2201.00029.pdf\", None),\n    ],\n)\ndef test_page_operations(pdf_path, password):\n    \"\"\"\n    This test just checks if the operation throws an exception.\n\n    This should be done way more thoroughly: It should be checked if the output\n    is as expected.\n    \"\"\"\n    if pdf_path.startswith(\"http\"):\n        pdf_path = BytesIO(get_data_from_url(pdf_path, pdf_path.split(\"/\")[-1]))\n    else:\n        pdf_path = RESOURCE_ROOT / pdf_path\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter()\n\n    if password:\n        reader.decrypt(password)\n\n    writer.clone_document_from_reader(reader)\n    page: PageObject = writer.pages[0]\n\n    t = Transformation().translate(50, 100).rotate(90)\n    assert abs(t.ctm[4] + 100) < 0.01\n    assert abs(t.ctm[5] - 50) < 0.01\n\n    transformation = (\n        Transformation()\n        .rotate(90)\n        .scale(1)\n        .translate(1, 1)\n        .transform(Transformation((1, 0, 0, -1, 0, 0)))\n    )\n    page.add_transformation(transformation, expand=True)\n    page.add_transformation((1, 0, 0, 0, 0, 0))\n    page.scale(2, 2)\n    page.scale_by(0.5)\n    page.scale_to(100, 100)\n    page.compress_content_streams()\n    page.extract_text()\n    page.scale_by(0.5)\n    page.scale_to(100, 100)\n    page.extract_text()\n\n\n@pytest.mark.parametrize(\n    (\"angle\", \"expected_width\", \"expected_height\"),\n    [\n        (175, 680, 844),\n        (45, 994, 994),\n        (-80, 888, 742),\n    ],\n)\ndef test_mediabox_expansion_after_rotation(\n    angle: float, expected_width: int, expected_height: int\n):\n    \"\"\"\n    Mediabox dimensions after rotation at a non-right angle with expansion are correct.\n\n    The test was validated against pillow (see PR #2282)\n    \"\"\"\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    writer = PdfWriter(clone_from=pdf_path)\n\n    transformation = Transformation().rotate(angle)\n    for page_box in writer.pages:\n        page_box.add_transformation(transformation, expand=True)\n\n    mediabox = writer.pages[0].mediabox\n\n    # Deviation of up to 2 pixels is acceptable\n    assert math.isclose(mediabox.width, expected_width, abs_tol=2)\n    assert math.isclose(mediabox.height, expected_height, abs_tol=2)\n\n\ndef test_transformation_equivalence():\n    pdf_path = RESOURCE_ROOT / \"labeled-edges-center-image.pdf\"\n    writer_base = PdfWriter(clone_from=pdf_path)\n    page_base = writer_base.pages[0]\n\n    pdf_path = RESOURCE_ROOT / \"box.pdf\"\n    writer_add = PdfWriter(clone_from=pdf_path)\n    page_box = writer_add.pages[0]\n\n    op = Transformation().scale(2).rotate(45)\n\n    # Option 1: The new way\n    page_box1 = deepcopy(page_box)\n    page_base1 = deepcopy(page_base)\n    page_box1.add_transformation(op, expand=True)\n    page_base1.merge_page(page_box1, expand=False)\n\n    # Option 2: The old way\n    page_box2 = deepcopy(page_box)\n    page_base2 = deepcopy(page_base)\n    page_base2.merge_transformed_page(page_box2, op, expand=False)\n    page_box2.add_transformation(op)\n    page_base2.merge_page(page_box2)\n\n    # Should be the same\n    assert page_base1[NameObject(PG.CONTENTS)] == page_base2[NameObject(PG.CONTENTS)]\n    assert page_base1.mediabox == page_base2.mediabox\n    assert page_base1.trimbox == page_base2.trimbox\n    assert page_base1.get(NameObject(PG.ANNOTS)) == page_base2.get(NameObject(PG.ANNOTS))\n    compare_dict_objects(\n        page_base1[NameObject(PG.RESOURCES)], page_base2[NameObject(PG.RESOURCES)]\n    )\n\n\ndef test_transformation_equivalence2():\n    pdf_path = RESOURCE_ROOT / \"labeled-edges-center-image.pdf\"\n    reader_base = PdfReader(pdf_path)\n\n    pdf_path = RESOURCE_ROOT / \"box.pdf\"\n    reader_add = PdfReader(pdf_path)\n\n    writer = PdfWriter()\n    writer.append(reader_base)\n    writer.pages[0].merge_transformed_page(\n        reader_add.pages[0], Transformation().scale(2).rotate(-45), False, False\n    )\n    writer.pages[0].merge_transformed_page(\n        reader_add.pages[0], Transformation().scale(2).translate(100, 100), True, False\n    )\n    # No special assert: the test should be visual in a viewer; 2 box with a arrow rotated and translated\n\n    writer = PdfWriter()\n    writer.append(reader_add)\n    writer.pages[0].merge_transformed_page(\n        reader_base.pages[0], Transformation(), True, True\n    )\n    # No special assert: Visual check the page has been increased and all is visible (box + graph)\n\n    writer = PdfWriter()\n    writer.append(reader_add)\n    height = reader_add.pages[0].mediabox.height\n    writer.pages[0].merge_transformed_page(\n        reader_base.pages[0],\n        Transformation().transform(Transformation((1, 0, 0, -1, 0, height))),\n        False,\n        False,\n    )\n    # No special assert: Visual check the page has been increased and all is visible (box + graph)\n\n    pdf_path = RESOURCE_ROOT / \"commented-xmp.pdf\"\n    reader_comments = PdfReader(pdf_path)\n\n    writer = PdfWriter()\n    writer.append(reader_base)\n    writer.pages[0].merge_transformed_page(\n        reader_comments.pages[0], Transformation().rotate(-15), True, True\n    )\n    nb_annots1 = len(writer.pages[0][\"/Annots\"])\n    writer.pages[0].merge_transformed_page(\n        reader_comments.pages[0], Transformation().rotate(-30), True, True\n    )\n    assert len(writer.pages[0][\"/Annots\"]) == 2 * nb_annots1\n    # No special assert: Visual check the overlay has its comments at the good position\n\n\ndef test_get_user_unit_property():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    assert reader.pages[0].user_unit == 1\n\n\ndef compare_dict_objects(d1, d2):\n    assert sorted(d1.keys()) == sorted(d2.keys())\n    for key in d1:\n        if isinstance(d1[key], DictionaryObject):\n            compare_dict_objects(d1[key], d2[key])\n        else:\n            assert d1[key] == d2[key]\n\n\n@pytest.mark.slow\ndef test_page_transformations():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    writer = PdfWriter(clone_from=pdf_path)\n\n    page: PageObject = writer.pages[0]\n    page.merge_rotated_page(page, 90, expand=True)\n\n    op = Transformation().rotate(90).scale(1, 1)\n    page.merge_transformed_page(page, op, expand=True)\n\n    op = Transformation().rotate(90).scale(1, 1).translate(1, 1)\n    page.merge_transformed_page(page, op, expand=True)\n\n    op = Transformation().translate(-100, -100).rotate(90).translate(100, 100)\n    page.merge_transformed_page(page, op, expand=False)\n\n    page.merge_scaled_page(page, 2, expand=False)\n\n    op = Transformation().scale(1, 1).translate(1, 1)\n    page.merge_transformed_page(page, op)\n\n    page.merge_translated_page(page, 100, 100, expand=False)\n    page.add_transformation((1, 0, 0, 0, 0, 0))\n\n\n@pytest.mark.parametrize(\n    (\"pdf_path\", \"password\"),\n    [\n        (RESOURCE_ROOT / \"crazyones.pdf\", None),\n        (RESOURCE_ROOT / \"attachment.pdf\", None),\n        (RESOURCE_ROOT / \"side-by-side-subfig.pdf\", None),\n        (\n            RESOURCE_ROOT / \"libreoffice-writer-password.pdf\",\n            \"openpassword\",\n        ),\n    ],\n)\ndef test_compress_content_streams(pdf_path, password):\n    reader = PdfReader(pdf_path)\n\n    writer = PdfWriter()\n    if password:\n        reader.decrypt(password)\n    for i, page in enumerate(reader.pages):\n        assert i == page.page_number\n\n    assert isinstance(reader.pages[0].get_contents(), ContentStream)\n    writer.clone_document_from_reader(reader)\n    assert isinstance(writer.pages[0].get_contents(), ContentStream)\n    for i, page in enumerate(writer.pages):\n        assert i == page.page_number\n        page.compress_content_streams()\n\n    # test from reader should fail as adding_object out of\n    # PdfWriter not possible\n    with pytest.raises(ValueError):\n        reader.pages[0].compress_content_streams()\n\n\ndef test_page_properties():\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    page = reader.pages[0]\n    assert page.mediabox == RectangleObject((0, 0, 612, 792))\n    assert page.cropbox == RectangleObject((0, 0, 612, 792))\n    assert page.bleedbox == RectangleObject((0, 0, 612, 792))\n    assert page.trimbox == RectangleObject((0, 0, 612, 792))\n    assert page.artbox == RectangleObject((0, 0, 612, 792))\n\n    page.bleedbox = RectangleObject((0, 1, 100, 101))\n    assert page.bleedbox == RectangleObject((0, 1, 100, 101))\n\n\ndef test_page_rotation():\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n    page = writer.pages[0]\n    with pytest.raises(ValueError) as exc:\n        page.rotate(91)\n    assert exc.value.args[0] == \"Rotation angle must be a multiple of 90\"\n\n    # test rotation\n    assert page.rotation == 0\n    page.rotation = 180\n    assert page.rotation == 180\n    page.rotation += 190\n    assert page.rotation == 0\n\n    # test transfer_rotate_to_content\n    page.rotation -= 90\n    page.transfer_rotation_to_content()\n    assert math.isclose(page.mediabox.left, 0, abs_tol=0.1)\n    assert math.isclose(page.mediabox.bottom, 0, abs_tol=0.1)\n    assert math.isclose(page.mediabox.right, 792, abs_tol=0.1)\n    assert math.isclose(page.mediabox.top, 612, abs_tol=0.1)\n\n\ndef test_page_indirect_rotation():\n    reader = PdfReader(RESOURCE_ROOT / \"indirect-rotation.pdf\")\n    page = reader.pages[0]\n\n    # test rotation\n    assert page.rotation == 0\n\n\ndef test_page_scale():\n    op = Transformation()\n    with pytest.raises(ValueError) as exc:\n        op.scale()\n    assert exc.value.args[0] == \"Either sx or sy must be specified\"\n\n    assert op.scale(sx=2).ctm == (2, 0, 0, 2, 0, 0)\n    assert op.scale(sy=3).ctm == (3, 0, 0, 3, 0, 0)\n\n\ndef test_add_transformation_on_page_without_contents():\n    page = PageObject()\n    assert page.get_contents() is None\n    page.add_transformation(Transformation())\n    page[NameObject(\"/Contents\")] = ContentStream(None, None)\n    assert isinstance(page.get_contents(), ContentStream)\n\n\n@pytest.mark.enable_socket\ndef test_iss_1142():\n    # check fix for problem of context save/restore (q/Q)\n    url = \"https://github.com/py-pdf/pypdf/files/9150656/ST.2019.PDF\"\n    name = \"st2019.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    txt = reader.pages[3].extract_text()\n    # The following text is contained in two different cells:\n    assert txt.find(\"有限公司\") > 0\n    assert txt.find(\"郑州分公司\") > 0\n    # 有限公司 = limited company\n    # 郑州分公司 = branch office in Zhengzhou\n    # First cell (see page 4/254):\n    assert txt.find(\"郑州药素电子商务有限公司\") > 0\n    # Next cell (first cell in next line):\n    assert txt.find(\"郑州分公司\") > 0\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        # keyerror_potentially_empty_page\n        (\n            \"https://github.com/user-attachments/files/18381736/tika-964029.pdf\",\n            \"tika-964029.pdf\",\n        ),\n        # 1140 / 1141:\n        (\n            \"https://github.com/user-attachments/files/18381702/tika-932446.pdf\",\n            \"tika-932446.pdf\",\n        ),\n        # iss 1134:\n        (\n            \"https://github.com/py-pdf/pypdf/files/9150656/ST.2019.PDF\",\n            \"iss_1134.pdf\",\n        ),\n        # iss 1:\n        (\n            \"https://github.com/py-pdf/pypdf/files/9432350/Work.Flow.From.Check.to.QA.pdf\",\n            \"WFCA.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381736/tika-964029.pdf\",\n            \"tika-964029.pdf\",\n        ),  # single_quote_op\n        (\n            \"https://github.com/py-pdf/pypdf/files/9428434/TelemetryTX_EM.pdf\",\n            \"tika-964029.pdf\",\n        ),  # no_resources\n        (\n            # https://www.itu.int/rec/T-REC-X.25-199610-I/en\n            \"https://github.com/py-pdf/pypdf/files/12423313/T-REC-X.25-199610-I.PDF-E.pdf\",\n            \"T-REC-X.25-199610-I!!PDF-E.pdf\",\n        ),\n    ],\n)\ndef test_extract_text(url, name):\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    for page in reader.pages:\n        page.extract_text()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\ndef test_extract_text_page_pdf_impossible_decode_xform(caplog):\n    url = \"https://github.com/user-attachments/files/18381748/tika-972962.pdf\"\n    name = \"tika-972962.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    for page in reader.pages:\n        page.extract_text()\n    warn_msgs = normalize_warnings(caplog.text)\n    assert warn_msgs == [\"\"]  # text extraction recognise no text\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\ndef test_extract_text_operator_t_star():  # L1266, L1267\n    url = \"https://github.com/user-attachments/files/18381740/tika-967943.pdf\"\n    name = \"tika-967943.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    for page in reader.pages:\n        page.extract_text()\n\n\ndef test_extract_text_visitor_callbacks():\n    \"\"\"\n    Extract text in rectangle-objects or simple tables.\n\n    This test uses GeoBase_NHNC1_Data_Model_UML_EN.pdf.\n    It extracts the labels of package-boxes in Figure 2.\n    It extracts the texts in table \"REVISION HISTORY\".\n    \"\"\"\n    # Test 1: We test the analysis of page 7 \"2.1 LRS model\".\n    reader = PdfReader(RESOURCE_ROOT / \"GeoBase_NHNC1_Data_Model_UML_EN.pdf\")\n    page_lrs_model = reader.pages[6]\n\n    # We ignore the invisible large rectangles.\n    def ignore_large_rectangles(r) -> bool:\n        return r.w < 400 and r.h < 400\n\n    (texts, rectangles) = extract_text_and_rectangles(\n        page_lrs_model, rect_filter=ignore_large_rectangles\n    )\n\n    # We see ten rectangles (5 tabs, 5 boxes) but there are 64 rectangles\n    # (including some invisible ones).\n    assert len(rectangles) == 60\n    rectangle2texts = {}\n    for t in texts:\n        for r in rectangles:\n            if r.contains(t.x, t.y):\n                texts = rectangle2texts.setdefault(r, [])\n                texts.append(t.text.strip())\n                break\n    # Five boxes and the figure-description below.\n    assert len(rectangle2texts) == 6\n    box_texts = [\" \".join(texts) for texts in rectangle2texts.values()]\n    assert \"Hydro Network\" in box_texts\n    assert \"Hydro Events\" in box_texts\n    assert \"Metadata\" in box_texts\n    assert \"Hydrography\" in box_texts\n    assert \"Toponymy (external model)\" in box_texts\n\n    # Test 2: Parse table \"REVISION HISTORY\" on page 3.\n    page_revisions = reader.pages[2]\n    # We ignore the second table, therefore: r.y > 350\n\n    def filter_first_table(r) -> bool:\n        return r.w > 1 and r.h > 1 and r.w < 400 and r.h < 400 and r.y > 350\n\n    (texts, rectangles) = extract_text_and_rectangles(\n        page_revisions, rect_filter=filter_first_table\n    )\n    rows = extract_table(texts, rectangles)\n\n    assert len(rows) == 9\n    assert extract_cell_text(rows[0][0]) == \"Date\"\n    assert extract_cell_text(rows[0][1]) == \"Version\"\n    assert extract_cell_text(rows[0][2]) == \"Description\"\n    assert extract_cell_text(rows[1][0]) == \"September 2002\"\n    # The line break between \"English review;\"\n    # and \"Remove\" is not detected.\n    assert (\n        extract_cell_text(rows[6][2])\n        == \"English review;Remove the UML model for the Segmented view.\"\n    )\n    assert extract_cell_text(rows[7][2]) == \"Update from the March Workshop comments.\"\n\n    # Check the fonts. We check: /F2 9.96 Tf [...] [(Dat)-2(e)] TJ\n    text_dat_of_date = rows[0][0][0]\n    assert text_dat_of_date.font_dict is not None\n    assert text_dat_of_date.font_dict[\"/Name\"] == \"/F2\"\n    assert text_dat_of_date.get_base_font() == \"/Arial,Bold\"\n    assert text_dat_of_date.font_dict[\"/Encoding\"] == \"/WinAnsiEncoding\"\n    assert text_dat_of_date.font_size == 9.96\n    # Check: /F1 9.96 Tf [...] [(S)4(ep)4(t)-10(em)-20(be)4(r)-3( 20)4(02)] TJ\n    texts = rows[1][0][0]\n    assert texts.font_dict is not None\n    assert texts.font_dict[\"/Name\"] == \"/F1\"\n    assert texts.get_base_font() == \"/Arial\"\n    assert texts.font_dict[\"/Encoding\"] == \"/WinAnsiEncoding\"\n    assert text_dat_of_date.font_size == 9.96\n\n    # Test 3: Read a table in a document using a non-translating\n    #         but scaling Tm-operand\n    reader = PdfReader(RESOURCE_ROOT / \"Sample_Td-matrix.pdf\")\n    page_td_model = reader.pages[0]\n    # We store the translations of the Td-executions.\n    list_td = []\n\n    def visitor_td(op, args, cm, tm) -> None:\n        if op == b\"Td\":\n            list_td.append((tm[4], tm[5]))\n\n    page_td_model.extract_text(visitor_operand_after=visitor_td)\n    assert len(list_td) == 4\n    # Check the translations of the four Td-executions.\n    assert list_td[0] == (210.0, 110.0)\n    assert list_td[1] == (410.0, 110.0)\n    assert list_td[2] == (210.0, 210.0)\n    assert list_td[3] == (410.0, 210.0)\n\n\n@pytest.mark.parametrize(\n    (\"pdf_path\", \"password\", \"embedded\", \"unembedded\"),\n    [\n        (\n            RESOURCE_ROOT / \"crazyones.pdf\",\n            None,\n            {\n                \"/HHXGQB+SFTI1440\",\n                \"/TITXYI+SFRM0900\",\n                \"/YISQAD+SFTI1200\",\n            },\n            set(),\n        ),\n        (\n            RESOURCE_ROOT / \"attachment.pdf\",\n            None,\n            {\n                \"/HHXGQB+SFTI1440\",\n                \"/TITXYI+SFRM0900\",\n                \"/YISQAD+SFTI1200\",\n            },\n            set(),\n        ),\n        (\n            RESOURCE_ROOT / \"libreoffice-writer-password.pdf\",\n            \"openpassword\",\n            {\"/BAAAAA+DejaVuSans\"},\n            set(),\n        ),\n        (\n            RESOURCE_ROOT / \"imagemagick-images.pdf\",\n            None,\n            set(),\n            {\"/Helvetica\"},\n        ),\n        (RESOURCE_ROOT / \"imagemagick-lzw.pdf\", None, set(), set()),\n        (\n            RESOURCE_ROOT / \"reportlab-inline-image.pdf\",\n            None,\n            set(),\n            {\"/Helvetica\"},\n        ),\n        # fonts in annotations\n        (\n            RESOURCE_ROOT / \"FormTestFromOo.pdf\",\n            None,\n            {\"/CAAAAA+LiberationSans\", \"/EAAAAA+SegoeUI\", \"/BAAAAA+LiberationSerif\"},\n            {\"/LiberationSans\", \"/ZapfDingbats\"},\n        ),\n    ],\n)\ndef test_get_fonts(pdf_path, password, embedded, unembedded):\n    reader = PdfReader(pdf_path, password=password)\n    a = set()\n    b = set()\n    for page in reader.pages:\n        a_tmp, b_tmp = page._get_fonts()\n        a = a.union(a_tmp)\n        b = b.union(b_tmp)\n    assert (a, b) == (embedded, unembedded)\n\n\n@pytest.mark.enable_socket\ndef test_get_fonts2():\n    url = \"https://github.com/py-pdf/pypdf/files/12618104/WS_T.483.8-2016.pdf\"\n    name = \"WS_T.483.8-2016.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert reader.pages[1]._get_fonts() == (\n        {\n            \"/E-HZ9-PK7483a5-Identity-H\",\n            \"/SSJ-PK748200005d9-Identity-H\",\n            \"/QGNGZS+FzBookMaker1DlFont10536872415\",\n            \"/E-BZ9-PK748344-Identity-H\",\n            \"/E-FZ9-PK74836f-Identity-H\",\n            \"/O9-PK748464-Identity-H\",\n            \"/QGNGZR+FzBookMaker0DlFont00536872414\",\n            \"/SSJ-PK748200005db-Identity-H\",\n            \"/F-BZ9-PK7483cb-Identity-H\",\n            \"/SSJ-PK748200005da-Identity-H\",\n            \"/H-SS9-PK748200005e0-Identity-H\",\n            \"/H-HT9-PK748200005e1-Identity-H\",\n        },\n        set(),\n    )\n    assert reader.pages[2]._get_fonts() == (\n        {\n            \"/E-HZ9-PK7483a5-Identity-H\",\n            \"/E-FZ9-PK74836f-Identity-H\",\n            \"/E-BZ9-PK748344-Identity-H\",\n            \"/QGNGZT+FzBookMaker0DlFont00536872418\",\n            \"/O9-PK748464-Identity-H\",\n            \"/F-BZ9-PK7483cb-Identity-H\",\n            \"/H-SS9-PK748200005e0-Identity-H\",\n            \"/QGNGZU+FzBookMaker1DlFont10536872420\",\n            \"/H-HT9-PK748200005e1-Identity-H\",\n        },\n        set(),\n    )\n\n\ndef test_annotation_getter():\n    pdf_path = RESOURCE_ROOT / \"commented.pdf\"\n    reader = PdfReader(pdf_path)\n    annotations = reader.pages[0].annotations\n    assert annotations is not None\n    assert isinstance(annotations[0], IndirectObject)\n\n    annot_dict = dict(annotations[0].get_object())\n    assert \"/P\" in annot_dict\n    assert isinstance(annot_dict[\"/P\"], IndirectObject)\n    del annot_dict[\"/P\"]\n\n    annot_dict[\"/Popup\"] = annot_dict[\"/Popup\"].get_object()\n    del annot_dict[\"/Popup\"][\"/P\"]\n    del annot_dict[\"/Popup\"][\"/Parent\"]\n    assert annot_dict == {\n        \"/Type\": \"/Annot\",\n        \"/Subtype\": \"/Text\",\n        \"/Rect\": ArrayObject(\n            [\n                270.75,\n                596.25,\n                294.75,\n                620.25,\n            ]\n        ),\n        \"/Contents\": \"Note in second paragraph\",\n        \"/C\": ArrayObject([1, 1, 0]),\n        \"/M\": \"D:20220406191858+02'00\",\n        \"/Popup\": DictionaryObject(\n            {\n                \"/M\": \"D:20220406191847+02'00\",\n                \"/Rect\": ArrayObject([294.75, 446.25, 494.75, 596.25]),\n                \"/Subtype\": \"/Popup\",\n                \"/Type\": \"/Annot\",\n            }\n        ),\n        \"/T\": \"moose\",\n    }\n\n\ndef test_annotation_setter(pdf_file_path):\n    # Arrange\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n    with pytest.raises(ValueError):\n        writer.add_page(DictionaryObject())\n\n    # Act\n    page_number = 0\n    page_link = writer.get_object(writer._pages)[\"/Kids\"][page_number]\n    annot_dict = {\n        NameObject(\"/P\"): page_link,\n        NameObject(\"/Type\"): NameObject(\"/Annot\"),\n        NameObject(\"/Subtype\"): NameObject(\"/Text\"),\n        NameObject(\"/Rect\"): ArrayObject(\n            [\n                FloatObject(270.75),\n                FloatObject(596.25),\n                FloatObject(294.75),\n                FloatObject(620.25),\n            ]\n        ),\n        NameObject(\"/Contents\"): TextStringObject(\"Note in second paragraph\"),\n        NameObject(\"/C\"): ArrayObject([FloatObject(1), FloatObject(1), FloatObject(0)]),\n        NameObject(\"/M\"): TextStringObject(\"D:20220406191858+02'00\"),\n        NameObject(\"/Popup\"): DictionaryObject(\n            {\n                NameObject(\"/M\"): TextStringObject(\"D:20220406191847+02'00\"),\n                NameObject(\"/Rect\"): ArrayObject(\n                    [\n                        FloatObject(294.75),\n                        FloatObject(446.25),\n                        FloatObject(494.75),\n                        FloatObject(596.25),\n                    ]\n                ),\n                NameObject(\"/Subtype\"): NameObject(\"/Popup\"),\n                NameObject(\"/Type\"): TextStringObject(\"/Annot\"),\n            }\n        ),\n        NameObject(\"/T\"): TextStringObject(\"moose\"),\n    }\n    arr = ArrayObject()\n    page.annotations = arr\n\n    # Delete Annotations\n    page.annotations = None\n\n    d = DictionaryObject(annot_dict)\n    ind_obj = writer._add_object(d)\n    arr.append(ind_obj)\n\n    # Assert manually\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\n@pytest.mark.enable_socket\n@pytest.mark.xfail(reason=\"#1091\")\ndef test_text_extraction_issue_1091():\n    url = \"https://github.com/user-attachments/files/18381737/tika-966635.pdf\"\n    name = \"tika-966635.pdf\"\n    stream = BytesIO(get_data_from_url(url, name=name))\n    with pytest.warns(PdfReadWarning):\n        reader = PdfReader(stream)\n    for page in reader.pages:\n        page.extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_empyt_password_1088():\n    url = \"https://github.com/user-attachments/files/18381712/tika-941536.pdf\"\n    name = \"tika-941536.pdf\"\n    stream = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(stream)\n    len(reader.pages)\n\n\n@pytest.mark.enable_socket\ndef test_old_habibi():\n    # this habibi has multiple characters associated with the h\n    reader = PdfReader(SAMPLE_ROOT / \"015-arabic/habibi.pdf\")\n    txt = reader.pages[0].extract_text()  # very odd file\n    # extract from acrobat reader \"حَبيبي habibi􀀃􀏲􀎒􀏴􀎒􀎣􀋴\n    assert \"habibi\" in txt\n    assert \"حَبيبي\" in txt\n\n\n@pytest.mark.samples\ndef test_read_link_annotation():\n    reader = PdfReader(SAMPLE_ROOT / \"016-libre-office-link/libre-office-link.pdf\")\n    assert len(reader.pages[0].annotations) == 1\n    annot = dict(reader.pages[0].annotations[0].get_object())\n    expected = {\n        \"/Type\": \"/Annot\",\n        \"/Subtype\": \"/Link\",\n        \"/A\": DictionaryObject(\n            {\n                \"/S\": \"/URI\",\n                \"/Type\": \"/Action\",\n                \"/URI\": \"https://martin-thoma.com/\",\n            }\n        ),\n        \"/Border\": ArrayObject([0, 0, 0]),\n        \"/Rect\": [\n            92.043,\n            771.389,\n            217.757,\n            785.189,\n        ],\n    }\n\n    assert set(expected.keys()) == set(annot.keys())\n    del expected[\"/Rect\"]\n    del annot[\"/Rect\"]\n    assert annot == expected\n\n\n@pytest.mark.enable_socket\ndef test_no_resources():\n    url = \"https://github.com/py-pdf/pypdf/files/9572045/108.pdf\"\n    name = \"108.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n    page_one = writer.pages[0]\n    page_two = writer.pages[0]\n    page_one.merge_page(page_two)\n\n\ndef test_merge_page_reproducible_with_proc_set():\n    page1 = PageObject.create_blank_page(width=100, height=100)\n    page2 = PageObject.create_blank_page(width=100, height=100)\n\n    ordered = sorted(NameObject(f\"/{x}\") for x in range(20))\n\n    shuffled = list(ordered)\n    shuffle(shuffled)\n\n    # each page has some overlap in their /ProcSet, and they're in a weird order\n    page1[NameObject(\"/Resources\")][NameObject(\"/ProcSet\")] = ArrayObject(shuffled[:15])\n    page2[NameObject(\"/Resources\")][NameObject(\"/ProcSet\")] = ArrayObject(shuffled[5:])\n    page1.merge_page(page2)\n\n    assert page1[NameObject(\"/Resources\")][NameObject(\"/ProcSet\")] == ordered\n\n\n@pytest.mark.parametrize(\n    (\"apage1\", \"apage2\", \"expected_result\", \"expected_renames\"),\n    [\n        # simple cases:\n        pytest.param({}, {}, {}, {}, id=\"no resources\"),\n        pytest.param(\n            {\"/1\": \"/v1\"},\n            {\"/2\": \"/v2\"},\n            {\"/1\": \"/v1\", \"/2\": \"/v2\"},\n            {},\n            id=\"no overlap\",\n        ),\n        pytest.param(\n            {\"/x\": \"/v\"}, {\"/x\": \"/v\"}, {\"/x\": \"/v\"}, {}, id=\"overlap, matching values\"\n        ),\n        pytest.param(\n            {\"/x\": \"/v1\"},\n            {\"/x\": \"/v2\"},\n            {\"/x\": \"/v1\", \"/x-0\": \"/v2\"},\n            {\"/x\": \"/x-0\"},\n            id=\"overlap, different values\",\n        ),\n        # carefully crafted names that match the renaming pattern:\n        pytest.param(\n            {\"/x\": \"/v1\", \"/x-0\": \"/v1\", \"/x-1\": \"/v1\"},\n            {\"/x\": \"/v2\"},\n            {\n                \"/x\": \"/v1\",\n                \"/x-0\": \"/v1\",\n                \"/x-1\": \"/v1\",\n                \"/x-2\": \"/v2\",\n            },\n            {\"/x\": \"/x-2\"},\n            id=\"crafted, different values\",\n        ),\n        pytest.param(\n            {\"/x\": \"/v1\", \"/x-0\": \"/v1\", \"/x-1\": \"/v\"},\n            {\"/x\": \"/v\"},\n            {\"/x\": \"/v1\", \"/x-0\": \"/v1\", \"/x-1\": \"/v\"},\n            {\"/x\": \"/x-1\"},\n            id=\"crafted, matching value in chain\",\n        ),\n        pytest.param(\n            {\"/x\": \"/v1\"},\n            {\"/x\": \"/v2.1\", \"/x-0\": \"/v2.2\"},\n            {\"/x\": \"/v1\", \"/x-0\": \"/v2.1\", \"/x-0-0\": \"/v2.2\"},\n            {\"/x\": \"/x-0\", \"/x-0\": \"/x-0-0\"},\n            id=\"crafted, overlaps with previous rename, different value\",\n        ),\n        pytest.param(\n            {\"/x\": \"/v1\"},\n            {\"/x\": \"/v2\", \"/x-0\": \"/v2\"},\n            {\"/x\": \"/v1\", \"/x-0\": \"/v2\"},\n            {\"/x\": \"/x-0\"},\n            id=\"crafted, overlaps with previous rename, matching value\",\n        ),\n    ],\n)\ndef test_merge_resources(apage1, apage2, expected_result, expected_renames):\n    for new_res in (False, True):\n        # Arrange\n        page1 = PageObject()\n        page1[NameObject(PG.RESOURCES)] = DictionaryObject()\n        for k, v in apage1.items():\n            page1[PG.RESOURCES][NameObject(k)] = NameObject(v)\n\n        page2 = PageObject()\n        page2[NameObject(PG.RESOURCES)] = DictionaryObject()\n        for k, v in apage2.items():\n            page2[PG.RESOURCES][NameObject(k)] = NameObject(v)\n\n        # Act\n        result, renames = page1._merge_resources(page1, page2, PG.RESOURCES, new_res)\n\n        # Assert\n        assert result == expected_result\n    assert renames == expected_renames\n\n\ndef test_merge_page_resources_smoke_test():\n    # Arrange\n    page1 = PageObject.create_blank_page(width=100, height=100)\n    page2 = PageObject.create_blank_page(width=100, height=100)\n\n    NO = NameObject\n\n    # set up some dummy resources that overlap (or not) between the two pages\n    # (note, all the edge cases are tested in test_merge_resources)\n    props1 = page1[NO(\"/Resources\")][NO(\"/Properties\")] = DictionaryObject(\n        {\n            NO(\"/just1\"): NO(\"/just1-value\"),\n            NO(\"/overlap-matching\"): NO(\"/overlap-matching-value\"),\n            NO(\"/overlap-different\"): NO(\"/overlap-different-value1\"),\n        }\n    )\n    props2 = page2[NO(\"/Resources\")][NO(\"/Properties\")] = DictionaryObject(\n        {\n            NO(\"/just2\"): NO(\"/just2-value\"),\n            NO(\"/overlap-matching\"): NO(\"/overlap-matching-value\"),\n            NO(\"/overlap-different\"): NO(\"/overlap-different-value2\"),\n        }\n    )\n    # use these keys for some \"operations\", to validate renaming\n    # (the operand name doesn't matter)\n    contents1 = page1[NO(\"/Contents\")] = ContentStream(None, None)\n    contents1.operations = [(ArrayObject(props1.keys()), b\"page1-contents\")]\n    contents2 = page2[NO(\"/Contents\")] = ContentStream(None, None)\n    contents2.operations = [(ArrayObject(props2.keys()), b\"page2-contents\")]\n\n    expected_properties = {\n        \"/just1\": \"/just1-value\",\n        \"/just2\": \"/just2-value\",\n        \"/overlap-matching\": \"/overlap-matching-value\",\n        \"/overlap-different\": \"/overlap-different-value1\",\n        \"/overlap-different-0\": \"/overlap-different-value2\",\n    }\n    expected_operations = [\n        # no renaming\n        (ArrayObject(props1.keys()), b\"page1-contents\"),\n        # some renaming\n        (\n            ArrayObject(\n                [\n                    NO(\"/just2\"),\n                    NO(\"/overlap-matching\"),\n                    NO(\"/overlap-different-0\"),\n                ]\n            ),\n            b\"page2-contents\",\n        ),\n    ]\n\n    # Act\n    page1.merge_page(page2)\n\n    # Assert\n    assert page1[NO(\"/Resources\")][NO(\"/Properties\")] == expected_properties\n\n    relevant_operations = [\n        (op, name)\n        for op, name in page1.get_contents().operations\n        if name in (b\"page1-contents\", b\"page2-contents\")\n    ]\n    assert relevant_operations == expected_operations\n\n\n@pytest.mark.enable_socket\ndef test_merge_transformed_page_into_blank():\n    url = \"https://github.com/py-pdf/pypdf/files/10768334/badges_3vjrh_7LXDZ_1-1.pdf\"\n    name = \"badges_3vjrh_7LXDZ_1.pdf\"\n    r1 = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/py-pdf/pypdf/files/10768335/badges_3vjrh_7LXDZ_2-1.pdf\"\n    name = \"badges_3vjrh_7LXDZ_2.pdf\"\n    r2 = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.add_blank_page(100, 100)\n    writer.pages[0].merge_translated_page(r1.pages[0], 0, 0, True, True)\n    writer.pages[0].merge_translated_page(r2.pages[0], 1000, 1000, True, True)\n    assert (\n        writer.pages[0][\"/Resources\"][\"/Font\"].raw_get(\"/F2+0\").idnum\n        != writer.pages[0][\"/Resources\"][\"/Font\"].raw_get(\"/F2+0-0\").idnum\n    )\n    writer.add_blank_page(100, 100)\n    for x in range(4):\n        for y in range(7):\n            writer.pages[1].merge_translated_page(\n                r1.pages[0],\n                x * r1.pages[0].trimbox[2],\n                y * r1.pages[0].trimbox[3],\n                True,\n                True,\n            )\n    blank = PageObject.create_blank_page(width=100, height=100)\n    assert blank.page_number is None\n    inserted_blank = writer.add_page(blank)\n    assert blank.page_number is None  # the inserted page is a clone\n    assert inserted_blank.page_number == len(writer.pages) - 1\n    writer.remove_page(inserted_blank.indirect_reference)\n    assert inserted_blank.page_number is None\n    inserted_blank = writer.add_page(blank)\n    del writer._pages.get_object()[\"/Kids\"][-1]\n    assert inserted_blank.page_number is not None\n\n\ndef test_pages_printing():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    assert str(reader.pages) == \"[PageObject(0)]\"\n    assert len(reader.pages[0].images) == 0\n    with pytest.raises(KeyError):\n        reader.pages[0].images[\"~1~\"]\n\n\n@pytest.mark.enable_socket\ndef test_del_pages():\n    url = \"https://github.com/user-attachments/files/18381712/tika-941536.pdf\"\n    name = \"tika-941536.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n    ll = len(writer.pages)\n    pp = writer.pages[1].indirect_reference\n    del writer.pages[1]\n    assert len(writer.pages) == ll - 1\n    pages = writer._pages.get_object()\n    assert pages[\"/Count\"] == ll - 1\n    assert len(pages[\"/Kids\"]) == ll - 1\n    assert pp not in pages[\"/Kids\"]\n    del writer.pages[-2]\n    with pytest.raises(TypeError):\n        del writer.pages[\"aa\"]\n    with pytest.raises(IndexError):\n        del writer.pages[9999]\n    pp = tuple(p.indirect_reference for p in writer.pages[3:5])\n    ll = len(writer.pages)\n    del writer.pages[3:5]\n    assert len(writer.pages) == ll - 2\n    for p in pp:\n        assert p not in pages[\"/Kids\"]\n    # del whole arborescence\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    # error case\n    pp = reader.pages[2]\n    i = pp[\"/Parent\"].get_object()[\"/Kids\"].index(pp.indirect_reference)\n    del pp[\"/Parent\"].get_object()[\"/Kids\"][i]\n    with pytest.raises(PdfReadError):\n        del reader.pages[2]\n\n    url = \"https://github.com/py-pdf/pypdf/files/13946477/panda.pdf\"\n    name = \"iss2343b.pdf\"\n    writer = PdfWriter(BytesIO(get_data_from_url(url, name=name)), incremental=True)\n    node, idx = writer._get_page_in_node(53)\n    assert (node.indirect_reference.idnum, idx) == (11776, 1)\n    node, idx = writer._get_page_in_node(10000)\n    assert (node.indirect_reference.idnum, idx) == (11769, -1)\n    with pytest.raises(PyPdfError):\n        writer._get_page_in_node(-1)\n\n    del writer.pages[4]  # to propagate among /Pages\n    del writer.pages[:]\n    assert len(writer.pages) == 0\n    assert len(writer.root_object[\"/Pages\"][\"/Kids\"]) == 0\n    assert len(writer.flattened_pages) == 0\n\n\ndef test_pdf_pages_missing_type():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    del reader.trailer[\"/Root\"][\"/Pages\"][\"/Kids\"][0].get_object()[\"/Type\"]\n    reader.pages[0]\n    writer = PdfWriter(clone_from=reader)\n    writer.pages[0]\n\n\n@pytest.mark.enable_socket\ndef test_merge_with_stream_wrapped_in_save_restore():\n    \"\"\"Test for issue #2587\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/14895914/blank_portrait.pdf\"\n    name = \"blank_portrait.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n    page_one = writer.pages[0]\n    assert page_one.get_contents().get_data() == b\"q Q\"\n    page_two = writer.pages[0]\n    page_one.merge_page(page_two)\n    assert b\"QQ\" not in page_one.get_contents().get_data()\n\n\n@pytest.mark.samples\ndef test_compression():\n    \"\"\"Test for issue #1897\"\"\"\n\n    def create_stamp_pdf() -> BytesIO:\n        pytest.importorskip(\"fpdf\")\n        from fpdf import FPDF  # noqa: PLC0415\n\n        pdf = FPDF()\n        pdf.add_page()\n        pdf.set_font(\"helvetica\", \"B\", 16)\n        pdf.cell(40, 10, \"Hello World!\")\n        byte_string = pdf.output()\n        return BytesIO(byte_string)\n\n    template = PdfReader(create_stamp_pdf())\n    template_page = template.pages[0]\n    writer = PdfWriter()\n    writer.append(SAMPLE_ROOT / \"009-pdflatex-geotopo/GeoTopo.pdf\", [1])\n    nb1 = len(writer._objects)\n\n    # 1 page only is modified\n    for page in writer.pages:\n        page.merge_page(template_page)\n    # font is added; +1 streamobjects + 1 ArrayObject\n    assert len(writer._objects) == nb1 + 1 + 2\n    for page in writer.pages:\n        page.compress_content_streams()\n    # objects are recycled\n    assert len(writer._objects) == nb1 + 1 + 2\n\n    contents = writer.pages[0][\"/Contents\"]\n    writer.pages[0].replace_contents(None)\n    writer.pages[0].replace_contents(None)\n    assert isinstance(\n        writer._objects[contents.indirect_reference.idnum - 1], NullObject\n    )\n\n\ndef test_merge_with_no_resources():\n    \"\"\"Test for issue #2147\"\"\"\n    writer = PdfWriter()\n    p0 = writer.add_blank_page(900, 1200)\n    del p0[\"/Resources\"]\n    p1 = writer.add_blank_page(900, 1200)\n    del p1[\"/Resources\"]\n    writer.pages[0].merge_page(p1)\n\n\ndef test_get_contents_from_nullobject():\n    \"\"\"Issue #2157\"\"\"\n    writer = PdfWriter()\n    page1 = writer.add_blank_page(100, 100)\n    page1[NameObject(\"/Contents\")] = writer._add_object(NullObject())\n    assert page1.get_contents() is None\n    page2 = writer.add_blank_page(100, 100)\n    page1.merge_page(page2, over=True)\n\n\n@pytest.mark.enable_socket\ndef test_pos_text_in_textvisitor():\n    \"\"\"See #2200\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12675974/page_178.pdf\"\n    name = \"test_text_pos.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    p = ()\n\n    def visitor_body2(text, cm, tm, fontdict, fontsize) -> None:\n        nonlocal p\n        if text.startswith(\"5425.\"):\n            p = (tm[4], tm[5])\n\n    reader.pages[0].extract_text(visitor_text=visitor_body2)\n    assert abs(p[0] - 323.5) < 0.1\n    assert abs(p[1] - 457.4) < 0.1\n\n\n@pytest.mark.enable_socket\ndef test_pos_text_in_textvisitor2():\n    \"\"\"See #2075\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12318042/LegIndex-page6.pdf\"\n    name = \"LegIndex-page6.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    x_lvl = 26\n    lst = []\n\n    def visitor_lvl(text, cm, tm, fontdict, fontsize) -> None:\n        nonlocal x_lvl, lst\n        if abs(tm[4] - x_lvl) < 2 and tm[5] < 740 and tm[5] > 210:\n            lst.append(text.strip(\" \\n\"))\n\n    reader.pages[0].extract_text(visitor_text=visitor_lvl)\n    assert lst == [\n        \"ACUPUNCTURE BOARD\",\n        \"ACUPUNCTURISTS AND ACUPUNCTURE\",\n        \"ADMINISTRATIVE LAW AND PROCEDURE\",\n        \"ADMINISTRATIVE LAW, OFFICE OF\",\n        \"ADOPTION\",\n        \"ADULT EDUCATION\",\n        \"ADVERTISING. See also MARKETING; and particular subject matter (e.g.,\",\n    ]\n    x_lvl = 35\n    lst = []\n    reader.pages[0].extract_text(visitor_text=visitor_lvl)\n    assert lst == [\n        \"members,  AB 1264\",\n        \"assistants, acupuncture,  AB 1264\",\n        \"complaints, investigations, etc.,  AB 1264\",\n        \"day, california acupuncture,  HR 48\",\n        \"massage services, asian,  AB 1264\",\n        \"supervising acupuncturists,  AB 1264\",\n        \"supportive acupuncture services, basic,  AB 1264\",\n        \"rules and regulations—\",\n        \"professional assistants and employees: employment and compensation,  AB 916\",\n        \"adults, adoption of,  AB 1756\",\n        \"agencies, organizations, etc.: requirements, prohibitions, etc.,  SB 807\",\n        \"assistance programs, adoption: nonminor dependents,  SB 9\",\n        \"birth certificates,  AB 1302\",\n        \"contact agreements, postadoption—\",\n        \"facilitators, adoption,  AB 120\",\n        \"failed adoptions: reproductive loss leave,  SB 848\",\n        \"hearings, adoption finalization: remote proceedings, technology, etc.,  SB 21\",\n        \"native american tribes,  AB 120\",\n        \"parental rights, reinstatement of,  AB 20\",\n        \"parents, prospective adoptive: criminal background checks,  SB 824\",\n        \"services, adult educational,  SB 877\",\n        \"week, adult education,  ACR 31\",\n        \"alcoholic beverages: tied-house restrictions,  AB 546\",\n        \"campaign re social equity, civil rights, etc.,  SB 447\",\n        \"cannabis,  AB 794\",\n        \"elections. See ELECTIONS.\",\n        \"false, misleading, etc., advertising—\",\n        \"hotels, short-term rentals, etc., advertised rates: mandatory fee disclosures,  SB 683\",\n        \"housing rental properties advertised rates: disclosures,  SB 611\",\n    ]\n\n\n@pytest.mark.enable_socket\ndef test_missing_basefont_in_type3():\n    \"\"\"Cf #2289\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/13307713/missing-base-font.pdf\"\n    name = \"missing-base-font.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0]._get_fonts()\n\n\ndef test_invalid_index():\n    src_abs = RESOURCE_ROOT / \"git.pdf\"\n    reader = PdfReader(src_abs)\n    with pytest.raises(TypeError):\n        _ = reader.pages[\"0\"]\n\n\ndef test_negative_index():\n    src_abs = RESOURCE_ROOT / \"git.pdf\"\n    reader = PdfReader(src_abs)\n    assert reader.pages[0] == reader.pages[-1]\n\n\ndef test_get_contents_as_bytes():\n    writer = PdfWriter(RESOURCE_ROOT / \"crazyones.pdf\")\n    co = writer.pages[0][\"/Contents\"][0]\n    expected = co.get_data()\n    assert writer.pages[0]._get_contents_as_bytes() == expected\n    writer.pages[0][NameObject(\"/Contents\")] = writer.pages[0][\"/Contents\"][0]\n    assert writer.pages[0]._get_contents_as_bytes() == expected\n    del writer.pages[0][\"/Contents\"]\n    assert writer.pages[0]._get_contents_as_bytes() is None\n\n\ndef test_recursive_get_page_from_node():\n    writer = PdfWriter(RESOURCE_ROOT / \"crazyones.pdf\", incremental=True)\n    writer.root_object[\"/Pages\"].get_object()[\n        NameObject(\"/Parent\")\n    ] = writer.root_object[\"/Pages\"].indirect_reference\n    with pytest.raises(PyPdfError):\n        writer.add_page(writer.pages[0])\n    writer = PdfWriter(RESOURCE_ROOT / \"crazyones.pdf\", incremental=True)\n    writer.insert_page(writer.pages[0], -1)\n    with pytest.raises(ValueError):\n        writer.insert_page(writer.pages[0], -10)\n\n\ndef test_get_contents__none_type():\n    # We can observe this in reality as well, but these documents might be\n    # confidential. Thus use a more complex dummy implementation here while\n    # assigning a value of `None` is not possible from code, but from PDFs\n    # itself.\n    class MyPage(PageObject):\n        def __contains__(self, item) -> bool:\n            assert item == \"/Contents\"\n            return True\n\n        def __getitem__(self, item) -> Any:\n            assert item == \"/Contents\"\n\n    page = MyPage()\n    assert page.get_contents() is None\n\n\ndef test_extract_text__none_type():\n    class MyPage(PageObject):\n        def __getitem__(self, item) -> Any:\n            if item == \"/Contents\":\n                return None\n            return super().__getitem__(item)\n\n    page = MyPage()\n    resources = DictionaryObject()\n    none_reference = IndirectObject(1, 0, None)\n    resources[NameObject(\"/Font\")] = none_reference\n    page[NameObject(\"/Resources\")] = resources\n    with mock.patch.object(none_reference, \"get_object\", return_value=None):\n        assert page.extract_text() == \"\"\n\n\n@pytest.mark.enable_socket\ndef test_scale_by():\n    \"\"\"Tests for #3487\"\"\"\n    url = \"https://github.com/user-attachments/files/22685841/input.pdf\"\n    name = \"issue3487.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n\n    original_box = RectangleObject((0, 0, 595.275604, 841.88974))\n    expected_box = RectangleObject((0.0, 0.0, 297.637802, 420.94487))\n    for page in writer.pages:\n        assert page.artbox == original_box\n        assert page.bleedbox == original_box\n        assert page.cropbox == original_box\n        assert page.mediabox == original_box\n        assert page.trimbox == original_box\n\n        page.scale_by(0.5)\n        assert page.artbox == expected_box\n        assert page.bleedbox == expected_box\n        assert page.cropbox == expected_box\n        assert page.mediabox == expected_box\n        assert page.trimbox == expected_box\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(GHOSTSCRIPT_BINARY is None, reason=\"Requires Ghostscript\")\ndef test_box_rendering(tmp_path):\n    \"\"\"Tests for issue #3487.\"\"\"\n    url = \"https://github.com/user-attachments/files/22685841/input.pdf\"\n    name = \"issue3487.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n\n    for page in writer.pages:\n        page.scale_by(0.5)\n\n    target_png_path = tmp_path / \"target.png\"\n    url = \"https://github.com/user-attachments/assets/e9c2271c-bfc3-4a6f-8c91-ffefa24502e2\"\n    name = \"issue3487.png\"\n    target_png_path.write_bytes(get_data_from_url(url, name=name))\n\n    pdf_path = tmp_path / \"out.pdf\"\n    writer.write(pdf_path)\n\n    for box in [\"Art\", \"Bleed\", \"Crop\", \"Media\", \"Trim\"]:\n        png_path = tmp_path / f\"{box}.png\"\n        # False positive: https://github.com/PyCQA/bandit/issues/333\n        subprocess.run(  # noqa: S603\n            [\n                GHOSTSCRIPT_BINARY,\n                f\"-dUse{box}Box\",\n                \"-dFirstPage=1\",\n                \"-dLastPage=1\",\n                \"-sDEVICE=pngalpha\",\n                \"-o\",\n                png_path,\n                pdf_path,\n            ]\n        )\n        assert png_path.is_file(), box\n        assert image_similarity(png_path, target_png_path) >= 0.95, box\n\n\ndef test_delete_non_existent_annotations():\n    writer = PdfWriter()\n    writer.add_blank_page(width=100, height=100)\n    page = writer.pages[0]\n    assert page.annotations is None\n    page.annotations = None\n    assert page.annotations is None\n\n\ndef test_replace_contents_on_reader():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    content_stream = ContentStream(stream=None, pdf=reader)\n    content_stream.set_data(b\"Test data\")\n\n    expected_message = (\n        \"Calling `PageObject.replace_contents()` for pages not assigned to a writer is deprecated and \"\n        \"will be removed in pypdf 7.0.0. Attach the page to the writer first or use `PdfWriter(clone_from=...)` \"\n        \"directly. The existing approach has proved being unreliable.\"\n    )\n    with pytest.warns(DeprecationWarning, match=rf\"^{re.escape(expected_message)}$\"):\n        page.replace_contents(content_stream)\n\n\n@pytest.mark.enable_socket\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_replace_contents_on_reader__indirect_reference():\n    url = \"https://github.com/user-attachments/files/24195534/test.pdf\"\n    name = \"issue3568.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n\n    lhs = reader.get_page(3)\n    writer.add_page(lhs)\n\n    lhs = reader.get_page(1)\n    lhs.merge_page(PageObject.create_blank_page(reader))\n    writer.add_page(lhs)\n\n\ndef test_merge_page__coverage():\n    # Test with some otherwise untested cases.\n\n    # Own resources are missing.\n    page = PageObject.create_blank_page(width=10, height=10)\n    del page[PageAttributes.RESOURCES]\n    page.merge_page(PageObject.create_blank_page(width=10, height=10))\n\n    # Other resources are missing.\n    page = PageObject.create_blank_page(width=10, height=10)\n    del page[PageAttributes.RESOURCES]\n    PageObject.create_blank_page(width=10, height=10).merge_page(page)\n\n    # No expansion.\n    page = PageObject.create_blank_page(width=10, height=10)\n    page.merge_page(PageObject.create_blank_page(width=20, height=30))\n    assert page.mediabox == RectangleObject((0.0, 0.0, 10, 10))\n\n    # With expansion.\n    page = PageObject.create_blank_page(width=10, height=10)\n    page.merge_page(PageObject.create_blank_page(width=20, height=5), expand=True)\n    assert page.mediabox == RectangleObject((0.0, 0.0, 20, 10))\n\n    # With transformation.\n    path = RESOURCE_ROOT / \"crazyones.pdf\"\n    page = PdfWriter(clone_from=path).pages[0]\n    page.indirect_reference = None\n    page2 = PageObject.create_blank_page(width=20, height=5)\n    transformation = Transformation().rotate(90)\n    page2.merge_transformed_page(page, ctm=transformation, expand=True)\n    assert page2.mediabox == RectangleObject((-792, 0.0, 20, 612))\n\n    page2 = PageObject.create_blank_page(width=20, height=5)\n    page2.merge_transformed_page(page, ctm=transformation.ctm, expand=True)\n    assert page2.mediabox == RectangleObject((-792, 0.0, 20, 612))\n\n    # Not over.\n    page = PdfWriter(clone_from=path).pages[0]\n    page.indirect_reference = None\n    page2 = PageObject.create_blank_page(width=20, height=5)\n    page2.merge_page(page, over=False)\n\n\n@pytest.mark.enable_socket\ndef test_importing_without_pillow(tmp_path):\n    env = os.environ.copy()\n    env[\"COVERAGE_PROCESS_START\"] = \"pyproject.toml\"\n\n    source_file = tmp_path / \"script.py\"\n    source_file.write_text(\n        \"\"\"\nimport sys\nsys.modules[\"PIL\"] = None\n\nfrom pypdf import PageObject\nfrom pypdf._page import pil_not_imported\n\nprint(pil_not_imported)\n\"\"\"\n    )\n\n    try:\n        env[\"PYTHONPATH\"] = \".\" + os.pathsep + env[\"PYTHONPATH\"]\n    except KeyError:\n        env[\"PYTHONPATH\"] = \".\"\n    result = subprocess.run(  # noqa: S603  # We have the control here.\n        [sys.executable, source_file],\n        capture_output=True,\n        env=env,\n    )\n    assert result.returncode == 0\n    assert result.stdout.replace(b\"\\r\\n\", b\"\\n\") == b\"True\\n\"\n    assert result.stderr == b\"\"\n\n\n@pytest.mark.enable_socket\ndef test_replace_contents__null_object_cloning_error():\n    url = \"https://github.com/user-attachments/files/25240822/ML-4.30.24.pdf\"\n    name = \"issue3632.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url=url, name=name)))\n    writer = PdfWriter()\n\n    for page in reader.pages:\n        new_page = writer.add_page(page)\n        new_page.scale_by(1)\n\n    page4_idnum = writer.pages[3].indirect_reference.idnum\n    assert isinstance(writer.get_object(page4_idnum)[\"/Contents\"], ContentStream)\n    assert isinstance(writer.get_object(page4_idnum + 1), NullObject)\n\n    data = BytesIO()\n    writer.write(data)\n\n    reader = PdfReader(data)\n    assert len(reader.pages) == 10\n\n\ndef test_get_rectangle__size_handling(caplog):\n    \"\"\"\n    See issue #2991 and related ones. We would previously generate invalid page boxes when they\n    were part of the `/Pages` instead of the `/Page` due to re-using the same target object,\n    while appending to the existing \"full\" object. To keep compatibility with our old code,\n    allow these boxes to have more than four entries.\n    \"\"\"\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    page = reader.pages[0]\n    assert page.mediabox == RectangleObject((0, 0, 612, 792))\n    assert caplog.messages == []\n\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    page = reader.pages[0]\n    page[NameObject(\"/MediaBox\")] = ArrayObject([0, 0, 13, 37, 0, 0, 13, 37])\n    assert page.mediabox == RectangleObject((0, 0, 13, 37))\n    assert \"Expected four values, got 8: [0, 0, 13, 37, 0, 0, 13, 37]\\n\" in caplog.text\n"
  },
  {
    "path": "tests/test_page_labels.py",
    "content": "\"\"\"Test the pypdf._page_labels module.\"\"\"\nfrom io import BytesIO\n\nimport pytest\n\nfrom pypdf import PdfReader\nfrom pypdf._page_labels import (\n    get_label_from_nums,\n    index2label,\n    number2lowercase_letter,\n    number2lowercase_roman_numeral,\n    number2uppercase_letter,\n    number2uppercase_roman_numeral,\n    nums_clear_range,\n    nums_insert,\n    nums_next,\n)\nfrom pypdf.generic import (\n    ArrayObject,\n    DictionaryObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n)\n\nfrom . import RESOURCE_ROOT, get_data_from_url\n\n\n@pytest.mark.parametrize(\n    (\"number\", \"expected\"),\n    [\n        (1, \"I\"),\n        (2, \"II\"),\n        (3, \"III\"),\n        (4, \"IV\"),\n        (5, \"V\"),\n        (6, \"VI\"),\n        (7, \"VII\"),\n        (8, \"VIII\"),\n        (9, \"IX\"),\n        (10, \"X\"),\n    ],\n)\ndef test_number2uppercase_roman_numeral(number, expected):\n    assert number2uppercase_roman_numeral(number) == expected\n\n\ndef test_number2lowercase_roman_numeral():\n    assert number2lowercase_roman_numeral(123) == \"cxxiii\"\n\n\n@pytest.mark.parametrize(\n    (\"number\", \"expected\"),\n    [\n        (1, \"a\"),\n        (2, \"b\"),\n        (3, \"c\"),\n        (25, \"y\"),\n        (26, \"z\"),\n        (27, \"aa\"),\n        (28, \"ab\"),\n    ],\n)\ndef test_number2lowercase_letter(number, expected):\n    assert number2lowercase_letter(number) == expected\n\n\ndef test_number2uppercase_letter():\n    with pytest.raises(ValueError):\n        number2uppercase_letter(-1)\n\n\n@pytest.mark.enable_socket\ndef test_index2label(caplog):\n    name = \"waarom-meisjes-het-beter-doen-op-HAVO-en-VWO-ROA.pdf\"\n    r = PdfReader(BytesIO(get_data_from_url(name=name)))\n    assert index2label(r, 1) == \"ii\"\n    assert index2label(r, 9) == \"6\"\n    # very silly data to get test cover\n    r.trailer[\"/Root\"][\"/PageLabels\"][\"/Nums\"].append(8)\n    r.trailer[\"/Root\"][\"/PageLabels\"][\"/Nums\"].append(NullObject())\n    assert index2label(r, 9) == \"10\"\n\n    with pytest.raises(ValueError):\n        nums_clear_range(\n            NumberObject(10), 8, r.trailer[\"/Root\"][\"/PageLabels\"][\"/Nums\"]\n        )\n    r.trailer[\"/Root\"][\"/PageLabels\"][\"/Nums\"].append(8)\n    with pytest.raises(ValueError):\n        nums_next(NumberObject(10), r.trailer[\"/Root\"][\"/PageLabels\"][\"/Nums\"])\n    with pytest.raises(ValueError):\n        nums_clear_range(\n            NumberObject(10), 8, r.trailer[\"/Root\"][\"/PageLabels\"][\"/Nums\"]\n        )\n    with pytest.raises(ValueError):\n        nums_insert(\n            NumberObject(10),\n            DictionaryObject(),\n            r.trailer[\"/Root\"][\"/PageLabels\"][\"/Nums\"],\n        )\n\n    del r.trailer[\"/Root\"][\"/PageLabels\"][\"/Nums\"]\n    assert index2label(r, 1) == \"2\"\n    caplog.clear()\n    r.trailer[\"/Root\"][\"/PageLabels\"][NameObject(\"/Kids\")] = NullObject()\n    assert index2label(r, 1) == \"2\"\n    assert caplog.text != \"\"\n\n\n@pytest.mark.enable_socket\ndef test_index2label_kids():\n    url = \"https://github.com/py-pdf/pypdf/files/14858124/Terminologie_Epochen.Schwerpunkte.Umsetzungen.pdf\"\n    r = PdfReader(BytesIO(get_data_from_url(url=url, name=\"index2label_kids.pdf\")))\n    expected = [\n        \"C1\",\n        \"I\",\n        \"II\",\n        \"III\",\n        \"IV\",\n        \"V\",\n        \"VI\",\n        \"VII\",\n        \"VIII\",\n        \"IX\",\n        \"X\",\n        \"XI\",\n        \"XII\",\n        \"XIII\",\n        \"XIV\",\n        \"XV\",\n        \"XVI\",\n        \"XVII\",\n        *list(map(str, range(1, 284)))\n    ]\n    for x in [\"20\", \"44\", \"58\", \"82\", \"94\", \"116\", \"154\", \"166\", \"192\", \"224\", \"250\"]:\n        # Some page labels are unused. Removing them is still easier than copying the\n        # whole list itself here.\n        expected.remove(x)\n    assert r.page_labels == expected\n\n\n@pytest.mark.enable_socket\ndef test_index2label_kids__recursive(caplog):\n    url = \"https://github.com/py-pdf/pypdf/files/14842446/tt1.pdf\"\n    r = PdfReader(\n        BytesIO(get_data_from_url(url=url, name=\"index2label_kids_recursive.pdf\"))\n    )\n    expected = [\n        \"A\",\n        \"B\",\n        \"C\",\n        \"D\",\n        \"E\",\n        \"F\",\n        \"G\",\n        \"H\",\n        \"I\",\n        \"J\",\n        \"K\",\n        \"L\",\n        \"M\",\n        \"N\",\n        \"O\",\n        \"P\",\n        \"17\",\n        \"18\",\n        \"19\",\n    ]\n    assert r.page_labels == expected\n    assert caplog.text != \"\"\n\n\ndef test_get_label_from_nums__empty_nums_list():\n    dictionary_object = DictionaryObject()\n    dictionary_object[NameObject(\"/Nums\")] = ArrayObject()\n    assert get_label_from_nums(dictionary_object, 13) == \"14\"\n\n\ndef test_index2label__empty_kids_list():\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    number_tree = DictionaryObject()\n    number_tree[NameObject(\"/Kids\")] = ArrayObject()\n    root = reader.root_object\n    root[NameObject(\"/PageLabels\")] = number_tree\n\n    assert index2label(reader, 42) == \"43\"\n"
  },
  {
    "path": "tests/test_pagerange.py",
    "content": "\"\"\"Test the pypdf.pagerange module.\"\"\"\nimport pytest\n\nfrom pypdf.pagerange import PageRange, ParseError, parse_filename_page_ranges\n\n\ndef test_equality():\n    pr1 = PageRange(slice(0, 5))\n    pr2 = PageRange(slice(0, 5))\n    assert pr1 == pr2\n\n\ndef test_hash():\n    pr1 = PageRange(slice(0, 5))\n    pr2 = PageRange(slice(0, 5))\n    pr3 = PageRange(slice(10, 11))\n    pr4 = PageRange(slice(10, 11, 1))\n    assert hash(pr1) == hash(pr2)\n    assert hash(pr1) != hash(pr3)\n    # Consider this different for now, although slicing with step size of 1 and `None` should be identical.\n    assert hash(pr3) != hash(pr4)\n\n\n@pytest.mark.parametrize(\n    (\"page_range\", \"expected\"),\n    [(slice(0, 5), \"0:5\"), (slice(0, 5, 2), \"0:5:2\"), (\"-1\", \"-1:\"), (\"0\", \"0\")],\n)\ndef test_str(page_range, expected):\n    assert str(PageRange(page_range)) == expected\n\n\n@pytest.mark.parametrize(\n    (\"page_range\", \"expected\"),\n    [(slice(0, 5), \"PageRange('0:5')\"), (slice(0, 5, 2), \"PageRange('0:5:2')\")],\n)\ndef test_repr(page_range, expected):\n    assert repr(PageRange(page_range)) == expected\n\n\ndef test_equality_other_objectc():\n    pr1 = PageRange(slice(0, 5))\n    pr2 = \"PageRange(slice(0, 5))\"\n    assert pr1 != pr2\n\n\ndef test_idempotency():\n    pr = PageRange(slice(0, 5))\n    pr2 = PageRange(pr)\n    assert pr == pr2\n\n\n@pytest.mark.parametrize(\n    (\"range_str\", \"expected\"),\n    [\n        (\"42\", slice(42, 43)),\n        (\"1:2\", slice(1, 2)),\n    ],\n)\ndef test_str_init(range_str, expected):\n    pr = PageRange(range_str)\n    assert pr._slice == expected\n    assert PageRange.valid\n\n\ndef test_str_init_error():\n    init_str = \"1-2\"\n    assert PageRange.valid(init_str) is False\n    with pytest.raises(ParseError) as exc:\n        PageRange(init_str)\n    assert exc.value.args[0] == \"1-2\"\n\n\n@pytest.mark.parametrize(\n    (\"params\", \"expected\"),\n    [\n        ([\"foo.pdf\", \"1:5\"], [(\"foo.pdf\", PageRange(\"1:5\"))]),\n        (\n            [\"foo.pdf\", \"1:5\", \"bar.pdf\"],\n            [(\"foo.pdf\", PageRange(\"1:5\")), (\"bar.pdf\", PageRange(\":\"))],\n        ),\n    ],\n)\ndef test_parse_filename_page_ranges(params, expected):\n    assert parse_filename_page_ranges(params) == expected\n\n\ndef test_parse_filename_page_ranges_err():\n    with pytest.raises(ValueError) as exc:\n        parse_filename_page_ranges([\"1:5\", \"foo.pdf\"])\n    assert (\n        exc.value.args[0] == \"The first argument must be a filename, not a page range.\"\n    )\n\n\n@pytest.mark.parametrize(\n    (\"a\", \"b\", \"expected\"),\n    [\n        (PageRange(slice(0, 5)), PageRange(slice(2, 10)), slice(0, 10)),\n        (PageRange(slice(0, 5)), PageRange(slice(2, 3)), slice(0, 5)),\n        (PageRange(slice(0, 5)), PageRange(slice(5, 10)), slice(0, 10)),\n    ],\n)\ndef test_addition(a, b, expected):\n    pr1 = PageRange(a)\n    pr2 = PageRange(b)\n    assert pr1 + pr2 == PageRange(expected)\n    assert pr2 + pr1 == PageRange(expected)  # addition is commutative\n\n\n@pytest.mark.parametrize(\n    (\"a\", \"b\"),\n    [\n        (PageRange(slice(0, 5)), PageRange(slice(7, 10))),\n        (PageRange(slice(7, 10)), PageRange(slice(0, 5))),\n    ],\n)\ndef test_addition_gap(a: PageRange, b: PageRange):\n    with pytest.raises(ValueError) as exc:\n        a + b\n    assert exc.value.args[0] == \"Can't add PageRanges with gap\"\n\n\ndef test_addition_non_page_range():\n    with pytest.raises(TypeError) as exc:\n        PageRange(slice(0, 5)) + \"2:7\"\n    assert exc.value.args[0] == \"Can't add PageRange and <class 'str'>\"\n\n\ndef test_addition_stride():\n    a = PageRange(slice(0, 5, 2))\n    b = PageRange(slice(7, 9))\n    with pytest.raises(ValueError) as exc:\n        a + b\n    assert exc.value.args[0] == \"Can't add PageRange with stride\"\n"
  },
  {
    "path": "tests/test_papersizes.py",
    "content": "\"\"\"Test the pypdf.papersizes module.\"\"\"\nimport pytest\n\nfrom pypdf import papersizes\n\n\ndef test_din_a0_paper_size():\n    \"\"\"The dimensions and area of the DIN A0 paper size are correct.\"\"\"\n    dim = papersizes.PaperSize.A0\n    area_square_pixels = float(dim.width) * dim.height\n\n    # 72 pixels is 1 inch\n    area_square_inch = area_square_pixels / 72**2\n\n    # 25.4 millimeter is equal to 1 inches\n    area_square_mm = area_square_inch * (25.4) ** 2\n    assert abs(area_square_mm - 999949) < 100\n    conversion_factor = 72 / 25.4\n    assert (dim.width - 841 * conversion_factor) < 1\n    assert (dim.width - 1189 * conversion_factor) < 1\n\n\n@pytest.mark.parametrize(\"dimensions\", papersizes._din_a)\ndef test_din_a_aspect_ratio(dimensions):\n    \"\"\"The aspect ratio of DIN A paper sizes is correct.\"\"\"\n    assert abs(dimensions.height - dimensions.width * 2**0.5) <= 2.5\n\n\n@pytest.mark.parametrize(\n    (\"dimensions_a\", \"dimensions_b\"),\n    list(zip(papersizes._din_a, papersizes._din_a[1:])),\n)\ndef test_din_a_size_doubling(dimensions_a, dimensions_b):\n    \"\"\"The height of a DIN A paper size doubles when moving to the next size.\"\"\"\n    assert abs(dimensions_a.height - 2 * dimensions_b.width) <= 4\n"
  },
  {
    "path": "tests/test_pdfa.py",
    "content": "\"\"\"Ensure that pypdf doesn't break PDF/A compliance.\"\"\"\n\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import Optional\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom tests import SAMPLE_ROOT\n\n\ndef is_pdfa1b_compliant(src: BytesIO):\n    \"\"\"Check if a PDF is PDF/A-1b compliant.\"\"\"\n\n    def document_information_has_analogous_xml(src: BytesIO) -> bool:\n        reader = PdfReader(src)\n        meta = reader.metadata\n        xmp = reader.xmp_metadata\n        if not meta:\n            return True\n        if not xmp:\n            return False\n        if meta.title and not xmp.dc_title:\n            return meta.title == xmp.dc_title\n        return True\n\n    return document_information_has_analogous_xml(src)\n\n\n@pytest.mark.samples\n@pytest.mark.parametrize(\n    (\"src\", \"diagnostic_write_name\"),\n    [\n        (SAMPLE_ROOT / \"021-pdfa/crazyones-pdfa.pdf\", None),\n    ],\n)\ndef test_pdfa(src: Path, diagnostic_write_name: Optional[str]):\n    with open(src, \"rb\") as fp:\n        data = BytesIO(fp.read())\n    reader = PdfReader(src)\n    assert is_pdfa1b_compliant(data)\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n\n    stream = BytesIO()\n    writer.write(stream)\n    stream.seek(0)\n\n    assert is_pdfa1b_compliant(stream)\n    if diagnostic_write_name:\n        with open(diagnostic_write_name, \"wb\") as fp:\n            stream.seek(0)\n            fp.write(stream.read())\n"
  },
  {
    "path": "tests/test_protocols.py",
    "content": "\"\"\"Test the pypdf._protocols module.\"\"\"\nfrom pypdf._protocols import PdfObjectProtocol\n\n\nclass IPdfObjectProtocol(PdfObjectProtocol):\n    pass\n\n\ndef test_pdfobjectprotocol():\n    o = IPdfObjectProtocol()\n    assert o.clone(None, False, None) is None\n    assert o._reference_clone(None, None) is None\n    assert o.get_object() is None\n    assert o.hash_value() is None\n    assert o.write_to_stream(None) is None\n"
  },
  {
    "path": "tests/test_reader.py",
    "content": "\"\"\"Test the pypdf._reader module.\"\"\"\nimport io\nimport sys\nimport time\nfrom io import BytesIO\nfrom pathlib import Path\nfrom typing import Union\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf._crypt_providers import crypt_provider\nfrom pypdf._reader import convert_to_int\nfrom pypdf.constants import ImageAttributes as IA\nfrom pypdf.constants import PageAttributes as PG\nfrom pypdf.constants import UserAccessPermissions as UAP\nfrom pypdf.errors import (\n    DeprecationError,\n    EmptyFileError,\n    FileNotDecryptedError,\n    LimitReachedError,\n    PdfReadError,\n    PdfStreamError,\n    WrongPasswordError,\n)\nfrom pypdf.generic import (\n    ArrayObject,\n    Destination,\n    DictionaryObject,\n    IndirectObject,\n    NameObject,\n    NumberObject,\n    TextStringObject,\n)\n\nfrom . import RESOURCE_ROOT, SAMPLE_ROOT, get_data_from_url, normalize_warnings\n\nHAS_AES = crypt_provider[0] in [\"pycryptodome\", \"cryptography\"]\n\n\nNestedList = Union[int, None, list[\"NestedList\"]]\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"num_pages\"),\n    [(\"selenium-pypdf-issue-177.pdf\", 1), (\"pdflatex-outline.pdf\", 4)],\n)\ndef test_get_num_pages(src, num_pages):\n    src = RESOURCE_ROOT / src\n    with PdfReader(src) as reader:\n        assert len(reader.pages) == num_pages\n        # from #1911\n        assert \"/Size\" in reader.trailer\n\n\n@pytest.mark.parametrize(\n    (\"pdf_path\", \"expected\"),\n    [\n        (\n            RESOURCE_ROOT / \"crazyones.pdf\",\n            {\n                \"/CreationDate\": \"D:20150604133406-06'00'\",\n                \"/Creator\": \" XeTeX output 2015.06.04:1334\",\n                \"/Producer\": \"xdvipdfmx (20140317)\",\n            },\n        ),\n        (\n            RESOURCE_ROOT / \"metadata.pdf\",\n            {\n                \"/CreationDate\": \"D:20220415093243+02'00'\",\n                \"/ModDate\": \"D:20220415093243+02'00'\",\n                \"/Creator\": \"pdflatex, or other tool\",\n                \"/Producer\": \"Latex with hyperref, or other system\",\n                \"/Author\": \"Martin Thoma\",\n                \"/Keywords\": \"Some Keywords, other keywords; more keywords\",\n                \"/Subject\": \"The Subject\",\n                \"/Title\": \"The Title\",\n                \"/Trapped\": \"/False\",\n                \"/PTEX.Fullbanner\": (\n                    \"This is pdfTeX, Version \"\n                    \"3.141592653-2.6-1.40.23 (TeX Live 2021) \"\n                    \"kpathsea version 6.3.3\"\n                ),\n            },\n        ),\n    ],\n    ids=[\"crazyones\", \"metadata\"],\n)\ndef test_read_metadata(pdf_path, expected):\n    with open(pdf_path, \"rb\") as inputfile:\n        reader = PdfReader(inputfile)\n        docinfo = reader.metadata\n        assert docinfo is not None\n        metadict = dict(docinfo)\n        assert metadict == expected\n        docinfo.title\n        docinfo.title_raw\n        docinfo.author\n        docinfo.author_raw\n        docinfo.creator\n        docinfo.creator_raw\n        docinfo.producer\n        docinfo.producer_raw\n        docinfo.subject\n        docinfo.subject_raw\n        docinfo.creation_date\n        docinfo.creation_date_raw\n        docinfo.modification_date\n        docinfo.modification_date_raw\n        docinfo.keywords\n        docinfo.keywords_raw\n        if \"/Title\" in metadict:\n            assert isinstance(docinfo.title, str)\n            assert metadict[\"/Title\"] == docinfo.title\n\n\ndef test_read_metadata_title_is_utf8():\n    with open(RESOURCE_ROOT / \"bytes.pdf\", \"rb\") as inputfile:\n        reader = PdfReader(inputfile)\n        title = reader.metadata.title\n        # Should be a str.\n        assert title == \"Microsoft Word - トランスバース社買収電話会議英語Final.docx\"\n\n\ndef test_iss1943():\n    with PdfReader(RESOURCE_ROOT / \"crazyones.pdf\") as reader:\n        docinfo = reader.metadata\n        docinfo.update(\n            {\n                NameObject(\"/CreationDate\"): TextStringObject(\n                    \"D:20230705005151Z00'00'\"\n                ),\n                NameObject(\"/ModDate\"): TextStringObject(\"D:20230705005151Z00'00'\"),\n            }\n        )\n        docinfo.creation_date\n        docinfo.creation_date_raw\n        docinfo.modification_date\n        docinfo.modification_date_raw\n        docinfo.update({NameObject(\"/CreationDate\"): NumberObject(1)})\n        assert docinfo.creation_date is None\n\n\n@pytest.mark.samples\n@pytest.mark.parametrize(\n    \"pdf_path\", [SAMPLE_ROOT / \"017-unreadable-meta-data/unreadablemetadata.pdf\"]\n)\ndef test_broken_meta_data(pdf_path):\n    with open(pdf_path, \"rb\") as f:\n        reader = PdfReader(f)\n        assert reader.metadata is None\n\n    with open(RESOURCE_ROOT / \"crazyones.pdf\", \"rb\") as f:\n        b = f.read(-1)\n    reader = PdfReader(BytesIO(b.replace(b\"/Info 2 0 R\", b\"/Info 2    \")))\n    with pytest.raises(PdfReadError) as exc:\n        reader.metadata\n    assert \"does not point to a document information dictionary\" in repr(exc)\n\n\n@pytest.mark.parametrize(\n    \"src\",\n    [\n        RESOURCE_ROOT / \"crazyones.pdf\",\n        RESOURCE_ROOT / \"commented.pdf\",\n    ],\n)\ndef test_get_annotations(src):\n    with PdfReader(src) as reader:\n        for page in reader.pages:\n            if PG.ANNOTS in page:\n                for annot in page[PG.ANNOTS]:\n                    subtype = annot.get_object()[IA.SUBTYPE]\n                    if subtype == \"/Text\":\n                        annot.get_object()[PG.CONTENTS]\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"nb_attachments\"),\n    [\n        (RESOURCE_ROOT / \"attachment.pdf\", 1),\n        (RESOURCE_ROOT / \"crazyones.pdf\", 0),\n    ],\n)\ndef test_get_attachments(src, nb_attachments):\n    reader = PdfReader(src)\n\n    attachments = {}\n    for page in reader.pages:\n        if PG.ANNOTS in page:\n            for annotation in page[PG.ANNOTS]:\n                annotobj = annotation.get_object()\n                if annotobj[IA.SUBTYPE] == \"/FileAttachment\":\n                    fileobj = annotobj[\"/FS\"]\n                    attachments[fileobj[\"/F\"]] = fileobj[\"/EF\"][\"/F\"].get_data()\n    assert len(attachments) == nb_attachments\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"outline_elements\"),\n    [\n        (RESOURCE_ROOT / \"pdflatex-outline.pdf\", 9),\n        (RESOURCE_ROOT / \"crazyones.pdf\", 0),\n    ],\n)\ndef test_get_outline(src, outline_elements):\n    reader = PdfReader(src)\n    outline = reader.outline\n    assert len(outline) == outline_elements\n\n\n@pytest.mark.samples\n@pytest.mark.parametrize(\n    (\"src\", \"expected_images\"),\n    [\n        (\"pdflatex-outline.pdf\", []),\n        (\"crazyones.pdf\", []),\n        (\"git.pdf\", [\"Image9.png\"]),\n        pytest.param(\n            \"imagemagick-lzw.pdf\",\n            [\"Im0.png\"],\n            marks=pytest.mark.xfail(reason=\"broken image extraction\"),\n        ),\n        pytest.param(\n            \"imagemagick-ASCII85Decode.pdf\",\n            [\"Im0.png\"],\n            # marks=pytest.mark.xfail(reason=\"broken image extraction\"),\n        ),\n        (\"imagemagick-CCITTFaxDecode.pdf\", [\"Im0.tiff\"]),\n        (SAMPLE_ROOT / \"019-grayscale-image/grayscale-image.pdf\", [\"X0.png\"]),\n    ],\n)\ndef test_get_images(src, expected_images):\n    from PIL import Image  # noqa: PLC0415\n\n    src_abs = RESOURCE_ROOT / src\n    reader = PdfReader(src_abs)\n    page = reader.pages[0]\n    images_extracted = page.images\n\n    assert len(images_extracted) == len(expected_images)\n    for image, expected_image in zip(images_extracted, expected_images):\n        assert image.name == expected_image\n        assert (\n            image.name.split(\".\")[-1].upper()\n            == Image.open(io.BytesIO(image.data)).format\n        )\n\n\n@pytest.mark.parametrize(\n    (\"strict\", \"with_prev_0\", \"startx_correction\", \"should_fail\", \"warning_msgs\"),\n    [\n        (\n            True,\n            False,\n            -1,\n            False,\n            [\n                \"startxref on same line as offset\",\n                \"Xref table not zero-indexed. \"\n                \"ID numbers for objects will be corrected.\",\n            ],\n        ),  # all nominal => no fail\n        (True, True, -1, True, \"\"),  # Prev=0 => fail expected\n        (\n            False,\n            False,\n            -1,\n            False,\n            [\n                \"startxref on same line as offset\",\n            ],\n        ),\n        (\n            False,\n            True,\n            -1,\n            False,\n            [\n                \"startxref on same line as offset\",\n                \"/Prev=0 in the trailer - assuming there is no previous xref table\",\n            ],\n        ),  # Prev =0 => no strict so tolerant\n        (True, False, 0, True, \"\"),  # error on startxref, in strict => fail expected\n        (True, True, 0, True, \"\"),\n        (\n            False,\n            False,\n            0,\n            False,\n            [\n                \"startxref on same line as offset\",\n                \"incorrect startxref pointer(1)\",\n                \"parsing for Object Streams\",\n            ],\n        ),  # error on startxref, but no strict => xref rebuilt,no fail\n        (\n            False,\n            True,\n            0,\n            False,\n            [\n                \"startxref on same line as offset\",\n                \"incorrect startxref pointer(1)\",\n                \"parsing for Object Streams\",\n            ],\n        ),\n    ],\n)\ndef test_get_images_raw(\n    caplog, strict, with_prev_0, startx_correction, should_fail, warning_msgs\n):\n    pdf_data = (\n        b\"%%PDF-1.7\\n\"\n        b\"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\\n\"\n        b\"2 0 obj << >> endobj\\n\"\n        b\"3 0 obj << >> endobj\\n\"\n        b\"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]\"\n        b\" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R\"\n        b\" /Resources << /Font << >> >>\"\n        b\" /Rotate 0 /Type /Page >> endobj\\n\"\n        b\"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\\n\"\n        b\"xref 1 5\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"trailer << %s/Root 5 0 R /Size 6 >>\\n\"\n        b\"startxref %d\\n\"\n        b\"%%%%EOF\"\n    )\n    pdf_data = pdf_data % (\n        # - 1 below in the find because of the double %\n        pdf_data.find(b\"1 0 obj\") - 1,\n        pdf_data.find(b\"2 0 obj\") - 1,\n        pdf_data.find(b\"3 0 obj\") - 1,\n        pdf_data.find(b\"4 0 obj\") - 1,\n        pdf_data.find(b\"5 0 obj\") - 1,\n        b\"/Prev 0 \" if with_prev_0 else b\"\",\n        # startx_correction should be -1 due to double % at the beginning\n        # inducing an error on startxref computation\n        pdf_data.find(b\"xref\") + startx_correction,\n    )\n    pdf_stream = io.BytesIO(pdf_data)\n    if should_fail:\n        with pytest.raises(PdfReadError) as exc:\n            PdfReader(pdf_stream, strict=strict)\n        assert exc.type == PdfReadError\n        if startx_correction == -1:\n            assert (\n                exc.value.args[0]\n                == \"/Prev=0 in the trailer (try opening with strict=False)\"\n            )\n    else:\n        PdfReader(pdf_stream, strict=strict)\n        assert normalize_warnings(caplog.text) == warning_msgs\n\n\ndef test_issue297(caplog):\n    path = RESOURCE_ROOT / \"issue-297.pdf\"\n    with pytest.raises(PdfReadError) as exc:\n        reader = PdfReader(path, strict=True)\n    assert caplog.text == \"\"\n    assert \"Broken xref table\" in exc.value.args[0]\n    reader = PdfReader(path, strict=False)\n    assert normalize_warnings(caplog.text) == [\n        \"incorrect startxref pointer(1)\",\n        \"parsing for Object Streams\",\n    ]\n    reader.pages[0]\n\n\n@pytest.mark.parametrize(\n    (\"pdffile\", \"password\", \"should_fail\"),\n    [\n        (\"encrypted-file.pdf\", \"test\", False),\n        (\"encrypted-file.pdf\", b\"test\", False),\n        (\"encrypted-file.pdf\", \"qwerty\", True),\n        (\"encrypted-file.pdf\", b\"qwerty\", True),\n    ],\n)\ndef test_get_page_of_encrypted_file(pdffile, password, should_fail):\n    \"\"\"\n    Check if we can read a page of an encrypted file.\n\n    This is a regression test for issue 327:\n    IndexError for get_page() of decrypted file\n    \"\"\"\n    path = RESOURCE_ROOT / pdffile\n    if should_fail:\n        with pytest.raises(PdfReadError):\n            PdfReader(path, password=password)\n    else:\n        PdfReader(path, password=password).pages[0]\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"expected\", \"expected_get_fields\"),\n    [\n        (\n            \"form.pdf\",\n            {\"foo\": \"\"},\n            {\"foo\": {\"/DV\": \"\", \"/FT\": \"/Tx\", \"/T\": \"foo\", \"/V\": \"\"}},\n        ),\n        (\n            \"form_acrobatReader.pdf\",\n            {\"foo\": \"Bar\"},\n            {\"foo\": {\"/DV\": \"\", \"/FT\": \"/Tx\", \"/T\": \"foo\", \"/V\": \"Bar\"}},\n        ),\n        (\n            \"form_evince.pdf\",\n            {\"foo\": \"bar\"},\n            {\"foo\": {\"/DV\": \"\", \"/FT\": \"/Tx\", \"/T\": \"foo\", \"/V\": \"bar\"}},\n        ),\n        (\n            \"crazyones.pdf\",\n            {},\n            None,\n        )\n    ],\n)\ndef test_get_form(src, expected, expected_get_fields, txt_file_path):\n    \"\"\"Check if we can read out form data.\"\"\"\n    src = RESOURCE_ROOT / src\n    reader = PdfReader(src)\n    fields = reader.get_form_text_fields()\n    assert fields == expected\n\n    with open(txt_file_path, \"w\") as f:\n        fields = reader.get_fields(fileobj=f)\n    assert fields == expected_get_fields\n    if fields:\n        for field in fields.values():\n            # Just access the attributes\n            [\n                field.field_type,\n                field.parent,\n                field.kids,\n                field.name,\n                field.alternate_name,\n                field.mapping_name,\n                field.flags,\n                field.value,\n                field.default_value,\n                field.additional_actions,\n            ]\n\n\n@pytest.mark.enable_socket\ndef test_reading_choice_field_without_opt_key():\n    \"\"\"Tests reading a choice field in a PDF without an /Opt key.\"\"\"\n    url = \"https://github.com/user-attachments/files/23853677/Musterservicevertrag-HNRAGB_Okt2022-Blanko.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=\"Musterservicevertrag-HNRAGB_Okt2022-Blanko.pdf\")))\n    fields = reader.get_fields()\n\n    tn_anrede = fields.get(\"TN_Anrede\")\n    assert tn_anrede is not None\n\n    # Ensure that parsing of a choice field without /Opt key worked\n    tn_anrede_opt = tn_anrede.get(\"/Opt\")\n    assert tn_anrede_opt is None\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"page_number\"),\n    [\n        (\"form.pdf\", 0),\n        (\"pdflatex-outline.pdf\", 2),\n    ],\n)\ndef test_get_page_number(src, page_number):\n    src = RESOURCE_ROOT / src\n    reader = PdfReader(src)\n    reader.get_page(0)\n    page = reader.pages[page_number]\n    assert reader.get_page_number(page) == page_number\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"expected\"),\n    [(\"form.pdf\", None), (\"AutoCad_Simple.pdf\", \"/SinglePage\")],\n)\ndef test_get_page_layout(src, expected):\n    src = RESOURCE_ROOT / src\n    reader = PdfReader(src)\n    assert reader.page_layout == expected\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"expected\"),\n    [\n        (\"form.pdf\", \"/UseNone\"),\n        (\"crazyones.pdf\", None),\n    ],\n)\ndef test_get_page_mode(src, expected):\n    src = RESOURCE_ROOT / src\n    reader = PdfReader(src)\n    assert reader.page_mode == expected\n\n\ndef test_read_empty():\n    with pytest.raises(EmptyFileError) as exc:\n        PdfReader(io.BytesIO())\n    assert exc.value.args[0] == \"Cannot read an empty file\"\n\n\ndef test_read_malformed_header(caplog):\n    with pytest.raises(PdfReadError) as exc:\n        PdfReader(io.BytesIO(b\"foo\"), strict=True)\n    assert exc.value.args[0] == \"PDF starts with 'foo', but '%PDF-' expected\"\n    caplog.clear()\n    try:\n        PdfReader(io.BytesIO(b\"foo\"), strict=False)\n    except Exception:\n        pass\n    assert caplog.messages[0].startswith(\"invalid pdf header\")\n\n\ndef test_read_malformed_body():\n    with pytest.raises(PdfReadError) as exc:\n        PdfReader(io.BytesIO(b\"%PDF-\"), strict=True)\n    assert (\n        exc.value.args[0] == \"EOF marker not found\"\n    )  # used to be:STREAM_TRUNCATED_PREMATURELY\n\n\ndef test_read_prev_0_trailer():\n    pdf_data = (\n        b\"%%PDF-1.7\\n\"\n        b\"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\\n\"\n        b\"2 0 obj << >> endobj\\n\"\n        b\"3 0 obj << >> endobj\\n\"\n        b\"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]\"\n        b\" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R\"\n        b\" /Resources << /Font << >> >>\"\n        b\" /Rotate 0 /Type /Page >> endobj\\n\"\n        b\"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\\n\"\n        b\"xref 1 5\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"trailer << %s/Root 5 0 R /Size 6 >>\\n\"\n        b\"startxref %d\\n\"\n        b\"%%%%EOF\"\n    )\n    with_prev_0 = True\n    pdf_data = pdf_data % (\n        pdf_data.find(b\"1 0 obj\"),\n        pdf_data.find(b\"2 0 obj\"),\n        pdf_data.find(b\"3 0 obj\"),\n        pdf_data.find(b\"4 0 obj\"),\n        pdf_data.find(b\"5 0 obj\"),\n        b\"/Prev 0 \" if with_prev_0 else b\"\",\n        pdf_data.find(b\"xref\") - 1,\n    )\n    pdf_stream = io.BytesIO(pdf_data)\n    with pytest.raises(PdfReadError) as exc:\n        PdfReader(pdf_stream, strict=True)\n    assert exc.value.args[0] == \"/Prev=0 in the trailer (try opening with strict=False)\"\n\n\ndef test_circular_xref_prev_reference(caplog):\n    \"\"\"Circular /Prev in trailer should be detected, not loop forever (#3654).\"\"\"\n    pdf_data = (\n        b\"%%PDF-1.7\\n\"\n        b\"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\\n\"\n        b\"2 0 obj << >> endobj\\n\"\n        b\"3 0 obj << >> endobj\\n\"\n        b\"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]\"\n        b\" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R\"\n        b\" /Resources << /Font << >> >>\"\n        b\" /Rotate 0 /Type /Page >> endobj\\n\"\n        b\"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\\n\"\n        b\"xref 1 5\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"trailer << /Prev %d /Root 5 0 R /Size 6 >>\\n\"\n        b\"startxref %d\\n\"\n        b\"%%%%EOF\"\n    )\n    xref_offset = pdf_data.find(b\"xref\") - 1\n    pdf_data = pdf_data % (\n        pdf_data.find(b\"1 0 obj\"),\n        pdf_data.find(b\"2 0 obj\"),\n        pdf_data.find(b\"3 0 obj\"),\n        pdf_data.find(b\"4 0 obj\"),\n        pdf_data.find(b\"5 0 obj\"),\n        xref_offset,  # /Prev points to same xref = circular\n        xref_offset,  # startxref\n    )\n    PdfReader(io.BytesIO(pdf_data))\n    assert \"Circular xref chain detected\" in caplog.text\n\n\ndef test_read_missing_startxref():\n    pdf_data = (\n        b\"%%PDF-1.7\\n\"\n        b\"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\\n\"\n        b\"2 0 obj << >> endobj\\n\"\n        b\"3 0 obj << >> endobj\\n\"\n        b\"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]\"\n        b\" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R\"\n        b\" /Resources << /Font << >> >>\"\n        b\" /Rotate 0 /Type /Page >> endobj\\n\"\n        b\"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\\n\"\n        b\"xref 1 5\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"trailer << /Root 5 0 R /Size 6 >>\\n\"\n        # Removed for this test: b\"startxref %d\\n\"\n        b\"%%%%EOF\"\n    )\n    pdf_data = pdf_data % (\n        pdf_data.find(b\"1 0 obj\"),\n        pdf_data.find(b\"2 0 obj\"),\n        pdf_data.find(b\"3 0 obj\"),\n        pdf_data.find(b\"4 0 obj\"),\n        pdf_data.find(b\"5 0 obj\"),\n        # Removed for this test: pdf_data.find(b\"xref\") - 1,\n    )\n    pdf_stream = io.BytesIO(pdf_data)\n    with pytest.raises(PdfReadError) as exc:\n        PdfReader(pdf_stream, strict=True)\n    assert exc.value.args[0] == \"startxref not found\"\n\n\ndef test_read_unknown_zero_pages(caplog):\n    pdf_data = (\n        b\"%%PDF-1.7\\n\"\n        b\"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\\n\"\n        b\"2 0 obj << >> endobj\\n\"\n        b\"3 0 obj << >> endobj\\n\"\n        b\"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]\"\n        b\" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R\"\n        b\" /Resources << /Font << >> >>\"\n        b\" /Rotate 0 /Type /Page >> endobj\\n\"\n        # Pages 0 0 is the key point:\n        b\"5 0 obj << /Pages 0 0 R /Type /Catalog >> endobj\\n\"\n        b\"xref 1 5\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"trailer << /Root 5 1 R /Size 6 >>\\n\"\n        b\"startxref %d\\n\"\n        b\"%%%%EOF\"\n    )\n    pdf_data = pdf_data % (\n        pdf_data.find(b\"1 0 obj\") - 1,\n        pdf_data.find(b\"2 0 obj\") - 1,\n        pdf_data.find(b\"3 0 obj\") - 1,\n        pdf_data.find(b\"4 0 obj\") - 1,\n        pdf_data.find(b\"5 0 obj\") - 1,\n        pdf_data.find(b\"xref\") - 1,\n    )\n    pdf_stream = io.BytesIO(pdf_data)\n    reader = PdfReader(pdf_stream, strict=True)\n    warnings = [\n        \"startxref on same line as offset\",\n        \"Xref table not zero-indexed. ID numbers for objects will be corrected.\",\n    ]\n    assert normalize_warnings(caplog.text) == warnings\n    with pytest.raises(PdfReadError) as exc:\n        len(reader.pages)\n\n    assert exc.value.args[0] == \"Could not find object.\"\n    reader = PdfReader(pdf_stream, strict=False)\n    warnings += [\n        \"Object 5 1 not defined.\",\n        \"startxref on same line as offset\",\n    ]\n    assert normalize_warnings(caplog.text) == warnings\n    with pytest.raises(PdfReadError) as exc:\n        len(reader.pages)\n    assert exc.value.args[0] == \"Invalid object in /Pages\"\n\n\ndef test_read_encrypted_without_decryption():\n    src = RESOURCE_ROOT / \"libreoffice-writer-password.pdf\"\n    reader = PdfReader(src)\n    with pytest.raises(FileNotDecryptedError) as exc:\n        len(reader.pages)\n    assert exc.value.args[0] == \"File has not been decrypted\"\n\n\ndef test_get_destination_page_number():\n    src = RESOURCE_ROOT / \"pdflatex-outline.pdf\"\n    reader = PdfReader(src)\n    outline = reader.outline\n    for outline_item in outline:\n        if not isinstance(outline_item, list):\n            reader.get_destination_page_number(outline_item)\n\n\ndef test_do_not_get_stuck_on_large_files_without_start_xref():\n    \"\"\"\n    Tests for the absence of a DoS bug, where a large file without an\n    startxref mark would cause the library to hang for minutes to hours.\n    \"\"\"\n    start_time = time.time()\n    broken_stream = BytesIO(b\"\\0\" * 5 * 1000 * 1000)\n    with pytest.raises(PdfReadError):\n        PdfReader(broken_stream)\n    parse_duration = time.time() - start_time\n    # parsing is expected take less than a second on a modern cpu, but include\n    # a large tolerance to account for busy or slow systems\n    assert parse_duration < 60\n\n\n@pytest.mark.enable_socket\ndef test_decrypt_when_no_id():\n    \"\"\"\n    Decrypt an encrypted file that's missing the 'ID' value in its trailer.\n\n    https://github.com/py-pdf/pypdf/issues/608\n    \"\"\"\n    with open(RESOURCE_ROOT / \"encrypted_doc_no_id.pdf\", \"rb\") as inputfile:\n        ipdf = PdfReader(inputfile)\n        ipdf.decrypt(\"\")\n        assert ipdf.metadata == {\"/Producer\": \"European Patent Office\"}\n\n\ndef test_reader_properties():\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    assert reader.outline == []\n    assert len(reader.pages) == 1\n    assert reader.page_layout is None\n    assert reader.page_mode is None\n    assert reader.is_encrypted is False\n\n\n@pytest.mark.parametrize(\n    \"strict\",\n    [True, False],\n)\ndef test_issue604(caplog, strict):\n    \"\"\"Test with invalid destinations.\"\"\"\n    with open(RESOURCE_ROOT / \"issue-604.pdf\", \"rb\") as f:\n        pdf = None\n        outline = None\n        if strict:\n            pdf = PdfReader(f, strict=strict)\n            with pytest.raises(PdfReadError) as exc:\n                outline = pdf.outline\n            if \"Unknown Destination\" not in exc.value.args[0]:\n                raise Exception(\"Expected exception not raised\")\n            return  # outline is not correct\n        pdf = PdfReader(f, strict=strict)\n        outline = pdf.outline\n        msg = [\n            \"Unknown destination: 'ms_Thyroid_2_2020_071520_watermarked.pdf' [0, 1]\"\n        ]\n        assert normalize_warnings(caplog.text) == msg\n\n        def get_dest_pages(x) -> NestedList:\n            if isinstance(x, list):\n                return [get_dest_pages(y) for y in x]\n            destination_page_number = pdf.get_destination_page_number(x)\n            if destination_page_number is None:\n                return destination_page_number\n            return destination_page_number + 1\n\n        out = []\n\n        # oi can be destination or a list:preferred to just print them\n        for oi in outline:\n            out.append(get_dest_pages(oi))  # noqa: PERF401\n\n\ndef test_decode_permissions():\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    base = {\n        \"accessability\": False,  # Do not fix typo, as part of official, but deprecated API.\n        \"annotations\": False,\n        \"assemble\": False,\n        \"copy\": False,\n        \"forms\": False,\n        \"modify\": False,\n        \"print_high_quality\": False,\n        \"print\": False,\n    }\n\n    print_ = base.copy()\n    print_[\"print\"] = True\n    with pytest.raises(\n        DeprecationError,\n            match=(\n                r\"decode_permissions is deprecated and was removed in pypdf 5\\.0\\.0\\. \"\n                r\"Use user_access_permissions instead\"\n            ),\n    ):\n        assert reader.decode_permissions(4) == print_\n\n    modify = base.copy()\n    modify[\"modify\"] = True\n    with pytest.raises(\n        DeprecationError,\n        match=(\n            r\"decode_permissions is deprecated and was removed in pypdf 5\\.0\\.0\\. \"\n            r\"Use user_access_permissions instead\"\n        ),\n    ):\n        assert reader.decode_permissions(8) == modify\n\n\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES implementation\")\ndef test_user_access_permissions():\n    # Not encrypted.\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    assert reader.user_access_permissions is None\n\n    # Encrypted.\n    reader = PdfReader(RESOURCE_ROOT / \"encryption\" / \"r6-owner-password.pdf\")\n    assert reader.user_access_permissions == UAP.all()\n\n    # Custom writer permissions.\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n    writer.encrypt(\n        user_password=\"\",\n        owner_password=\"abc\",\n        permissions_flag=UAP.PRINT | UAP.FILL_FORM_FIELDS,\n    )\n    output = BytesIO()\n    writer.write(output)\n    reader = PdfReader(output)\n    assert reader.user_access_permissions == (UAP.PRINT | UAP.FILL_FORM_FIELDS)\n\n    # All writer permissions.\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n    writer.encrypt(\n        user_password=\"\",\n        owner_password=\"abc\",\n        permissions_flag=UAP.all(),\n    )\n    output = BytesIO()\n    writer.write(output)\n    reader = PdfReader(output)\n    assert reader.user_access_permissions == UAP.all()\n\n\ndef test_pages_attribute():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n\n    # Test if getting as slice throws an error\n    assert len(reader.pages[:]) == 1\n\n    with pytest.raises(IndexError) as exc:\n        reader.pages[-1000]\n\n    assert exc.value.args[0] == \"Sequence index out of range\"\n\n    with pytest.raises(IndexError):\n        reader.pages[1000]\n\n    assert exc.value.args[0] == \"Sequence index out of range\"\n\n\ndef test_convert_to_int():\n    assert convert_to_int(b\"\\x01\", 8) == 1\n\n\ndef test_convert_to_int_error():\n    with pytest.raises(PdfReadError) as exc:\n        convert_to_int(b\"256\", 16)\n    assert exc.value.args[0] == \"Invalid size in convert_to_int\"\n\n\n@pytest.mark.enable_socket\ndef test_iss925():\n    url = \"https://github.com/py-pdf/pypdf/files/8796328/1.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=\"iss925.pdf\")))\n\n    for page_sliced in reader.pages:\n        page_object = page_sliced.get_object()\n        # Extracts the PDF's Annots (Annotations and Commenting):\n        annots = page_object.get(\"/Annots\")\n        if annots is not None:\n            for annot in annots:\n                annot.get_object()\n\n\ndef test_get_object():\n    reader = PdfReader(RESOURCE_ROOT / \"hello-world.pdf\")\n    assert reader.get_object(22)[\"/Type\"] == \"/Catalog\"\n    assert reader._get_indirect_object(22, 0)[\"/Type\"] == \"/Catalog\"\n\n\ndef test_extract_text_hello_world():\n    reader = PdfReader(RESOURCE_ROOT / \"hello-world.pdf\")\n    text = reader.pages[0].extract_text().split(\"\\n\")\n    assert text == [\n        \"English:\",\n        \"Hello World\",\n        \"Arabic:\",\n        \"مرحبا بالعالم\",\n        \"Russian:\",\n        \"Привет, мир\",\n        \"Chinese (traditional):\",\n        \"你好世界\",\n        \"Thai:\",\n        \"สวัสดีชาวโลก\",\n        \"Japanese:\",\n        \"こんにちは世界\",\n    ]\n\n\ndef test_read_path():\n    path = Path(RESOURCE_ROOT, \"crazyones.pdf\")\n    reader = PdfReader(path)\n    assert len(reader.pages) == 1\n\n\ndef test_read_not_binary_mode(caplog):\n    with open(RESOURCE_ROOT / \"crazyones.pdf\") as f:\n        msg = (\n            \"PdfReader stream/file object is not in binary mode. \"\n            \"It may not be read correctly.\"\n        )\n        with pytest.raises(io.UnsupportedOperation):\n            PdfReader(f)\n    assert normalize_warnings(caplog.text) == [msg]\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(not HAS_AES, reason=\"No AES algorithm available\")\ndef test_read_form_416():\n    url = (\n        \"https://www.fda.gov/downloads/AboutFDA/ReportsManualsForms/Forms/UCM074728.pdf\"\n    )\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=\"issue_416.pdf\")))\n    fields = reader.get_form_text_fields()\n    assert len(fields) > 0\n\n\ndef test_form_topname_with_and_without_acroform(caplog):\n    r = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    r.add_form_topname(\"no\")\n    r.rename_form_topname(\"renamed\")\n    assert \"/AcroForm\" not in r.trailer[\"/Root\"]\n    r.trailer[\"/Root\"][NameObject(\"/AcroForm\")] = DictionaryObject()\n    r.add_form_topname(\"toto\")\n    r.rename_form_topname(\"renamed\")\n    assert len(r.get_fields()) == 0\n\n    r = PdfReader(RESOURCE_ROOT / \"form.pdf\")\n    r.add_form_topname(\"top\")\n    flds = r.get_fields()\n    assert \"top\" in flds\n    assert \"top.foo\" in flds\n    r.rename_form_topname(\"renamed\")\n    flds = r.get_fields()\n    assert \"renamed\" in flds\n    assert \"renamed.foo\" in flds\n\n    r = PdfReader(RESOURCE_ROOT / \"form.pdf\")\n    r.get_fields()[\"foo\"].indirect_reference.get_object()[\n        NameObject(\"/Parent\")\n    ] = DictionaryObject()\n    r.add_form_topname(\"top\")\n    assert \"have a non-expected parent\" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_extract_text_xref_issue_2(caplog):\n    # pdf/0264cf510015b2a4b395a15cb23c001e.pdf\n    url = \"https://github.com/user-attachments/files/18381758/tika-981961.pdf\"\n    msg = [\n        \"incorrect startxref pointer(2)\",\n        \"parsing for Object Streams\",\n    ]\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=\"tika-981961.pdf\")))\n    for page in reader.pages:\n        page.extract_text()\n    assert normalize_warnings(caplog.text) == msg\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\ndef test_extract_text_xref_issue_3(caplog):\n    # pdf/0264cf510015b2a4b395a15cb23c001e.pdf\n    url = \"https://github.com/user-attachments/files/18381755/tika-977774.pdf\"\n    msg = [\n        \"incorrect startxref pointer(3)\",\n    ]\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=\"tika-977774.pdf\")))\n    for page in reader.pages:\n        page.extract_text()\n    assert normalize_warnings(caplog.text) == msg\n\n\n@pytest.mark.enable_socket\ndef test_extract_text_pdf15():\n    # pdf/0264cf510015b2a4b395a15cb23c001e.pdf\n    url = \"https://github.com/user-attachments/files/18381751/tika-976030.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=\"tika-976030.pdf\")))\n    for page in reader.pages:\n        page.extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_extract_text_xref_table_21_bytes_clrf():\n    # pdf/0264cf510015b2a4b395a15cb23c001e.pdf\n    url = \"https://github.com/user-attachments/files/18381723/tika-956939.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=\"tika-956939.pdf\")))\n    for page in reader.pages:\n        page.extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_get_fields():\n    url = \"https://github.com/user-attachments/files/18381747/tika-972486.pdf\"\n    name = \"tika-972486.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    fields = reader.get_fields()\n    assert fields is not None\n    assert \"c1-1\" in fields\n    assert dict(fields[\"c1-1\"]) == (\n        {\"/FT\": \"/Btn\", \"/T\": \"c1-1\", \"/_States_\": [\"/On\", \"/Off\"]}\n    )\n\n\n@pytest.mark.enable_socket\ndef test_get_full_qualified_fields():\n    url = \"https://github.com/py-pdf/pypdf/files/10142389/fields_with_dots.pdf\"\n    name = \"fields_with_dots.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    fields = reader.get_form_text_fields(True)\n    assert fields is not None\n    assert \"customer.name\" in fields\n\n    fields = reader.get_form_text_fields(False)\n    assert fields is not None\n    assert \"customer.name\" not in fields\n    assert \"name\" in fields\n\n    fields = reader.get_fields(True)\n    assert fields is not None\n    assert \"customer.name\" in fields\n    assert fields[\"customer.name\"][\"/T\"] == \"name\"\n\n\n@pytest.mark.enable_socket\n@pytest.mark.filterwarnings(\"ignore::pypdf.errors.PdfReadWarning\")\ndef test_get_fields_read_else_block():\n    # covers also issue 1089\n    url = \"https://github.com/user-attachments/files/18381705/tika-934771.pdf\"\n    name = \"tika-934771.pdf\"\n    PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n\n@pytest.mark.enable_socket\ndef test_get_fields_read_else_block2():\n    url = \"https://github.com/user-attachments/files/18381689/tika-914902.pdf\"\n    name = \"tika-914902.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    fields = reader.get_fields()\n    assert fields is None\n\n\n@pytest.mark.enable_socket\n@pytest.mark.filterwarnings(\"ignore::pypdf.errors.PdfReadWarning\")\ndef test_get_fields_read_else_block3():\n    url = \"https://github.com/user-attachments/files/18381726/tika-957721.pdf\"\n    name = \"tika-957721.pdf\"\n    PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n\n@pytest.mark.enable_socket\ndef test_metadata_is_none():\n    url = \"https://github.com/user-attachments/files/18381735/tika-963692.pdf\"\n    name = \"tika-963692.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert reader.metadata is None\n\n\n@pytest.mark.enable_socket\ndef test_get_fields_read_write_report(txt_file_path):\n    url = \"https://github.com/user-attachments/files/18381683/tika-909655.pdf\"\n    name = \"tika-909655.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    with open(txt_file_path, \"w\") as fp:\n        fields = reader.get_fields(fileobj=fp)\n    assert fields\n\n\n@pytest.mark.parametrize(\n    \"src\",\n    [\n        RESOURCE_ROOT / \"crazyones.pdf\",\n        RESOURCE_ROOT / \"commented.pdf\",\n    ],\n)\ndef test_xfa(src):\n    reader = PdfReader(src)\n    assert reader.xfa is None\n\n\n@pytest.mark.enable_socket\ndef test_xfa_non_empty():\n    url = \"https://github.com/user-attachments/files/18381713/tika-942050.pdf\"\n    name = \"tika-942050.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert list(reader.xfa.keys()) == [\n        \"preamble\",\n        \"config\",\n        \"template\",\n        \"PDFSecurity\",\n        \"datasets\",\n        \"postamble\",\n    ]\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"pdf_header\"),\n    [\n        (RESOURCE_ROOT / \"attachment.pdf\", \"%PDF-1.5\"),\n        (RESOURCE_ROOT / \"crazyones.pdf\", \"%PDF-1.5\"),\n    ],\n)\ndef test_header(src, pdf_header):\n    reader = PdfReader(src)\n\n    assert reader.pdf_header == pdf_header\n\n\n@pytest.mark.enable_socket\ndef test_outline_color():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"tika-924546.pdf\")))\n    assert reader.outline[0].color == [0, 0, 1]\n\n\n@pytest.mark.enable_socket\ndef test_outline_font_format():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"tika-924546.pdf\")))\n    assert reader.outline[0].font_format == 2\n\n\ndef get_outline_property(outline, attribute_name: str):\n    results = []\n    if isinstance(outline, list):\n        for outline_item in outline:\n            if isinstance(outline_item, Destination):\n                results.append(getattr(outline_item, attribute_name))\n            else:\n                results.append(get_outline_property(outline_item, attribute_name))\n    else:\n        raise ValueError(f\"got {type(outline)}\")\n    return results\n\n\n@pytest.mark.samples\ndef test_outline_title_issue_1121():\n    reader = PdfReader(SAMPLE_ROOT / \"014-outlines/mistitled_outlines_example.pdf\")\n\n    assert get_outline_property(reader.outline, \"title\") == [\n        \"First\",\n        [\n            \"Second\",\n            \"Third\",\n            \"Fourth\",\n            [\n                \"Fifth\",\n                \"Sixth\",\n            ],\n            \"Seventh\",\n            [\n                \"Eighth\",\n                \"Ninth\",\n            ],\n        ],\n        \"Tenth\",\n        [\n            \"Eleventh\",\n            \"Twelfth\",\n            \"Thirteenth\",\n            \"Fourteenth\",\n        ],\n        \"Fifteenth\",\n        [\n            \"Sixteenth\",\n            \"Seventeenth\",\n        ],\n        \"Eighteenth\",\n        \"Nineteenth\",\n        [\n            \"Twentieth\",\n            \"Twenty-first\",\n            \"Twenty-second\",\n            \"Twenty-third\",\n            \"Twenty-fourth\",\n            \"Twenty-fifth\",\n            \"Twenty-sixth\",\n            \"Twenty-seventh\",\n        ],\n    ]\n\n\n@pytest.mark.samples\ndef test_outline_count():\n    reader = PdfReader(SAMPLE_ROOT / \"014-outlines/mistitled_outlines_example.pdf\")\n\n    assert get_outline_property(reader.outline, \"outline_count\") == [\n        5,\n        [\n            None,\n            None,\n            2,\n            [\n                None,\n                None,\n            ],\n            -2,\n            [\n                None,\n                None,\n            ],\n        ],\n        4,\n        [\n            None,\n            None,\n            None,\n            None,\n        ],\n        -2,\n        [\n            None,\n            None,\n        ],\n        None,\n        8,\n        [\n            None,\n            None,\n            None,\n            None,\n            None,\n            None,\n            None,\n            None,\n        ],\n    ]\n\n\ndef test_outline_missing_title(caplog):\n    # Strict\n    reader = PdfReader(RESOURCE_ROOT / \"outline-without-title.pdf\", strict=True)\n    with pytest.raises(PdfReadError) as exc:\n        reader.outline\n    assert exc.value.args[0].startswith(\"Outline Entry Missing /Title attribute:\")\n\n    # Non-strict : no errors\n    reader = PdfReader(RESOURCE_ROOT / \"outline-without-title.pdf\", strict=False)\n    assert reader.outline[0][\"/Title\"] == \"\"\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        # 1st case : the named_dest are stored directly as a dictionary, PDF 1.1 style\n        (\n            \"https://github.com/py-pdf/pypdf/files/9197028/lorem_ipsum.pdf\",\n            \"lorem_ipsum.pdf\",\n        ),\n        # 2nd case : Dest below names and with Kids...\n        (\n            \"https://github.com/py-pdf/pypdf/files/11714214/PDF32000_2008.pdf\",\n            \"PDF32000_2008.pdf\",\n        )\n        # 3rd case : Dests with Name tree (TODO: Add this case)\n    ],\n    ids=[\"stored_directly\", \"dest_below_names_with_kids\"],\n)\ndef test_named_destination(url, name):\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert len(reader.named_destinations) > 0\n\n\n@pytest.mark.enable_socket\ndef test_outline_with_missing_named_destination():\n    url = \"https://github.com/user-attachments/files/18381686/tika-913678.pdf\"\n    name = \"tika-913678.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    # outline items in document reference a named destination that is not defined\n    assert reader.outline[1][0].title.startswith(\"Report for 2002AZ3B: Microbial\")\n\n\n@pytest.mark.enable_socket\ndef test_outline_with_empty_action():\n    url = \"https://github.com/user-attachments/files/18381697/tika-924546.pdf\"\n    name = \"tika-924546.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    # outline items (entitled Tables and Figures) utilize an empty action (/A)\n    # that has no type or destination\n    assert reader.outline[-4].title == \"Tables\"\n\n\ndef test_outline_with_invalid_destinations():\n    reader = PdfReader(RESOURCE_ROOT / \"outlines-with-invalid-destinations.pdf\")\n    # contains 9 outline items, 6 with invalid destinations\n    # caused by different malformations\n    assert len(reader.outline) == 9\n\n\n@pytest.mark.enable_socket\ndef test_pdfreader_multiple_definitions(caplog):\n    \"\"\"iss325\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/9176644/multipledefs.pdf\"\n    name = \"multipledefs.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].extract_text()\n    assert normalize_warnings(caplog.text) == [\n        \"Multiple definitions in dictionary at byte 0xb5 for key /Group\"\n    ]\n\n\ndef test_wrong_password_error():\n    encrypted_pdf_path = RESOURCE_ROOT / \"encrypted-file.pdf\"\n    with pytest.raises(WrongPasswordError):\n        PdfReader(\n            encrypted_pdf_path,\n            password=\"definitely_the_wrong_password!\",\n        )\n\n\ndef test_get_page_number_by_indirect():\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    reader._get_page_number_by_indirect(1)\n\n\n@pytest.mark.enable_socket\ndef test_corrupted_xref_table():\n    # issue #1292\n    url = \"https://github.com/py-pdf/pypdf/files/9444747/BreezeManual.orig.pdf\"\n    name = \"BreezeMan1.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].extract_text()\n    url = \"https://github.com/py-pdf/pypdf/files/9444748/BreezeManual.failed.pdf\"\n    name = \"BreezeMan2.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_reader(caplog):\n    # iss #1273\n    url = \"https://github.com/py-pdf/pypdf/files/9464742/shiv_resume.pdf\"\n    name = \"shiv_resume.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert \"Previous trailer cannot be read\" in caplog.text\n    caplog.clear()\n    # first call requires some reparations...\n    reader.pages[0].extract_text()\n    caplog.clear()\n    # ...and now no more required\n    reader.pages[0].extract_text()\n    assert caplog.text == \"\"\n\n\n@pytest.mark.enable_socket\ndef test_zeroing_xref():\n    # iss #328\n    url = (\n        \"https://github.com/py-pdf/pypdf/files/9066120/\"\n        \"UTA_OSHA_3115_Fall_Protection_Training_09162021_.pdf\"\n    )\n    name = \"UTA_OSHA.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    len(reader.pages)\n\n\n@pytest.mark.enable_socket\ndef test_thread():\n    url = (\n        \"https://github.com/py-pdf/pypdf/files/9066120/\"\n        \"UTA_OSHA_3115_Fall_Protection_Training_09162021_.pdf\"\n    )\n    name = \"UTA_OSHA.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert reader.threads is None\n    url = \"https://github.com/user-attachments/files/18381699/tika-924666.pdf\"\n    name = \"tika-924666.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert isinstance(reader.threads, ArrayObject)\n    assert len(reader.threads) >= 1\n\n\n@pytest.mark.enable_socket\ndef test_build_outline_item(caplog):\n    url = \"https://github.com/py-pdf/pypdf/files/9464742/shiv_resume.pdf\"\n    name = \"shiv_resume.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    outline = reader._build_outline_item(\n        DictionaryObject(\n            {\n                NameObject(\"/Title\"): TextStringObject(\"Toto\"),\n                NameObject(\"/Dest\"): NumberObject(2),\n            }\n        )\n    )\n    assert \"Removed unexpected destination 2 from destination\" in caplog.text\n    assert outline[\"/Title\"] == \"Toto\"\n    reader.strict = True\n    with pytest.raises(PdfReadError) as exc:\n        reader._build_outline_item(\n            DictionaryObject(\n                {\n                    NameObject(\"/Title\"): TextStringObject(\"Toto\"),\n                    NameObject(\"/Dest\"): NumberObject(2),\n                }\n            )\n        )\n    assert \"Unexpected destination 2\" in exc.value.args[0]\n\n\n@pytest.mark.samples\n@pytest.mark.parametrize(\n    (\"src\", \"page_labels\"),\n    [\n        (RESOURCE_ROOT / \"selenium-pypdf-issue-177.pdf\", [\"1\"]),\n        (RESOURCE_ROOT / \"encrypted_doc_no_id.pdf\", [\"1\", \"2\", \"3\"]),\n        (RESOURCE_ROOT / \"pdflatex-outline.pdf\", [\"1\", \"2\", \"3\", \"4\"]),\n        (\n            SAMPLE_ROOT / \"009-pdflatex-geotopo/GeoTopo.pdf\",\n            [\"i\", \"ii\", \"iii\", \"1\", \"2\", \"3\"],\n        ),\n    ],\n    ids=[\n        \"selenium-pypdf-issue-177.pdf\",\n        \"encrypted_doc_no_id.pdf\",\n        \"pdflatex-outline.pdf\",\n        \"GeoTopo.pdf\",\n    ],\n)\ndef test_page_labels(src, page_labels):\n    max_indices = 6\n    assert PdfReader(src).page_labels[:max_indices] == page_labels[:max_indices]\n\n\n@pytest.mark.enable_socket\ndef test_iss1559():\n    url = \"https://github.com/py-pdf/pypdf/files/10441992/default.pdf\"\n    name = \"iss1559.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    for p in reader.pages:\n        p.extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_iss1652():\n    # test of an annotation(link) directly stored in the /Annots in the page\n    url = \"https://github.com/py-pdf/pypdf/files/10818844/tt.pdf\"\n    name = \"invalidNamesDest.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.named_destinations\n\n\n@pytest.mark.enable_socket\ndef test_iss1689():\n    url = \"https://github.com/py-pdf/pypdf/files/10948283/error_file_without_data.pdf\"\n    name = \"iss1689.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0]\n\n\n@pytest.mark.enable_socket\ndef test_iss1710():\n    url = \"https://github.com/py-pdf/pypdf/files/15234776/irbookonlinereading.pdf\"\n    name = \"irbookonlinereading.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.outline\n\n\ndef test_broken_file_header():\n    pdf_data = (\n        b\"%%PDF-\\xa0sd\\n\"\n        b\"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\\n\"\n        b\"2 0 obj << >> endobj\\n\"\n        b\"3 0 obj << >> endobj\\n\"\n        b\"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]\"\n        b\" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R\"\n        b\" /Resources << /Font << >> >>\"\n        b\" /Rotate 0 /Type /Page >> endobj\\n\"\n        b\"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\\n\"\n        b\"xref 1 5\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"trailer << %s/Root 5 0 R /Size 6 >>\\n\"\n        b\"startxref %d\\n\"\n        b\"%%%%EOF\"\n    )\n    with_prev_0 = True\n    pdf_data = pdf_data % (\n        pdf_data.find(b\"1 0 obj\"),\n        pdf_data.find(b\"2 0 obj\"),\n        pdf_data.find(b\"3 0 obj\"),\n        pdf_data.find(b\"4 0 obj\"),\n        pdf_data.find(b\"5 0 obj\"),\n        b\"/Prev 0 \" if with_prev_0 else b\"\",\n        pdf_data.find(b\"xref\") - 1,\n    )\n    PdfReader(io.BytesIO(pdf_data))\n\n\n@pytest.mark.enable_socket\ndef test_iss1756():\n    url = \"https://github.com/py-pdf/pypdf/files/11105591/641-Attachment-B-Pediatric-Cardiac-Arrest-8-1-2019.pdf\"\n    name = \"iss1756.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.trailer[\"/ID\"]\n    # removed to cope with missing cryptodome during commit check : len(reader.pages)\n\n\n@pytest.mark.enable_socket\n@pytest.mark.timeout(30)\ndef test_iss1825():\n    url = \"https://github.com/py-pdf/pypdf/files/11367871/MiFO_LFO_FEIS_NOA_Published.3.pdf\"\n    name = \"iss1825.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[0]\n    page.extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_iss2082():\n    url = \"https://github.com/py-pdf/pypdf/files/12317939/test.pdf\"\n    name = \"iss2082.pdf\"\n    b = get_data_from_url(url, name=name)\n    reader = PdfReader(BytesIO(b))\n    reader.pages[0].extract_text()\n\n    bb = bytearray(b)\n    bb[b.find(b\"xref\") + 2] = ord(b\"E\")\n    with pytest.raises(PdfReadError):\n        reader = PdfReader(BytesIO(bb))\n\n\n@pytest.mark.enable_socket\ndef test_issue_140():\n    url = \"https://github.com/py-pdf/pypdf/files/12168578/bad_pdf_example.pdf\"\n    name = \"issue-140.pdf\"\n    b = get_data_from_url(url, name=name)\n    reader = PdfReader(BytesIO(b))\n    assert len(reader.pages) == 54\n\n\n@pytest.mark.enable_socket\ndef test_xyz_with_missing_param():\n    \"\"\"Cf #2236\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12795356/tt1.pdf\"\n    name = \"issue2236.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert reader.outline[0][\"/Left\"] == 820\n    assert reader.outline[0][\"/Top\"] == 0\n    assert reader.outline[1][\"/Left\"] == 0\n    assert reader.outline[0][\"/Top\"] == 0\n\n\n@pytest.mark.enable_socket\ndef test_corrupted_xref():\n    url = \"https://github.com/py-pdf/pypdf/files/14628314/iss2516.pdf\"\n    name = \"iss2516.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert reader.root_object[\"/Type\"] == \"/Catalog\"\n\n\n@pytest.mark.enable_socket\ndef test_truncated_xref(caplog):\n    url = \"https://github.com/py-pdf/pypdf/files/14843553/002-trivial-libre-office-writer-broken.pdf\"\n    name = \"iss2575.pdf\"\n    PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert \"Invalid/Truncated xref table. Rebuilding it.\" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_damaged_pdf():\n    url = \"https://github.com/py-pdf/pypdf/files/15186107/malformed_pdf.pdf\"\n    name = \"malformed_pdf.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)), strict=False)\n    len(reader.pages)\n    strict_reader = PdfReader(BytesIO(get_data_from_url(url, name=name)), strict=True)\n    with pytest.raises(PdfReadError) as exc:\n        len(strict_reader.pages)\n    assert (\n        exc.value.args[0] == \"Expected object ID (21 0) does not match actual (-1 -1).\"\n    )\n\n\n@pytest.mark.enable_socket\n@pytest.mark.timeout(10)\ndef test_looping_form(caplog):\n    \"\"\"Cf iss 2643\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/15306053/inheritance.pdf\"\n    name = \"iss2643.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)), strict=False)\n    flds = reader.get_fields()\n    assert all(\n        x in flds\n        for x in (\n            \"Text10\",\n            \"Text10.0.0.1.1.1.1.1.1.1.1.1.1.1.1.1.1.1\",\n            \"amt1.0\",\n            \"amt1.1\",\n            \"DSS#3pg3#0hgu7\",\n        )\n    )\n    writer = PdfWriter(reader)\n    writer.root_object[\"/AcroForm\"][\"/Fields\"][5][\"/Kids\"].append(\n        writer.root_object[\"/AcroForm\"][\"/Fields\"][5][\"/Kids\"][0]\n    )\n    flds2 = writer.get_fields()\n    assert \"Text68.0 already parsed\" in caplog.text\n    assert list(flds.keys()) == list(flds2.keys())\n\n\ndef test_context_manager_with_stream():\n    pdf_data = (\n        b\"%%PDF-1.7\\n\"\n        b\"1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj\\n\"\n        b\"2 0 obj << >> endobj\\n\"\n        b\"3 0 obj << >> endobj\\n\"\n        b\"4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]\"\n        b\" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R\"\n        b\" /Resources << /Font << >> >>\"\n        b\" /Rotate 0 /Type /Page >> endobj\\n\"\n        b\"5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\\n\"\n        b\"xref 1 5\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"trailer << /Root 5 0 R /Size 6 >>\\n\"\n        b\"startxref %d\\n\"\n        b\"%%%%EOF\"\n    )\n    pdf_data = pdf_data % (\n        pdf_data.find(b\"1 0 obj\"),\n        pdf_data.find(b\"2 0 obj\"),\n        pdf_data.find(b\"3 0 obj\"),\n        pdf_data.find(b\"4 0 obj\"),\n        pdf_data.find(b\"5 0 obj\"),\n        pdf_data.find(b\"xref\") - 1,\n    )\n    pdf_stream = io.BytesIO(pdf_data)\n    with PdfReader(pdf_stream) as reader:\n        assert not reader.stream.closed\n    assert not pdf_stream.closed\n\n\n@pytest.mark.enable_socket\n@pytest.mark.timeout(10)\ndef test_iss2761():\n    url = \"https://github.com/user-attachments/files/16312198/crash-b26d05712a29b241ac6f9dc7fff57428ba2d1a04.pdf\"\n    name = \"iss2761.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)), strict=False)\n    with pytest.raises(PdfReadError):\n        reader.pages[0].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_iss2817():\n    \"\"\"Test for rebuiling Xref_ObjStm\"\"\"\n    url = \"https://github.com/user-attachments/files/16764070/crash-7e1356f1179b4198337f282304cb611aea26a199.pdf\"\n    name = \"iss2817.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert (\n        reader.pages[0][\"/Annots\"][0].get_object()[\"/Contents\"]\n        == \"A\\xa0\\xa0\\xa0\\xa0\\xa0\\xa0\\xa0\\xa0\\xa0 B\"\n    )\n\n\n@pytest.mark.enable_socket\ndef test_truncated_files(caplog):\n    \"\"\"Cf #2853\"\"\"\n    url = \"https://github.com/user-attachments/files/16796095/f5471sm-2.pdf\"\n    name = \"iss2780.pdf\"  # reused\n    b = get_data_from_url(url, name=name)\n    reader = PdfReader(BytesIO(b))\n    assert caplog.text == \"\"\n    # remove \\n at end of file : invisible\n    reader = PdfReader(BytesIO(b[:-1]))\n    assert caplog.text == \"\"\n    # truncate but still detectable\n    for i in range(-2, -6, -1):\n        caplog.clear()\n        reader = PdfReader(BytesIO(b[:i]))\n        assert \"EOF marker seems truncated\" in caplog.text\n        assert reader._startxref == 100993\n    # remove completely EOF : we will not read last section\n    caplog.clear()\n    reader = PdfReader(BytesIO(b[:-6]))\n    assert \"CAUTION: startxref found while searching for %%EOF\" in caplog.text\n    assert reader._startxref < 100993\n\n\n@pytest.mark.enable_socket\ndef test_comments_in_array(caplog):\n    \"\"\"Cf #2843: this deals with comments\"\"\"\n    url = \"https://github.com/user-attachments/files/16992416/crash-2347912aa2a6f0fab5df4ebc8a424735d5d0d128.pdf\"\n    name = \"iss2843.pdf\"  # reused\n    b = get_data_from_url(url, name=name)\n    reader = PdfReader(BytesIO(b))\n    reader.pages[0]\n    assert caplog.text == \"\"\n    reader = PdfReader(BytesIO(b))\n    reader.stream = BytesIO(b[:1149])\n    with pytest.raises(PdfStreamError):\n        reader.pages[0]\n\n\n@pytest.mark.enable_socket\ndef test_space_in_names_to_continue_processing(caplog):\n    \"\"\"\n    This deals with space not encoded in names inducing errors.\n    Also covers case where NameObject not met for key.\n    \"\"\"\n    url = \"https://github.com/user-attachments/files/17095516/crash-e108c4f677040b61e12fa9f1cfde025d704c9b0d.pdf\"\n    name = \"iss2866.pdf\"  # reused\n    b = get_data_from_url(url, name=name)\n    reader = PdfReader(BytesIO(b))\n    obj = reader.get_object(70)\n    assert all(\n        x in obj\n        for x in (\n            \"/BaseFont\",\n            \"/DescendantFonts\",\n            \"/Encoding\",\n            \"/Subtype\",\n            \"/ToUnicode\",\n            \"/Type\",\n        )\n    )\n    assert obj[\"/BaseFont\"] == \"/AASGAA+Arial,Unicode\"  # MS is missing to meet spec\n    assert 'PdfReadError(\"Invalid Elementary Object starting with' in caplog.text\n\n    caplog.clear()\n\n    b = b[:264] + b\"(Inv) /d \" + b[273:]\n    reader = PdfReader(BytesIO(b))\n    obj = reader.get_object(70)\n    assert all(\n        x in obj\n        for x in [\"/DescendantFonts\", \"/Encoding\", \"/Subtype\", \"/ToUnicode\", \"/Type\"]\n    )\n    assert all(\n        x in caplog.text\n        for x in (\n            \"Expecting a NameObject for key but\",\n            'PdfReadError(\"Invalid Elementary Object starting with',\n        )\n    )\n    reader = PdfReader(BytesIO(b), strict=True)\n    with pytest.raises(PdfReadError):\n        obj = reader.get_object(70)\n\n\n@pytest.mark.enable_socket\ndef test_unbalanced_brackets_in_dictionary_object(caplog):\n    \"\"\"Cf #2877\"\"\"\n    url = \"https://github.com/user-attachments/files/17162634/7f40cb209fb97d1782bffcefc5e7be40.pdf\"\n    name = \"iss2877.pdf\"  # reused\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert len(reader.pages) == 43  # note:  /Count = 46 but 3 kids are None\n\n\n@pytest.mark.enable_socket\ndef test_repair_root(caplog):\n    \"\"\"Cf #2877\"\"\"\n    url = \"https://github.com/user-attachments/files/17162216/crash-6620e8b1abfe3da639b654595da859b87f985748.pdf\"\n    name = \"iss2875.pdf\"\n\n    b = get_data_from_url(url, name=name)\n    reader = PdfReader(BytesIO(b))\n    assert len(reader.pages) == 1\n    assert all(\n        msg in caplog.text\n        for msg in (\n            \"Invalid Root object\",\n            'Searching object with \"/Catalog\" key',\n            \"Root found at IndirectObject(2, 0,\",\n        )\n    )\n\n    # no /Root Entry\n    reader = PdfReader(BytesIO(b.replace(b\"/Root\", b\"/Roo \")))\n    caplog.clear()\n    assert len(reader.pages) == 1\n    assert all(\n        msg in caplog.text\n        for msg in (\n            'Cannot find \"/Root\" key in trailer',\n            'Searching object with \"/Catalog\" key',\n            \"Root found at IndirectObject(2, 0,\",\n        )\n    )\n\n    # Invalid /Root Entry\n    caplog.clear()\n    reader = PdfReader(\n        BytesIO(\n            b.replace(b\"/Root 1 0 R\", b\"/Root 2 0 R\").replace(b\"/Catalog/Pages 3 0 R\", b\"/Catalo \")\n        )\n    )\n    with pytest.raises(PdfReadError):\n        len(reader.pages)\n    assert all(\n        msg in caplog.text\n        for msg in (\n            \"Invalid Root object in trailer\",\n            'Searching object with \"/Catalog\" key',\n        )\n    )\n\n    # Invalid /Root Entry + error in get_object\n    caplog.clear()\n    data = b.replace(b\"/Root 1 0 R\", b\"/Root 2 0 R\").replace(b\"/Catalog/Pages 3 0 R\", b\"/Catalo \")\n    data = data[:5124] + b\"A\" + data[5125:]\n    reader = PdfReader(BytesIO(data))\n    with pytest.raises(PdfReadError):\n        len(reader.pages)\n    assert all(\n        msg in caplog.text\n        for msg in (\n            \"Invalid Root object in trailer\",\n            'Searching object with \"/Catalog\" key',\n        )\n    )\n\n    # Invalid /Root Entry without /Type, but /Pages.\n    caplog.clear()\n    reader = PdfReader(\n        BytesIO(\n            b.replace(b\"/Root 1 0 R\", b\"/Root 2 0 R\").replace(b\"/Catalog\", b\"/Catalo \")\n        )\n    )\n    assert len(reader.pages) == 1\n    assert all(\n        msg in caplog.text\n        for msg in (\n            \"Invalid Root object in trailer\",\n            'Searching object with \"/Catalog\" key',\n            f\"Possible root found at IndirectObject(2, 0, {id(reader)}), but missing /Catalog key\"\n        )\n    )\n\n\n@pytest.mark.enable_socket\ndef test_issue3151(caplog):\n    \"\"\"Tests for #3151\"\"\"\n    url = \"https://github.com/user-attachments/files/18941494/bible.pdf\"\n    name = \"issue3151.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert len(reader.pages) == 742\n\n\n@pytest.mark.enable_socket\ndef test_issue2886(caplog):\n    \"\"\"Tests for #2886\"\"\"\n    url = \"https://github.com/user-attachments/files/17187711/crash-e8a85d82de01cab5eb44e7993304d8b9d1544970.pdf\"\n    name = \"issue2886.pdf\"\n\n    with pytest.raises(PdfReadError, match=r\"Unexpected empty line in Xref table\\.\"):\n        _ = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n\n@pytest.mark.enable_socket\ndef test_infinite_loop_for_length_value():\n    \"\"\"Tests for #3112\"\"\"\n    url = \"https://github.com/user-attachments/files/19106009/Special.n.15.du.jeudi.22.fevrier.2024.pdf\"\n    name = \"issue3112.pdf\"\n\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    with pytest.raises(PdfReadError, match=r\"^Detected loop with self reference for IndirectObject\\(165, 0, \\d+\\)\\.$\"):\n        writer.add_page(reader.pages[0])\n\n\ndef test_trailer_cannot_be_read():\n    path = RESOURCE_ROOT / \"crazyones.pdf\"\n    data = path.read_bytes().replace(b\"/Type/XRef\", b\"/Type/Invalid\")\n    with pytest.raises(PdfReadError, match=r\"^Trailer cannot be read: Unexpected type '/Invalid'$\"):\n        reader = PdfReader(BytesIO(data))\n        list(reader.pages)\n\n\n@pytest.mark.enable_socket\ndef test_read_pdf15_xref_stream():\n    data = get_data_from_url(name=\"issue-3429.pdf\")\n\n    with pytest.raises(PdfReadError, match=r\"^Trailer cannot be read: Size missing from XRef stream {\"):\n        PdfReader(BytesIO(data))\n\n    data_modified = data.replace(b\"/XRef/\", b\"/XRef/Size/2/\")\n    with pytest.raises(\n            PdfReadError,\n            match=r\"^Trailer cannot be read: Limit reached while decompressing\\. 1545392 bytes remaining\\.$\"\n    ):\n        PdfReader(BytesIO(data_modified))\n\n\n@pytest.mark.enable_socket\ndef test_read_standard_xref_table__two_whitespace_characters_between_offset_and_generation():\n    \"\"\"Tests for #3482\"\"\"\n    url = \"https://github.com/user-attachments/files/22591813/helloworld.pdf\"\n    name = \"issue3482.pdf\"\n\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert len(reader.pages) == 1\n    assert reader.pages[0].extract_text() == \"Hello World!\"\n\n\n@pytest.mark.enable_socket\ndef test_root_object_recovery_limit(caplog):\n    url = \"https://github.com/user-attachments/files/24525509/root_object_recovery_limit.pdf\"\n    name = \"root_object_recovery_limit.pdf\"\n    data = get_data_from_url(url, name=name)\n\n    # Default limit.\n    reader = PdfReader(BytesIO(data))\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Maximum Root object recovery limit reached\\.$\"\n    ):\n        _ = list(reader.pages)\n    message_numbers = {\n        int(message.split(\" \", maxsplit=2)[1])\n        for message in caplog.messages\n        if message.startswith(\"Object \") and message.endswith(\" 0 not defined.\")\n    }\n    assert sorted(message_numbers) == list(range(5, 10001))\n\n    # Custom limit.\n    caplog.clear()\n    reader = PdfReader(BytesIO(data), root_object_recovery_limit=42)\n    with pytest.raises(\n            expected_exception=LimitReachedError, match=r\"^Maximum Root object recovery limit reached\\.$\"\n    ):\n        _ = list(reader.pages)\n    message_numbers = {\n        int(message.split(\" \", maxsplit=2)[1])\n        for message in caplog.messages\n        if message.startswith(\"Object \") and message.endswith(\" 0 not defined.\")\n    }\n    assert sorted(message_numbers) == list(range(5, 43))\n\n    # No limit. Do not run actual process for speed reasons.\n    reader = PdfReader(BytesIO(data), root_object_recovery_limit=None)\n    assert reader._root_object_recovery_limit == sys.maxsize\n\n    # Strict mode.\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^Broken xref table$\"):\n        reader = PdfReader(BytesIO(data), strict=True)\n        _ = list(reader.pages)\n\n\n@pytest.mark.timeout(10)\ndef test_rebuild_xref_table__speed():\n    total_len = 2_000_790\n    middle = b\"\\nstartxref   1\\n % \"\n    leading_len = 0x55E  # 1374\n    leading = b\" \" * leading_len\n    trailing = b\" \" * (total_len - leading_len - len(middle))\n    data = leading + middle + trailing\n\n    reader = PdfReader(BytesIO(data))\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^Cannot find Root object in pdf$\"):\n        _ = list(reader.pages)\n\n\ndef test_find_pdf_objects():\n    data = (\n        b\"     \\n\"\n        b\"  11 0 obj\\n\"\n        b\"  12 0 obj\\n\"\n        b\"13  1  obj\\n\"\n        b\"ob\\n\"\n        b\"ab obj\\n\"\n        b\"42 1337 obj \\n\"\n        b\"\\n\"\n    )\n\n    result = list(PdfReader._find_pdf_objects(data))\n    assert result == [(11, 0, 8), (12, 0, 19), (13, 1, 28), (42, 1337, 49)]\n\n\n@pytest.mark.parametrize(\n    (\"data\", \"expected\"),\n    [\n        (b\"\\n\\ntrailer\", []),\n        (b\"\\n\\ntrailer abc\", []),\n        (b\"\\n\\ntrailer <<\", [10]),\n        (b\"\\n\\ntrailer << /Key null >>\\n\\n  trailer << /Key 42 >>\\n\", [10, 37])\n    ]\n)\ndef test_find_pdf_trailers(data: bytes, expected: list[int]):\n    result = list(PdfReader._find_pdf_trailers(data))\n    assert result == expected\n\n\ndef test_objstm_batch_parse_caches_all_objects():\n    \"\"\"Resolving one ObjStm object should batch-cache all siblings.\"\"\"\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    assert len(reader.xref_objStm) > 0\n\n    obj_ids = list(reader.xref_objStm.keys())\n    first_obj = reader.get_object(obj_ids[0])\n    assert first_obj is not None\n\n    for idnum in obj_ids[1:]:\n        cached = reader.cache_get_indirect_object(0, idnum)\n        assert cached is not None, f\"Object {idnum} was not batch-cached\"\n\n\ndef test_objstm_cache_hit_returns_target():\n    \"\"\"Second call to _get_object_from_stream should return cached objects.\"\"\"\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    obj_ids = list(reader.xref_objStm.keys())\n\n    # Trigger batch parse\n    reader.get_object(obj_ids[0])\n\n    # Call again — all objects are already cached\n    second_id = obj_ids[1]\n    ref = IndirectObject(second_id, 0, reader)\n    result = reader._get_object_from_stream(ref)\n    assert result is reader.cache_get_indirect_object(0, second_id)\n\n\ndef test_objstm_skips_cache_for_overridden_objects():\n    \"\"\"Objects removed from xref_objStm should not be cached during batch parse.\"\"\"\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    obj_ids = list(reader.xref_objStm.keys())\n    assert len(obj_ids) >= 2\n\n    # Simulate an incremental update overriding one object\n    removed_id = obj_ids[-1]\n    saved_entry = reader.xref_objStm.pop(removed_id)\n    reader.resolved_objects.clear()\n\n    result = reader.get_object(obj_ids[0])\n    assert result is not None\n    assert reader.cache_get_indirect_object(0, removed_id) is None\n    assert reader.cache_get_indirect_object(0, obj_ids[0]) is not None\n\n    reader.xref_objStm[removed_id] = saved_entry\n"
  },
  {
    "path": "tests/test_text_extraction.py",
    "content": "\"\"\"\nTesting the text-extraction submodule and ensuring the quality of text extraction.\n\nThe tested code might be in _page.py.\n\"\"\"\n\nimport re\nfrom dataclasses import asdict\nfrom io import BytesIO\nfrom unittest.mock import patch\n\nimport pytest\n\nfrom pypdf import PdfReader, PdfWriter, mult\nfrom pypdf._font import Font\nfrom pypdf._text_extraction import set_custom_rtl\nfrom pypdf._text_extraction._layout_mode._fixed_width_page import text_show_operations\nfrom pypdf.errors import PdfReadError\nfrom pypdf.generic import ContentStream\n\nfrom . import RESOURCE_ROOT, SAMPLE_ROOT, get_data_from_url\n\n\n@pytest.mark.samples\n@pytest.mark.parametrize((\"visitor_text\"), [None, lambda a, b, c, d, e: None])  # noqa: ARG005\ndef test_multi_language(visitor_text):\n    reader = PdfReader(RESOURCE_ROOT / \"multilang.pdf\")\n    txt = reader.pages[0].extract_text(visitor_text=visitor_text)\n    assert \"Hello World\" in txt, \"English not correctly extracted\"\n    # iss #1296\n    assert \"مرحبا بالعالم\" in txt, \"Arabic not correctly extracted\"\n    assert \"Привет, мир\" in txt, \"Russian not correctly extracted\"\n    assert \"你好世界\" in txt, \"Chinese not correctly extracted\"\n    assert \"สวัสดีชาวโลก\" in txt, \"Thai not correctly extracted\"\n    assert \"こんにちは世界\" in txt, \"Japanese not correctly extracted\"\n    # check customizations\n    set_custom_rtl(None, None, \"Russian:\")\n    assert \":naissuR\" in reader.pages[0].extract_text(\n        visitor_text=visitor_text\n    ), \"(1) CUSTOM_RTL_SPECIAL_CHARS failed\"\n    set_custom_rtl(None, None, [ord(x) for x in \"Russian:\"])\n    assert \":naissuR\" in reader.pages[0].extract_text(\n        visitor_text=visitor_text\n    ), \"(2) CUSTOM_RTL_SPECIAL_CHARS failed\"\n    set_custom_rtl(0, 255, None)\n    assert \":hsilgnE\" in reader.pages[0].extract_text(\n        visitor_text=visitor_text\n    ), \"CUSTOM_RTL_MIN/MAX failed\"\n    set_custom_rtl(\"A\", \"z\", [])\n    assert \":hsilgnE\" in reader.pages[0].extract_text(\n        visitor_text=visitor_text\n    ), \"CUSTOM_RTL_MIN/MAX failed\"\n    set_custom_rtl(-1, -1, [])  # to prevent further errors\n\n    reader = PdfReader(SAMPLE_ROOT / \"015-arabic/habibi-rotated.pdf\")\n    assert \"habibi\" in reader.pages[0].extract_text(visitor_text=visitor_text)\n    assert \"حَبيبي\" in reader.pages[0].extract_text(visitor_text=visitor_text)\n    assert \"habibi\" in reader.pages[1].extract_text(visitor_text=visitor_text)\n    assert \"حَبيبي\" in reader.pages[1].extract_text(visitor_text=visitor_text)\n    assert \"habibi\" in reader.pages[2].extract_text(visitor_text=visitor_text)\n    assert \"حَبيبي\" in reader.pages[2].extract_text(visitor_text=visitor_text)\n    assert \"habibi\" in reader.pages[3].extract_text(visitor_text=visitor_text)\n    assert \"حَبيبي\" in reader.pages[3].extract_text(visitor_text=visitor_text)\n\n\n@pytest.mark.parametrize(\n    (\"file_name\", \"constraints\"),\n    [\n        (\n            \"inkscape-abc.pdf\",\n            {\n                \"A\": lambda x, y: 0 < x < 94 and 189 < y < 283,  # In upper left\n                \"B\": lambda x, y: 94 < x < 189 and 94 < y < 189,  # In the center\n                \"C\": lambda x, y: 189 < x < 283 and 0 < y < 94,  # In lower right\n            },\n        )\n    ],\n)\ndef test_visitor_text_matrices(file_name, constraints):\n    \"\"\"\n    Checks if the matrices given to the visitor_text function when calling\n    `extract_text` on the first page of `file_name` match some given constraints.\n    `constraints` is a dictionary mapping a line of text to a constraint that should\n    evaluate to `True` on its expected x,y-coordinates.\n    \"\"\"\n    reader = PdfReader(RESOURCE_ROOT / file_name)\n\n    lines = []\n\n    def visitor_text(text, cm, tm, font_dict, font_size) -> None:\n        ctm = mult(tm, cm)\n        x = ctm[4]  # mult(tm, cm)[4]\n        y = ctm[5]  # mult(tm, cm)[5]\n        lines.append({\"text\": text, \"x\": x, \"y\": y})\n\n    reader.pages[0].extract_text(visitor_text=visitor_text)\n\n    for text, constraint in constraints.items():\n        matches = [li for li in lines if li[\"text\"].strip() == text]\n        assert len(matches) <= 1, f\"Multiple lines match {text}\"\n        assert len(matches) >= 1, f\"No lines match {text}\"\n\n        x = matches[0][\"x\"]\n        y = matches[0][\"y\"]\n        assert constraint(x, y), f'Line \"{text}\" is wrong at x:{x}, y:{y}'\n\n\n@pytest.mark.xfail(reason=\"known whitespace issue #2336\")\n@pytest.mark.enable_socket\ndef test_issue_2336():\n    name = \"Pesquisa-de-Precos-Combustiveis-novembro-2023.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=name)))\n    page = reader.pages[0]\n    actual_text = page.extract_text()\n    assert \"Beira Rio\" in actual_text\n\n\ndef test_font_class_to_dict():\n    font = Font(\n        name = \"Unknown\",\n        space_width=8,\n        character_map={},\n        encoding = \"utf-16-be\"\n    )\n    assert asdict(font) == {\n        \"name\": \"Unknown\",\n        \"character_map\": {},\n        \"encoding\": \"utf-16-be\",\n        \"sub_type\": \"Unknown\",\n        \"font_descriptor\": {\n            \"name\": \"Unknown\",\n            \"family\": \"Unknown\",\n            \"weight\": \"Unknown\",\n            \"ascent\": 700.0,\n            \"descent\": -200.0,\n            \"cap_height\": 600.0,\n            \"x_height\": 500.0,\n            \"italic_angle\": 0.0,\n            \"flags\": 32,\n            \"bbox\": (\n                -100.0,\n                -200.0,\n                1000.0,\n                900.0,\n            ),\n        },\n        \"character_widths\": {\"default\": 500},\n        \"space_width\": 8,\n        \"interpretable\": True,\n    }\n\n\n@pytest.mark.enable_socket\n@patch(\"pypdf._text_extraction._layout_mode._fixed_width_page.logger_warning\")\ndef test_uninterpretable_type3_font(mock_logger_warning):\n    url = \"https://github.com/user-attachments/files/18551904/UninterpretableType3Font.pdf\"\n    name = \"UninterpretableType3Font.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[0]\n    assert page.extract_text(extraction_mode=\"layout\") == \"\"\n    mock_logger_warning.assert_called_with(\n        \"PDF contains an uninterpretable font. Output will be incomplete.\",\n        \"pypdf._text_extraction._layout_mode._fixed_width_page\"\n    )\n\n\n@pytest.mark.enable_socket\ndef test_layout_mode_epic_page_fonts():\n    url = \"https://github.com/py-pdf/pypdf/files/13836944/Epic.Page.PDF\"\n    name = \"Epic Page.PDF\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    expected = (RESOURCE_ROOT / \"Epic.Page.layout.txt\").read_text(encoding=\"utf-8\")\n    assert expected == reader.pages[0].extract_text(extraction_mode=\"layout\")\n\n\ndef test_layout_mode_uncommon_operators():\n    # Coverage for layout mode Tc, Tz, Ts, ', \", TD, TL, and Tw\n    reader = PdfReader(RESOURCE_ROOT / \"toy.pdf\")\n    expected = (RESOURCE_ROOT / \"toy.layout.txt\").read_text(encoding=\"utf-8\")\n    assert expected == reader.pages[0].extract_text(extraction_mode=\"layout\")\n\n\n@pytest.mark.enable_socket\ndef test_layout_mode_type0_font_widths():\n    # Cover both the 'int int int' and 'int [int int ...]' formats for Type0\n    # /DescendantFonts /W array entries.\n    url = \"https://github.com/py-pdf/pypdf/files/13533204/Claim.Maker.Alerts.Guide_pg2.PDF\"\n    name = \"Claim Maker Alerts Guide_pg2.PDF\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    expected = (RESOURCE_ROOT / \"Claim Maker Alerts Guide_pg2.layout.txt\").read_text(\n        encoding=\"utf-8\"\n    )\n    assert expected == reader.pages[0].extract_text(extraction_mode=\"layout\")\n\n\n@pytest.mark.enable_socket\ndef test_layout_mode_indirect_sequence_font_widths(caplog):\n    # Cover the situation where the sequence for font widths is an IndirectObject\n    # https://github.com/py-pdf/pypdf/pull/2788\n    url = \"https://github.com/user-attachments/files/16491621/2788_example.pdf\"\n    name = \"2788_example.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert reader.pages[0].extract_text(extraction_mode=\"layout\") == \"\"\n    url = \"https://github.com/user-attachments/files/16491619/2788_example_malformed.pdf\"\n    name = \"2788_example_malformed.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader.pages[0].extract_text(extraction_mode=\"layout\")\n    assert \"Invalid font width definition\" in caplog.text\n\n\ndef dummy_visitor_text(text, ctm, tm, fd, fs):\n    pass\n\n\n@patch(\"pypdf._page.logger_warning\")\ndef test_layout_mode_warnings(mock_logger_warning):\n    # Check that a warning is issued when an argument is ignored\n    reader = PdfReader(RESOURCE_ROOT / \"hello-world.pdf\")\n    page = reader.pages[0]\n    page.extract_text(extraction_mode=\"plain\", visitor_text=dummy_visitor_text)\n    mock_logger_warning.assert_not_called()\n    page.extract_text(extraction_mode=\"layout\", visitor_text=dummy_visitor_text)\n    mock_logger_warning.assert_called_with(\n        \"Argument visitor_text is ignored in layout mode\", \"pypdf._page\"\n    )\n\n\n@pytest.mark.enable_socket\ndef test_space_with_one_unit_smaller_than_font_width():\n    \"\"\"Tests for #1328\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/9498481/0004.pdf\"\n    name = \"iss1328.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[0]\n    extracted = page.extract_text()\n    assert \"Reporting crude oil leak.\\n\" in extracted\n\n\n@pytest.mark.enable_socket\ndef test_space_position_calculation():\n    \"\"\"Tests for #1153\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/9164743/file-0.pdf\"\n    name = \"iss1153.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    page = reader.pages[3]\n    extracted = page.extract_text()\n    assert \"Shortly after the Geneva BOF session, the\" in extracted\n\n\ndef test_text_leading_height_unit():\n    \"\"\"Tests for #2262\"\"\"\n    reader = PdfReader(RESOURCE_ROOT / \"toy.pdf\")\n    page = reader.pages[0]\n    extracted = page.extract_text()\n    assert \"Something[cited]\\n\" in extracted\n\n\ndef test_layout_mode_space_vertically_font_height_weight():\n    \"\"\"Tests layout mode with vertical space and font height weight (issue #2915)\"\"\"\n    with open(RESOURCE_ROOT / \"crazyones.pdf\", \"rb\") as inputfile:\n        # Load PDF file from file\n        reader = PdfReader(inputfile)\n        page = reader.pages[0]\n\n        # Normal behaviour\n        with open(RESOURCE_ROOT / \"crazyones_layout_vertical_space.txt\", \"rb\") as pdftext_file:\n            pdftext = pdftext_file.read()\n\n        text = page.extract_text(extraction_mode=\"layout\", layout_mode_space_vertically=True).encode(\"utf-8\")\n\n        # Compare the text of the PDF to a known source\n        for expected_line, actual_line in zip(text.splitlines(), pdftext.splitlines()):\n            assert expected_line == actual_line\n\n        pdftext = pdftext.replace(b\"\\r\\n\", b\"\\n\")  # fix for windows\n        assert text == pdftext\n\n        # Blank lines are added to truly separate paragraphs\n        with open(RESOURCE_ROOT / \"crazyones_layout_vertical_space_font_height_weight.txt\", \"rb\") as pdftext_file:\n            pdftext = pdftext_file.read()\n\n        text = page.extract_text(extraction_mode=\"layout\", layout_mode_space_vertically=True,\n                                 layout_mode_font_height_weight=0.85).encode(\"utf-8\")\n\n        # Compare the text of the PDF to a known source\n        for expected_line, actual_line in zip(text.splitlines(), pdftext.splitlines()):\n            assert expected_line == actual_line\n\n        pdftext = pdftext.replace(b\"\\r\\n\", b\"\\n\")  # fix for windows\n        assert text == pdftext\n\n\n@pytest.mark.enable_socket\ndef test_infinite_loop_arrays():\n    \"\"\"Tests for #2928\"\"\"\n    url = \"https://github.com/user-attachments/files/17576546/arrayabruptending.pdf\"\n    name = \"arrayabruptending.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    page = reader.pages[0]\n    extracted = page.extract_text()\n    assert \"RNA structure comparison\" in extracted\n\n\n@pytest.mark.enable_socket\ndef test_content_stream_is_dictionary_object(caplog):\n    \"\"\"Tests for #2995\"\"\"\n    url = \"https://github.com/user-attachments/files/18049322/6fa5fd46-5f98-4a67-800d-5e2362b0164f.pdf\"\n    name = \"iss2995.pdf\"\n    data = get_data_from_url(url, name=name)\n\n    reader = PdfReader(BytesIO(data))\n    page = reader.pages[0]\n    assert \"\\nYours faithfully   \\n\" in page.extract_text()\n    assert \"Expected StreamObject, got DictionaryObject instead. Data might be wrong.\" in caplog.text\n    caplog.clear()\n\n    reader = PdfReader(BytesIO(data), strict=True)\n    page = reader.pages[0]\n    with pytest.raises(PdfReadError) as exception:\n        page.extract_text()\n    assert (\n        \"Invalid Elementary Object starting with b\\\\'\\\\\\\\x18\\\\' @3557: b\\\\'ateDecode/Length 629\\\\\\\\x18ck[\"\n        in exception.value.args[0]\n    )\n\n\n@pytest.mark.enable_socket\ndef test_tz_with_no_operands():\n    \"\"\"Tests for #2975\"\"\"\n    url = \"https://github.com/user-attachments/files/17974120/9E5E080E-C8DB-4A6B-822B-9A67DC04E526-120438.pdf\"\n    name = \"iss2975.pdf\"\n    data = get_data_from_url(url, name=name)\n\n    reader = PdfReader(BytesIO(data))\n    page = reader.pages[1]\n    assert \"\\nThankyouforyourattentiontothismatter.\\n\" in page.extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_iss3060():\n    \"\"\"Test for not throwing 'font not set: is PDF missing a Tf operator'\"\"\"\n    url = \"https://github.com/user-attachments/files/18482531/test-anon.pdf\"\n    name = \"iss3060.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    # pypdf.errors.PdfReadError: font not set: is PDF missing a Tf operator?\n    txt = reader.pages[0].extract_text(extraction_mode=\"layout\")\n    assert txt.startswith(\" *******\")\n\n\n@pytest.mark.enable_socket\ndef test_iss3074():\n    \"\"\"Test for not throwing 'ZeroDivisionError: float division by zero'\"\"\"\n    url = \"https://github.com/user-attachments/files/18533211/test-anon.pdf\"\n    name = \"iss3074.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    # pypdf.errors.PdfReadError: ZeroDivisionError: float division by zero\n    txt = reader.pages[0].extract_text(extraction_mode=\"layout\")\n    assert txt.strip().startswith(\"AAAAAA\")\n\n\n@pytest.mark.enable_socket\ndef test_layout_mode_text_state():\n    \"\"\"Ensure the text state is stored and reset with q/Q operators.\"\"\"\n    # Get the PDF from issue #3212\n    url = \"https://github.com/user-attachments/files/19396790/garbled.pdf\"\n    name = \"garbled-font.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    # Get the txt from issue #3212 and normalize line endings\n    txt_url = \"https://github.com/user-attachments/files/19510731/garbled-font.layout.txt\"\n    txt_name = \"garbled-font.layout.txt\"\n    expected = get_data_from_url(txt_url, name=txt_name).decode(\"utf-8\").replace(\"\\r\\n\", \"\\n\")\n    # Ignore differences in rendering of spaces to work around older differences between the\n    # old layout mode Font code and the new Font class in calculating and dealing with the\n    # fallback width for a character that has no width defined in character_widths.\n    assert expected.replace(\" \", \"\") == reader.pages[0].extract_text(extraction_mode=\"layout\").replace(\" \", \"\")\n\n\n@pytest.mark.enable_socket\ndef test_rotated_line_wrap():\n    \"\"\"Ensure correct 2D translation of rotated text after a line wrap.\"\"\"\n    # Get the PDF from issue #3247\n    url = \"https://github.com/user-attachments/files/19696918/link16-line-wrap.sanitized.pdf\"\n    name = \"link16-line-wrap.sanitized.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    # Get the txt from issue #3247 and normalize line endings\n    txt_url = \"https://github.com/user-attachments/files/19696917/link16-line-wrap.sanitized.expected.txt\"\n    txt_name = \"link16-line-wrap.sanitized.expected.txt\"\n    expected = get_data_from_url(txt_url, name=txt_name).decode(\"utf-8\").replace(\"\\r\\n\", \"\\n\")\n\n    assert expected == reader.pages[0].extract_text()\n\n\n@pytest.mark.parametrize(\n        (\"op\", \"msg\"),\n        [\n            (b\"BT\", \"Unbalanced target operations, expected b'ET'.\"),\n            (b\"q\", \"Unbalanced target operations, expected b'Q'.\"),\n        ],\n)\ndef test_layout_mode_warns_on_malformed_content_stream(op, msg, caplog):\n    \"\"\"Ensures that imbalanced q/Q or EB/ET is handled gracefully.\"\"\"\n    text_show_operations(ops=iter([([], op)]), fonts={})\n    assert caplog.records\n    assert caplog.records[-1].msg == msg\n\n\ndef test_process_operation__cm_multiplication_issue():\n    \"\"\"Test for #3262.\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n    page = writer.pages[0]\n    content = page.get_contents().get_data()\n    content = content.replace(b\" 1 0 0 1 72 720 cm \", b\" 0.70278 65.3 163.36 cm \")\n    stream = ContentStream(stream=None, pdf=writer)\n    stream.set_data(content)\n    page.replace_contents(stream)\n    assert page.extract_text().startswith(\"The Crazy Ones\\nOctober 14, 1998\\n\")\n\n\n@pytest.mark.enable_socket\ndef test_rotated_layout_mode(caplog):\n    \"\"\"Ensures text extraction of rotated pages, as in issue #3270.\"\"\"\n    url = \"https://github.com/user-attachments/files/19981120/rotated-page.pdf\"\n    name = \"rotated-page.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n    page = writer.pages[0]\n\n    page.transfer_rotation_to_content()\n    text = page.extract_text(extraction_mode=\"layout\")\n\n    assert not caplog.records, \"No warnings should be issued\"\n    assert text, \"Text matching the page rotation should be extracted\"\n    assert re.search(r\"\\r?\\n +69\\r?\\n +UNCLASSIFIED$\", text), \"Contents should be in expected layout\"\n\n\n@pytest.mark.enable_socket\n@pytest.mark.filterwarnings(\"ignore::pypdf.errors.PdfReadWarning\")\ndef test_extract_text__none_objects():\n    url = \"https://github.com/user-attachments/files/18381726/tika-957721.pdf\"\n    name = \"tika-957721.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    reader.pages[0].extract_text()\n    reader.pages[8].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_extract_text__with_visitor_text():\n    def visitor_text(*args, **kwargs):  # noqa: ANN002, ANN003, ANN202\n        pass\n\n    url = \"https://github.com/user-attachments/files/18381718/tika-952016.pdf\"\n    name = \"tika-952016.pdf\"\n    stream = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(stream)\n    page = reader.pages[0]\n    page.extract_text(visitor_text=visitor_text)\n\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"TextAttack_paper.pdf\")))\n    page = reader.pages[0]\n    page.extract_text(visitor_text=visitor_text)\n\n\n@pytest.mark.enable_socket\ndef test_extract_text__restore_cm_stack_pop_error():\n    url = \"https://github.com/user-attachments/files/18381737/tika-966635.pdf\"\n    name = \"tika-966635.pdf\"\n    stream = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(stream)\n    page = reader.pages[10]\n\n    # There is a previous error we already omit (\"pop from empty list\"), thus\n    # check for the message explicitly here.\n    with pytest.raises(IndexError, match=\"list index out of range\"):\n        page.extract_text()\n\n\n@pytest.mark.timeout(60)\n@pytest.mark.enable_socket\ndef test_slow_huge_string():\n    \"\"\"Tests for #3541\"\"\"\n    url = \"https://github.com/user-attachments/files/23855795/file.pdf\"\n    name = \"issue-3541.pdf\"\n    stream = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(stream)\n    page = reader.pages[0]\n\n    _ = page.extract_text(extraction_mode=\"layout\")\n\n\n@pytest.mark.enable_socket\ndef test_extract_text_with_missing_font_bbox():\n    url = \"https://github.com/user-attachments/files/24611650/bbox_bug_emoji.pdf\"\n    name = \"issue-3599.pdf\"\n    stream = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(stream)\n    page = reader.pages[0]\n    text = page.extract_text()\n    assert \"🎉\" in text\n"
  },
  {
    "path": "tests/test_utils.py",
    "content": "\"\"\"Test the pypdf._utils module.\"\"\"\nimport functools\nimport io\nimport re\nimport subprocess\nimport sys\nfrom datetime import datetime, timedelta, timezone\nfrom pathlib import Path\nfrom typing import Any, Callable\n\nimport pytest\n\nimport pypdf._utils\nfrom pypdf._utils import (\n    File,\n    Version,\n    _get_max_pdf_version_header,\n    _human_readable_bytes,\n    check_if_whitespace_only,\n    classproperty,\n    deprecate_with_replacement,\n    deprecation_no_replacement,\n    format_iso8824_date,\n    logger_error,\n    mark_location,\n    matrix_multiply,\n    parse_iso8824_date,\n    read_block_backwards,\n    read_previous_line,\n    read_until_regex,\n    read_until_whitespace,\n    rename_kwargs,\n    skip_over_comment,\n    skip_over_whitespace,\n)\nfrom pypdf.errors import DeprecationError, PdfReadError, PdfStreamError\nfrom pypdf.generic import DictionaryObject, NameObject, TextStringObject\n\nfrom . import is_sublist\n\n\n@pytest.mark.parametrize(\n    (\"stream\", \"expected\"),\n    [\n        (io.BytesIO(b\"foo\"), False),\n        (io.BytesIO(b\"\"), False),\n        (io.BytesIO(b\" \"), True),\n        (io.BytesIO(b\"  \"), True),\n        (io.BytesIO(b\"  \\n\"), True),\n        (io.BytesIO(b\"    \\n\"), True),\n        (io.BytesIO(b\"\\f\"), True),\n    ],\n)\ndef test_skip_over_whitespace(stream, expected):\n    assert skip_over_whitespace(stream) == expected\n\n\n@pytest.mark.parametrize(\n    (\"value\", \"expected\"),\n    [\n        (b\"foo\", False),\n        (b\" a\", False),\n        (b\" a\\n b\", False),\n        (b\"\", True),\n        (b\" \", True),\n        (b\"  \", True),\n        (b\"  \\n\", True),\n        (b\"    \\n\", True),\n        (b\"\\f\", True),\n    ],\n)\ndef test_check_if_whitespace_only(value, expected):\n    assert check_if_whitespace_only(value) is expected\n\n\ndef test_read_until_whitespace():\n    assert read_until_whitespace(io.BytesIO(b\"foo\"), maxchars=1) == b\"f\"\n\n\n@pytest.mark.parametrize(\n    (\"stream\", \"remainder\"),\n    [\n        (io.BytesIO(b\"% foobar\\n\"), b\"\"),\n        (io.BytesIO(b\"\"), b\"\"),\n        (io.BytesIO(b\" \"), b\" \"),\n        (io.BytesIO(b\"% foo%\\nbar\"), b\"bar\"),\n    ],\n)\ndef test_skip_over_comment(stream, remainder):\n    skip_over_comment(stream)\n    assert stream.read() == remainder\n\n\ndef test_read_until_regex_premature_ending_name():\n    stream = io.BytesIO(b\"\")\n    assert read_until_regex(stream, re.compile(b\".\")) == b\"\"\n\n\ndef test_read_until_regex_match_in_first_chunk():\n    \"\"\"Match within the first 16-byte chunk.\"\"\"\n    stream = io.BytesIO(b\"hello world\")\n    result = read_until_regex(stream, re.compile(b\" \"))\n    assert result == b\"hello\"\n    assert stream.tell() == 5\n\n\ndef test_read_until_regex_match_in_second_chunk():\n    \"\"\"Match lands in the second chunk (past first 16 bytes).\"\"\"\n    payload = b\"0123456789abcdefghij\"\n    assert len(payload) == 20\n    data = payload + b\" rest\"\n    stream = io.BytesIO(data)\n    result = read_until_regex(stream, re.compile(b\" \"))\n    assert result == payload\n    assert stream.tell() == 20\n\n\ndef test_read_until_regex_match_at_chunk_boundary():\n    \"\"\"Delimiter sits exactly at byte 16 (first byte of second chunk).\"\"\"\n    payload = b\"0123456789abcdef\"\n    assert len(payload) == 16\n    data = payload + b\" after\"\n    stream = io.BytesIO(data)\n    result = read_until_regex(stream, re.compile(b\" \"))\n    assert result == payload\n    assert stream.tell() == 16\n\n\ndef test_read_until_regex_multi_byte_spanning_boundary():\n    \"\"\"Multi-byte regex pattern spans across a chunk boundary.\"\"\"\n    # \"X\" at byte 15 (last byte of first chunk), \"Y\" at byte 16 (first of second)\n    payload = b\"0123456789abcde\"\n    assert len(payload) == 15\n    data = payload + b\"XYafter\"\n    stream = io.BytesIO(data)\n    result = read_until_regex(stream, re.compile(b\"XY\"))\n    assert result == payload\n    assert stream.tell() == 15\n\n\ndef test_read_until_regex_no_match_exhausted():\n    \"\"\"No match - stream is fully consumed and all data returned.\"\"\"\n    data = b\"0123456789\" * 10\n    stream = io.BytesIO(data)\n    result = read_until_regex(stream, re.compile(b\"ZZZ\"))\n    assert result == data\n\n\ndef test_read_until_regex_exponential_chunk_growth():\n    \"\"\"Verify correctness with long input that exercises chunk doubling.\"\"\"\n    payload = (b\"0123456789abcdef\" * 3125)[:50_000]\n    assert len(payload) == 50_000\n    data = payload + b\"|done\"\n    stream = io.BytesIO(data)\n    result = read_until_regex(stream, re.compile(rb\"\\|\"))\n    assert result == payload\n    assert stream.tell() == 50_000\n\n\ndef test_read_until_regex_match_spanning_later_boundary():\n    \"\"\"Multi-byte match spanning a boundary after chunk size has grown.\"\"\"\n    # Chunk 1: 16 bytes, chunk 2: 32 bytes → total 48 after two reads.\n    # Place \"END\" at offset 47 so it spans bytes 47-49.\n    payload = (b\"0123456789abcdef\" * 3)[:47]\n    assert len(payload) == 47\n    data = payload + b\"ENDrest\"\n    stream = io.BytesIO(data)\n    result = read_until_regex(stream, re.compile(b\"END\"))\n    assert result == payload\n    assert stream.tell() == 47\n\n\ndef test_read_until_regex_tail_overlap_is_fixed():\n    \"\"\"Tail overlap is 16 bytes regardless of chunk size growth.\n\n    Chunk reads: 16, 32, 64 -> total 112. Place a 16-byte pattern starting\n    one byte before the 64-byte chunk boundary (at offset 47) so it spans\n    into the third chunk. This only works if the tail kept from chunk 2\n    covers at least 16 bytes.\n    \"\"\"\n    pattern = b\"ABCDEFGHIJKLMNOP\"  # 16 bytes\n    assert len(pattern) == 16\n    # Chunk 1: 16 bytes, chunk 2: 32 bytes -> boundary at offset 48.\n    # Pattern starts at 47, spanning bytes 47-62.\n    payload = b\"x\" * 47\n    data = payload + pattern + b\"rest\"\n    stream = io.BytesIO(data)\n    result = read_until_regex(stream, re.compile(re.escape(pattern)))\n    assert result == payload\n    assert stream.tell() == 47\n\n\n@pytest.mark.parametrize(\n    (\"a\", \"b\", \"expected\"),\n    [\n        (((3,),), ((7,),), ((21,),)),\n        (((3, 7),), ((5,), (13,)), ((3 * 5.0 + 7 * 13,),)),\n        (((3,), (7,)), ((5, 13),), ((3 * 5, 3 * 13), (7 * 5, 7 * 13))),\n    ],\n)\ndef test_matrix_multiply(a, b, expected):\n    assert matrix_multiply(a, b) == expected\n\n\ndef test_mark_location():\n    stream = io.BytesIO(b\"abde\" * 6000)\n    mark_location(stream)\n    Path(\"pypdf_pdfLocation.txt\").unlink()  # cleanup\n\n\ndef test_deprecate_no_replacement():\n    with pytest.warns(\n            expected_warning=DeprecationWarning,\n            match=\"foo is deprecated and will be removed in pypdf 3.0.0.\"\n    ):\n        pypdf._utils.deprecate_no_replacement(\"foo\", removed_in=\"3.0.0\")\n\n\n@pytest.mark.parametrize(\n    (\"dat\", \"pos\", \"to_read\", \"expected\", \"expected_pos\"),\n    [\n        (b\"abc\", 1, 0, b\"\", 1),\n        (b\"abc\", 1, 1, b\"a\", 0),\n        (b\"abc\", 2, 1, b\"b\", 1),\n        (b\"abc\", 2, 2, b\"ab\", 0),\n        (b\"abc\", 3, 1, b\"c\", 2),\n        (b\"abc\", 3, 2, b\"bc\", 1),\n        (b\"abc\", 3, 3, b\"abc\", 0),\n        (b\"\", 0, 1, None, 0),\n        (b\"a\", 0, 1, None, 0),\n        (b\"abc\", 0, 10, None, 0),\n    ],\n)\ndef test_read_block_backwards(dat, pos, to_read, expected, expected_pos):\n    s = io.BytesIO(dat)\n    s.seek(pos)\n    if expected is not None:\n        assert read_block_backwards(s, to_read) == expected\n    else:\n        with pytest.raises(PdfStreamError):\n            read_block_backwards(s, to_read)\n    assert s.tell() == expected_pos\n\n\ndef test_read_block_backwards_at_start():\n    s = io.BytesIO(b\"abc\")\n    with pytest.raises(PdfStreamError) as _:\n        read_previous_line(s)\n\n\n@pytest.mark.parametrize(\n    (\"dat\", \"pos\", \"expected\", \"expected_pos\"),\n    [\n        (b\"abc\", 1, b\"a\", 0),\n        (b\"abc\", 2, b\"ab\", 0),\n        (b\"abc\", 3, b\"abc\", 0),\n        (b\"abc\\n\", 3, b\"abc\", 0),\n        (b\"abc\\n\", 4, b\"\", 3),\n        (b\"abc\\n\\r\", 4, b\"\", 3),\n        (b\"abc\\nd\", 5, b\"d\", 3),\n        # Skip over multiple CR/LF bytes\n        (b\"abc\\n\\r\\ndef\", 9, b\"def\", 3),\n    ],\n    ids=list(range(8)),\n)\ndef test_read_previous_line(dat, pos, expected, expected_pos):\n    s = io.BytesIO(dat)\n    s.seek(pos)\n    assert read_previous_line(s) == expected\n    assert s.tell() == expected_pos\n\n\n# for unknown reason if the parameters are passed through pytest, errors are reported\ndef test_read_previous_line2():\n    # Include a block full of newlines...\n    test_read_previous_line(\n        b\"abc\" + b\"\\n\" * (2 * io.DEFAULT_BUFFER_SIZE) + b\"d\",\n        2 * io.DEFAULT_BUFFER_SIZE + 4,\n        b\"d\",\n        3,\n    )\n    # Include a block full of non-newline characters\n    test_read_previous_line(\n        b\"abc\\n\" + b\"d\" * (2 * io.DEFAULT_BUFFER_SIZE),\n        2 * io.DEFAULT_BUFFER_SIZE + 4,\n        b\"d\" * (2 * io.DEFAULT_BUFFER_SIZE),\n        3,\n    )\n    # Both\n    test_read_previous_line(\n        b\"abcxyz\"\n        + b\"\\n\" * (2 * io.DEFAULT_BUFFER_SIZE)\n        + b\"d\" * (2 * io.DEFAULT_BUFFER_SIZE),\n        4 * io.DEFAULT_BUFFER_SIZE + 6,\n        b\"d\" * (2 * io.DEFAULT_BUFFER_SIZE),\n        6,\n    )\n\n\ndef test_get_max_pdf_version_header():\n    with pytest.raises(ValueError) as exc:\n        _get_max_pdf_version_header(b\"\", b\"PDF-1.2\")\n    assert exc.value.args[0] == \"Neither b'' nor b'PDF-1.2' are proper headers\"\n\n\ndef test_read_block_backwards_exception():\n    stream = io.BytesIO(b\"foobar\")\n    stream.seek(6)\n    with pytest.raises(PdfReadError) as exc:\n        read_block_backwards(stream, 7)\n    assert exc.value.args[0] == \"Could not read malformed PDF file\"\n\n\ndef test_deprecate_with_replacement():\n    def foo() -> None:\n        deprecate_with_replacement(\"foo\", \"bar\", removed_in=\"4.3.2\")\n\n    with pytest.warns(\n        DeprecationWarning,\n        match=\"foo is deprecated and will be removed in pypdf 4.3.2. Use bar instead.\",\n    ):\n        foo()\n\n\ndef test_deprecation_no_replacement():\n    def foo() -> None:\n        deprecation_no_replacement(\"foo\", removed_in=\"4.3.2\")\n\n    with pytest.raises(\n        DeprecationError,\n        match=r\"foo is deprecated and was removed in pypdf 4\\.3\\.2\\.\",\n    ):\n        foo()\n\n\ndef test_logger_error(caplog):\n    enc = NameObject(\"/Invalid\")\n    message = \"Advanced encoding %(encoding)s not implemented yet\"\n    logger_error(message, source=__name__, encoding=enc)\n    assert \"Advanced encoding /Invalid not implemented yet\" in caplog.text\n    encoding = DictionaryObject({NameObject(\"/key\"): TextStringObject(\"value\")})\n    message = \"Advanced encoding %(encoding)s not implemented yet\"\n    logger_error(message, source=__name__, encoding=encoding)\n    assert \"Advanced encoding {'/key': 'value'} not implemented yet\" in caplog.text\n\n\ndef test_rename_kwargs():\n    def deprecation_bookmark_nofail(**aliases: str) -> Callable:\n        \"\"\"\n        Decorator for deprecated term \"bookmark\".\n\n        To be used for methods and function arguments\n            outline_item = a bookmark\n            outline = a collection of outline items.\n        \"\"\"\n\n        def decoration(func: Callable) -> Any:  # type: ignore\n            @functools.wraps(func)\n            def wrapper(*args: Any, **kwargs: Any) -> Any:  # type: ignore\n                rename_kwargs(func.__name__, kwargs, aliases, fail=False)\n                return func(*args, **kwargs)\n\n            return wrapper\n\n        return decoration\n\n    @deprecation_bookmark_nofail(old_param=\"new_param\")\n    def foo(old_param: int = 1, baz: int = 2, new_param: int = 1) -> None:\n        pass\n\n    expected_msg = (\n        \"foo received both old_param and new_param as an argument. \"\n        \"old_param is deprecated. Use new_param instead.\"\n    )\n    with pytest.raises(TypeError, match=expected_msg):\n        foo(old_param=12, new_param=13)\n\n    with pytest.warns(\n        DeprecationWarning,\n        match=\"old_param is deprecated as an argument. Use new_param instead\",\n    ):\n        foo(old_param=12)\n\n\ndef test_rename_kwargs__stacklevel(tmp_path: Path) -> None:\n    script = tmp_path / \"script.py\"\n    script.write_text(\"\"\"\nimport functools\nimport warnings\n\nfrom pypdf._utils import rename_kwargs\n\ndef deprecation(**aliases: str):\n    def decoration(func):\n        @functools.wraps(func)\n        def wrapper(*args, **kwargs):\n            rename_kwargs(func.__name__, kwargs, aliases, fail=False)\n            return func(*args, **kwargs)\n\n        return wrapper\n\n    return decoration\n\n@deprecation(old_param=\"new_param\")\ndef foo(old_param: int = 1, baz: int = 2, new_param: int = 1) -> None:\n    pass\n\nwarnings.simplefilter(\"always\")\nfoo(old_param=12)\n    \"\"\")\n\n    result = subprocess.run([sys.executable, script], capture_output=True, text=True)  # noqa: S603\n    assert result.returncode == 0\n    assert result.stderr == (\n        f\"{script}:23: DeprecationWarning: old_param is deprecated as an argument. \"\n        f\"Use new_param instead\\n  foo(old_param=12)\\n\"\n    )\n\n\n@pytest.mark.parametrize(\n    (\"input_int\", \"expected_output\"),\n    [\n        (123, \"123 Byte\"),\n        (1234, \"1.2 kB\"),\n        (123_456, \"123.5 kB\"),\n        (1_234_567, \"1.2 MB\"),\n        (1_234_567_890, \"1.2 GB\"),\n        (1_234_567_890_000, \"1234.6 GB\"),\n    ],\n)\ndef test_human_readable_bytes(input_int, expected_output):\n    \"\"\"_human_readable_bytes correctly transforms the integer to a string.\"\"\"\n    assert _human_readable_bytes(input_int) == expected_output\n\n\ndef test_file_class():\n    \"\"\"File class can be instantiated and string representation is ok.\"\"\"\n    f = File(name=\"image.png\", data=b\"\")\n    assert str(f) == \"File(name=image.png, data: 0 Byte)\"\n    # hash(b\"\") varies between CPython and PyPy\n    assert repr(f) == f\"File(name=image.png, data: 0 Byte, hash: {hash(b'')})\"\n\n\n@pytest.mark.parametrize(\n    (\"text\", \"expected\"),\n    [\n        (\"D:20210318000756\", \"2021-03-18T00:07:56\"),\n        (\"20210318000756\", \"2021-03-18T00:07:56\"),\n        (\"D:2021\", \"2021-01-01T00:00:00\"),\n        (\"D:202103\", \"2021-03-01T00:00:00\"),\n        (\"D:20210304\", \"2021-03-04T00:00:00\"),\n        (\"D:2021030402\", \"2021-03-04T02:00:00\"),\n        (\"D:20210408054711\", \"2021-04-08T05:47:11\"),\n        (\"D:20210408054711Z\", \"2021-04-08T05:47:11+00:00\"),\n        (\"D:20210408054711Z00\", \"2021-04-08T05:47:11+00:00\"),\n        (\"D:20210408054711Z0000\", \"2021-04-08T05:47:11+00:00\"),\n        (\"D:20210408075331+02'00'\", \"2021-04-08T07:53:31+02:00\"),\n        (\"D:20210408075331-03'00'\", \"2021-04-08T07:53:31-03:00\"),\n    ],\n)\ndef test_parse_datetime(text, expected):\n    date = parse_iso8824_date(text)\n    date_str = (date.isoformat() + date.strftime(\"%z\"))[: len(expected)]\n    assert date_str == expected\n\n\n@pytest.mark.parametrize(\n    (\"text\", \"expected\"),\n    [\n        (\"\", None),\n        (None, None),\n    ],\n)\ndef test_parse_datetime_edge_cases(text, expected):\n    date = parse_iso8824_date(text)\n    assert date == expected\n\n\ndef test_parse_datetime_err():\n    with pytest.raises(ValueError) as ex:\n        parse_iso8824_date(\"D:20210408T054711Z\")\n    assert ex.value.args[0] == \"Can not convert date: D:20210408T054711Z\"\n    assert parse_iso8824_date(\"D:20210408054711\").tzinfo is None\n\n\ndef test_format_iso8824_date():\n    \"\"\"Test format_iso8824_date function with timezone handling.\"\"\"\n    dt_naive = datetime(2021, 3, 18, 12, 7, 56)\n    result = format_iso8824_date(dt_naive)\n    assert result == \"D:20210318120756\"\n\n    dt_utc = datetime(2021, 3, 18, 12, 7, 56, tzinfo=timezone.utc)\n    result = format_iso8824_date(dt_utc)\n    assert result == \"D:20210318120756+00'00'\"\n\n    dt_positive = datetime(2021, 3, 18, 12, 7, 56, tzinfo=timezone(timedelta(hours=2, minutes=30)))\n    result = format_iso8824_date(dt_positive)\n    assert result == \"D:20210318120756+02'30'\"\n\n    dt_negative = datetime(2021, 3, 18, 12, 7, 56, tzinfo=timezone(timedelta(hours=-5, minutes=-30)))\n    result = format_iso8824_date(dt_negative)\n    assert result == \"D:20210318120756-05'30'\"\n\n\ndef test_format_iso8824_date_roundtrip():\n    dt_naive = datetime(2021, 3, 18, 12, 7, 56)\n    formatted = format_iso8824_date(dt_naive)\n    parsed = parse_iso8824_date(formatted)\n    assert parsed == dt_naive\n\n    dt_utc = datetime(2021, 3, 18, 12, 7, 56, tzinfo=timezone.utc)\n    formatted = format_iso8824_date(dt_utc)\n    parsed = parse_iso8824_date(formatted)\n    assert parsed == dt_utc\n\n    dt_positive = datetime(2021, 3, 18, 12, 7, 56, tzinfo=timezone(timedelta(hours=2, minutes=30)))\n    formatted = format_iso8824_date(dt_positive)\n    parsed = parse_iso8824_date(formatted)\n    assert parsed == dt_positive\n\n    dt_negative = datetime(2021, 3, 18, 12, 7, 56, tzinfo=timezone(timedelta(hours=-5, minutes=-30)))\n    formatted = format_iso8824_date(dt_negative)\n    parsed = parse_iso8824_date(formatted)\n    assert parsed == dt_negative\n\n\ndef test_is_sublist():\n    # Basic checks:\n    assert is_sublist([0, 1], [0, 1, 2]) is True\n    assert is_sublist([0, 2], [0, 1, 2]) is True\n    assert is_sublist([1, 2], [0, 1, 2]) is True\n    assert is_sublist([0, 3], [0, 1, 2]) is False\n    # Ensure order is checked:\n    assert is_sublist([1, 0], [0, 1, 2]) is False\n    # Ensure duplicates are handled:\n    assert is_sublist([0, 1, 1], [0, 1, 1, 2]) is True\n    assert is_sublist([0, 1, 1], [0, 1, 2]) is False\n    # Edge cases with empty lists:\n    assert is_sublist([], [0, 1, 2]) is True\n    assert is_sublist([0, 1], []) is False\n    # Self-sublist edge case:\n    assert is_sublist([0, 1, 2], [0, 1, 2]) is True\n\n\n@pytest.mark.parametrize(\n    (\"left\", \"right\", \"is_less_than\"),\n    [\n        (\"1\", \"2\", True),\n        (\"2\", \"1\", False),\n        (\"1\", \"1\", False),\n        (\"1.0\", \"1.1\", True),\n        (\"1\", \"1.1\", True),\n        # Suffix left\n        (\"1a\", \"2\", True),\n        (\"2a\", \"1\", False),\n        (\"1a\", \"1\", False),\n        (\"1.0a\", \"1.1\", True),\n        # I'm not sure about that, but seems special enough that it\n        # probably doesn't matter:\n        (\"1a\", \"1.1\", False),\n        # Suffix right\n        (\"1\", \"2a\", True),\n        (\"2\", \"1a\", False),\n        (\"1\", \"1a\", True),\n        (\"1.0\", \"1.1a\", True),\n        (\"1\", \"1.1a\", True),\n        (\"\", \"0.0.0\", True),\n        # Just suffix matters ... hm, I think this is actually wrong:\n        (\"1.0a\", \"1.0\", False),\n        (\"1.0\", \"1.0a\", True),\n    ],\n)\ndef test_version_compare(left, right, is_less_than):\n    assert (Version(left) < Version(right)) is is_less_than\n\n\ndef test_version_compare_equal_str():\n    a = Version(\"1.0\")\n    assert a != \"1.0\"\n\n\ndef test_version_compare_lt_str():\n    a = Version(\"1.0\")\n    with pytest.raises(ValueError) as exc:\n        a < \"1.0\"  # noqa: B015\n    assert exc.value.args[0] == \"Version cannot be compared against <class 'str'>\"\n\n\ndef test_bad_version():\n    assert Version(\"a\").components == [(0, \"a\")]\n\n\ndef test_version_eq_hash():\n    version1 = Version(\"1.0\")\n    version2 = Version(\"1.0\")\n    version3 = Version(\"1.1\")\n    assert version1 == version2\n    assert version1 != version3\n    assert hash(version1) == hash(version2)\n    assert hash(version1) != hash(version3)\n\n\ndef test_classproperty():\n    class Container:\n        @classproperty\n        def value1(cls) -> int:  # noqa: N805\n            return 42\n\n        @classproperty\n        def value2(cls) -> int:  # noqa: N805\n            return 1337\n\n        @classproperty\n        def value3(cls) -> int:  # noqa: N805\n            return 1\n\n        @value3.getter\n        def value3(cls) -> int:  # noqa: N805\n            return 2\n\n    assert Container.value1 == 42\n    assert Container.value2 == 1337\n    assert Container.value3 == 2\n    assert Container().value1 == 42\n    assert Container().value2 == 1337\n    assert Container().value3 == 2\n"
  },
  {
    "path": "tests/test_workflows.py",
    "content": "\"\"\"\nTests in this module behave like user code.\n\nThey don't mock/patch anything, they cover typical user needs.\n\"\"\"\n\nimport binascii\nfrom io import BytesIO\nfrom pathlib import Path\nfrom re import findall\n\nimport pytest\nfrom PIL import Image, ImageChops\nfrom PIL import __version__ as pil_version\n\nfrom pypdf import PdfReader, PdfWriter, Transformation\nfrom pypdf.constants import PageAttributes as PG\nfrom pypdf.errors import PdfReadError, PdfReadWarning\nfrom pypdf.generic import (\n    ArrayObject,\n    ContentStream,\n    DictionaryObject,\n    NameObject,\n    TextStringObject,\n    read_object,\n)\n\nfrom . import PROJECT_ROOT, RESOURCE_ROOT, SAMPLE_ROOT, PILContext, get_data_from_url, normalize_warnings\nfrom .utils import get_image_data\n\n\ndef test_basic_features(tmp_path):\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter()\n\n    assert len(reader.pages) == 1\n\n    # add page 1 from input1 to output document, unchanged\n    writer.add_page(reader.pages[0])\n\n    # add page 2 from input1, but rotated clockwise 90 degrees\n    writer.add_page(reader.pages[0].rotate(90))\n    assert writer.pages[0].rotation == 0\n    assert writer.pages[1].rotation == 90\n\n    # add page 3 from input1, but crop it to half size:\n    page4 = reader.pages[0]\n    page4 = writer.add_page(page4)\n    page4.mediabox.upper_right = (\n        page4.mediabox.right / 2,\n        page4.mediabox.top / 2,\n    )\n    del page4.mediabox\n\n    # add page 4 from input1, but first add a watermark from another PDF:\n    page3 = reader.pages[0]\n    page3 = writer.add_page(page3)\n    watermark_pdf = pdf_path\n    watermark = PdfReader(watermark_pdf)\n    page3.merge_page(watermark.pages[0])\n\n    # add some Javascript to launch the print window on opening this PDF.\n    # the password dialog may prevent the print dialog from being shown,\n    # comment the encryption lines, if that's the case, to try this out\n    writer.add_js(\"this.print({bUI:true,bSilent:false,bShrinkToFit:true});\")\n\n    # encrypt your new PDF and add a password\n    password = \"secret\"\n    writer.encrypt(password)\n    # doing it twice should not change anything\n    writer.encrypt(password)\n\n    # finally, write \"output\" to pypdf-output.pdf\n    write_path = tmp_path / \"pypdf-output.pdf\"\n    with open(write_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n\ndef test_dropdown_items():\n    inputfile = RESOURCE_ROOT / \"libreoffice-form.pdf\"\n    reader = PdfReader(inputfile)\n    fields = reader.get_fields()\n    assert \"/Opt\" in fields[\"Nationality\"]\n\n\ndef test_pdfreader_file_load():\n    \"\"\"\n    Test loading and parsing of a file.\n\n    Extract text of the file and compare to expected textual output. Expected\n    outcome: file loads, text matches expected.\n    \"\"\"\n    with open(RESOURCE_ROOT / \"crazyones.pdf\", \"rb\") as inputfile:\n        # Load PDF file from file\n        reader = PdfReader(inputfile)\n        page = reader.pages[0]\n\n        # Retrieve the text of the PDF\n        with open(RESOURCE_ROOT / \"crazyones.txt\", \"rb\") as pdftext_file:\n            pdftext = pdftext_file.read()\n\n        text = page.extract_text().encode(\"utf-8\")\n\n        # Compare the text of the PDF to a known source\n        for expected_line, actual_line in zip(text.splitlines(), pdftext.splitlines()):\n            assert expected_line == actual_line\n\n        pdftext = pdftext.replace(b\"\\r\\n\", b\"\\n\")  # fix for windows\n        assert text == pdftext\n\n\ndef test_pdfreader_jpeg_image():\n    \"\"\"\n    Test loading and parsing of a file. Extract the image of the file and\n    compare to expected textual output.\n\n    Expected outcome: file loads, image matches expected.\n    \"\"\"\n    with open(RESOURCE_ROOT / \"jpeg.pdf\", \"rb\") as inputfile:\n        # Load PDF file from file\n        reader = PdfReader(inputfile)\n\n        # Retrieve the text of the image\n        with open(RESOURCE_ROOT / \"jpeg.txt\") as pdftext_file:\n            imagetext = pdftext_file.read()\n\n        page = reader.pages[0]\n        x_object = page[PG.RESOURCES][\"/XObject\"].get_object()\n        data = x_object[\"/Im4\"].get_data()\n\n        # Compare the text of the PDF to a known source\n        assert binascii.hexlify(data).decode() == imagetext\n\n\ndef test_decrypt():\n    with open(RESOURCE_ROOT / \"libreoffice-writer-password.pdf\", \"rb\") as inputfile:\n        reader = PdfReader(inputfile)\n        assert reader.is_encrypted is True\n        reader.decrypt(\"openpassword\")\n        assert len(reader.pages) == 1\n        assert reader.is_encrypted is True\n        metadict = reader.metadata\n        assert dict(metadict) == {\n            \"/CreationDate\": \"D:20220403203552+02'00'\",\n            \"/Creator\": \"Writer\",\n            \"/Producer\": \"LibreOffice 6.4\",\n        }\n\n\ndef test_text_extraction_encrypted():\n    inputfile = RESOURCE_ROOT / \"libreoffice-writer-password.pdf\"\n    reader = PdfReader(inputfile)\n    assert reader.is_encrypted is True\n    reader.decrypt(\"openpassword\")\n    assert (\n        reader.pages[0]\n        .extract_text()\n        .strip()\n        .startswith(\"Lorem ipsum dolor sit amet\")\n    )\n\n\n@pytest.mark.parametrize(\"degree\", [0, 90, 180, 270, 360, -90])\ndef test_rotate(degree):\n    with open(RESOURCE_ROOT / \"crazyones.pdf\", \"rb\") as inputfile:\n        reader = PdfReader(inputfile)\n        page = reader.pages[0]\n        page.rotate(degree)\n\n\ndef test_rotate_45():\n    with open(RESOURCE_ROOT / \"crazyones.pdf\", \"rb\") as inputfile:\n        reader = PdfReader(inputfile)\n        page = reader.pages[0]\n        with pytest.raises(ValueError) as exc:\n            page.rotate(45)\n        assert exc.value.args[0] == \"Rotation angle must be a multiple of 90\"\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\n@pytest.mark.parametrize(\n    (\"enable\", \"url\", \"pages\"),\n    [\n        (True, \"https://arxiv.org/pdf/2201.00214.pdf\", [0, 1, 5, 10]),\n        (\n            True,\n            \"https://github.com/py-pdf/sample-files/raw/main/009-pdflatex-geotopo/GeoTopo.pdf\",\n            [0, 1, 5, 10],\n        ),\n        (True, \"https://arxiv.org/pdf/2201.00151.pdf\", [0, 1, 5, 10]),\n        (True, \"https://arxiv.org/pdf/1707.09725.pdf\", [0, 1, 5, 10]),\n        (True, \"https://arxiv.org/pdf/2201.00021.pdf\", [0, 1, 5, 8]),\n        (True, \"https://arxiv.org/pdf/2201.00037.pdf\", [0, 1, 5, 10]),\n        (True, \"https://arxiv.org/pdf/2201.00069.pdf\", [0, 1, 5, 10]),\n        (True, \"https://arxiv.org/pdf/2201.00178.pdf\", [0, 1, 5, 10]),\n        (True, \"https://arxiv.org/pdf/2201.00201.pdf\", [0, 1, 5, 8]),\n        (True, \"https://arxiv.org/pdf/1602.06541.pdf\", [0, 1, 5, 10]),\n        (True, \"https://arxiv.org/pdf/2201.00200.pdf\", [0, 1, 5, 6]),\n        (True, \"https://arxiv.org/pdf/2201.00022.pdf\", [0, 1, 5, 10]),\n        (True, \"https://arxiv.org/pdf/2201.00029.pdf\", [0, 1, 6, 10]),\n        # #1145\n        (True, \"https://github.com/py-pdf/pypdf/files/9174594/2017.pdf\", [0]),\n        # #1145, remaining issue (empty arguments for FlateEncoding)\n        (\n            True,\n            \"https://github.com/py-pdf/pypdf/files/9175966/2015._pb_decode_pg0.pdf\",\n            [0],\n        ),\n        # 6 instead of 5: as there is an issue in page 5 (missing objects)\n        # and too complex to handle the warning without hiding real regressions\n        (True, \"https://arxiv.org/pdf/1601.03642.pdf\", [0, 1, 5, 7]),\n        (\n            True,\n            \"https://github.com/py-pdf/pypdf/files/3796761/17343_2008_Order_09-Jan-2019.pdf\",\n            [0, 1],\n        ),\n        (\n            True,\n            \"https://github.com/py-pdf/pypdf/files/8884471/ssi_manwaring.pdf\",\n            [0, 1],\n        ),\n        (True, \"https://github.com/py-pdf/pypdf/files/8884469/999092.pdf\", [0, 1]),\n        (\n            True,\n            \"file://\" + str(RESOURCE_ROOT / \"test Orient.pdf\"),\n            [0],\n        ),  # TODO: preparation of text orientation validation\n        (\n            True,\n            \"https://github.com/py-pdf/pypdf/files/8884470/fdocuments.in_sweet-fundamentals-of-crystallography.pdf\",\n            [0, 1, 34, 35, 36, 118, 119, 120, 121],\n        ),\n        (True, \"https://github.com/py-pdf/pypdf/files/8884493/998167.pdf\", [0]),\n        (\n            True,\n            \"https://github.com/user-attachments/files/18382039/971703.pdf\",\n            [0, 1, 5, 8, 14],\n        ),\n        (  # faulty PDF, wrongly linearized and with 2 trailer, second with /Root\n            True,\n            \"https://github.com/user-attachments/files/18382034/989691.pdf\",\n            [0],\n        ),\n    ],\n)\ndef test_extract_textbench(enable, url, pages):\n    if not enable:\n        return\n    print_result = False\n    try:\n        reader = PdfReader(BytesIO(get_data_from_url(url, url.split(\"/\")[-1])))\n        for page_number in pages:\n            if print_result:\n                print(f\"**************** {url} / page {page_number} ****************\")\n            rst = reader.pages[page_number].extract_text()\n            if print_result:\n                print(f\"{rst}\\n*****************************\\n\")\n    except PdfReadWarning:\n        pass\n\n\ndef test_transform_compress_identical_objects():\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"two-different-pages.pdf\")\n\n    for page in writer.pages:\n        op = Transformation().scale(sx=0.8, sy=0.8)\n        page.add_transformation(op)\n    writer.compress_identical_objects()\n    bytes_out = BytesIO()\n    writer.write(bytes_out)\n    result_reader = PdfReader(bytes_out)\n    pg1_text = result_reader.pages[0].extract_text()\n    pg2_text = result_reader.pages[1].extract_text()\n    assert pg1_text.strip() == \"1\"\n    assert pg2_text.strip() == \"2\"\n\n\n@pytest.mark.slow\ndef test_orientations():\n    p = PdfReader(RESOURCE_ROOT / \"test Orient.pdf\").pages[0]\n    p.extract_text(\"\", \"\")\n    p.extract_text(\"\", \"\", 0)\n    p.extract_text(\"\", \"\", 0, 200)\n    p.extract_text()\n    assert findall(\"\\\\((.)\\\\)\", p.extract_text()) == [\"T\", \"B\", \"L\", \"R\"]\n    with pytest.raises(Exception):\n        p.extract_text(None)\n    p.extract_text(\"\", 0)\n    with pytest.raises(Exception):\n        p.extract_text(\"\", \"\", None)\n    with pytest.raises(Exception):\n        p.extract_text(\"\", \"\", 0, \"\")\n    with pytest.raises(Exception):\n        p.extract_text(0, \"\")\n\n    p.extract_text(0, 0)\n    p.extract_text(orientations=0)\n\n    for req, rst in (\n        (0, [\"T\"]),\n        (90, [\"L\"]),\n        (180, [\"B\"]),\n        (270, [\"R\"]),\n        ((0,), [\"T\"]),\n        ((0, 180), [\"T\", \"B\"]),\n        ((45,), []),\n    ):\n        assert (\n            findall(\"\\\\((.)\\\\)\", p.extract_text(req)) == rst\n        ), f\"extract_text({req}) => {rst}\"\n\n\n@pytest.mark.samples\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"base_path\", \"overlay_path\"),\n    [\n        (\n            \"resources/crazyones.pdf\",\n            \"sample-files/013-reportlab-overlay/reportlab-overlay.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381707/tika-935981.pdf\",\n            \"sample-files/013-reportlab-overlay/reportlab-overlay.pdf\",\n        ),\n    ],\n)\ndef test_overlay(pdf_file_path, base_path, overlay_path):\n    if base_path.startswith(\"http\"):\n        base_path = BytesIO(get_data_from_url(base_path, name=\"tika-935981.pdf\"))\n    else:\n        base_path = PROJECT_ROOT / base_path\n    writer = PdfWriter(clone_from=base_path)\n\n    reader_overlay = PdfReader(PROJECT_ROOT / overlay_path)\n    overlay = reader_overlay.pages[0]\n\n    for page in writer.pages:\n        page.merge_page(overlay)\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381697/tika-924546.pdf\",\n            \"tika-924546.pdf\",\n        )\n    ],\n)\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_merge_with_warning(tmp_path, url, name):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n    merger = PdfWriter()\n    merger.append(reader)\n    # This could actually be a performance bottleneck:\n    merger.write(tmp_path / \"tmp.merged.pdf\")\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381757/tika-980613.pdf\",\n            \"tika-980613.pdf\",\n        )\n    ],\n)\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_merge(tmp_path, url, name):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(tmp_path / \"tmp.merged.pdf\")\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"expected_metadata\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381708/tika-935996.pdf\",\n            \"tika-935996.pdf\",\n            {\n                \"/Author\": \"Unknown\",\n                \"/CreationDate\": \"Thursday, May 06, 1999 3:56:54 PM\",\n                \"/Creator\": r\"C:\\DEB\\6338\",\n                \"/Keywords\": \"\",\n                \"/Producer\": \"Acrobat PDFWriter 3.02 for Windows\",\n                \"/Subject\": \"\",\n                \"/Title\": r\"C:\\DEB\\6338-6R.PDF\",\n            },\n        )\n    ],\n)\ndef test_get_metadata(url, name, expected_metadata):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n    data = reader.metadata\n    assert expected_metadata == data\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"strict\", \"exception\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/16624503/tika-938702.pdf\",\n            \"tika-938702.pdf\",\n            False,\n            None,  # iss #1090 is now fixed\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381715/tika-942358.pdf\",\n            \"tika-942358.pdf\",\n            False,\n            None,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381684/tika-911260.pdf\",\n            \"tika-911260.pdf\",\n            False,\n            None,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381766/tika-992472.pdf\",\n            \"tika-992472.pdf\",\n            False,\n            None,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381756/tika-978477.pdf\",\n            \"tika-978477.pdf\",\n            False,\n            None,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381731/tika-960317.pdf\",\n            \"tika-960317.pdf\",\n            False,\n            None,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381701/tika-930513.pdf\",\n            \"tika-930513.pdf\",\n            False,\n            None,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381691/tika-918113.pdf\",\n            \"tika-918113.pdf\",\n            True,\n            None,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381711/tika-940704.pdf\",\n            \"tika-940704.pdf\",\n            True,\n            None,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381752/tika-976488.pdf\",\n            \"tika-976488.pdf\",\n            True,\n            None,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381716/tika-948176.pdf\",\n            \"tika-948176.pdf\",\n            True,\n            None,\n        ),\n    ],\n)\ndef test_extract_text(url, name, strict, exception):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data, strict=strict)\n    if not exception:\n        for page in reader.pages:\n            page.extract_text()\n    else:\n        exc, exc_text = exception\n        with pytest.raises(exc) as ex_info:\n            for page in reader.pages:\n                page.extract_text()\n        assert ex_info.value.args[0] == exc_text\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381710/tika-938702.pdf\",\n            \"tika-938702.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381725/tika-957304.pdf\",\n            \"tika-957304.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381690/tika-915194.pdf\",\n            \"tika-915194.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381717/tika-950337.pdf\",\n            \"tika-950337.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381734/tika-962292.pdf\",\n            \"tika-962292.pdf\",\n        ),\n    ],\n)\ndef test_compress_raised(url, name):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    # no more error since iss #1090 fix\n    for page in writer.pages:\n        page.compress_content_streams()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381733/tika-961883.pdf\",\n            \"tika-961883.pdf\",\n        ),\n    ],\n)\ndef test_get_fields_warns(tmp_path, caplog, url, name):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n    write_path = tmp_path / \"tmp.txt\"\n    with open(write_path, \"w\") as fp:\n        retrieved_fields = reader.get_fields(fileobj=fp)\n\n    assert retrieved_fields == {}\n    assert normalize_warnings(caplog.text) == [\n        \"Ignoring wrong pointing object 1 65536 (offset 0)\",\n        \"Ignoring wrong pointing object 2 65536 (offset 0)\",\n        \"Object 2 0 not defined.\",\n    ]\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381713/tika-942050.pdf\",\n            \"tika-942050.pdf\",\n        ),\n    ],\n)\ndef test_get_fields_no_warning(tmp_path, url, name):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n    write_path = tmp_path / \"tmp.txt\"\n    with open(write_path, \"w\") as fp:\n        retrieved_fields = reader.get_fields(fileobj=fp)\n\n    assert len(retrieved_fields) == 10\n\n\n@pytest.mark.enable_socket\ndef test_scale_rectangle_indirect_object():\n    url = \"https://github.com/user-attachments/files/18381778/tika-999944.pdf\"\n    name = \"tika-999944.pdf\"\n    data = BytesIO(get_data_from_url(url, name=name))\n    writer = PdfWriter(clone_from=data)\n\n    for page in writer.pages:\n        page.scale(sx=2, sy=3)\n\n\ndef test_merge_output(caplog):\n    # Arrange\n    base = RESOURCE_ROOT / \"Seige_of_Vicksburg_Sample_OCR.pdf\"\n    crazy = RESOURCE_ROOT / \"crazyones.pdf\"\n    expected = RESOURCE_ROOT / \"Seige_of_Vicksburg_Sample_OCR-crazyones-merged.pdf\"\n\n    # Act\n    merger = PdfWriter()\n    merger.append(base)\n    merger.merge(1, crazy)\n    stream = BytesIO()\n    merger.write(stream)\n\n    # Assert\n    stream.seek(0)\n    actual = stream.read()\n    with open(expected, \"rb\") as fp:\n        expected_data = fp.read()\n    if actual != expected_data:\n        # See https://github.com/pytest-dev/pytest/issues/9124\n        pytest.fail(\n            f\"len(actual) = {len(actual):,} vs len(expected) = {len(expected_data):,}\"\n        )\n\n    # Cleanup\n    merger.close()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381767/tika-994636.pdf\",\n            \"tika-994636.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381719/tika-952133.pdf\",\n            \"tika-952133.pdf\",\n        ),\n        (  # JPXDecode\n            \"https://github.com/user-attachments/files/18381688/tika-914568.pdf\",\n            \"tika-914568.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381718/tika-952016.pdf\",\n            \"tika-952016.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18382223/965118.pdf\",\n            \"tika-965118.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381729/tika-959184.pdf\",\n            \"tika-959184.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381727/tika-958496.pdf\",\n            \"tika-958496.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381744/tika-972174.pdf\",\n            \"tika-972174.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381745/tika-972243.pdf\",\n            \"tika-972243.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381743/tika-969502.pdf\",\n            \"tika-969502.pdf\",\n        ),\n        (\"https://arxiv.org/pdf/2201.00214.pdf\", \"arxiv-2201.00214.pdf\"),\n    ],\n)\ndef test_image_extraction(url, name):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n\n    images_extracted = []\n    root = Path(\"extracted-images\")\n    if not root.exists():\n        root.mkdir()\n\n    with PILContext():\n        for page in reader.pages:\n            for image in page.images:\n                filename = root / image.name\n                with open(filename, \"wb\") as img:\n                    img.write(image.data)\n                images_extracted.append(filename)\n\n    # Cleanup\n    do_cleanup = True  # set this to False for manual inspection\n    if do_cleanup:\n        for filepath in images_extracted:\n            if Path(filepath).exists():\n                Path(filepath).unlink()\n\n\n@pytest.mark.enable_socket\ndef test_image_extraction_strict():\n    # Emits log messages\n    url = \"https://github.com/user-attachments/files/18381687/tika-914102.pdf\"\n    name = \"tika-914102.pdf\"\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data, strict=True)\n\n    images_extracted = []\n    root = Path(\"extracted-images\")\n    if not root.exists():\n        root.mkdir()\n\n    for page in reader.pages:\n        for image in page.images:\n            filename = root / image.name\n            with open(filename, \"wb\") as fp:\n                fp.write(image.data)\n            images_extracted.append(filename)\n\n    # Cleanup\n    do_cleanup = True  # set this to False for manual inspection\n    if do_cleanup:\n        for filepath in images_extracted:\n            if Path(filepath).exists():\n                Path(filepath).unlink()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381754/tika-977609.pdf\",\n            \"tika-977609.pdf\",\n        ),\n    ],\n)\ndef test_image_extraction2(url, name):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n\n    images_extracted = []\n    root = Path(\"extracted-images\")\n    if not root.exists():\n        root.mkdir()\n\n    for page in reader.pages:\n        for image in page.images:\n            filename = root / image.name\n            with open(filename, \"wb\") as img:\n                img.write(image.data)\n            images_extracted.append(filename)\n\n    # Cleanup\n    do_cleanup = True  # set this to False for manual inspection\n    if do_cleanup:\n        for filepath in images_extracted:\n            if Path(filepath).exists():\n                Path(filepath).unlink()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381692/tika-918137.pdf\",\n            \"tika-918137.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/22596566/7552c42e9280b4476e59e77acc0bc812.pdf\",\n            \"7552c42e9280b4476e59e77acc0bc812.pdf\",\n        ),\n    ],\n)\ndef test_get_outline(url, name):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n    reader.outline\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381707/tika-935981.pdf\",\n            \"tika-935981.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381709/tika-937334.pdf\",\n            \"tika-937334.pdf\",\n        ),\n    ],\n)\ndef test_get_xfa(url, name):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data)\n    reader.xfa\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"strict\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381765/tika-988698.pdf\",\n            \"tika-988698.pdf\",\n            False,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18382162/914133.pdf\",\n            \"tika-914133.pdf\",\n            False,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381685/tika-912552.pdf\",\n            \"tika-912552.pdf\",\n            False,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381687/tika-914102.pdf\",\n            \"tika-914102.pdf\",\n            True,\n        ),\n    ],\n)\ndef test_get_fonts(url, name, strict):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data, strict=strict)\n    for page in reader.pages:\n        page._get_fonts()\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"strict\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18382060/tika-942303.pdf\",\n            \"tika-942303.pdf\",\n            True,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381707/tika-935981.pdf\",\n            \"tika-935981.pdf\",\n            True,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381738/tika-967399.pdf\",\n            \"tika-967399.pdf\",\n            True,\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381707/tika-935981.pdf\",\n            \"tika-935981.pdf\",\n            False,\n        ),\n    ],\n)\ndef test_get_xmp(url, name, strict):\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data, strict=strict)\n    xmp_info = reader.xmp_metadata\n    if xmp_info:\n        xmp_info.dc_contributor\n        xmp_info.dc_coverage\n        xmp_info.dc_creator\n        xmp_info.dc_date\n        xmp_info.dc_description\n        xmp_info.dc_format\n        xmp_info.dc_identifier\n        xmp_info.dc_language\n        xmp_info.dc_publisher\n        xmp_info.dc_relation\n        xmp_info.dc_rights\n        xmp_info.dc_source\n        xmp_info.dc_subject\n        xmp_info.dc_title\n        xmp_info.dc_type\n        xmp_info.pdf_keywords\n        xmp_info.pdf_pdfversion\n        xmp_info.pdf_producer\n        xmp_info.xmp_create_date\n        xmp_info.xmp_modify_date\n        xmp_info.xmp_metadata_date\n        xmp_info.xmp_creator_tool\n        xmp_info.xmpmm_document_id\n        xmp_info.xmpmm_instance_id\n        xmp_info.custom_properties\n\n\n@pytest.mark.enable_socket\ndef test_tounicode_is_identity():\n    url = \"https://github.com/py-pdf/pypdf/files/9998335/FP_Thesis.pdf\"\n    name = \"FP_Thesis.pdf\"\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data, strict=False)\n    reader.pages[0].extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_append_forms():\n    # from #1538\n    writer = PdfWriter()\n\n    url = \"https://github.com/py-pdf/pypdf/files/10367412/pdfa.pdf\"\n    name = \"form_a.pdf\"\n    reader1 = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader1.add_form_topname(\"form_a\")\n    writer.append(reader1)\n\n    url = \"https://github.com/py-pdf/pypdf/files/10367413/pdfb.pdf\"\n    name = \"form_b.pdf\"\n    reader2 = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    reader2.add_form_topname(\"form_b\")\n    writer.append(reader2)\n\n    b = BytesIO()\n    writer.write(b)\n    reader = PdfReader(b)\n    assert len(reader.get_form_text_fields()) == len(\n        reader1.get_form_text_fields()\n    ) + len(reader2.get_form_text_fields())\n\n\n@pytest.mark.enable_socket\ndef test_extra_test_iss1541():\n    url = \"https://github.com/py-pdf/pypdf/files/10418158/tst_iss1541.pdf\"\n    name = \"tst_iss1541.pdf\"\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data, strict=False)\n    reader.pages[0].extract_text()\n\n    cs = ContentStream(reader.pages[0][\"/Contents\"], None, None)\n    cs.operations.insert(-1, ([], b\"EMC\"))\n    stream = BytesIO()\n    cs.write_to_stream(stream)\n    stream.seek(0)\n    ContentStream(read_object(stream, None, None), None, None).operations\n\n    cs = ContentStream(reader.pages[0][\"/Contents\"], None, None)\n    cs.operations.insert(-1, ([], b\"E!C\"))\n    stream = BytesIO()\n    cs.write_to_stream(stream)\n    stream.seek(0)\n    ContentStream(read_object(stream, None, None), None, None).operations\n\n    b = BytesIO(data.getbuffer())\n    reader = PdfReader(\n        BytesIO(bytes(b.getbuffer()).replace(b\"EI \\n\", b\"E! \\n\")), strict=False\n    )\n    with pytest.raises(PdfReadError) as exc:\n        reader.pages[0].extract_text()\n    assert exc.value.args[0] == \"Unexpected end of stream\"\n\n\n@pytest.mark.enable_socket\ndef test_fields_returning_stream():\n    \"\"\"This problem was reported in #424\"\"\"\n    url = \"https://github.com/mstamy2/PyPDF2/files/1948267/Simple.form.pdf\"\n    name = \"tst_iss424.pdf\"\n    data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(data, strict=False)\n    assert \"BtchIssQATit_time\" in reader.get_form_text_fields()[\"TimeStampData\"]\n\n\ndef test_replace_image(tmp_path):\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"labeled-edges-center-image.pdf\")\n    reader = PdfReader(RESOURCE_ROOT / \"jpeg.pdf\")\n    img = reader.pages[0].images[0].image\n    if int(pil_version.split(\".\")[0]) < 9:\n        img = img.convert(\"RGB\")\n    writer.pages[0].images[0].replace(img)\n    b = BytesIO()\n    writer.write(b)\n    reader2 = PdfReader(b)\n    if int(pil_version.split(\".\")[0]) >= 9:\n        assert reader2.pages[0].images[0].image.mode == \"RGBA\"\n    # very simple image distance evaluation\n    diff = ImageChops.difference(reader2.pages[0].images[0].image, img)\n    d = sum(get_image_data(diff.convert(\"L\"))) / (diff.size[0] * diff.size[1])\n    assert d < 1.5\n    img = img.convert(\"RGB\")  # quality does not apply to RGBA/JP2\n    writer.pages[0].images[0].replace(img, quality=20)\n    diff = ImageChops.difference(writer.pages[0].images[0].image, img)\n    d1 = sum(get_image_data(diff.convert(\"L\"))) / (diff.size[0] * diff.size[1])\n    assert d1 > d\n    # extra tests for coverage\n    with pytest.raises(TypeError) as exc:\n        reader.pages[0].images[0].replace(img)\n    assert exc.value.args[0] == \"Cannot update an image not belonging to a PdfWriter.\"\n    i = writer.pages[0].images[0]\n    with pytest.raises(TypeError) as exc:\n        i.replace(reader.pages[0].images[0])  # missing .image\n    assert exc.value.args[0] == \"new_image shall be a PIL Image\"\n    i.indirect_reference = None  # to behave like an inline image\n    with pytest.raises(TypeError) as exc:\n        i.replace(reader.pages[0].images[0].image)\n    assert exc.value.args[0] == \"Cannot update an inline image.\"\n\n    import pypdf  # noqa: PLC0415\n\n    try:\n        pypdf._page.pil_not_imported = True\n        with pytest.raises(ImportError) as exc:\n            i.replace(reader.pages[0].images[0].image)\n    finally:\n        pypdf._page.pil_not_imported = False\n\n\n@pytest.mark.enable_socket\ndef test_inline_images():\n    \"\"\"This problem was reported in #424\"\"\"\n    url = \"https://arxiv.org/pdf/2201.00151.pdf\"\n    name = \"2201.00151.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/py-pdf/pypdf/assets/4083478/28e8b87c-be2c-40d9-9c86-15c7819021bf\"\n    name = \"inline4.png\"\n    img_ref = Image.open(BytesIO(get_data_from_url(url, name=name)))\n    assert get_image_data(reader.pages[1].images[4].image) == get_image_data(img_ref)\n    with pytest.raises(KeyError):\n        reader.pages[0].images[\"~999~\"]\n    del reader.pages[1][\"/Resources\"][\"/ColorSpace\"][\"/R124\"]\n    reader.pages[1].inline_images = None  # to force recalculation\n    with pytest.raises(PdfReadError):\n        reader.pages[1].images[\"~1~\"]\n\n    co = reader.pages[0].get_contents()\n    co.operations.append(([], b\"BI\"))\n    reader.pages[0][NameObject(\"/Contents\")] = co\n    reader.pages[0].images.keys()\n\n    with pytest.raises(TypeError) as exc:\n        reader.pages[0].images[0].replace(img_ref)\n    assert exc.value.args[0] == \"Cannot update an inline image.\"\n\n    _a = {}\n    for x, y in reader.pages[2].images[0:-2].items():\n        _a[x] = y  # noqa: PERF403  # Testing code and easier to read this way.\n    with pytest.raises(KeyError) as exc:\n        reader.pages[2]._get_image((\"test\",))\n\n    url = \"https://github.com/py-pdf/pypdf/files/15233597/bug1065245.pdf\"\n    name = \"iss2598c.pdf\"  # test data also used in test_images.py/test_inline_image_extraction()\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert len(reader.pages[0].images) == 3\n\n\n@pytest.mark.enable_socket\ndef test_issue1899():\n    url = \"https://github.com/py-pdf/pypdf/files/11801077/lv2018tconv.pdf\"\n    name = \"lv2018tconv.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    for i, page in enumerate(reader.pages):\n        print(i)\n        page.extract_text()\n\n\n@pytest.mark.enable_socket\ndef test_cr_with_cm_operation():\n    \"\"\"Issue #2138\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12483807/AEO.1172.pdf\"\n    name = \"iss2138.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    assert (\n        \"\"\"STATUS: FNL\nSTYLE: 1172 1172 KNIT SHORTIE SUMMER-B 2023\nCompany: AMERICAN EAGLE OUTFITTERS\nDivision / Dept: 50 / 170\nSeason: SUMMER-B 2023\"\"\"\n        in reader.pages[0].extract_text()\n    )\n    # currently there is still a white space on last line missing\n    # so we can not do a full comparison.\n\n\ndef remove_trailing_whitespace(text: str) -> str:\n    text = text.strip()\n    return \"\\n\".join(line.rstrip() for line in text.splitlines())\n\n\n@pytest.mark.samples\n@pytest.mark.parametrize(\n    (\"pdf_path\", \"expected_path\"),\n    [\n        (\n            SAMPLE_ROOT / \"026-latex-multicolumn/multicolumn.pdf\",\n            RESOURCE_ROOT / \"multicolumn-lorem-ipsum.txt\",\n        ),\n        (\n            SAMPLE_ROOT / \"010-pdflatex-forms/pdflatex-forms.pdf\",\n            RESOURCE_ROOT / \"010-pdflatex-forms.txt\",\n        ),\n    ],\n)\ndef test_text_extraction_layout_mode(pdf_path, expected_path):\n    reader = PdfReader(pdf_path)\n    actual = reader.pages[0].extract_text(extraction_mode=\"layout\")\n    expected = expected_path.read_text(encoding=\"utf-8\")\n    # We don't care about trailing whitespace\n    assert remove_trailing_whitespace(actual) == remove_trailing_whitespace(expected)\n\n\n@pytest.mark.enable_socket\ndef test_layout_mode_space_vertically():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss2138.pdf\")))\n    # remove automatically added final newline\n    expected = (\n        (RESOURCE_ROOT / \"AEO.1172.layout.txt\").read_text(encoding=\"utf-8\").rstrip()\n    )\n    assert expected == reader.pages[0].extract_text(\n        extraction_mode=\"layout\", layout_mode_space_vertically=False\n    )\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"rotation\", \"strip_rotated\"), [(90, True), (180, False), (270, True)]\n)\ndef test_layout_mode_rotations(rotation, strip_rotated):\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(name=\"iss2138.pdf\")))\n    rotated_page = writer.pages[0].rotate(rotation)\n    rotated_page.transfer_rotation_to_content()\n    expected = \"\"\n    if not strip_rotated:\n        expected = (\n            (RESOURCE_ROOT / \"AEO.1172.layout.rot180.txt\")\n            .read_text(encoding=\"utf-8\")\n            .rstrip()\n        )  # remove automatically added final newline\n    assert expected == rotated_page.extract_text(\n        extraction_mode=\"layout\",\n        layout_mode_space_vertically=False,\n        layout_mode_strip_rotated=strip_rotated,\n    )\n\n\ndef test_text_extraction_invalid_mode():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    with pytest.raises(ValueError, match=\"Invalid text extraction mode\"):\n        reader.pages[0].extract_text(extraction_mode=\"foo\")  # type: ignore\n\n\n@pytest.mark.enable_socket\ndef test_get_page_showing_field():\n    \"\"\"\n    Uses testfile from #2452 in order to get fields on multiple pages,\n        choices boxes,...\n    \"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/14031491/Form_Structure_v50.pdf\"\n    name = \"iss2452.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name)))\n    writer = PdfWriter(clone_from=reader)\n\n    # validate with Field:  only works on Reader (no get_fields on writer yet)\n    fld = reader.get_fields()\n    assert [\n        p.page_number for p in reader.get_pages_showing_field(fld[\"FormVersion\"])\n    ] == [0]\n\n    # validate with dictionary object\n    # NRCategory field is a radio box\n    assert [\n        p.page_number\n        for p in reader.get_pages_showing_field(\n            reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][8].get_object()\n        )\n    ] == [0, 0, 0, 0, 0]\n    assert [\n        p.page_number\n        for p in writer.get_pages_showing_field(\n            writer._root_object[\"/AcroForm\"][\"/Fields\"][8].get_object()\n        )\n    ] == [0, 0, 0, 0, 0]\n\n    # validate with IndirectObject\n    # SiteID field is a textbox on multiple pages\n    assert [\n        p.page_number\n        for p in reader.get_pages_showing_field(\n            reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][99]\n        )\n    ] == [0, 1]\n    assert [\n        p.page_number\n        for p in writer.get_pages_showing_field(\n            writer._root_object[\"/AcroForm\"][\"/Fields\"][99]\n        )\n    ] == [0, 1]\n    # test directly on the widget:\n    assert [\n        p.page_number\n        for p in reader.get_pages_showing_field(\n            reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][99][\"/Kids\"][1]\n        )\n    ] == [1]\n    assert [\n        p.page_number\n        for p in writer.get_pages_showing_field(\n            writer._root_object[\"/AcroForm\"][\"/Fields\"][99][\"/Kids\"][1]\n        )\n    ] == [1]\n\n    # Exceptions:\n    # Invalid Object\n    with pytest.raises(ValueError) as exc:\n        reader.get_pages_showing_field(None)\n    with pytest.raises(ValueError) as exc:\n        writer.get_pages_showing_field(None)\n    assert \"Field type is invalid\" in exc.value.args[0]\n\n    # Damage Field\n    del reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][1].get_object()[\"/FT\"]\n    del writer._root_object[\"/AcroForm\"][\"/Fields\"][1].get_object()[\"/FT\"]\n    with pytest.raises(ValueError) as exc:\n        reader.get_pages_showing_field(\n            reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][1]\n        )\n    with pytest.raises(ValueError) as exc:\n        writer.get_pages_showing_field(writer._root_object[\"/AcroForm\"][\"/Fields\"][1])\n    assert \"Field is not valid\" in exc.value.args[0]\n\n    # missing Parent in field\n    del reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][99][\"/Kids\"][1].get_object()[\n        \"/Parent\"\n    ]\n    del writer._root_object[\"/AcroForm\"][\"/Fields\"][99][\"/Kids\"][1].get_object()[\n        \"/Parent\"\n    ]\n    with pytest.raises(ValueError) as exc:\n        reader.get_pages_showing_field(\n            reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][1]\n        )\n    with pytest.raises(ValueError) as exc:\n        writer.get_pages_showing_field(writer._root_object[\"/AcroForm\"][\"/Fields\"][1])\n\n    # remove \"/P\" (optional)\n    del reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][8][\"/Kids\"][1].get_object()[\n        \"/P\"\n    ]\n    del writer._root_object[\"/AcroForm\"][\"/Fields\"][8][\"/Kids\"][1].get_object()[\"/P\"]\n    assert [\n        p.page_number\n        for p in reader.get_pages_showing_field(\n            reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][8][\"/Kids\"][1]\n        )\n    ] == [0]\n    assert [\n        p.page_number\n        for p in writer.get_pages_showing_field(\n            writer._root_object[\"/AcroForm\"][\"/Fields\"][8][\"/Kids\"][1]\n        )\n    ] == [0]\n    assert [\n        p.page_number\n        for p in reader.get_pages_showing_field(\n            reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][8].get_object()\n        )\n    ] == [0, 0, 0, 0, 0]\n    assert [\n        p.page_number\n        for p in writer.get_pages_showing_field(\n            writer._root_object[\"/AcroForm\"][\"/Fields\"][8].get_object()\n        )\n    ] == [0, 0, 0, 0, 0]\n\n    # Grouping fields\n    reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][-1].get_object()[\n        NameObject(\"/Kids\")\n    ] = ArrayObject([reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][0]])\n    del reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][-1].get_object()[\"/T\"]\n    del reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][-1].get_object()[\"/P\"]\n    del reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][-1].get_object()[\"/Subtype\"]\n    writer._root_object[\"/AcroForm\"][\"/Fields\"].append(\n        writer._add_object(\n            DictionaryObject(\n                {\n                    NameObject(\"/T\"): TextStringObject(\"grouping\"),\n                    NameObject(\"/FT\"): NameObject(\"/Tx\"),\n                    NameObject(\"/Kids\"): ArrayObject(\n                        [reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][0]]\n                    ),\n                }\n            )\n        )\n    )\n    assert [\n        p.page_number\n        for p in reader.get_pages_showing_field(\n            reader.trailer[\"/Root\"][\"/AcroForm\"][\"/Fields\"][-1]\n        )\n    ] == []\n    assert [\n        p.page_number\n        for p in writer.get_pages_showing_field(\n            writer._root_object[\"/AcroForm\"][\"/Fields\"][-1]\n        )\n    ] == []\n\n\n@pytest.mark.enable_socket\ndef test_extract_empty_page():\n    \"\"\"Cf #2533\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/14718318/test.pdf\"\n    name = \"iss2533.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name)))\n    assert reader.pages[1].extract_text(extraction_mode=\"layout\") == \"\"\n\n\n@pytest.mark.enable_socket\ndef test_iss2815():\n    \"\"\"Cf #2815\"\"\"\n    url = \"https://github.com/user-attachments/files/16760725/crash-c1920c7a064649e1191d7879952ec252473fc7e6.pdf\"\n    name = \"iss2815.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name)))\n    assert reader.pages[0].extract_text() == \"test command with wrong number of args\"\n"
  },
  {
    "path": "tests/test_writer.py",
    "content": "\"\"\"Test the pypdf._writer module.\"\"\"\n\nimport re\nimport shutil\nimport subprocess\nfrom io import BytesIO\nfrom pathlib import Path\nfrom tempfile import NamedTemporaryFile\nfrom typing import Any\nfrom unittest import mock\n\nimport pytest\n\nfrom pypdf import (\n    ImageType,\n    ObjectDeletionFlag,\n    PageObject,\n    PdfReader,\n    PdfWriter,\n    Transformation,\n)\nfrom pypdf.annotations import Link\nfrom pypdf.errors import DeprecationError, PageSizeNotDefinedError, PdfReadError, PyPdfError\nfrom pypdf.generic import (\n    ArrayObject,\n    ByteStringObject,\n    ContentStream,\n    DecodedStreamObject,\n    Destination,\n    DictionaryObject,\n    Fit,\n    IndirectObject,\n    NameObject,\n    NullObject,\n    NumberObject,\n    RectangleObject,\n    StreamObject,\n    TextStringObject,\n)\n\nfrom . import RESOURCE_ROOT, SAMPLE_ROOT, get_data_from_url, is_sublist\nfrom .test_images import image_similarity\n\nGHOSTSCRIPT_BINARY = shutil.which(\"gs\")\n\n\ndef _get_write_target(convert) -> Any:\n    target = convert\n    if callable(convert):\n        with NamedTemporaryFile(suffix=\".pdf\", delete=False) as temporary:\n            target = temporary.name\n    return target\n\n\ndef test_writer_exception_non_binary(tmp_path, caplog):\n    src = RESOURCE_ROOT / \"pdflatex-outline.pdf\"\n\n    reader = PdfReader(src)\n    writer = PdfWriter()\n    writer.add_page(reader.pages[0])\n\n    with open(tmp_path / \"out.txt\", \"w\") as fp, pytest.raises(TypeError):\n        writer.write_stream(fp)\n    ending = \"to write to is not in binary mode. It may not be written to correctly.\\n\"\n    assert caplog.text.endswith(ending)\n\n\ndef test_writer_clone():\n    src = RESOURCE_ROOT / \"pdflatex-outline.pdf\"\n\n    reader = PdfReader(src)\n    writer = PdfWriter(clone_from=reader)\n    assert len(writer.pages) == 4\n    assert \"PageObject\" in str(type(writer.pages[0]))\n\n    writer = PdfWriter(clone_from=src)\n    assert len(writer.pages) == 4\n    assert \"PageObject\" in str(type(writer.pages[0]))\n\n\ndef test_clone_metadata():\n    src = RESOURCE_ROOT / \"pdflatex-outline.pdf\"\n    reader = PdfReader(src)\n\n    writer = PdfWriter(clone_from=reader)\n    writer.add_metadata({\"/foo\": \"bar\"})\n    assert writer.metadata == {\n        **reader.metadata,\n        \"/foo\": \"bar\",\n    }\n\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    writer.add_metadata({\"/foo\": \"bar\"})\n    assert writer.metadata == {\n        **reader.metadata,\n        \"/foo\": \"bar\",\n    }\n    writer.metadata = None\n    writer.add_metadata({\"/foo\": \"bar\"})\n    assert writer.metadata == {\"/foo\": \"bar\"}\n\n    writer = PdfWriter()\n    writer.clone_reader_document_root(reader)\n    writer.add_metadata({\"/foo\": \"bar\"})\n    assert writer.metadata == {\"/foo\": \"bar\"}\n\n\ndef test_writer_clone_bookmarks():\n    # Arrange\n    src = RESOURCE_ROOT / \"Seige_of_Vicksburg_Sample_OCR-crazyones-merged.pdf\"\n    reader = PdfReader(src)\n    writer = PdfWriter()\n\n    # Act + test cat\n    cat = \"\"\n\n    def cat1(p) -> None:\n        nonlocal cat\n        cat += p.__repr__()\n\n    writer.clone_document_from_reader(reader, cat1)\n    assert \"/Page\" in cat\n    assert writer.pages[0].raw_get(\"/Parent\") == writer._pages\n    writer.add_outline_item(\"Page 1\", 0)\n    writer.add_outline_item(\"Page 2\", 1)\n\n    # Assert\n    bytes_stream = BytesIO()\n    writer.write(bytes_stream)\n    bytes_stream.seek(0)\n    reader2 = PdfReader(bytes_stream)\n    assert len(reader2.pages) == len(reader.pages)\n    assert len(reader2.outline) == 2\n\n    # test with append\n    writer = PdfWriter()\n    writer.append(reader)\n    writer.add_outline_item(\"Page 1\", 0)\n    writer.add_outline_item(\"Page 2\", 1)\n\n    # Assert\n    bytes_stream = BytesIO()\n    writer.write(bytes_stream)\n    bytes_stream.seek(0)\n    reader2 = PdfReader(bytes_stream)\n    assert len(reader2.pages) == len(reader.pages)\n    assert len(reader2.outline) == 2\n\n\ndef writer_operate(writer: PdfWriter) -> None:\n    \"\"\"\n    To test the writer that initialized by each of the four usages.\n\n    Args:\n        writer: A PdfWriter object\n\n    \"\"\"\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    pdf_outline_path = RESOURCE_ROOT / \"pdflatex-outline.pdf\"\n\n    reader = PdfReader(pdf_path)\n    reader_outline = PdfReader(pdf_outline_path)\n\n    page = reader.pages[0]\n    with pytest.raises(PageSizeNotDefinedError) as exc:\n        writer.add_blank_page()\n    assert exc.value.args == ()\n    writer.insert_page(page, 1)\n    writer.insert_page(reader_outline.pages[0], 0)\n    writer.add_outline_item_destination(page)\n    writer.remove_links()\n    writer.add_outline_item_destination(page)\n    oi = writer.add_outline_item(\n        \"An outline item\", 0, None, (255, 0, 15), True, True, Fit.fit_box_vertically(10)\n    )\n    writer.add_outline_item(\n        \"The XYZ fit\", 0, oi, (255, 0, 15), True, True, Fit.xyz(left=10, top=20, zoom=3)\n    )\n    writer.add_outline_item(\n        \"The XYZ fit no args\", 0, oi, (255, 0, 15), True, True, Fit.xyz()\n    )\n    writer.add_outline_item(\n        \"The FitH fit\", 0, oi, (255, 0, 15), True, True, Fit.fit_horizontally(top=10)\n    )\n    writer.add_outline_item(\n        \"The FitV fit\", 0, oi, (255, 0, 15), True, True, Fit.fit_vertically(left=10)\n    )\n    writer.add_outline_item(\n        \"The FitR fit\",\n        0,\n        oi,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit_rectangle(left=10, bottom=20, right=30, top=40),\n    )\n    writer.add_outline_item(\n        \"The FitB fit\", 0, oi, (255, 0, 15), True, True, Fit.fit_box()\n    )\n    writer.add_outline_item(\n        \"The FitBH fit\",\n        0,\n        oi,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit_box_horizontally(top=10),\n    )\n    writer.add_outline_item(\n        \"The FitBV fit\",\n        0,\n        oi,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit_box_vertically(left=10),\n    )\n    writer.add_blank_page()\n    writer.add_uri(2, \"https://example.com\", RectangleObject([0, 0, 100, 100]))\n    writer.add_uri(2, \"https://example.com\", RectangleObject([0, 0, 100, 100]))\n    writer.add_annotation(\n        page_number=2,\n        annotation=Link(target_page_index=1, rect=RectangleObject([0, 0, 100, 100])),\n    )\n    assert writer._get_page_layout() is None\n    writer.page_layout = \"broken\"\n    assert writer.page_layout == \"broken\"\n    writer.page_layout = NameObject(\"/SinglePage\")\n    assert writer._get_page_layout() == \"/SinglePage\"\n    assert writer._get_page_mode() is None\n    writer.page_mode = \"/UseNone\"\n    assert writer._get_page_mode() == \"/UseNone\"\n    writer.page_mode = NameObject(\"/UseOC\")\n    assert writer._get_page_mode() == \"/UseOC\"\n    writer.insert_blank_page(width=100, height=100)\n    page = writer.insert_blank_page(width=100)\n    assert page.mediabox.height == 100\n    page = writer.insert_blank_page(height=100)\n    assert page.mediabox.width == 100\n    writer.insert_blank_page()  # without parameters\n\n    writer.remove_images()\n\n    writer.add_metadata(reader.metadata)\n    writer.add_metadata({\"/Author\": \"Martin Thoma\"})\n    writer.add_metadata({\"/MyCustom\": 1234})\n\n    writer.add_attachment(\"foobar.gif\", b\"foobarcontent\")\n\n    # Check that every key in _idnum_hash is correct\n    objects_hash = [o.hash_value() for o in writer._objects]\n    for k, v in writer._idnum_hash.items():\n        assert v.pdf == writer\n        assert k in objects_hash, f\"Missing {v}\"\n\n\ndef test_insert_blank_page():\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n\n    old_page_count = len(writer.pages)\n\n    old_page = writer.pages[0]\n    page = writer.insert_blank_page(index=0)\n    assert len(writer.pages) == old_page_count + 1\n    assert page.mediabox.width == old_page.mediabox.width\n    assert page.mediabox.height == old_page.mediabox.height\n\n    old_page = writer.pages[0]\n    page = writer.insert_blank_page(width=10, index=0)\n    assert len(writer.pages) == old_page_count + 2\n    assert page.mediabox.width == 10\n    assert page.mediabox.height == old_page.mediabox.height\n\n    old_page = writer.pages[0]\n    page = writer.insert_blank_page(width=-10, index=0)\n    assert len(writer.pages) == old_page_count + 3\n    assert page.mediabox.width == old_page.mediabox.width\n    assert page.mediabox.height == old_page.mediabox.height\n\n    old_page = writer.pages[0]\n    page = writer.insert_blank_page(height=20, index=0)\n    assert len(writer.pages) == old_page_count + 4\n    assert page.mediabox.width == old_page.mediabox.width\n    assert page.mediabox.height == 20\n\n    old_page = writer.pages[0]\n    page = writer.insert_blank_page(height=-20, index=0)\n    assert len(writer.pages) == old_page_count + 5\n    assert page.mediabox.width == old_page.mediabox.width\n    assert page.mediabox.height == old_page.mediabox.height\n\n    page = writer.insert_blank_page(width=30, height=40, index=0)\n    assert len(writer.pages) == old_page_count + 6\n    assert page.mediabox.width == 30\n    assert page.mediabox.height == 40\n\n    old_page = writer.pages[0]\n    page = writer.insert_blank_page(width=-30, height=-40, index=0)\n    assert len(writer.pages) == old_page_count + 7\n    assert page.mediabox.width == old_page.mediabox.width\n    assert page.mediabox.height == old_page.mediabox.height\n\n    page = writer.insert_blank_page(width=50, height=60, index=len(writer.pages))\n    assert len(writer.pages) == old_page_count + 8\n    assert page.mediabox.width == 50\n    assert page.mediabox.height == 60\n\n    old_page = writer.pages[0]\n    page = writer.insert_blank_page(width=-50, height=-60, index=-len(writer.pages))\n    assert len(writer.pages) == old_page_count + 9\n    assert page.mediabox.width == old_page.mediabox.width\n    assert page.mediabox.height == old_page.mediabox.height\n\n    page = writer.insert_blank_page(width=70, height=80, index=len(writer.pages) // 2)\n    assert len(writer.pages) == old_page_count + 10\n    assert page.mediabox.width == 70\n    assert page.mediabox.height == 80\n\n    page = writer.insert_blank_page(width=70, height=80, index=-len(writer.pages) // 2)\n    assert len(writer.pages) == old_page_count + 11\n    assert page.mediabox.width == 70\n    assert page.mediabox.height == 80\n\n    num_pages = len(writer.pages)\n\n    with pytest.raises(\n        IndexError,\n        match=re.escape(f\"Index should be in range [-{num_pages}, {num_pages}]\"),\n    ):\n        page = writer.insert_blank_page(width=90, height=100, index=len(writer.pages) + 1)\n\n    with pytest.raises(\n        IndexError,\n        match=re.escape(f\"Index should be in range [-{num_pages}, {num_pages}]\"),\n    ):\n        page = writer.insert_blank_page(width=-90, height=-100, index=-len(writer.pages) - 1)\n\n\n@pytest.mark.parametrize(\n    (\"convert\", \"needs_cleanup\"),\n    [\n        (str, True),\n        (Path, True),\n        (BytesIO(), False),\n    ],\n)\ndef test_writer_operations_by_traditional_usage(convert, needs_cleanup):\n    write_data_here = _get_write_target(convert)\n    writer = PdfWriter()\n    writer_operate(writer)\n\n    # finally, write \"output\" to pypdf-output.pdf\n    if needs_cleanup:\n        with open(write_data_here, \"wb\") as output_stream:\n            writer.write(output_stream)\n    else:\n        output_stream = write_data_here\n        writer.write(output_stream)\n\n    if needs_cleanup:\n        Path(write_data_here).unlink()\n\n\n@pytest.mark.parametrize(\n    (\"convert\", \"needs_cleanup\"),\n    [\n        (str, True),\n        (Path, True),\n        (BytesIO(), False),\n    ],\n)\ndef test_writer_operations_by_semi_traditional_usage(convert, needs_cleanup):\n    write_data_here = _get_write_target(convert)\n\n    with PdfWriter() as writer:\n        writer_operate(writer)\n\n        # finally, write \"output\" to pypdf-output.pdf\n        if needs_cleanup:\n            with open(write_data_here, \"wb\") as output_stream:\n                writer.write(output_stream)\n        else:\n            output_stream = write_data_here\n            writer.write(output_stream)\n\n    if needs_cleanup:\n        Path(write_data_here).unlink()\n\n\n@pytest.mark.parametrize(\n    (\"convert\", \"needs_cleanup\"),\n    [\n        (str, True),\n        (Path, True),\n        (BytesIO(), False),\n    ],\n)\ndef test_writer_operations_by_semi_new_traditional_usage(convert, needs_cleanup):\n    write_data_here = _get_write_target(convert)\n\n    with PdfWriter() as writer:\n        writer_operate(writer)\n\n        # finally, write \"output\" to pypdf-output.pdf\n        writer.write(write_data_here)\n\n    if needs_cleanup:\n        Path(write_data_here).unlink()\n\n\n@pytest.mark.parametrize(\n    (\"convert\", \"needs_cleanup\"),\n    [\n        (str, True),\n        (Path, True),\n        (BytesIO(), False),\n    ],\n)\ndef test_writer_operation_by_new_usage(convert, needs_cleanup):\n    write_data_here = _get_write_target(convert)\n\n    # This includes write \"output\" to pypdf-output.pdf\n    with PdfWriter(write_data_here) as writer:\n        writer_operate(writer)\n\n    if needs_cleanup:\n        Path(write_data_here).unlink()\n\n\n@pytest.mark.parametrize(\n    \"input_path\",\n    [\n        \"side-by-side-subfig.pdf\",\n        \"reportlab-inline-image.pdf\",\n    ],\n)\ndef test_remove_images(pdf_file_path, input_path):\n    pdf_path = RESOURCE_ROOT / input_path\n\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter()\n\n    page = reader.pages[0]\n    writer.insert_page(page, 0)\n    writer.remove_images()\n    page_contents_stream = writer.pages[0][\"/Contents\"]._data\n    assert len(page_contents_stream.strip())\n\n    # finally, write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n    with open(pdf_file_path, \"rb\") as input_stream:\n        reader = PdfReader(input_stream)\n        if input_path == \"side-by-side-subfig.pdf\":\n            extracted_text = reader.pages[0].extract_text()\n            assert extracted_text\n            assert \"Lorem ipsum dolor sit amet\" in extracted_text\n\n\n@pytest.mark.enable_socket\ndef test_remove_images_sub_level():\n    \"\"\"Cf #2035\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12394781/2210.03142-1.pdf\"\n    name = \"iss2103.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n    writer.remove_images()\n    assert (\n        len(\n            [\n                o.get_object()\n                for o in writer.pages[0][\"/Resources\"][\"/XObject\"][\"/Fm1\"][\n                    \"/Resources\"\n                ][\"/XObject\"][\"/Im1\"][\"/Resources\"][\"/XObject\"].values()\n                if not isinstance(o.get_object(), NullObject)\n            ]\n        )\n        == 0\n    )\n\n\n@pytest.mark.parametrize(\n    \"input_path\",\n    [\n        \"side-by-side-subfig.pdf\",\n        \"reportlab-inline-image.pdf\",\n    ],\n)\ndef test_remove_text(input_path, pdf_file_path):\n    pdf_path = RESOURCE_ROOT / input_path\n\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter()\n\n    page = reader.pages[0]\n    writer.insert_page(page, 0)\n    writer.remove_text()\n\n    # finally, write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n\ndef test_remove_text_all_operators(pdf_file_path):\n    stream = (\n        b\"BT \"\n        b\"/F0 36 Tf \"\n        b\"50 706 Td \"\n        b\"36 TL \"\n        b\"(The Tj operator) Tj \"\n        b'1 2 (The double quote operator) \" '\n        b\"(The single quote operator) ' \"\n        b\"ET\"\n    )\n    pdf_data = (\n        b\"%%PDF-1.7\\n\"\n        b\"1 0 obj << /Count 1 /Kids [5 0 R] /Type /Pages >> endobj\\n\"\n        b\"2 0 obj << >> endobj\\n\"\n        b\"3 0 obj << >> endobj\\n\"\n        b\"4 0 obj << /Length %d >>\\n\"\n        b\"stream\\n\" + (b\"%s\\n\" % stream) + b\"endstream\\n\"\n        b\"endobj\\n\"\n        b\"5 0 obj << /Contents 4 0 R /CropBox [0.0 0.0 2550.0 3508.0]\\n\"\n        b\" /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R\"\n        b\" /Resources << /Font << >> >>\"\n        b\" /Rotate 0 /Type /Page >> endobj\\n\"\n        b\"6 0 obj << /Pages 1 0 R /Type /Catalog >> endobj\\n\"\n        b\"xref 1 6\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"%010d 00000 n\\n\"\n        b\"trailer << /Root 6 0 R /Size 6 >>\\n\"\n        b\"startxref\\n%d\\n\"\n        b\"%%%%EOF\"\n    )\n    startx_correction = -1\n    pdf_data = pdf_data % (\n        len(stream),\n        pdf_data.find(b\"1 0 obj\") + startx_correction,\n        pdf_data.find(b\"2 0 obj\") + startx_correction,\n        pdf_data.find(b\"3 0 obj\") + startx_correction,\n        pdf_data.find(b\"4 0 obj\") + startx_correction,\n        pdf_data.find(b\"5 0 obj\") + startx_correction,\n        pdf_data.find(b\"6 0 obj\") + startx_correction,\n        # startx_correction should be -1 due to double % at the beginning\n        # inducing an error on startxref computation\n        pdf_data.find(b\"xref\"),\n    )\n    pdf_stream = BytesIO(pdf_data)\n\n    reader = PdfReader(pdf_stream, strict=False)\n    writer = PdfWriter()\n\n    page = reader.pages[0]\n    writer.insert_page(page, 0)\n    writer.remove_text()\n\n    # finally, write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n\ndef test_write_metadata(pdf_file_path):\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter()\n\n    writer.add_page(reader.pages[0])\n    for page in reader.pages:\n        writer.add_page(page)\n\n    metadata = reader.metadata\n    writer.add_metadata(metadata)\n\n    writer.add_metadata({\"/Title\": \"The Crazy Ones\"})\n\n    # finally, write data to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n    # Check if the title was set\n    reader = PdfReader(pdf_file_path)\n    metadata = reader.metadata\n    assert metadata.get(\"/Title\") == \"The Crazy Ones\"\n\n\ndef test_fill_form(pdf_file_path):\n    reader = PdfReader(RESOURCE_ROOT / \"form.pdf\")\n    writer = PdfWriter()\n\n    writer.append(reader, [0])\n    writer.append(RESOURCE_ROOT / \"crazyones.pdf\", [0])\n\n    writer.update_page_form_field_values(\n        writer.pages[0], {\"foo\": \"some filled in text\"}, flags=1, flatten=True\n    )\n\n    # check if no fields to fill in the page\n    writer.update_page_form_field_values(\n        writer.pages[1], {\"foo\": \"some filled in text\"}, flags=1, flatten=True\n    )\n\n    writer.update_page_form_field_values(\n        writer.pages[0], {\"foo\": \"some filled in text\"}\n    )\n\n    # write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n\ndef test_fill_form_with_qualified():\n    reader = PdfReader(RESOURCE_ROOT / \"form.pdf\")\n    reader.add_form_topname(\"top\")\n\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    writer.add_page(reader.pages[0])\n    writer.update_page_form_field_values(\n        writer.pages[0], {\"top.foo\": \"filling\"}, flags=1\n    )\n    b = BytesIO()\n    writer.write(b)\n\n    reader2 = PdfReader(b)\n    fields = reader2.get_fields()\n    assert fields[\"top.foo\"][\"/V\"] == \"filling\"\n\n\n@pytest.mark.parametrize(\n    (\"use_128bit\", \"user_password\", \"owner_password\"),\n    [(True, \"userpwd\", \"ownerpwd\"), (False, \"userpwd\", \"ownerpwd\")],\n)\ndef test_encrypt(use_128bit, user_password, owner_password, pdf_file_path):\n    reader = PdfReader(RESOURCE_ROOT / \"form.pdf\")\n    writer = PdfWriter()\n\n    page = reader.pages[0]\n    orig_text = page.extract_text()\n\n    writer.add_page(page)\n\n    writer.encrypt(\n        owner_password=owner_password,\n        user_password=user_password,\n        use_128bit=use_128bit,\n    )\n    writer.encrypt(\n        user_password=user_password,\n        owner_password=owner_password,\n        use_128bit=use_128bit,\n    )\n\n    # write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n    # Test that the data is not there in clear text\n    with open(pdf_file_path, \"rb\") as input_stream:\n        data = input_stream.read()\n    assert b\"foo\" not in data\n\n    # Test the user password (str):\n    reader = PdfReader(pdf_file_path, password=\"userpwd\")\n    new_text = reader.pages[0].extract_text()\n    assert reader.metadata.get(\"/Producer\") == \"pypdf\"\n    assert new_text == orig_text\n\n    # Test the owner password (str):\n    reader = PdfReader(pdf_file_path, password=\"ownerpwd\")\n    new_text = reader.pages[0].extract_text()\n    assert reader.metadata.get(\"/Producer\") == \"pypdf\"\n    assert new_text == orig_text\n\n    # Test the user password (bytes):\n    reader = PdfReader(pdf_file_path, password=b\"userpwd\")\n    new_text = reader.pages[0].extract_text()\n    assert reader.metadata.get(\"/Producer\") == \"pypdf\"\n    assert new_text == orig_text\n\n    # Test the owner password (bytes):\n    reader = PdfReader(pdf_file_path, password=b\"ownerpwd\")\n    new_text = reader.pages[0].extract_text()\n    assert reader.metadata.get(\"/Producer\") == \"pypdf\"\n    assert new_text == orig_text\n\n\ndef test_add_outline_item(pdf_file_path):\n    reader = PdfReader(RESOURCE_ROOT / \"pdflatex-outline.pdf\")\n    writer = PdfWriter()\n\n    for page in reader.pages:\n        writer.add_page(page)\n\n    outline_item = writer.add_outline_item(\n        \"An outline item\",\n        1,\n        None,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit(),\n        is_open=False,\n    )\n    _o2a = writer.add_outline_item(\n        \"Another\", 2, outline_item, None, False, False, Fit.fit()\n    )\n    _o2b = writer.add_outline_item(\n        \"Another bis\", 2, outline_item, None, False, False, Fit.fit()\n    )\n    outline_item2 = writer.add_outline_item(\n        \"An outline item 2\",\n        1,\n        None,\n        (255, 0, 15),\n        True,\n        True,\n        Fit.fit(),\n        is_open=True,\n    )\n    _o3a = writer.add_outline_item(\n        \"Another 2\", 2, outline_item2, None, False, False, Fit.fit()\n    )\n    _o3b = writer.add_outline_item(\n        \"Another 2bis\", 2, outline_item2, None, False, False, Fit.fit()\n    )\n\n    # write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"w+b\") as output_stream:\n        writer.write(output_stream)\n        output_stream.seek(0)\n        reader = PdfReader(output_stream)\n        assert reader.trailer[\"/Root\"][\"/Outlines\"][\"/Count\"] == 3\n        assert reader.outline[0][\"/Count\"] == -2\n        assert reader.outline[0][\"/%is_open%\"] == False  # noqa: E712\n        assert reader.outline[2][\"/Count\"] == 2\n        assert reader.outline[2][\"/%is_open%\"] == True  # noqa: E712\n        assert reader.outline[1][0][\"/Count\"] == 0\n\n\ndef test_add_named_destination(pdf_file_path):\n    reader = PdfReader(RESOURCE_ROOT / \"pdflatex-outline.pdf\")\n    writer = PdfWriter()\n    assert writer.get_named_dest_root() == []\n\n    for page in reader.pages:\n        writer.add_page(page)\n\n    assert writer.get_named_dest_root() == []\n\n    writer.add_named_destination(TextStringObject(\"A named dest\"), 2)\n    writer.add_named_destination(TextStringObject(\"A named dest2\"), 2)\n    writer.add_named_destination(TextStringObject(\"A named dest3\"), page_number=2)\n    writer.add_named_destination(TextStringObject(\"A named dest3\"), page_number=2)\n\n    root = writer.get_named_dest_root()\n    assert root[0] == \"A named dest\"\n    assert root[1].pdf == writer\n    assert root[1].get_object()[\"/S\"] == NameObject(\"/GoTo\")\n    assert root[1].get_object()[\"/D\"][0] == writer.pages[2].indirect_reference\n    assert root[2] == \"A named dest2\"\n    assert root[3].pdf == writer\n    assert root[3].get_object()[\"/S\"] == NameObject(\"/GoTo\")\n    assert root[3].get_object()[\"/D\"][0] == writer.pages[2].indirect_reference\n    assert root[4] == \"A named dest3\"\n\n    # test get_object\n    assert writer.get_object(root[1].idnum) == writer.get_object(root[1])\n    with pytest.raises(ValueError) as exc:\n        writer.get_object(reader.pages[0].indirect_reference)\n    assert exc.value.args[0] == \"PDF must be self\"\n\n    # write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n\ndef test_add_named_destination_sort_order(pdf_file_path):\n    \"\"\"\n    Issue #1927 does not appear.\n\n    add_named_destination() maintains the named destination list sort order\n    \"\"\"\n    writer = PdfWriter()\n\n    assert writer.get_named_dest_root() == []\n\n    writer.add_blank_page(200, 200)\n    writer.add_named_destination(\"b\", 0)\n    # \"a\" should be moved before \"b\" on insert\n    writer.add_named_destination(\"a\", 0)\n\n    root = writer.get_named_dest_root()\n\n    assert len(root) == 4\n    assert (\n        root[0] == \"a\"\n    ), '\"a\" was not inserted before \"b\" in the named destination root'\n    assert root[2] == \"b\"\n\n    # write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n\ndef test_add_uri(pdf_file_path):\n    reader = PdfReader(RESOURCE_ROOT / \"pdflatex-outline.pdf\")\n    writer = PdfWriter()\n\n    for page in reader.pages:\n        writer.add_page(page)\n\n    writer.add_uri(\n        1,\n        \"http://www.example.com\",\n        RectangleObject([0, 0, 100, 100]),\n        border=[1, 2, 3, [4]],\n    )\n    writer.add_uri(\n        2,\n        \"https://pypdf.readthedocs.io/en/latest/\",\n        RectangleObject([20, 30, 50, 80]),\n        border=[1, 2, 3],\n    )\n    writer.add_uri(\n        3,\n        \"https://pypdf.readthedocs.io/en/latest/user/adding-pdf-annotations.html\",\n        \"[ 200 300 250 350 ]\",\n        border=[0, 0, 0],\n    )\n    writer.add_uri(\n        3,\n        \"https://pypdf.readthedocs.io/en/latest/user/adding-pdf-annotations.html\",\n        [100, 200, 150, 250],\n        border=[0, 0, 0],\n    )\n\n    # write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n\ndef test_link_annotation(pdf_file_path):\n    reader = PdfReader(RESOURCE_ROOT / \"pdflatex-outline.pdf\")\n    writer = PdfWriter()\n\n    for page in reader.pages:\n        writer.add_page(page)\n\n    writer.add_annotation(\n        page_number=1,\n        annotation=Link(\n            target_page_index=2,\n            rect=RectangleObject(\n                [0, 0, 100, 100],\n            ),\n            border=[1, 2, 3, [4]],\n            fit=Fit.fit(),\n        ),\n    )\n    writer.add_annotation(\n        page_number=2,\n        annotation=Link(\n            target_page_index=3,\n            rect=RectangleObject(\n                [0, 0, 100, 100],\n            ),\n            border=[1, 2, 3],\n            fit=Fit.fit_horizontally(),\n        ),\n    )\n    writer.add_annotation(\n        page_number=3,\n        annotation=Link(\n            target_page_index=0,\n            rect=RectangleObject(\n                [200, 300, 250, 350],\n            ),\n            border=[0, 0, 0],\n            fit=Fit.xyz(left=0, top=0, zoom=2),\n        ),\n    )\n    writer.add_annotation(\n        page_number=3,\n        annotation=Link(\n            target_page_index=0,\n            rect=RectangleObject([100, 200, 150, 250]),\n            border=[0, 0, 0],\n        ),\n    )\n\n    # write \"output\" to pypdf-output.pdf\n    with open(pdf_file_path, \"wb\") as output_stream:\n        writer.write(output_stream)\n\n\ndef test_io_streams():\n    \"\"\"This is the example from the docs (\"Streaming data\").\"\"\"\n    filepath = RESOURCE_ROOT / \"pdflatex-outline.pdf\"\n    with open(filepath, \"rb\") as fh:\n        bytes_stream = BytesIO(fh.read())\n\n    # Read from bytes stream\n    reader = PdfReader(bytes_stream)\n    assert len(reader.pages) == 4\n\n    # Write to bytes stream\n    writer = PdfWriter()\n    with BytesIO() as output_stream:\n        writer.write(output_stream)\n\n\ndef test_regression_issue670(pdf_file_path):\n    filepath = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(filepath, strict=False)\n    for _ in range(2):\n        writer = PdfWriter()\n        writer.add_page(reader.pages[0])\n        with open(pdf_file_path, \"wb\") as f_pdf:\n            writer.write(f_pdf)\n\n\ndef test_issue301():\n    \"\"\"Test with invalid stream length object.\"\"\"\n    with open(RESOURCE_ROOT / \"issue-301.pdf\", \"rb\") as f:\n        reader = PdfReader(f)\n        writer = PdfWriter()\n        writer.append_pages_from_reader(reader)\n        b = BytesIO()\n        writer.write(b)\n\n\ndef test_append_pages_from_reader_append():\n    \"\"\"Use append_pages_from_reader with a callable.\"\"\"\n    with open(RESOURCE_ROOT / \"issue-301.pdf\", \"rb\") as f:\n        reader = PdfReader(f)\n        writer = PdfWriter()\n        writer.append_pages_from_reader(reader, callable)\n        b = BytesIO()\n        writer.write(b)\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_sweep_indirect_references_nullobject_exception(pdf_file_path):\n    # TODO: Check this more closely... this looks weird\n    url = \"https://github.com/user-attachments/files/18381699/tika-924666.pdf\"\n    name = \"tika-924666.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n\n\n@pytest.mark.enable_socket\n@pytest.mark.slow\n@pytest.mark.parametrize(\n    (\"url\", \"name\"),\n    [\n        (\n            \"https://github.com/user-attachments/files/18381699/tika-924666.pdf\",\n            \"test_sweep_indirect_references_nullobject_exception.pdf\",\n        ),\n        (\n            \"https://github.com/user-attachments/files/18381694/tika-922840.pdf\",\n            \"test_write_outline_item_on_page_fitv.pdf\",\n        ),\n        (\"https://github.com/py-pdf/pypdf/files/10715624/test.pdf\", \"iss1627.pdf\"),\n    ],\n)\n@pytest.mark.filterwarnings(\"ignore::DeprecationWarning\")\ndef test_some_appends(pdf_file_path, url, name):\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    merger = PdfWriter()\n    merger.append(reader)\n    merger.write(pdf_file_path)\n\n\ndef test_pdf_header():\n    writer = PdfWriter()\n    assert writer.pdf_header == \"%PDF-1.3\"\n\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    writer.add_page(reader.pages[0])\n    assert writer.pdf_header == \"%PDF-1.5\"\n\n    writer.pdf_header = b\"%PDF-1.6\"\n    assert writer.pdf_header == \"%PDF-1.6\"\n\n\ndef test_write_dict_stream_object(pdf_file_path):\n    stream = (\n        b\"BT \"\n        b\"/F0 36 Tf \"\n        b\"50 706 Td \"\n        b\"36 TL \"\n        b\"(The Tj operator) Tj \"\n        b'1 2 (The double quote operator) \" '\n        b\"(The single quote operator) ' \"\n        b\"ET\"\n    )\n\n    stream_object = StreamObject()\n    stream_object[NameObject(\"/Type\")] = NameObject(\"/Text\")\n    stream_object._data = stream\n\n    writer = PdfWriter()\n\n    page_object = PageObject.create_blank_page(writer, 1000, 1000)\n    # Construct dictionary object (PageObject) with stream object\n    # Writer will replace this stream object with indirect object\n    page_object[NameObject(\"/Test\")] = stream_object\n\n    page_object = writer.add_page(page_object)\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n    for k, v in page_object.items():\n        if k == \"/Test\":\n            assert repr(v) != repr(stream_object)\n            assert isinstance(v, IndirectObject)\n            assert str(v) == str(stream_object)  # expansion of IndirectObjects\n            assert str(v.get_object()) == str(stream_object)\n            break\n    else:\n        pytest.fail(\"/Test not found\")\n\n    # Check that every key in _idnum_hash is correct\n    objects_hash = [o.hash_value() for o in writer._objects]\n    for k, v in writer._idnum_hash.items():\n        assert v.pdf == writer\n        assert k in objects_hash, f\"Missing {v}\"\n\n\ndef test_add_single_annotation(pdf_file_path):\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    page = reader.pages[0]\n    writer = PdfWriter()\n    writer.add_page(page)\n\n    annot_dict = {\n        \"/Type\": \"/Annot\",\n        \"/Subtype\": \"/Text\",\n        \"/Rect\": [270.75, 596.25, 294.75, 620.25],\n        \"/Contents\": \"Note in second paragraph\",\n        \"/C\": [1, 1, 0],\n        \"/M\": \"D:20220406191858+02'00\",\n        \"/Popup\": {\n            \"/Type\": \"/Annot\",\n            \"/Subtype\": \"/Popup\",\n            \"/Rect\": [294.75, 446.25, 494.75, 596.25],\n            \"/M\": \"D:20220406191847+02'00\",\n        },\n        \"/T\": \"moose\",\n    }\n    writer.add_annotation(0, annot_dict)\n\n    # Inspect manually by adding 'assert False' and viewing the PDF\n    with open(pdf_file_path, \"wb\") as fp:\n        writer.write(fp)\n\n\n@pytest.mark.samples\ndef test_colors_in_outline_item(pdf_file_path):\n    reader = PdfReader(SAMPLE_ROOT / \"004-pdflatex-4-pages/pdflatex-4-pages.pdf\")\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    purple_rgb = (0.5019607843137255, 0.0, 0.5019607843137255)\n    writer.add_outline_item(\"First Outline Item\", page_number=2, color=\"800080\")\n    writer.add_outline_item(\"Second Outline Item\", page_number=3, color=\"#800080\")\n    writer.add_outline_item(\"Third Outline Item\", page_number=4, color=purple_rgb)\n\n    with open(pdf_file_path, \"wb\") as f:\n        writer.write(f)\n\n    reader2 = PdfReader(pdf_file_path)\n    for outline_item in reader2.outline:\n        # convert float to string because of mutability\n        assert [f\"{c:.5f}\" for c in outline_item.color] == [\n            f\"{p:.5f}\" for p in purple_rgb\n        ]\n\n\n@pytest.mark.samples\ndef test_write_empty_stream():\n    reader = PdfReader(SAMPLE_ROOT / \"004-pdflatex-4-pages/pdflatex-4-pages.pdf\")\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n\n    with pytest.raises(ValueError) as exc:\n        writer.write(\"\")\n    assert exc.value.args[0] == \"Output(stream='') is empty.\"\n\n\ndef test_startup_dest():\n    pdf_file_writer = PdfWriter()\n    pdf_file_writer.append_pages_from_reader(PdfReader(RESOURCE_ROOT / \"issue-604.pdf\"))\n\n    assert pdf_file_writer.open_destination is None\n    pdf_file_writer.open_destination = pdf_file_writer.pages[9]\n    # checked also using Acrobrat to verify the good page is opened\n    op = pdf_file_writer.root_object[\"/OpenAction\"]\n    assert op[0] == pdf_file_writer.pages[9].indirect_reference\n    assert op[1] == \"/Fit\"\n    op = pdf_file_writer.open_destination\n    assert op.raw_get(\"/Page\") == pdf_file_writer.pages[9].indirect_reference\n    assert op[\"/Type\"] == \"/Fit\"\n    pdf_file_writer.open_destination = op\n    assert pdf_file_writer.open_destination == op\n\n    # irrelevant, just for coverage\n    pdf_file_writer.root_object[NameObject(\"/OpenAction\")][0] = NumberObject(0)\n    pdf_file_writer.open_destination\n    with pytest.raises(Exception) as exc:\n        del pdf_file_writer.root_object[NameObject(\"/OpenAction\")][0]\n        pdf_file_writer.open_destination\n    assert \"Invalid Destination\" in str(exc.value)\n\n    pdf_file_writer.open_destination = \"Test\"\n    # checked also using Acrobrat to verify open_destination\n    op = pdf_file_writer.root_object[\"/OpenAction\"]\n    assert isinstance(op, TextStringObject)\n    assert op == \"Test\"\n    op = pdf_file_writer.open_destination\n    assert isinstance(op, TextStringObject)\n    assert op == \"Test\"\n\n    # irrelevant, this is just for coverage\n    pdf_file_writer.root_object[NameObject(\"/OpenAction\")] = NumberObject(0)\n    assert pdf_file_writer.open_destination is None\n    pdf_file_writer.open_destination = None\n    assert \"/OpenAction\" not in pdf_file_writer.root_object\n    pdf_file_writer.open_destination = None\n\n\n@pytest.mark.enable_socket\ndef test_iss471():\n    url = \"https://github.com/py-pdf/pypdf/files/9139245/book.pdf\"\n    name = \"book_471.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    writer = PdfWriter()\n    writer.append(reader, excluded_fields=[])\n    assert isinstance(\n        writer.pages[0][\"/Annots\"][0].get_object()[\"/Dest\"], TextStringObject\n    )\n\n\n@pytest.mark.enable_socket\ndef test_reset_translation():\n    url = \"https://github.com/user-attachments/files/18381699/tika-924666.pdf\"\n    name = \"tika-924666.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader, (0, 10))\n    nb = len(writer._objects)\n    writer.append(reader, (0, 10))\n    assert (\n        len(writer._objects) == nb + 11\n    )  # +10 (pages) +1 because of the added outline\n    nb += 1\n    writer.reset_translation(reader)\n    writer.append(reader, (0, 10))\n    assert len(writer._objects) >= nb + 200\n    nb = len(writer._objects)\n    writer.reset_translation(reader.pages[0].indirect_reference)\n    writer.append(reader, (0, 10))\n    assert len(writer._objects) >= nb + 200\n    nb = len(writer._objects)\n    writer.reset_translation()\n    writer.append(reader, (0, 10))\n    assert len(writer._objects) >= nb + 200\n    nb = len(writer.pages)\n    writer.append(reader, [reader.pages[0], reader.pages[0]])\n    assert len(writer.pages) == nb + 2\n\n\ndef test_threads_empty():\n    writer = PdfWriter()\n    thr = writer.threads\n    assert isinstance(thr, ArrayObject)\n    assert len(thr) == 0\n    thr2 = writer.threads\n    assert thr == thr2\n\n\n@pytest.mark.enable_socket\ndef test_append_without_annots_and_articles():\n    url = \"https://github.com/user-attachments/files/18381699/tika-924666.pdf\"\n    name = \"tika-924666.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader, None, (0, 10), True, [\"/B\"])\n    writer.reset_translation()\n    writer.append(reader, (0, 10), True, [\"/B\"])\n    assert writer.threads == []\n    writer = PdfWriter()\n    writer.append(reader, None, (0, 10), True, [\"/Annots\"])\n    assert \"/Annots\" not in writer.pages[5]\n    writer = PdfWriter()\n    writer.append(reader, None, (0, 10), True, [])\n    assert \"/Annots\" in writer.pages[5]\n    assert len(writer.threads) >= 1\n\n\n@pytest.mark.enable_socket\ndef test_append_multiple():\n    url = \"https://github.com/user-attachments/files/18381699/tika-924666.pdf\"\n    name = \"tika-924666.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(\n        reader, [0, 0, 0]\n    )  # to demonstre multiple insertion of same page at once\n    writer.append(reader, [0, 0, 0])  # second pack\n    pages = writer.root_object[\"/Pages\"][\"/Kids\"]\n    assert pages[0] not in pages[1:]  # page not repeated\n    assert pages[-1] not in pages[0:-1]  # page not repeated\n\n\n@pytest.mark.samples\ndef test_set_page_label(pdf_file_path):\n    src = RESOURCE_ROOT / \"GeoBase_NHNC1_Data_Model_UML_EN.pdf\"  # File without labels\n    reader = PdfReader(src)\n\n    expected = [\n        \"i\",\n        \"ii\",\n        \"1\",\n        \"2\",\n        \"A\",\n        \"B\",\n        \"1\",\n        \"2\",\n        \"3\",\n        \"4\",\n        \"A\",\n        \"i\",\n        \"I\",\n        \"II\",\n        \"1\",\n        \"2\",\n        \"3\",\n        \"I\",\n        \"II\",\n    ]\n\n    # Tests full length with labels assigned at first and last elements\n    # Tests different labels assigned to consecutive ranges\n    writer = PdfWriter(reader, full=True)\n    writer.set_page_label(0, 1, \"/r\")\n    writer.set_page_label(4, 5, \"/A\")\n    writer.set_page_label(10, 10, \"/A\")\n    writer.set_page_label(11, 11, \"/r\")\n    writer.set_page_label(12, 13, \"/R\")\n    writer.set_page_label(17, 18, \"/R\")\n    writer.write(pdf_file_path)\n    assert PdfReader(pdf_file_path).page_labels == expected\n\n    writer = PdfWriter()  # Same labels, different set order\n    writer.clone_document_from_reader(reader)\n    writer.set_page_label(17, 18, \"/R\")\n    writer.set_page_label(4, 5, \"/A\")\n    writer.set_page_label(10, 10, \"/A\")\n    writer.set_page_label(0, 1, \"/r\")\n    writer.set_page_label(12, 13, \"/R\")\n    writer.set_page_label(11, 11, \"/r\")\n    writer.write(pdf_file_path)\n    assert PdfReader(pdf_file_path).page_labels == expected\n\n    # Tests labels assigned only in the middle\n    # Tests label assigned to a range already containing labelled ranges\n    expected = [\"1\", \"2\", \"i\", \"ii\", \"iii\", \"iv\", \"v\", \"1\"]\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    writer.set_page_label(3, 4, \"/a\")\n    writer.set_page_label(5, 5, \"/A\")\n    writer.set_page_label(2, 6, \"/r\")\n    writer.write(pdf_file_path)\n    assert PdfReader(pdf_file_path).page_labels[: len(expected)] == expected\n\n    # Tests labels assigned inside a previously existing range\n    expected = [\"1\", \"2\", \"i\", \"a\", \"b\", \"A\", \"1\", \"1\", \"2\"]\n    # Ones repeat because user did not cover the entire original range\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    writer.set_page_label(2, 6, \"/r\")\n    writer.set_page_label(3, 4, \"/a\")\n    writer.set_page_label(5, 5, \"/A\")\n    writer.write(pdf_file_path)\n    assert PdfReader(pdf_file_path).page_labels[: len(expected)] == expected\n\n    # Tests invalid user input\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    with pytest.raises(\n        ValueError, match=\"At least one of style and prefix must be given\"\n    ):\n        writer.set_page_label(0, 5, start=2)\n    with pytest.raises(\n        ValueError, match=\"page_index_from must be greater or equal than 0\"\n    ):\n        writer.set_page_label(-1, 5, \"/r\")\n    with pytest.raises(\n        ValueError, match=\"page_index_to must be greater or equal than page_index_from\"\n    ):\n        writer.set_page_label(5, 0, \"/r\")\n    with pytest.raises(ValueError, match=\"page_index_to exceeds number of pages\"):\n        writer.set_page_label(0, 19, \"/r\")\n    with pytest.raises(\n        ValueError, match=\"If given, start must be greater or equal than one\"\n    ):\n        writer.set_page_label(0, 5, \"/r\", start=-1)\n\n    pdf_file_path.unlink()\n\n    src = (\n        SAMPLE_ROOT / \"009-pdflatex-geotopo/GeoTopo.pdf\"\n    )  # File with pre existing labels\n    reader = PdfReader(src)\n\n    # Tests adding labels to existing ones\n    expected = [\"i\", \"ii\", \"A\", \"B\", \"1\"]\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    writer.set_page_label(2, 3, \"/A\")\n    writer.write(pdf_file_path)\n    assert PdfReader(pdf_file_path).page_labels[: len(expected)] == expected\n\n    # Tests replacing existing labels\n    expected = [\"A\", \"B\", \"1\", \"1\", \"2\"]\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    writer.set_page_label(0, 1, \"/A\")\n    writer.write(pdf_file_path)\n    assert PdfReader(pdf_file_path).page_labels[: len(expected)] == expected\n\n    pdf_file_path.unlink()\n\n    # Tests prefix and start.\n    src = RESOURCE_ROOT / \"issue-604.pdf\"  # File without page labels\n    reader = PdfReader(src)\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n\n    writer.set_page_label(0, 0, prefix=\"FRONT\")\n    writer.set_page_label(1, 2, \"/D\", start=2)\n    writer.set_page_label(3, 6, prefix=\"UPDATES\")\n    writer.set_page_label(7, 10, \"/D\", prefix=\"THYR-\")\n    writer.set_page_label(11, 21, \"/D\", prefix=\"PAP-\")\n    writer.set_page_label(22, 30, \"/D\", prefix=\"FOLL-\")\n    writer.set_page_label(31, 39, \"/D\", prefix=\"HURT-\")\n    writer.write(pdf_file_path)\n\n\n@pytest.mark.enable_socket\ndef test_iss1601():\n    url = \"https://github.com/py-pdf/pypdf/files/10579503/badges-38.pdf\"\n    name = \"badge-38.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    original_cs_operations = ContentStream(\n        reader.pages[0].get_contents(), reader\n    ).operations\n    writer = PdfWriter()\n    page_1 = writer.add_blank_page(\n        reader.pages[0].mediabox[2], reader.pages[0].mediabox[3]\n    )\n    page_1.merge_transformed_page(reader.pages[0], Transformation())\n    page_1_cs_operations = page_1.get_contents().operations\n    assert is_sublist(original_cs_operations, page_1_cs_operations)\n    page_1 = writer.add_blank_page(\n        reader.pages[0].mediabox[2], reader.pages[0].mediabox[3]\n    )\n    page_1.merge_page(reader.pages[0])\n    page_1_cs_operations = page_1.get_contents().operations\n    assert is_sublist(original_cs_operations, page_1_cs_operations)\n\n\ndef test_attachments():\n    writer = PdfWriter()\n    writer.add_blank_page(100, 100)\n    b = BytesIO()\n    writer.write(b)\n    b.seek(0)\n    reader = PdfReader(b)\n    b = None\n    assert reader.attachments == {}\n    assert reader._list_attachments() == []\n    assert reader._get_attachments() == {}\n    to_add = [\n        (\"foobar.txt\", b\"foobarcontent\"),\n        (\"foobar2.txt\", b\"foobarcontent2\"),\n        (\"foobar2.txt\", \"2nd_foobarcontent\"),\n    ]\n    for name, content in to_add:\n        writer.add_attachment(name, content)\n\n    b = BytesIO()\n    writer.write(b)\n    b.seek(0)\n    reader = PdfReader(b)\n    b = None\n    assert sorted(reader.attachments.keys()) == sorted({name for name, _ in to_add})\n    assert str(reader.attachments) == \"LazyDict(keys=['foobar.txt', 'foobar2.txt'])\"\n    assert reader._list_attachments() == [name for name, _ in to_add]\n\n    # We've added the same key twice - hence only 2 and not 3:\n    att = reader._get_attachments()\n    assert len(att) == 2  # we have 2 keys, but 3 attachments!\n\n    # The content for foobar.txt is clear and just a single value:\n    assert att[\"foobar.txt\"] == b\"foobarcontent\"\n\n    # The content for foobar2.txt is a list!\n    att = reader._get_attachments(\"foobar2.txt\")\n    assert len(att) == 1\n    assert att[\"foobar2.txt\"] == [b\"foobarcontent2\", b\"2nd_foobarcontent\"]\n\n    # Let's do both cases with the public interface:\n    assert reader.attachments[\"foobar.txt\"][0] == b\"foobarcontent\"\n    assert reader.attachments[\"foobar2.txt\"][0] == b\"foobarcontent2\"\n    assert reader.attachments[\"foobar2.txt\"][1] == b\"2nd_foobarcontent\"\n\n\n@pytest.mark.enable_socket\ndef test_iss1614():\n    # test of an annotation(link) directly stored in the /Annots in the page\n    url = \"https://github.com/py-pdf/pypdf/files/10669995/broke.pdf\"\n    name = \"iss1614.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader)\n    # test for 2nd error case reported in #1614\n    url = \"https://github.com/py-pdf/pypdf/files/10696390/broken.pdf\"\n    name = \"iss1614.2.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer.append(reader)\n\n\n@pytest.mark.enable_socket\ndef test_new_removes():\n    # test of an annotation(link) directly stored in the /Annots in the page\n    url = \"https://github.com/py-pdf/pypdf/files/10807951/tt.pdf\"\n    name = \"iss1650.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    writer.remove_images()\n    b = BytesIO()\n    writer.write(b)\n    bb = bytes(b.getbuffer())\n    assert b\"/Im0 Do\" not in bb\n    assert b\"/Fm0 Do\" in bb\n    assert b\" TJ\" in bb\n\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    writer.remove_text()\n    b = BytesIO()\n    writer.write(b)\n    bb = bytes(b.getbuffer())\n    assert b\"/Im0\" in bb\n    assert b\"Chap\" not in bb\n    assert b\" TJ\" not in bb\n\n    # Test removing text in a specified font\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    b = BytesIO()\n    writer.write(b)\n    temp_reader = PdfReader(b)\n    text = temp_reader.pages[0].extract_text()\n    assert \"Arbeitsschritt\" in text\n    assert \"Modelltechnik\" in text\n    writer.remove_text(font_names=[\"LiberationSans-Bold\"])\n    b = BytesIO()\n    writer.write(b)\n    temp_reader = PdfReader(b)\n    text = temp_reader.pages[0].extract_text()\n    assert \"Arbeitsschritt\" not in text\n    assert \"Modelltechnik\" in text\n\n    # Test removing text in a specified font that doesn't exist (nothing should happen)\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    b = BytesIO()\n    writer.write(b)\n    temp_reader = PdfReader(b)\n    text = temp_reader.pages[0].extract_text()\n    assert \"Arbeitsschritt\" in text\n    assert \"Modelltechnik\" in text\n    writer.remove_text(font_names=[\"ComicSans-Oblique\"])\n    b = BytesIO()\n    writer.write(b)\n    temp_reader = PdfReader(b)\n    text = temp_reader.pages[0].extract_text()\n    assert \"Arbeitsschritt\" in text\n    assert \"Modelltechnik\" in text\n\n    url = \"https://github.com/py-pdf/pypdf/files/10832029/tt2.pdf\"\n    name = \"GeoBaseWithComments.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer.append(reader)\n    writer.remove_objects_from_page(writer.pages[0], [ObjectDeletionFlag.LINKS])\n    assert \"/Links\" not in [\n        a.get_object()[\"/Subtype\"] for a in writer.pages[0][\"/Annots\"]\n    ]\n    writer.remove_objects_from_page(writer.pages[0], ObjectDeletionFlag.ATTACHMENTS)\n    assert \"/FileAttachment\" not in [\n        a.get_object()[\"/Subtype\"] for a in writer.pages[0][\"/Annots\"]\n    ]\n\n    writer.pages[0][\"/Annots\"].append(\n        DictionaryObject({NameObject(\"/Subtype\"): TextStringObject(\"/3D\")})\n    )\n    assert \"/3D\" in [a.get_object()[\"/Subtype\"] for a in writer.pages[0][\"/Annots\"]]\n    writer.remove_objects_from_page(writer.pages[0], ObjectDeletionFlag.OBJECTS_3D)\n    assert \"/3D\" not in [a.get_object()[\"/Subtype\"] for a in writer.pages[0][\"/Annots\"]]\n\n    writer.remove_links()\n    assert len(writer.pages[0][\"/Annots\"]) == 0\n    assert len(writer.pages[3][\"/Annots\"]) == 0\n\n    writer.remove_annotations(\"/Text\")\n\n\n@pytest.mark.enable_socket\ndef test_late_iss1654():\n    url = \"https://github.com/py-pdf/pypdf/files/10935632/bid1.pdf\"\n    name = \"bid1.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.clone_document_from_reader(reader)\n    for p in writer.pages:\n        p.compress_content_streams()\n    b = BytesIO()\n    writer.write(b)\n\n\n@pytest.mark.enable_socket\ndef test_iss1723():\n    # test of an annotation(link) directly stored in the /Annots in the page\n    url = \"https://github.com/py-pdf/pypdf/files/11015242/inputFile.pdf\"\n    name = \"iss1723.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader, (3, 5))\n\n\n@pytest.mark.enable_socket\ndef test_iss1767():\n    # test with a pdf which is buggy because the object 389,0 exists 3 times:\n    # twice to define catalog and one as an XObject inducing a loop when\n    # cloning\n    url = \"https://github.com/py-pdf/pypdf/files/11138472/test.pdf\"\n    name = \"iss1767.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    PdfWriter(clone_from=reader)\n\n\n@pytest.mark.enable_socket\ndef test_named_dest_page_number():\n    \"\"\"\n    Closes iss471\n    tests appending with named destinations as integers\n    \"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/10704333/central.pdf\"\n    name = \"central.pdf\"\n    writer = PdfWriter()\n    writer.add_blank_page(100, 100)\n    writer.append(BytesIO(get_data_from_url(url, name=name)), pages=[0, 1, 2])\n    assert len(writer.root_object[\"/Names\"][\"/Dests\"][\"/Names\"]) == 2\n    assert writer.root_object[\"/Names\"][\"/Dests\"][\"/Names\"][-1][0] == (1 + 1)\n    writer.append(BytesIO(get_data_from_url(url, name=name)))\n    assert len(writer.root_object[\"/Names\"][\"/Dests\"][\"/Names\"]) == 6\n    writer2 = PdfWriter()\n    writer2.add_blank_page(100, 100)\n    dest = writer2.add_named_destination(\"toto\", 0)\n    dest.get_object()[NameObject(\"/D\")][0] = NullObject()\n    b = BytesIO()\n    writer2.write(b)\n    b.seek(0)\n    writer.append(b)\n    assert len(writer.root_object[\"/Names\"][\"/Dests\"][\"/Names\"]) == 6\n\n\ndef test_update_form_fields(caplog, tmp_path):\n    write_data_here = tmp_path / \"out.pdf\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"FormTestFromOo.pdf\")\n    writer.update_page_form_field_values(\n        writer.pages[0],\n        {\n            \"CheckBox1\": \"/Yes\",\n            \"Text1\": \"mon Text1\",\n            \"Text2\": \"ligne1\\nligne2\",\n            \"RadioGroup1\": \"/2\",\n            \"RdoS1\": \"/\",\n            \"Combo1\": \"!!monCombo!!\",\n            \"Liste1\": \"Liste2\",\n            \"Liste2\": [\"Lst1\", \"Lst3\"],\n            \"DropList1\": \"DropListe3\",\n        },\n        auto_regenerate=False,\n        flatten=True,\n    )\n    del writer.pages[0][\"/Annots\"][1].get_object()[\"/AP\"][\"/N\"]\n    del writer.pages[0][\"/Resources\"][\"/Font\"]\n    writer.update_page_form_field_values(\n        writer.pages[0],\n        {\"Text1\": \"my Text1\", \"Text2\": \"ligne1\\nligne2\\nligne3\"},\n        auto_regenerate=False,\n    )\n    writer.update_page_form_field_values(\n        writer.pages[0],\n        {\"Text1\": None, \"Text2\": None},\n        auto_regenerate=False,\n        flatten=True,\n    )\n\n    writer.write(write_data_here)\n    reader = PdfReader(write_data_here)\n    flds = reader.get_fields()\n    assert flds[\"CheckBox1\"][\"/V\"] == \"/Yes\"\n    assert flds[\"CheckBox1\"].indirect_reference.get_object()[\"/AS\"] == \"/Yes\"\n    assert (\n        b\"(my Text1)\"\n        in flds[\"Text1\"].indirect_reference.get_object()[\"/AP\"][\"/N\"].get_data()\n    )\n    assert flds[\"Text2\"][\"/V\"] == \"ligne1\\nligne2\\nligne3\"\n    assert (\n        b\"(ligne3)\"\n        in flds[\"Text2\"].indirect_reference.get_object()[\"/AP\"][\"/N\"].get_data()\n    )\n    assert flds[\"RadioGroup1\"][\"/V\"] == \"/2\"\n    assert flds[\"RadioGroup1\"][\"/Kids\"][0].get_object()[\"/AS\"] == \"/Off\"\n    assert flds[\"RadioGroup1\"][\"/Kids\"][1].get_object()[\"/AS\"] == \"/2\"\n    assert all(x in flds[\"Liste2\"][\"/V\"] for x in [\"Lst1\", \"Lst3\"])\n\n    assert all(x in flds[\"CheckBox1\"][\"/_States_\"] for x in [\"/Off\", \"/Yes\"])\n    assert all(x in flds[\"RadioGroup1\"][\"/_States_\"] for x in [\"/1\", \"/2\", \"/3\"])\n    assert all(x in flds[\"Liste1\"][\"/_States_\"] for x in [\"Liste1\", \"Liste2\", \"Liste3\"])\n\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"FormTestFromOo.pdf\")\n    writer.add_annotation(\n        page_number=0,\n        annotation=Link(target_page_index=1, rect=RectangleObject([0, 0, 100, 100])),\n    )\n    writer.insert_blank_page(100, 100, 0)\n    del writer.root_object[\"/AcroForm\"][\"/Fields\"][1].get_object()[\"/DA\"]\n    del writer.root_object[\"/AcroForm\"][\"/Fields\"][1].get_object()[\"/DR\"][\"/Font\"]\n    writer.update_page_form_field_values(\n        [writer.pages[0], writer.pages[1]],\n        {\"Text1\": \"!مرحبا بالعالم\", \"Text2\": \"ligne1\\nligne2\\nligne3\"},\n        auto_regenerate=False,\n    )\n    assert b\"/Helvetica \" in writer.pages[1][\"/Annots\"][1][\"/AP\"][\"/N\"].get_data()\n    assert \"Text string '!مرحبا بالعالم' contains characters not supported by font encoding.\" in caplog.text\n    writer.update_page_form_field_values(\n        None,\n        {\"Text1\": \"my Text1\", \"Text2\": \"ligne1\\nligne2\\nligne3\"},\n        auto_regenerate=False,\n        flatten=True\n    )\n\n    Path(write_data_here).unlink()\n\n\ndef test_add_apstream_object():\n    writer = PdfWriter()\n    page = writer.add_blank_page(1000, 1000)\n    assert NameObject(\"/Contents\") not in page\n    apstream_object = DecodedStreamObject.initialize_from_dictionary(\n        {\n            NameObject(\"/Type\"): NameObject(\"/XObject\"),\n            NameObject(\"/Subtype\"): NameObject(\"/Form\"),\n            NameObject(\"/BBox\"): RectangleObject([0.0, 0.0, 10.5, 10.5]),\n            \"__streamdata__\": ByteStringObject(b\"BT /F1 12 Tf (Hello World) Tj ET\")\n        }\n    )\n    writer._add_object(apstream_object)\n    object_name = \"AA2342!@#$% ^^##aa:-)\"\n    x_offset = 200\n    y_offset = 200\n    writer._add_apstream_object(page, apstream_object, object_name, x_offset, y_offset)\n    assert NameObject(\"/XObject\") in page[NameObject(\"/Resources\")]\n    assert \"/Fm_AA2342__________aa_-_\" in page[NameObject(\"/Resources\")][NameObject(\"/XObject\")]\n    assert NameObject(\"/Contents\") in page\n    contents_obj = page[NameObject(\"/Contents\")]\n    stream = contents_obj.get_object()\n    assert isinstance(stream, StreamObject)\n    assert stream.get_data() == (\n        b\"q\\n1.0000 0.0000 0.0000 1.0000 200.0000 200.0000 cm\\n/Fm_AA2342__________aa_-_ Do\\nQ\"\n    )\n\n\ndef test_merge_content_stream_to_page():\n    \"\"\"Test that new content data is correctly added to page contents\n    in the form of an ArrayObject or StreamObject. The\n    test_add_apstream_object code already correctly checks that\n    _merge_content_stream_to_page works for an emtpy page.\n    \"\"\"\n    writer = PdfWriter()\n    page = writer.add_blank_page(100, 100)\n    new_content = b\"BT /F1 12 Tf (Hello World) Tj ET\"\n    # Call the method under test\n    writer._merge_content_stream_to_page(page, new_content)\n    more_content = b\"BT /F1 12 Tf (Hello Again, World) Tj ET\"\n    writer._merge_content_stream_to_page(page, more_content)\n    contents_obj = page[NameObject(\"/Contents\")]\n    stream = contents_obj.get_object()\n    assert isinstance(stream, StreamObject)\n    assert stream.get_data() == b\"BT /F1 12 Tf (Hello World) Tj ET\\nBT /F1 12 Tf (Hello Again, World) Tj ET\"\n    new_stream_obj = StreamObject()\n    new_stream_obj.set_data(new_content)\n    content = ArrayObject()\n    content.append(new_stream_obj)\n    page[NameObject(\"/Contents\")] = writer._add_object(content)\n    writer._merge_content_stream_to_page(page, more_content)\n    contents_obj = page[NameObject(\"/Contents\")]\n    array = contents_obj.get_object()\n    assert isinstance(array, ArrayObject)\n    contents = page[NameObject(\"/Contents\")].get_object()\n    assert contents[0].get_object().get_data() == new_content\n    assert contents[1].get_object().get_data() == more_content\n\n\n@pytest.mark.enable_socket\ndef test_update_form_fields2(caplog):\n    my_files = {\n        \"test1\": {\n            \"name\": \"Test1 Form\",\n            \"url\": \"https://github.com/py-pdf/pypdf/files/14817365/test1.pdf\",\n            \"path\": \"iss2234a.pdf\",\n            \"usage\": {\n                \"fields\": {\n                    \"First Name\": \"Reed\",\n                    \"Middle Name\": \"R\",\n                    \"MM\": \"04\",\n                    \"DD\": \"21\",\n                    \"YY\": \"24\",\n                    \"Initial\": \"RRG\",\n                    # \"I DO NOT Agree\": null,\n                    # \"Last Name\": null\n                },\n            },\n        },\n        \"test2\": {\n            \"name\": \"Test2 Form\",\n            \"url\": \"https://github.com/py-pdf/pypdf/files/14817366/test2.pdf\",\n            \"path\": \"iss2234b.pdf\",\n            \"usage\": {\n                \"fields\": {\n                    \"p2 First Name\": \"Joe\",\n                    \"p2 Middle Name\": \"S\",\n                    \"p2 MM\": \"03\",\n                    \"p2 DD\": \"31\",\n                    \"p2 YY\": \"24\",\n                    \"Initial\": \"JSS\",\n                    # \"p2 I DO NOT Agree\": \"null\",\n                    \"p2 Last Name\": \"Smith\",\n                    \"p3 First Name\": \"شهرزاد\",\n                    \"p3 Middle Name\": \"R\",\n                    \"p3 MM\": \"01\",\n                    \"p3 DD\": \"25\",\n                    \"p3 YY\": \"21\",\n                },\n            },\n        },\n    }\n    merger = PdfWriter()\n\n    for file in my_files:\n        reader = PdfReader(\n            BytesIO(get_data_from_url(my_files[file][\"url\"], name=my_files[file][\"path\"]))\n        )\n        reader.add_form_topname(file)\n        writer = PdfWriter(clone_from=reader)\n\n        writer.update_page_form_field_values(\n            None, my_files[file][\"usage\"][\"fields\"], auto_regenerate=True\n        )\n        merger.append(writer)\n    assert merger.get_form_text_fields(True) == {\n        \"test1.First Name\": \"Reed\",\n        \"test1.Middle Name\": \"R\",\n        \"test1.MM\": \"04\",\n        \"test1.DD\": \"21\",\n        \"test1.YY\": \"24\",\n        \"test1.Initial\": \"RRG\",\n        \"test1.I DO NOT Agree\": None,\n        \"test1.Last Name\": None,\n        \"test2.p2 First Name\": \"Joe\",\n        \"test2.p2 Middle Name\": \"S\",\n        \"test2.p2 MM\": \"03\",\n        \"test2.p2 DD\": \"31\",\n        \"test2.p2 YY\": \"24\",\n        \"test2.Initial\": \"JSS\",\n        \"test2.p2 I DO NOT Agree\": None,\n        \"test2.p2 Last Name\": \"Smith\",\n        \"test2.p3 First Name\": \"شهرزاد\",\n        \"test2.p3 Middle Name\": \"R\",\n        \"test2.p3 MM\": \"01\",\n        \"test2.p3 DD\": \"25\",\n        \"test2.p3 YY\": \"21\",\n    }\n    assert \"Text string 'شهرزاد' contains characters not supported by font encoding.\" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_iss1862():\n    # The file here has \"/B\" entry to define the font in a object below the page\n    # The excluded field shall be considered only at first level (page) and not\n    # below\n    url = \"https://github.com/py-pdf/pypdf/files/11708801/intro.pdf\"\n    name = \"iss1862.pdf\"\n    writer = PdfWriter()\n    writer.append(BytesIO(get_data_from_url(url, name=name)))\n    # check that \"/B\" is in the font\n    writer.pages[0][\"/Resources\"][\"/Font\"][\"/F1\"][\"/CharProcs\"][\"/B\"].get_data()\n\n\ndef test_empty_objects_before_cloning():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter(clone_from=reader)\n    nb_obj_reader = len(reader.xref_objStm) + sum(\n        len(reader.xref[i]) for i in reader.xref\n    )\n    nb_obj_reader -= 1  # for trailer\n    nb_obj_reader -= len(\n        {x: 1 for x, y in reader.xref_objStm.values()}\n    )  # to remove object streams\n    assert len(writer._objects) == nb_obj_reader\n\n\n@pytest.mark.enable_socket\ndef test_watermark():\n    url = \"https://github.com/py-pdf/pypdf/files/11985889/bg.pdf\"\n    name = \"bgwatermark.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://github.com/py-pdf/pypdf/files/11985888/source.pdf\"\n    name = \"srcwatermark.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n    for p in writer.pages:\n        p.merge_page(reader.pages[0], over=False)\n\n    assert isinstance(p[\"/Contents\"], ArrayObject)\n    assert isinstance(p[\"/Contents\"][0], IndirectObject)\n\n    b = BytesIO()\n    writer.write(b)\n    assert len(b.getvalue()) < 2.1 * 1024 * 1024\n\n\n@pytest.mark.enable_socket\n@pytest.mark.timeout(4)\ndef test_watermarking_speed():\n    url = \"https://github.com/py-pdf/pypdf/files/11985889/bg.pdf\"\n    name = \"bgwatermark.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    url = \"https://arxiv.org/pdf/2201.00214.pdf\"\n    name = \"2201.00214.pdf\"\n    writer = PdfWriter(clone_from=BytesIO(get_data_from_url(url, name=name)))\n    for p in writer.pages:\n        p.merge_page(reader.pages[0], over=False)\n    out_pdf_bytesio = BytesIO()\n    writer.write(out_pdf_bytesio)\n    pdf_size_in_mib = len(out_pdf_bytesio.getvalue()) / 1024 / 1024\n    assert pdf_size_in_mib < 20\n\n\n@pytest.mark.enable_socket\n@pytest.mark.skipif(GHOSTSCRIPT_BINARY is None, reason=\"Requires Ghostscript\")\ndef test_watermark_rendering(tmp_path):\n    \"\"\"Ensure the visual appearance of watermarking stays correct.\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/11985889/bg.pdf\"\n    name = \"bgwatermark.pdf\"\n    watermark = PdfReader(BytesIO(get_data_from_url(url, name=name))).pages[0]\n    url = \"https://github.com/py-pdf/pypdf/files/11985888/source.pdf\"\n    name = \"srcwatermark.pdf\"\n    page = PdfReader(BytesIO(get_data_from_url(url, name=name))).pages[0]\n    writer = PdfWriter()\n    page = writer.add_page(page)\n    page.merge_page(watermark, over=False)\n\n    target_png_path = tmp_path / \"target.png\"\n    url = \"https://github.com/py-pdf/pypdf/assets/96178532/d5c72d0e-7047-4504-bbf6-bc591c80d7c0\"\n    name = \"dstwatermark.png\"\n    target_png_path.write_bytes(get_data_from_url(url, name=name))\n\n    pdf_path = tmp_path / \"out.pdf\"\n    png_path = tmp_path / \"out.png\"\n    writer.write(pdf_path)\n\n    # False positive: https://github.com/PyCQA/bandit/issues/333\n    subprocess.run(  # noqa: S603\n        [\n            GHOSTSCRIPT_BINARY,\n            \"-sDEVICE=pngalpha\",\n            \"-o\",\n            png_path,\n            pdf_path,\n        ]\n    )\n    assert png_path.is_file()\n    assert image_similarity(png_path, target_png_path) >= 0.95\n\n\n@pytest.mark.samples\n@pytest.mark.skipif(GHOSTSCRIPT_BINARY is None, reason=\"Requires Ghostscript\")\ndef test_watermarking_reportlab_rendering(tmp_path):\n    \"\"\"\n    This test is showing a rotated+mirrored watermark in pypdf==3.15.4.\n\n    Replacing the generate_base with e.g. the crazyones did not show the issue.\n    \"\"\"\n    base_path = SAMPLE_ROOT / \"022-pdfkit/pdfkit.pdf\"\n    watermark_path = SAMPLE_ROOT / \"013-reportlab-overlay/reportlab-overlay.pdf\"\n\n    reader = PdfReader(base_path)\n    base_page = reader.pages[0]\n    watermark = PdfReader(watermark_path).pages[0]\n\n    writer = PdfWriter()\n    base_page = writer.add_page(base_page)\n    base_page.merge_page(watermark)\n\n    target_png_path = RESOURCE_ROOT / \"test_watermarking_reportlab_rendering.png\"\n    pdf_path = tmp_path / \"out.pdf\"\n    png_path = tmp_path / \"test_watermarking_reportlab_rendering.png\"\n\n    writer.write(pdf_path)\n    # False positive: https://github.com/PyCQA/bandit/issues/333\n    subprocess.run(  # noqa: S603\n        [\n            GHOSTSCRIPT_BINARY,\n            \"-r120\",\n            \"-sDEVICE=pngalpha\",\n            \"-o\",\n            png_path,\n            pdf_path,\n        ]\n    )\n    assert png_path.is_file()\n    assert image_similarity(png_path, target_png_path) >= 0.999\n\n\n@pytest.mark.enable_socket\ndef test_da_missing_in_annot():\n    url = \"https://github.com/py-pdf/pypdf/files/12136285/Building.Division.Permit.Application.pdf\"\n    name = \"BuildingDivisionPermitApplication.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter(clone_from=reader)\n    writer.update_page_form_field_values(\n        writer.pages[0], {\"PCN-1\": \"0\"}, auto_regenerate=False\n    )\n    b = BytesIO()\n    writer.write(b)\n    reader = PdfReader(BytesIO(b.getvalue()))\n    ff = reader.get_fields()\n    # check for autosize processing\n    assert (\n        b\" 0 Tf\"\n        not in ff[\"PCN-1\"].indirect_reference.get_object()[\"/AP\"][\"/N\"].get_data()\n    )\n    f2 = writer.get_object(ff[\"PCN-2\"].indirect_reference.idnum)\n    f2[NameObject(\"/Parent\")] = writer.get_object(\n        ff[\"PCN-1\"].indirect_reference.idnum\n    ).indirect_reference\n    writer.update_page_form_field_values(\n        writer.pages[0], {\"PCN-2\": \"1\"}, auto_regenerate=False\n    )\n\n\ndef test_missing_fields(pdf_file_path):\n    reader = PdfReader(RESOURCE_ROOT / \"form.pdf\")\n\n    writer = PdfWriter()\n    writer.add_page(reader.pages[0])\n\n    with pytest.raises(PyPdfError) as exc:\n        writer.update_page_form_field_values(\n            writer.pages[0], {\"foo\": \"some filled in text\"}, flags=1\n        )\n    assert exc.value.args[0] == \"No /AcroForm dictionary in PDF of PdfWriter Object\"\n\n    writer = PdfWriter()\n    writer.append(reader, [0])\n    del writer.root_object[\"/AcroForm\"][\"/Fields\"]\n    with pytest.raises(PyPdfError) as exc:\n        writer.update_page_form_field_values(\n            writer.pages[0], {\"foo\": \"some filled in text\"}, flags=1\n        )\n    assert exc.value.args[0] == \"No /Fields dictionary in PDF of PdfWriter Object\"\n\n\ndef test_missing_info():\n    reader = PdfReader(RESOURCE_ROOT / \"missing_info.pdf\")\n\n    writer = PdfWriter(clone_from=reader)\n    assert len(writer.pages) == len(reader.pages)\n    assert writer.metadata is None\n    b = BytesIO()\n    writer.write(b)\n    assert b\"/Info\" not in b.getvalue()\n\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    writer.metadata = reader.metadata\n    assert dict(writer._info) == dict(reader._info)\n    assert writer.metadata == reader.metadata\n    b = BytesIO()\n    writer.write(b)\n    assert b\"/Info\" in b.getvalue()\n\n    writer.metadata = {}\n    writer._info = DictionaryObject()  # for code coverage\n    b = BytesIO()\n    writer.write(b)\n    assert b\"/Info\" in b.getvalue()\n    assert writer.metadata == {}\n\n    writer.metadata = None\n    writer.metadata = None  # for code coverage\n    assert writer.metadata is None\n    assert PdfWriter().metadata == {\"/Producer\": \"pypdf\"}\n    b = BytesIO()\n    writer.write(b)\n    assert b\"/Info\" not in b.getvalue()\n\n\n@pytest.mark.enable_socket\ndef test_germanfields():\n    \"\"\"Cf #2035\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12194195/test.pdf\"\n    name = \"germanfields.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter(clone_from=reader)\n    form_fields = {\"Text Box 1\": \"test æ ø å\"}\n    writer.update_page_form_field_values(\n        writer.pages[0], form_fields, auto_regenerate=False\n    )\n    bytes_stream = BytesIO()\n    writer.write(bytes_stream)\n    bytes_stream.seek(0)\n    reader2 = PdfReader(bytes_stream)\n    assert (\n        b\"test \\xe6 \\xf8 \\xe5\"\n        in reader2.get_fields()[\"Text Box 1\"]\n        .indirect_reference.get_object()[\"/AP\"][\"/N\"]\n        .get_data()\n    )\n\n\n@pytest.mark.enable_socket\ndef test_no_t_in_articles():\n    \"\"\"Cf #2078\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12311735/bad.pdf\"\n    name = \"iss2078.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader)\n\n\n@pytest.mark.enable_socket\ndef test_no_i_in_articles():\n    \"\"\"Cf #2089\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12352793/kim2002.pdf\"\n    name = \"iss2089.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader)\n\n\n@pytest.mark.enable_socket\ndef test_damaged_pdf_length_returning_none():\n    \"\"\"\n    Cf #140\n    https://github.com/py-pdf/pypdf/issues/140#issuecomment-1685380549\n    \"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12168578/bad_pdf_example.pdf\"\n    name = \"iss140_bad_pdf.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader)\n\n\n@pytest.mark.enable_socket\ndef test_viewerpreferences():\n    \"\"\"Add Tests for ViewerPreferences\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/9175966/2015._pb_decode_pg0.pdf\"\n    name = \"2015._pb_decode_pg0.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    v = reader.viewer_preferences\n    assert v.center_window == True  # noqa: E712\n    writer = PdfWriter(clone_from=reader)\n    v = writer.viewer_preferences\n    assert v.center_window == True  # noqa: E712\n    v.center_window = False\n    assert (\n        writer.root_object[\"/ViewerPreferences\"][\"/CenterWindow\"] == False  # noqa: E712\n    )\n    assert v.print_area == \"/CropBox\"\n    with pytest.raises(ValueError):\n        v.non_fullscreen_pagemode = \"toto\"\n    with pytest.raises(ValueError):\n        v.non_fullscreen_pagemode = \"/toto\"\n    v.non_fullscreen_pagemode = \"/UseOutlines\"\n    assert (\n        writer.root_object[\"/ViewerPreferences\"][\"/NonFullScreenPageMode\"]\n        == \"/UseOutlines\"\n    )\n    writer = PdfWriter(clone_from=reader)\n    v = writer.viewer_preferences\n    assert v.center_window == True  # noqa: E712\n    v.center_window = False\n    assert (\n        writer.root_object[\"/ViewerPreferences\"][\"/CenterWindow\"] == False  # noqa: E712\n    )\n\n    writer = PdfWriter(clone_from=reader)\n    writer.root_object[NameObject(\"/ViewerPreferences\")] = writer._add_object(\n        writer.root_object[\"/ViewerPreferences\"]\n    )\n    v = writer.viewer_preferences\n    v.center_window = False\n    assert (\n        writer.root_object[\"/ViewerPreferences\"][\"/CenterWindow\"] == False  # noqa: E712\n    )\n    v.num_copies = 1\n    assert v.num_copies == 1\n    assert v.print_pagerange is None\n    with pytest.raises(ValueError):\n        v.print_pagerange = \"toto\"\n    v.print_pagerange = ArrayObject()\n    assert len(v.print_pagerange) == 0\n\n    writer.create_viewer_preferences()\n    assert len(writer.root_object[\"/ViewerPreferences\"]) == 0\n    writer.viewer_preferences.direction = \"/R2L\"\n    assert len(writer.root_object[\"/ViewerPreferences\"]) == 1\n\n    assert writer.viewer_preferences.enforce == []\n    assert \"/Enforce\" not in writer.viewer_preferences\n    writer.viewer_preferences.enforce += writer.viewer_preferences.PRINT_SCALING\n    assert writer.viewer_preferences[\"/Enforce\"] == [\"/PrintScaling\"]\n    writer.viewer_preferences.enforce = None\n    assert \"/Enforce\" not in writer.viewer_preferences\n    writer.viewer_preferences.enforce = None\n\n    del reader.trailer[\"/Root\"][\"/ViewerPreferences\"]\n    assert reader.viewer_preferences is None\n    writer = PdfWriter(clone_from=reader)\n    assert writer.viewer_preferences is None\n\n\ndef test_extra_spaces_in_da_text(caplog):\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"form.pdf\")\n    t = writer.pages[0][\"/Annots\"][0].get_object()[\"/DA\"]\n    t = t.replace(\"/Helv\", \"/Helv   \")\n    writer.pages[0][\"/Annots\"][0].get_object()[NameObject(\"/DA\")] = TextStringObject(t)\n    writer.update_page_form_field_values(\n        writer.pages[0], {\"foo\": \"abcd\"}, auto_regenerate=False\n    )\n    t = writer.pages[0][\"/Annots\"][0].get_object()[\"/AP\"][\"/N\"].get_data()\n    assert \"Font dictionary for  not found.\" not in caplog.text\n    assert b\"/Helv\" in t\n    assert b\"(abcd)\" in t\n\n\n@pytest.mark.enable_socket\ndef test_object_contains_indirect_reference_to_self():\n    url = \"https://github.com/py-pdf/pypdf/files/12389243/testbook.pdf\"\n    name = \"iss2102.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    width, height = 595, 841\n    outpage = writer.add_blank_page(width, height)\n    outpage.merge_page(reader.pages[6])\n    writer.append(reader)\n\n\ndef test_remove_image_per_type():\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"reportlab-inline-image.pdf\")\n    writer.remove_images(ImageType.INLINE_IMAGES)\n\n    assert all(\n        x not in writer.pages[0].get_contents().get_data()\n        for x in (b\"BI\", b\"ID\", b\"EI\")\n    )\n\n    writer.remove_images()\n\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"GeoBase_NHNC1_Data_Model_UML_EN.pdf\")\n    writer.remove_images(ImageType.DRAWING_IMAGES)\n    assert all(\n        x not in writer.pages[1].get_contents().get_data()\n        for x in (b\" re\\n\", b\"W*\", b\"f*\")\n    )\n    assert all(\n        x in writer.pages[1].get_contents().get_data() for x in (b\" TJ\\n\", b\"rg\", b\"Tm\")\n    )\n    assert all(\n        x not in writer.pages[9][\"/Resources\"][\"/XObject\"][\"/Meta84\"].get_data()\n        for x in (b\" re\\n\", b\"W*\", b\"f*\")\n    )\n    writer.remove_images(ImageType.XOBJECT_IMAGES)\n    assert b\"Do\\n\" not in writer.pages[0].get_contents().get_data()\n    assert len(writer.pages[0][\"/Resources\"][\"/XObject\"]) == 0\n\n\n@pytest.mark.enable_socket\ndef test_add_outlines_on_empty_dict():\n    \"\"\"Cf #2233\"\"\"\n\n    def _get_parent_bookmark(current_indent, history_indent, bookmarks) -> Any:\n        \"\"\"The parent of A is the nearest bookmark whose indent is smaller than A's\"\"\"\n        assert len(history_indent) == len(bookmarks)\n        if current_indent == 0:\n            return None\n        for i in range(len(history_indent) - 1, -1, -1):\n            # len(history_indent) - 1   ===>   0\n            if history_indent[i] < current_indent:\n                return bookmarks[i]\n        return None\n\n    bookmark_lines = \"\"\"1 FUNDAMENTALS OF RADIATIVE TRANSFER 1\n1.1 The Electromagnetic Spectrum; Elementary Properties of Radiation 1\n1.2 Radiative Flux 2\n    Macroscopic Description of the Propagation of Radiation 2\n    Flux from an Isotropic Source-The Inverse Square Law 2\n1.3 The Specific Intensity and Its Moments 3\n    Definition of Specific Intensity or Brightness 3\n    Net Flux and Momentum Flux 4\n    Radiative Energy Density 5\n    Radiation Pressure in an Enclosure Containing an Isotropic Radiation Field 6\n    Constancy of Specific Zntensiw Along Rays in Free Space 7\n    Proof of the Inverse Square Law for a Uniformly Bright Sphere 7\n1.4 Radiative Transfer 8\n    Emission 9\n    Absorption 9\n    The Radiative Transfer Equation 11\n    Optical Depth and Source Function 12\n    Mean Free Path 14\n    Radiation Force 15\n1.5 Thermal Radiation 15\n    Blackbody Radiation 15\n    Kirchhof's Law for Thermal Emission 16\n    Thermodynamics of Blackbody Radiation 17\n    The Planck Spectrum 20\n    Properties of the Planck Law 23\n    Characteristic Temperatures Related to Planck Spectrum 25\n1.6 The Einstein Coefficients 27\n    Definition of Coefficients 27\n    Relations between Einstein Coefficients 29\n    Absorption and Emission Coefficients in Terms of Einstein Coefficients 30\n1.7 Scattering Effects; Random Walks 33\n    Pure Scattering 33\n    Combined Scattering and Absorption 36\n1.8 Radiative Diffusion 39\n    The Rosseland Approximation 39\n    The Eddington Approximation; Two-Stream Approximation 42\nPROBLEMS 45\nREFERENCES 50\n2 BASIC THEORY OF RADIATION FIELDS 51\n2.1 Review of Maxwell’s Equations 51\n2.2 Plane Electromagnetic Waves 55\n2.3 The Radiation Spectrum 58\n2.4 Polarization and Stokes Parameters 62\n    Monochromatic Waves 62\n    Quasi-monochromatic Waves 65\n2.5 Electromagnetic Potentials 69\n2.6 Applicability of Transfer Theory and the Geometrical Optics Limit 72\nPROBLEMS 74\nREFERENCES 76\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/12797067/test-12.pdf\"\n    name = \"iss2233.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter(clone_from=reader)\n\n    bookmarks, history_indent = [], []\n    for line in bookmark_lines.split(\"\\n\"):\n        line2 = re.split(r\"\\s+\", line.strip())\n        indent_size = len(line) - len(line.lstrip())\n        parent = _get_parent_bookmark(indent_size, history_indent, bookmarks)\n        history_indent.append(indent_size)\n        title, page = \" \".join(line2[:-1]), int(line2[-1]) - 1\n        new_bookmark = writer.add_outline_item(title, page, parent=parent)\n        bookmarks.append(new_bookmark)\n\n\ndef test_merging_many_temporary_files(caplog):\n    def create_number_pdf(_n) -> BytesIO:\n        pytest.importorskip(\"fpdf\")\n        from fpdf import FPDF  # noqa: PLC0415\n\n        pdf = FPDF()\n        pdf.add_page()\n        pdf.set_font(\"helvetica\", \"B\", 16)\n        pdf.cell(40, 10, str(_n))\n        byte_string = pdf.output()\n        return BytesIO(byte_string)\n\n    writer = PdfWriter()\n    for n in range(100):\n        reader = PdfReader(create_number_pdf(n))\n        for page in reader.pages:\n            # Should only be one page.\n            writer.add_page(page)\n\n    pg = PageObject.create_blank_page(writer, 1000, 1000)\n    pg1 = writer.add_page(pg)\n    assert len(writer.pages) == 101\n    caplog.clear()\n    writer.remove_page(pg)\n    assert \"Cannot find page in pages\" in caplog.text\n    assert len(writer.pages) == 101\n    writer.remove_page(pg1)\n    assert len(writer.pages) == 100\n\n    out = BytesIO()\n    writer.write(out)\n\n    out.seek(0)\n    reader = PdfReader(out)\n    for n, page in enumerate(reader.pages):\n        text = page.extract_text()\n        assert text == str(n)\n    # test completed to validate remove_page\n    writer.remove_page(writer.pages[-1], True)\n\n    writer2 = PdfWriter()\n    writer2.remove_page(0)\n    writer2.flattened_pages = None\n    writer2.remove_page(0)\n\n    caplog.clear()\n    writer.remove_page(writer.pages[-1][\"/Contents\"].indirect_reference)\n    assert \"IndirectObject is not referencing a page\" in caplog.text\n\n    caplog.clear()\n    pg = PageObject.create_blank_page(writer, 1000, 1000)\n    writer.remove_page(pg)\n    assert \"Cannot find page in pages\" in caplog.text\n\n    caplog.clear()\n    writer.remove_page(999999)\n    assert \"Page number is out of range\" in caplog.text\n\n    pg = PageObject.create_blank_page(writer, 1000, 1000)\n    pg = writer._add_object(pg)\n    writer.flattened_pages.append(pg)\n    caplog.clear()\n    writer.remove_page(pg)\n    assert \"Cannot find page in pages\" in caplog.text\n\n\n@pytest.mark.enable_socket\ndef test_reattach_fields():\n    \"\"\"\n    Test Reattach function\n    addressed in #2453\n    \"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/14241368/ExampleForm.pdf\"\n    name = \"iss2453.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    for p in reader.pages:\n        writer.add_page(p)\n    assert len(writer.reattach_fields()) == 15\n    assert len(writer.reattach_fields()) == 0  # nothing to append anymore\n    assert len(writer.root_object[\"/AcroForm\"][\"/Fields\"]) == 15\n    writer = PdfWriter(clone_from=reader)\n    assert len(writer.reattach_fields()) == 7\n    writer.reattach_fields()\n    assert len(writer.root_object[\"/AcroForm\"][\"/Fields\"]) == 15\n\n    writer = PdfWriter()\n    for p in reader.pages:\n        writer.add_page(p)\n    ano = writer.pages[0][\"/Annots\"][0].get_object()\n    del ano.indirect_reference\n    writer.pages[0][\"/Annots\"][0] = ano\n    assert isinstance(writer.pages[0][\"/Annots\"][0], DictionaryObject)\n    assert len(writer.reattach_fields(writer.pages[0])) == 6\n    assert isinstance(writer.pages[0][\"/Annots\"][0], IndirectObject)\n    del writer.pages[1][\"/Annots\"]\n    assert len(writer.reattach_fields(writer.pages[1])) == 0\n\n\ndef test_get_pagenumber_from_indirectobject():\n    \"\"\"Test test_get_pagenumber_from_indirectobject\"\"\"\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    writer = PdfWriter(clone_from=pdf_path)\n    assert writer._get_page_number_by_indirect(None) is None\n    assert writer._get_page_number_by_indirect(NullObject()) is None\n\n    ind = writer.pages[0].indirect_reference\n    assert writer._get_page_number_by_indirect(ind) == 0\n    assert writer._get_page_number_by_indirect(ind.idnum) == 0\n    assert writer._get_page_number_by_indirect(ind.idnum + 1) is None\n\n\ndef test_replace_object():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter(clone_from=reader)\n    with pytest.raises(ValueError):\n        writer._replace_object(reader.pages[0].indirect_reference, reader.pages[0])\n    writer._replace_object(writer.pages[0].indirect_reference, reader.pages[0])\n    pg = PageObject.create_blank_page(writer, 1000, 1000)\n    writer._replace_object(writer.pages[0].indirect_reference, pg)\n\n    # mainly for coverage\n    reader = PdfReader(pdf_path)  # reload a new instance\n    with pytest.raises(ValueError):\n        reader._replace_object(writer.pages[0].indirect_reference, reader.pages[0])\n    with pytest.raises(ValueError):\n        reader._replace_object(IndirectObject(9999, 9999, reader), reader.pages[0])\n    reader._replace_object(reader.pages[0].indirect_reference, reader.pages[0])\n    pg = PageObject.create_blank_page(writer, 1000, 1000)\n    reader._replace_object(reader.pages[0].indirect_reference, pg)\n    pg = PageObject.create_blank_page(None, 1000, 1000)\n    pg[NameObject(\"/Contents\")] = writer.pages[0][\"/Contents\"]\n    writer._add_object(pg)\n    writer.add_page(pg)\n\n\ndef test_mime_jupyter():\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter(clone_from=reader)\n    assert reader._repr_mimebundle_((\"include\",), (\"exclude\",)) == {}\n    assert writer._repr_mimebundle_((\"include\",), (\"exclude\",)) == {}\n\n\ndef test_init_without_named_arg():\n    \"\"\"Test to use file_obj argument and not clone_from\"\"\"\n    pdf_path = RESOURCE_ROOT / \"crazyones.pdf\"\n    reader = PdfReader(pdf_path)\n    writer = PdfWriter(clone_from=reader)\n    nb = len(writer._objects)\n    writer = PdfWriter(reader)\n    assert len(writer._objects) == nb\n    with open(pdf_path, \"rb\") as f:\n        writer = PdfWriter(f)\n        f.seek(0, 0)\n        by = BytesIO(f.read())\n    assert len(writer._objects) == nb\n    writer = PdfWriter(pdf_path)\n    assert len(writer._objects) == nb\n    writer = PdfWriter(str(pdf_path))\n    assert len(writer._objects) == nb\n    writer = PdfWriter(by)\n    assert len(writer._objects) == nb\n\n\n@pytest.mark.enable_socket\ndef test_i_in_choice_fields():\n    \"\"\"Cf #2611\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/15176321/FRA.F.6180.150.pdf\"\n    name = \"iss2611.pdf\"\n    writer = PdfWriter(BytesIO(get_data_from_url(url, name=name)))\n    assert \"/I\" in writer.get_fields()[\"State\"].indirect_reference.get_object()\n    writer.update_page_form_field_values(\n        writer.pages[0], {\"State\": \"NY\"}, auto_regenerate=False\n    )\n    assert \"/I\" not in writer.get_fields()[\"State\"].indirect_reference.get_object()\n\n\ndef test_selfont():\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"FormTestFromOo.pdf\")\n    writer.update_page_form_field_values(\n        writer.pages[0],\n        {\"Text1\": (\"Text_1\", \"\", 5), \"Text2\": (\"Text_2\", \"/F3\", 0)},\n        auto_regenerate=False,\n    )\n    assert (\n        b\"/F3 5 Tf\"\n        in writer.pages[0][\"/Annots\"][1].get_object()[\"/AP\"][\"/N\"].get_data()\n    )\n    assert (\n        b\"Text_1\" in writer.pages[0][\"/Annots\"][1].get_object()[\"/AP\"][\"/N\"].get_data()\n    )\n    assert (\n        b\"/F3 12.0 Tf\"\n        in writer.pages[0][\"/Annots\"][2].get_object()[\"/AP\"][\"/N\"].get_data()\n    )\n    assert (\n        b\"Text_2\" in writer.pages[0][\"/Annots\"][2].get_object()[\"/AP\"][\"/N\"].get_data()\n    )\n\n\n@pytest.mark.enable_socket\ndef test_no_resource_for_14_std_fonts():\n    \"\"\"Cf #2670\"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/15405390/f1040.pdf\"\n    name = \"iss2670.pdf\"\n    writer = PdfWriter(BytesIO(get_data_from_url(url, name=name)))\n    p = writer.pages[0]\n    for a in p[\"/Annots\"]:\n        a = a.get_object()\n        if a[\"/FT\"] == \"/Tx\":\n            writer.update_page_form_field_values(\n                p, {a[\"/T\"]: \"Brooks\"}, auto_regenerate=False\n            )\n            assert \"/Helvetica\" in a[\"/AP\"][\"/N\"][\"/Resources\"][\"/Font\"]\n\n\n@pytest.mark.enable_socket\ndef test_field_box_upside_down():\n    \"\"\"Cf #2724\"\"\"\n    url = \"https://github.com/user-attachments/files/15996356/FRA.F.6180.55.pdf\"\n    name = \"iss2724.pdf\"\n    writer = PdfWriter(BytesIO(get_data_from_url(url, name=name)))\n    writer.update_page_form_field_values(None, {\"FreightTrainMiles\": \"0\"})\n    assert writer.pages[0][\"/Annots\"][13].get_object()[\"/AP\"][\"/N\"].get_data() == (\n        b\"q\\n/Tx BMC \\nq\\n2 1 102.29520000000001 9.835000000000036 re\\n\"\n        b\"W\\nBT\\n/Arial 8.0 Tf 0 g\\n2 3.0455000000000183 Td\\n(0) Tj\\nET\\n\"\n        b\"Q\\nEMC\\nQ\\n\"\n    )\n    box = writer.pages[0][\"/Annots\"][13].get_object()[\"/AP\"][\"/N\"][\"/BBox\"]\n    assert box[2] > 0\n    assert box[3] > 0\n\n\n@pytest.mark.enable_socket\ndef test_matrix_entry_in_field_annots():\n    \"\"\"Cf #2731\"\"\"\n    url = \"https://github.com/user-attachments/files/16036514/template.pdf\"\n    name = \"iss2731.pdf\"\n    writer = PdfWriter(BytesIO(get_data_from_url(url, name=name)))\n    writer.update_page_form_field_values(\n        writer.pages[0],\n        {\"Stellenbezeichnung_1\": \"some filled in text\"},\n        auto_regenerate=False,\n    )\n    assert \"/Matrix\" in writer.pages[0][\"/Annots\"][5].get_object()[\"/AP\"][\"/N\"]\n\n\n@pytest.mark.enable_socket\ndef test_compress_identical_objects():\n    \"\"\"Cf #2728 and #2794\"\"\"\n    url = \"https://github.com/user-attachments/files/16575458/tt2.pdf\"\n    name = \"iss2794.pdf\"\n    in_bytes = BytesIO(get_data_from_url(url, name=name))\n    writer = PdfWriter(in_bytes)\n    writer.compress_identical_objects(remove_orphans=False)\n    out1 = BytesIO()\n    writer.write(out1)\n    assert 0.5 * len(in_bytes.getvalue()) > len(out1.getvalue())\n    writer.remove_page(\n        1\n    )  # page0 contains fields which keep reference to the deleted page\n    out2 = BytesIO()\n    writer.write(out2)\n    assert len(out1.getvalue()) - 100 < len(out2.getvalue())\n    writer.compress_identical_objects(remove_identicals=False)\n    out3 = BytesIO()\n    writer.write(out3)\n    assert len(out2.getvalue()) > len(out3.getvalue())\n\n\ndef test_set_need_appearances_writer():\n    \"\"\"Minimal test for coverage\"\"\"\n    writer = PdfWriter()\n    writer.set_need_appearances_writer()\n\n\ndef test_utf16_metadata():\n    \"\"\"See #2754\"\"\"\n    writer = PdfWriter(RESOURCE_ROOT / \"crazyones.pdf\")\n    writer.add_metadata(\n        {\n            \"/Subject\": \"Invoice №AI_047\",\n        }\n    )\n    b = BytesIO()\n    writer.write(b)\n    b.seek(0)\n    reader = PdfReader(b)\n    assert reader.metadata.subject == \"Invoice №AI_047\"\n    bb = b.getvalue()\n    i = bb.find(b\"/Subject\")\n    assert bb[i : i + 100] == (\n        b\"/Subject (\\\\376\\\\377\\\\000I\\\\000n\\\\000v\\\\000o\\\\000i\\\\000c\\\\000e\"\n        b\"\\\\000 \\\\041\\\\026\\\\000A\\\\000I\\\\000\\\\137\\\\0000\\\\0004\\\\0007)\"\n    )\n\n\n@pytest.mark.enable_socket\ndef test_increment_writer(caplog):\n    \"\"\"Tests for #2811\"\"\"\n    writer = PdfWriter(\n        RESOURCE_ROOT / \"Seige_of_Vicksburg_Sample_OCR-crazyones-merged.pdf\",\n        incremental=True,\n    )\n    # Contains JBIG2 not decoded for the moment\n    assert writer.list_objects_in_increment() == []  # no flowdown of properties\n\n    # test writing with empty increment\n    b = BytesIO()\n    writer.write(b)\n    with open(\n        RESOURCE_ROOT / \"Seige_of_Vicksburg_Sample_OCR-crazyones-merged.pdf\", \"rb\"\n    ) as f:\n        assert b.getvalue() == f.read(-1)\n    b.seek(0)\n    writer2 = PdfWriter(b, incremental=True)\n    assert len([x for x in writer2._objects if x is not None]) == len(\n        [x for x in writer._objects if x is not None]\n    )\n    writer2.add_metadata({\"/Author\": \"test\"})\n    assert len(writer2.list_objects_in_increment()) == 1\n    b = BytesIO()\n    writer2.write(b)\n\n    # modify one object\n    writer.pages[0][NameObject(\"/MediaBox\")] = ArrayObject(\n        [NumberObject(0), NumberObject(0), NumberObject(864), NumberObject(648)]\n    )\n    assert writer.list_objects_in_increment() == [IndirectObject(4, 0, writer)]\n    b = BytesIO()\n    writer.write(b)\n    writer.pages[5][NameObject(\"/MediaBox\")] = ArrayObject(\n        [NumberObject(0), NumberObject(0), NumberObject(864), NumberObject(648)]\n    )\n    assert len(writer.list_objects_in_increment()) == 2\n    # modify object IndirectObject(5,0) : for coverage\n    writer.get_object(5)[NameObject(\"/ForTestOnly\")] = NameObject(\"/ForTestOnly\")\n\n    b = BytesIO()\n    writer.write(b)\n    assert b.getvalue().startswith(writer._reader.stream.getvalue())\n    b.seek(0)\n    reader = PdfReader(b)\n    assert reader.pages[0][\"/MediaBox\"] == ArrayObject(\n        [NumberObject(0), NumberObject(0), NumberObject(864), NumberObject(648)]\n    )\n    assert \"/ForTestOnly\" in reader.get_object(5)\n    with pytest.raises(PyPdfError):\n        writer = PdfWriter(1, incremental=True)\n    b.seek(0)\n    writer = PdfWriter(b, incremental=True)\n    assert writer.list_objects_in_increment() == []  # no flowdown of properties\n\n    writer = PdfWriter(RESOURCE_ROOT / \"crazyones.pdf\", incremental=True)\n    # 1 object is modified: page 0  inherits MediaBox so is changed\n    assert len(writer.list_objects_in_increment()) == 1\n    b = BytesIO()\n    writer.write(b)\n\n    writer = PdfWriter(RESOURCE_ROOT / \"crazyones.pdf\", incremental=False)\n    # 1 object is modified: page 0  inherits MediaBox so is changed\n    assert len(writer.list_objects_in_increment()) == len(writer._objects)\n\n    # insert pages in a tree\n    url = \"https://github.com/py-pdf/pypdf/files/13946477/panda.pdf\"\n    name = \"iss2343b.pdf\"\n    writer = PdfWriter(BytesIO(get_data_from_url(url, name=name)), incremental=True)\n    reader = PdfReader(RESOURCE_ROOT / \"crazyones.pdf\")\n    pg = writer.insert_page(reader.pages[0], 4)\n    assert (\n        pg.raw_get(\"/Parent\")\n        == writer.root_object[\"/Pages\"][\"/Kids\"][0].get_object()[\"/Kids\"][0]\n    )\n    assert pg[\"/Parent\"][\"/Count\"] == 8\n    assert writer.root_object[\"/Pages\"][\"/Count\"] == 285\n    assert len(writer.flattened_pages) == 285\n\n    # clone without info\n    writer = PdfWriter(RESOURCE_ROOT / \"missing_info.pdf\", incremental=True)\n    assert len(writer.list_objects_in_increment()) == 0\n    assert writer.metadata is None\n    writer.metadata = {}\n    assert writer.metadata == {}\n    assert len(writer.list_objects_in_increment()) == 1\n    writer.metadata = None\n    assert len(writer.list_objects_in_increment()) == 0\n    assert writer.metadata is None\n    b = BytesIO()\n    writer.write(b)\n\n\n@pytest.mark.enable_socket\ndef test_append_pdf_with_dest_without_page(caplog):\n    \"\"\"Tests for #2842\"\"\"\n    url = \"https://github.com/user-attachments/files/16990834/test.pdf\"\n    name = \"iss2842.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader)\n    assert \"/__WKANCHOR_8\" not in writer.named_destinations\n    assert len(writer.named_destinations) == 3\n\n\n@pytest.mark.enable_socket\ndef test_destination_is_nullobject():\n    \"\"\"Tests for #2958\"\"\"\n    url = \"https://github.com/user-attachments/files/17822279/C0.00.-.COVER.SHEET.pdf\"\n    name = \"iss2958.pdf\"\n    source_data = BytesIO(get_data_from_url(url, name=name))\n    writer = PdfWriter()\n    writer.append(source_data)\n\n\n@pytest.mark.enable_socket\ndef test_destination_page_is_none():\n    \"\"\"Tests for #2963\"\"\"\n    url = \"https://github.com/user-attachments/files/17879461/3.pdf\"\n    name = \"iss2963.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    writer = PdfWriter()\n    writer.append(reader)\n\n\ndef test_stream_not_closed():\n    \"\"\"Tests for #2905\"\"\"\n    src = RESOURCE_ROOT / \"pdflatex-outline.pdf\"\n    with NamedTemporaryFile(suffix=\".pdf\") as tmp:\n        with PdfReader(src) as reader, PdfWriter() as writer:\n            writer.add_page(reader.pages[0])\n            writer.write(tmp)\n        assert not tmp.file.closed\n\n    with NamedTemporaryFile(suffix=\".pdf\") as target:\n        with PdfWriter(target.file) as writer:\n            writer.add_blank_page(100, 100)\n        assert not target.file.closed\n\n    with open(src, \"rb\") as fileobj:\n        with PdfWriter(fileobj) as writer:\n            pass\n        assert not fileobj.closed\n\n\ndef test_auto_write(tmp_path):\n    \"\"\"Another test for #2905\"\"\"\n    target = tmp_path / \"out.pdf\"\n    with PdfWriter(target) as writer:\n        writer.add_blank_page(100, 100)\n    assert target.stat().st_size > 0\n\n\ndef test_deprecate_with_as():\n    \"\"\"Yet another test for #2905\"\"\"\n    with PdfWriter() as writer:\n        with pytest.raises(\n                expected_exception=DeprecationError,\n                match=r\"with_as_usage is deprecated and was removed in pypdf 5\\.0\"\n        ):\n            _ = writer.with_as_usage\n\n        with pytest.raises(\n                expected_exception=DeprecationError,\n                match=r\"with_as_usage is deprecated and was removed in pypdf 5\\.0\"\n        ):\n            writer.with_as_usage = False  # old code allowed setting this, so...\n\n\n@pytest.mark.skipif(GHOSTSCRIPT_BINARY is None, reason=\"Requires Ghostscript\")\n@pytest.mark.enable_socket\ndef test_inline_image_q_operator_handling(tmp_path):\n    \"\"\"Test for #2927\"\"\"\n    pdf_url = \"https://github.com/user-attachments/files/17614880/test_clean.pdf\"\n    pdf_name = \"iss2927.pdf\"\n    pdf_data = BytesIO(get_data_from_url(pdf_url, name=pdf_name))\n\n    png_url = \"https://github.com/user-attachments/assets/abe16f48-9afa-4179-b1e8-62be27b95c26\"\n    png_name = \"iss2927.png\"\n    expected_png_path = tmp_path / \"expected.png\"\n    expected_png_path.write_bytes(get_data_from_url(png_url, name=png_name))\n\n    writer = PdfWriter()\n    writer.append(pdf_data)\n    for page in writer.pages:\n        page.transfer_rotation_to_content()\n\n    pdf_path = tmp_path / \"out.pdf\"\n    png_path = tmp_path / \"actual.png\"\n\n    writer.write(pdf_path)\n    # False positive: https://github.com/PyCQA/bandit/issues/333\n    subprocess.run(  # noqa: S603\n        [\n            GHOSTSCRIPT_BINARY,\n            \"-r120\",\n            \"-sDEVICE=pngalpha\",\n            \"-o\",\n            png_path,\n            pdf_path,\n        ]\n    )\n    assert png_path.is_file()\n    assert image_similarity(png_path, expected_png_path) >= 0.99999\n\n\ndef test_insert_filtered_annotations__annotations_are_none():\n    writer = PdfWriter()\n    writer.add_blank_page(72, 72)\n    stream = BytesIO()\n    writer.write(stream)\n    reader = PdfReader(stream)\n    assert writer._insert_filtered_annotations(\n        annots=None, page=PageObject(), pages={}, reader=reader\n    ) == []\n\n\ndef test_incremental_read():\n    \"\"\"Test for #3116\"\"\"\n    writer = PdfWriter()\n    writer.add_blank_page(72, 72)\n    stream0 = BytesIO()\n    writer.write(stream0)\n\n    reader = PdfReader(stream0)\n    # 1 = Catalog, 2 = Pages, 3 = New Page, 4 = Info, Size == 5\n    assert reader.trailer[\"/Size\"] == 5\n\n    stream0.seek(0, 0)\n    writer = PdfWriter(stream0, incremental=True)\n    assert len(writer._objects) == 4\n    assert writer._objects[-1] is not None\n    stream1 = BytesIO()\n    writer.write(stream1)\n\n    # nothing modified, so nothing added = ideal situation\n    assert stream1.getvalue() == stream1.getvalue()\n\n    stream0.seek(0, 0)\n    writer = PdfWriter(stream0, incremental=True)\n    assert len(writer._objects) == 4\n    assert writer._objects[-1] is not None\n    writer.add_blank_page(72, 72)\n    assert len(writer._objects) == 5\n    stream1 = BytesIO()\n    writer.write(stream1)\n    # 2 = Pages, 5 = New Page, 6 = XRef, Size == 7\n    # XRef is created on write and not counted\n    assert len(writer._objects) == 5\n\n\ndef test_compress_identical_objects__after_remove_images():\n    \"\"\"Test for #3237\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"AutoCad_Diagram.pdf\")\n    writer.remove_images()\n    writer.compress_identical_objects(remove_identicals=True, remove_orphans=True)\n\n\ndef test_merge__process_named_dests__no_dests_in_source_file():\n    \"\"\"Test for #3279\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n\n    # Hacky solution to avoid attribute errors.\n    names = DictionaryObject()\n    names.indirect_reference = names\n    writer.root_object[NameObject(\"/Names\")] = names\n\n    reader = PdfReader(RESOURCE_ROOT / \"hello-world.pdf\")\n    destination = Destination(title=\"test.pdf\", page=reader.pages[0], fit=Fit(\"/Fit\"))\n    with mock.patch.object(reader, \"_get_named_destinations\", return_value={\"test.pdf\": destination}):\n        writer.append(reader)\n        # The page now points to the appended one.\n        assert writer.named_destinations == {\n            \"test.pdf\": Destination(title=\"test.pdf\", page=writer.pages[1].indirect_reference, fit=Fit(\"/Fit\"))\n        }\n\n\ndef test_insert_filtered_annotations__link_without_destination():\n    \"\"\"Test for #3211\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n    reader = PdfReader(RESOURCE_ROOT / \"hello-world.pdf\")\n\n    annotations = [\n        DictionaryObject({\n            \"/A\": DictionaryObject({\"/S\": NameObject(\"/GoTo\"), \"/D\": None}),\n            \"/BS\": {\"/S\": \"/S\", \"/Type\": \"/Border\", \"/W\": 0},\n            \"/Border\": [0, 0, 0],\n            \"/H\": \"/I\",\n            \"/Rect\": [68.6001, 653.405, 526.2, 671.054],\n            \"/StructParent\": 9,\n            \"/Subtype\": NameObject(\"/Link\"),\n            \"/Type\": NameObject(\"/Annot\")\n        })\n    ]\n    result = writer._insert_filtered_annotations(\n        annots=annotations, page=writer.pages[0], pages={}, reader=reader\n    )\n    assert result == []\n\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n    del annotations[0][\"/A\"][\"/D\"]\n    result = writer._insert_filtered_annotations(\n        annots=annotations, page=writer.pages[0], pages={}, reader=reader\n    )\n    assert result == []\n\n\n@pytest.mark.enable_socket\ndef test_insert_filtered_annotations__annotations_are_no_list(caplog):\n    \"\"\"Tests for #3320\"\"\"\n    url = \"https://github.com/user-attachments/files/20818089/bugpdf.pdf\"\n    name = \"issue3320.pdf\"\n    source_data = BytesIO(get_data_from_url(url, name=name))\n    reader = PdfReader(source_data)\n    writer = PdfWriter()\n    writer.append(reader)\n    font_file2 = reader.get_object(36).indirect_reference\n    assert caplog.messages == [\n        (\n            f\"Expected annotation arrays: {{'/FontFile2': {font_file2!r}, \"\n            \"'/Descent': -269, '/CapHeight': 714, '/FontWeight': \"\n            \"300, '/FontName': '/JQJGLF+OpenSans-Light', '/ItalicAngle': 0, '/StemV': \"\n            \"48, '/Type': '/FontDescriptor', '/FontBBox': [-521, -269, 1140, 1048], \"\n            \"'/FontFamily': 'Open Sans Light', '/Flags': 32, '/XHeight': 531, \"\n            \"'/Ascent': 1048, '/FontStretch': '/Normal'} []. Ignoring annotations.\"\n        ),\n        (\n            f\"Expected list of annotations, got {{'/FontFile2': {font_file2!r}, \"\n            \"'/Descent': -269, '/CapHeight': 714, '/FontWeight': 300, '/FontName': '/JQJGLF+OpenSans-Light', \"\n            \"'/ItalicAngle': 0, '/StemV': 48, '/Type': '/FontDescriptor', '/FontBBox': [-521, -269, 1140, 1048], \"\n            \"'/FontFamily': 'Open Sans Light', '/Flags': 32, '/XHeight': 531, '/Ascent': 1048, '/FontStretch': \"\n            \"'/Normal'} of type DictionaryObject.\"\n        )\n    ]\n\n\ndef test_unterminated_object__with_incremental_writer():\n    \"\"\"Test for #3118\"\"\"\n    reader = PdfReader(RESOURCE_ROOT / \"bytes.pdf\")\n    writer = PdfWriter(reader, incremental=True)\n\n    writer.add_blank_page(72, 72)\n\n    fi = BytesIO()\n    writer.write(fi)\n    b = fi.getvalue()\n    assert b[-39:] == b\"\\nendstream\\nendobj\\nstartxref\\n1240\\n%%EOF\\n\"\n\n\ndef test_wrong_size_in_incremental_pdf(caplog):\n    source_data = RESOURCE_ROOT.joinpath(\"crazyones.pdf\").read_bytes()\n    writer = PdfWriter(BytesIO(source_data), incremental=True)\n    writer._add_object(DictionaryObject())\n\n    incremental_data = BytesIO()\n    writer.write(incremental_data)\n    modified_data = incremental_data.getvalue().replace(b\"/Size 25\", b\"/Size 2\")\n\n    writer = PdfWriter(BytesIO(modified_data), incremental=False)\n    assert \"Object count 19 exceeds defined trailer size 2\" in caplog.text\n    assert len(writer._objects) == 20\n\n    caplog.clear()\n    writer = PdfWriter(incremental=False, strict=True)\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^Object count 19 exceeds defined trailer size 2$\"):\n        writer.clone_reader_document_root(reader=PdfReader(BytesIO(modified_data)))\n\n    with pytest.raises(expected_exception=PdfReadError, match=r\"^Got index error while flattening\\.$\"):\n        PdfWriter(BytesIO(modified_data), incremental=True)\n\n\n@pytest.mark.enable_socket\ndef test_flatten_form_field_without_font_in_resources():\n    \"\"\"\n    This test is a regression test for issue #3553.\n    Flatten form field with /Resources lacking /Font.\n    \"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"issue-3553.pdf\")))\n    writer = PdfWriter()\n    writer.append(reader)\n    writer.update_page_form_field_values(\n        writer.pages[0],\n        {\"Unique reference numberRow1\": \"test\"},\n        flatten=True,\n    )\n    b = BytesIO()\n    writer.write(b)\n\n    reader = PdfReader(b)\n    form_text_fields = reader.get_form_text_fields()\n    assert form_text_fields[\"Unique reference numberRow1\"] == \"test\"\n\n\ndef test_merge_with_null_acroform_does_not_raise_typeerror():\n    \"\"\"\n    Source PDFs may contain '/AcroForm null'.\n\n    Test for issue #3598.\n    \"\"\"\n    src_writer = PdfWriter()\n    src_writer.add_blank_page(72, 72)\n    src_writer.root_object[NameObject(\"/AcroForm\")] = NullObject()\n\n    src_bytes = BytesIO()\n    src_writer.write(src_bytes)\n    src_bytes.seek(0)\n\n    source = PdfReader(src_bytes)\n\n    target = PdfWriter()\n    target.merge(0, source)\n\n    assert \"/AcroForm\" not in target.root_object\n\n\ndef test_compress_identical_objects__info_is_none():\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"crazyones.pdf\")\n    writer.compress_identical_objects()\n\n    writer.metadata = None\n    writer.compress_identical_objects()\n\n\n@pytest.mark.enable_socket\ndef test_flatten_form_field_with_signature():\n    \"\"\"\n    This test is a regression test for issue #3633.\n    Flatten form field with /Sig.\n    \"\"\"\n    writer = PdfWriter(BytesIO(get_data_from_url(name=\"issue-3633.pdf\")))\n    writer.update_page_form_field_values(\n        writer.pages[0],\n        {\"signature\": \"test\"},\n        flatten=True,\n    )\n    b = BytesIO()\n    writer.write(b)\n\n    _ = PdfReader(b)\n"
  },
  {
    "path": "tests/test_xmp.py",
    "content": "\"\"\"Test the pypdf.xmp module.\"\"\"\nfrom datetime import datetime, timedelta, timezone\nfrom io import BytesIO\n\nimport pytest\n\nimport pypdf.generic\nimport pypdf.xmp\nfrom pypdf import PdfReader, PdfWriter\nfrom pypdf.errors import PdfReadError, XmpDocumentError\nfrom pypdf.generic import ContentStream, NameObject, StreamObject\nfrom pypdf.xmp import XmpInformation\n\nfrom . import RESOURCE_ROOT, SAMPLE_ROOT, get_data_from_url\n\n\n@pytest.mark.samples\n@pytest.mark.parametrize(\n    \"src\",\n    [\n        (SAMPLE_ROOT / \"020-xmp/output_with_metadata_pymupdf.pdf\"),\n    ],\n)\ndef test_read_xmp_metadata_samples(src):\n    reader = PdfReader(src)\n    xmp = reader.xmp_metadata\n    assert xmp\n    assert xmp.dc_contributor == []\n    assert xmp.dc_creator == [\"John Doe\"]\n    assert xmp.dc_source == \"Martin Thoma\"  # attribute node\n    assert xmp.dc_description == {\"x-default\": \"This is a text\"}\n    assert xmp.dc_date == [datetime(1990, 4, 28, 0, 0)]\n    assert xmp.dc_title == {\"x-default\": \"Sample PDF with XMP Metadata\"}\n    assert xmp.custom_properties == {\n        \"Style\": \"FooBarStyle\",\n        \"other\": \"worlds\",\n        \"⏰\": \"time\",\n    }\n\n\n@pytest.mark.samples\ndef test_writer_xmp_metadata_samples():\n    writer = PdfWriter(SAMPLE_ROOT / \"020-xmp/output_with_metadata_pymupdf.pdf\")\n    xmp = writer.xmp_metadata\n    assert xmp\n    assert xmp.dc_contributor == []\n    assert xmp.dc_creator == [\"John Doe\"]\n    assert xmp.dc_source == \"Martin Thoma\"  # attribute node\n    assert xmp.dc_description == {\"x-default\": \"This is a text\"}\n    assert xmp.dc_date == [datetime(1990, 4, 28, 0, 0)]\n    assert xmp.dc_title == {\"x-default\": \"Sample PDF with XMP Metadata\"}\n    assert xmp.custom_properties == {\n        \"Style\": \"FooBarStyle\",\n        \"other\": \"worlds\",\n        \"⏰\": \"time\",\n    }\n    co = pypdf.generic.ContentStream(None, None)\n    co.set_data(\n        xmp.stream.get_data().replace(\n            b'dc:source=\"Martin Thoma\"', b'dc:source=\"Pubpub-Zz\"'\n        )\n    )\n    writer.xmp_metadata = pypdf.xmp.XmpInformation(co)\n    b = BytesIO()\n    writer.write(b)\n    reader = PdfReader(b)\n    xmp2 = reader.xmp_metadata\n    assert xmp2.dc_source == \"Pubpub-Zz\"\n\n\n@pytest.mark.parametrize(\n    (\"src\", \"has_xmp\"),\n    [\n        (RESOURCE_ROOT / \"commented-xmp.pdf\", True),\n        (RESOURCE_ROOT / \"crazyones.pdf\", False),\n    ],\n)\ndef test_read_xmp_metadata(src, has_xmp):\n    \"\"\"Read XMP metadata from PDF files.\"\"\"\n    reader = PdfReader(src)\n    xmp = reader.xmp_metadata\n    assert (xmp is None) == (not has_xmp)\n    if has_xmp:\n        for _ in xmp.get_element(\n            about_uri=\"\", namespace=pypdf.xmp.RDF_NAMESPACE, name=\"Artist\"\n        ):\n            pass\n\n        assert get_all_tiff(xmp) == {\"tiff:Artist\": [\"me\"]}\n        assert xmp.dc_contributor == []\n\n\ndef get_all_tiff(xmp: pypdf.xmp.XmpInformation):\n    \"\"\"Return all TIFF metadata as a dictionary.\"\"\"\n    data = {}\n    tiff_ns = xmp.get_nodes_in_namespace(\n        about_uri=\"\", namespace=\"http://ns.adobe.com/tiff/1.0/\"\n    )\n    for tag in tiff_ns:\n        contents = [content.data for content in tag.childNodes]\n        data[tag.tagName] = contents\n    return data\n\n\ndef test_converter_date():\n    \"\"\"\n    _converter_date returns the correct datetime.\n\n    This is a regression test for issue #774.\n    \"\"\"\n    date = pypdf.xmp._converter_date(\"2021-04-28T12:23:34.123Z\")\n    assert date == datetime(2021, 4, 28, 12, 23, 34, 123000)\n\n    with pytest.raises(ValueError) as exc:\n        pypdf.xmp._converter_date(\"today\")\n    assert exc.value.args[0].startswith(\"Invalid date format\")\n\n    date = pypdf.xmp._converter_date(\"2021-04-28T12:23:01-03:00\")\n    assert date == datetime(2021, 4, 28, 15, 23, 1)\n\n\ndef test_modify_date():\n    \"\"\"\n    xmp_modify_date is extracted correctly.\n\n    This is a regression test for issue #914.\n    \"\"\"\n    path = RESOURCE_ROOT / \"issue-914-xmp-data.pdf\"\n    reader = PdfReader(path)\n    assert reader.xmp_metadata.xmp_modify_date == datetime(2022, 4, 9, 15, 22, 43)\n\n\n@pytest.mark.parametrize(\n    \"x\",\n    [\"a\", 42, 3.141, False, True],\n)\ndef test_identity_function(x):\n    \"\"\"The identity is returning its input.\"\"\"\n    assert pypdf.xmp._identity(x) == x\n\n\n@pytest.mark.enable_socket\n@pytest.mark.parametrize(\n    (\"url\", \"name\", \"xmpmm_instance_id\"),\n    [\n        (\n            None,\n            \"tika-955562.pdf\",\n            \"uuid:ca96e032-c2af-49bd-a71c-95889bafbf1d\",\n        )\n    ],\n)\ndef test_xmpmm_instance_id(url, name, xmpmm_instance_id):\n    \"\"\"XMPMM instance id is correctly extracted.\"\"\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    xmp_metadata = reader.xmp_metadata\n    assert xmp_metadata.xmpmm_instance_id == xmpmm_instance_id\n    # cache hit:\n    assert xmp_metadata.xmpmm_instance_id == xmpmm_instance_id\n\n\n@pytest.mark.enable_socket\ndef test_xmp_dc_description_extraction():\n    \"\"\"XMP dc_description is correctly extracted.\"\"\"\n    url = \"https://github.com/user-attachments/files/18381721/tika-953770.pdf\"\n    name = \"tika-953770.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    xmp_metadata = reader.xmp_metadata\n    assert xmp_metadata.dc_description == {\n        \"x-default\": \"U.S. Title 50 Certification Form\"\n    }\n    # cache hit:\n    assert xmp_metadata.dc_description == {\n        \"x-default\": \"U.S. Title 50 Certification Form\"\n    }\n\n\n@pytest.mark.enable_socket\ndef test_dc_creator_extraction():\n    \"\"\"XMP dc_creator is correctly extracted.\"\"\"\n    url = \"https://github.com/user-attachments/files/18381721/tika-953770.pdf\"\n    name = \"tika-953770.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    xmp_metadata = reader.xmp_metadata\n    assert xmp_metadata.dc_creator == [\"U.S. Fish and Wildlife Service\"]\n    # cache hit:\n    assert xmp_metadata.dc_creator == [\"U.S. Fish and Wildlife Service\"]\n\n\n@pytest.mark.enable_socket\ndef test_custom_properties_extraction():\n    \"\"\"XMP custom_properties is correctly extracted.\"\"\"\n    url = \"https://github.com/user-attachments/files/18381764/tika-986065.pdf\"\n    name = \"tika-986065.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    xmp_metadata = reader.xmp_metadata\n    assert xmp_metadata.custom_properties == {\"Style\": \"Searchable Image (Exact)\"}\n    # cache hit:\n    assert xmp_metadata.custom_properties == {\"Style\": \"Searchable Image (Exact)\"}\n\n\n@pytest.mark.enable_socket\ndef test_dc_subject_extraction():\n    \"\"\"XMP dc_subject is correctly extracted.\"\"\"\n    url = \"https://github.com/user-attachments/files/18381730/tika-959519.pdf\"\n    name = \"tika-959519.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    xmp_metadata = reader.xmp_metadata\n    assert xmp_metadata.dc_subject == [\n        \"P&P\",\n        \"manual\",\n        \"1240.2325\",\n        \"CVM\",\n        \"PROCEDURES ON MEDIA INQUIRIES\",\n        \"animal\",\n        \"media\",\n        \"procedures\",\n        \"inquiries\",\n    ]\n    # Cache hit:\n    assert xmp_metadata.dc_subject == [\n        \"P&P\",\n        \"manual\",\n        \"1240.2325\",\n        \"CVM\",\n        \"PROCEDURES ON MEDIA INQUIRIES\",\n        \"animal\",\n        \"media\",\n        \"procedures\",\n        \"inquiries\",\n    ]\n\n\n@pytest.mark.enable_socket\ndef test_invalid_xmp_information_handling():\n    \"\"\"\n    Invalid XML in xmp_metadata is gracefully handled.\n\n    This is a regression test for issue #585.\n    \"\"\"\n    url = \"https://github.com/py-pdf/pypdf/files/5536984/test.pdf\"\n    name = \"pypdf-5536984.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n    with pytest.raises(PdfReadError) as exc:\n        reader.xmp_metadata\n    assert exc.value.args[0].startswith(\"XML in XmpInformation was invalid\")\n\n@pytest.mark.samples\ndef test_pdfa_xmp_metadata_with_values():\n    \"\"\"Test PDF/A XMP metadata extraction from a file with PDF/A metadata.\"\"\"\n    reader = PdfReader(SAMPLE_ROOT / \"021-pdfa\" / \"crazyones-pdfa.pdf\")\n    xmp = reader.xmp_metadata\n\n    assert xmp is not None\n    assert xmp.pdfaid_part == \"1\"\n    assert xmp.pdfaid_conformance == \"B\"\n\n\n@pytest.mark.samples\ndef test_pdfa_xmp_metadata_without_values():\n    \"\"\"Test PDF/A XMP metadata extraction from a file without PDF/A metadata.\"\"\"\n    reader = PdfReader(SAMPLE_ROOT / \"020-xmp\" / \"output_with_metadata_pymupdf.pdf\")\n    xmp = reader.xmp_metadata\n\n    assert xmp is not None\n    assert xmp.pdfaid_part is None\n    assert xmp.pdfaid_conformance is None\n\n\n@pytest.mark.enable_socket\ndef test_xmp_metadata__content_stream_is_dictionary_object():\n    url = \"https://github.com/user-attachments/files/18943249/testing.pdf\"\n    name = \"issue3107.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    with pytest.raises(\n            PdfReadError,\n            match=\"XML in XmpInformation was invalid: 'DictionaryObject' object has no attribute 'get_data'\"\n    ):\n        assert reader.xmp_metadata is not None\n\n\n@pytest.mark.enable_socket\ndef test_dc_creator__bag_instead_of_seq():\n    url = \"https://github.com/user-attachments/files/18381698/tika-924562.pdf\"\n    name = \"tika-924562.pdf\"\n    reader = PdfReader(BytesIO(get_data_from_url(url, name=name)))\n\n    assert reader.xmp_metadata is not None\n    assert reader.xmp_metadata.dc_creator == [\"William J. Hussar\"]\n\n\n@pytest.mark.enable_socket\ndef test_dc_language__no_bag_container():\n    reader = PdfReader(BytesIO(get_data_from_url(name=\"iss2138.pdf\")))\n\n    assert reader.xmp_metadata is not None\n    assert reader.xmp_metadata.dc_language == [\"x-unknown\"]\n\n\ndef test_reading_does_not_destroy_root_object():\n    \"\"\"Test for #3391.\"\"\"\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"commented-xmp.pdf\")\n    xmp = writer.xmp_metadata\n    assert xmp is not None\n    assert not isinstance(writer.root_object[\"/Metadata\"], XmpInformation)\n    assert isinstance(writer.root_object[\"/Metadata\"].get_object(), StreamObject)\n\n    output = BytesIO()\n    writer.write(output)\n    output_bytes = output.getvalue()\n    assert b\"\\n/Metadata 27 0 R\\n\" in output_bytes\n\n\ndef test_xmp_information__write_to_stream():\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"commented-xmp.pdf\")\n    xmp = writer.xmp_metadata\n\n    output = BytesIO()\n    with pytest.warns(\n            DeprecationWarning,\n            match=(\n                r\"^XmpInformation\\.write_to_stream is deprecated and will be removed in pypdf 6\\.0\\.0\\. \"\n                r\"Use PdfWriter\\.xmp_metadata instead\\.$\"\n            )\n    ):\n        xmp.write_to_stream(output)\n    output_bytes = output.getvalue()\n    assert output_bytes.startswith(b\"<<\\n/Type /Metadata\\n/Subtype /XML\\n/Length 2786\\n>>\\nstream\\n<?xpacket begin\")\n\n\ndef test_pdf_writer__xmp_metadata_setter():\n    # Clear existing metadata.\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"commented-xmp.pdf\")\n    assert writer.xmp_metadata is not None\n    original_metadata = writer.xmp_metadata.stream.get_data()\n    writer.xmp_metadata = None\n    output = BytesIO()\n    writer.write(output)\n    output_bytes = output.getvalue()\n    reader = PdfReader(BytesIO(output_bytes))\n    assert reader.xmp_metadata is None\n\n    # Attempt to clear again.\n    writer = PdfWriter(clone_from=reader)\n    assert writer.xmp_metadata is None\n    writer.xmp_metadata = None\n    output = BytesIO()\n    writer.write(output)\n    output_bytes = output.getvalue()\n    reader = PdfReader(BytesIO(output_bytes))\n    assert reader.xmp_metadata is None\n\n    # Set new metadata from bytes.\n    writer = PdfWriter(clone_from=reader)\n    assert writer.xmp_metadata is None\n    writer.xmp_metadata = original_metadata\n    output = BytesIO()\n    writer.write(output)\n    output_bytes = output.getvalue()\n    reader = PdfReader(BytesIO(output_bytes))\n    assert get_all_tiff(reader.xmp_metadata) == {\"tiff:Artist\": [\"me\"]}\n\n    # Set metadata from XmpInformation.\n    writer = PdfWriter(clone_from=reader)\n    xmp_metadata = writer.xmp_metadata\n    assert get_all_tiff(xmp_metadata) == {\"tiff:Artist\": [\"me\"]}\n    new_metadata = original_metadata.replace(b\"<tiff:Artist>me</tiff:Artist>\", b\"<tiff:Artist>Foo Bar</tiff:Artist>\")\n    xmp_metadata.stream.set_data(new_metadata)\n    output = BytesIO()\n    writer.write(output)\n    output_bytes = output.getvalue()\n    reader = PdfReader(BytesIO(output_bytes))\n    assert get_all_tiff(reader.xmp_metadata) == {\"tiff:Artist\": [\"Foo Bar\"]}\n\n    # Fix metadata not being an IndirectObject before.\n    writer = PdfWriter(clone_from=RESOURCE_ROOT / \"commented-xmp.pdf\")\n    writer.root_object[NameObject(\"/Metadata\")] = writer.root_object[\"/Metadata\"].get_object()\n    assert \"/XML\" in str(writer.root_object)\n    writer.xmp_metadata = new_metadata\n    output = BytesIO()\n    writer.write(output)\n    output_bytes = output.getvalue()\n    reader = PdfReader(BytesIO(output_bytes))\n    assert get_all_tiff(reader.xmp_metadata) == {\"tiff:Artist\": [\"Foo Bar\"]}\n    assert \"/XML\" not in str(writer.root_object)\n\n\ndef test_xmp_information__create():\n    \"\"\"Test XmpInformation.create() classmethod.\"\"\"\n    xmp = XmpInformation.create()\n    assert xmp is not None\n    assert xmp.dc_title == {}\n    assert xmp.dc_creator == []\n    assert xmp.dc_description == {}\n    assert xmp.xmp_create_date is None\n    assert xmp.pdf_producer is None\n\n\ndef test_xmp_information__set_dc_title():\n    \"\"\"Test setting dc:title metadata.\"\"\"\n    xmp = XmpInformation.create()\n\n    title_values = {\"x-default\": \"Test Title\", \"en\": \"Test Title EN\"}\n    xmp.dc_title = title_values\n    assert xmp.dc_title == title_values\n\n    xmp.dc_title = None\n    assert xmp.dc_title is None or xmp.dc_title == {}\n\n\ndef test_xmp_information__set_dc_creator():\n    \"\"\"Test setting dc:creator metadata.\"\"\"\n    xmp = XmpInformation.create()\n\n    creators = [\"Author One\", \"Author Two\"]\n    xmp.dc_creator = creators\n    assert xmp.dc_creator == creators\n\n    xmp.dc_creator = None\n    assert xmp.dc_creator is None or xmp.dc_creator == []\n\n\ndef test_xmp_information__set_dc_description():\n    \"\"\"Test setting dc:description metadata.\"\"\"\n    xmp = XmpInformation.create()\n\n    description_values = {\"x-default\": \"Test Description\", \"en\": \"Test Description EN\"}\n    xmp.dc_description = description_values\n    assert xmp.dc_description == description_values\n\n    xmp.dc_description = None\n    assert xmp.dc_description is None or xmp.dc_description == {}\n\n\ndef test_xmp_information__set_dc_subject():\n    \"\"\"Test setting dc:subject metadata.\"\"\"\n    xmp = XmpInformation.create()\n\n    subjects = [\"keyword1\", \"keyword2\", \"keyword3\"]\n    xmp.dc_subject = subjects\n    assert xmp.dc_subject == subjects\n\n    xmp.dc_subject = None\n    assert xmp.dc_subject is None or xmp.dc_subject == []\n\n\ndef test_xmp_information__set_dc_date():\n    \"\"\"Test setting dc:date metadata.\"\"\"\n    xmp = XmpInformation.create()\n\n    test_date = datetime(2023, 12, 25, 10, 30, 45)\n    xmp.dc_date = [test_date]\n    stored_dates = xmp.dc_date\n    assert len(stored_dates) == 1\n\n    date_string = \"2023-12-25T10:30:45.000000Z\"\n    xmp.dc_date = [date_string]\n    stored_dates = xmp.dc_date\n    assert len(stored_dates) == 1\n\n    xmp.dc_date = None\n    assert xmp.dc_date is None or xmp.dc_date == []\n\n\ndef test_xmp_information__set_single_fields():\n    \"\"\"Test setting single-value metadata fields.\"\"\"\n    xmp = XmpInformation.create()\n\n    xmp.dc_coverage = \"Global coverage\"\n    assert xmp.dc_coverage == \"Global coverage\"\n    xmp.dc_coverage = None\n    assert xmp.dc_coverage is None\n\n    xmp.dc_format = \"application/pdf\"\n    assert xmp.dc_format == \"application/pdf\"\n    xmp.dc_format = None\n    assert xmp.dc_format is None\n\n    xmp.dc_identifier = \"unique-id-123\"\n    assert xmp.dc_identifier == \"unique-id-123\"\n    xmp.dc_identifier = None\n    assert xmp.dc_identifier is None\n\n    xmp.dc_source = \"Original Source\"\n    assert xmp.dc_source == \"Original Source\"\n    xmp.dc_source = None\n    assert xmp.dc_source is None\n\n\ndef test_xmp_information__set_bag_fields():\n    \"\"\"Test setting bag (unordered array) metadata fields.\"\"\"\n    xmp = XmpInformation.create()\n\n    contributors = [\"Contributor One\", \"Contributor Two\"]\n    xmp.dc_contributor = contributors\n    assert xmp.dc_contributor == contributors\n    xmp.dc_contributor = None\n    assert xmp.dc_contributor is None or xmp.dc_contributor == []\n\n    languages = [\"en\", \"fr\", \"de\"]\n    xmp.dc_language = languages\n    assert xmp.dc_language == languages\n    xmp.dc_language = None\n    assert xmp.dc_language is None or xmp.dc_language == []\n\n    publishers = [\"Publisher One\", \"Publisher Two\"]\n    xmp.dc_publisher = publishers\n    assert xmp.dc_publisher == publishers\n    xmp.dc_publisher = None\n    assert xmp.dc_publisher is None or xmp.dc_publisher == []\n\n    relations = [\"Related Doc 1\", \"Related Doc 2\"]\n    xmp.dc_relation = relations\n    assert xmp.dc_relation == relations\n    xmp.dc_relation = None\n    assert xmp.dc_relation is None or xmp.dc_relation == []\n\n    types = [\"Document\", \"Text\"]\n    xmp.dc_type = types\n    assert xmp.dc_type == types\n    xmp.dc_type = None\n    assert xmp.dc_type is None or xmp.dc_type == []\n\n\ndef test_xmp_information__set_dc_rights():\n    \"\"\"Test setting dc:rights metadata.\"\"\"\n    xmp = XmpInformation.create()\n\n    rights_values = {\"x-default\": \"All rights reserved\", \"en\": \"All rights reserved EN\"}\n    xmp.dc_rights = rights_values\n    assert xmp.dc_rights == rights_values\n\n    xmp.dc_rights = None\n    assert xmp.dc_rights is None or xmp.dc_rights == {}\n\n\ndef test_xmp_information__set_pdf_fields():\n    \"\"\"Test setting PDF namespace metadata fields.\"\"\"\n    xmp = XmpInformation.create()\n\n    xmp.pdf_keywords = \"keyword1, keyword2, keyword3\"\n    assert xmp.pdf_keywords == \"keyword1, keyword2, keyword3\"\n    xmp.pdf_keywords = None\n    assert xmp.pdf_keywords is None\n\n    xmp.pdf_pdfversion = \"1.4\"\n    assert xmp.pdf_pdfversion == \"1.4\"\n    xmp.pdf_pdfversion = None\n    assert xmp.pdf_pdfversion is None\n\n    xmp.pdf_producer = \"pypdf\"\n    assert xmp.pdf_producer == \"pypdf\"\n    xmp.pdf_producer = None\n    assert xmp.pdf_producer is None\n\n\ndef test_xmp_information__set_xmp_date_fields():\n    \"\"\"Test setting XMP date metadata fields.\"\"\"\n    xmp = XmpInformation.create()\n    test_date = datetime(2023, 12, 25, 10, 30, 45)\n    aware_date = datetime(2023, 1, 1, 12, 0, 0, tzinfo=timezone(timedelta(hours=-5)))\n\n    xmp.xmp_create_date = test_date\n    stored_date = xmp.xmp_create_date\n    assert isinstance(stored_date, datetime)\n    xmp.xmp_create_date = aware_date\n    stored_date = xmp.xmp_create_date\n    assert stored_date == datetime(2023, 1, 1, 17, 0, 0)\n    xmp.xmp_create_date = None\n    assert xmp.xmp_create_date is None\n\n    xmp.xmp_modify_date = test_date\n    stored_date = xmp.xmp_modify_date\n    assert isinstance(stored_date, datetime)\n    xmp.xmp_modify_date = aware_date\n    stored_date = xmp.xmp_modify_date\n    assert stored_date == datetime(2023, 1, 1, 17, 0, 0)\n    xmp.xmp_modify_date = None\n    assert xmp.xmp_modify_date is None\n\n    xmp.xmp_metadata_date = test_date\n    stored_date = xmp.xmp_metadata_date\n    assert isinstance(stored_date, datetime)\n    xmp.xmp_metadata_date = aware_date\n    stored_date = xmp.xmp_metadata_date\n    assert stored_date == datetime(2023, 1, 1, 17, 0, 0)\n    xmp.xmp_metadata_date = None\n    assert xmp.xmp_metadata_date is None\n\n\ndef test_xmp_information__set_xmp_creator_tool():\n    \"\"\"Test setting xmp:CreatorTool metadata.\"\"\"\n    xmp = XmpInformation.create()\n\n    xmp.xmp_creator_tool = \"pypdf\"\n    assert xmp.xmp_creator_tool == \"pypdf\"\n    xmp.xmp_creator_tool = None\n    assert xmp.xmp_creator_tool is None\n\n\ndef test_xmp_information__set_xmpmm_fields():\n    \"\"\"Test setting XMPMM namespace metadata fields.\"\"\"\n    xmp = XmpInformation.create()\n\n    doc_id = \"uuid:12345678-1234-1234-1234-123456789abc\"\n    xmp.xmpmm_document_id = doc_id\n    assert xmp.xmpmm_document_id == doc_id\n    xmp.xmpmm_document_id = None\n    assert xmp.xmpmm_document_id is None\n\n    instance_id = \"uuid:87654321-4321-4321-4321-cba987654321\"\n    xmp.xmpmm_instance_id = instance_id\n    assert xmp.xmpmm_instance_id == instance_id\n    xmp.xmpmm_instance_id = None\n    assert xmp.xmpmm_instance_id is None\n\n\ndef test_xmp_information__set_pdfaid_fields():\n    \"\"\"Test setting PDF/A ID namespace metadata fields.\"\"\"\n    xmp = XmpInformation.create()\n\n    xmp.pdfaid_part = \"1\"\n    assert xmp.pdfaid_part == \"1\"\n    xmp.pdfaid_part = None\n    assert xmp.pdfaid_part is None\n\n    xmp.pdfaid_conformance = \"B\"\n    assert xmp.pdfaid_conformance == \"B\"\n    xmp.pdfaid_conformance = None\n    assert xmp.pdfaid_conformance is None\n\n\ndef test_xmp_information__create_with_writer():\n    \"\"\"Test using XmpInformation.create() with PdfWriter.\"\"\"\n    xmp = XmpInformation.create()\n    xmp.dc_title = {\"x-default\": \"Created with pypdf\"}\n    xmp.dc_creator = [\"pypdf user\"]\n    xmp.pdf_producer = \"pypdf library\"\n\n    writer = PdfWriter()\n    writer.add_blank_page(612, 792)\n    writer.xmp_metadata = xmp\n\n    output = BytesIO()\n    writer.write(output)\n    output_bytes = output.getvalue()\n\n    reader = PdfReader(BytesIO(output_bytes))\n    xmp_read = reader.xmp_metadata\n    assert xmp_read is not None\n    assert xmp_read.dc_title == {\"x-default\": \"Created with pypdf\"}\n    assert xmp_read.dc_creator == [\"pypdf user\"]\n    assert xmp_read.pdf_producer == \"pypdf library\"\n\n\ndef test_xmp_information__namespace_prefix():\n    \"\"\"Test _get_namespace_prefix method.\"\"\"\n    xmp = XmpInformation.create()\n\n    assert xmp._get_namespace_prefix(pypdf.xmp.DC_NAMESPACE) == \"dc\"\n    assert xmp._get_namespace_prefix(pypdf.xmp.XMP_NAMESPACE) == \"xmp\"\n    assert xmp._get_namespace_prefix(pypdf.xmp.PDF_NAMESPACE) == \"pdf\"\n    assert xmp._get_namespace_prefix(pypdf.xmp.XMPMM_NAMESPACE) == \"xmpMM\"\n    assert xmp._get_namespace_prefix(pypdf.xmp.PDFAID_NAMESPACE) == \"pdfaid\"\n    assert xmp._get_namespace_prefix(pypdf.xmp.PDFX_NAMESPACE) == \"pdfx\"\n    assert xmp._get_namespace_prefix(\"unknown://namespace\") == \"unknown\"\n\n\ndef test_xmp_information__owner_document_none_errors():\n    xmp = XmpInformation.create()\n\n    original_owner = xmp.rdf_root.ownerDocument\n\n    try:\n        for desc in list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\")):\n            xmp.rdf_root.removeChild(desc)\n\n        xmp.rdf_root.ownerDocument = None\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp._get_or_create_description()\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp._update_stream()\n\n        xmp.rdf_root.ownerDocument = original_owner\n        for desc in list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\")):\n            xmp.rdf_root.removeChild(desc)\n        xmp.rdf_root.ownerDocument = None\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp.dc_coverage = \"test coverage\"\n\n        xmp.rdf_root.ownerDocument = original_owner\n        for desc in list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\")):\n            xmp.rdf_root.removeChild(desc)\n        xmp.rdf_root.ownerDocument = None\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp.dc_contributor = [\"contributor\"]\n\n        xmp.rdf_root.ownerDocument = original_owner\n        for desc in list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\")):\n            xmp.rdf_root.removeChild(desc)\n        xmp.rdf_root.ownerDocument = None\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp.dc_creator = [\"creator\"]\n\n        xmp.rdf_root.ownerDocument = original_owner\n        for desc in list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\")):\n            xmp.rdf_root.removeChild(desc)\n        xmp.rdf_root.ownerDocument = None\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp.dc_title = {\"x-default\": \"title\"}\n\n        xmp.rdf_root.ownerDocument = original_owner\n        desc = xmp._get_or_create_description()\n        desc.setAttribute(\"test-attr\", \"test-value\")\n        xmp.rdf_root.ownerDocument = None\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp._set_single_value(\"test-namespace\", \"test-attr\", \"new-value\")\n\n        xmp.rdf_root.ownerDocument = original_owner\n        desc = xmp._get_or_create_description()\n        xmp.rdf_root.ownerDocument = None\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp._set_bag_values(\"test-namespace\", \"test-name\", [\"value\"])\n\n        xmp.rdf_root.ownerDocument = original_owner\n        desc = xmp._get_or_create_description()\n        xmp.rdf_root.ownerDocument = None\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp._set_seq_values(\"test-namespace\", \"test-name\", [\"value\"])\n\n        xmp.rdf_root.ownerDocument = original_owner\n        desc = xmp._get_or_create_description()\n        xmp.rdf_root.ownerDocument = None\n\n        with pytest.raises(XmpDocumentError, match=\"XMP Document is None\"):\n            xmp._set_langalt_values(\"test-namespace\", \"test-name\", {\"x-default\": \"value\"})\n\n    finally:\n        xmp.rdf_root.ownerDocument = original_owner\n\n\ndef test_xmp_information__remove_existing_attribute():\n    xmp = XmpInformation.create()\n\n    xmp.dc_coverage = \"initial coverage\"\n    assert xmp.dc_coverage == \"initial coverage\"\n\n    xmp.dc_coverage = \"updated coverage\"\n    assert xmp.dc_coverage == \"updated coverage\"\n\n    xmp.dc_coverage = None\n    assert xmp.dc_coverage is None\n\n    desc = xmp._get_or_create_description()\n    desc.setAttributeNS(pypdf.xmp.DC_NAMESPACE, \"dc:coverage\", \"original attribute\")\n\n    assert desc.getAttributeNS(pypdf.xmp.DC_NAMESPACE, \"coverage\") == \"original attribute\"\n\n    xmp.dc_coverage = \"new element value\"\n    assert xmp.dc_coverage == \"new element value\"\n\n    assert desc.getAttributeNS(pypdf.xmp.DC_NAMESPACE, \"coverage\") == \"\"\n\n    elements = desc.getElementsByTagNameNS(pypdf.xmp.DC_NAMESPACE, \"coverage\")\n    assert len(elements) == 1\n    assert elements[0].firstChild.data == \"new element value\"\n\n\ndef test_xmp_information__edge_case_coverage():\n    xmp = XmpInformation.create()\n\n    xmp.dc_contributor = []\n    assert xmp.dc_contributor == []\n\n    xmp.dc_creator = []\n    assert xmp.dc_creator == []\n\n    xmp.dc_title = {}\n    assert xmp.dc_title == {}\n\n    xmp.dc_contributor = None\n    assert xmp.dc_contributor == []\n\n    xmp.dc_creator = None\n    assert xmp.dc_creator == []\n\n    xmp.dc_title = None\n    assert xmp.dc_title == {}\n\n\ndef test_xmp_information__create_new_description():\n    \"\"\"Test creating new description elements.\"\"\"\n    xmp = XmpInformation.create()\n\n    for desc in list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\")):\n        xmp.rdf_root.removeChild(desc)\n\n    desc = xmp._get_or_create_description(\"test-uri\")\n    assert desc.getAttributeNS(pypdf.xmp.RDF_NAMESPACE, \"about\") == \"test-uri\"\n\n    assert desc.tagName == \"rdf:Description\"\n    assert desc.namespaceURI == pypdf.xmp.RDF_NAMESPACE\n\n\ndef test_xmp_information__get_text_skips_non_text_nodes():\n    xmp = XmpInformation.create()\n\n    doc = xmp.rdf_root.ownerDocument\n    el = doc.createElementNS(pypdf.xmp.DC_NAMESPACE, \"dc:test\")\n    el.appendChild(doc.createTextNode(\"hello\"))\n    el.appendChild(doc.createElement(\"ignored-node\"))\n    el.appendChild(doc.createTextNode(\" world\"))\n\n    assert xmp._get_text(el) == \"hello world\"\n\n\ndef test_xmp_information__get_or_create_description_mismatch_about_uri():\n    xmp = XmpInformation.create()\n\n    existing = xmp._get_or_create_description()\n    existing.setAttributeNS(pypdf.xmp.RDF_NAMESPACE, \"rdf:about\", \"foo-uri\")\n\n    new_desc = xmp._get_or_create_description(\"bar-uri\")\n    assert new_desc is not existing\n    assert new_desc.getAttributeNS(pypdf.xmp.RDF_NAMESPACE, \"about\") == \"bar-uri\"\n\n    all_desc = list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\"))\n    about_values = {d.getAttributeNS(pypdf.xmp.RDF_NAMESPACE, \"about\") for d in all_desc}\n    assert {\"foo-uri\", \"bar-uri\"}.issubset(about_values)\n\n\ndef test_xmp_information__attribute_handling():\n    \"\"\"Test attribute node removal and creation (line 479, 484, 506, 535, 564).\"\"\"\n    xmp = XmpInformation.create()\n\n    for desc in list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\")):\n        xmp.rdf_root.removeChild(desc)\n\n    xmp.dc_coverage = \"test coverage\"\n    assert xmp.dc_coverage == \"test coverage\"\n\n    xmp.dc_contributor = [\"contributor1\", \"contributor2\"]\n    assert xmp.dc_contributor == [\"contributor1\", \"contributor2\"]\n\n    xmp.dc_creator = [\"creator1\", \"creator2\"]\n    assert xmp.dc_creator == [\"creator1\", \"creator2\"]\n\n    xmp.dc_title = {\"x-default\": \"Test Title\", \"en\": \"Test Title EN\"}\n    assert xmp.dc_title == {\"x-default\": \"Test Title\", \"en\": \"Test Title EN\"}\n\n    xmp.dc_format = \"application/pdf\"\n    assert xmp.dc_format == \"application/pdf\"\n\n    xmp.dc_format = \"text/plain\"\n    assert xmp.dc_format == \"text/plain\"\n\n\ndef test_xmp_information__create_and_set_metadata():\n    xmp = XmpInformation.create()\n\n    for desc in list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\")):\n        xmp.rdf_root.removeChild(desc)\n\n    desc = xmp._get_or_create_description()\n    desc.setAttribute(\"test\", \"value\")\n    xmp.dc_source = \"original\"\n    xmp.dc_source = \"modified\"\n    assert xmp.dc_source == \"modified\"\n\n    for desc in list(xmp.rdf_root.getElementsByTagNameNS(pypdf.xmp.RDF_NAMESPACE, \"Description\")):\n        xmp.rdf_root.removeChild(desc)\n\n    xmp.dc_contributor = [\"test1\"]\n    xmp.dc_creator = [\"test2\"]\n    xmp.dc_title = {\"x-default\": \"test3\"}\n\n    assert xmp.dc_contributor == [\"test1\"]\n    assert xmp.dc_creator == [\"test2\"]\n    assert xmp.dc_title == {\"x-default\": \"test3\"}\n\n\ndef test_xmp_information__external_entity_expansion(tmpdir):\n    path = tmpdir / \"secret.txt\"\n    path.write(\"VERY SECRET\")\n\n    stream = ContentStream(pdf=None, stream=None)\n    stream.set_data(f\"\"\"<?xml version=\"1.0\"?>\n<!DOCTYPE foo [\n  <!ENTITY xxe SYSTEM \"file://{path}\">\n]>\n<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n  <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n    <rdf:Description rdf:about=\"\">\n      <dc:creator xmlns:dc=\"http://purl.org/dc/elements/1.1/\">&xxe;abc</dc:creator>\n    </rdf:Description>\n  </rdf:RDF>\n</x:xmpmeta>\"\"\".encode())\n\n    xmp = XmpInformation(stream)\n    assert xmp.dc_creator == [\"abc\"]\n\n\n@pytest.mark.timeout(10)\ndef test_xmp_information__exponential_entity_expansion():\n    stream = ContentStream(pdf=None, stream=None)\n    stream.set_data(b\"\"\"<?xml version=\"1.0\"?>\n<!DOCTYPE lolz [\n  <!ENTITY lol \"lol\">\n  <!ENTITY lol2 \"&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;&lol;\">\n  <!ENTITY lol3 \"&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;&lol2;\">\n  <!ENTITY lol4 \"&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;&lol3;\">\n  <!ENTITY lol5 \"&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;&lol4;\">\n  <!ENTITY lol6 \"&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;&lol5;\">\n  <!ENTITY lol7 \"&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;&lol6;\">\n  <!ENTITY lol8 \"&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;&lol7;\">\n  <!ENTITY lol9 \"&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;&lol8;\">\n]>\n<x:xmpmeta xmlns:x=\"adobe:ns:meta/\">\n  <rdf:RDF xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\">\n    <rdf:Description rdf:about=\"\">\n      <dc:title xmlns:dc=\"http://purl.org/dc/elements/1.1/\">&lol9;</dc:title>\n    </rdf:Description>\n  </rdf:RDF>\n</x:xmpmeta>\"\"\")\n\n    with pytest.raises(\n            expected_exception=PdfReadError,\n            match=(\n                r\"^XML in XmpInformation was invalid: limit on input amplification factor \"\n                r\"\\(from DTD and entities\\) breached: line 16, column 60$\"\n            )\n    ):\n        XmpInformation(stream)\n"
  },
  {
    "path": "tests/utils.py",
    "content": "\"\"\"Utility functions and classes for testing.\"\"\"\nimport logging\nfrom typing import Union\n\nfrom PIL import Image\n\nfrom pypdf import PageObject\nfrom pypdf.generic import DictionaryObject, IndirectObject\n\n\nclass PositionedText:\n    \"\"\"\n    Specify a text with coordinates, font-dictionary and font-size.\n\n    The font-dictionary may be None in case of an unknown font.\n    \"\"\"\n\n    def __init__(self, text, x, y, font_dict, font_size) -> None:\n        # TODO: \\0-replace: Encoding issue in some files?\n        self.text = text.replace(\"\\0\", \"\")\n        self.x = x\n        self.y = y\n        self.font_dict = font_dict\n        self.font_size = font_size\n\n    def get_base_font(self) -> str:\n        \"\"\"\n        Gets the base font of the text.\n\n        Return UNKNOWN in case of an unknown font.\n        \"\"\"\n        if (self.font_dict is None) or \"/BaseFont\" not in self.font_dict:\n            return \"UNKNOWN\"\n        return self.font_dict[\"/BaseFont\"]\n\n\nclass Rectangle:\n    \"\"\"Specify a rectangle.\"\"\"\n\n    def __init__(self, x, y, w, h) -> None:\n        self.x = x.as_numeric()\n        self.y = y.as_numeric()\n        self.w = w.as_numeric()\n        self.h = h.as_numeric()\n\n    def contains(self, x, y) -> bool:\n        return (\n                self.x <= x <= (self.x + self.w)\n                and self.y <= y <= (self.y + self.h)\n        )\n\n\ndef extract_text_and_rectangles(\n        page: PageObject, rect_filter=None\n) -> tuple[list[PositionedText], list[Rectangle]]:\n    \"\"\"\n    Extracts texts and rectangles of a page of type pypdf._page.PageObject.\n\n    This function supports simple coordinate transformations only.\n    The optional rect_filter-lambda can be used to filter wanted\n    rectangles.\n    rect_filter has Rectangle as argument and must return a boolean.\n\n    It returns a tuple containing a list of extracted texts and\n    a list of extracted rectangles.\n    \"\"\"\n    logger = logging.getLogger(\"extract_text_and_rectangles\")\n\n    rectangles = []\n    texts = []\n\n    def print_op_b(op, args, cm_matrix, tm_matrix) -> None:\n        if logger.isEnabledFor(logging.DEBUG):\n            logger.debug(f\"before: {op} at {cm_matrix}, {tm_matrix}\")\n        if op == b\"re\":\n            if logger.isEnabledFor(logging.DEBUG):\n                logger.debug(f\"  add rectangle: {args}\")\n            w = args[2]\n            h = args[3]\n            r = Rectangle(args[0], args[1], w, h)\n            if (rect_filter is None) or rect_filter(r):\n                rectangles.append(r)\n\n    def print_visi(text, cm_matrix, tm_matrix, font_dict, font_size) -> None:\n        if text.strip() != \"\":\n            if logger.isEnabledFor(logging.DEBUG):\n                logger.debug(f\"at {cm_matrix}, {tm_matrix}, font size={font_size}\")\n            texts.append(\n                PositionedText(\n                    text, tm_matrix[4], tm_matrix[5], font_dict, font_size\n                )\n            )\n\n    visitor_before = print_op_b\n    visitor_text = print_visi\n\n    page.extract_text(\n        visitor_operand_before=visitor_before, visitor_text=visitor_text\n    )\n\n    return texts, rectangles\n\n\ndef extract_table(\n        texts: list[PositionedText], rectangles: list[Rectangle]\n) -> list[list[list[PositionedText]]]:\n    \"\"\"\n    Extracts a table containing text.\n\n    It is expected that each cell is marked by a rectangle-object.\n    It is expected that the page contains one table only.\n    It is expected that the table contains at least 3 columns and 2 rows.\n\n    A list of rows is returned.\n    Each row contains a list of cells.\n    Each cell contains a list of PositionedText-elements.\n    \"\"\"\n    logger = logging.getLogger(\"extractTable\")\n\n    # Step 1: Count number of x- and y-coordinates of rectangles.\n    # Remove duplicate rectangles. The new list is rectangles_filtered.\n    col2count = {}\n    row2count = {}\n    key2rectangle = {}\n    rectangles_filtered = []\n    for r in rectangles:\n        # Coordinates may be inaccurate, we have to round.\n        # cell: x=72.264, y=386.57, w=93.96, h=46.584\n        # cell: x=72.271, y=386.56, w=93.96, h=46.59\n        key = f\"{round(r.x, 0)} {round(r.y, 0)} {round(r.w, 0)} {round(r.h, 0)}\"\n        if key in key2rectangle:\n            # Ignore duplicate rectangles\n            continue\n        key2rectangle[key] = r\n        if r.x not in col2count:\n            col2count[r.x] = 0\n        if r.y not in row2count:\n            row2count[r.y] = 0\n        col2count[r.x] += 1\n        row2count[r.y] += 1\n        rectangles_filtered.append(r)\n\n    # Step 2: Look for texts in rectangles.\n    rectangle2texts = {}\n    for text in texts:\n        for r in rectangles_filtered:\n            if r.contains(text.x, text.y):\n                if r not in rectangle2texts:\n                    rectangle2texts[r] = []\n                rectangle2texts[r].append(text)\n                break\n\n    # PDF: y = 0 is expected at the bottom of the page.\n    # So the header-row is expected to have the highest y-value.\n    rectangles.sort(key=lambda r: (-r.y, r.x))\n\n    # Step 3: Build the list of rows containing list of cell-texts.\n    rows = []\n    row_nr = 0\n    col_nr = 0\n    curr_y = None\n    curr_row = None\n    for r in rectangles_filtered:\n        if col2count[r.x] < 3 or row2count[r.y] < 2:\n            # We expect at least 3 columns and 2 rows.\n            continue\n        if curr_y is None or r.y != curr_y:\n            # next row\n            curr_y = r.y\n            col_nr = 0\n            row_nr += 1\n            curr_row = []\n            rows.append(curr_row)\n        col_nr += 1\n        if logger.isEnabledFor(logging.DEBUG):\n            logger.debug(f\"cell: x={r.x}, y={r.y}, w={r.w}, h={r.h}\")\n        if r not in rectangle2texts:\n            curr_row.append(\"\")\n            continue\n        cell_texts = list(rectangle2texts[r])\n        curr_row.append(cell_texts)\n\n    return rows\n\n\ndef extract_cell_text(cell_texts: list[PositionedText]) -> str:\n    \"\"\"Joins the text-objects of a cell.\"\"\"\n    return (\"\".join(t.text for t in cell_texts)).strip()\n\n\ndef get_image_data(\n        image: Image.Image, band: Union[int, None] = None\n) -> Union[tuple[tuple[int, ...], ...], tuple[float, ...]]:\n    try:\n        return image.get_flattened_data(band=band)\n    except AttributeError:\n        # For Pillow < 12.1.0\n        return tuple(image.getdata(band=band))\n\n\nclass ReaderDummy:\n    def __init__(self, strict=False) -> None:\n        self.strict = strict\n\n    def get_object(self, indirect_reference):\n        class DummyObj:\n            def get_object(self) -> \"DummyObj\":\n                return self\n\n        return DictionaryObject()\n\n    def get_reference(self, obj):\n        return IndirectObject(idnum=1, generation=1, pdf=self)\n"
  }
]