{"id":13593561,"url":"https://github.com/pypdfium2-team/pypdfium2","last_synced_at":"2026-01-05T18:13:37.862Z","repository":{"id":37487370,"uuid":"420412416","full_name":"pypdfium2-team/pypdfium2","owner":"pypdfium2-team","description":"Python bindings to PDFium","archived":false,"fork":false,"pushed_at":"2025-04-09T02:18:39.000Z","size":2765,"stargazers_count":553,"open_issues_count":5,"forks_count":24,"subscribers_count":8,"default_branch":"main","last_synced_at":"2025-04-09T03:19:45.348Z","etag":null,"topics":["pdf","pdf-documents","pdf-to-image","pdfium","python","rasterisation"],"latest_commit_sha":null,"homepage":"https://pypdfium2.readthedocs.io/","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/pypdfium2-team.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSES/Apache-2.0.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-10-23T12:55:36.000Z","updated_at":"2025-04-08T15:04:27.000Z","dependencies_parsed_at":"2023-09-24T14:11:00.680Z","dependency_job_id":"21019823-2f0b-424c-ac26-77e9757d648e","html_url":"https://github.com/pypdfium2-team/pypdfium2","commit_stats":null,"previous_names":[],"tags_count":99,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pypdfium2-team%2Fpypdfium2","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pypdfium2-team%2Fpypdfium2/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pypdfium2-team%2Fpypdfium2/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/pypdfium2-team%2Fpypdfium2/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/pypdfium2-team","download_url":"https://codeload.github.com/pypdfium2-team/pypdfium2/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247986751,"owners_count":21028886,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["pdf","pdf-documents","pdf-to-image","pdfium","python","rasterisation"],"created_at":"2024-08-01T16:01:21.595Z","updated_at":"2026-01-05T18:13:37.830Z","avatar_url":"https://github.com/pypdfium2-team.png","language":"Python","funding_links":[],"categories":["Python","Multi-Purpose Libraries"],"sub_categories":[],"readme":"\u003c!-- SPDX-FileCopyrightText: 2025 geisserml \u003cgeisserml@gmail.com\u003e --\u003e\n\u003c!-- SPDX-License-Identifier: CC-BY-4.0 --\u003e\n\n# pypdfium2\n\n[![Downloads](https://pepy.tech/badge/pypdfium2/month)](https://pepy.tech/project/pypdfium2)\n\n[pypdfium2](https://github.com/pypdfium2-team/pypdfium2) is an [ABI-level](#drawbacks-of-abi-level-bindings) Python 3 binding to [PDFium](https://pdfium.googlesource.com/pdfium/+/refs/heads/main), a powerful and liberal-licensed library for PDF rendering, inspection, manipulation and creation.\n\nIt is built with [ctypesgen](https://github.com/pypdfium2-team/ctypesgen) and external [PDFium binaries](https://github.com/bblanchon/pdfium-binaries/).\nThe custom setup infrastructure provides a seamless packaging and installation process. A wide range of platforms is supported with pre-built packages.\n\npypdfium2 includes helpers to simplify common use cases, while the raw PDFium/ctypes API remains accessible as well.\n\n\n## Installation\n\n\u003c!-- FIXME is there a better/universal way to create linkable sub-sections without them being a heading? Note, the current solution is specific to GH and doesn't work on RTD. --\u003e\n\n* \u003ca id=\"user-content-install-pypi\" class=\"anchor\" href=\"#install-pypi\"\u003eFrom PyPI 🔗\u003c/a\u003e (recommended)\n  ```bash\n  python -m pip install -U pypdfium2\n  ```\n  This will use a pre-built wheel package, the easiest way of installing pypdfium2.\n\n\n* \u003ca id=\"user-content-install-source\" class=\"anchor\" href=\"#install-source\"\u003eFrom source 🔗\u003c/a\u003e\n  \n  * Dependencies:\n    - System: git, C pre-processor (gcc/clang - alternatively, specify the command to invoke via `$CPP`)\n    - Python: ctypesgen (pypdfium2-team fork), wheel, setuptools. Usually installed automatically.\n  \n  * Get the code\n    ```\n    git clone \"https://github.com/pypdfium2-team/pypdfium2.git\"\n    cd pypdfium2/\n    ```\n  \n  * \u003ca id=\"user-content-install-source-default\" class=\"anchor\" href=\"#install-source-default\"\u003eWith pre-built binary 🔗\u003c/a\u003e\n    ```bash\n    # In the pypdfium2/ directory\n    python -m pip install -v .\n    ```\n    A binary is downloaded implicitly from `pdfium-binaries` and bundled into pypdfium2.\n  \n  * \u003ca id=\"user-content-install-source-selfbuilt\" class=\"anchor\" href=\"#install-source-selfbuilt\"\u003eWith self-built binary 🔗\u003c/a\u003e\n    ```bash\n    # call build script with --help to list options\n    python setupsrc/pypdfium2_setup/sourcebuild.py\n    PDFIUM_PLATFORM=\"sourcebuild\" python -m pip install -v .\n    ```\n    Building PDFium may take a long time, as it comes with its bundled toolchain and deps, rather than taking them from the system.[^pdfium_buildsystem]\n    However, we can at least provide the `--use-syslibs` option to build against system runtime libraries.\n  \n  * \u003ca id=\"user-content-install-source-system\" class=\"anchor\" href=\"#install-source-system\"\u003eWith system-level binary 🔗\u003c/a\u003e\n    ```bash\n    # Substitute $PDFIUM_VER with the system pdfium build's version.\n    # For ABI safety reasons, you'll want to make sure `$PDFIUM_VER` is correct and the bindings are rebuilt whenever system pdfium is updated.\n    PDFIUM_PLATFORM=\"system:$PDFIUM_VER\" python -m pip install -v .\n    ```\n    Link against external pdfium instead of bundling it.\n    Note, this is basically a high-level convenience entry point to internal bindings generation, and intended for end users. Therefore it is less flexible, supporting only the \"simple case\" for now.\n    For more sohpisticated use cases that need passing custom parameters to ctypesgen (e.g. runtime libdirs / headers / feature flags), consider [caller-provided data files](#install-source-caller).\n\n  * \u003ca id=\"user-content-install-source-libreoffice\" class=\"anchor\" href=\"#install-source-libreoffice\"\u003eWith system-level binary (non-standard location, e.g. LibreOffice) 🔗\u003c/a\u003e\n    ```bash\n    # if root rights are available and targeting /usr/local/lib is OK\n    sudo ln -s /usr/lib/libreoffice/program/libpdfiumlo.so /usr/local/lib/libpdfium.so\n    # Substitute $PDFIUM_VER with the pdfium build's version.\n    PDFIUM_PLATFORM=\"system:$PDFIUM_VER\" python -m pip install -v .\n    ```\n    \n    Symlink pdfium from a non-standard location (e.g. libreoffice libdir) to a directory that is on the search path, determine the version, and install with system pdfium [as described above](#install-source-system).\n    \n    Note, if elevated privileges are not available, you can target e.g. `~/.local/lib` and add it to [`LD_LIBRARY_PATH`](https://docs.python.org/3/library/ctypes.html#finding-shared-libraries) in your `~/.bashrc` file.\n  \n  \u003c!-- TODO version.json: reconsider origin - should we use a separate field for the packager? --\u003e\n  * \u003ca id=\"user-content-install-source-caller\" class=\"anchor\" href=\"#install-source-caller\"\u003eWith caller-provided data files 🔗\u003c/a\u003e (this is expected to work offline)\n    ```bash\n    # Call ctypesgen (see --help or base.py::run_ctypesgen() for further options)\n    # Reminder: you'll want to use the pypdfium2-team fork of ctypesgen\n    ctypesgen --library pdfium --runtime-libdirs $MY_LIBDIRS --headers $MY_INCLUDE_DIR/fpdf*.h -o src/pypdfium2_raw/bindings.py [-D $MY_FLAGS]\n    \n    # Write the version file (fill the placeholders).\n    # See https://pypdfium2.readthedocs.io/en/stable/python_api.html#pypdfium2.version.PDFIUM_INFO for field documentation\n    # Note, this is not a mature interface yet and might change any time!\n    # major/minor/build/patch: integers forming the pdfium version being packaged\n    # n_commits/hash: git describe like post-tag info (0/null for release commit)\n    # origin: a string to identify the build, in the form `$BUILDER`, `$DISTNAME/$BUILDER`, `system/$BUILDER` or `system/$DISTNAME/$BUILDER`. (Use the `$DISTNAME/$BUILDER` form if you are a distribution maintainer re-packaging another builder's binaries. Add the `system` prefix if the binary is loaded from a system path rather than bundled with pypdfium2.)\n    # flags: a comma-delimited list of pdfium feature flag strings (e.g. \"V8\", \"XFA\") - may be empty for default build\n    cat \u003e \"src/pypdfium2_raw/version.json\" \u003c\u003cEND\n    {\n      \"major\": $PDFIUM_MAJOR,\n      \"minor\": $PDFIUM_MINOR,\n      \"build\": $PDFIUM_BUILD,\n      \"patch\": $PDFIUM_PATCH,\n      \"n_commits\": $POST_TAG_COMMIT_COUNT,\n      \"hash\": $POST_TAG_HASH,\n      \"origin\": \"$ORIGIN\",\n      \"flags\": [$MY_FLAGS]\n    }\n    END\n    \n    # optional: copy in a binary if bundling\n    cp \"$BINARY_PATH\" src/pypdfium2_raw/libpdfium.so\n    \n    # Finally, install\n    # set $MY_PLATFORM to \"system\" if building against system pdfium (not bundled), \"auto\", \"sourcebuild\" or the platform name otherwise.\n    PDFIUM_PLATFORM='prepared!$MY_PLATFORM:$PDFIUM_BUILD' python -m pip install --no-build-isolation -v .\n    ```\n  \n  See [Setup Magic](#setup-magic) for details.\n  \n  Installing an `sdist` does not implicitly trigger a sourcebuild if no pre-built binary is available. We prefer to let callers decide consciously what to do, and run the build script without pip encapsulation.\n  \n  Relevant pip options:\n  * `-v`: Verbose logging output. Useful for debugging.\n  * `-e`: Install in editable mode, so the installation points to the source tree. This way, changes directly take effect without needing to re-install. Recommended for development.\n  * `--no-build-isolation`: Do not isolate setup in a virtual env; use the main env instead. This renders `pyproject.toml [build-system]` inactive, so setup deps must be prepared by caller. Useful to install custom versions of setup deps, or as speedup when installing repeatedly.\n  \n  That said, do not expect us to provide much guidance with source installs, or to support the result, as this may be a crafty process, and we can't be sure whether it was done correctly (e.g. ABI safety, ctypesgen version used, etc.).\n  \n  [^pdfium_buildsystem]: This means pdfium may not compile on arbitrary hosts. The script is limited to build hosts supported by Google's toolchain. Ideally, we'd need an alternative build system that runs with system packages instead.\n\n\n* \u003ca id=\"user-content-install-conda\" class=\"anchor\" href=\"#install-conda\"\u003eFrom Conda 🔗\u003c/a\u003e\n  \n  _**Beware:** Any conda packages/recipes of pypdfium2 or pdfium-binaries that might be provided by other distributors, including `anaconda/main` or `conda-forge` default channels, are [unofficial](#install-unofficial)._\n  \n  + To install\n    \n    With permanent channel config (encouraged):\n    ```bash\n    conda config --add channels bblanchon\n    conda config --add channels pypdfium2-team\n    conda config --set channel_priority strict\n    conda install pypdfium2-team::pypdfium2_helpers\n    ```\n    \n    Alternatively, with temporary channel config:\n    ```bash\n    conda install pypdfium2-team::pypdfium2_helpers --override-channels -c pypdfium2-team -c bblanchon -c defaults\n    ```\n    \n    If desired, you may limit the channel config to the current environment by adding `--env`.\n    Adding the channels permanently and tightening priority is encouraged to include pypdfium2 in `conda update` by default, and to avoid accidentally replacing the install with a different channel.\n    Otherwise, you should be cautious when making changes to the environment.\n  \n  + To depend on pypdfium2 in a `conda-build` recipe\n    ```yaml\n    requirements:\n      run:\n        - pypdfium2-team::pypdfium2_helpers\n    ```\n    You'll want to have downstream callers handle the custom channels as shown above, otherwise conda will not be able to satisfy requirements.\n  \n  + To set up channels in a GH workflow\n    ```yaml\n    - name: ...\n      uses: conda-incubator/setup-miniconda@v3\n      with:\n        # ... your options\n        channels: pypdfium2-team,bblanchon\n        channel-priority: strict\n    ```\n    This is just a suggestion, you can also call `conda config` manually, or pass channels on command basis using `-c`, as discussed above.\n  \n  + To verify the sources\n    ```bash\n    conda list --show-channel-urls \"pypdfium2|pdfium-binaries\"\n    conda config --show-sources\n    ```\n    The table should show `pypdfium2-team` and `bblanchon` in the channels column.\n    If added permanently, the config should also include these channels, ideally with top priority.\n    Please check this before reporting any issue with a conda install of pypdfium2.\n  \n  _**Note:** Conda packages are normally managed using recipe feedstocks driven by third parties, in a Linux repository like fashion. However, with some quirks it is also possible to do conda packaging within the original project and publish to a custom channel, which is what pypdfium2-team does, and the above instructions are referring to._\n\n\n* \u003ca id=\"user-content-install-unofficial\" class=\"anchor\" href=\"#install-unofficial\"\u003eUnofficial packages 🔗\u003c/a\u003e\n  \n  The authors of this project have no control over and are not responsible for possible third-party builds of pypdfium2, and we do not support them. Please use the official packages where possible.\n  If you have an issue with a third-party build, either contact your distributor, or try to reproduce with an official build.\n  \n  Do not expect us to help with the creation of unofficial builds or add/change code for downstream setup tasks. Related issues or PRs may be closed without further notice if we don't see fit for upstream.\n  \n  If you are a third-party distributor, please point out in the description that your package is unofficial, i.e. not affiliated with or endorsed by pypdfium2 team.\n\n\n### Runtime Dependencies\n\nAs of this writing, pypdfium2 does not require any mandatory runtime dependencies apart from Python itself.\n\nHowever, some optional support model features need additional packages:\n* [`Pillow`](https://pillow.readthedocs.io/en/stable/) (module `PIL`) is a pouplar imaging library for Python. pypdfium2 provides convenience adapters to translate between raw bitmap buffers and PIL images. It also uses PIL for some command-line functionality (e.g. image saving).\n* [`NumPy`](https://numpy.org/doc/stable/index.html) is a library for scientific computing. As with `Pillow`, pypdfium2 provides helpers to get a numpy array view of a raw bitmap.\n* [`opencv-python`](https://github.com/opencv/opencv-python) (module `cv2`) is an imaging library built around numpy arrays. It can be used in the rendering CLI to save with pypdfium2's numpy adapter.\n\npypdfium2 tries to defer imports of optional dependencies until they are actually needed, so there should be no startup overhead if you don't use them.\n\n### Setup Magic\n\nAs pypdfium2 requires a C extension and has custom setup code, there are some special features to consider. Note, the APIs below may change any time and are mostly of internal interest.\n\n* Binaries are stored in platform-specific sub-directories of `data/`, along with bindings and version information.\n\n* `$PDFIUM_PLATFORM` defines which binary to include on setup.\n  - Format spec: `[$PLATFORM][-v8][:$VERSION]` (`[]` = segments, `$CAPS` = variables).\n  - Examples: `auto`, `auto:5975` `auto-v8:5975` (`auto` may be substituted by an explicit platform name, e.g. `linux_x64`).\n  - Platform:\n    + If unset or `auto`, the host platform is detected and a corresponding binary will be selected.\n    + If an explicit platform identifier (e.g. `linux_x64`, `darwin_arm64`, ...), binaries for the requested platform will be used.[^platform_ids]\n    + If `system`, bind against system-provided pdfium instead of embedding a binary. Version must be given explicitly so matching bindings can be generated.\n    + If `sourcebuild`, binaries will be taken from `data/sourcebuild/`, assuming a prior run of `sourcebuild.py`.\n    + If `sdist`, no platform-dependent files will be included, so as to create a source distribution.\n    `sourcebuild` and `sdist` are standalone, they cannot be followed by additional specifiers.\n  - V8: If given, use the V8 (JavaScript) and XFA enabled pdfium binaries. Otherwise, use the regular (non-V8) binaries.\n  - Version: If given, use the specified pdfium-binaries release. Otherwise, use the latest one.\n  - It is possible to prepend `prepared!` to install with existing platform files instead of generating on the fly; the value will be used for metadata / file inclusion. This can be helpful when installing in an isolated env where ctypesgen is not available, but it is not desirable to use the reference bindings (e.g. conda).\n\n* `$PYPDFIUM_MODULES=[raw,helpers]` defines the modules to include. Metadata adapts dynamically.\n  - May be used by packagers to decouple raw bindings and helpers, which may be relevant if packaging against system pdfium.\n  - Would also allow to install only the raw module without helpers, or only helpers with a custom raw module.\n\n* `$PDFIUM_BINDINGS=reference` allows to override ctypesgen and use the reference bindings file `autorelease/bindings.py` instead.\n  - This is a convenience option to get pypdfium2 installed from source even if a working ctypesgen is not available in the install env.\n  - Warning: This may not be ABI-safe. Please make sure binary/bindings build headers match to avoid ABI issues.\n\n[^platform_ids]: Intended for packaging, so that wheels can be crafted for any platform without access to a native host.\n\n\n## Usage\n\n### [Support model](https://pypdfium2.readthedocs.io/en/stable/python_api.html)\n\n\u003c!-- TODO demonstrate more APIs (e. g. XObject placement, transform matrices, image extraction, ...) --\u003e\n\nHere are some examples of using the support model API.\n\n* Import the library\n  ```python\n  import pypdfium2 as pdfium\n  import pypdfium2.raw as pdfium_c\n  ```\n\n* Open a PDF using the helper class `PdfDocument` (supports file path strings, bytes, and byte buffers)\n  ```python\n  pdf = pdfium.PdfDocument(\"./path/to/document.pdf\")\n  version = pdf.get_version()  # get the PDF standard version\n  n_pages = len(pdf)  # get the number of pages in the document\n  page = pdf[0]  # load a page\n  ```\n\n* Render the page\n  ```python\n  bitmap = page.render(\n      scale = 1,    # 72dpi resolution\n      rotation = 0, # no additional rotation\n      # ... further rendering options\n  )\n  pil_image = bitmap.to_pil()\n  pil_image.show()\n  ```\n  \n  Note, with the PIL adapter, it might be advantageous to use `force_bitmap_format=pdfium_c.FPDFBitmap_BGRA, rev_byteorder=True` or maybe `prefer_bgrx=True, use_bgra_on_transparency=True, rev_byteorder=True`, to achieve a pixel format supported natively by PIL, and avoid rendering with transparency to a non-alpha bitmap, which can slow down pdfium.\n  \n  With `.to_numpy()`, all formats are zero-copy, but passing either `use_bgra_on_transparency=True` (if dynamic pixel format is acceptable) or `force_bitmap_format=pdfium_c.FPDFBitmap_BGRA` is also recommended for the transparency problem.\n\n* Try some page methods\n  ```python\n  # Get page dimensions in PDF canvas units (1pt-\u003e1/72in by default)\n  width, height = page.get_size()\n  # Set the absolute page rotation to 90° clockwise\n  page.set_rotation(90)\n  \n  # Locate objects on the page\n  for obj in page.get_objects():\n      print(obj.level, obj.type, obj.get_bounds())\n  ```\n\n* Extract and search text\n  ```python\n  # Load a text page helper\n  textpage = page.get_textpage()\n  \n  # Extract text from the whole page\n  text_all = textpage.get_text_bounded()\n  # Extract text from a specific rectangular area\n  text_rect = textpage.get_text_bounded(left=50, bottom=100, right=width-50, top=height-100)\n  # Extract text from a specific char range\n  text_span = textpage.get_text_range(index=10, count=15)\n  \n  # Locate text on the page\n  searcher = textpage.search(\"something\", match_case=False, match_whole_word=False)\n  # This returns the next occurrence as (char_index, char_count), or None if not found\n  match = searcher.get_next()\n  ```\n\n* Read the table of contents\n  ```python\n  import pypdfium2.internal as pdfium_i\n  \n  for bm in pdf.get_toc(max_depth=15):\n      count, dest = bm.get_count(), bm.get_dest()\n      out = \"    \" * bm.level\n      out += \"[%s] %s -\u003e \" % (\n          f\"{count:+}\" if count != 0 else \"*\",\n          bm.get_title(),\n      )\n      if dest:\n          index, (view_mode, view_pos) = dest.get_index(), dest.get_view()\n          out += \"%s  # %s %s\" % (\n              index+1 if index != None else \"?\",\n              pdfium_i.ViewmodeToStr.get(view_mode),\n              round(view_pos, 3),\n          )\n      else:\n          out += \"_\"\n      print(out)\n  ```\n\n* Create a new PDF with an empty A4 sized page\n  ```python\n  pdf = pdfium.PdfDocument.new()\n  width, height = (595, 842)\n  page_a = pdf.new_page(width, height)\n  ```\n\n* Include a JPEG image in a PDF\n  ```python\n  pdf = pdfium.PdfDocument.new()\n  \n  image = pdfium.PdfImage.new(pdf)\n  image.load_jpeg(\"./tests/resources/mona_lisa.jpg\")\n  width, height = image.get_px_size()\n  \n  matrix = pdfium.PdfMatrix().scale(width, height)\n  image.set_matrix(matrix)\n  \n  page = pdf.new_page(width, height)\n  page.insert_obj(image)\n  page.gen_content()\n  ```\n\n* Save the document\n  ```python\n  # PDF 1.7 standard\n  pdf.save(\"output.pdf\", version=17)\n  ```\n\n### Raw PDFium API\n\nWhile helper classes conveniently wrap the raw PDFium API, it may still be accessed directly and is available in the namespace `pypdfium2.raw`. Lower-level utilities that may aid with using the raw API are provided in `pypdfium2.internal`.\n\n```python\nimport pypdfium2.raw as pdfium_c\nimport pypdfium2.internal as pdfium_i\n```\n\nSince PDFium is a large library, many components are not covered by helpers yet. However, as helpers expose their underlying raw objects, you may seamlessly integrate raw APIs while using helpers as available. When passed as ctypes function parameter, helpers automatically resolve to the raw object handle (but you may still access it explicitly if desired):\n```python\npermission_flags = pdfium_c.FPDF_GetDocPermission(pdf.raw)  # explicit\npermission_flags = pdfium_c.FPDF_GetDocPermission(pdf)      # implicit\n```\n\nFor PDFium docs, please look at the comments in its [public header files](https://pdfium.googlesource.com/pdfium/+/refs/heads/main/public/).[^pdfium_docs]\nA variety of examples on how to interface with the raw API using [`ctypes`](https://docs.python.org/3/library/ctypes.html) is already provided with [support model source code](src/pypdfium2/_helpers).\nNonetheless, the following guide may be helpful to get started with the raw API, if you are not familiar with `ctypes` yet.\n\n[^pdfium_docs]: Unfortunately, no recent HTML-rendered docs are available for PDFium at the moment.\n\n\u003c!-- TODO write something about weakref.finalize(); add example on creating a C page array --\u003e\n\u003c!-- TODO doctests? --\u003e\n\n* In general, PDFium functions can be called just like normal Python functions.\n  However, parameters may only be passed positionally, i.e. it is not possible to use keyword arguments.\n  There are no defaults, so you always need to provide a value for each argument.\n  ```python\n  # arguments: filepath (bytes), password (bytes|None)\n  # NUL-terminate filepath and encode as UTF-8\n  pdf = pdfium_c.FPDF_LoadDocument((filepath+\"\\x00\").encode(\"utf-8\"), None)\n  ```\n  This is the underlying bindings declaration,[^bindings_decl] which loads the function from the binary and\n  contains the information required to convert Python types to their C equivalents.\n  ```python\n  if _libs[\"pdfium\"].has(\"FPDF_LoadDocument\", \"cdecl\"):\n      FPDF_LoadDocument = _libs[\"pdfium\"].get(\"FPDF_LoadDocument\", \"cdecl\")\n      FPDF_LoadDocument.argtypes = [FPDF_STRING, FPDF_BYTESTRING]\n      FPDF_LoadDocument.restype = FPDF_DOCUMENT\n  ```\n  Python `bytes` are converted to `FPDF_STRING` by ctypes autoconversion. This works because `FPDF_STRING` is actually an alias to `POINTER(c_char)` (i.e. `char*`), which is a primitive pointer type.\n  When passing a string to a C function, it must always be NUL-terminated, as the function merely receives a pointer to the first item and then continues to read memory until it finds a NUL terminator.\n  \n[^bindings_decl]: From the auto-generated bindings file. We maintain a reference copy at `autorelease/bindings.py`. Or if you have an editable install, there will also be `src/pypdfium2_raw/bindings.py`.\n\n* While some functions are quite easy to use, things may soon get more special.\n  First of all, function parameters are not only used for input, but also for output:\n  ```python\n  # Initialise an integer object (defaults to 0)\n  c_version = ctypes.c_int()\n  # Let the function assign a value to the c_int object, and capture its return code (True for success, False for failure)\n  ok = pdfium_c.FPDF_GetFileVersion(pdf, c_version)\n  # If successful, get the Python int by accessing the `value` attribute of the c_int object\n  # Otherwise, set the variable to None (in other cases, it may be desired to raise an exception instead)\n  version = c_version.value if ok else None\n  ```\n\n* If an array is required as output parameter, you can initialise one like this (in general terms):\n  ```python\n  # long form\n  array_type = (c_type * array_length)\n  array_object = array_type()\n  # short form\n  array_object = (c_type * array_length)()\n  ```\n  Example: Getting view mode and target position from a destination object returned by some other function.\n  ```python\n  # (Assuming `dest` is an FPDF_DEST)\n  n_params = ctypes.c_ulong()\n  # Create a C array to store up to four coordinates\n  view_pos = (pdfium_c.FS_FLOAT * 4)()\n  view_mode = pdfium_c.FPDFDest_GetView(dest, n_params, view_pos)\n  # Convert the C array to a Python list and cut it down to the actual number of coordinates\n  view_pos = list(view_pos)[:n_params.value]\n  ```\n\n* For string output parameters, callers needs to provide a sufficiently long, pre-allocated buffer.\n  This may work differently depending on what type the function requires, which encoding is used, whether the number of bytes or characters is returned, and whether space for a NUL terminator is included or not. Carefully review the documentation of the function in question to fulfill its requirements.\n  \n  Example A: Getting the title string of a bookmark.\n  ```python\n  # (Assuming `bookmark` is an FPDF_BOOKMARK)\n  # First call to get the required number of bytes (not units!), including space for a NUL terminator\n  n_bytes = pdfium_c.FPDFBookmark_GetTitle(bookmark, None, 0)\n  # Initialise the output buffer\n  buffer = ctypes.create_string_buffer(n_bytes)\n  # Second call with the actual buffer\n  pdfium_c.FPDFBookmark_GetTitle(bookmark, buffer, n_bytes)\n  # Decode to string, cutting off the NUL terminator (encoding: UTF-16LE)\n  title = buffer.raw[:n_bytes-2].decode(\"utf-16-le\")\n  ```\n  \n  Example B: Extracting text in given boundaries.\n  ```python\n  # (Assuming `textpage` is an FPDF_TEXTPAGE and the boundary variables are set)\n  # Store common arguments for the two calls\n  args = (textpage, left, top, right, bottom)\n  # First call to get the required number of units (not bytes!) - a possible NUL terminator is not included\n  n_chars = pdfium_c.FPDFText_GetBoundedText(*args, None, 0)\n  # If no characters were found, return an empty string\n  if n_chars \u003c= 0:\n      return \"\"\n  # Calculate the required number of bytes (encoding: UTF-16LE again)\n  # The function signature uses c_ushort, so 1 unit takes sizeof(c_ushort) == 2 bytes\n  n_bytes = 2 * n_chars\n  # Initialise the output buffer - this function can work without NUL terminator, so skip it\n  buffer = ctypes.create_string_buffer(n_bytes)\n  # Re-interpret the type from char to unsigned short* as required by the function\n  buffer_ptr = ctypes.cast(buffer, ctypes.POINTER(ctypes.c_ushort))\n  # Second call with the actual buffer\n  pdfium_c.FPDFText_GetBoundedText(*args, buffer_ptr, n_chars)\n  # Decode to string (You may want to pass `errors=\"ignore\"` to skip possible errors in the PDF's encoding)\n  text = buffer.raw.decode(\"utf-16-le\")\n  ```\n\n* Not only are there different ways of string output that need to be handled according to the requirements of the function in question.\n  String input, too, can work differently depending on encoding and type.\n  We have already discussed `FPDF_LoadDocument()`, which takes a UTF-8 encoded string as `char*`.\n  A different examples is `FPDFText_FindStart()`, which needs a UTF-16LE encoded string, given as `unsigned short*`:\n  ```python\n  # (Assuming `text` is a str and `textpage` an FPDF_TEXTPAGE)\n  # Add the NUL terminator and encode as UTF-16LE\n  enc_text = (text + \"\\x00\").encode(\"utf-16-le\")\n  # cast `enc_text` to a c_ushort pointer\n  text_ptr = ctypes.cast(enc_text, ctypes.POINTER(ctypes.c_ushort))\n  search = pdfium_c.FPDFText_FindStart(textpage, text_ptr, 0, 0)\n  ```\n\n* Leaving strings, let's suppose you have a C memory buffer allocated by PDFium and wish to read its data.\n  PDFium will provide you with a pointer to the first item of the byte array.\n  To access the data, you'll want to re-interpret the pointer to an array view with `.from_address()`:\n  ```python\n  # (Assuming `bitmap` is an FPDF_BITMAP and `size` is the expected number of bytes in the buffer)\n  # FPDFBitmap_GetBuffer() has c_void_p as restype, which ctypes will auto-resolve to int or None\n  buffer_ptrval = pdfium_c.FPDFBitmap_GetBuffer(bitmap)\n  assert buffer_ptrval  # make sure it's non-null\n  # Get an actual pointer object so we can access .contents\n  buffer_ptr = ctypes.cast(buffer_ptrval, ctypes.POINTER(ctypes.c_ubyte))\n  # Buffer as ctypes array (referencing the original buffer, will be unavailable as soon as the bitmap is destroyed)\n  c_buffer = (ctypes.c_ubyte * size).from_address( ctypes.addressof(buffer_ptr.contents) )\n  # Buffer as Python bytes (independent copy)\n  py_buffer = bytes(c_buffer)\n  ```\n  Note that you can achieve the same result with `ctypes.cast(ptr, POINTER(type * size)).contents`, but this is somewhat problematic as ctypes seems to cache pointer types eternally. As `size` may vary, this can lead to memory leak like scenarios with long-running applications, so better avoid doing that.\n\n* Writing data from Python into a C buffer works in a similar fashion:\n  ```python\n  # (Assuming `buffer_ptr` is a pointer to the first item of a C buffer to write into,\n  #  `size` the number of bytes it can store, and `py_buffer` a Python byte buffer)\n  buffer = (ctypes.c_char * size).from_address( ctypes.addressof(buffer_ptr.contents) )\n  # Read from the Python buffer, starting at its current position, directly into the C buffer\n  # (until the target is full or the end of the source is reached)\n  n_bytes = py_buffer.readinto(buffer)  # returns the number of bytes read\n  ```\n\n* If you wish to check whether two objects returned by PDFium are the same, the `is` operator won't help because `ctypes` does not have original object return (OOR), i.e. new, equivalent Python objects are created each time, although they might represent one and the same C object.[^ctypes_no_oor]\n  That's why you'll want to use `ctypes.addressof()` to get the memory addresses of the underlying C object.\n  For instance, this is used to avoid infinite loops on circular bookmark references when iterating through the document outline:\n  ```python\n  # (Assuming `pdf` is an FPDF_DOCUMENT)\n  seen = set()\n  bookmark = pdfium_c.FPDFBookmark_GetFirstChild(pdf, None)\n  while bookmark:\n      # bookmark is a pointer, so we need to use its `contents` attribute to get the object the pointer refers to\n      # (otherwise we'd only get the memory address of the pointer itself, which would result in random behaviour)\n      address = ctypes.addressof(bookmark.contents)\n      if address in seen:\n          break  # circular reference detected\n      else:\n          seen.add(address)\n      bookmark = pdfium_c.FPDFBookmark_GetNextSibling(pdf, bookmark)\n  ```\n  \n  [^ctypes_no_oor]: Confer the [ctypes documentation on Pointers](https://docs.python.org/3/library/ctypes.html#pointers).\n\n* In many situations, callback functions come in handy.[^callback_usecases] Thanks to `ctypes`, it is seamlessly possible to use callbacks across Python/C language boundaries.\n  \n  [^callback_usecases]: e. g. incremental read/write, management of progressive tasks, ...\n  \n  Example: Loading a document from a Python buffer. This way, file access can be controlled in Python while the data does not need to be in memory at once.\n  ```python\n  import os\n  \n  # Factory class to create callable objects holding a reference to a Python buffer\n  class _reader_class:\n    \n    def __init__(self, py_buffer):\n        self.py_buffer = py_buffer\n    \n    def __call__(self, _, position, buffer_ptr, size):\n        # Write data from Python buffer into C buffer, as explained before\n        buffer_ptr = ctypes.cast(buffer_ptr, ctypes.POINTER(ctypes.c_char * size))\n        self.py_buffer.seek(position)\n        self.py_buffer.readinto(buffer_ptr.contents)\n        return 1  # non-zero return code for success\n  \n  # (Assuming py_buffer is a Python file buffer, e. g. io.BufferedReader)\n  # Get the length of the buffer\n  py_buffer.seek(0, os.SEEK_END)\n  file_len = py_buffer.tell()\n  py_buffer.seek(0)\n  \n  # Set up an interface structure for custom file access\n  fileaccess = pdfium_c.FPDF_FILEACCESS()\n  fileaccess.m_FileLen = file_len\n  \n  # Assign the callback, wrapped in its CFUNCTYPE\n  fileaccess.m_GetBlock = type(fileaccess.m_GetBlock)( _reader_class(py_buffer) )\n  \n  # Finally, load the document\n  pdf = pdfium_c.FPDF_LoadCustomDocument(fileaccess, None)\n  ```\n\n\u003c!-- TODO mention pdfium_i.get_bufreader() as a shortcut to set up an FPDF_FILEACCESS interface --\u003e\n\n* When using the raw API, special care needs to be taken regarding object lifetime, considering that Python may garbage collect objects as soon as their reference count reaches zero. However, the interpreter has no way of magically knowing how long the underlying resources of a Python object might still be needed on the C side, so measures need to be taken to keep such objects referenced until PDFium does not depend on them anymore.\n  \n  If resources need to remain valid after the time of a function call, PDFium docs usually indicate this clearly. Ignoring requirements on object lifetime will lead to memory corruption (commonly resulting in a segfault sooner or later).\n  \n  For instance, the docs on `FPDF_LoadCustomDocument()` state that\n  \u003e The application must keep the file resources |pFileAccess| points to valid until the returned FPDF_DOCUMENT is closed. |pFileAccess| itself does not need to outlive the FPDF_DOCUMENT.\n  \n  This means that the callback function and the Python buffer need to be kept alive as long as the `FPDF_DOCUMENT` is used.\n  This can be achieved by referencing these objects in an accompanying class, e. g.\n  \n  ```python\n  class PdfDataHolder:\n      \n      def __init__(self, buffer, function):\n          self.buffer = buffer\n          self.function = function\n      \n      def close(self):\n          # Make sure both objects remain available until this function is called\n          # No-op id() call to denote that the object needs to stay in memory up to this point\n          id(self.function)\n          self.buffer.close()\n  \n  # ... set up an FPDF_FILEACCESS structure\n  \n  # (Assuming `py_buffer` is the buffer and `fileaccess` the FPDF_FILEACCESS interface)\n  data_holder = PdfDataHolder(py_buffer, fileaccess.m_GetBlock)\n  pdf = pdfium_c.FPDF_LoadCustomDocument(fileaccess, None)\n  \n  # ... work with the pdf\n  \n  # Close the PDF to free resources\n  pdfium_c.FPDF_CloseDocument(pdf)\n  # Close the data holder, to keep the object itself and thereby the objects it\n  # references alive up to this point, as well as to release the buffer\n  data_holder.close()\n  ```\n\n* Finally, let's finish this guide with an example how to render the first page of a document to a `PIL` image in `RGBA` color format.\n  ```python\n  import math\n  import ctypes\n  import os.path\n  import PIL.Image\n  import pypdfium2.raw as pdfium_c\n  \n  # Load the document\n  filepath = os.path.abspath(\"tests/resources/render.pdf\")\n  pdf = pdfium_c.FPDF_LoadDocument((filepath+\"\\x00\").encode(\"utf-8\"), None)\n  \n  # Check page count to make sure it was loaded correctly\n  page_count = pdfium_c.FPDF_GetPageCount(pdf)\n  assert page_count \u003e= 1\n  \n  # Load the first page and get its dimensions\n  page = pdfium_c.FPDF_LoadPage(pdf, 0)\n  width  = math.ceil(pdfium_c.FPDF_GetPageWidthF(page))\n  height = math.ceil(pdfium_c.FPDF_GetPageHeightF(page))\n  \n  # Create a bitmap\n  # (Note, pdfium is faster at rendering transparency if we use BGRA rather than BGRx)\n  use_alpha = pdfium_c.FPDFPage_HasTransparency(page)\n  bitmap = pdfium_c.FPDFBitmap_Create(width, height, int(use_alpha))\n  # Fill the whole bitmap with a white background\n  # The color is given as a 32-bit integer in ARGB format (8 bits per channel)\n  pdfium_c.FPDFBitmap_FillRect(bitmap, 0, 0, width, height, 0xFFFFFFFF)\n  \n  # Store common rendering arguments\n  render_args = (\n      bitmap,  # the bitmap\n      page,    # the page\n      # positions and sizes are to be given in pixels and may exceed the bitmap\n      0,       # left start position\n      0,       # top start position\n      width,   # horizontal size\n      height,  # vertical size\n      0,       # rotation (as constant, not in degrees!)\n      pdfium_c.FPDF_LCD_TEXT | pdfium_c.FPDF_ANNOT,  # rendering flags, combined with binary or\n  )\n  \n  # Render the page\n  pdfium_c.FPDF_RenderPageBitmap(*render_args)\n  \n  # Get the value of a pointer to the first item of the buffer\n  buffer_ptrval = pdfium_c.FPDFBitmap_GetBuffer(bitmap)\n  assert buffer_ptrval, \"buffer pointer value must be non-null\"\n  # Cast the pointer value to an actual pointer object so we can access .contents\n  buffer_ptr = ctypes.cast(buffer_ptrval, ctypes.POINTER(ctypes.c_ubyte))\n  # Re-interpret as array\n  buffer = (ctypes.c_ubyte * (width * height * 4)).from_address(ctypes.addressof(buffer_ptr.contents))\n  \n  # Create a PIL image from the buffer contents\n  img = PIL.Image.frombuffer(\"RGBA\", (width, height), buffer, \"raw\", \"BGRA\", 0, 1)\n  # Save it as file\n  img.save(\"out.png\")\n  \n  # Free resources\n  pdfium_c.FPDFBitmap_Destroy(bitmap)\n  pdfium_c.FPDF_ClosePage(page)\n  pdfium_c.FPDF_CloseDocument(pdf)\n  ```\n\n### [Command-line Interface](https://pypdfium2.readthedocs.io/en/stable/shell_api.html)\n\npypdfium2 also ships with a simple command-line interface, providing access to key features of the support model in a shell environment (e. g. rendering, content extraction, document inspection, page rearranging, ...).\n\nThe primary motivation behind this is to have a nice testing interface, but it may be helpful in a variety of other situations as well.\nUsage should be largely self-explanatory, assuming a minimum of familiarity with the command-line.\n\n\n## Licensing\n\n*Disclaimer: This project is provided on an \"as-is\" basis. This is not legal advice, and there is ABSOLUTELY NO WARRANTY for any information provided in this document or elsewhere in the pypdfium2 project, including earlier revisions. We disclaim liability for any possible damages resulting from using this license information. It is the embedder's responsibility to check on licensing. See also [GitHub's disclaimer](https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/customizing-your-repository/licensing-a-repository#disclaimer).*\n\npypdfium2 itself is available by the terms and conditions of [`Apache-2.0`](LICENSES/Apache-2.0.txt) / [`BSD-3-Clause`](LICENSES/BSD-3-Clause.txt).\nDocumentation and examples of pypdfium2 are licensed under [`CC-BY-4.0`](LICENSES/CC-BY-4.0.txt).\n\nPDFium is available under a BSD-style license that can be found in its [`LICENSE`](https://pdfium.googlesource.com/pdfium/+/refs/heads/main/LICENSE) file.\nVarious other open-source licenses apply to dependencies bundled with PDFium. These also have to be shipped alongside binary redistributions. Copies of identified licenses are provided in [`LicenseRef-PdfiumThirdParty.txt`](LICENSES/LicenseRef-PdfiumThirdParty.txt).\nNote that pdfium's dependencies might change over time. Although we try to keep an eye on the situation, there is no guarantee of completeness. Please notify us if you think this misses a relevant license.\n\npypdfium2 includes [SPDX](https://spdx.org/licenses/) headers in source files.\nLicense information for data files is provided in [`REUSE.toml`](REUSE.toml) as per the [`reuse` standard](https://reuse.software/spec/).\n\nTo the author's knowledge, pypdfium2 is one of the rare Python libraries that are capable of PDF rendering while not being covered by copyleft licenses (such as the `GPL`).[^liberal_pdf_renderlibs]\n\n[^liberal_pdf_renderlibs]: The only other liberal-licensed PDF rendering libraries known to the author are [`pdf.js`](https://github.com/mozilla/pdf.js/) (JavaScript) and [`Apache PDFBox`](https://github.com/apache/pdfbox) (Java), but python bindings packages don't exist yet or are unsatisfactory. However, we wrote some gists that show it'd be possible in principle: [pdfbox](https://gist.github.com/mara004/51c3216a9eabd3dcbc78a86d877a61dc) (+ [setup](https://gist.github.com/mara004/881d0c5a99b8444fd5d1d21a333b70f8)), [pdfjs](https://gist.github.com/mara004/87276da4f8be31c80c38036c6ab667d7).\n\n\n## Issues / Contributions\n\nWhile using pypdfium2, you might encounter bugs or missing features.\nIn this case, feel free to open an issue or discussion thread. If applicable, include details such as tracebacks, OS and CPU type, as well as the versions of pypdfium2 and used dependencies.\n\nRoadmap:\n* pypdfium2\n  - [Issues panel](https://github.com/pypdfium2-team/pypdfium2/issues): Initial bug reports and feature requests. May need to be transferred to dependencies.\n  - [Discussions page](https://github.com/pypdfium2-team/pypdfium2/discussions): General questions and suggestions.\n* PDFium\n  - [Bug tracker](https://bugs.chromium.org/p/pdfium/issues/list): Issues in PDFium.\n    Beware: The bridge between Python and C increases the probability of integration issues or API misuse. The symptoms can often make it look like a PDFium bug while it is not.\n  - [Mailing list](https://groups.google.com/g/pdfium/): Questions regarding PDFium usage.\n* [pdfium-binaries](https://github.com/bblanchon/pdfium-binaries/issues): Binary builder.\n* [ctypesgen](https://github.com/ctypesgen/ctypesgen/issues): Bindings generator.\n\n### Response policy\n\u003c!-- Inspired by bluesky's contribution rules: https://github.com/bluesky-social/indigo --\u003e\n\nGiven this is a volunteer open-source project, it is possible you may not get a response to your issue, or it may be closed without much feedback. Conversations may be locked if we feel like our attention is getting DDOSed. We may not have time to provide usage support.\n\nThe same applies to Pull Requests. We will accept contributions only if we find them suitable. Do not reach out with a strong expectation to get your change merged; it is solely up to the repository owner to decide if and when a PR will be merged, and we are free to silently reject PRs we do not like.\n\n### Known limitations\n\n#### Incompatibility with Threading\n\nPDFium is inherently not thread-safe. See the [API docs](https://pypdfium2.readthedocs.io/en/stable/python_api.html#incompatibility-with-threading) for more information.\n\n#### Risk of unknown object lifetime violations\n\nAs outlined in the raw API section, it is essential that Python-managed resources remain available as long as they are needed by PDFium.\n\nThe problem is that the Python interpreter may garbage collect objects with reference count zero at any time, so it can happen that an unreferenced but still required object by chance stays around long enough before it is garbage collected. However, it could also disappear too soon and cause breakage. Such dangling objects result in non-deterministic memory issues that are hard to debug.\nIf the timeframe between reaching reference count zero and removal is sufficiently large and roughly consistent across different runs, it is even possible that mistakes regarding object lifetime remain unnoticed for a long time.\n\nAlthough we intend to develop helpers carefully, it cannot be fully excluded that unknown object lifetime violations might still be lurking around somewhere, especially if unexpected requirements were not documented by the time the code was written.\n\n#### Missing raw PDF access\n\nAs of this writing, PDFium's public interface does not provide access to the raw PDF data structure (see [issue 1694](https://crbug.com/pdfium/1694)). It does not expose APIs to read/write PDF dictionaries, streams, name/number trees, etc. Instead, it merely offers a predefined set of abstracted functions. This considerably limits the library's potential, compared to other products such as `pikepdf`.\n\n#### Limitations of ABI bindings\n\nPDFium's non-public backend would provide extended capabilities, including [raw access](#missing-raw-pdf-access), but it is written in C++, which (unlike pure C) does not result in a stable ABI, so we cannot use it with `ctypes`. This means it's out of scope for this project.\n\nAlso, while ABI bindings tend to be more convenient, they have some technical drawbacks compared to API bindings (see e.g. [1](https://cffi.readthedocs.io/en/latest/overview.html#abi-versus-api), [2](https://github.com/ocrmypdf/OCRmyPDF/issues/541#issuecomment-1834684532))\n\n\n## Development\n\u003c!-- TODO wheel tags, maintainer access, GitHub peculiarities --\u003e\n\n### Long lines\n\nThe pypdfium2 codebase does not hard wrap long lines.\nIt is recommended to set up automatic word wrap in your text editor, e.g. VS Code:\n```\neditor.wordWrap = bounded\neditor.wordWrapColumn = 100\n```\n\n### Command recipes\n\nThe pypdfium2 project uses the [`just` command runner](https://github.com/casey/just), which can be seen as a more modern, more flexible alternative to `make`. In particular, there's no good way to pass through positional arguments with `make`.\n\nRun `just -l` (or open the `justfile`) to view the available commands.\n\n### Docs\n\npypdfium2 provides API documentation using [Sphinx](https://github.com/sphinx-doc/sphinx/), which can be rendered to various formats, including HTML:\n```bash\nsphinx-build -b html ./docs/source ./docs/build/html/\njust docs-build  # short alias\n```\n\u003c!-- there's also a Makefile in docs/ as generated by sphinx-quickstart (cd docs/; make build) --\u003e\n\nBuilt docs are primarily hosted on [`readthedocs.org`](https://readthedocs.org/projects/pypdfium2/).\nIt may be configured using a [`.readthedocs.yaml`](.readthedocs.yaml) file (see [instructions](https://docs.readthedocs.io/en/stable/config-file/v2.html)), and the administration page on the web interface.\nRTD supports hosting multiple versions, so we currently have one linked to the `main` branch and another to `stable`.\nNew builds are automatically triggered by a webhook whenever you push to a linked branch.\n\nAdditionally, one doc build can also be hosted on [GitHub Pages](https://pypdfium2-team.github.io/pypdfium2/index.html).\nIt is implemented with a CI workflow, which is supposed to be triggered automatically on release.\nThis provides us with full control over the build env and the used commands, whereas RTD may be less liberal in this regard.\n\n\n### Testing\n\npypdfium2 contains a small test suite to verify the library's functionality. It is written with [pytest](https://github.com/pytest-dev/pytest/):\n```bash\njust test\n```\n\nNote that ...\n* you can pass `-sv` to get more detailed output.\n* `$DEBUG_AUTOCLOSE=1` may be set to get debugging information on automatic object finalization.\n\nTo get code coverage statistics, you may call\n```bash\njust coverage\n```\n\nSometimes, it can also be helpful to test code on many PDFs.[^testing_corpora]\nIn this case, the command-line interface and `find` come in handy:\n```bash\n# Example A: Analyse PDF images (in the current working directory)\nfind . -name '*.pdf' -exec bash -c \"echo \\\"{}\\\" \u0026\u0026 pypdfium2 pageobjects \\\"{}\\\" --filter image\" \\;\n# Example B: Parse PDF table of contents\nfind . -name '*.pdf' -exec bash -c \"echo \\\"{}\\\" \u0026\u0026 pypdfium2 toc \\\"{}\\\"\" \\;\n```\n\n[^testing_corpora]: For instance, one could use the testing corpora of open-source PDF libraries (pdfium, pikepdf/ocrmypdf, mupdf/ghostscript, tika/pdfbox, pdfjs, ...)\n\n### Release workflow\n\nThe release process is fully automated using Python scripts and scheduled release workflows.\nYou may also trigger the workflow manually using the GitHub Actions panel or the [`gh`](https://cli.github.com/) command-line tool.\n\nPython release scripts are located in the folder `setupsrc/pypdfium2_setup`, along with custom setup code:\n* `update.py` downloads binaries.\n* `craft.py` builds platform-specific wheel packages and a source distribution suitable for PyPI upload.\n* `autorelease.py` takes care of versioning, changelog, release note generation and VCS check-in.\n\nThe autorelease script has some peculiarities maintainers should know about:\n* The changelog for the next release shall be written into `docs/devel/changelog_staging.md`.\n  On release, it will be moved into the main changelog under `docs/source/changelog.md`, annotated with the PDFium version update.\n  It will also be shown on the GitHub release page.\n* pypdfium2 versioning uses the pattern `major.minor.patch`, optionally with an appended beta mark (e. g. `2.7.1`, `2.11.0`, `3.0.0b1`, ...).\n  Update types such as major or beta may be controlled via `autorelease/config.json`\n\nIn case of necessity, you may also forego autorelease/CI and do the release manually, which will roughly work like this (though ideally it should never be needed):\n* Commit changes to the version file\n  ```bash\n  git add src/pypdfium2/version.py\n  git commit -m \"increment version\"\n  git push\n  ```\n* Create a new tag that matches the version file\n  ```bash\n  # substitute $VERSION accordingly\n  git tag -a $VERSION\n  git push --tags\n  ```\n* Build the packages\n  ```bash\n  python setupsrc/pypdfium2_setup/update.py\n  python setupsrc/pypdfium2_setup/craft.py\n  ```\n* Upload to PyPI\n  ```bash\n  # make sure the packages are valid\n  twine check dist/*\n  # upload to PyPI (this will interactively ask for your username/password)\n  twine upload dist/*\n  ```\n* Update the `stable` branch to trigger a documentation rebuild\n  ```bash\n  git checkout stable\n  git rebase origin/main  # alternatively: git reset --hard main\n  git checkout main\n  ```\n\nIf something went wrong with commit or tag, you can still revert the changes:\n```bash\n# perform an interactive rebase to change history (substitute $N_COMMITS with the number of commits to drop or modify)\ngit rebase -i HEAD~$N_COMMITS\ngit push --force\n# delete local tag (substitute $TAGNAME accordingly)\ngit tag -d $TAGNAME\n# delete remote tag\ngit push --delete origin $TAGNAME\n```\nFaulty PyPI releases may be yanked using the web interface.\n\n\n## Popular dependents\n\npypdfium2 is used by popular packages such as\n[langchain](https://github.com/langchain-ai/langchain),\n[docling](https://github.com/DS4SD/docling),\n[nougat](https://github.com/facebookresearch/nougat),\n[pdfplumber](https://github.com/jsvine/pdfplumber),\nand [doctr](https://github.com/mindee/doctr/).\n\nThis results in pypdfium2 being part of a large dependency tree.\n\n\n## Thanks to[^thanks_to]\n\n* [Yinlin Hu](https://github.com/YinlinHu): `pypdfium` prototype and `kuafu` PDF viewer.\n* [Mike Kroutikov](https://github.com/mkroutikov): Examples on how to use PDFium in `redstork`, `redstork-ui` and `pdfbrain`.\n* [Tim Head](https://github.com/betatim): Original idea for Python bindings to PDFium with ctypesgen in `wowpng`.\n* [Benoît Blanchon](https://github.com/bblanchon): Author of [PDFium binaries](https://github.com/bblanchon/pdfium-binaries/) and [patches](sourcebuild/patches/).\n* [Adam Huganir](https://github.com/adam-huganir): Help with maintenance and development decisions since the beginning of the project.\n* [kobaltcore](https://github.com/kobaltcore): Bug fix for `PdfDocument.save()`.\n* [Anderson Bravalheri](https://github.com/abravalheri): Help with PEP 517/518 compliance. Hint to use an environment variable rather than separate setup files.\n* [Bastian Germann](https://github.com/bgermann): Help with inclusion of licenses for third-party components of PDFium.\n\n... and further [code contributors](https://github.com/pypdfium2-team/pypdfium2/graphs/contributors) (GitHub stats).\n\n*If you have contributed to this project but are not mentioned here yet, please let us know.*\n\n[^thanks_to]: People listed in this section may not necessarily have contributed any copyrightable code to the repository. Some have rather helped with ideas, or contributions to dependencies of pypdfium2.\n\n\n## History\n\n### PDFium\n\nThe PDFium code base was originally developed as part of the commercial Foxit SDK, before being acquired and open-sourced by Google, who maintain PDFium independently ever since, while Foxit continue to develop their SDK closed-source.\n\n### pypdfium2\n\npypdfium2 is the successor of *pypdfium* and *pypdfium-reboot*.\n\nInspired by *wowpng*, the first known proof of concept Python binding to PDFium using ctypesgen, the initial *pypdfium* package was created. It had to be updated manually, which did not happen frequently. There were no platform-specific wheels, but only a single wheel that contained binaries for 64-bit Linux, Windows and macOS.\n\n*pypdfium-reboot* then added a script to automate binary deployment and bindings generation to simplify regular updates. However, it was still not platform specific.\n\npypdfium2 is a full rewrite of *pypdfium-reboot* to build platform-specific wheels and consolidate the setup scripts. Further additions include ...\n* A CI workflow to automatically release new wheels at a defined schedule\n* Convenience support models that wrap the raw PDFium/ctypes API\n* Test code\n* A script to build PDFium from source\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpypdfium2-team%2Fpypdfium2","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fpypdfium2-team%2Fpypdfium2","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fpypdfium2-team%2Fpypdfium2/lists"}