pypdf Documentation

A pure-python PDF library capable of splitting, merging, cropping, and transforming the pages of PDF files

RAW Doc

Technical Documentation: py-pdf/pypdf

ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference

Overview (`README.md`)





pypdf

pypdf is a free and open-source pure-python PDF library capable of splitting,
merging,
cropping, and transforming
the pages of PDF files. It can also add
custom data, viewing options, and
passwords
to PDF files. pypdf can
retrieve text
and
metadata
from PDFs as well.

See pdfly for a CLI application that uses pypdf to interact with PDFs.

Installation

Install pypdf using pip:

text
pip install pypdf

For using pypdf with AES encryption or decryption, install extra dependencies:

text
pip install pypdf[crypto]

NOTE: pypdf 3.1.0 and above include significant improvements compared to
previous versions. Please refer to the migration
guide
for
more information.

Usage

python
from pypdf import PdfReader

reader = PdfReader("example.pdf")
number_of_pages = len(reader.pages)
page = reader.pages[0]
text = page.extract_text()

pypdf can do a lot more, e.g. splitting, merging, reading and creating annotations, decrypting and encrypting. Check out the
documentation for additional usage
examples!

For questions and answers, visit
StackOverflow
(tagged with pypdf).

Contributions

Maintaining pypdf is a collaborative effort. You can support the project by
writing documentation, helping to narrow down issues, and submitting code.
See the CONTRIBUTING.md file for more information.

Q&A

The experience pypdf users have covers the whole range from beginner to expert. You can contribute to the pypdf community by answering questions
on StackOverflow,
helping in discussions,
and asking users who report issues for MCVE's (Code + example PDF!).

Issues

A good bug ticket includes a MCVE - a minimal complete verifiable example.
For pypdf, this means that you must upload a PDF that causes the bug to occur
as well as the code you're executing with all of the output. Use
print(pypdf.__version__) to tell us which version you're using.

Code

All code contributions are welcome, but smaller ones have a better chance to
get included in a timely manner. Adding unit tests for new features or test
cases for bugs you've fixed help us to ensure that the Pull Request (PR) is fine.

pypdf includes a test suite which can be executed with pytest:

bash
$ pytest
===================== test session starts =====================
platform linux -- Python 3.6.15, pytest-7.0.1, pluggy-1.0.0
rootdir: /home/moose/GitHub/Martin/pypdf
plugins: cov-3.0.0
collected 233 items

tests/test_basic_features.py ..                         [  0%]
tests/test_constants.py .                               [  1%]
tests/test_filters.py .................x.....           [ 11%]
tests/test_generic.py ................................. [ 25%]
.............                                           [ 30%]
tests/test_javascript.py ..                             [ 31%]
tests/test_merger.py .                                  [ 32%]
tests/test_page.py .........................            [ 42%]
tests/test_pagerange.py ................                [ 49%]
tests/test_papersizes.py ..................             [ 57%]
tests/test_reader.py .................................. [ 72%]
...............                                         [ 78%]
tests/test_utils.py ....................                [ 87%]
tests/test_workflows.py ..........                      [ 91%]
tests/test_writer.py .................                  [ 98%]
tests/test_xmp.py ...                                   [100%]

========== 232 passed, 1 xfailed, 1 warning in 4.52s ==========

Dev Intro (`docs/dev/intro.md`)

Developer Intro

pypdf is a library and hence its users are developers. This document is not for
the users, but for people who want to work on pypdf itself.

{note}
Our CI (continuous integration) validates that relevant standards are met with your contribution.
Especially for regular contributors or larger changes, it is highly recommended that you set up your own development environment
to already cover the most important aspects locally. This greatly helps us to reduce the noise compared to when you open an untested
PR early and use our CI to do your debugging and improvements from there. The maintainers usually receive a notification on every push
to a branch where a corresponding PR is open, possibly hiding important notifications.

Installing Requirements

text
pip install -r requirements/dev.txt

Running Tests

See testing pypdf with pytest.

The sample-files git submodule

The reason for having the submodule sample-files is that we want to keep
the size of the pypdf repository small while we also want to have an extensive
test suite. Those two goals contradict each other.

The resources folder should contain a select set of core examples that cover
most cases we typically want to test for. The sample-files might cover a lot
more edge cases, the behavior we get when file sizes get bigger, different
PDF producers.

To get the sample-files folder, you need to execute:

text
git submodule update --init

Tools: git and pre-commit

Git is a command line application for version control. If you don't know it,
you can play ohmygit to learn it.

GitHub is the service where the pypdf project is hosted. While git is free and
open source, GitHub is a paid service by Microsoft, but free in a lot of
cases.

pre-commit is a command line application
that uses git hooks to automatically execute code. This allows you to avoid
style issues and other code quality issues. After you entered pre-commit install
once in your local copy of pypdf, it will automatically be executed when
you git commit.

Commit Messages

Having a clean commit message helps people to quickly understand what the commit
is about, without actually looking at the changes. The first line of the
commit message is used to auto-generate the CHANGELOG.
For this reason, the format should be:

text
PREFIX: DESCRIPTION

BODY

The PREFIX can be:

  • SEC: Security improvements. Typically, an infinite loop that was possible.
    Usually only used by the maintainers - others should report possible issues in
    this category privately first and only push a public change if told so.
  • BUG: A bug was fixed. Likely there are one or multiple issues. Then write in
    the BODY: Closes #123 where 123 is the issue number on GitHub.
    It would be absolutely amazing if you could write a regression test in those
    cases. That is a test that would fail without the fix.
    A bug is always an issue for pypdf users - test code or CI that was fixed is
    not considered a bug here.
  • ENH: A new feature! Describe in the body what it can be used for.
  • DEP: Deprecation. Either marking something as "this is going to be removed"
    or actually removing it.
  • PI: A performance improvement. This could also be a reduction in the
    file size of PDF files generated by pypdf.
  • ROB: A robustness change. Dealing better with broken PDF files.
  • DOC: A documentation change.
  • TST: Adding or adjusting tests.
  • DEV: Developer experience improvements, e.g., pre-commit or setting up CI.
  • MAINT: Quite a lot of different stuff. Performance improvements are, for sure,
    the most interesting changes in here. Refactorings as well.
  • STY: A style change. Something that makes pypdf code more consistent.
    Typically, a small change. It could also be better error messages for
    end users.

The prefix is used to generate the CHANGELOG. Every PR must have exactly one -
if you feel like several match, take the top one from this list that matches for
your PR.

Pull Request Size

Smaller Pull Requests (PRs) are preferred as it's typically easier to merge
them. For example, if you have some typos, a few code-style changes, a new
feature, and a bug-fix, that could be three or four PRs.

A PR must be complete. That means if you introduce a new feature, it must be
finished within the PR and have a test for that feature.

Benchmarks

We need to keep an eye on performance, and thus we have a few benchmarks.

See py-pdf.github.io/pypdf/dev/bench


Index (`docs/index.rst`)

.. pypdf documentation main file, created by
sphinx-quickstart on Thu Apr 7 20:13:19 2022.
You can adapt this file completely to your liking, but it should at least
contain the root toctree directive.

Welcome to pypdf

pypdf is a free <https://en.wikipedia.org/wiki/Free_software>_ and open
source pure-python PDF library capable of splitting,
merging, cropping, and transforming the pages of PDF files. It can also add
custom data, viewing options, and passwords to PDF files.
pypdf can retrieve text and metadata from PDFs as well.

See pdfly <https://github.com/py-pdf/pdfly>_ for a CLI application that uses pypdf to interact with PDFs.

You can contribute to pypdf on GitHub <https://github.com/py-pdf/pypdf>_.

.. toctree::
:caption: User Guide
:maxdepth: 1

user/installation
user/robustness
user/security
user/suppress-warnings
user/metadata
user/extract-text
user/post-processing-in-text-extraction
user/extract-images
user/handle-attachments
user/encryption-decryption
user/merging-pdfs
user/cropping-and-transforming
user/reading-pdf-annotations
user/adding-pdf-annotations
user/add-watermark
user/add-javascript
user/viewer-preferences
user/forms
user/handling-outlines
user/streaming-data
user/file-size
user/pdf-version-support
user/pdfa-compliance

.. toctree::
:caption: API Reference
:maxdepth: 1

modules/PdfReader
modules/PdfWriter
modules/Destination
modules/DocumentInformation
modules/Field
modules/Fit
modules/PageObject
modules/PageRange
modules/PaperSize
modules/RectangleObject
modules/Transformation
modules/XmpInformation
modules/actions
modules/annotations
modules/constants
modules/configuration
modules/errors
modules/generic
modules/PdfDocCommon

.. toctree::
:caption: Developer Guide
:maxdepth: 1

dev/intro
dev/pdf-format
dev/pypdf-parsing
dev/pypdf-writing
dev/cmaps
dev/deprecations
dev/documentation
dev/testing
dev/releasing

.. toctree::
:caption: About pypdf
:maxdepth: 1

meta/CHANGELOG
meta/changelog-v1
meta/migration-1-to-2
meta/project-governance
meta/taking-ownership
meta/history
meta/CONTRIBUTORS
meta/scope-of-pypdf
meta/comparisons
meta/faq

Indices and tables

  • :ref:genindex
  • :ref:modindex
  • :ref:search

Dev Cmaps (`docs/dev/cmaps.md`)

CMaps

Looking at the cmap of "crazyones":

bash
pdftk crazyones.pdf output crazyones-uncomp.pdf uncompress

You can see this:

text
begincmap
/CMapName /T1Encoding-UTF16 def
/CMapType 2 def
/CIDSystemInfo <<
  /Registry (Adobe)
  /Ordering (UCS)
  /Supplement 0
>> def
1 begincodespacerange
<00> <FF>
endcodespacerange
1 beginbfchar
<1B> <FB00>
endbfchar
endcmap
CMapName currentdict /CMap defineresource pop

codespacerange

A codespacerange maps a complete sequence of bytes to a range of Unicode glyphs.
It defines a starting point:

text
1 beginbfchar
<1B> <FB00>

That means that 1B (Hex for 27) maps to the Unicode character FB00 - the ligature ff (two lowercase f's).

The two numbers in begincodespacerange mean that it starts with an offset of
0 (hence from 1B ➜ FB00) up to an offset of FF (dec: 255), hence 1B+FF = 282
➜ FBFF.

Within the text stream, there is

text
(The)-342(mis\034ts.)

\034 is octal for the decimal value 28.


Dev Deprecations (`docs/dev/deprecations.md`)

The Deprecation Process

pypdf strives to be an excellent library for its current users and for new
ones. We are careful with introducing potentially breaking changes, but we
will do them if they provide value for the community in the long run.

We hope and think that deprecations will not happen frequently. If they do,
users can rely on the following procedure.

Semantic Versioning

pypdf uses semantic versioning. If you want to avoid
breaking changes, please use dependency pinning (also known as version pinning).
In Python, this is done by specifying the exact version you want to use in a
requirements.txt file. A tool that can support you is pip-compile from
pip-tools.

If you are using Poetry it is done with the
poetry.lock file.

How pypdf deprecates features

Assume the current version of pypdf is x.y.z. After a discussion (e.g., via
GitHub issues), we decided to remove a class / function / method. This is how
we do it:

  1. x.y.(z+1): Add a DeprecationWarning. If there is a replacement,
    the replacement is also introduced and the warning informs about the change
    and when it will happen.
    The docs let users know about the deprecation and when it will happen and the new function.
    The CHANGELOG informs about it.
  2. (x+1).0.0: Remove / change the code in the breaking way by replacing
    DeprecationWarnings by DeprecationErrors.
    We do this to help people who didn't look at the warnings before.
    The CHANGELOG informs about it.
  3. (x+2).0.0: The DeprecationErrors are removed.

This means the users have three warnings in the CHANGELOG, a DeprecationWarning
until the next major release and a DeprecationError until the major release
after that.

Please note that adding warnings can be a breaking change for some users; most
likely just in the CI.
This means it needs to be properly documented.


Dev Documentation (`docs/dev/documentation.md`)

Documentation

This documentation is build with Sphinx and
hosted by Read the Docs

Testing code snippets

Almost all python code snippets in documentation tested using Sphinx's extension
sphinx.ext.doctest.
This allows to make sure that we have no typos, missed imports and other problems in:

  • code snippets marked with testcode directive in *.md files
  • code snippets from python's docstrings imported via autoclass directive in *.rst files

CI pipeline is configured run Sphinx's doctest build automatically for each PR.
It is also possible to run it locally:

  1. First you need to install docs requirements

    bash
    pip install -r requirements/docs.txt
  2. Change current directory

    bash
    cd docs
  3. Run doctest build. It uses indirectly sphinx-build command line tool
    installed with docs requirements. See
    Sphinx's docs
    for details.

    bash
    make doctest
  4. If everything is okay you should see in output Doctest summary without failures

API Reference

Method / Function Docstrings

We use Google-Style Docstrings:

text
def example(param1: int, param2: str) -> bool:
    """
    Example function with PEP 484 type annotations.

    Args:
      param1: The first parameter.
      param2: The second parameter.

    Returns:
      The return value. True for success, False otherwise.

    Raises:
      AttributeError: The ``Raises`` section is a list of all exceptions
        that are relevant to the interface.
      ValueError: If `param2` is equal to `param1`.

    Examples:
        Examples should be written in doctest format, and should illustrate how
        to use the function.

        >>> print([i for i in example_generator(4)])
        [0, 1, 2, 3]
    """
  • The order of sections is (1) Args (2) Returns (3) Raises (4) Examples
  • If there is no return value, remove the 'Returns' block
  • Properties should not have any sections

Issues and PRs

An issue can be used to discuss what we want to achieve.

A PR can be used to discuss how we achieve it.

Commit Messages

We want to have descriptive commits in the main branch. For this reason, every
pull request (PR) is squashed. That means no matter how many commits a PR has,
in the end only one combined commit will be in main.

The title of the PR will be used as the first line of that combined commit message.

The first comment within the commit will be used as the message body.

See developer intro for more details.


Dev Pdf Format (`docs/dev/pdf-format.md`)

The PDF Format

It is recommended to look in the PDF specification for details and clarifications.

{note}
We currently generate files with a header for PDF 1.3 by default. At the same time, we strive
to support the PDF 1.7 specification.

Features specific to PDF 2.0 might be available, but we always ensure that older versions do
not break due to the rather limited general PDF 2.0 support in the wild and to not break for
old PDF files. For this reason, some historical aspects (like insecure encryption algorithms)
are required to be supported, although PDF 2.0 deprecates most of them and allows more secure
variants.

Below is only intended to give a very rough overview of the format.

Overall Structure

A PDF consists of:

  1. Header: Contains the version of the PDF, e.g. %PDF-1.7
  2. Body: Contains a sequence of indirect objects
  3. Cross-reference table (xref): Contains a list of the indirect objects in the body
  4. Trailer

The xref table

A cross-reference table (xref) is a table of the indirect objects in the body.
It allows quick access to those objects by pointing to their location in the file.

It looks like this:

text
xref 42 5
0000001000 65535 f
0000001234 00000 n
0000001987 00000 n
0000011987 00000 n
0000031987 00000 n

Let's go through it step-by-step:

  • xref is just a keyword that specifies the start of the xref table.
  • 42 is the numerical ID of the first object in this xref section; 5 is the number of entries in the xref table.
  • Now every object has 3 entries nnnnnnnnnn ggggg n: a 10-digit byte offset,
    a 5-digit generation number, and a literal keyword which is either n or f.
    • nnnnnnnnnn is the byte offset of the object. It tells the reader where
      the object is in the file.
    • ggggg is the generation number. It tells the reader how old the object is.
    • n means that the object is a normal in-use object, f means that the object
      is a free object.
      • The first free object always has a generation number of 65535. It forms
        the head of a linked-list of all free objects.
      • The generation number of a normal object is always 0. The generation
        number allows the PDF format to contain multiple versions of the same
        object. This is a version history mechanism.

The body

The body is a sequence of indirect objects:

counter generation_number << the_object >> endobj

  • counter (integer) is a unique identifier for the object.
  • generation_number (integer) is the generation number of the object.
  • the_object is the object itself. It can be empty. Starts with /Keyword to
    specify which kind of object it is.
  • endobj marks the end of the object.

A concrete example can be found in test_reader.py::test_get_images_raw:

text
1 0 obj << /Count 1 /Kids [4 0 R] /Type /Pages >> endobj
2 0 obj << >> endobj
3 0 obj << >> endobj
4 0 obj << /Contents 3 0 R /CropBox [0.0 0.0 2550.0 3508.0]
 /MediaBox [0.0 0.0 2550.0 3508.0] /Parent 1 0 R
 /Resources << /Font << >> >>
 /Rotate 0 /Type /Page >> endobj
5 0 obj << /Pages 1 0 R /Type /Catalog >> endobj

The trailer

The trailer looks like this:

text
trailer << /Root 5 0 R
           /Size 6
        >>
startxref 1234
%%EOF

Let's go through it:

  • trailer << indicates that the trailer dictionary starts. It ends with >>.
  • startxref is a keyword followed by the byte-location of the xref keyword.
    As the trailer is always at the bottom of the file, this allows readers to
    quickly find the xref table.
  • %%EOF is the end-of-file marker.

The trailer dictionary is a key-value list. The keys are specified in
Table 15 of the PDF Reference 1.7, e.g. /Root and /Size (both are required).

  • /Root (dictionary) contains the document catalog.
    • The 5 is the object number of the catalog dictionary.
    • 0 is the generation number of the catalog dictionary.
    • R is the keyword that indicates that the object is a reference to the
      catalog dictionary.
  • /Size (integer) contains the total number of entries in the files xref table.

Reading PDF files

Most PDF files are compressed. If you want to read them, first uncompress them:

bash
pdftk crazyones.pdf output crazyones-uncomp.pdf uncompress

Then rename crazyones-uncomp.pdf to crazyones-uncomp.txt and open it in
your favorite IDE / text editor.


Dev Pypdf Parsing (`docs/dev/pypdf-parsing.md`)

How pypdf parses PDF files

pypdf uses {class}~pypdf.PdfReader to parse PDF files.
The method {py:meth}PdfReader.read shows the basic
structure of parsing:

  1. Finding and reading the cross-reference tables / trailer: The
    cross-reference table (xref table) is a table of byte offsets that indicate
    the locations of objects within the file. The trailer provides additional
    information such as the root object (Catalog) and the Info object containing
    metadata.
  2. Parsing the objects: After locating the xref table and the trailer, pypdf
    proceeds to parse the objects in the PDF. Objects in a PDF can be of various
    types such as dictionaries, arrays, streams, and simple data types (e.g.,
    integers, strings). pypdf parses these objects and stores them in
    {py:meth}PdfReader.resolved_objects ,
    populated by {py:meth}cache_indirect_object .
  3. Decoding content streams: The content of a PDF is typically stored in
    content streams, which are sequences of PDF operators and operands. pypdf
    decodes these content streams by applying filters (e.g., FlateDecode,
    LZWDecode) specified in the stream's dictionary. This is only done when the
    object is requested by {py:meth}PdfReader.get_object
    which uses the PdfReader._get_object_from_stream method.

References

PDF 1.7 specification:

  • 7.5 File Structure
  • 7.5.4 Cross-Reference Table
  • 7.8 Content Streams and Resources

Dev Pypdf Writing (`docs/dev/pypdf-writing.md`)

How pypdf writes PDF files

pypdf uses {py:class}PdfWriter to write PDF files. pypdf has
{py:class}PdfObject and several subclasses with the
{py:meth}write_to_stream method.
The {py:meth}PdfWriter.write method uses the
write_to_stream methods of the referenced objects.

The {py:meth}PdfWriter.write_stream method
has the following core steps:

  1. _sweep_indirect_references: This step ensures that any circular references
    to objects are correctly handled. It adds the object reference numbers of any
    circularly referenced objects to an external reference map, so that
    self-page-referencing trees can reference the correct new object location,
    rather than copying in a new copy of the page object.
  2. Write the File Header and Body with _write_pdf_structure: In this step,
    the PDF header and objects are written to the output stream. This includes
    the PDF version (e.g., %PDF-1.7) and the objects that make up the content of
    the PDF, such as pages, annotations, and form fields. The locations (byte
    offsets) of these objects are stored for later use in generating the xref
    table.
  3. Write the Cross-Reference Table with _write_xref_table: Using the stored
    object locations, this step generates and writes the cross-reference table
    (xref table) to the output stream. The cross-reference table contains the
    byte offsets for each object in the PDF file, allowing for quick random
    access to objects when reading the PDF.
  4. Write the File Trailer with _write_trailer: The trailer is written to
    the output stream in this step. The trailer contains essential information,
    such as the number of objects in the PDF, the location of the root object
    (Catalog), and the Info object containing metadata. The trailer also
    specifies the location of the xref table.

How others do it

Looking at alternative software designs and implementations can help to improve
our choices.

fpdf2

fpdf2 has a PDFObject class
with a serialize method which roughly maps to pypdf.PdfObject.write_to_stream.
Some other similarities include:

pdfrw

pdfrw, in contrast, seems to work more with
the standard Python objects (bool, float, string) and not wrap them in custom
objects, if possible. It still has:

The core classes of pdfrw are
PdfReader
and
PdfWriter


Dev Releasing (`docs/dev/releasing.md`)

Releasing

A pypdf release contains the following artifacts:

Who does it?

pypdf should typically only be released by one of the core maintainers / the
core maintainer. At the moment, this usually is stefan6419846.

Any owner of the py-pdf organization also has the technical permissions to
release.

How is it done?

With direct push permissions

This is the typical way for the core maintainer/benevolent dictator.

The release contains the following steps:

  1. Update the CHANGELOG.md and the _version.py via python make_release.py.
    This also prepares the release commit message.
  2. Create a release commit: git commit -eF RELEASE_COMMIT_MSG.md.
  3. Push commit: git push.
  4. Create the tag: git tag -s 6.7.1 -eF RELEASE_COMMIT_MSG.md.
  5. Push the tag: git push origin 6.7.1.
  6. CI now builds a source and a wheels package which it pushes to PyPI. It also
    creates the corresponding GitHub release.

Using a Pull Request

This is the typical way for collaborators which do not have direct push permissions for
the main branch.

The release contains the following steps:

  1. Update the CHANGELOG.md and the _version.py via python make_release.py.
    This also prepares the release commit message.
  2. Push the changes to a dedicated branch.
  3. Open a pull request starting with REL: , followed by the new version number.
  4. Wait for the approval of another eligible maintainer.
  5. Merge the pull request with the name being the PR title and the body being
    the content of RELEASE_COMMIT_MSG.md.
  6. Create the tag: git tag -s 6.7.1 -eF RELEASE_COMMIT_MSG.md.
  7. Push the tag: git push origin 6.7.1.
  8. CI now builds a source and a wheels package which it pushes to PyPI. It also
    creates the corresponding GitHub release.

The Release Tag

  • Use the release version as the tag name. No need for a leading "v".
  • Use the changelog entry as the body.

When are releases done?

There is no need to wait for anything. If the CI is green (all tests succeeded),
we can release.

At the moment, there is no fixed release cycle.


Dev Testing (`docs/dev/testing.md`)

Testing

pypdf uses pytest for testing.

To run the tests, you need to install the CI (Continuous Integration) requirements by running pip install -r requirements/ci.txt or
pip install -r requirements/ci-3.11.txt if running Python ≥ 3.11.

Deselecting groups of tests

pypdf makes use of the following pytest markers:

  • slow: Tests that require more than 5 seconds.
  • samples: Tests that require the sample-files git submodule to be initialized. As of October 2022, this is about 25 MB.
  • 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.
    • To successfully run the tests, please download most of the documents beforehand: python -c "from tests import download_test_pdfs; download_test_pdfs()"

You can disable them by pytest -m "not enable_socket" or pytest -m "not samples".
You can even disable all of them: pytest -m "not enable_socket" -m "not samples" -m "not slow".

Please note that this reduces test coverage. The CI will always test all files.

Docstrings in Unit tests

The first line of a docstring in a unit test should be written in a way that
you could prefix it with "This tests ensures that ...", e.g.

  • Invalid XML in xmp_metadata is gracefully handled.
  • The identity is returning its input.
  • xmp_modify_date is extracted correctly.

This way, plugins like pytest-testdox
can generate really nice output when the tests are running. This looks similar
to the output of mocha.js.

If the test is a regression test, write

This test is a regression test for issue #1234

If the regression test is just one parameter of other tests, then add it as
a comment for that parameter.

Evaluate a PR in-progress version

You may want to test a version from a PR which has not been released yet.
The easiest way is to use pip and install a version from git:

a) Go the PR and identify the repository and branch.

Example from below : repository: pubpub-zz / branch: iss2200 :

b) you can then install the version using pip from git:

Example:

text
pip install git+https://github.com/pubpub-zz/pypdf.git@iss2200

Meta Changelog V1 (`docs/meta/changelog-v1.md`)

Changelog of PyPDF2 1.X

Version 1.28.4, 2022-05-29

Bug Fixes (BUG):

  • XmpInformation._converter_date was unusable (#921)

Full Changelog

Version 1.28.3, 2022-05-28

Deprecations (DEP)

  • PEP8 renaming (#905)

Bug Fixes (BUG)

  • XmpInformation missing method _getText (#917)
  • Fix PendingDeprecationWarning on _merge_page (#904)

Full Changelog

Version 1.28.2, 2022-05-23

Bug Fixes (BUG)

  • PendingDeprecationWarning for getContents (#893)
  • PendingDeprecationWarning on using PdfMerger (#891)

Full Changelog

Version 1.28.1, 2022-05-22

Bug Fixes (BUG)

  • Incorrectly show deprecation warnings on internal usage (#887)

Maintenance (MAINT)

  • Add stacklevel=2 to deprecation warnings (#889)
  • Remove duplicate warnings imports (#888)

Full Changelog

Version 1.28.0, 2022-05-22

This release adds a lot of deprecation warnings in preparation of the
PyPDF2 2.0.0 release. The changes are mostly using snake_case function-, method-,
and variable-names as well as using properties instead of getter-methods.

Maintenance (MAINT):

  • Remove IronPython Fallback for zlib (#868)

Full Changelog

Deprecations (DEP)

  • Make the PyPDF2.utils module private
  • Rename of core classes:
    • PdfFileReader ➔ PdfReader
    • PdfFileWriter ➔ PdfWriter
    • PdfFileMerger ➔ PdfMerger
  • Use PEP8 conventions for function names and parameters
  • If a property and a getter-method are both present, use the property

Details

In many places:

  • getObject ➔ get_object
  • writeToStream ➔ write_to_stream
  • readFromStream ➔ read_from_stream

PyPDF2.generic

  • readObject ➔ read_object
  • convertToInt ➔ convert_to_int
  • DocumentInformation.getText ➔ DocumentInformation._get_text :
    This method should typically not be used; please let me know if you need it.

PdfReader class:

  • reader.getPage(pageNumber) ➔ reader.pages[page_number]
  • reader.getNumPages() / reader.numPages ➔ len(reader.pages)
  • getDocumentInfo ➔ metadata
  • flattenedPages attribute ➔ flattened_pages
  • resolvedObjects attribute ➔ resolved_objects
  • xrefIndex attribute ➔ xref_index
  • getNamedDestinations / namedDestinations attribute ➔ named_destinations
  • getPageLayout / pageLayout ➔ page_layout attribute
  • getPageMode / pageMode ➔ page_mode attribute
  • getIsEncrypted / isEncrypted ➔ is_encrypted attribute
  • getOutlines ➔ get_outlines
  • readObjectHeader ➔ read_object_header
  • cacheGetIndirectObject ➔ cache_get_indirect_object
  • cacheIndirectObject ➔ cache_indirect_object
  • getDestinationPageNumber ➔ get_destination_page_number
  • readNextEndLine ➔ read_next_end_line
  • _zeroXref ➔ _zero_xref
  • _authenticateUserPassword ➔ _authenticate_user_password
  • _pageId2Num attribute ➔ _page_id2num
  • _buildDestination ➔ _build_destination
  • _buildOutline ➔ _build_outline
  • _getPageNumberByIndirect(indirectRef) ➔ _get_page_number_by_indirect(indirect_ref)
  • _getObjectFromStream ➔ _get_object_from_stream
  • _decryptObject ➔ _decrypt_object
  • _flatten(..., indirectRef) ➔ _flatten(..., indirect_ref)
  • _buildField ➔ _build_field
  • _checkKids ➔ _check_kids
  • _writeField ➔ _write_field
  • _write_field(..., fieldAttributes) ➔ _write_field(..., field_attributes)
  • _read_xref_subsections(..., getEntry, ...) ➔ _read_xref_subsections(..., get_entry, ...)

PdfWriter class:

  • writer.getPage(pageNumber) ➔ writer.pages[page_number]
  • writer.getNumPages() ➔ len(writer.pages)
  • addMetadata ➔ add_metadata
  • addPage ➔ add_page
  • addBlankPage ➔ add_blank_page
  • addAttachment(fname, fdata) ➔ add_attachment(filename, data)
  • insertPage ➔ insert_page
  • insertBlankPage ➔ insert_blank_page
  • appendPagesFromReader ➔ append_pages_from_reader
  • updatePageFormFieldValues ➔ update_page_form_field_values
  • cloneReaderDocumentRoot ➔ clone_reader_document_root
  • cloneDocumentFromReader ➔ clone_document_from_reader
  • getReference ➔ get_reference
  • getOutlineRoot ➔ get_outline_root
  • getNamedDestRoot ➔ get_named_dest_root
  • addBookmarkDestination ➔ add_bookmark_destination
  • addBookmarkDict ➔ add_bookmark_dict
  • addBookmark ➔ add_bookmark
  • addNamedDestinationObject ➔ add_named_destination_object
  • addNamedDestination ➔ add_named_destination
  • removeLinks ➔ remove_links
  • removeImages(ignoreByteStringObject) ➔ remove_images(ignore_byte_string_object)
  • removeText(ignoreByteStringObject) ➔ remove_text(ignore_byte_string_object)
  • addURI ➔ add_uri
  • addLink ➔ add_link
  • getPage(pageNumber) ➔ get_page(page_number)
  • getPageLayout / setPageLayout / pageLayout ➔ page_layout attribute
  • getPageMode / setPageMode / pageMode ➔ page_mode attribute
  • _addObject ➔ _add_object
  • _addPage ➔ _add_page
  • _sweepIndirectReferences ➔ _sweep_indirect_references

PdfMerger class

  • __init__ parameter: strict=True ➔ strict=False (the PdfFileMerger still has the old default)
  • addMetadata ➔ add_metadata
  • addNamedDestination ➔ add_named_destination
  • setPageLayout ➔ set_page_layout
  • setPageMode ➔ set_page_mode

Page class:

  • artBox / bleedBox/ cropBox/ mediaBox / trimBox ➔ artbox / bleedbox/ cropbox/ mediabox / trimbox
    • getWidth, getHeight ➔ width / height
    • getLowerLeft_x / getUpperLeft_x ➔ left
    • getUpperRight_x / getLowerRight_x ➔ right
    • getLowerLeft_y / getLowerRight_y ➔ bottom
    • getUpperRight_y / getUpperLeft_y ➔ top
    • getLowerLeft / setLowerLeft ➔ lower_left property
    • upperRight ➔ upper_right
  • mergePage ➔ merge_page
  • rotateClockwise / rotateCounterClockwise ➔ rotate_clockwise
  • _mergeResources ➔ _merge_resources
  • _contentStreamRename ➔ _content_stream_rename
  • _pushPopGS ➔ _push_pop_gs
  • _addTransformationMatrix ➔ _add_transformation_matrix
  • _mergePage ➔ _merge_page

XmpInformation class:

  • getElement(..., aboutUri, ...) ➔ get_element(..., about_uri, ...)
  • getNodesInNamespace(..., aboutUri, ...) ➔ get_nodes_in_namespace(..., aboutUri, ...)
  • _getText ➔ _get_text

utils.py:

  • matrixMultiply ➔ matrix_multiply
  • RC4_encrypt is moved to the security module

Version 1.27.12, 2022-05-02

Bug Fixes (BUG)

  • _rebuild_xref_table expects trailer to be a dict (#857)

Documentation (DOC)

  • Security Policy

Full Changelog

Version 1.27.11, 2022-05-02

Bug Fixes (BUG)

  • Incorrectly issued xref warning/exception (#855)

Full Changelog

Version 1.27.10, 2022-05-01

Robustness (ROB)

  • Handle missing destinations in reader (#840)
  • warn-only in readStringFromStream (#837)
  • Fix corruption in startxref or xref table (#788 and #830)

Documentation (DOC)

  • Project Governance (#799)
  • History of PyPDF2
  • PDF feature/version support (#816)
  • More details on text parsing issues (#815)

Developer Experience (DEV)

  • Add benchmark command to Makefile
  • Ignore IronPython parts for code coverage (#826)

Maintenance (MAINT)

  • Split pdf module (#836)
  • Separated CCITTFax param parsing/decoding (#841)
  • Update requirements files

Testing (TST)

  • Use external repository for larger/more PDFs for testing (#820)
  • Swap incorrect test names (#838)
  • Add test for PdfFileReader and page properties (#835)
  • Add tests for PyPDF2.generic (#831)
  • Add tests for utils, form fields, PageRange (#827)
  • Add test for ASCII85Decode (#825)
  • Add test for FlateDecode (#823)
  • Add test for filters.ASCIIHexDecode (#822)

Code Style (STY)

  • Apply pre-commit (black, isort) + use snake_case variables (#832)
  • Remove debug code (#828)
  • Documentation, Variable names (#839)

Full Changelog

Version 1.27.9, 2022-04-24

A change I would like to highlight is the performance improvement for
large PDF files (#808) 🎉

New Features (ENH)

  • Add papersizes (#800)
  • Allow setting permission flags when encrypting (#803)
  • Allow setting form field flags (#802)

Bug Fixes (BUG)

  • TypeError in xmp._converter_date (#813)
  • Improve spacing for text extraction (#806)
  • Fix PDFDocEncoding Character Set (#809)

Robustness (ROB)

  • Use null ID when encrypted but no ID given (#812)
  • Handle recursion error (#804)

Documentation (DOC)

  • CMaps (#811)
  • The PDF Format + commit prefixes (#810)
  • Add compression example (#792)

Developer Experience (DEV)

  • Add Benchmark for Performance Testing (#781)

Maintenance (MAINT)

  • Validate PDF magic byte in strict mode (#814)
  • Make PdfFileMerger.addBookmark() behave life PdfFileWriters' (#339)
  • Quadratic runtime while parsing reduced to linear (#808)

Testing (TST)

  • Newlines in text extraction (#807)

Full Changelog

Version 1.27.8, 2022-04-21

Bug Fixes (BUG)

  • Use 1MB as offset for readNextEndLine (#321)
  • 'PdfFileWriter' object has no attribute 'stream' (#787)

Robustness (ROB)

  • Invalid float object; use 0 as fallback (#782)

Documentation (DOC)

  • Robustness (#785)

Full Changelog

Version 1.27.7, 2022-04-19

Bug Fixes (BUG)

  • Import exceptions from PyPDF2.errors in PyPDF2.utils (#780)

Code Style (STY)

  • Naming in 'make_changelog.py'

Version 1.27.6, 2022-04-18

Deprecations (DEP)

  • Remove support for Python 2.6 and older (#776)

New Features (ENH)

  • Extract document permissions (#320)

Bug Fixes (BUG)

  • Clip by trimBox when merging pages, which would otherwise be ignored (#240)
  • Add overwriteWarnings parameter PdfFileMerger (#243)
  • IndexError for getPage() of decrypted file (#359)
  • Handle cases where decodeParms is an ArrayObject (#405)
  • Updated PDF fields don't show up when page is written (#412)
  • Set Linked Form Value (#414)
  • Fix zlib -5 error for corrupt files (#603)
  • Fix reading more than last1K for EOF (#642)
  • Accidental import

Robustness (ROB)

  • Allow extra whitespace before "obj" in readObjectHeader (#567)

Documentation (DOC)

  • Link to pdftoc in Sample_Code (#628)
  • Working with annotations (#764)
  • Structure history

Developer Experience (DEV)

  • Add issue templates (#765)
  • Add tool to generate changelog

Maintenance (MAINT)

  • Use grouped constants instead of string literals (#745)
  • Add error module (#768)
  • Use decorators for @staticmethod (#775)
  • Split long functions (#777)

Testing (TST)

  • Run tests in CI once with -OO Flags (#770)
  • Filling out forms (#771)
  • Add tests for Writer (#772)
  • Error cases (#773)
  • Check Error messages (#769)
  • Regression test for issue #88
  • Regression test for issue #327

Code Style (STY)

  • Make variable naming more consistent in tests

Full changelog

Version 1.27.5, 2022-04-15

Security (SEC)

  • ContentStream_readInlineImage had potential infinite loop (#740)

Bug fixes (BUG)

  • Fix merging encrypted files (#757)
  • CCITTFaxDecode decodeParms can be an ArrayObject (#756)

Robustness improvements (ROBUST)

  • title sometimes None (#744)

Documentation (DOC)

  • Adjust short description of the package

Tests and Test setup (TST)

  • Rewrite JS tests from unittest to pytest (#746)
  • Increase Test coverage, mainly with filters (#756)
  • Add test for inline images (#758)

Developer Experience Improvements (DEV)

  • Remove unused Travis-CI configuration (#747)
  • Show code coverage (#754, #755)
  • Add mutmut (#760)

Miscellaneous

  • STY: Closing file handles, explicit exports, ... (#743)

Full Changelog

Version 1.27.4, 2022-04-12

Bug fixes (BUG)

  • Guard formatting of __init__.__doc__ string (#738)

Packaging (PKG)

  • Add more precise license field to setup (#733)

Testing (TST)

  • Add test for issue #297

Miscellaneous

  • DOC: Miscallenious ➔ Miscellaneous (Typo)
  • TST: Fix CI triggering (master ➔ main) (#739)
  • STY: Fix various style issues (#742)

Full Changelog

Version 1.27.3, 2022-04-10

  • PKG: Make Tests not a subpackage (#728)
  • BUG: Fix ASCII85Decode.decode assertion (#729)
  • BUG: Error in Chinese character encoding (#463)
  • BUG: Code duplication in Scripts/2-up.py
  • ROBUST: Guard 'obj.writeToStream' with 'if obj is not None'
  • ROBUST: Ignore a /Prev entry with value 0 in the trailer
  • MAINT: Remove Sample_Code (#726)
  • TST: Close file handle in test_writer (#722)
  • TST: Fix test_get_images (#730)
  • DEV: Make tox use pytest and add more Python versions (#721)
  • DOC: Many (#720, #723-725, #469)

Full Changelog

Version 1.27.2, 2022-04-09

  • Add Scripts (including pdfcat), Resources, Tests, and Sample_Code back to
    PyPDF2. It was removed by accident in 1.27.0, but might get removed with 2.0.0
    See discussions/718.

Full Changelog

Version 1.27.1, 2022-04-08

  • Fixed project links on PyPI page after migration from mstamy2
    to MartinThoma to the py-pdf organization on GitHub
  • Documentation is now at pypdf2.readthedocs.io

Full Changelog

Version 1.27.0, 2022-04-07

Features:

  • Add alpha channel support for png files in Script (#614)

Bug fixes (BUG)

  • Fix formatWarning for filename without slash (#612)
  • Add whitespace between words for extractText() (#569, #334)
  • "invalid escape sequence" SyntaxError (#522)
  • Avoid error when printing warning in pythonw (#486)
  • Stream operations can be List or Dict (#665)

Documentation (DOC)

  • Added Scripts/pdf-image-extractor.py
  • Documentation improvements (#550, #538, #324, #426, #394)

Tests and Test setup (TST)

  • Add GitHub Action which automatically runs unit tests via pytest and
    static code analysis with Flake8 (#660)
  • Add several unit tests (#661, #663)
  • Add .coveragerc to create coverage reports

Developer Experience Improvements (DEV)

  • Pre commit: Developers can now pre-commit install to avoid tiny issues like trailing whitespaces

Miscellaneous

  • Add the LICENSE file to the distributed packages (#288)
  • Use setuptools instead of distutils (#599)
  • Improvements for the PyPI page (#644)
  • Python 3 changes (#504, #366)

Full Changelog

Version 1.26.0, 2016-05-18

  • NOTE: Active maintenance on PyPDF2 is resuming after a hiatus

  • Fixed a bug where image resources where incorrectly
    overwritten when merging pages

  • Added dictionary for JavaScript actions to the root (louib)

  • Added unit tests for the JS functionality (louib)

  • Add more Python 3 compatibility when reading inline images (im2703
    and (VyacheslavHashov)

  • Return NullObject instead of raising error when failing to resolve
    object (ctate)

  • Don't output warning for non-zeroed xref table when strict=False
    (BenRussert)

  • Remove extraneous zeroes from output formatting (speedplane)

  • Fix bug where reading an inline image would cut off prematurely
    in certain cases (speedplane)

Version 1.25.1, 2015-07-20

  • Fix bug when parsing inline images. Occurred when merging
    certain pages with inline images

  • Fixed type error when creating outlines by utilizing the
    isString() test

Version 1.25, 2015-07-07

BUGFIXES:

  • Added Python 3 algorithm for ASCII85Decode. Fixes issue when
    reading reportlab-generated files with Py 3 (jerickbixly)

  • Recognize more escape sequence which would otherwise throw an
    exception (manuelzs, robertsoakes)

  • Fixed overflow error in generic.py. Occurred
    when reading a too-large int in Python 2 (by Raja Jamwal)

  • Allow access to files which were encrypted with an empty
    password. Previously threw a "File has not been decrypted"
    exception (Elena Williams)

  • Do not attempt to decode an empty data stream. Previously
    would cause an error in decode algorithms (vladir)

  • Fixed some type issues specific to Py 2 or Py 3

  • Fix issue when stream data begins with whitespace (soloma83)

  • Recognize abbreviated filter names (AlmightyOatmeal and
    Matthew Weiss)

  • Copy decryption key from PdfFileReader to PdfFileMerger.
    Allows usage of PdfFileMerger with encrypted files (twolfson)

  • Fixed bug which occurred when a NameObject is present at end
    of a file stream. Threw a "Stream has ended unexpectedly"
    exception (speedplane)

FEATURES:

  • Initial work on a test suite; to be expanded in future.
    Tests and Resources directory added, README updated (robertsoakes)

  • Added document cloning methods to PdfFileWriter:
    appendPagesFromReader, cloneReaderDocumentRoot, and
    cloneDocumentFromReader. See official documentation (robertsoakes)

  • Added method for writing to form fields: updatePageFormFieldValues.
    This will be enhanced in the future. See official documentation
    (robertsoakes)

  • New addAttachment method. See documentation. Support for adding
    and extracting embedded files to be enhanced in the future
    (moshekaplan)

  • Added methods to get page number of given PageObject or
    Destination: getPageNumber and getDestinationPageNumber.
    See documentation (mozbugbox)

OTHER ENHANCEMENTS:

  • Enhanced type handling (Brent Amrhein)

  • Enhanced exception handling in NameObject (sbywater)

  • Enhanced extractText method output (peircej)

  • Better exception handling

  • Enhanced regex usage in NameObject class (speedplane)

Version 1.24, 2014-12-31

  • Bugfixes for reading files in Python 3 (by Anthony Tuininga and
    pqqp)

  • Appropriate errors are now raised instead of infinite loops (by
    naure and Cyrus Vafadari)

  • Bugfix for parsing number tokens with leading spaces (by Maxim
    Kamenkov)

  • Don't crash on bad /Outlines reference (by eshellman)

  • Conform tabs/spaces and blank lines to PEP 8 standards

  • Utilize the readUntilRegex method when reading Number Objects
    (by Brendan Jurd)

  • More bugfixes for Python 3 and clearer exception handling

  • Fixed encoding issue in merger (with eshellman)

  • Created separate folder for scripts

Version 1.23, 2014-08-11

  • Documentation now available at pythonhosted.org

  • Bugfix in pagerange.py for when __init__.__doc__ has no value (by
    Vladir Cruz)

  • Fix typos in OutlinesObject().add() (by shilluc)

  • Re-added a missing return statement in a utils.py method

  • Corrected viewing mode names (by Jason Scheirer)

  • New PdfFileWriter method: addJS() (by vfigueiro)

  • New bookmark features: color, boldness, italics, and page fit
    (by Joshua Arnott)

  • New PdfFileReader method: getFields(). Used to extract field
    information from PDFs with interactive forms. See documentation
    for details

  • Converted README file to markdown format (by Stephen Bussard)

  • Several improvements to overall performance and efficiency
    (by mozbugbox)

  • Fixed a bug where geospatial information was not scaling along with
    its page

  • Fixed a type issue and a Python 3 issue in the decryption algorithms
    (with Francisco Vieira and koba-ninkigumi)

  • Fixed a bug causing an infinite loop in the ASCII 85 decoding
    algorithm (by madmaardigan)

  • Annotations (links, comment windows, etc.) are now preserved when
    pages are merged together

  • Used the Destination class in addLink() and addBookmark() so that
    the page fit option could be properly customized

Version 1.22, 2014-05-29

  • Added .DS_Store to .gitignore (for Mac users) (by Steve Witham)

  • Removed __init__() implementation in NameObject (by Steve Witham)

  • Fixed bug (inf. loop) when merging pages in Python 3 (by commx)

  • Corrected error when calculating height in scaleTo()

  • Removed unnecessary code from DictionaryObject (by Georges Dubus)

  • Fixed bug where an exception was thrown upon reading a NULL string
    (by speedplane)

  • Allow string literals (non-unicode strings in Python 2) to be passed
    to PdfFileReader

  • Allow ConvertFunctionsToVirtualList to be indexed with slices and
    longs (in Python 2) (by Matt Gilson)

  • Major improvements and bugfixes to addLink() method (see documentation
    in source code) (by Henry Keiter)

  • General code clean-up and improvements (with Steve Witham and Henry Keiter)

  • Fixed bug that caused crash when comments are present at end of
    dictionary

Version 1.21, 2014-04-21

  • Fix for when /Type isn't present in the Pages dictionary (by Rob1080)

  • More tolerance for extra whitespace in Indirect Objects

  • Improved Exception handling

  • Fixed error in getHeight() method (by Simon Kaempflein)

  • implement use of utils.string_type to resolve Py2-3 compatibility issues

  • Prevent exception for multiple definitions in a dictionary (with carlosfunk)
    (only when strict = False)

  • Fixed errors when parsing a slice using pdfcat on command line (by
    Steve Witham)

  • Tolerance for EOF markers within 1024 bytes of the actual end of the
    file (with David Wolever)

  • Added overwriteWarnings parameter to PdfFileReader constructor, if False
    PyPDF2 will NOT overwrite methods from Python's warnings.py module with
    a custom implementation.

  • Fix NumberObject and NameObject constructors for compatibility with PyPy
    (Rüdiger Jungbeck, Xavier Dupré, shezadkhan137, Steven Witham)

  • Utilize utils.Str in pdf.py and pagerange.py to resolve type issues (by
    egbutter)

  • Improvements in implementing StringIO for Python 2 and BytesIO for
    Python 3 (by Xavier Dupré)

  • Added /x00 to Whitespaces, defined utils.WHITESPACES to clarify code (by
    Maxim Kamenkov)

  • Bugfix for merging 3 or more resources with the same name (by lucky-user)

  • Improvements to Xref parsing algorithm (by speedplane)

Version 1.20, 2014-01-27

  • Official Python 3+ support (with contributions from TWAC and cgammans)
    Support for Python versions 2.6 and 2.7 will be maintained

  • Command line concatenation (see pdfcat in sample code) (by Steve Witham)

  • New FAQ; link included in README

  • Allow more (although unnecessary) escape sequences

  • Prevent exception when reading a null object in decoding parameters

  • Corrected error in reading destination types (added a slash since they
    are name objects)

  • Corrected TypeError in scaleTo() method

  • addBookmark() method in PdfFileMerger now returns bookmark (so nested
    bookmarks can be created)

  • Additions to Sample Code and Sample PDFs

  • changes to allow 2up script to work (see sample code) (by Dylan McNamee)

  • changes to metadata encoding (by Chris Hiestand)

  • New methods for links: addLink() (by Enrico Lambertini) and removeLinks()

  • Bugfix to handle nested bookmarks correctly (by Jamie Lentin)

  • New methods removeImages() and removeText() available for PdfFileWriter
    (by Tien Haï)

  • Exception handling for illegal characters in Name Objects

Version 1.19, 2013-10-08

BUGFIXES:

  • Removed pop in sweepIndirectReferences to prevent infinite loop
    (provided by ian-su-sirca)

  • Fixed bug caused by whitespace when parsing PDFs generated by AutoCad

  • Fixed a bug caused by reading a 'null' ASCII value in a dictionary
    object (primarily in PDFs generated by AutoCad).

FEATURES:

  • Added new folders for PyPDF2 sample code and example PDFs; see README
    for each folder

  • Added a method for debugging purposes to show current location while
    parsing

  • Ability to create custom metadata (by jamma313)

  • Ability to access and customize document layout and view mode
    (by Joshua Arnott)

OTHER:

  • Added and corrected some documentation

  • Added some more warnings and exception messages

  • Removed old test/debugging code

UPCOMING:

  • More bugfixes (We have received many problematic PDFs via email, we
    will work with them)

  • Documentation - It's time for PyPDF2 to get its own documentation
    since it has grown much since the original pyPdf

  • A FAQ to answer common questions

Version 1.18, 2013-08-19

  • Fixed a bug where older versions of objects were incorrectly added to the
    cache, resulting in outdated or missing pages, images, and other objects
    (from speedplane)

  • Fixed a bug in parsing the xref table where new xref values were
    overwritten; also cleaned up code (from speedplane)

  • New method mergeRotatedAroundPointPage which merges a page while rotating
    it around a point (from speedplane)

  • Updated Destination syntax to respect PDF 1.6 specifications (from
    jamma313)

  • Prevented infinite loop when a PdfFileReader object was instantiated
    with an empty file (from Jerome Nexedi)

Other Changes:

  • Downloads now available via PyPI
  • Installation through pip library is fixed

Version 1.17, 2013-07-25

  • Removed one (from pdf.py) of the two Destination classes. Both
    classes had the same name, but were slightly different in content,
    causing some errors. (from Janne Vanhala)

  • Corrected and Expanded README file to demonstrate PdfFileMerger

  • Added filter for LZW encoded streams (from Michal Horejsek)

  • PyPDF2 issue tracker enabled on Github to allow community
    discussion and collaboration

Versions -1.16, -2013-06-30

  • Note: This ChangeLog has not been kept up-to-date for a while.
    Hopefully we can keep better track of it from now on. Some of the
    changes listed here come from previous versions 1.14 and 1.15; they
    were only vaguely defined. With the new _version.py file we should
    have more structured and better documented versioning from now on.

  • Defined PyPDF2.__version__

  • Fixed encrypt() method (from Martijn The)

  • Improved error handling on PDFs with truncated streams (from cecilkorik)

  • Python 3 support (from kushal-kumaran)

  • Fixed example code in README (from Jeremy Bethmont)

  • Fixed an bug caused by DecimalError Exception (from Adam Morris)

  • Many other bug fixes and features by:

    jeansch
    Anton Vlasenko
    Joseph Walton
    Jan Oliver Oelerich
    Fabian Henze
    And any others I missed.
    Thanks for contributing!

Version 1.13, 2010-12-04

  • Fixed a typo in code for reading a "\b" escape character in strings.

  • Improved __repr__ in FloatObject.

  • Fixed a bug in reading octal escape sequences in strings.

  • Added getWidth and getHeight methods to the RectangleObject class.

  • Fixed compatibility warnings with Python 2.4 and 2.5.

  • Added addBlankPage and insertBlankPage methods on PdfFileWriter class.

  • Fixed a bug with circular references in page's object trees (typically
    annotations) that prevented correctly writing out a copy of those pages.

  • New merge page functions allow application of a transformation matrix.

  • To all patch contributors: I did a poor job of keeping this ChangeLog
    up-to-date for this release, so I am missing attributions here for any
    changes you submitted. Sorry! I'll do better in the future.

Version 1.12, 2008-09-02

  • Added support for XMP metadata.

  • Fix reading files with xref streams with multiple /Index values.

  • Fix extracting content streams that use graphics operators longer than 2
    characters. Affects merging PDF files.

Version 1.11, 2008-05-09

  • Patch from Hartmut Goebel to permit RectangleObjects to accept NumberObject
    or FloatObject values.

  • PDF compatibility fixes.

  • Fix to read object xref stream in correct order.

  • Fix for comments inside content streams.

Version 1.10, 2007-10-04

  • Text strings from PDF files are returned as Unicode string objects when
    pyPdf determines that they can be decoded (as UTF-16 strings, or as
    PDFDocEncoding strings). Unicode objects are also written out when
    necessary. This means that string objects in pyPdf can be either
    generic.ByteStringObject instances, or generic.TextStringObject instances.

  • The extractText method now returns a unicode string object.

  • All document information properties now return unicode string objects. In
    the event that a document provides docinfo properties that are not decoded by
    pyPdf, the raw byte strings can be accessed with an "_raw" property (ie.
    title_raw rather than title)

  • generic.DictionaryObject instances have been enhanced to be easier to use.
    Values coming out of dictionary objects will automatically be de-referenced
    (.getObject will be called on them), unless accessed by the new "raw_get"
    method. DictionaryObjects can now only contain PdfObject instances (as keys
    and values), making it easier to debug where non-PdfObject values (which
    cannot be written out) are entering dictionaries.

  • Support for reading named destinations and outlines in PDF files. Original
    patch by Ashish Kulkarni.

  • Stream compatibility reading enhancements for malformed PDF files.

  • Cross reference table reading enhancements for malformed PDF files.

  • Encryption documentation.

  • Replace some "assert" statements with error raising.

  • Minor optimizations to FlateDecode algorithm increase speed when using PNG
    predictors.

Version 1.9, 2006-12-15

  • Fix several serious bugs introduced in version 1.8, caused by a failure to
    run through our PDF test suite before releasing that version.

  • Fix bug in NullObject reading and writing.

Version 1.8, 2006-12-14

  • Add support for decryption with the standard PDF security handler. This
    allows for decrypting PDF files given the proper user or owner password.

  • Add support for encryption with the standard PDF security handler.

  • Add new pythondoc documentation.

  • Fix bug in ASCII85 decode that occurs when whitespace exists inside the
    two terminating characters of the stream.

Version 1.7, 2006-12-10

  • Fix a bug when using a single page object in two PdfFileWriter objects.

  • Adjust PyPDF to be tolerant of whitespace characters that don't belong
    during a stream object.

  • Add documentInfo property to PdfFileReader.

  • Add numPages property to PdfFileReader.

  • Add pages property to PdfFileReader.

  • Add extractText function to PdfFileReader.

Version 1.6, 2006-06-06

  • Add basic support for comments in PDF files. This allows us to read some
    ReportLab PDFs that could not be read before.

  • Add "auto-repair" for finding xref table at slightly bad locations.

  • New StreamObject backend, cleaner and more powerful. Allows the use of
    stream filters more easily, including compressed streams.

  • Add a graphics state push/pop around page merges. Improves quality of
    page merges when one page's content stream leaves the graphics
    in an abnormal state.

  • Add PageObject.compressContentStreams function, which filters all content
    streams and compresses them. This will reduce the size of PDF pages,
    especially after they could have been decompressed in a mergePage
    operation.

  • Support inline images in PDF content streams.

  • Add support for using .NET framework compression when zlib is not
    available. This does not make pyPdf compatible with IronPython, but it
    is a first step.

  • Add support for reading the document information dictionary, and extracting
    title, author, subject, producer and creator tags.

  • Add patch to support NullObject and multiple xref streams, from Bradley
    Lawrence.

Version 1.5, 2006-01-28

  • Fix a bug where merging pages did not work in "no-rename" cases when the
    second page has an array of content streams.

  • Remove some debugging output that should not have been present.

Version 1.4, 2006-01-27

  • Add capability to merge pages from multiple PDF files into a single page
    using the PageObject.mergePage function. See example code (README or web
    site) for more information.

  • Add ability to modify a page's MediaBox, CropBox, BleedBox, TrimBox, and
    ArtBox properties through PageObject. See example code (README or web site)
    for more information.

  • Refactor pdf.py into multiple files: generic.py (contains objects like
    NameObject, DictionaryObject), filters.py (contains filter code),
    utils.py (various). This does not affect importing PdfFileReader
    or PdfFileWriter.

  • Add new decoding functions for standard PDF filters ASCIIHexDecode and
    ASCII85Decode.

  • Change url and download_url to refer to new pybrary.net web site.

Version 1.3, 2006-01-23

  • Fix new bug introduced in 1.2 where PDF files with \r line endings did not
    work properly anymore. A new test suite developed with various PDF files
    should prevent regression bugs from now on.

  • Fix a bug where inheriting attributes from page nodes did not work.

Version 1.2, 2006-01-23

  • Improved support for files with CRLF-based line endings, fixing a common
    reported problem stating "assertion error: assert line == "%%EOF"".

  • Software author/maintainer is now officially a proud married person, which
    is sure to result in better software... somehow.

Version 1.1, 2006-01-18

  • Add capability to rotate pages.

  • Improved PDF reading support to properly manage inherited attributes from
    /Type=/Pages nodes. This means that page groups that are rotated or have
    different media boxes or whatever will now work properly.

  • Added PDF 1.5 support. Namely cross-reference streams and object streams.
    This release can mangle Adobe's PDFReference16.pdf successfully.

Version 1.0, 2006-01-17

  • First distutils-capable true public release. Supports a wide variety of PDF
    files that I found sitting around on my system.

  • Does not support some PDF 1.5 features, such as object streams,
    cross-reference streams.


Meta Comparisons (`docs/meta/comparisons.md`)

pypdf vs X

pypdf is a free and open source pure-python PDF library capable of
splitting, merging, cropping, and transforming the pages of PDF files.
It can also add custom data, viewing options, and passwords to PDF
files. pypdf can retrieve text and metadata from PDFs as well.

PyMuPDF and PikePDF

PyMuPDF is a Python binding to MuPDF and PikePDF is the Python
binding to QPDF.

While both are excellent libraries for various use-cases, using them is
not always possible even when they support the use-case. Both of them
are powered by C libraries which make installation harder and might
cause security concerns. For MuPDF, you might also need to buy a
commercial license.

A core feature of pypdf is that it's pure Python. That means there is
no C dependency. It has been used for over 10 years and for this reason
a lot of support via StackOverflow and examples on the internet.

pypdf

PyPDF2 was merged back into pypdf. The development continues at pypdf.

PyPDF3 and PyPDF4

Developing and maintaining open source software is extremely
time-intensive and in the case of pypdf not paid at all. Having
continuous support is hard.

pypdf was initially released in 2012 on PyPI and received releases
until 2016. From 2016 to 2022, there was no update - but people were
still using it.

As pypdf is free software, there were attempts to fork it and continue
the development. PyPDF3 was first released in 2018 and still receives
updates. PyPDF4 has only one release from 2018.

Martin Thoma has worked on bringing the community back to one path of
development. He deprecated PyPDF2 in favor of pypdf already, and pypdf has
more features and a cleaner interface than PyPDF2 now. See history of
pypdf
.

pdfminer.six and pdfplumber

pdfminer.six is capable of
extracting the font size
/ font weight (bold-ness). It has no capabilities for writing PDF files.

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)). However, pdfplumber is capable of converting a PDF file into an image, draw lines and rectangles on the image, and save it as an image file. Please note that the image conversion is done via ImageMagick (see pdfplumber's documentation).

The pdfplumber community is active in answering questions and the library is maintained as of May 2023.

pdfrw / pdfrw2

I don't have experience with any of those libraries. Please add a
comparison if you know pypdf and pdfrw!

Please be aware that there is also
pdfminer which is not maintained.
Then there is pdfrw2 which doesn't have
a large community behind it.

Document Generation

There are (Python) tools to generate PDF documents.
pypdf is not one of them.

CLI applications

pypdf is a pure Python PDF library. If you're looking for an application which
you can use from the terminal, give pdfly
a shot.


Meta Faq (`docs/meta/faq.md`)

Frequently Asked Questions

PyPDF2 was a fork from the original pyPdf. After several years, the fork was
merged back into pypdf (now all lowercase).

Which Python versions are supported?

pypdf 3.0+ supports Python 3.6 and later.
PyPDF2 2.0+ supports Python 3.6 and later.
PyPDF2 1.27.10 supported Python 2.7 to 3.10.

Who uses pypdf?

pyPdf is vendored into several projects. That
means the code of pyPdf was copied into that project.

Projects that depend on pypdf:

  • Camelot: A Python library to extract tabular data from PDFs
  • edi: Electronic Data Interchange modules
  • amazon-textract-textractor: Analyze documents with Amazon Textract and generate output in multiple formats.
  • maigret: Collect a dossier on a person by username from thousands of sites
  • deda: tracking Dots Extraction, Decoding and Anonymisation toolkit
  • opencanary
  • Document Conversions
  • 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.

How do I cite pypdf?

In BibTeX format:

text
@misc{pypdf,
 title         = {The {pypdf} library},
 author        = {Mathieu Fenniak and
                  Matthew Stamy and
                  pubpub-zz and
                  Martin Thoma and
                  Matthew Peveler and
                  exiledkingcc and {pypdf Contributors}},
 year          = {2024},
 url           = {https://pypi.org/project/pypdf/}
 note          = {See https://pypdf.readthedocs.io/en/latest/meta/CONTRIBUTORS.html for all contributors}
}

Which License does pypdf use?

pypdf uses the BSD-3-Clause license, see the LICENSE file.


Meta History (`docs/meta/history.md`)

History of pypdf

The Origins: pyPdf (2005-2010)

In 2005, Mathieu Fenniak launched pyPdf "as a PDF toolkit..."
focused on

  • document manipulation: by-page splitting, concatenation, and
    merging;
  • document introspection;
  • page cropping; and
  • document encryption and decryption.

The last release of PyPI was pyPdf 1.13
in 2010.

PyPDF2 is born (2011-2016)

At the end of 2011, after consultation with Mathieu and others, Phaseit
sponsored PyPDF2 as a fork of pyPdf on GitHub. The initial impetus was
to handle a wider range of input PDF instances; Phaseit's commercial
work often encounters PDF instances "in the wild" that it needs to
manage (mostly concatenate and paginate), but that deviates so much from
PDF standards that pyPdf can't read them. PyPDF2 reads a considerably
wider range of real-world PDF instances.

Neither pyPdf nor PyPDF2 aims to be universal, that is, to provide all
possible PDF-related functionality. Note that the similar-appearing
pyfpdf of Mariano Reingart is most comparable to ReportLab, in that
both ReportLab and pyfpdf emphasize document generation. Interestingly
enough, pyfpdf builds in a basic HTML→PDF converter while PyPDF2 has no
knowledge of HTML.

So what is PyPDF2 truly about? Think about popular pdftk for a moment.
PyPDF2 does what pdftk does, and it does so within your current Python
process, and it handles a wider range of variant PDF formats
[explain]. PyPDF2 has its own FAQ to answer other questions that have
arisen.

The Reddit /r/python crowd chatted obliquely and briefly about PyPDF2
in March 2012.

The core developer / maintainer was Matthew Stamy.

PyPDF3 and PyPDF4 (2018-2022)

Two approaches were made to get PyPDF2 active again: PyPDF3 and PyPDF4.

PyPDF3 had its first release in 2018 and its last one in February 2022.
It never got the user base from PyPDF2.

PyPDF4 only had one release in 2018.

PyPDF2: Reborn (2022)

Martin Thoma took over maintenance of PyPDF2 in April 2022. It had over 100
open PRs and 321 open issues.

pubpub-zz was extremely active, especially
for text extraction.

Matthew Peveler helped a lot with reviews
and general project decisions.

exiledkingcc added support for modern
encryption schemes.

pypdf: Back to the Roots (2023-2024)

In order to simplify things for beginners, PyPDF2 was merged back into
pypdf. Now all lowercase, without a number. We hope that the folks who
develop PyPDF3 and PyPDF4 also join us.

Compared to PyPDF2 >= 3.0.0, pypdf >= 3.1.0 now offers:

stefan6419846 made his first PR for pypdf
in July 2023 and joined the project.


Meta Migration 1 To 2 (`docs/meta/migration-1-to-2.md`)

Migration Guide: 1.x to 2.x

PyPDF2<2.0.0 (docs)
is very different from PyPDF2>=2.0.0 (docs).

Luckily, most changes are simple naming adjustments. This guide helps you to
make the step from PyPDF2 1.x (or even the original PyPdf) to PyPDF2>=2.0.0.

You can execute your code with the updated version and show deprecation warnings
by running python -W all your_code.py.

Imports and Modules

  • PyPDF2.utils no longer exists
  • PyPDF2.pdf no longer exists. You can import from PyPDF2 directly or from
    PyPDF2.generic

Naming Adjustments

Classes

The base classes were renamed as they also allow operating with BytesIO streams
instead of files. Also, the strict parameter changed the default value from
strict=True to strict=False.

  • PdfFileReader ➔ PdfReader
  • PdfFileWriter ➔ PdfWriter
  • PdfFileMerger ➔ PdfMerger

PdfFileReader and PdfFileMerger no longer have the overwriteWarnings
parameter. The new behavior is overwriteWarnings=False.

Function, Method, and Property Names

In PyPDF2.xmp.XmpInformation:

  • rdfRoot ➔ rdf_root
  • xmp_createDate ➔ xmp_create_date
  • xmp_creatorTool ➔ xmp_creator_tool
  • xmp_metadataDate ➔ xmp_metadata_date
  • xmp_modifyDate ➔ xmp_modify_date
  • xmpMetadata ➔ xmp_metadata
  • xmpmm_documentId ➔ xmpmm_document_id
  • xmpmm_instanceId ➔ xmpmm_instance_id

In PyPDF2.generic:

  • readObject ➔ read_object
  • convertToInt ➔ convert_to_int
  • DocumentInformation.getText ➔ DocumentInformation._get_text : This method should typically not be used; please let me know if you need it.
  • readHexStringFromStream ➔ read_hex_string_from_stream
  • initializeFromDictionary ➔ initialize_from_dictionary
  • createStringObject ➔ create_string_object
  • TreeObject.hasChildren ➔ TreeObject.has_children
  • TreeObject.emptyTree ➔ TreeObject.empty_tree

In many places:

  • getObject ➔ get_object
  • writeToStream ➔ write_to_stream
  • readFromStream ➔ read_from_stream

PdfReader class:

  • reader.getPage(pageNumber) ➔ reader.pages[page_number]
  • reader.getNumPages() / reader.numPages ➔ len(reader.pages)
  • getDocumentInfo ➔ metadata
  • flattenedPages attribute ➔ flattened_pages
  • resolvedObjects attribute ➔ resolved_objects
  • xrefIndex attribute ➔ xref_index
  • getNamedDestinations / namedDestinations attribute ➔ named_destinations
  • getPageLayout / pageLayout ➔ page_layout attribute
  • getPageMode / pageMode ➔ page_mode attribute
  • getIsEncrypted / isEncrypted ➔ is_encrypted attribute
  • getOutlines ➔ get_outlines
  • readObjectHeader ➔ read_object_header
  • cacheGetIndirectObject ➔ cache_get_indirect_object
  • cacheIndirectObject ➔ cache_indirect_object
  • getDestinationPageNumber ➔ get_destination_page_number
  • readNextEndLine ➔ read_next_end_line
  • _zeroXref ➔ _zero_xref
  • _authenticateUserPassword ➔ _authenticate_user_password
  • _pageId2Num attribute ➔ _page_id2num
  • _buildDestination ➔ _build_destination
  • _buildOutline ➔ _build_outline
  • _getPageNumberByIndirect(indirectRef) ➔ _get_page_number_by_indirect(indirect_ref)
  • _getObjectFromStream ➔ _get_object_from_stream
  • _decryptObject ➔ _decrypt_object
  • _flatten(..., indirectRef) ➔ _flatten(..., indirect_ref)
  • _buildField ➔ _build_field
  • _checkKids ➔ _check_kids
  • _writeField ➔ _write_field
  • _write_field(..., fieldAttributes) ➔ _write_field(..., field_attributes)
  • _read_xref_subsections(..., getEntry, ...) ➔ _read_xref_subsections(..., get_entry, ...)

PdfWriter class:

  • writer.getPage(pageNumber) ➔ writer.pages[page_number]
  • writer.getNumPages() ➔ len(writer.pages)
  • addMetadata ➔ add_metadata
  • addPage ➔ add_page
  • addBlankPage ➔ add_blank_page
  • addAttachment(fname, fdata) ➔ add_attachment(filename, data)
  • insertPage ➔ insert_page
  • insertBlankPage ➔ insert_blank_page
  • appendPagesFromReader ➔ append_pages_from_reader
  • updatePageFormFieldValues ➔ update_page_form_field_values
  • cloneReaderDocumentRoot ➔ clone_reader_document_root
  • cloneDocumentFromReader ➔ clone_document_from_reader
  • getReference ➔ get_reference
  • getOutlineRoot ➔ get_outline_root
  • getNamedDestRoot ➔ get_named_dest_root
  • addBookmarkDestination ➔ add_bookmark_destination
  • addBookmarkDict ➔ add_bookmark_dict
  • addBookmark ➔ add_bookmark
  • addNamedDestinationObject ➔ add_named_destination_object
  • addNamedDestination ➔ add_named_destination
  • removeLinks ➔ remove_links
  • removeImages(ignoreByteStringObject) ➔ remove_images(ignore_byte_string_object)
  • removeText(ignoreByteStringObject) ➔ remove_text(ignore_byte_string_object)
  • addURI ➔ add_uri
  • addLink ➔ add_link
  • getPage(pageNumber) ➔ get_page(page_number)
  • getPageLayout / setPageLayout / pageLayout ➔ page_layout attribute
  • getPageMode / setPageMode / pageMode ➔ page_mode attribute
  • _addObject ➔ _add_object
  • _addPage ➔ _add_page
  • _sweepIndirectReferences ➔ _sweep_indirect_references

PdfMerger class

  • __init__ parameter: strict=True ➔ strict=False (the PdfFileMerger still has the old default)
  • addMetadata ➔ add_metadata
  • addNamedDestination ➔ add_named_destination
  • setPageLayout ➔ set_page_layout
  • setPageMode ➔ set_page_mode

Page class:

  • artBox / bleedBox / cropBox / mediaBox / trimBox ➔ artbox / bleedbox / cropbox / mediabox / trimbox
    • getWidth, getHeight ➔ width / height
    • getLowerLeft_x / getUpperLeft_x ➔ left
    • getUpperRight_x / getLowerRight_x ➔ right
    • getLowerLeft_y / getLowerRight_y ➔ bottom
    • getUpperRight_y / getUpperLeft_y ➔ top
    • getLowerLeft / setLowerLeft ➔ lower_left property
    • upperRight ➔ upper_right
  • mergePage ➔ merge_page
  • rotateClockwise / rotateCounterClockwise ➔ rotate_clockwise
  • _mergeResources ➔ _merge_resources
  • _contentStreamRename ➔ _content_stream_rename
  • _pushPopGS ➔ _push_pop_gs
  • _addTransformationMatrix ➔ _add_transformation_matrix
  • _mergePage ➔ _merge_page

XmpInformation class:

  • getElement(..., aboutUri, ...) ➔ get_element(..., about_uri, ...)
  • getNodesInNamespace(..., aboutUri, ...) ➔ get_nodes_in_namespace(..., aboutUri, ...)
  • _getText ➔ _get_text

utils.py:

  • matrixMultiply ➔ `matrix_multiply
  • RC4_encrypt is moved to the security module

Parameter Names

  • PdfWriter.get_page: pageNumber ➔ page_number
  • PyPDF2.filters (all classes): decodeParms ➔ decode_parms
  • PyPDF2.filters (all classes): decodeStreamData ➔ decode_stream_data
  • pagenum ➔ page_number
  • PdfMerger.merge: position ➔ page_number
  • PdfWriter.add_outline_item_destination: dest ➔ page_destination
  • PdfWriter.add_named_destination_object: dest ➔ page_destination
  • PdfWriter.encrypt: user_pwd ➔ user_password
  • PdfWriter.encrypt: owner_pwd ➔ owner_password

Deprecations

A few classes / functions were deprecated without replacement:

  • PyPDF2.utils.ConvertFunctionsToVirtualList
  • PyPDF2.utils.formatWarning
  • PyPDF2.isInt(obj): Use instance(obj, int) instead
  • PyPDF2.u_(s): Use s directly
  • PyPDF2.chr_(c): Use chr(c) instead
  • PyPDF2.barray(b): Use bytearray(b) instead
  • PyPDF2.isBytes(b): Use instance(b, type(bytes())) instead
  • PyPDF2.xrange_fn: Use range instead
  • PyPDF2.string_type: Use str instead
  • PyPDF2.isString(s): Use instance(s, str) instead
  • PyPDF2._basestring: Use str instead
  • b_(...) was removed. You should typically be able to use the bytes object directly, otherwise you can copy this

Meta Project Governance (`docs/meta/project-governance.md`)

Project Governance

This document describes how the pypdf project is managed. It describes the
different actors, their roles, and the responsibilities they have.

Terminology

  • The project is pypdf - a free and open-source pure-python PDF library
    capable of splitting, merging, cropping, and transforming the pages of PDF files.
    It includes the code, issues, and discussions on GitHub,
    and the documentation on ReadTheDocs,
    the package on PyPI, and
    the website on GitHub.
  • A maintainer is a person who has technical permissions to change one or
    more parts of the projects. It is a person driven to keep the project running
    and improving.
  • A contributor is a person who contributes to the project. That could be
    through writing code - in the best case through forking and creating a pull
    request, but that is up to the maintainer. Other contributors describe issues,
    help to ask questions on existing issues to make them easier to answer,
    participate in discussions, and help to improve the documentation. Contributors
    are similar to maintainers, but without technical permissions.
  • A user is a person who imports pypdf into their code. All pypdf users
    are developers, but not developers who know the internals of pypdf. They only
    use the public interface of pypdf. They will likely have less knowledge about
    PDF than contributors.
  • The community is all of that - the users, the contributors, and the maintainers.

Governance, Leadership, and Steering pypdf forward

pypdf is a free and open source project with over 100 contributors and likely
(way) more than 1000 users.

As pypdf does not have any formal relationship with any company and no funding,
all the work done by the community are voluntary contributions. People don't
get paid, but choose to spend their free time to create software of which
many more are profiting. This has to be honored and respected.

Despite such a big community, the project was dormant from 2016 to 2022.
There were still questions asked, issues reported, and pull requests created.
But the maintainer didn't have the time to move pypdf forward. During that
time, nobody else stepped up to become the new maintainer.

For this reason, pypdf has the Benevolent Dictator
governance model. The benevolent dictator is a maintainer with all technical permissions -
most importantly the permission to push new pypdf versions on PyPI.

Being benevolent, the benevolent dictator listens for decisions to the community and tries
their best to make decisions from which the overall community profits - the
current one and the potential future one. Being a dictator, the benevolent dictator always has
the power and the right to make decisions on their own - also against some
members of the community.

As pypdf is free software, parts of the community can split off (fork the code)
and create a new community. This should limit the harm a bad benevolent dictator can do.

Project Language

The project language is (american) English. All documentation and issues must
be written in English to ensure that the community can understand it.

We appreciate the fact that large parts of the community don't have English
as their native language. We try our best to understand others -
automatic translators might help.

Expectations

The community can expect the following:

  • The benevolent dictator tries their best to make decisions from which the overall
    community profits. The benevolent dictator is aware that his/her decisions can shape the
    overall community. Once the benevolent dictator notices that she/he doesn't have the time
    to advance pypdf, he/she looks for a new benevolent dictator. As it is expected
    that the benevolent dictator will step down at some point of their choice
    (hopefully before their death), it is NOT a benevolent dictator for life
    (BDFL).
  • Every maintainer (including the benevolent dictator) is aware of their permissions and
    the harm they could do. They value security and ensure that the project is
    not harmed. They give their technical permissions back if they don't need them
    any longer. Any long-time contributor can become a maintainer. Maintainers
    can - and should! - step down from their role when they realize that they
    can no longer commit that time. Their contribution will be honored in the
    {doc}history.
  • Every contributor is aware that the time of maintainers and the benevolent dictator is
    limited. Short pull requests that briefly describe the solved issue and have
    a unit test have a higher chance to get merged soon - simply because it's
    easier for maintainers to see that the contribution will not harm the overall
    project. Their contributions are documented in the git history and in the
    public issues. Let us know
    if you would appreciate something else!
  • Every community member uses a respectful language. We are all human, we
    get upset about things we care and other things than what's visible on the
    internet go on in our live. pypdf does not pay its contributors - keep all
    of that in mind when you interact with others. We are here because we want to
    help others.

Issues and Discussions

An issue is any technical description that aims at bringing pypdf forward:

  • Bugs tickets: Something went wrong because pypdf developers made a mistake.
  • Feature requests: pypdf does not support all features of the PDF specifications.
    There are certainly also convenience methods that would help users a lot.
  • Robustness requests: There are many broken PDFs around. In some cases, we can
    deal with that. It's kind of a mixture between a bug ticket and a feature
    request.
  • Performance tickets: pypdf could be faster - let us know about your specific
    scenario.

Any comment that is in those technical descriptions which is not helping the
discussion can be deleted. This is especially true for "me too" comments on bugs
or "bump" comments for desired features. People can express this with 👍 / 👎
reactions.

Discussions are open. No comments
will be deleted there - except if they are unrelated spam or only
try to insult people (luckily, the community was very respectful so far 🤞)

Releases

The maintainers follow semantic versioning. Most
importantly, that means that breaking changes will have a major version bump.

Be aware that unintentional breaking changes might still happen. The pypdf
maintainers do their best to fix that in a timely manner - please
report such issues!

People

  • stefan6419846 is the benevolent dictator since January 2025
  • Martin Thoma was the benevolent dictator from April 2022 to January 2025.
    He still has most of the permissions as a fallback.
  • Maintainers:
    • Matthew Stamy (mstamy2) was the benevolent dictator for a long time.
      He still is around on GitHub once in a while and has permissions on PyPI and GitHub.
    • Matthew Peveler (MasterOdin) is a maintainer on GitHub.

Meta Scope Of Pypdf (`docs/meta/scope-of-pypdf.md`)

Scope of pypdf

What features should pypdf have and which features will it never have?

pypdf aims at simplifying interactions with PDF documents. Core tasks that
pypdf can perform are:

  • Document manipulation: Splitting, merging, cropping, and transforming the pages of PDF files
  • Data Extraction: Extract text and metadata from PDF documents
  • Security: Decrypt / encrypt PDF documents

Typical indicators that pypdf should do something:

  • The task needs in-depth knowledge of the PDF format
  • It currently requires a lot of code or even is impossible to do with pypdf
  • It's neither mentioned in "belongs in user code" nor in "out of scope"
  • It already is in the issue list with the is-feature tag.

The moonshot extensions are
features we would like to have, but are currently not able to add (PRs are
welcome 😉)

Belongs in user code

Here are a few indicators that a feature belongs into users' code (and not into pypdf):

  1. The use-case is very specific. Most people will not encounter the same need.
  2. It can be done without knowledge of the PDF specification
  3. It cannot be done without (non-pdf) domain knowledge. Anything that is
    specific to your industry.

Out of scope

While this list is infinitely long, there are a few topics that are asked
multiple times.

Those topics are out of scope for pypdf. They will never be part of pypdf:

  1. Optical Character Recognition (OCR): OCR is about extracting text from
    images. That is very different from the kind of text extraction pypdf is
    doing. Please note that images can be within PDF documents. In the case of
    scanned documents, the whole page is an image. Some scanners automatically
    execute OCR and add a text-layer behind the scanned page. That is something
    pypdf can use if it's present. As a rule-of-thumb: If you cannot mark/copy
    the text, it's likely an image. A noteworthy open source OCR project is
    tesseract.
  2. Format Conversion: Converting docx / HTML to PDF or PDF to those formats.
    You might want to have a look at pdfkit
    and similar projects.

Out of scope for the moment, but might be added if there are enough contributors:

  • Digital Signature Support (reference ticket): Cryptography is
    complicated. It's important to get it right. pypdf currently doesn't have
    enough active contributors to properly add digital signature support. For the
    moment, pyhanko seems to be the best choice.
  • PDF Generation from Scratch: pypdf can manipulate existing PDF documents,
    add annotations, combine / split / crop / transform. It can add blank pages.
    But if you want to generate invoices, you might want to have a look at
    reportlab /
    fpdf2 or document conversion tools like
    pdfkit.
  • Replacing words within a PDF: Extracting text from PDF is hard.
    Replacing text in a reliable way is even harder. For example, one word might
    be split into multiple tokens. Hence, it's not a simple "search and replace"
    in some cases.
  • (Not) Extracting headers/footers/page numbers: While you can apply
    heuristics, there is no way to always make it work. PDF documents simply
    don't contain the information what a header/footer/page number is.

Library vs. Application

It's also worth pointing out that pypdf is designed to be a library. It is not
an application. That has several implications:

  • Execution: pypdf cannot be executed directly, but only be called from within
    a program written by a pypdf user. In contrast, an application is executed
    on its own.
  • Dependencies: pypdf should have a minimal set of dependencies and only
    restrict them where it is strictly necessary. In contrast, applications should
    be installed in environments which are isolated from other applications. They
    can pin their dependencies.

If you're looking for a way to interact with PDF files via Shell, you should
either write a script using pypdf or use pdfly.


Meta Taking Ownership (`docs/meta/taking-ownership.md`)

Taking Ownership of pypdf

pypdf is currently maintained by stefan6419846. We want to avoid that
pypdf ever goes unmaintained again. This document serves as a guide to avoid
that if I become unavailable, e.g., due to severe health issues.

This currently is just an abstract scenario. I'm fine, and I will likely do this
for several more years, but I have seen how projects stand still for many years
because of the maintainer becoming inactive.

What belongs to pypdf?

The resources needed for maintaining pypdf are:

When may somebody take ownership?

No activity in 180 days: If I don't answer e-mails (see my GitHub profile)
and don't make any commits / merges for half a year, you can consider pypdf "not
maintained."

Who may take ownership?

Preferably, one of the owners of the GitHub py-pdf organization takes care of
that.

As of 27th of August 2023, the following people might be candidates:

  • Lucas-C: He maintains fpdf2 and is a py-pdf owner
  • pubpub-zz: He is one of the most active contributors
    to pypdf
  • Matthew Peveler: Less active, but he is cautious
    about breaking changes and an experienced software developer.
  • exiledkingcc: He has contributed the core
    changes related to encryption.

How to take ownership?


Modules Destination (`docs/modules/Destination.rst`)

The Destination Class

.. autoclass:: pypdf.generic.Destination
:members:
:undoc-members:
:show-inheritance:


Modules DocumentInformation (`docs/modules/DocumentInformation.rst`)

The DocumentInformation Class

.. autoclass:: pypdf.DocumentInformation
:members:
:undoc-members:
:show-inheritance:


Modules Field (`docs/modules/Field.rst`)

The Field Class

.. autoclass:: pypdf.generic.Field
:members:
:undoc-members:
:show-inheritance:


Modules Fit (`docs/modules/Fit.rst`)

The Fit Class

.. autoclass:: pypdf.generic.Fit
:members:
:undoc-members:
:show-inheritance:


Modules PageObject (`docs/modules/PageObject.rst`)

The PageObject Class

.. autoclass:: pypdf._page.PageObject
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf._page.VirtualListImages
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf._page.ImageFile
:members:
:inherited-members: File
:undoc-members:

.. autofunction:: pypdf.mult


Modules PageRange (`docs/modules/PageRange.rst`)

The PageRange Class

.. autoclass:: pypdf.PageRange
:members:
:undoc-members:
:show-inheritance:


Modules PaperSize (`docs/modules/PaperSize.rst`)

The PaperSize Class

.. autoclass:: pypdf.PaperSize
:members:
:undoc-members:
:show-inheritance:

Add blank page with PaperSize
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

.. testsetup ::

text
pypdf_test_setup("modules/PaperSize", {
    "example.pdf": "../resources/example.pdf",
})

.. testcode ::

text
from pypdf import PaperSize, PdfWriter

writer = PdfWriter(clone_from="example.pdf")
writer.add_blank_page(PaperSize.A8.width, PaperSize.A8.height)
writer.write("out-add-page.pdf")

Insert blank page with PaperSize
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. testcode ::

text
from pypdf import PaperSize, PdfWriter

writer = PdfWriter(clone_from="example.pdf")
writer.insert_blank_page(PaperSize.A8.width, PaperSize.A8.height, 1)
writer.write("out-insert-page.pdf")

Modules PdfDocCommon (`docs/modules/PdfDocCommon.rst`)

The PdfDocCommon Class

PdfDocCommon is an abstract class which is inherited by :class:~pypdf.PdfReader and :class:~pypdf.PdfWriter.

Where identified in the API, you can use any of the derived class.

.. autoclass:: pypdf._doc_common.PdfDocCommon
:members:
:inherited-members:
:undoc-members:
:show-inheritance:


Modules PdfReader (`docs/modules/PdfReader.rst`)

The PdfReader Class

.. autoclass:: pypdf.PdfReader
:members:
:inherited-members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf.PasswordType
:members:
:undoc-members:
:show-inheritance:


Modules PdfWriter (`docs/modules/PdfWriter.rst`)

The PdfWriter Class

.. autoclass:: pypdf.PdfWriter
:members:
:inherited-members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf.ObjectDeletionFlag
:members:
:undoc-members:
:show-inheritance:


Modules RectangleObject (`docs/modules/RectangleObject.rst`)

The RectangleObject Class

.. autoclass:: pypdf.generic.RectangleObject
:members:
:undoc-members:
:show-inheritance:


Modules Transformation (`docs/modules/Transformation.rst`)

The Transformation Class

.. autoclass:: pypdf.Transformation
:members:
:undoc-members:
:show-inheritance:


Modules XmpInformation (`docs/modules/XmpInformation.rst`)

The XmpInformation Class

.. autoclass:: pypdf.xmp.XmpInformation
:members:
:undoc-members:
:show-inheritance:


Modules Actions (`docs/modules/actions.rst`)

Actions

.. automodule:: pypdf.actions
:members:
:undoc-members:
:show-inheritance:


Modules Annotations (`docs/modules/annotations.rst`)

The annotations module

.. automodule:: pypdf.annotations
:members:
:undoc-members:
:show-inheritance:


Modules Configuration (`docs/modules/configuration.rst`)

Configuration

.. autoclass:: pypdf.Configuration
:members:
:show-inheritance:

.. autofunction:: pypdf.apply_configuration

.. autofunction:: pypdf.get_configuration

.. autofunction:: pypdf.overwrite_configuration


Modules Constants (`docs/modules/constants.rst`)

Constants

.. autoclass:: pypdf.constants.AnnotationFlag
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf.constants.ImageType
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf.constants.PageLabelStyle
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf.constants.UserAccessPermissions
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf.constants.FieldDictionaryAttributes
:members:
:undoc-members:
:exclude-members: FT, Parent, Kids, T, TU, TM, V, DV, AA, Opt, attributes, attributes_dict
:show-inheritance:


Modules Errors (`docs/modules/errors.rst`)

Errors

.. automodule:: pypdf.errors
:members:
:undoc-members:
:show-inheritance:


Modules Generic (`docs/modules/generic.rst`)

Generic PDF objects

.. automodule:: pypdf.generic
:members:
:undoc-members:
:show-inheritance:
:exclude-members: Destination, Field, Fit, RectangleObject

.. autoclass:: pypdf._protocols.PdfObjectProtocol
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf._protocols.XmpInformationProtocol
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf._protocols.PdfCommonDocProtocol
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf._protocols.PdfReaderProtocol
:members:
:undoc-members:
:show-inheritance:

.. autoclass:: pypdf._protocols.PdfWriterProtocol
:members:
:undoc-members:
:show-inheritance:


User Add Javascript (`docs/user/add-javascript.md`)

Adding JavaScript to a PDF

PDF readers vary in the extent they support JavaScript, with some not supporting it at all.

Adobe has documentation on its support here:
https://opensource.adobe.com/dc-acrobat-sdk-docs/library/jsapiref/index.html

Launch print window on opening

{testsetup}
pypdf_test_setup("user/add-javascript", {
    "example.pdf": "../resources/example.pdf",
})
{testcode}
from pypdf import PdfWriter
from pypdf.actions import JavaScript

writer = PdfWriter(clone_from="example.pdf")

# Add JavaScript to launch the print window on opening this PDF
writer.add_open_action(JavaScript("this.print({bUI:true,bSilent:false,bShrinkToFit:true});"))
# bUI: (optional) If true (the default), will cause a UI to be presented to the
# user to obtain printing information and confirm the action.

# bSilent: (optional) If true, suppresses the cancel dialog box while the
# document is printing. The default is false.

# bShrinkToFit: If true, the page is shrunk (if necessary) to fit within the
# imageable area of the printed page. If false, it is not. The default is false.

writer.write("out-print-window.pdf")

User Add Watermark (`docs/user/add-watermark.md`)

Adding a Stamp or Watermark to a PDF

Adding stamps or watermarks are two common ways to manipulate PDF files.
A stamp is adding something on top of the document, a watermark is in the
background of the document.

Stamp (Overlay) / Watermark (Underlay)

The process of stamping and watermarking is the same, you just need to set over parameter to True for stamping and False for watermarking.

You can use {func}~pypdf._page.PageObject.merge_page if you don't need to transform the stamp:

{testsetup}
pypdf_test_setup("user/add-watermark", {
    "crazyones.pdf": "../resources/crazyones.pdf",
    "nup-source.png": "../docs/user/nup-source.png",
    "jpeg.pdf": "../resources/jpeg.pdf",
})
{testcode}
from pypdf import PdfReader, PdfWriter

stamp = PdfReader("jpeg.pdf").pages[0]
writer = PdfWriter(clone_from="crazyones.pdf")
for page in writer.pages:
    page.merge_page(stamp, over=False)  # here set to False for watermarking

writer.write("out-watermark.pdf")

Otherwise 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.

{testcode}
from pathlib import Path
from typing import List, Union

from pypdf import PdfReader, PdfWriter, Transformation


def stamp(
    content_pdf: Union[Path, str],
    stamp_pdf: Union[Path, str],
    pdf_result: Union[Path, str],
    page_indices: Union[None, List[int]] = None,
):
    stamp_page = PdfReader(stamp_pdf).pages[0]

    writer = PdfWriter()
    # page_indices can be a List(array) of page, tuples are for range definition
    reader = PdfReader(content_pdf)
    writer.append(reader, pages=page_indices)

    for content_page in writer.pages:
        content_page.merge_transformed_page(
            stamp_page,
            Transformation().scale(0.5),
        )

    writer.write(pdf_result)


stamp("crazyones.pdf", "jpeg.pdf", "out-scale.pdf")

If you are experiencing wrongly rotated watermarks/stamps, try to use
{func}~pypdf._page.PageObject.transfer_rotation_to_content on the corresponding pages beforehand
to fix the page boxes.

Example of stamp:

Example of watermark:

Stamping images directly

The above code only works for stamps that are already in PDF format.
However, you can easily convert an image to PDF image using
Pillow.

{testcode}
from io import BytesIO
from pathlib import Path
from typing import List, Union

from PIL import Image
from pypdf import PageRange, PdfReader, PdfWriter, Transformation


def image_to_pdf(stamp_img: Union[Path, str]) -> PdfReader:
    img = Image.open(stamp_img)
    img_as_pdf = BytesIO()
    img.save(img_as_pdf, "pdf")
    return PdfReader(img_as_pdf)


def stamp_img(
    content_pdf: Union[Path, str],
    stamp_img: Union[Path, str],
    pdf_result: Union[Path, str],
    page_indices: Union[PageRange, List[int], None] = None,
):
    # Convert the image to a PDF
    stamp_pdf = image_to_pdf(stamp_img)

    # Then use the same stamp code from above
    stamp_page = stamp_pdf.pages[0]

    writer = PdfWriter()

    reader = PdfReader(content_pdf)
    writer.append(reader, pages=page_indices)
    for content_page in writer.pages:
        content_page.merge_transformed_page(
            stamp_page,
            Transformation(),
        )

    writer.write(pdf_result)


stamp_img("crazyones.pdf", "nup-source.png", "out-image.pdf")

User Adding Pdf Annotations (`docs/user/adding-pdf-annotations.md`)

Adding PDF Annotations

{note}
By default, some annotations might be invisible, for example polylines, as the default color is "transparent".

To 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:

  * With one element, a grayscale value.
  * With three elements, a RGB definition.
  * With four elements, a CMYK definition.

Attachments

{testsetup}
pypdf_test_setup("user/adding-pdf-annotations", {
    "crazyones.pdf": "../resources/crazyones.pdf",
})
{testcode}
from pypdf import PdfWriter

writer = PdfWriter()
writer.add_blank_page(width=200, height=200)

data = b"any bytes - typically read from a file"
writer.add_attachment("smile.png", data)

writer.write("out-attachment.pdf")

Free Text

If you want to add text in a box like this

you can use {class}~pypdf.annotations.FreeText:

{testcode}
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import FreeText
from pypdf.constants import AnnotationFlag

# Fill the writer with the pages you want
reader = PdfReader("crazyones.pdf")
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

# Create the annotation and add it
annotation = FreeText(
    text="Hello World\nThis is the second line!",
    rect=(50, 550, 200, 650),
    font="Arial",
    bold=True,
    italic=True,
    font_size="20pt",
    font_color="00ff00",
    border_color="0000ff",
    background_color="cdcdcd",
)

# Mark the annotation as printable.
# See "AnnotationFlag" for other options, e.g. hidden etc.
annotation.flags = AnnotationFlag.PRINT

writer.add_annotation(page_number=0, annotation=annotation)

# Write the annotated file to disk
writer.write("out-free-text.pdf")

Text

A text annotation looks like this:

Line

If you want to add a line like this:

you can use {class}~pypdf.annotations.Line:

{testcode}
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Line

reader = PdfReader("crazyones.pdf")
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

# Add the line
annotation = Line(
    text="Hello World\nLine2",
    rect=(50, 550, 200, 650),
    p1=(50, 550),
    p2=(200, 650),
)
writer.add_annotation(page_number=0, annotation=annotation)

# Write the annotated file to disk
writer.write("out-line.pdf")

PolyLine

If you want to add a line like this:

you can use {class}~pypdf.annotations.PolyLine:

{testcode}
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import PolyLine
from pypdf.generic import ArrayObject, FloatObject, NameObject

reader = PdfReader("crazyones.pdf")
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

# Add the polyline
# By default, the line will be transparent. Set an explicit color.
annotation = PolyLine(
    vertices=[(50, 550), (200, 650), (70, 750), (50, 700)],
)
annotation[NameObject("/C")] = ArrayObject(
    [FloatObject(0.9), FloatObject(0.1), FloatObject(0)]
)
writer.add_annotation(page_number=0, annotation=annotation)

# Write the annotated file to disk
writer.write("out-polyline.pdf")

Rectangle

If you want to add a rectangle like this:

you can use {class}~pypdf.annotations.Rectangle:

{testcode}
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Rectangle

reader = PdfReader("crazyones.pdf")
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

# Add the rectangle
annotation = Rectangle(
    rect=(50, 550, 200, 650),
)
writer.add_annotation(page_number=0, annotation=annotation)

# Write the annotated file to disk
writer.write("out-rectangle.pdf")

If you want the rectangle to be filled, use the interiour_color="ff0000" parameter.

This method uses the "square" annotation type of the PDF format.

Ellipse

If you want to add a circle like this:

you can use {class}~pypdf.annotations.Ellipse:

{testcode}
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Ellipse

reader = PdfReader("crazyones.pdf")
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

# Add the rectangle
annotation = Ellipse(
    rect=(50, 550, 200, 650),
)
writer.add_annotation(page_number=0, annotation=annotation)

# Write the annotated file to disk
writer.write("out-ellipse.pdf")

Polygon

If you want to add a polygon like this:

you can use {class}~pypdf.annotations.Polygon:

{testcode}
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Polygon

reader = PdfReader("crazyones.pdf")
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

# Add the line
annotation = Polygon(
    vertices=[(50, 550), (200, 650), (70, 750), (50, 700)],
)
writer.add_annotation(page_number=0, annotation=annotation)

# Write the annotated file to disk
writer.write("out-polygon.pdf")

Manage the Popup windows for markups, looks like this:

you can use {py:class}~pypdf.annotations.Popup:

{testcode}
from pypdf import PdfWriter
from pypdf.annotations import Popup, Text

# Arrange
writer = PdfWriter()
writer.append("crazyones.pdf", [0])

# Act
text_annotation = writer.add_annotation(
    0,
    Text(
        text="Hello World\nThis is the second line!",
        rect=(50, 550, 200, 650),
        open=True,
    ),
)

popup_annotation = Popup(
    rect=(50, 550, 200, 650),
    open=True,
    parent=text_annotation,  # use the output of add_annotation
)

writer.write("out-popup.pdf")

You have to use the returned result from add_annotation() as it is
the parent annotation with which this popup annotation shall be associated.

If you want to add a link, you can use {class}~pypdf.annotations.Link:

{testcode}
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Link

reader = PdfReader("crazyones.pdf")
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

# Add the link
annotation = Link(
    rect=(50, 550, 200, 650),
    url="https://martin-thoma.com/",
)
writer.add_annotation(page_number=0, annotation=annotation)

# Write the annotated file to disk
writer.write("out-link.pdf")

You can also add internal links:

{testcode}
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Link
from pypdf.generic import Fit

reader = PdfReader("crazyones.pdf")
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)
writer.add_blank_page(
    width=page.mediabox.width,
    height=page.mediabox.height,
)

# Add the link
annotation = Link(
    rect=(50, 550, 200, 650),
    target_page_index=1,
    fit=Fit(fit_type="/FitH", fit_args=(123,)),
    border=[0, 0, 2],
)
writer.add_annotation(page_number=0, annotation=annotation)

# Write the annotated file to disk
writer.write("out-internal-link.pdf")

Text Markup Annotations

Text markup annotations refer to a specific piece of text within the document.

These are a bit more complicated as you need to know exactly where the text
is, the so-called "Quad points".

Highlighting

If you want to highlight text like this:

you can use {class}~pypdf.annotations.Highlight:

{testcode}
from pypdf import PdfReader, PdfWriter
from pypdf.annotations import Highlight
from pypdf.generic import ArrayObject, FloatObject

reader = PdfReader("crazyones.pdf")
page = reader.pages[0]
writer = PdfWriter()
writer.add_page(page)

rect = (50, 550, 200, 650)
quad_points = [rect[0], rect[1], rect[2], rect[1], rect[0], rect[3], rect[2], rect[3]]

# Add the highlight
annotation = Highlight(
    rect=rect,
    quad_points=ArrayObject([FloatObject(quad_point) for quad_point in quad_points]),
)
writer.add_annotation(page_number=0, annotation=annotation)

# Write the annotated file to disk
writer.write("out-highlight.pdf")

User Cropping And Transforming (`docs/user/cropping-and-transforming.md`)

Cropping and Transforming PDFs

{note}
Just because content is no longer visible, it is not gone.
Cropping works by adjusting the viewbox. That means content that was cropped
away can still be restored.
{testsetup}
pypdf_test_setup("user/cropping-and-transforming", {
    "example.pdf": "../resources/example.pdf",
    "Seige_of_Vicksburg_Sample_OCR.pdf": "../resources/Seige_of_Vicksburg_Sample_OCR.pdf",
    "labeled-edges-center-image.pdf": "../resources/labeled-edges-center-image.pdf",
    "side-by-side-subfig.pdf": "../resources/side-by-side-subfig.pdf",
    "nup-source.pdf": "../resources/box.pdf",
    "box.pdf": "../resources/box.pdf",
})
{testcode}
from pypdf import PdfReader, PdfWriter

reader = PdfReader("Seige_of_Vicksburg_Sample_OCR.pdf")
writer = PdfWriter()

# Add page 1 from reader to output document, unchanged.
writer.add_page(reader.pages[0])

# Add page 2 from reader, but rotated clockwise 90 degrees.
writer.add_page(reader.pages[1].rotate(90))

# Add page 3 from reader, but crop it to half size.
page3 = writer.add_page(reader.pages[2])
page3.mediabox.upper_right = (
    page3.mediabox.right / 2,
    page3.mediabox.top / 2,
)

writer.write("out-all-in-one.pdf")

Page rotation

The most typical rotation is a clockwise rotation of the page by multiples of
90 degrees. That is done when the orientation of the page is wrong. You can
do that with the {func}~pypdf._page.PageObject.rotate method:

{testcode}
from pypdf import PdfReader, PdfWriter

reader = PdfReader("example.pdf")
writer = PdfWriter()

writer.add_page(reader.pages[0])
writer.pages[0].rotate(90)

writer.write("out-page-rotation.pdf")

The rotate method is typically preferred over the page.add_transformation(Transformation().rotate())
method, because rotate will ensure that the page is still in the mediabox/cropbox.
The transformation object operates on the coordinates of the page
contents and does not change the mediabox or cropbox.

Plain Merge

is the result of

{testcode}
from pypdf import PdfReader, PdfWriter, Transformation

# Get the data
reader_base = PdfReader("labeled-edges-center-image.pdf")
page_base = reader_base.pages[0]

reader = PdfReader("box.pdf")
page_box = reader.pages[0]

# Write the result back
writer = PdfWriter()
page = writer.add_page(page_base)
page.merge_page(page_box)
writer.write("out-plain-merge.pdf")

Merge with Rotation

{testcode}
from pypdf import PdfReader, PdfWriter, Transformation

# Get the data
reader_base = PdfReader("labeled-edges-center-image.pdf")
page_base = reader_base.pages[0]

reader = PdfReader("box.pdf")
page_box = reader.pages[0]

# Prepare writer
writer = PdfWriter()

# Add base page.
writer_page = writer.add_page(page_base)

# Apply the transformation and merge the pages.
transformation = Transformation().rotate(45)
writer_page.merge_transformed_page(page_box, transformation)

# Write the result back
writer.write("out-merge-with-rotation.pdf")

If you add the expand parameter:

{testcode}
transformation = Transformation().rotate(45)
writer_page.merge_transformed_page(page_box, transformation, expand=True)

you get:

Alternatively, you can move the merged image a bit to the right by using

{testcode}
op = Transformation().rotate(45).translate(tx=50)

Scaling

In pypdf, the content and the page can either be scaled together or separately.
Content scaling scales the contents on a page, and page scaling scales just the page size (the canvas).
Typically, you want to combine both.

Scaling both the Page and contents together

{testcode}
from pypdf import PdfReader, PdfWriter

# Read the input
reader = PdfReader("side-by-side-subfig.pdf")
page = reader.pages[0]

# Add to the writer
writer = PdfWriter()
writer_page = writer.add_page(page)

# Scale
writer_page.scale_by(0.5)

# Write the result to a file
writer.write("out-scale-all.pdf")

Scaling the content only

The content is scaled around the origin of the coordinate system.
Typically, that is the lower-left corner.

{testcode}
from pypdf import PdfReader, PdfWriter, Transformation

# Read the input
reader = PdfReader("side-by-side-subfig.pdf")
page = reader.pages[0]

# Prepare the writer
writer = PdfWriter()
writer_page = writer.add_page(page)

# Scale
op = Transformation().scale(sx=0.7, sy=0.7)
writer_page.add_transformation(op)

# Write the result to a file
writer.write("out-scale-content.pdf")

Scaling the page only

To scale the page by sx in the X direction and sy in the Y direction:

{testcode}
page.mediabox = page.mediabox.scale(sx=0.7, sy=0.7)

If you wish to have more control, you can adjust the various page boxes directly:

{testcode}
from pypdf.generic import RectangleObject

mb = page.mediabox

page.mediabox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))
page.cropbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))
page.trimbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))
page.bleedbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))
page.artbox = RectangleObject((mb.left, mb.bottom, mb.right, mb.top))

Page merge box

pypdf<=3.4.0 used to merge the other page with trimbox.
pypdf>3.4.0 changes this behavior to cropbox.

In case anybody has good reasons to use/expect trimbox, you can add the
following code to get the old behavior:

{testcode}
from pypdf import overwrite_configuration

overwrite_configuration(page_merge_box="trimbox")

Transforming several copies of the same page

We have designed the following business card (A8 format) to advertise our new startup.

We 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).

{testcode}
from pypdf import PaperSize, PdfReader, PdfWriter, Transformation

# Read source file
reader = PdfReader("nup-source.pdf")
sourcepage = reader.pages[0]

# Create a destination file, and add a blank page to it
writer = PdfWriter()
destpage = writer.add_blank_page(width=PaperSize.A4.height, height=PaperSize.A4.width)

# Copy source page to destination page, several times
for x in range(4):
    for y in range(4):
        # Translate page
        transformation = Transformation().translate(
            x * PaperSize.A8.height,
            y * PaperSize.A8.width,
        )
        # Merge translated page
        destpage.merge_transformed_page(sourcepage, transformation)

# Write file
writer.write("out-nup-dest1.pdf")

There 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…

Possible issues

Especially when combining {func}~pypdf._page.PageObject.merge_page with transformations, you might end up with a cropped PDF file.
In these cases, consider setting expand=True to re-calculate the corresponding media box.


User Encryption Decryption (`docs/user/encryption-decryption.md`)

Encryption and Decryption of PDFs

PDF encryption makes use of RC4 and
AES algorithms
with different key length. pypdf supports all of them until PDF-2.0, which
is the latest PDF standard.

pypdf use an extra dependency to do encryption or decryption for AES algorithms.
We recommend pyca/cryptography. Alternatively,
you can use pycryptodome.

{note}
Please see the note in the [installation guide](installation.md)
for installing the extra dependencies if interacting with PDFs that use AES.

Encrypt

You can encrypt a PDF by using a password:

{testsetup}
pypdf_test_setup("user/encryption-decryption", {
    "example.pdf": "../resources/example.pdf",
    "encrypted-file.pdf": "../resources/encrypted-file.pdf",
})
{testcode}
from pypdf import PdfReader, PdfWriter

reader = PdfReader("example.pdf")
writer = PdfWriter(clone_from=reader)

# Add a password to the new PDF
writer.encrypt("my-secret-password", algorithm="AES-256")

# Save the new PDF to a file
writer.write("out-encrypt.pdf")

The algorithm can be one of RC4-40, RC4-128, AES-128, AES-256-R5, AES-256.
We recommend using AES-256-R5.

{warning}
pypdf uses `RC4` by default for compatibility if you omit the "algorithm" parameter.
Since `RC4` is insecure, you should use `AES` algorithms.

Decrypt

You can decrypt a PDF using the appropriate password:

{testcode}
from pypdf import PdfReader, PdfWriter

reader = PdfReader("encrypted-file.pdf")

if reader.is_encrypted:
    reader.decrypt("test")  # secret password

writer = PdfWriter(clone_from=reader)

# Save the new PDF to a file
writer.write("out-decrypt.pdf")

User Extract Images (`docs/user/extract-images.md`)

Extract Images

{note}
In order to use the following code you need to install optional
dependencies, see [installation guide](installation.md).

Every page of a PDF document can contain an arbitrary number of images.
The names of the files may not be unique.

{warning}
The names can contain arbitrary characters. Please make sure to sanitize them before
using them to write the file content to the disk for example.
{testsetup}
pypdf_test_setup("user/extract-images", {
    "example.pdf": "../resources/example.pdf",
})
{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

page = reader.pages[0]

for i, image_file_object in enumerate(page.images):
    file_name = "out-image-" + str(i) + "-" + image_file_object.name
    image_file_object.image.save(file_name)

Other images

Some other objects can contain images, such as stamp annotations.

You can extract the image from the annotation with the following code:

{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")
im = (
    reader.pages[0]["/Annots"][4]["/Parent"]
    .get_object()["/AP"]["/N"]["/Resources"]["/XObject"]["/Im4"]
    .decode_as_image()
)

im.save("out-annotation-image.png")

Error handling

Iterating over page.images directly will raise an exception on the first issue.
If you expect some more or less broken PDF files, but still want to retrieve as many images as possible,
consider making this a multistep process:

{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

for page in reader.pages:
    for name in page.images.keys():
        try:
            # Try to retrieve actual image.
            image = page.images[name]
        except Exception as exception:
            # Handle exceptions.
            pass

User Extract Text (`docs/user/extract-text.md`)

Extract Text from a PDF

You can extract text from a PDF:

{testsetup}
pypdf_test_setup("user/extract-text", {
    "test Orient.pdf": "../resources/test Orient.pdf",
    "GeoBase_NHNC1_Data_Model_UML_EN.pdf": "../resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf",
})
{testcode}
from pypdf import PdfReader

reader = PdfReader("test Orient.pdf")
page = reader.pages[0]
print(page.extract_text())

# extract only text oriented up
print(page.extract_text(0))

# extract text oriented up and turned left
print(page.extract_text((0, 90)))

# extract text in a fixed width format that closely adheres to the rendered
# layout in the source pdf
print(page.extract_text(extraction_mode="layout"))

# extract text preserving horizontal positioning without excess vertical
# whitespace (removes blank and "whitespace only" lines)
print(page.extract_text(extraction_mode="layout", layout_mode_space_vertically=False))

# adjust horizontal spacing
print(page.extract_text(extraction_mode="layout", layout_mode_scale_weight=1.0))

# exclude (default) or include (as shown below) text rotated w.r.t. the page
print(page.extract_text(extraction_mode="layout", layout_mode_strip_rotated=False))
{testoutput}
:options: +NORMALIZE_WHITESPACE
:hide:


(T) This is box text at top
written down from top
(B)  This is box text at bottom written up from bottom
(L) This is box text on left written vertically to starboard
(R) This is box text on righy written vertically to port


(T) This is box text at top
written down from top


(T) This is box text at top
written down from top
(L) This is box text on left written vertically to starboard

 (B)

This is box text at bottom
 from bottom upwritten




(T) This is box text at top
written down from top
 (B)
This is box text at bottom
 from bottom upwritten
(T) This is box text at top
written down from top
 (B)

This is box text at bottom
 from bottom upwritten




(T) This is box text at top
written down from top
 (B)

This is box text at bottom
 from bottom upwritten

(L) This is box textwritten vertically to starboard


 on righy


on left

 ) This is box text
written vertically to port (R




(T) This is box text at top
written down from top

Refer to {func}~pypdf._page.PageObject.extract_text for more details.

{note}
Extracting the text of a page requires parsing its whole content stream. This can require quite a lot of memory -
we have seen 10 GB RAM being required for an uncompressed content stream of about 300 MB (which should not occur
very often).

To limit the size of the content streams to process (and avoid OOM errors in your application), consider
checking `len(page.get_contents().get_data())` beforehand.
{note}
If a PDF page appears to contain only an image (e.g., a scanned document), the extracted text may be minimal or visually empty.
In such cases, consider using OCR software such as [Tesseract OCR](https://github.com/tesseract-ocr/tesseract) to extract text from images.

Using a visitor

You can use visitor functions to control which part of a page you want to process and extract. The visitor functions
you provide will get called for each operator or for each text fragment.

The function provided in argument visitor_text of function extract_text has five arguments:

  • text: the current text (as long as possible, can be up to a full line)
  • user_matrix: current matrix to move from user coordinate space (also known as CTM)
  • tm_matrix: current matrix from text coordinate space
  • font_dictionary: full font dictionary
  • font_size: the size (in text coordinate space)

The matrix stores six parameters. The first four provide the rotation/scaling matrix, and the last two provide the translation (horizontal/vertical).
It is recommended to use the user_matrix as it takes into account all transformations.

Notes :

  • 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.
  • If you want to get the full transformation from text to user space, you can use the {func}~.pypdf.mult function as follows:
    txt2user = mult(tm, cm).
    The font size is the raw text size and affected by the user_matrix.

The font_dictionary may be None in case of unknown fonts.
If not None, it could contain something like the key "/BaseFont" with the value "/Arial,Bold".

Caveat: In complicated documents, the calculated positions may be difficult to determine (if you move from multiple forms to page user space, for example).

The function provided in argument visitor_operand_before has four arguments:
operator, operand-arguments, current transformation matrix, and text matrix.

The following example reads the text of page four of this PDF document, but ignores the header (y > 720) and footer (y < 50). In this file we also need to include new line characters (y == 0).

{testcode}
from pypdf import PdfReader, mult

reader = PdfReader("GeoBase_NHNC1_Data_Model_UML_EN.pdf")
page = reader.pages[3]

parts = []


def visitor_body(text, cm, tm, font_dict, font_size):
    txt2user = mult(tm, cm)
    y = txt2user[5]
    if 50 < y < 720 or y == 0:
        parts.append(text)


page.extract_text(visitor_text=visitor_body)
text_body = "".join(parts)

print(text_body)
text
/* Detailed source-code truncated for AI context efficiency. */

Example 2: Extract rectangles and texts into an SVG file

The following example converts page three of this PDF document into
an SVG file.

Such an SVG export may help to understand what is going on in a page.

% We prefer not to execute doc examples for unmaintained third-party package "svgwrite"

{testcode}
:skipif: True

from pypdf import PdfReader, mult
import svgwrite

reader = PdfReader("GeoBase_NHNC1_Data_Model_UML_EN.pdf")
page = reader.pages[2]

dwg = svgwrite.Drawing("GeoBase_test.svg", profile="tiny")


def visitor_svg_rect(op, args, cm, tm):
    if op == b"re":
        (x, y, w, h) = (args[i].as_numeric() for i in range(4))
        dwg.add(dwg.rect((x, y), (w, h), stroke="red", fill_opacity=0.05))


def visitor_svg_text(text, cm, tm, font_dict, font_size):
    txt2user = mult(tm, cm)
    (x, y) = (txt2user[4], txt2user[5])
    dwg.add(dwg.text(text, insert=(x, y), fill="blue"))


page.extract_text(
    visitor_operand_before=visitor_svg_rect, visitor_text=visitor_svg_text
)
dwg.save()

The SVG generated here is bottom-up because the coordinate systems of PDF and SVG differ.

Unfortunately, in complicated PDF documents the coordinates given to the visitor functions may be wrong.

Why Text Extraction is hard

Unclear Objective

Extracting text from a PDF can be tricky. In several cases, there is no
clear answer to what the expected result should look like:

  1. Paragraphs: Should the text of a paragraph have line breaks at the same places
    where the original PDF had them or should it rather be one block of text?
  2. Page numbers: Should they be included in the extract?
  3. Headers and Footers: Similar to page numbers - should they be extracted?
  4. Outlines: Should outlines be extracted at all?
  5. Formatting: If the text is bold or italic, should it be included in the
    output?
  6. Tables: Should the text extraction skip tables? Should it extract just the
    text? Should the borders be shown in some Markdown-like way or should the
    structure be present e.g. as an HTML table? How would you deal with merged
    cells?
  7. Captions: Should image and table captions be included?
  8. Ligatures: The Unicode symbol U+FB00
    is a single symbol ff for two lowercase letters 'f'. Should that be parsed as
    the Unicode symbol 'ff' or as two ASCII symbols 'ff'?
  9. SVG images: Should the text parts be extracted?
  10. Mathematical Formulas: Should they be extracted? Formulas have indices
    and nested fractions.
  11. Whitespace characters: How many new lines should be extracted for 3 cm of
    vertical whitespace? How many spaces should be extracted if there is 3 cm of
    horizontal whitespace? When would you extract tabs and when spaces?
  12. Footnotes: When the text of multiple pages is extracted, where should footnotes be shown?
  13. Hyperlinks and Metadata: Should it be extracted at all? Where should it
    be placed in which format?
  14. Linearization: Assume you have a floating figure in between a paragraph.
    Do you first finish the paragraph, or do you put the figure text in between?

Then there are issues where most people would agree on the correct output, but
the way PDF stores information just makes it hard to achieve that:

  1. Tables: Typically, tables are just absolutely positioned text. In the worst
    case, every single letter could be absolutely positioned. That makes it hard
    to tell where columns / rows are.
  2. Images: Sometimes PDFs do not contain the text as it is displayed, but
    instead an image. You notice that when you cannot copy the text. Then there
    are PDF files that contain an image and a text layer in the background.
    That typically happens when a document was scanned. Although the scanning
    software (OCR) is pretty good today, it still fails once in a while. pypdf
    is no OCR software; it will not be able to detect those failures. pypdf
    will also never be able to extract text from images.

Finally, there are issues that pypdf will deal with. If you find such a
text extraction bug, please share the PDF with us so we can work on it!

Missing Semantic Layer

The PDF file format is all about producing the desired visual result for
printing. It was not created for parsing the content. PDF files don't contain a
semantic layer.

Specifically, there is no information what the header, footer, page numbers,
tables, and paragraphs are. The visual appearance is there, and people might
find heuristics to make educated guesses, but there is no way of being certain.

This is a shortcoming of the PDF file format, not of pypdf.

It is possible to apply machine learning on PDF documents to make good
heuristics, but that will not be part of pypdf. However, pypdf could be used to
feed such a machine learning system with the relevant information.

Whitespaces

The PDF format is meant for printing. It is not designed to be read by machines.
The text within a PDF document is absolutely positioned, meaning that every single
character could be positioned on the page.

The text

This is a test document by Ethan Nelson.

can be represented as

[(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

Where the numbers are adjustments of vertical space. This representation used
within the PDF file makes it very hard to guarantee correct whitespaces.

More information:

OCR vs. Text Extraction

Optical Character Recognition (OCR) is the process of extracting text from
images. Software which does this is called OCR software. The
tesseract OCR engine is the
most commonly known open source OCR software.

pypdf is not OCR software.

Digitally-born vs. Scanned PDF files

PDF documents can contain images and text. PDF files don't store text in a
semantically meaningful way, but in a way that makes it easy to show the
text on screen or print it. For this reason, text extraction from PDFs is hard.

If you scan a document, the resulting PDF typically shows the image of the scan.
Scanners then also run OCR software and put the recognized text in the background
of the image. pypdf can extract this result of the scanners OCR software. However,
in such cases, it's recommended to directly use OCR software as
errors can accumulate: The OCR software is not perfect in recognizing the text.
Then it stores the text in a format that is not meant for text extraction and
pypdf might make mistakes parsing that.

Hence, I would distinguish three types of PDF documents:

  • Digitally born PDF files: The file was created digitally on the computer.
    It can contain images, texts, links, outline items (a.k.a., bookmarks), JavaScript, ...
    If you Zoom in a lot, the text still looks sharp.
  • Scanned PDF files: Any number of pages was scanned. The images were then
    stored in a PDF file. Hence, the file is just a container for those images.
    You cannot copy the text, you don't have links, outline items, JavaScript.
  • OCRed PDF files: The scanner ran OCR software and put the recognized text
    in the background of the image. Hence, you can copy the text, but it still looks
    like a scan. If you zoom in enough, you can recognize pixels.

Can we just always use OCR?

You might now wonder if it makes sense to just always use OCR software. If the
PDF file is digitally-born, you can render it to an image.

I would recommend not to do that.

Text extraction software like pypdf can use more information from the
PDF than just the image. It can know about fonts, encodings, typical character
distances and similar topics.

That means pypdf has a clear advantage when it
comes to characters which are easy to confuse such as oO0ö.
pypdf will never confuse characters. It just reads what is in the file.

pypdf also has an edge when it comes to characters which are rare, e.g.
🤰. OCR software will not be able to recognize smileys correctly.

Attempts to prevent text extraction

If people who share PDF documents want to prevent text extraction, they have
multiple ways to do so:

  1. Store the contents of the PDF as an image
  2. Use a scrambled font

However, text extraction cannot be completely prevented if people should still
be able to read the document. In the worst case, people can make a screenshot,
print it, scan it, and run OCR over it.


User File Size (`docs/user/file-size.md`)

Reduce PDF File Size

There are multiple ways to reduce the size of a given PDF file. The easiest
one is to remove content (e.g., images) or pages.

Removing duplication

Some PDF documents contain the same object multiple times. For example, if an
image appears three times in a PDF, it could be embedded three times. Or it can
be embedded once and referenced twice.

When adding data to a PdfWriter, the data is copied while respecting the original format.
For example, if two pages include the same image which is duplicated in the source document, the object will be duplicated in the PdfWriter object.

Additionally, 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).

To reduce the file size, use a compression call: writer.compress_identical_objects(remove_duplicates=True, remove_unreferenced=True)

  • remove_duplicates enables/disables compression merging identical objects.
  • remove_unreferenced enables/disables suppression of unused objects.

It is recommended to apply this process just before writing to the file/stream.

It depends on the PDF how well this works, but we have seen an 86% file
reduction (from 5.7 MB to 0.8 MB) within a real PDF.

Removing Images

{testsetup}
pypdf_test_setup("user/file-size", {
    "example.pdf": "../resources/example.pdf",
})
{testcode}
from pypdf import PdfWriter

writer = PdfWriter(clone_from="example.pdf")

writer.remove_images()

writer.write("out-no-images.pdf")

Reducing Image Quality

If we reduce the quality of the images within the PDF, we can sometimes
reduce the file size of the PDF overall. That depends on how well the reduced
quality image can be compressed.

{testcode}
from pypdf import PdfWriter

writer = PdfWriter(clone_from="example.pdf")

for page in writer.pages:
    for img in page.images:
        img.replace(img.image, quality=80)

writer.write("out-low-quality.pdf")

Lossless Compression

pypdf supports the FlateDecode filter which uses the zlib/deflate compression
method. It is a lossless compression, meaning the resulting PDF looks exactly
the same.

Deflate compression can be applied to a page via
{meth}page.compress_content_streams :

{testcode}
from pypdf import PdfWriter

writer = PdfWriter(clone_from="example.pdf")

for page in writer.pages:
    page.compress_content_streams()  # This is CPU intensive!

writer.write("out-lossless.pdf")

page.compress_content_streams uses zlib.compress
and supports the level parameter: level=0 means no compression,
level=9 refers to the highest compression.

Using this method, we have seen a reduction by 70% (from 11.8 MB to 3.5 MB)
with a real PDF.

Removing Sources

When a page is removed from the page list, its content will still be present in
the PDF file. This means that the data may still be used elsewhere.

Simply removing a page from the page list will reduce the page count but not the
file size. To exclude the content completely, the pages should not be
added to the PDF using the PdfWriter.append() function. Instead, only the
desired pages should be selected for inclusion
(note: PR #1843 will add a page
deletion feature).

There can be issues with poor PDF formatting, such as when all pages are linked
to the same resource. In such cases, dropping references to specific pages
becomes useless because there is only one source for all pages.

Cropping is an ineffective method for reducing the file size because it only
adjusts the viewboxes and not the external parts of the source image. Therefore,
the content that is no longer visible will still be present in the PDF.

Going Further

The presentation Putting a Squeeze on Your PDF 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.


User Forms (`docs/user/forms.md`)

Interactions with PDF Forms

Reading form fields

{testsetup}
pypdf_test_setup("user/forms", {
    "form.pdf": "../resources/form.pdf",
})
{testcode}
from pypdf import PdfReader

reader = PdfReader("form.pdf")
fields = reader.get_form_text_fields()
fields == {"key": "value", "key2": "value2"}

# You can also get all fields:
fields = reader.get_fields()

Filling out forms

{testcode}
from pypdf import PdfReader, PdfWriter

reader = PdfReader("form.pdf")
writer = PdfWriter()

page = reader.pages[0]
fields = reader.get_fields()

writer.append(reader)

writer.update_page_form_field_values(
    writer.pages[0],
    {"fieldname": "some filled in text"},
    auto_regenerate=False,
)

writer.write("out-filled-form.pdf")

Generally speaking, you will always want to use auto_regenerate=False. The
parameter is True by default for legacy compatibility, but this flags the PDF
processor to recompute the field's rendering, and may trigger a "save changes"
dialog for users who open the generated PDF.

If you want to flatten your form, that is, keeping all form field contents while
removing the form fields themselves, you can set the flatten parameter in
{func}~pypdf.PdfWriter.update_page_form_field_values to True. This
will convert form field contents to regular PDF content. Afterwards, use
{func}~pypdf.PdfWriter.remove_annotations with subtypes="/Widget"
to remove all form fields to get an actual flattened PDF.

Some notes about form fields and annotations

PDF forms have a dual-nature approach to the fields:

  • Within the root object, an /AcroForm structure exists.
    Inside it, you could find (optional):

    • some global elements (Fonts, Resources,...)
    • 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)
    • /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
    • /Fields that houses an array of indirect references that reference the upper Field Objects (roots)
  • Within the page /Annots, you will spot /Widget annotations that define the visual rendering.

To flesh out this overview:

  • The core-specific properties of a field are:

    • /FT: The field type (Button, Text, Choice, or Signature).
    • /T: The partial field name.
    • /V: The field’s value, whose format varies depending on the field type.
    • /DV: The default value to which the field reverts when a reset-form action is executed.
  • To streamline readability, Field Objects and Widget Objects can be fused housing all properties.

  • 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 .)

    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.

  • 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.

  • If Fields stores only hidden values, no Widgets are required.

In pypdf fields are extracted from the /Fields array:

{testcode}
from pypdf import PdfReader

reader = PdfReader("form.pdf")
fields = reader.get_fields()
{testcode}
from pypdf import PdfReader
from pypdf.constants import AnnotationDictionaryAttributes

reader = PdfReader("form.pdf")
fields = []
for page in reader.pages:
    for annot in page.annotations:
        annot = annot.get_object()
        if annot[AnnotationDictionaryAttributes.Subtype] == "/Widget":
            fields.append(annot)

However, 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_from_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.

However, 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).

{note}
Remember 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.

In 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.

Identify pages where fields are used

To 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).

The page numbers can then be retrieved as usual by using page.page_number.


User Handle Attachments (`docs/user/handle-attachments.md`)

Handle Attachments

PDF documents can contain attachments, from time to time named embedded file as well.

Retrieve Attachments

Attachments have a name, but it might not be unique. For this reason, the value of reader.attachments["attachment_name"]
is a list.

{warning}
The names can contain arbitrary characters. Please make sure to sanitize them before
using them to write the file content to the disk for example.

You can extract all attachments like this:

{testsetup}
pypdf_test_setup("user/handle-attachments", {
    "example.pdf": "../resources/example.pdf",
})
{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

for name, content_list in reader.attachments.items():
    for i, content in enumerate(content_list):
        with open(f"out-attachment-{i}-{name}", "wb") as fp:
            fp.write(content)

Alternatively, you can retrieve them in an object-oriented fashion if you need
further details for these files:

{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

for attachment in reader.attachment_list:
    print(attachment.name, attachment.alternative_name, attachment.content)

Add Attachments

To add a new attachment, use the following code:

{testcode}
from pypdf import PdfWriter

writer = PdfWriter(clone_from="example.pdf")
writer.add_attachment(filename="test.txt", data=b"Hello World!")

As you can see, the basic attachment properties are its name and content. If you
want to modify further properties of it, the returned object provides corresponding
setters:

{testcode}
import datetime
import hashlib

from pypdf import PdfWriter
from pypdf.generic import create_string_object, ByteStringObject, NameObject, NumberObject


writer = PdfWriter(clone_from="example.pdf")
embedded_file = writer.add_attachment(filename="test.txt", data=b"Hello World!")

embedded_file.size = NumberObject(len(b"Hello World!"))
embedded_file.alternative_name = create_string_object("test1.txt")
embedded_file.description = create_string_object("My test file")
embedded_file.subtype = NameObject("/text/plain")
embedded_file.checksum = ByteStringObject(hashlib.md5(b"Hello World!").digest())
embedded_file.modification_date = datetime.datetime.now(tz=datetime.timezone.utc)
# embedded_file.content = "My new content."

writer.write("out-add-attachment.pdf")

The same functionality is available if you iterate over the attachments of a writer
using writer.attachment_list.

Delete Attachments

To delete an existing attachment, use the following code:

{testcode}
from pypdf import PdfWriter

writer = PdfWriter(clone_from="example.pdf")
attachment = writer.add_attachment(filename="test.txt", data=b"Hello World!")
attachment.delete()
assert list(writer.attachment_list) == []

Please note that this will not delete the associated file relationship
if it exists. Deleting them as well would require us to know where this has
been defined, which requires more complexity. For now, please consider looking
for the corresponding definition yourself and delete it from the array.

PDF/A compliance

The following example shows how to add an attachment to a PDF/A-3B compliant document
without breaking compliance:

{testcode}
from pypdf import PdfWriter
from pypdf.constants import AFRelationship
from pypdf.generic import create_string_object, ArrayObject, NameObject

writer = PdfWriter(clone_from="example.pdf")
attachment = writer.add_attachment(filename="test.txt", data="Hello World!")
attachment.subtype = NameObject("/text/plain")
attachment.associated_file_relationship = NameObject(AFRelationship.SUPPLEMENT)
attachment.alternative_name = create_string_object(attachment.name)

if "/AF" in writer.root_object:
    af = writer.root_object["/AF"].get_object()
else:
    af = ArrayObject()
    writer.root_object[NameObject("/AF")] = af
af.append(attachment.pdf_object.indirect_reference)

writer.write("out-a3b.pdf")

This example marks a relationship of the attachment to the whole document.
Alternatively, it can be added to most of the other PDF objects as well.
For details, see the corresponding PDF specification, like section 14.13
of the PDF 2.0 specification.


User Handling Outlines (`docs/user/handling-outlines.md`)

Handling Outlines

PDF 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.

Writing PDF Outlines

To 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.

Adding a Simple Outline

The 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.

{testsetup}
pypdf_test_setup("user/handling-outlines", {
    "crazyones.pdf":"../resources/crazyones.pdf",
})
{testcode}
from pypdf import PdfWriter

writer = PdfWriter(clone_from="crazyones.pdf")

# Add a top-level bookmark
writer.add_outline_item(
    title="Introduction",
    page_number=0
)

writer.write("simple-example.pdf")

Adding Nested Outlines

You can build hierarchies (like Chapter → Section) by passing the parent outline item to the parent parameter of a new item.

In the example below, we create a root item "Introduction" and nest two sections under it.

{testcode}
from pypdf import PdfWriter

writer = PdfWriter(clone_from="crazyones.pdf")

# Add parent (Chapter)
introduction = writer.add_outline_item(
    title="Chapter 1",
    page_number=0
)

# Add children (sections) nested under the introduction
writer.add_outline_item(
    title="Section 1.1",
    page_number=0,
    parent=introduction
)

writer.add_outline_item(
    title="Section 1.2",
    page_number=0,
    parent=introduction
)

writer.write("nested-example.pdf")

Advanced Styling and View Modes (Fit Options)

You can customize the appearance and behavior of bookmarks using optional parameters, such as changing the text color or applying bold and italic styles.

For detailed information on all available parameters and their formats, please refer to the {meth}~pypdf.PdfWriter.add_outline_item API documentation.

The 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.

{testcode}
from pypdf import PdfWriter
from pypdf.generic import Fit

writer = PdfWriter(clone_from="crazyones.pdf")

# Top-level chapter (Points to Page 3, Index 2)
chapter2 = writer.add_outline_item(
    title="Chapter 2",
    page_number=0,
    color=(0, 0, 1),
    bold=True,
    italic=False,
    is_open=True,
    fit=Fit.fit()
)

# Section under Chapter 2 (Points to Page 3, Index 2)
section2_1 = writer.add_outline_item(
    title="Section 2.1",
    page_number=0,
    parent=chapter2,
    color=(0, 0.5, 0),
    bold=False,
    italic=True,
    is_open=False,
    fit=Fit.fit_horizontally(top=800)
)

# Section with custom zoom (Points to Page 3, Index 2)
section2_2 = writer.add_outline_item(
    title="Section 2.2",
    page_number=0,
    parent=chapter2,
    color=(1, 0, 0),
    bold=True,
    italic=True,
    is_open=True,
    fit=Fit.xyz(left=0, top=800, zoom=1.25)
)

writer.write("advanced-example.pdf")
{figure} complete-outlines.png
:alt: An annotated screenshot illustrating simple, nested, and advanced PDF bookmarks.

An annotated screenshot illustrating simple, nested, and advanced PDF bookmarks in a Table of Contents.

Reading PDF Outlines

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.

To 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.

Reading Simple Outlines

To 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.

{testcode}
from pypdf import PdfReader

reader = PdfReader("simple-example.pdf")

print("Simple Outline (Top-Level Only):")
print("-" * 32)

for outline in reader.outline:
    # Check if the item is a list (which represents nested children)
    if isinstance(outline, list):
        continue  # Skip the nested parts completely

    page_number = reader.get_destination_page_number(outline)

    if page_number is None:
        print(f"{outline.title} -> No page destination")
    else:
        print(f"{outline.title} -> page {page_number + 1}")
{testoutput}
Simple Outline (Top-Level Only):
--------------------------------
Introduction -> page 1

Reading Nested Outlines

When 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.

The following example defines a print_outline function that handles indentation and nested lists to display the structure of the document we created earlier.

{testcode}
from typing import List, Union

from pypdf import PdfReader
from pypdf.generic import Destination


def print_outline(
    outlines: List[Union[Destination, List[Destination]]],
    reader: PdfReader,
    level: int = 0
) -> None:
    """Recursively print all outline items with indentation."""
    for item in outlines:
        if isinstance(item, list):
            # Recursively handle the nested list of children
            print_outline(item, reader, level + 1)
        else:
            page_number = reader.get_destination_page_number(item)

            indent = "  " * level

            if page_number is None:
                print(f"{indent}- {item.title} (No page destination)")
            else:
                print(f"{indent}- {item.title} (Page {page_number + 1})")


reader = PdfReader("nested-example.pdf")

print("Nested Outline Hierarchy:")
print("-" * 25)

print_outline(reader.outline, reader)
{testoutput}
Nested Outline Hierarchy:
-------------------------
- Chapter 1 (Page 1)
  - Section 1.1 (Page 1)
  - Section 1.2 (Page 1)

User Installation (`docs/user/installation.md`)

Installation

There are several ways to install pypdf. The most common option is to use pip.

pip

pypdf requires Python 3.9+ to run.

Typically, Python comes with pip, a package installer. Using it, you can
install pypdf:

bash
pip install pypdf

If you are not a superuser (a system administrator / root), you can also just
install pypdf for your current user:

bash
pip install --user pypdf

Optional dependencies

pypdf tries to be as self-contained as possible, but for some tasks, the amount
of work to properly maintain the code would be too high. This is especially the
case for cryptography and image formats.

If you simply want to install all optional dependencies, run:

text
pip install pypdf[full]

Alternatively, you can install just some:

If you plan to use pypdf for encrypting or decrypting PDFs that use AES, you
will need to install some extra dependencies. Encryption using RC4 is supported
using the regular installation.

text
pip install pypdf[crypto]

If you plan to use image extraction, you need Pillow:

text
pip install pypdf[image]

For JBIG2 support, you need to install a global OS-level package as well:
jbig2dec The installation procedure
depends on our operating system. For Ubuntu, use the following, for example:

text
sudo apt-get install jbig2dec

Python Version Support

Since pypdf 4.0, every release, including point releases, should work with all
supported versions of Python. Thus,
every point release is designed to work with all existing Python versions,
excluding end-of-life versions.

Previous versions of pypdf support the following versions of Python:

Python 3.11 3.10 3.9 3.8 3.7 3.6 2.7
pypdf 3.x ✅ ✅ ✅ ✅ ✅ ✅ ❌
PyPDF2 >= 2.0 ✅ ✅ ✅ ✅ ✅ ✅ ❌
PyPDF2 1.20.0 - 1.28.4 ❌ ✅ ✅ ✅ ✅ ✅ ✅
PyPDF2 1.15.0 - 1.20.0 ❌ ❌ ❌ ❌ ❌ ❌ ✅

Anaconda

Anaconda users can install pypdf via conda-forge.

Development Version

In case you want to use the current version under development:

bash
pip install git+https://github.com/py-pdf/pypdf.git

User Merging Pdfs (`docs/user/merging-pdfs.md`)

Merging PDF files

Basic Example

{testsetup}
pypdf_test_setup("user/merging-pdfs", {
    "example.pdf": "../resources/example.pdf",
    "hello-world.pdf": "../resources/hello-world.pdf",
    "jpeg.pdf": "../resources/jpeg.pdf",
    "GeoBase_NHNC1_Data_Model_UML_EN.pdf": "../resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf",
    "Seige_of_Vicksburg_Sample_OCR.pdf": "../resources/Seige_of_Vicksburg_Sample_OCR.pdf",
    "two-different-pages.pdf": "../resources/two-different-pages.pdf",
})
{testcode}
from pypdf import PdfWriter

merger = PdfWriter()

for pdf in ["example.pdf", "hello-world.pdf", "jpeg.pdf"]:
    merger.append(pdf)

merger.write("out-basic.pdf")

For more details, see an excellent answer on
StackOverflow
by Paul Rooney.

{note}
Dealing with large PDF files might reach the recursion limit of the current
Python interpreter. In these cases, increasing the limit might help:

```{testcode}
import sys

# Example: Increase the current limit by factor 5.
sys.setrecursionlimit(sys.getrecursionlimit() * 5)
```

Showing more merging options

{testcode}
from pypdf import PdfWriter

merger = PdfWriter()

with (
    open("Seige_of_Vicksburg_Sample_OCR.pdf", "rb") as input1,
    open("two-different-pages.pdf", "rb") as input2,
    open("example.pdf", "rb") as input3
):
    # Add the first 3 pages of input1 document to output
    merger.append(fileobj=input1, pages=(0, 3))

    # Insert the first page of input2 into the output beginning after the second page
    merger.merge(position=2, fileobj=input2, pages=(0, 1))

    # Append entire input3 document to the end of the output document
    merger.append(input3)

    # Write to an output PDF document
    merger.write("out-advanced.pdf")

append

append has been slightly extended in PdfWriter. See {func}~pypdf.PdfWriter.append for more details.

Examples

{testcode}
from pypdf import PdfWriter, PdfReader

writer = PdfWriter()

source_file_name = "GeoBase_NHNC1_Data_Model_UML_EN.pdf"

# Append the first 10 pages from pdf file
writer.append(source_file_name, (0, 10))

reader = PdfReader(source_file_name)

# Append the first and 10th page from reader and create an outline
writer.append(reader, "page 1 and 10", [0, 9])

During merging, the relevant named destination will also be imported.

If you want to insert pages in the middle of the destination, use merge (which provides an insertion position).
You can insert the same page multiple times, if necessary, even using a list-based syntax:

{testcode}
# Insert pages 2 and 3, with page 1 before, between, and after
writer.append(reader, [0, 1, 0, 2, 0])

add_page / insert_page

It is recommended to use append or merge instead.

Merging forms

When merging forms, some form fields may have the same names, preventing access to some data.

A grouping field should be added before adding the source PDF to prevent that.
The original fields will be identified by adding the group name.

For example, after calling reader.add_form_topname("form1"), the field
previously named field1 is now identified as form1.field1 when calling
reader.get_form_text_fields(True) or reader.get_fields().

After that, you can append the input PDF completely or partially using
writer.append or writer.merge. If you insert a set of pages, only those
fields will be listed.

reset_translation

During cloning, if an object has been already cloned, it will not be cloned again, and a pointer
to this previously cloned object is returned instead. Because of that, if you add/merge a page that has
already been added, the same object will be added the second time. If you modify any of these two pages later,
both pages can be modified independently.

To reset, call writer.reset_translation(reader).

Advanced cloning

To prevent side effects between pages/objects and all objects linked cloning is done during the merge.

This process will be automatically applied if you use PdfWriter.append/merge/add_page/insert_page.
If you want to clone an object before attaching it "manually", use the clone method of any PdfObject:

{testcode}
from pypdf.generic import NameObject, NumberObject, StreamObject

stream_object = StreamObject()

cloned_object = stream_object.clone(writer)

If you try to clone an object already belonging to the writer, it will return the same object:

{testcode}
assert cloned_object == stream_object.clone(writer)

The same holds true if you try to clone an object twice. It will return the previously cloned object:

{testcode}
assert stream_object.clone(writer) == stream_object.clone(writer)

Please note that if you clone an object, you will clone all the objects below as well,
including the objects pointed by IndirectObject. Due to this, if you clone a page that
includes some articles ("/B"), not only the first article, but also all the chained articles
and the pages where those articles can be read will be copied.
This means that you may copy lots of objects which will be saved in the output PDF as well.

To prevent this, you can provide the list of fields in the dictionaries to be ignored:

{testcode}
new_page = writer.add_page(reader.pages[0], excluded_keys=["/B"])

Merging rotated pages

If you are working with rotated pages, you might want to call {func}~pypdf._page.PageObject.transfer_rotation_to_content on the page
before merging to avoid wrongly rotated results:

{testcode}
background = PdfReader("jpeg.pdf").pages[0]

for page in writer.pages:
    if page.rotation != 0:
        page.transfer_rotation_to_content()
    page.merge_page(background, over=False)

User Metadata (`docs/user/metadata.md`)

Metadata

PDF files can have two types of metadata: "Regular" and XMP ones. They can both exist at the same time.

Reading metadata

{testsetup}
pypdf_test_setup("user/metadata", {
    "example.pdf": "../resources/example.pdf",
    "commented-xmp.pdf": "../resources/commented-xmp.pdf",
})
{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

meta = reader.metadata

# All the following could be None!
print(meta.title)
print(meta.author)
print(meta.subject)
print(meta.creator)
print(meta.producer)
print(meta.creation_date)
print(meta.modification_date)

% Two last rows masked to allow to change example.pdf

{testoutput}
:hide:

PDF Example Document
None
None
None
Skia/PDF m103 Google Docs Renderer
...
...

Writing metadata

{testcode}
from datetime import datetime
from pypdf import PdfReader, PdfWriter

reader = PdfReader("example.pdf")
writer = PdfWriter()

# Add all pages to the writer
for page in reader.pages:
    writer.add_page(page)

# If you want to add the old metadata, include these two lines
if reader.metadata is not None:
    writer.add_metadata(reader.metadata)

# Format the current date and time for the metadata
utc_time = "-05'00'"  # UTC time optional
time = datetime.now().strftime(f"D\072%Y%m%d%H%M%S{utc_time}")

# Add the new metadata
writer.add_metadata(
    {
        "/Author": "Martin",
        "/Producer": "Libre Writer",
        "/Title": "Title",
        "/Subject": "Subject",
        "/Keywords": "Keywords",
        "/CreationDate": time,
        "/ModDate": time,
        "/Creator": "Creator",
        "/CustomField": "CustomField",
    }
)

# Save the new PDF to a file
writer.write("out-meta-create.pdf")

Updating metadata

{testcode}
from pypdf import PdfWriter

writer = PdfWriter(clone_from="example.pdf")

# Change some values
writer.add_metadata(
    {
        "/Author": "Martin",
        "/Producer": "Libre Writer",
        "/Title": "Title",
    }
)

# Clear all data but keep the entry in PDF
writer.metadata = {}

# Replace all entries with new set of entries
writer.metadata = {
    "/Author": "Martin",
    "/Producer": "Libre Writer",
}

# Save the new PDF to a file
writer.write("out-meta-update.pdf")

Removing metadata entry

{testcode}
from pypdf import PdfWriter

writer = PdfWriter("example.pdf")

# Remove Metadata (/Info entry)
writer.metadata = None

# Save the new PDF to a file
writer.write("out-meta-remove.pdf")

Reading XMP metadata

{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

meta = reader.xmp_metadata
if meta:
    print(meta.dc_title)
    print(meta.dc_description)
    print(meta.xmp_create_date)
{testoutput}
:hide:

{'x-default': 'PDF Example Document'}
{}
2025-10-30 09:29:55

Creating XMP metadata

You can create XMP metadata easily using the XmpInformation.create() method:

{testcode}
from pypdf import PdfWriter
from pypdf.xmp import XmpInformation

# Create a new XMP metadata object
xmp = XmpInformation.create()

# Set metadata fields
xmp.dc_title = {"x-default": "My Document Title"}
xmp.dc_creator = ["Author One", "Author Two"]
xmp.dc_description = {"x-default": "Document description"}
xmp.dc_subject = ["keyword1", "keyword2", "keyword3"]
xmp.pdf_producer = "pypdf"

# Create a writer and add the metadata
writer = PdfWriter()
writer.add_blank_page(612, 792)  # Add a page
writer.xmp_metadata = xmp
writer.write("out-xmp-create.pdf")

Setting XMP metadata fields

The XmpInformation class provides property-based access for all supported metadata fields:

Dublin Core fields

{testcode}
from datetime import datetime
from pypdf.xmp import XmpInformation

xmp = XmpInformation.create()

# Single value fields
xmp.dc_coverage = "Global coverage"
xmp.dc_format = "application/pdf"
xmp.dc_identifier = "unique-id-123"
xmp.dc_source = "Original Source"

# Array fields (bags - unordered)
xmp.dc_contributor = ["Contributor One", "Contributor Two"]
xmp.dc_language = ["en", "fr", "de"]
xmp.dc_publisher = ["Publisher One"]
xmp.dc_relation = ["Related Doc 1", "Related Doc 2"]
xmp.dc_subject = ["keyword1", "keyword2"]
xmp.dc_type = ["Document", "Text"]

# Sequence fields (ordered arrays)
xmp.dc_creator = ["Primary Author", "Secondary Author"]
xmp.dc_date = [datetime.now()]

# Language alternative fields
xmp.dc_title = {"x-default": "Title", "en": "English Title", "fr": "Titre français"}
xmp.dc_description = {"x-default": "Description", "en": "English Description"}
xmp.dc_rights = {"x-default": "All rights reserved"}

XMP fields

{testcode}
from datetime import datetime

# Date fields accept both datetime objects and strings
xmp.xmp_create_date = datetime.now()
xmp.xmp_modify_date = datetime.fromisoformat("2023-12-25T10:30:45Z")
xmp.xmp_metadata_date = datetime.now()

# Text field
xmp.xmp_creator_tool = "pypdf"

PDF fields

{testcode}
xmp.pdf_keywords = "keyword1, keyword2, keyword3"
xmp.pdf_pdfversion = "1.4"
xmp.pdf_producer = "pypdf"

XMP Media Management fields

{testcode}
xmp.xmpmm_document_id = "uuid:12345678-1234-1234-1234-123456789abc"
xmp.xmpmm_instance_id = "uuid:87654321-4321-4321-4321-cba987654321"

PDF/A fields

{testcode}
xmp.pdfaid_part = "1"
xmp.pdfaid_conformance = "B"

Clearing metadata fields

You can clear any field by assigning None:

{testcode}
xmp.dc_title = None
xmp.dc_creator = None
xmp.pdf_producer = None

Incrementally updating XMP metadata fields

When 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:

{testcode}
from pypdf.xmp import XmpInformation

xmp = XmpInformation.create()

# Language alternative fields return dictionaries
title = xmp.dc_title or {}
title["en"] = "English Title"
title["fr"] = "Titre français"
xmp.dc_title = title

# Bag fields (unordered collections) return lists
subjects = xmp.dc_subject or []
subjects.append("new_keyword")
xmp.dc_subject = subjects

# Sequence fields (ordered collections) return lists
creators = xmp.dc_creator or []
creators.append("New Author")
xmp.dc_creator = creators

This approach provides direct control over the data structures while maintaining the property-based interface.

Modifying XMP metadata

Modifying XMP metadata is a bit more complicated.

As an example, we want to add the following PDF/UA identifier section to the XMP metadata:

xml
<rdf:Description rdf:about="" xmlns:pdfuaid="http://www.aiim.org/pdfua/ns/id/">
    <pdfuaid:part>1</pdfuaid:part>
</rdf:Description>

This could be written like this:

{testcode}
from pypdf import PdfWriter

writer = PdfWriter(clone_from="commented-xmp.pdf")

metadata = writer.xmp_metadata
assert metadata  # Ensure that it is not `None`.
rdf_root = metadata.rdf_root
xmp_meta = rdf_root.parentNode
xmp_document = xmp_meta.parentNode

# Please note that without a text node, the corresponding elements might
# be omitted completely.
pdfuaid_description = xmp_document.createElement("rdf:Description")
pdfuaid_description.setAttribute("rdf:about", "")
pdfuaid_description.setAttribute("xmlns:pdfuaid", "http://www.aiim.org/pdfua/ns/id/")
pdfuaid_part = xmp_document.createElement("pdfuaid:part")
pdfuaid_part_text = xmp_document.createTextNode("1")
pdfuaid_part.appendChild(pdfuaid_part_text)
pdfuaid_description.appendChild(pdfuaid_part)
rdf_root.appendChild(pdfuaid_description)

metadata.stream.set_data(xmp_document.toxml().encode("utf-8"))

writer.write("out-xmp-update.pdf")

For further details on modifying the structure, please refer to {py:mod}xml.dom.minidom.


User Pdf Version Support (`docs/user/pdf-version-support.md`)

PDF Version Support

PDF comes in the following versions:

  • 1993: 1.0
  • 1994: 1.1
  • 1996: 1.2
  • 1999: 1.3
  • 2001: 1.4
  • 2003: 1.5
  • 2004: 1.6
  • 2008: 1.7, ISO 32000-1:2008
  • 2017: 2.0, ISO 32000-2:2017

The general format didn't change, but new features got added. It can be that
pypdf can do the operations you want on PDF 2.0 files without fully supporting
all features of PDF 2.0.

PDF Feature Support by pypdf

Feature PDF Version pypdf Support
CMaps 1.4 ✅
Transparent Graphics 1.4 ✅
Content Stream Compression 1.5 ✅
Cross-reference Streams 1.5 ✅
Object Streams 1.5 ✅
Optional Content Groups (OCGs) 1.5 ❓
AES Encryption 1.6 ✅

This table is not complete - if in doubt, consider having a look at the API documentation or
inside the issues or try with a corresponding PDF file. In general, we are open to
add support for missing features. Please open a new issue if it does not exist yet, and
keep in mind that we rely on external contributors to support us with the implementation.

One commonly requested feature is proper support reading/handling incremental PDF files, see
issue #3304.

See History of PDF for more
features.

Some PDF features are not supported by pypdf, but other libraries can be used
for them:


User Pdfa Compliance (`docs/user/pdfa-compliance.md`)

PDF/A Compliance

PDF/A is a specialized, ISO-standardized version of the Portable Document Format
(PDF) specifically designed for the long-term preservation and archiving of
electronic documents. It ensures that files remain accessible, readable, and
true to their original appearance by embedding all necessary fonts, images, and
metadata within the document itself. By adhering to strict guidelines and
minimizing dependencies on external resources or proprietary software, PDF/A
ensures the consistent and reliable reproduction of content, safeguarding it
against future technological changes and obsolescence.

PDF/A Versions

  • PDF/A-1: Based on PDF 1.4, PDF/A-1 is the first version of the standard
    and is divided into two levels: PDF/A-1a (Level A, ensuring accessibility) and
    PDF/A-1b (Level B, ensuring visual preservation).
    • Level B (Basic): Ensures visual preservation and basic requirements for archiving.
    • Level A (Accessible): Everything from level B, but includes additional
      requirements for accessibility, such as tagging, Unicode character
      mapping, and logical structure.
  • PDF/A-2: Based on PDF 1.7 (ISO 32000-1), PDF/A-2 adds features and
    improvements over PDF/A-1, while maintaining compatibility with PDF/A-1b
    (Level B) documents.
    • Level B (Basic): Like PDF/A-1b, but support for PDF 1.7 features such
      as transparency layers.
    • Level U (Unicode): Ensures Unicode mapping without the full
      accessibility requirements of PDF/A-1a (Level A).
    • Level A (Accessible): Similar to PDF/A-1a
  • PDF/A-3: Based on PDF 1.7 (ISO 32000-1), PDF/A-3 is similar to PDF/A-2 but
    allows the embedding of non-PDF/A files as attachments, enabling the archiving
    of source or supplementary data alongside the PDF/A document. This is
    interesting for invoices which can add XML files.
  • PDF/A-4: Based on PDF 2.0 (ISO 32000-2), PDF/A-4 introduces new features
    and improvements for better archiving and accessibility. The previous levels
    are replaced by PDF/A-4f (ensuring visual preservation and allowing attachments)
    and PDF/A-4e (Engineering, allows 3D content).

PDF/A-1b

In contrast to other PDF documents, PDF/A-1b documents must fulfill those
requirements:

  • MarkInfo Object: The MarkInfo object is a dictionary object within a PDF/A
    file that provides information about the logical structure and tagging of the
    document. The MarkInfo object indicates whether the document is tagged,
    contains optional content, or has a structure tree that describes the logical
    arrangement of content such as headings, paragraphs, lists, and tables. By
    including the MarkInfo object, PDF/A ensures that electronic documents are
    accessible to users with disabilities, such as those using screen readers or
    other assistive technologies.
  • Embedded fonts: All fonts used in the document must be embedded to ensure
    consistent text rendering across different devices and systems.
  • Color Spaces: DeviceRGB is a device-dependent color space that relies on
    the specific characteristics of the output device, which can lead to
    inconsistent color rendering across various devices. To achieve accurate and
    consistent color representation, PDF/A requires the use of device-independent
    color spaces, such as ICC-based color profiles.
  • XMP (Extensible Metadata Platform) metadata: XMP metadata provides a
    standardized and extensible way to store essential information about a
    document and its properties. XMP metadata is an XML-based format embedded
    directly within a PDF/A file. It contains various types of information, such
    as document title, author, creation and modification dates, keywords, and
    copyright information, as well as PDF/A-specific details like conformance
    level and OutputIntent.

Validation

VeraPDF is the go-to PDF/A validator.

There are several online validators that allow you to simply upload the document:

pypdf and PDF/A

At the moment, pypdf does not make any guarantees regarding PDF/A.
Support is very welcome.


User Post Processing In Text Extraction (`docs/user/post-processing-in-text-extraction.md`)

Post-Processing of Text Extraction

Post-processing can recognizably improve the results of text extraction. It is,
however, outside the scope of pypdf itself. Hence, the library will not give
any direct support for it. It is a natural language processing (NLP) task.

This page lists a few examples of what can be done as well as a community recipe
that can be used as a general purpose post-processing step. If you know more
about the specific domain of your documents, e.g., the language, it is likely
that you can find custom solutions that work better in your context.

Ligature Replacement

{testcode}
def replace_ligatures(text: str) -> str:
    ligatures = {
        "ff": "ff",
        "fi": "fi",
        "fl": "fl",
        "ffi": "ffi",
        "ffl": "ffl",
        "ſt": "ft",
        "st": "st",
        # "Ꜳ": "AA",
        # "Æ": "AE",
        "ꜳ": "aa",
    }
    for search, replace in ligatures.items():
        text = text.replace(search, replace)
    return text

Dehyphenation

Hyphens are used to break words up so that the appearance of the page is nicer.

{testcode}
from typing import List


def remove_hyphens(text: str) -> str:
    """

    This fails for:
    * Natural dashes: well-known, self-replication, use-cases, non-semantic,
                      Post-processing, Window-wise, viewpoint-dependent
    * Trailing math operands: 2 - 4
    * Names: Lopez-Ferreras, VGG-19, CIFAR-100
    """
    lines = [line.rstrip() for line in text.split("\n")]

    # Find dashes
    line_numbers = []
    for line_no, line in enumerate(lines[:-1]):
        if line.endswith("-"):
            line_numbers.append(line_no)

    # Replace
    for line_no in line_numbers:
        lines = dehyphenate(lines, line_no)

    return "\n".join(lines)


def dehyphenate(lines: List[str], line_no: int) -> List[str]:
    next_line = lines[line_no + 1]
    word_suffix = next_line.split(" ")[0]

    lines[line_no] = lines[line_no][:-1] + word_suffix
    lines[line_no + 1] = lines[line_no + 1][len(word_suffix) :]
    return lines

The following header/footer removal has several drawbacks:

  • False-positives, e.g., for the first page when there is a date like 2024.
  • False-negatives in many cases:
    • Dynamic part, e.g., page label is in the header.
    • Even/odd pages have different headers.
    • Some pages, e.g., the first one or chapter pages, do not have a header.
{testcode}
def remove_footer(extracted_texts: list[str], page_labels: list[str]):
    def remove_page_labels(extracted_texts, page_labels):
        processed = []
        for text, label in zip(extracted_texts, page_labels):
            text_left = text.lstrip()
            if text_left.startswith(label):
                text = text_left[len(label) :]

            text_right = text.rstrip()
            if text_right.endswith(label):
                text = text_right[: -len(label)]

            processed.append(text)
        return processed

    extracted_texts = remove_page_labels(extracted_texts, page_labels)
    return extracted_texts

Other ideas

  • Whitespaces in units: Between a number and its unit should be a space.
    (source).
    That means: 42 ms, 42 GHz, 42 GB.
  • Percent: English style guides prescribe writing the percent sign following the number without any space between (e.g., 50%).
  • Whitespaces before dots: Should typically be removed.
  • Whitespaces after dots: Should typically be added.

User Reading Pdf Annotations (`docs/user/reading-pdf-annotations.md`)

Reading PDF Annotations

PDF 2.0 defines the following annotation types:

  • Text
  • Link
  • FreeText
  • Line
  • Square
  • Circle
  • Polygon
  • PolyLine
  • Highlight
  • Underline
  • Squiggly
  • StrikeOut
  • Caret
  • Stamp
  • Ink
  • Popup
  • FileAttachment
  • Sound
  • Movie
  • Screen
  • Widget
  • PrinterMark
  • TrapNet
  • Watermark
  • 3D
  • Redact
  • Projection
  • RichMedia

In general, annotations can be read like this:

{testsetup}
pypdf_test_setup("user/reading-pdf-annotations", {
    "example.pdf": "../resources/example.pdf",
})
{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

for page in reader.pages:
    if "/Annots" in page:
        for annotation in page["/Annots"]:
            obj = annotation.get_object()
            print({"subtype": obj["/Subtype"], "location": obj["/Rect"]})
{testoutput}
:hide:

{'subtype': '/Highlight', 'location': [376.771, 406.213, 413.78, 422.506]}
{'subtype': '/Popup', 'location': [531.053, 327.965, 715.198, 422.219]}
{'subtype': '/FileAttachment', 'location': [245.819, 223.288, 252.819, 240.288]}
{'subtype': '/Stamp', 'location': [68.7536, 187.259, 151.442, 254.124]}
{'subtype': '/Popup', 'location': [612, 631.925, 816, 745.925]}
{'subtype': '/Text', 'location': [176.9, 216.719, 200.9, 240.719]}
{'subtype': '/Popup', 'location': [596, 709.445, 780, 801.445]}

Examples of reading three of the most common annotations:

Text

{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

for page in reader.pages:
    if "/Annots" in page:
        for annotation in page["/Annots"]:
            subtype = annotation.get_object()["/Subtype"]
            if subtype == "/Text":
                print(annotation.get_object()["/Contents"])
{testoutput}
:hide:

Text comment

Highlights

{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

for page in reader.pages:
    if "/Annots" in page:
        for annotation in page["/Annots"]:
            subtype = annotation.get_object()["/Subtype"]
            if subtype == "/Highlight":
                coords = annotation.get_object()["/QuadPoints"]
                x1, y1, x2, y2, x3, y3, x4, y4 = coords

Attachments

{testcode}
from pypdf import PdfReader

reader = PdfReader("example.pdf")

attachments = {}
for page in reader.pages:
    if "/Annots" in page:
        for annotation in page["/Annots"]:
            subtype = annotation.get_object()["/Subtype"]
            if subtype == "/FileAttachment":
                fileobj = annotation.get_object()["/FS"]
                attachments[fileobj["/F"]] = fileobj["/EF"]["/F"].get_data()

User Robustness (`docs/user/robustness.md`)

Robustness and strict=False

PDF is specified in various versions.
The specification of PDF 2.0 has 1003 pages. This length makes it hard to get
everything right. As a consequence, a lot of PDF files are not strictly following the
specification.

If a PDF file does not follow the specification, it is not always possible to
be certain what the intended effect would be. Think of the following broken
Python code as an example:

{testcode}
# Broken
function (foo, bar):

# Potentially intended:
def function(foo, bar):
    ...

# Also possible:
function = (foo, bar)
{testoutput}
:hide:

Traceback (most recent call last):
    ...
SyntaxError: invalid syntax

Writing a parser, you can go two paths: Either you try to be forgiving and try
to figure out what the user intended, or you are strict and just tell the user
that they should fix their stuff.

pypdf gives you the option to be strict or not.

pypdf has two core objects:

  • {class}~pypdf.PdfReader
  • {class}~pypdf.PdfWriter

PdfReader and PdfWriter both have a strict parameter.

Choosing strict=True means that pypdf will raise an exception if a PDF does
not follow the specification.

Choosing strict=False means that pypdf will try to be forgiving and do
something reasonable, but it will log a warning message. It is a best-effort
approach.


User Security (`docs/user/security.md`)

Security

We strive to provide a library with secure defaults.

Configuration

Global

pypdf currently employs a set of global configuration values, whose descriptions and defaults are
available at {py:class}~pypdf._configuration.Configuration. They internally rely on
{py:mod}contextvars.

{testsetup}
pypdf_test_setup("user/security", {
    "example.pdf": "../resources/example.pdf",
})
{testcode}
from pypdf import Configuration, PdfReader, apply_configuration, overwrite_configuration

# Limit the values to the current scope.
# The changed configuration value will be reset when exiting the context manager.
with apply_configuration(maximum_declared_stream_length=10_000):
    reader = PdfReader("example.pdf")
    for page in reader.pages:
        # Do something with the page.
        pass

# Overwrite the values globally.
overwrite_configuration(maximum_declared_stream_length=5_000)
reader = PdfReader("example.pdf")
for page in reader.pages:
    # Do something with the page.
    pass

Reading

pypdf currently employs the following reading limits on PdfReader instances:

  • root_object_recovery_limit limits the number of objects to read before stopping with Root object recovery in
    non-strict mode. It defaults to 10 000. Setting it to None will fully disable this limit.

If you want to employ custom limits for the PdfWriter as well, the currently preferred way
is to initialize it from the reader, id est something like
PdfWriter(clone_from=PdfReader("file.pdf", root_object_recovery_limit=42)).

For PdfWriter instances, the following limits are employed for incremental reading:

  • incremental_clone_object_count_limit limits the number of objects to read during cloning. It defaults to
    500 000. Setting it to None will fully disable this limit.
  • incremental_clone_object_id_limit limits the maximum object ID to read during cloning. It defaults to
    1 000 000. Setting it to None will fully disable this limit.

Reporting possible vulnerabilities

Please refer to our security policy.

Invalid reports

Exceptions

Most exceptions raised by our code are considered bugs or robustness issues and can be reported publicly.
We consider it the task of the library user to catch exceptions which could cause their service to crash, although we try to
only raise a known set of exception types.

Cryptographic functions

We receive reports about possibly insecure cryptography from time to time. This includes the following aspects:

  • Using the ARC4 cipher
  • Using the AES cipher in ECB mode
  • Using MD5 for hashing

These are requirements of the PDF standard, which we need to achieve the greatest compatibility with.
Although 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.

XML parsing

We use xml.minidom for parsing XMP information. Given recent Python versions built against recent Expat versions, the usual attacks
(exponential entity expansion and external entity expansion) should not be possible. We have corresponding tests in place to ensure
this for the platforms our tests run against.

For some details, see the official documentation and the
README for defusedxml.

Please note that automated scanners tend to still flag any direct imports of XML modules from the Python standard library as unsafe.
There have been discussions about this being outdated already, but they are still being flagged.


User Streaming Data (`docs/user/streaming-data.md`)

Streaming Data with pypdf

In some cases, you might want to avoid saving things explicitly as a file
to disk, e.g. when you want to store the PDF in a database or AWS S3.

pypdf supports streaming data to a file-like object:

{testsetup}
pypdf_test_setup("user/streaming-data", {
    "example.pdf": "../resources/example.pdf",
})
{testcode}
from io import BytesIO
from pypdf import PdfReader, PdfWriter

# Prepare example
with open("example.pdf", "rb") as fh:
    bytes_stream = BytesIO(fh.read())

# Read from bytes_stream
reader = PdfReader(bytes_stream)

# Write to bytes_stream
writer = PdfWriter()
with BytesIO() as bytes_stream:
    writer.write(bytes_stream)

Writing a PDF directly to AWS S3

Suppose you want to manipulate a PDF and write it directly to AWS S3 without having
to write the document to a file first. We have the original PDF in raw_bytes_data as bytes
and want to set my-secret-password:

% We prefer not to execute doc examples which require access to cloud providers

{testcode}
:skipif: True

from io import BytesIO

import boto3
from pypdf import PdfReader, PdfWriter


reader = PdfReader(BytesIO(raw_bytes_data))
writer = PdfWriter()

# Add all pages to the writer
for page in reader.pages:
    writer.add_page(page)

# Add a password to the new PDF
writer.encrypt("my-secret-password")

# Save the new PDF to a file
with BytesIO() as bytes_stream:
    writer.write(bytes_stream)
    bytes_stream.seek(0)
    s3 = boto3.client("s3")
    s3.write_get_object_response(
        Body=bytes_stream, RequestRoute=request_route, RequestToken=request_token
    )

Reading PDFs directly from cloud services

One option is to first download the file and then pass the local file path to PdfReader.
Another option is to get a byte stream.

For AWS S3 it works like this:

% We prefer not to execute doc examples which require access to cloud providers

{testcode}
:skipif: True

from io import BytesIO

import boto3
from pypdf import PdfReader


s3 = boto3.client("s3")
obj = s3.get_object(Body=csv_buffer.getvalue(), Bucket="my-bucket", Key="my/doc.pdf")
reader = PdfReader(BytesIO(obj["Body"].read()))

To use with Google Cloud storage:

% We prefer not to execute doc examples which require access to cloud providers

{testcode}
:skipif: True

from io import BytesIO

from google.cloud import storage

# os.environ["GOOGLE_APPLICATION_CREDENTIALS"] must be set
storage_client = storage.Client()
blob = storage_client.bucket("my-bucket").blob("mydoc.pdf")
file_stream = BytesIO()
blob.download_to_file(file_stream)
reader = PdfReader(file_stream)

User Suppress Warnings (`docs/user/suppress-warnings.md`)

Exceptions, Warnings, and Log messages

pypdf makes use of three mechanisms to show if something went wrong:

  • Exceptions are error cases that pypdf users should explicitly handle.
    In the strict=True mode, most log messages with the warning level will
    become exceptions. This can be useful in applications where you can require
    a user to fix the broken PDF.
  • Warnings are avoidable issues, such as using deprecated classes /
    functions / parameters. Another example is missing capabilities of pypdf.
    In those cases, pypdf users should adjust their code. Warnings
    are issued by the warnings module - those are different from the log-level
    "warning."
  • Log messages are informative messages that can be used for post-mortem
    analysis. Most of the time, users can ignore them. They come in different
    levels, such as info / warning / error indicating the severity.
    Examples are non-standard compliant PDF files which pypdf can deal with or
    a missing implementation that leads to a part of the text not being extracted.

Exceptions

Exceptions need to be caught if you want to handle them. For example, you could
want to read the text from a PDF as a part of a search function.

Most PDF files do not follow the specification. In this case, pypdf needs to
guess which kinds of mistakes were potentially done when the PDF file was created.
See the robustness page for the related issues.

As a user, you likely do not care about it. If it is readable in any way, you
want the text. You might use pdfminer.six as a fallback and do this:

% We prefer not to execute doc examples for third-party package "pdfminer.six" used in one code snippet only

{testcode}
:skipif: True

from pypdf import PdfReader
from pdfminer.high_level import extract_text as fallback_text_extraction

text = ""
try:
    reader = PdfReader("example.pdf")
    for page in reader.pages:
        text += page.extract_text()
except Exception as exc:
    text = fallback_text_extraction("example.pdf")

You could also capture pypdf.errors.PyPdfError
if you prefer something more specific.

Warnings

The warnings module allows
you to ignore warnings:

{testcode}
import warnings

warnings.filterwarnings("ignore")

In many cases, you actually want to start Python with the -W flag so that you
see all warnings. This is especially true for Continuous Integration (CI).

Log messages

Log messages can be noisy in some cases. pypdf hopefully has a reasonable
level of log messages, but you can reduce which types of messages you want to
see:

{testcode}
import logging

logger = logging.getLogger("pypdf")
logger.setLevel(logging.ERROR)

The logging module
defines six log levels:

  • CRITICAL
  • ERROR
  • WARNING
  • INFO
  • DEBUG
  • NOTSET

User Viewer Preferences (`docs/user/viewer-preferences.md`)

Adding Viewer Preferences

It is possible to set viewer preferences of a PDF file.
§12.2 of the PDF 1.7 specification.

Note that the /ViewerPreferences dictionary does not exist by default.
If it is not already present, it must be created by calling the
{func}~pypdf.PdfWriter.create_viewer_preferences method.

If viewer preferences exist in a PDF file being read with {class}~pypdf.PdfReader,
you can access them as properties of {attr}~pypdf.PdfReader.viewer_preferences.
Otherwise, the {attr}~pypdf.PdfReader.viewer_preferences property will be set to None.

Example

{testsetup}
pypdf_test_setup("user/viewer-preferences")
text
/* Detailed source-code truncated for AI context efficiency. */

The names beginning with a slash character are part of the PDF file format. They are
included here to ease searching the pypdf documentation
for these names from the PDF specification.


--- METRICS ---