# Technical Documentation: py-pdf/pypdf

> ℹ️ **Provenance:** Multi-Tier Ingestion · [CodeWiki Reference](https://codewiki.google/github.com/py-pdf/pypdf)

### Overview (`README.md`)

[](https://badge.fury.io/py/pypdf)
[](https://pypi.org/project/pypdf/)
[](https://pypdf.readthedocs.io/en/stable/)
[](https://github.com/py-pdf/pypdf)
[](https://codecov.io/gh/py-pdf/pypdf)

# pypdf

pypdf is a free and open-source pure-python PDF library capable of splitting,
[merging](https://pypdf.readthedocs.io/en/stable/user/merging-pdfs.html),
[cropping, and transforming](https://pypdf.readthedocs.io/en/stable/user/cropping-and-transforming.html)
the pages of PDF files. It can also add
custom data, viewing options, and
[passwords](https://pypdf.readthedocs.io/en/stable/user/encryption-decryption.html)
to PDF files. pypdf can
[retrieve text](https://pypdf.readthedocs.io/en/stable/user/extract-text.html)
and
[metadata](https://pypdf.readthedocs.io/en/stable/user/metadata.html)
from PDFs as well.

See [pdfly](https://github.com/py-pdf/pdfly) for a CLI application that uses pypdf to interact with PDFs.

## Installation

Install pypdf using pip:

```
pip install pypdf
```

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

```
pip install pypdf[crypto]
```

> **NOTE**: `pypdf` 3.1.0 and above include significant improvements compared to
> previous versions. Please refer to [the migration
> guide](https://pypdf.readthedocs.io/en/latest/user/migration-1-to-2.html) 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](https://pypdf.readthedocs.io/en/stable/) for additional usage
examples!

For questions and answers, visit
[StackOverflow](https://stackoverflow.com/questions/tagged/pypdf)
(tagged with [pypdf](https://stackoverflow.com/questions/tagged/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](https://github.com/py-pdf/pypdf/blob/main/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](https://stackoverflow.com/questions/tagged/pypdf),
helping in [discussions](https://github.com/py-pdf/pypdf/discussions),
and asking users who report issues for [MCVE](https://stackoverflow.com/help/minimal-reproducible-example)'s (Code + example PDF!).


### Issues

A good bug ticket includes a MCVE - a minimal complete verifiable example.
For 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

```
pip install -r requirements/dev.txt
```

## Running Tests

See [testing pypdf with pytest](testing.md).

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

```
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](https://ohmygit.org/) 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](https://pypi.org/project/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](https://github.com/py-pdf/pypdf/blob/main/make_release.py).
For this reason, the format should be:

```
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](https://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`](https://unicode-table.com/en/FB00/) - the ligature ﬀ (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](https://www.compart.com/de/unicode/U+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](https://semver.org/). 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`](https://pypi.org/project/pip-tools/).

If you are using [Poetry](https://pypi.org/project/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](https://www.sphinx-doc.org/) and
hosted by [Read the Docs](https://about.readthedocs.com/)

## Testing code snippets

Almost all python code snippets in documentation tested using Sphinx's extension
[sphinx.ext.doctest](https://www.sphinx-doc.org/en/master/usage/extensions/doctest.html).
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](https://www.sphinx-doc.org/en/master/usage/quickstart.html#running-the-build)
   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:

```
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](intro.md#commit-messages) 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.

* [PDF Specification Archive](https://pdfa.org/resource/pdf-specification-archive/)
* [Portable Document Format Reference Manual, 1993. ISBN 0-201-62628-4](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.0.pdf)
* [ISO 32000-1:2008 (PDF 1.7)](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf)
* ISO 32000-2:2020 (PDF 2.0)

```{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 <pypdf.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 <pypdf.PdfReader.resolved_objects>`,
   populated by {py:meth}`cache_indirect_object <pypdf.PdfReader.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 <pypdf.PdfReader.get_object>`
   which uses the `PdfReader._get_object_from_stream` method.

## References

[PDF 1.7 specification](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf):
* 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 <pypdf.PdfWriter>` to write PDF files. pypdf has
{py:class}`PdfObject <pypdf.generic.PdfObject>` and several subclasses with the
{py:meth}`write_to_stream <pypdf.generic.PdfObject.write_to_stream>` method.
The {py:meth}`PdfWriter.write <pypdf.PdfWriter.write>` method uses the
`write_to_stream` methods of the referenced objects.

The {py:meth}`PdfWriter.write_stream <pypdf.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](https://pypi.org/project/fpdf2/) has a [`PDFObject` class](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/syntax.py)
with a serialize method which roughly maps to `pypdf.PdfObject.write_to_stream`.
Some other similarities include:

* [fpdf.output.OutputProducer.buffersize](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/output.py#L370-L485) vs. {py:meth}`pypdf.PdfWriter.write_stream <pypdf.PdfWriter.write_stream>`
* [fpdpf.syntax.Name](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/syntax.py#L124) vs. {py:class}`pypdf.generic.NameObject <pypdf.generic.NameObject>`
* [fpdf.syntax.build_obj_dict](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/syntax.py#L222) vs. {py:class}`pypdf.generic.DictionaryObject <pypdf.generic.DictionaryObject>`
* [fpdf.structure_tree.NumberTree](https://github.com/PyFPDF/fpdf2/blob/master/fpdf/structure_tree.py#L17) vs. {py:class}`pypdf.generic.TreeObject <pypdf.generic.TreeObject>`


### pdfrw

[pdfrw](https://pypi.org/project/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:

* [PdfArray](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfarray.py#L13)
* [PdfDict](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfdict.py#L49)
* [PdfName](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfname.py#L65)
* [PdfString](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfstring.py#L322)
* [PdfIndirect](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/objects/pdfindirect.py#L10)

The core classes of pdfrw are
[PdfReader](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/pdfreader.py#L26)
and
[PdfWriter](https://github.com/pmaupin/pdfrw/blob/master/pdfrw/pdfwriter.py#L224)

---

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

# Releasing

A `pypdf` release contains the following artifacts:

* A new [release on PyPI](https://pypi.org/project/pypdf/)
* A [release commit](https://github.com/py-pdf/pypdf/commit/91391b18bb8ec9e6e561e2795d988e8634a01a50)
    * Containing a changelog update
    * A new [git tag](https://github.com/py-pdf/pypdf/tags)
        * A [GitHub release](https://github.com/py-pdf/pypdf/releases/tag/3.15.0)

## 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`](https://docs.pytest.org/en/7.1.x/) 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](https://github.com/py-pdf/sample-files) 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`](https://pypi.org/project/pytest-testdox/)
can generate really nice output when the tests are running. This looks similar
to the output of [mocha.js](https://mochajs.org/).

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:
```
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](https://github.com/py-pdf/PyPDF2/compare/1.28.3...1.28.4)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.28.2...1.28.3)

## Version 1.28.2, 2022-05-23

### Bug Fixes (BUG)
-  PendingDeprecationWarning for getContents (#893)
-  PendingDeprecationWarning on using PdfMerger (#891)

[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.28.1...1.28.2)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.28.0...1.28.1)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.27.12...1.27.13)

### 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](https://github.com/py-pdf/PyPDF2/compare/1.27.11...1.27.12)

## Version 1.27.11, 2022-05-02

### Bug Fixes (BUG)
-  Incorrectly issued xref warning/exception (#855)

[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.10...1.27.11)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.27.9...1.27.10)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.27.8...1.27.9)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.27.7...1.27.8)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.27.5...1.27.6)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.27.4...1.27.5)


## Version 1.27.4, 2022-04-12

### Bug fixes (BUG)

- Guard formatting of `__init__.__doc__` string (#738)

### Packaging (PKG)

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

### Testing (TST)

- Add test for issue #297

### Miscellaneous

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

[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.3...1.27.4)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.27.2...1.27.3)

## 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](https://github.com/py-pdf/PyPDF2/discussions/718).

[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.1...1.27.2)

## 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](https://pypdf2.readthedocs.io/en/latest/)

[Full Changelog](https://github.com/py-pdf/PyPDF2/compare/1.27.0...1.27.1)

## 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](https://github.com/py-pdf/PyPDF2/compare/1.26.0...1.27.0)

## 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](history.md).

  [free]: https://en.wikipedia.org/wiki/Free_software
  [PyMuPDF]: https://pypi.org/project/PyMuPDF/
  [MuPDF]: https://mupdf.com/
  [PikePDF]: https://pypi.org/project/pikepdf/
  [QPDF]: https://github.com/qpdf/qpdf


## pdfminer.six and pdfplumber

[`pdfminer.six`](https://pypi.org/project/pdfminer.six/) is capable of
extracting the [font size](https://stackoverflow.com/a/69962459/562769)
/ font weight (bold-ness). It has no capabilities for writing PDF files.

[`pdfplumber`](https://pypi.org/project/pdfplumber/) is a library focused on extracting data from PDF documents. Since `pdfplumber` is built on top of `pdfminer.six`, there are **no capabilities of exporting or modifying a PDF file** (see [#440 (discussions)](https://github.com/jsvine/pdfplumber/discussions/440#discussioncomment-803880)). However, `pdfplumber` is capable of converting a PDF file into an image, [draw lines and rectangles on the image](https://github.com/jsvine/pdfplumber#drawing-methods), and save it as an image file. Please note that the image conversion is done via ImageMagick (see [`pdfplumber`'s documentation](https://github.com/jsvine/pdfplumber#visual-debugging)).

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`](https://pypi.org/project/pdfrw/)!

Please be aware that there is also
[`pdfminer`](https://pypi.org/project/pdfminer/) which is not maintained.
Then there is [`pdfrw2`](https://pypi.org/project/pdfrw2/) which doesn't have
a large community behind it.

## Document Generation

There are (Python) [tools to generate PDF documents](https://github.com/py-pdf/awesome-pdf#generators).
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`](https://pdfly.readthedocs.io/en/latest/)
a shot.

---

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

# Frequently Asked Questions

## How is pypdf related to PyPDF2?

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.

  [Matthew]: https://github.com/mstamy2
  [source]: https://github.com/py-pdf/PyPDF2/commit/24b270d876518d15773224b5d0d6c2206db29f64#commitcomment-5038317
  [this sort of thing]: https://github.com/py-pdf/PyPDF2/issues/24
  [GitHub issue]: https://github.com/py-pdf/PyPDF2/issues

## Who uses pypdf?

pyPdf is vendored [into](https://github.com/Buyanbat/XacCRM/tree/ee78e8df967182f661b6494a86444501e7d89c8f/report/pyPdf) [several](https://github.com/MyBook/calibre/tree/ca1efe3c21f6553e096dab745b3cdeb36244a5a9/src/pyPdf) [projects](https://github.com/Giacomo-De-Florio-Dev/Make_Your_PDF_Safe/tree/ec439f92243d12d54ae024668792470c6b40ee96/MakeYourPDFsafe_V1.3/PyPDF2). That
means the code of pyPdf was copied into that project.

Projects that depend on pypdf:

* [Camelot](https://github.com/camelot-dev/camelot): A Python library to extract tabular data from PDFs
* [edi](https://github.com/OCA/edi): Electronic Data Interchange modules
* [amazon-textract-textractor](https://github.com/aws-samples/amazon-textract-textractor/blob/42444b08c672607eadbdcd64f3c5adb2d85383de/helper/setup.py): Analyze documents with Amazon Textract and generate output in multiple formats.
* [maigret](https://github.com/soxoj/maigret): Collect a dossier on a person by username from thousands of sites
* [deda](https://github.com/dfd-tud/deda): tracking Dots Extraction, Decoding and Anonymisation toolkit
* [opencanary](https://github.com/thinkst/opencanary)
* Document Conversions
  * [rst2pdf](https://github.com/rst2pdf/rst2pdf)
  * [xhtml2pdf](https://github.com/xhtml2pdf/xhtml2pdf)
  * [doc2text](https://github.com/jlsutherland/doc2text)
* [pdfalyzer](https://pypi.org/project/pdfalyzer/): A PDF analysis tool for visualizing the inner tree-like data structure of a PDF in spectacularly large and colorful diagrams as well as scanning the binary streams embedded in the PDF for hidden potentially malicious content.

## How do I cite pypdf?

In BibTeX format:

```
@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](https://en.wikipedia.org/wiki/BSD_licenses#3-clause), 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](https://pypi.org/project/pyPdf/#history)
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](https://github.com/pubpub-zz) was extremely active, especially
for text extraction.

[Matthew Peveler](https://github.com/MasterOdin) helped a lot with reviews
and general project decisions.

[exiledkingcc](https://github.com/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:

* AES reading and writing support. Not only with PyCryptoDome, but also with cryptography.
* Text extraction improvements, e.g., for math content. [pypdf is now comparable with Tika, pypdfium2, and PyMuPDF](https://github.com/py-pdf/benchmarks)
* Annotation support
* Performance Improvements and Bugfixes
* Page Label support

stefan6419846 made his [first PR for pypdf](https://github.com/py-pdf/pypdf/pull/2022)
in July 2023 and joined the project.


  [Mathieu Fenniak]: https://mathieu.fenniak.net/
  [pyfpdf]: https://github.com/reingart/pyfpdf
  [ReportLab]: https://www.reportlab.com/software/opensource/rl-toolkit/
  [pdftk]: https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/https://www.pdflabs.com/tools/pdftk-the-pdf-toolkit/
  [/r/python crowd chatted]: https://www.reddit.com/r/Python/comments/qsvfm/pypdf2_updates_pypdf_pypdf2_is_an_opensource/

---

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

# Migration Guide: 1.x to 2.x

`PyPDF2<2.0.0` ([docs](https://pypdf2.readthedocs.io/en/1.27.12/meta/history.html))
is very different from `PyPDF2>=2.0.0` ([docs](../meta/history.md)).

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](https://github.com/py-pdf/PyPDF2/pull/986#issuecomment-1230698069)

---

### 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](https://github.com/py-pdf/pypdf),
  and [the documentation on ReadTheDocs](https://pypdf.readthedocs.io/en/latest/),
  [the package on PyPI](https://pypi.org/project/pypdf/), and
  [the website on GitHub](https://py-pdf.github.io/pypdf/dev/bench/).
* 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](https://translate.google.com/) 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](https://github.com/py-pdf/pypdf/discussions/798)
  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](https://github.com/py-pdf/pypdf/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](https://semver.org/). 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](https://github.com/py-pdf/pypdf/issues)!


## People

* [stefan6419846](https://github.com/stefan6419846) is the benevolent dictator since January 2025
* [Martin Thoma](https://github.com/MartinThoma) 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](https://github.com/py-pdf/pypdf/labels/is-feature).

The [moonshot extensions](https://github.com/py-pdf/pypdf/discussions/1181) 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](https://github.com/tesseract-ocr/tesseract).
2. **Format Conversion**: Converting docx / HTML to PDF or PDF to those formats.
   You might want to have a look at [`pdfkit`](https://pypi.org/project/pdfkit/)
   and similar projects.

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

* **Digital Signature Support** ([reference ticket](https://github.com/py-pdf/pypdf/issues/302)): 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](https://pypi.org/project/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`](https://pypi.org/project/reportlab/) /
  [`fpdf2`](https://pypi.org/project/fpdf2/) or document conversion tools like
  [`pdfkit`](https://pypi.org/project/pdfkit/).
* **Replacing words within a PDF**: [Extracting text from PDF is hard](../user/extract-text.md#why-text-extraction-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`](https://pypi.org/project/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:

* PyPI: [pypdf](https://pypi.org/project/pypdf/) and [PyPDF2](https://pypi.org/project/PyPDF2/)
* GitHub: [pypdf](https://github.com/py-pdf/pypdf) (the repository, not the organization)
* ReadTheDocs: [pypdf](https://readthedocs.org/projects/pypdf/) and [PyPDF2](https://readthedocs.org/projects/pypdf2/)

## 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](https://github.com/Lucas-C): He maintains fpdf2 and is a py-pdf owner
* [pubpub-zz](https://github.com/pubpub-zz): He is one of the most active contributors
  to pypdf
* [Matthew Peveler](https://github.com/MasterOdin): Less active, but he is cautious
  about breaking changes and an experienced software developer.
* [exiledkingcc](https://github.com/exiledkingcc): He has contributed the core
  changes related to encryption.

## How to take ownership?

* PyPI: Follow [PEP 541 – Package Index Name Retention](https://peps.python.org/pep-0541/)
* GitHub: Talk with one of the other py-pdf organization owners
* ReadTheDocs: Follow the [Abandoned projects policy](https://docs.readthedocs.io/en/latest/abandoned-projects.html)

---

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

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

.. testcode ::

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

    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](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](https://pypi.org/project/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")
```

## Popup

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.

## Link

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`](https://en.wikipedia.org/wiki/RC4) and
[`AES`](https://en.wikipedia.org/wiki/Advanced_Encryption_Standard) 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`](https://cryptography.io/en/latest/). Alternatively,
you can use [`pycryptodome`](https://pypi.org/project/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.

### Example 1: Ignore header and footer

The following example reads the text of page four of [this PDF document](https://github.com/py-pdf/pypdf/blob/main/resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf), but ignores the header (y > 720) and footer (y < 50). In this file we also need to include new line characters (y == 0).

```{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)
```

```
/* 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](https://github.com/py-pdf/pypdf/blob/main/resources/GeoBase_NHNC1_Data_Model_UML_EN.pdf) into
an [SVG file](https://en.wikipedia.org/wiki/Scalable_Vector_Graphics).

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](https://www.compart.com/de/unicode/U+FB00)
   is a single symbol ﬀ for two lowercase letters 'f'. Should that be parsed as
   the Unicode symbol 'ﬀ' 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:

* [issue #1507](https://github.com/py-pdf/pypdf/issues/1507)
* [Negative numbers in PDF content stream text object](https://stackoverflow.com/a/28203655/562769)
* Mark Stephens: [Understanding PDF text objects](https://blog.idrsolutions.com/understanding-pdf-text-objects/), 2010.

## 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](https://github.com/tesseract-ocr/tesseract) 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](https://stackoverflow.com/a/43466923/562769)

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 <pypdf._page.PageObject.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`](https://docs.python.org/3/library/zlib.html#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](https://github.com/py-pdf/pypdf/pull/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](https://youtube.com/watch?v=tgOABUhVwFs) has other suggestions. One takeaway is that most of the significant size optimizations usually come from image and font modification. However, font optimization, such as replacing, merging, and subsetting, is not within the functionality of pypdf at the moment.

---

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

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

```
pip install pypdf[crypto]
```

If you plan to use image extraction, you need Pillow:

```
pip install pypdf[image]
```

For JBIG2 support, you need to install a global OS-level package as well:
[`jbig2dec`](https://github.com/ArtifexSoftware/jbig2dec) The installation procedure
depends on our operating system. For Ubuntu, use the following, for example:

```
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](https://devguide.python.org/versions/). 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](https://anaconda.org/conda-forge/pypdf).


## 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](https://stackoverflow.com/questions/3444645/merge-pdf-files)
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](https://github.com/py-pdf/pypdf/issues/3304).

See [History of PDF](https://en.wikipedia.org/wiki/History_of_PDF) for more
features.

Some PDF features are not supported by pypdf, but other libraries can be used
for them:

* [pyHanko](https://pyhanko.readthedocs.io/en/latest/index.html): Cryptographically sign a PDF ([#302](https://github.com/py-pdf/pypdf/issues/302))
* [camelot-py](https://pypi.org/project/camelot-py/): Table Extraction ([#231](https://github.com/py-pdf/pypdf/issues/231))

---

### 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](https://docs.verapdf.org/install/) is the go-to PDF/A validator.

There are several online validators that allow you to simply upload the document:

* [pdfen.com](https://www.pdfen.com/pdf-a-validator)
* [avepdf.com](https://avepdf.com/pdfa-validation) : Gives an error report
* [pdfa.org](https://pdfa.org/pdfa-online-verification-service/)
* [visual-paradigm.com](https://online.visual-paradigm.com/de/online-pdf-editor/pdfa-validator/) - can convert the PDF to a PDF/A
* [pdf2go.com](https://www.pdf2go.com/validate-pdfa)
* [slub-dresden.de](https://www.slub-dresden.de/veroeffentlichen/dissertationen-habilitationen/elektronische-veroeffentlichung/slub-pdfa-validator) links to relevant parts in the specification.

## pypdf and PDF/A

At the moment, pypdf does not make any guarantees regarding PDF/A.
[Support is very welcome](https://github.com/py-pdf/pypdf/labels/is-pdf%2Fa-compliance).

---

### 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",
        "ﬁ": "fi",
        "ﬂ": "fl",
        "ﬃ": "ffi",
        "ﬄ": "ffl",
        "ﬅ": "ft",
        "ﬆ": "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
```

## Header/Footer Removal

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](https://tex.stackexchange.com/questions/20962/should-i-put-a-space-between-a-number-and-its-unit)).
  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](https://pdfa.org/resource/pdf-specification-archive/).
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](https://github.com/py-pdf/pypdf/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](https://docs.python.org/3/library/xml.html#xml-security) and the
[README for defusedxml](https://github.com/tiran/defusedxml/blob/main/README.md#python-xml-libraries).

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](robustness.md) 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`](https://github.com/py-pdf/pypdf/blob/main/pypdf/errors.py)
if you prefer something more specific.

## Warnings

The [`warnings` module](https://docs.python.org/3/library/warnings.html) 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](https://docs.python.org/3/library/logging.html#logging-levels)
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](https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf).

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")
```

```
/* 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 ---
- Files Extracted: 61
- Estimated Token Budget: ~51642 tokens
- Canonical Reference: https://codewiki.google/github.com/py-pdf/pypdf
