Technical Documentation: py-pdf/pypdf
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
pypdf
pypdf is a free and open-source pure-python PDF library capable of splitting,
merging,
cropping, and transforming
the pages of PDF files. It can also add
custom data, viewing options, and
passwords
to PDF files. pypdf can
retrieve text
and
metadata
from PDFs as well.
See pdfly for a CLI application that uses pypdf to interact with PDFs.
Installation
Install pypdf using pip:
pip install pypdfFor using pypdf with AES encryption or decryption, install extra dependencies:
pip install pypdf[crypto]NOTE:
pypdf3.1.0 and above include significant improvements compared to
previous versions. Please refer to the migration
guide for
more information.
Usage
from pypdf import PdfReader
reader = PdfReader("example.pdf")
number_of_pages = len(reader.pages)
page = reader.pages[0]
text = page.extract_text()pypdf can do a lot more, e.g. splitting, merging, reading and creating annotations, decrypting and encrypting. Check out the
documentation for additional usage
examples!
For questions and answers, visit
StackOverflow
(tagged with pypdf).
Contributions
Maintaining pypdf is a collaborative effort. You can support the project by
writing documentation, helping to narrow down issues, and submitting code.
See the CONTRIBUTING.md file for more information.
Q&A
The experience pypdf users have covers the whole range from beginner to expert. You can contribute to the pypdf community by answering questions
on StackOverflow,
helping in discussions,
and asking users who report issues for MCVE's (Code + example PDF!).
Issues
A good bug ticket includes a MCVE - a minimal complete verifiable example.
For pypdf, this means that you must upload a PDF that causes the bug to occur
as well as the code you're executing with all of the output. Useprint(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:
$ 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.
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.txtRunning Tests
See testing pypdf with pytest.
The sample-files git submodule
The reason for having the submodule sample-files is that we want to keep
the size of the pypdf repository small while we also want to have an extensive
test suite. Those two goals contradict each other.
The resources folder should contain a select set of core examples that cover
most cases we typically want to test for. The sample-files might cover a lot
more edge cases, the behavior we get when file sizes get bigger, different
PDF producers.
To get the sample-files folder, you need to execute:
git submodule update --initTools: git and pre-commit
Git is a command line application for version control. If you don't know it,
you can play ohmygit to learn it.
GitHub is the service where the pypdf project is hosted. While git is free and
open source, GitHub is a paid service by Microsoft, but free in a lot of
cases.
pre-commit is a command line application
that uses git hooks to automatically execute code. This allows you to avoid
style issues and other code quality issues. After you entered pre-commit install
once in your local copy of pypdf, it will automatically be executed when
you git commit.
Commit Messages
Having a clean commit message helps people to quickly understand what the commit
is about, without actually looking at the changes. The first line of the
commit message is used to auto-generate the CHANGELOG.
For this reason, the format should be:
PREFIX: DESCRIPTION
BODYThe 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
theBODY:Closes #123where 123 is the issue number on GitHub.
It would be absolutely amazing if you could write a regression test in those
cases. That is a test that would fail without the fix.
A bug is always an issue for pypdf users - test code or CI that was fixed is
not considered a bug here.ENH: A new feature! Describe in the body what it can be used for.DEP: Deprecation. Either marking something as "this is going to be removed"
or actually removing it.PI: A performance improvement. This could also be a reduction in the
file size of PDF files generated by pypdf.ROB: A robustness change. Dealing better with broken PDF files.DOC: A documentation change.TST: Adding or adjusting tests.DEV: Developer experience improvements, e.g., pre-commit or setting up CI.MAINT: Quite a lot of different stuff. Performance improvements are, for sure,
the most interesting changes in here. Refactorings as well.STY: A style change. Something that makes pypdf code more consistent.
Typically, a small change. It could also be better error messages for
end users.
The prefix is used to generate the CHANGELOG. Every PR must have exactly one -
if you feel like several match, take the top one from this list that matches for
your PR.
Pull Request Size
Smaller Pull Requests (PRs) are preferred as it's typically easier to merge
them. For example, if you have some typos, a few code-style changes, a new
feature, and a bug-fix, that could be three or four PRs.
A PR must be complete. That means if you introduce a new feature, it must be
finished within the PR and have a test for that feature.
Benchmarks
We need to keep an eye on performance, and thus we have a few benchmarks.
See py-pdf.github.io/pypdf/dev/bench
Index (`docs/index.rst`)
.. pypdf documentation main file, created by
sphinx-quickstart on Thu Apr 7 20:13:19 2022.
You can adapt this file completely to your liking, but it should at least
contain the root toctree directive.
Welcome to pypdf
pypdf is a free <https://en.wikipedia.org/wiki/Free_software>_ and open
source pure-python PDF library capable of splitting,
merging, cropping, and transforming the pages of PDF files. It can also add
custom data, viewing options, and passwords to PDF files.
pypdf can retrieve text and metadata from PDFs as well.
See pdfly <https://github.com/py-pdf/pdfly>_ for a CLI application that uses pypdf to interact with PDFs.
You can contribute to pypdf on GitHub <https://github.com/py-pdf/pypdf>_.
.. toctree::
:caption: User Guide
:maxdepth: 1
user/installation
user/robustness
user/security
user/suppress-warnings
user/metadata
user/extract-text
user/post-processing-in-text-extraction
user/extract-images
user/handle-attachments
user/encryption-decryption
user/merging-pdfs
user/cropping-and-transforming
user/reading-pdf-annotations
user/adding-pdf-annotations
user/add-watermark
user/add-javascript
user/viewer-preferences
user/forms
user/handling-outlines
user/streaming-data
user/file-size
user/pdf-version-support
user/pdfa-compliance
.. toctree::
:caption: API Reference
:maxdepth: 1
modules/PdfReader
modules/PdfWriter
modules/Destination
modules/DocumentInformation
modules/Field
modules/Fit
modules/PageObject
modules/PageRange
modules/PaperSize
modules/RectangleObject
modules/Transformation
modules/XmpInformation
modules/actions
modules/annotations
modules/constants
modules/configuration
modules/errors
modules/generic
modules/PdfDocCommon
.. toctree::
:caption: Developer Guide
:maxdepth: 1
dev/intro
dev/pdf-format
dev/pypdf-parsing
dev/pypdf-writing
dev/cmaps
dev/deprecations
dev/documentation
dev/testing
dev/releasing
.. toctree::
:caption: About pypdf
:maxdepth: 1
meta/CHANGELOG
meta/changelog-v1
meta/migration-1-to-2
meta/project-governance
meta/taking-ownership
meta/history
meta/CONTRIBUTORS
meta/scope-of-pypdf
meta/comparisons
meta/faq
Indices and tables
- :ref:
genindex - :ref:
modindex - :ref:
search
Dev Cmaps (`docs/dev/cmaps.md`)
CMaps
Looking at the cmap of "crazyones":
pdftk crazyones.pdf output crazyones-uncomp.pdf uncompressYou can see this:
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 popcodespacerange
A codespacerange maps a complete sequence of bytes to a range of Unicode glyphs.
It defines a starting point:
1 beginbfchar
<1B> <FB00>That means that 1B (Hex for 27) maps to the Unicode character FB00 - the ligature ff (two lowercase f's).
The two numbers in begincodespacerange mean that it starts with an offset of
0 (hence from 1B ➜ FB00) up to an offset of FF (dec: 255), hence 1B+FF = 282
➜ FBFF.
Within the text stream, there is
(The)-342(mis\034ts.)\034 is octal for the decimal value 28.
Dev Deprecations (`docs/dev/deprecations.md`)
The Deprecation Process
pypdf strives to be an excellent library for its current users and for new
ones. We are careful with introducing potentially breaking changes, but we
will do them if they provide value for the community in the long run.
We hope and think that deprecations will not happen frequently. If they do,
users can rely on the following procedure.
Semantic Versioning
pypdf uses semantic versioning. If you want to avoid
breaking changes, please use dependency pinning (also known as version pinning).
In Python, this is done by specifying the exact version you want to use in arequirements.txt file. A tool that can support you is pip-compile frompip-tools.
If you are using Poetry it is done with thepoetry.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:
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.(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.(x+2).0.0: The DeprecationErrors are removed.
This means the users have three warnings in the CHANGELOG, a DeprecationWarning
until the next major release and a DeprecationError until the major release
after that.
Please note that adding warnings can be a breaking change for some users; most
likely just in the CI.
This means it needs to be properly documented.
Dev Documentation (`docs/dev/documentation.md`)
Documentation
This documentation is build with Sphinx and
hosted by Read the Docs
Testing code snippets
Almost all python code snippets in documentation tested using Sphinx's extension
sphinx.ext.doctest.
This allows to make sure that we have no typos, missed imports and other problems in:
- code snippets marked with
testcodedirective in*.mdfiles - code snippets from python's docstrings imported via
autoclassdirective in*.rstfiles
CI pipeline is configured run Sphinx's doctest build automatically for each PR.
It is also possible to run it locally:
First you need to install docs requirements
bashpip install -r requirements/docs.txtChange current directory
bashcd docsRun
doctestbuild. It uses indirectlysphinx-buildcommand line tool
installed with docs requirements. See
Sphinx's docs
for details.bashmake doctestIf everything is okay you should see in output
Doctest summarywithout 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 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
- Portable Document Format Reference Manual, 1993. ISBN 0-201-62628-4
- ISO 32000-1:2008 (PDF 1.7)
- ISO 32000-2:2020 (PDF 2.0)
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:
- Header: Contains the version of the PDF, e.g.
%PDF-1.7 - Body: Contains a sequence of indirect objects
- Cross-reference table (xref): Contains a list of the indirect objects in the body
- 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:
xref 42 5
0000001000 65535 f
0000001234 00000 n
0000001987 00000 n
0000011987 00000 n
0000031987 00000 nLet's go through it step-by-step:
xrefis just a keyword that specifies the start of the xref table.42is the numerical ID of the first object in this xref section;5is 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 eithernorf.nnnnnnnnnnis the byte offset of the object. It tells the reader where
the object is in the file.gggggis the generation number. It tells the reader how old the object is.nmeans that the object is a normal in-use object,fmeans 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 first free object always has a generation number of 65535. It forms
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_objectis the object itself. It can be empty. Starts with/Keywordto
specify which kind of object it is.endobjmarks the end of the object.
A concrete example can be found in test_reader.py::test_get_images_raw:
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 >> endobjThe trailer
The trailer looks like this:
trailer << /Root 5 0 R
/Size 6
>>
startxref 1234
%%EOFLet's go through it:
trailer <<indicates that the trailer dictionary starts. It ends with>>.startxrefis a keyword followed by the byte-location of thexrefkeyword.
As the trailer is always at the bottom of the file, this allows readers to
quickly find the xref table.%%EOFis 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
5is the object number of the catalog dictionary. 0is the generation number of the catalog dictionary.Ris the keyword that indicates that the object is a reference to the
catalog dictionary.
- The
/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:
pdftk crazyones.pdf output crazyones-uncomp.pdf uncompressThen rename crazyones-uncomp.pdf to crazyones-uncomp.txt and open it in
your favorite IDE / text editor.
Dev Pypdf Parsing (`docs/dev/pypdf-parsing.md`)
How pypdf parses PDF files
pypdf uses {class}~pypdf.PdfReader to parse PDF files.
The method {py:meth}PdfReader.read shows the basic
structure of parsing:
- 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. - Parsing the objects: After locating the xref table and the trailer, pypdf
proceeds to parse the objects in the PDF. Objects in a PDF can be of various
types such as dictionaries, arrays, streams, and simple data types (e.g.,
integers, strings). pypdf parses these objects and stores them in
{py:meth}PdfReader.resolved_objects,
populated by {py:meth}cache_indirect_object. - Decoding content streams: The content of a PDF is typically stored in
content streams, which are sequences of PDF operators and operands. pypdf
decodes these content streams by applying filters (e.g.,FlateDecode,LZWDecode) specified in the stream's dictionary. This is only done when the
object is requested by {py:meth}PdfReader.get_object
which uses thePdfReader._get_object_from_streammethod.
References
- 7.5 File Structure
- 7.5.4 Cross-Reference Table
- 7.8 Content Streams and Resources
Dev Pypdf Writing (`docs/dev/pypdf-writing.md`)
How pypdf writes PDF files
pypdf uses {py:class}PdfWriter to write PDF files. pypdf has
{py:class}PdfObject and several subclasses with the
{py:meth}write_to_stream method.
The {py:meth}PdfWriter.write method uses thewrite_to_stream methods of the referenced objects.
The {py:meth}PdfWriter.write_stream method
has the following core steps:
_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.- 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. - 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. - Write the File Trailer with
_write_trailer: The trailer is written to
the output stream in this step. The trailer contains essential information,
such as the number of objects in the PDF, the location of the root object
(Catalog), and the Info object containing metadata. The trailer also
specifies the location of the xref table.
How others do it
Looking at alternative software designs and implementations can help to improve
our choices.
fpdf2
fpdf2 has a PDFObject class
with a serialize method which roughly maps to pypdf.PdfObject.write_to_stream.
Some other similarities include:
- fpdf.output.OutputProducer.buffersize vs. {py:meth}
pypdf.PdfWriter.write_stream - fpdpf.syntax.Name vs. {py:class}
pypdf.generic.NameObject - fpdf.syntax.build_obj_dict vs. {py:class}
pypdf.generic.DictionaryObject - fpdf.structure_tree.NumberTree vs. {py:class}
pypdf.generic.TreeObject
pdfrw
pdfrw, in contrast, seems to work more with
the standard Python objects (bool, float, string) and not wrap them in custom
objects, if possible. It still has:
The core classes of pdfrw are
PdfReader
and
PdfWriter
Dev Releasing (`docs/dev/releasing.md`)
Releasing
A pypdf release contains the following artifacts:
- A new release on PyPI
- A release commit
- Containing a changelog update
- A new git tag
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:
- Update the CHANGELOG.md and the _version.py via
python make_release.py.
This also prepares the release commit message. - Create a release commit:
git commit -eF RELEASE_COMMIT_MSG.md. - Push commit:
git push. - Create the tag:
git tag -s 6.7.1 -eF RELEASE_COMMIT_MSG.md. - Push the tag:
git push origin 6.7.1. - 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:
- Update the CHANGELOG.md and the _version.py via
python make_release.py.
This also prepares the release commit message. - Push the changes to a dedicated branch.
- Open a pull request starting with
REL:, followed by the new version number. - Wait for the approval of another eligible maintainer.
- Merge the pull request with the name being the PR title and the body being
the content ofRELEASE_COMMIT_MSG.md. - Create the tag:
git tag -s 6.7.1 -eF RELEASE_COMMIT_MSG.md. - Push the tag:
git push origin 6.7.1. - CI now builds a source and a wheels package which it pushes to PyPI. It also
creates the corresponding GitHub release.
The Release Tag
- Use the release version as the tag name. No need for a leading "v".
- Use the changelog entry as the body.
When are releases done?
There is no need to wait for anything. If the CI is green (all tests succeeded),
we can release.
At the moment, there is no fixed release cycle.
Dev Testing (`docs/dev/testing.md`)
Testing
pypdf uses pytest for testing.
To run the tests, you need to install the CI (Continuous Integration) requirements by running pip install -r requirements/ci.txt orpip 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 thesample-filesgit submodule to be initialized. As of October 2022, this is about 25 MB.enable_socket: Tests that download PDF documents. They are stored locally and thus only need to be downloaded once. As of October 2022, this is about 200 MB.- To successfully run the tests, please download most of the documents beforehand:
python -c "from tests import download_test_pdfs; download_test_pdfs()"
- To successfully run the tests, please download most of the documents beforehand:
You can disable them by pytest -m "not enable_socket" or pytest -m "not samples".
You can even disable all of them: pytest -m "not enable_socket" -m "not samples" -m "not slow".
Please note that this reduces test coverage. The CI will always test all files.
Docstrings in Unit tests
The first line of a docstring in a unit test should be written in a way that
you could prefix it with "This tests ensures that ...", e.g.
- Invalid XML in xmp_metadata is gracefully handled.
- The identity is returning its input.
- xmp_modify_date is extracted correctly.
This way, plugins like pytest-testdox
can generate really nice output when the tests are running. This looks similar
to the output of mocha.js.
If the test is a regression test, write
This test is a regression test for issue #1234
If the regression test is just one parameter of other tests, then add it as
a comment for that parameter.
Evaluate a PR in-progress version
You may want to test a version from a PR which has not been released yet.
The easiest way is to use pip and install a version from git:
a) Go the PR and identify the repository and branch.
Example from below : repository: pubpub-zz / branch: iss2200 :
b) you can then install the version using pip from git:
Example:
pip install git+https://github.com/pubpub-zz/pypdf.git@iss2200Meta 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)
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)
Version 1.28.2, 2022-05-23
Bug Fixes (BUG)
- PendingDeprecationWarning for getContents (#893)
- PendingDeprecationWarning on using PdfMerger (#891)
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)
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)
Deprecations (DEP)
- Make the
PyPDF2.utilsmodule 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
Version 1.27.11, 2022-05-02
Bug Fixes (BUG)
- Incorrectly issued xref warning/exception (#855)
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)
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)
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)
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
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)
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)
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)
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.
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
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 installto 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)
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 pagesAdded 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 imagesFixed 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 detailsConverted 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 pageFixed 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 togetherUsed 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 PdfFileReaderAllow 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 maintainedCommand 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 folderAdded a method for debugging purposes to show current location while
parsingAbility 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 pyPdfA FAQ to answer common questions
Version 1.18, 2013-08-19
Fixed a bug where older versions of objects were incorrectly added to the
cache, resulting in outdated or missing pages, images, and other objects
(from speedplane)Fixed a bug in parsing the xref table where new xref values were
overwritten; also cleaned up code (from speedplane)New method mergeRotatedAroundPointPage which merges a page while rotating
it around a point (from speedplane)Updated Destination syntax to respect PDF 1.6 specifications (from
jamma313)Prevented infinite loop when a PdfFileReader object was instantiated
with an empty file (from Jerome Nexedi)
Other Changes:
- Downloads now available via PyPI
- Installation through pip library is fixed
Version 1.17, 2013-07-25
Removed one (from pdf.py) of the two Destination classes. Both
classes had the same name, but were slightly different in content,
causing some errors. (from Janne Vanhala)Corrected and Expanded README file to demonstrate PdfFileMerger
Added filter for LZW encoded streams (from Michal Horejsek)
PyPDF2 issue tracker enabled on Github to allow community
discussion and collaboration
Versions -1.16, -2013-06-30
Note: This ChangeLog has not been kept up-to-date for a while.
Hopefully we can keep better track of it from now on. Some of the
changes listed here come from previous versions 1.14 and 1.15; they
were only vaguely defined. With the new _version.py file we should
have more structured and better documented versioning from now on.Defined
PyPDF2.__version__Fixed encrypt() method (from Martijn The)
Improved error handling on PDFs with truncated streams (from cecilkorik)
Python 3 support (from kushal-kumaran)
Fixed example code in README (from Jeremy Bethmont)
Fixed an bug caused by DecimalError Exception (from Adam Morris)
Many other bug fixes and features by:
jeansch
Anton Vlasenko
Joseph Walton
Jan Oliver Oelerich
Fabian Henze
And any others I missed.
Thanks for contributing!
Version 1.13, 2010-12-04
Fixed a typo in code for reading a "\b" escape character in strings.
Improved
__repr__in FloatObject.Fixed a bug in reading octal escape sequences in strings.
Added getWidth and getHeight methods to the RectangleObject class.
Fixed compatibility warnings with Python 2.4 and 2.5.
Added addBlankPage and insertBlankPage methods on PdfFileWriter class.
Fixed a bug with circular references in page's object trees (typically
annotations) that prevented correctly writing out a copy of those pages.New merge page functions allow application of a transformation matrix.
To all patch contributors: I did a poor job of keeping this ChangeLog
up-to-date for this release, so I am missing attributions here for any
changes you submitted. Sorry! I'll do better in the future.
Version 1.12, 2008-09-02
Added support for XMP metadata.
Fix reading files with xref streams with multiple /Index values.
Fix extracting content streams that use graphics operators longer than 2
characters. Affects merging PDF files.
Version 1.11, 2008-05-09
Patch from Hartmut Goebel to permit RectangleObjects to accept NumberObject
or FloatObject values.PDF compatibility fixes.
Fix to read object xref stream in correct order.
Fix for comments inside content streams.
Version 1.10, 2007-10-04
Text strings from PDF files are returned as Unicode string objects when
pyPdf determines that they can be decoded (as UTF-16 strings, or as
PDFDocEncoding strings). Unicode objects are also written out when
necessary. This means that string objects in pyPdf can be either
generic.ByteStringObject instances, or generic.TextStringObject instances.The extractText method now returns a unicode string object.
All document information properties now return unicode string objects. In
the event that a document provides docinfo properties that are not decoded by
pyPdf, the raw byte strings can be accessed with an "_raw" property (ie.
title_raw rather than title)generic.DictionaryObject instances have been enhanced to be easier to use.
Values coming out of dictionary objects will automatically be de-referenced
(.getObject will be called on them), unless accessed by the new "raw_get"
method. DictionaryObjects can now only contain PdfObject instances (as keys
and values), making it easier to debug where non-PdfObject values (which
cannot be written out) are entering dictionaries.Support for reading named destinations and outlines in PDF files. Original
patch by Ashish Kulkarni.Stream compatibility reading enhancements for malformed PDF files.
Cross reference table reading enhancements for malformed PDF files.
Encryption documentation.
Replace some "assert" statements with error raising.
Minor optimizations to FlateDecode algorithm increase speed when using PNG
predictors.
Version 1.9, 2006-12-15
Fix several serious bugs introduced in version 1.8, caused by a failure to
run through our PDF test suite before releasing that version.Fix bug in NullObject reading and writing.
Version 1.8, 2006-12-14
Add support for decryption with the standard PDF security handler. This
allows for decrypting PDF files given the proper user or owner password.Add support for encryption with the standard PDF security handler.
Add new pythondoc documentation.
Fix bug in ASCII85 decode that occurs when whitespace exists inside the
two terminating characters of the stream.
Version 1.7, 2006-12-10
Fix a bug when using a single page object in two PdfFileWriter objects.
Adjust PyPDF to be tolerant of whitespace characters that don't belong
during a stream object.Add documentInfo property to PdfFileReader.
Add numPages property to PdfFileReader.
Add pages property to PdfFileReader.
Add extractText function to PdfFileReader.
Version 1.6, 2006-06-06
Add basic support for comments in PDF files. This allows us to read some
ReportLab PDFs that could not be read before.Add "auto-repair" for finding xref table at slightly bad locations.
New StreamObject backend, cleaner and more powerful. Allows the use of
stream filters more easily, including compressed streams.Add a graphics state push/pop around page merges. Improves quality of
page merges when one page's content stream leaves the graphics
in an abnormal state.Add PageObject.compressContentStreams function, which filters all content
streams and compresses them. This will reduce the size of PDF pages,
especially after they could have been decompressed in a mergePage
operation.Support inline images in PDF content streams.
Add support for using .NET framework compression when zlib is not
available. This does not make pyPdf compatible with IronPython, but it
is a first step.Add support for reading the document information dictionary, and extracting
title, author, subject, producer and creator tags.Add patch to support NullObject and multiple xref streams, from Bradley
Lawrence.
Version 1.5, 2006-01-28
Fix a bug where merging pages did not work in "no-rename" cases when the
second page has an array of content streams.Remove some debugging output that should not have been present.
Version 1.4, 2006-01-27
Add capability to merge pages from multiple PDF files into a single page
using the PageObject.mergePage function. See example code (README or web
site) for more information.Add ability to modify a page's MediaBox, CropBox, BleedBox, TrimBox, and
ArtBox properties through PageObject. See example code (README or web site)
for more information.Refactor pdf.py into multiple files: generic.py (contains objects like
NameObject, DictionaryObject), filters.py (contains filter code),
utils.py (various). This does not affect importing PdfFileReader
or PdfFileWriter.Add new decoding functions for standard PDF filters ASCIIHexDecode and
ASCII85Decode.Change url and download_url to refer to new pybrary.net web site.
Version 1.3, 2006-01-23
Fix new bug introduced in 1.2 where PDF files with \r line endings did not
work properly anymore. A new test suite developed with various PDF files
should prevent regression bugs from now on.Fix a bug where inheriting attributes from page nodes did not work.
Version 1.2, 2006-01-23
Improved support for files with CRLF-based line endings, fixing a common
reported problem stating "assertion error: assert line == "%%EOF"".Software author/maintainer is now officially a proud married person, which
is sure to result in better software... somehow.
Version 1.1, 2006-01-18
Add capability to rotate pages.
Improved PDF reading support to properly manage inherited attributes from
/Type=/Pages nodes. This means that page groups that are rotated or have
different media boxes or whatever will now work properly.Added PDF 1.5 support. Namely cross-reference streams and object streams.
This release can mangle Adobe's PDFReference16.pdf successfully.
Version 1.0, 2006-01-17
First distutils-capable true public release. Supports a wide variety of PDF
files that I found sitting around on my system.Does not support some PDF 1.5 features, such as object streams,
cross-reference streams.
Meta Comparisons (`docs/meta/comparisons.md`)
pypdf vs X
pypdf is a free and open source pure-python PDF library capable of
splitting, merging, cropping, and transforming the pages of PDF files.
It can also add custom data, viewing options, and passwords to PDF
files. pypdf can retrieve text and metadata from PDFs as well.
PyMuPDF and PikePDF
PyMuPDF is a Python binding to MuPDF and PikePDF is the Python
binding to QPDF.
While both are excellent libraries for various use-cases, using them is
not always possible even when they support the use-case. Both of them
are powered by C libraries which make installation harder and might
cause security concerns. For MuPDF, you might also need to buy a
commercial license.
A core feature of pypdf is that it's pure Python. That means there is
no C dependency. It has been used for over 10 years and for this reason
a lot of support via StackOverflow and examples on the internet.
pypdf
PyPDF2 was merged back into pypdf. The development continues at pypdf.
PyPDF3 and PyPDF4
Developing and maintaining open source software is extremely
time-intensive and in the case of pypdf not paid at all. Having
continuous support is hard.
pypdf was initially released in 2012 on PyPI and received releases
until 2016. From 2016 to 2022, there was no update - but people were
still using it.
As pypdf is free software, there were attempts to fork it and continue
the development. PyPDF3 was first released in 2018 and still receives
updates. PyPDF4 has only one release from 2018.
Martin Thoma has worked on bringing the community back to one path of
development. He deprecated PyPDF2 in favor of pypdf already, and pypdf has
more features and a cleaner interface than PyPDF2 now. See history of
pypdf.
pdfminer.six and pdfplumber
pdfminer.six is capable of
extracting the font size
/ font weight (bold-ness). It has no capabilities for writing PDF files.
pdfplumber is a library focused on extracting data from PDF documents. Since pdfplumber is built on top of pdfminer.six, there are no capabilities of exporting or modifying a PDF file (see #440 (discussions)). However, pdfplumber is capable of converting a PDF file into an image, draw lines and rectangles on the image, and save it as an image file. Please note that the image conversion is done via ImageMagick (see pdfplumber's documentation).
The pdfplumber community is active in answering questions and the library is maintained as of May 2023.
pdfrw / pdfrw2
I don't have experience with any of those libraries. Please add a
comparison if you know pypdf and pdfrw!
Please be aware that there is alsopdfminer which is not maintained.
Then there is pdfrw2 which doesn't have
a large community behind it.
Document Generation
There are (Python) tools to generate PDF documents.
pypdf is not one of them.
CLI applications
pypdf is a pure Python PDF library. If you're looking for an application which
you can use from the terminal, give pdfly
a shot.
Meta Faq (`docs/meta/faq.md`)
Frequently Asked Questions
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.
Who uses pypdf?
pyPdf is vendored into several projects. That
means the code of pyPdf was copied into that project.
Projects that depend on pypdf:
- Camelot: A Python library to extract tabular data from PDFs
- edi: Electronic Data Interchange modules
- amazon-textract-textractor: Analyze documents with Amazon Textract and generate output in multiple formats.
- maigret: Collect a dossier on a person by username from thousands of sites
- deda: tracking Dots Extraction, Decoding and Anonymisation toolkit
- opencanary
- Document Conversions
- pdfalyzer: A PDF analysis tool for visualizing the inner tree-like data structure of a PDF in spectacularly large and colorful diagrams as well as scanning the binary streams embedded in the PDF for hidden potentially malicious content.
How do I cite pypdf?
In BibTeX format:
@misc{pypdf,
title = {The {pypdf} library},
author = {Mathieu Fenniak and
Matthew Stamy and
pubpub-zz and
Martin Thoma and
Matthew Peveler and
exiledkingcc and {pypdf Contributors}},
year = {2024},
url = {https://pypi.org/project/pypdf/}
note = {See https://pypdf.readthedocs.io/en/latest/meta/CONTRIBUTORS.html for all contributors}
}Which License does pypdf use?
pypdf uses the BSD-3-Clause license, see the LICENSE file.
Meta History (`docs/meta/history.md`)
History of pypdf
The Origins: pyPdf (2005-2010)
In 2005, Mathieu Fenniak launched pyPdf "as a PDF toolkit..."
focused on
- document manipulation: by-page splitting, concatenation, and
merging; - document introspection;
- page cropping; and
- document encryption and decryption.
The last release of PyPI was pyPdf 1.13
in 2010.
PyPDF2 is born (2011-2016)
At the end of 2011, after consultation with Mathieu and others, Phaseit
sponsored PyPDF2 as a fork of pyPdf on GitHub. The initial impetus was
to handle a wider range of input PDF instances; Phaseit's commercial
work often encounters PDF instances "in the wild" that it needs to
manage (mostly concatenate and paginate), but that deviates so much from
PDF standards that pyPdf can't read them. PyPDF2 reads a considerably
wider range of real-world PDF instances.
Neither pyPdf nor PyPDF2 aims to be universal, that is, to provide all
possible PDF-related functionality. Note that the similar-appearing
pyfpdf of Mariano Reingart is most comparable to ReportLab, in that
both ReportLab and pyfpdf emphasize document generation. Interestingly
enough, pyfpdf builds in a basic HTML→PDF converter while PyPDF2 has no
knowledge of HTML.
So what is PyPDF2 truly about? Think about popular pdftk for a moment.
PyPDF2 does what pdftk does, and it does so within your current Python
process, and it handles a wider range of variant PDF formats
[explain]. PyPDF2 has its own FAQ to answer other questions that have
arisen.
The Reddit /r/python crowd chatted obliquely and briefly about PyPDF2
in March 2012.
The core developer / maintainer was Matthew Stamy.
PyPDF3 and PyPDF4 (2018-2022)
Two approaches were made to get PyPDF2 active again: PyPDF3 and PyPDF4.
PyPDF3 had its first release in 2018 and its last one in February 2022.
It never got the user base from PyPDF2.
PyPDF4 only had one release in 2018.
PyPDF2: Reborn (2022)
Martin Thoma took over maintenance of PyPDF2 in April 2022. It had over 100
open PRs and 321 open issues.
pubpub-zz was extremely active, especially
for text extraction.
Matthew Peveler helped a lot with reviews
and general project decisions.
exiledkingcc added support for modern
encryption schemes.
pypdf: Back to the Roots (2023-2024)
In order to simplify things for beginners, PyPDF2 was merged back into
pypdf. Now all lowercase, without a number. We hope that the folks who
develop PyPDF3 and PyPDF4 also join us.
Compared to PyPDF2 >= 3.0.0, pypdf >= 3.1.0 now offers:
- 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
- Annotation support
- Performance Improvements and Bugfixes
- Page Label support
stefan6419846 made his first PR for pypdf
in July 2023 and joined the project.
Meta Migration 1 To 2 (`docs/meta/migration-1-to-2.md`)
Migration Guide: 1.x to 2.x
PyPDF2<2.0.0 (docs)
is very different from PyPDF2>=2.0.0 (docs).
Luckily, most changes are simple naming adjustments. This guide helps you to
make the step from PyPDF2 1.x (or even the original PyPdf) to PyPDF2>=2.0.0.
You can execute your code with the updated version and show deprecation warnings
by running python -W all your_code.py.
Imports and Modules
PyPDF2.utilsno longer existsPyPDF2.pdfno longer exists. You can import fromPyPDF2directly or fromPyPDF2.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 fromstrict=True to strict=False.
PdfFileReader➔PdfReaderPdfFileWriter➔PdfWriterPdfFileMerger➔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_rootxmp_createDate➔xmp_create_datexmp_creatorTool➔xmp_creator_toolxmp_metadataDate➔xmp_metadata_datexmp_modifyDate➔xmp_modify_datexmpMetadata➔xmp_metadataxmpmm_documentId➔xmpmm_document_idxmpmm_instanceId➔xmpmm_instance_id
In PyPDF2.generic:
readObject➔read_objectconvertToInt➔convert_to_intDocumentInformation.getText➔DocumentInformation._get_text: This method should typically not be used; please let me know if you need it.readHexStringFromStream➔read_hex_string_from_streaminitializeFromDictionary➔initialize_from_dictionarycreateStringObject➔create_string_objectTreeObject.hasChildren➔TreeObject.has_childrenTreeObject.emptyTree➔TreeObject.empty_tree
In many places:
getObject➔get_objectwriteToStream➔write_to_streamreadFromStream➔read_from_stream
PdfReader class:
reader.getPage(pageNumber)➔reader.pages[page_number]reader.getNumPages()/reader.numPages➔len(reader.pages)getDocumentInfo➔metadataflattenedPagesattribute ➔flattened_pagesresolvedObjectsattribute ➔resolved_objectsxrefIndexattribute ➔xref_indexgetNamedDestinations/namedDestinationsattribute ➔named_destinationsgetPageLayout/pageLayout➔page_layoutattributegetPageMode/pageMode➔page_modeattributegetIsEncrypted/isEncrypted➔is_encryptedattributegetOutlines➔get_outlinesreadObjectHeader➔read_object_headercacheGetIndirectObject➔cache_get_indirect_objectcacheIndirectObject➔cache_indirect_objectgetDestinationPageNumber➔get_destination_page_numberreadNextEndLine➔read_next_end_line_zeroXref➔_zero_xref_authenticateUserPassword➔_authenticate_user_password_pageId2Numattribute ➔_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_metadataaddPage➔add_pageaddBlankPage➔add_blank_pageaddAttachment(fname, fdata)➔add_attachment(filename, data)insertPage➔insert_pageinsertBlankPage➔insert_blank_pageappendPagesFromReader➔append_pages_from_readerupdatePageFormFieldValues➔update_page_form_field_valuescloneReaderDocumentRoot➔clone_reader_document_rootcloneDocumentFromReader➔clone_document_from_readergetReference➔get_referencegetOutlineRoot➔get_outline_rootgetNamedDestRoot➔get_named_dest_rootaddBookmarkDestination➔add_bookmark_destinationaddBookmarkDict➔add_bookmark_dictaddBookmark➔add_bookmarkaddNamedDestinationObject➔add_named_destination_objectaddNamedDestination➔add_named_destinationremoveLinks➔remove_linksremoveImages(ignoreByteStringObject)➔remove_images(ignore_byte_string_object)removeText(ignoreByteStringObject)➔remove_text(ignore_byte_string_object)addURI➔add_uriaddLink➔add_linkgetPage(pageNumber)➔get_page(page_number)getPageLayout / setPageLayout / pageLayout➔page_layout attributegetPageMode / setPageMode / pageMode➔page_mode attribute_addObject➔_add_object_addPage➔_add_page_sweepIndirectReferences➔_sweep_indirect_references
PdfMerger class
__init__parameter:strict=True➔strict=False(thePdfFileMergerstill has the old default)addMetadata➔add_metadataaddNamedDestination➔add_named_destinationsetPageLayout➔set_page_layoutsetPageMode➔set_page_mode
Page class:
artBox/bleedBox/cropBox/mediaBox/trimBox➔artbox/bleedbox/cropbox/mediabox/trimboxgetWidth,getHeight➔width/heightgetLowerLeft_x/getUpperLeft_x➔leftgetUpperRight_x/getLowerRight_x➔rightgetLowerLeft_y/getLowerRight_y➔bottomgetUpperRight_y/getUpperLeft_y➔topgetLowerLeft/setLowerLeft➔lower_leftpropertyupperRight➔upper_right
mergePage➔merge_pagerotateClockwise/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_multiplyRC4_encryptis moved to the security module
Parameter Names
PdfWriter.get_page:pageNumber➔page_numberPyPDF2.filters(all classes):decodeParms➔decode_parmsPyPDF2.filters(all classes):decodeStreamData➔decode_stream_datapagenum➔page_numberPdfMerger.merge:position➔page_numberPdfWriter.add_outline_item_destination:dest➔page_destinationPdfWriter.add_named_destination_object:dest➔page_destinationPdfWriter.encrypt:user_pwd➔user_passwordPdfWriter.encrypt:owner_pwd➔owner_password
Deprecations
A few classes / functions were deprecated without replacement:
PyPDF2.utils.ConvertFunctionsToVirtualListPyPDF2.utils.formatWarningPyPDF2.isInt(obj): Useinstance(obj, int)insteadPyPDF2.u_(s): UsesdirectlyPyPDF2.chr_(c): Usechr(c)insteadPyPDF2.barray(b): Usebytearray(b)insteadPyPDF2.isBytes(b): Useinstance(b, type(bytes()))insteadPyPDF2.xrange_fn: UserangeinsteadPyPDF2.string_type: UsestrinsteadPyPDF2.isString(s): Useinstance(s, str)insteadPyPDF2._basestring: Usestrinsteadb_(...)was removed. You should typically be able to use the bytes object directly, otherwise you can copy this
Meta Project Governance (`docs/meta/project-governance.md`)
Project Governance
This document describes how the pypdf project is managed. It describes the
different actors, their roles, and the responsibilities they have.
Terminology
- The project is pypdf - a free and open-source pure-python PDF library
capable of splitting, merging, cropping, and transforming the pages of PDF files.
It includes the code, issues, and discussions on GitHub,
and the documentation on ReadTheDocs,
the package on PyPI, and
the website on GitHub. - A maintainer is a person who has technical permissions to change one or
more parts of the projects. It is a person driven to keep the project running
and improving. - A contributor is a person who contributes to the project. That could be
through writing code - in the best case through forking and creating a pull
request, but that is up to the maintainer. Other contributors describe issues,
help to ask questions on existing issues to make them easier to answer,
participate in discussions, and help to improve the documentation. Contributors
are similar to maintainers, but without technical permissions. - A user is a person who imports pypdf into their code. All pypdf users
are developers, but not developers who know the internals of pypdf. They only
use the public interface of pypdf. They will likely have less knowledge about
PDF than contributors. - The community is all of that - the users, the contributors, and the maintainers.
Governance, Leadership, and Steering pypdf forward
pypdf is a free and open source project with over 100 contributors and likely
(way) more than 1000 users.
As pypdf does not have any formal relationship with any company and no funding,
all the work done by the community are voluntary contributions. People don't
get paid, but choose to spend their free time to create software of which
many more are profiting. This has to be honored and respected.
Despite such a big community, the project was dormant from 2016 to 2022.
There were still questions asked, issues reported, and pull requests created.
But the maintainer didn't have the time to move pypdf forward. During that
time, nobody else stepped up to become the new maintainer.
For this reason, pypdf has the Benevolent Dictator
governance model. The benevolent dictator is a maintainer with all technical permissions -
most importantly the permission to push new pypdf versions on PyPI.
Being benevolent, the benevolent dictator listens for decisions to the community and tries
their best to make decisions from which the overall community profits - the
current one and the potential future one. Being a dictator, the benevolent dictator always has
the power and the right to make decisions on their own - also against some
members of the community.
As pypdf is free software, parts of the community can split off (fork the code)
and create a new community. This should limit the harm a bad benevolent dictator can do.
Project Language
The project language is (american) English. All documentation and issues must
be written in English to ensure that the community can understand it.
We appreciate the fact that large parts of the community don't have English
as their native language. We try our best to understand others -
automatic translators might help.
Expectations
The community can expect the following:
- The benevolent dictator tries their best to make decisions from which the overall
community profits. The benevolent dictator is aware that his/her decisions can shape the
overall community. Once the benevolent dictator notices that she/he doesn't have the time
to advance pypdf, he/she looks for a new benevolent dictator. As it is expected
that the benevolent dictator will step down at some point of their choice
(hopefully before their death), it is NOT a benevolent dictator for life
(BDFL). - Every maintainer (including the benevolent dictator) is aware of their permissions and
the harm they could do. They value security and ensure that the project is
not harmed. They give their technical permissions back if they don't need them
any longer. Any long-time contributor can become a maintainer. Maintainers
can - and should! - step down from their role when they realize that they
can no longer commit that time. Their contribution will be honored in the
{doc}history. - Every contributor is aware that the time of maintainers and the benevolent dictator is
limited. Short pull requests that briefly describe the solved issue and have
a unit test have a higher chance to get merged soon - simply because it's
easier for maintainers to see that the contribution will not harm the overall
project. Their contributions are documented in the git history and in the
public issues. Let us know
if you would appreciate something else! - Every community member uses a respectful language. We are all human, we
get upset about things we care and other things than what's visible on the
internet go on in our live. pypdf does not pay its contributors - keep all
of that in mind when you interact with others. We are here because we want to
help others.
Issues and Discussions
An issue is any technical description that aims at bringing pypdf forward:
- Bugs tickets: Something went wrong because pypdf developers made a mistake.
- Feature requests: pypdf does not support all features of the PDF specifications.
There are certainly also convenience methods that would help users a lot. - Robustness requests: There are many broken PDFs around. In some cases, we can
deal with that. It's kind of a mixture between a bug ticket and a feature
request. - Performance tickets: pypdf could be faster - let us know about your specific
scenario.
Any comment that is in those technical descriptions which is not helping the
discussion can be deleted. This is especially true for "me too" comments on bugs
or "bump" comments for desired features. People can express this with 👍 / 👎
reactions.
Discussions are open. No comments
will be deleted there - except if they are unrelated spam or only
try to insult people (luckily, the community was very respectful so far 🤞)
Releases
The maintainers follow semantic versioning. Most
importantly, that means that breaking changes will have a major version bump.
Be aware that unintentional breaking changes might still happen. The pypdf
maintainers do their best to fix that in a timely manner - please
report such issues!
People
- stefan6419846 is the benevolent dictator since January 2025
- Martin Thoma was the benevolent dictator from April 2022 to January 2025.
He still has most of the permissions as a fallback. - Maintainers:
- Matthew Stamy (mstamy2) was the benevolent dictator for a long time.
He still is around on GitHub once in a while and has permissions on PyPI and GitHub. - Matthew Peveler (MasterOdin) is a maintainer on GitHub.
- Matthew Stamy (mstamy2) was the benevolent dictator for a long time.
Meta Scope Of Pypdf (`docs/meta/scope-of-pypdf.md`)
Scope of pypdf
What features should pypdf have and which features will it never have?
pypdf aims at simplifying interactions with PDF documents. Core tasks that
pypdf can perform are:
- Document manipulation: Splitting, merging, cropping, and transforming the pages of PDF files
- Data Extraction: Extract text and metadata from PDF documents
- Security: Decrypt / encrypt PDF documents
Typical indicators that pypdf should do something:
- The task needs in-depth knowledge of the PDF format
- It currently requires a lot of code or even is impossible to do with pypdf
- It's neither mentioned in "belongs in user code" nor in "out of scope"
- It already is in the issue list with the is-feature tag.
The moonshot extensions are
features we would like to have, but are currently not able to add (PRs are
welcome 😉)
Belongs in user code
Here are a few indicators that a feature belongs into users' code (and not into pypdf):
- The use-case is very specific. Most people will not encounter the same need.
- It can be done without knowledge of the PDF specification
- 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:
- 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. - Format Conversion: Converting docx / HTML to PDF or PDF to those formats.
You might want to have a look atpdfkit
and similar projects.
Out of scope for the moment, but might be added if there are enough contributors:
- Digital Signature Support (reference ticket): Cryptography is
complicated. It's important to get it right. pypdf currently doesn't have
enough active contributors to properly add digital signature support. For the
moment, pyhanko seems to be the best choice. - PDF Generation from Scratch: pypdf can manipulate existing PDF documents,
add annotations, combine / split / crop / transform. It can add blank pages.
But if you want to generate invoices, you might want to have a look atreportlab/fpdf2or document conversion tools likepdfkit. - Replacing words within a PDF: Extracting text from PDF is hard.
Replacing text in a reliable way is even harder. For example, one word might
be split into multiple tokens. Hence, it's not a simple "search and replace"
in some cases. - (Not) Extracting headers/footers/page numbers: While you can apply
heuristics, there is no way to always make it work. PDF documents simply
don't contain the information what a header/footer/page number is.
Library vs. Application
It's also worth pointing out that pypdf is designed to be a library. It is not
an application. That has several implications:
- Execution: pypdf cannot be executed directly, but only be called from within
a program written by a pypdf user. In contrast, an application is executed
on its own. - Dependencies: pypdf should have a minimal set of dependencies and only
restrict them where it is strictly necessary. In contrast, applications should
be installed in environments which are isolated from other applications. They
can pin their dependencies.
If you're looking for a way to interact with PDF files via Shell, you should
either write a script using pypdf or use pdfly.
Meta Taking Ownership (`docs/meta/taking-ownership.md`)
Taking Ownership of pypdf
pypdf is currently maintained by stefan6419846. We want to avoid that
pypdf ever goes unmaintained again. This document serves as a guide to avoid
that if I become unavailable, e.g., due to severe health issues.
This currently is just an abstract scenario. I'm fine, and I will likely do this
for several more years, but I have seen how projects stand still for many years
because of the maintainer becoming inactive.
What belongs to pypdf?
The resources needed for maintaining pypdf are:
- PyPI: pypdf and PyPDF2
- GitHub: pypdf (the repository, not the organization)
- ReadTheDocs: pypdf and 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: He maintains fpdf2 and is a py-pdf owner
- pubpub-zz: He is one of the most active contributors
to pypdf - Matthew Peveler: Less active, but he is cautious
about breaking changes and an experienced software developer. - exiledkingcc: He has contributed the core
changes related to encryption.
How to take ownership?
- PyPI: Follow PEP 541 – Package Index Name Retention
- GitHub: Talk with one of the other py-pdf organization owners
- ReadTheDocs: Follow the Abandoned projects policy
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
Launch print window on opening
pypdf_test_setup("user/add-javascript", {
"example.pdf": "../resources/example.pdf",
})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:
pypdf_test_setup("user/add-watermark", {
"crazyones.pdf": "../resources/crazyones.pdf",
"nup-source.png": "../docs/user/nup-source.png",
"jpeg.pdf": "../resources/jpeg.pdf",
})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.
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.
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
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
pypdf_test_setup("user/adding-pdf-annotations", {
"crazyones.pdf": "../resources/crazyones.pdf",
})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:
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:
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:
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:
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:
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:
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:
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:
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:
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:
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
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.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",
})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:
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
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
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:
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
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
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.
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:
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:
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:
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).
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 andAES algorithms
with different key length. pypdf supports all of them until PDF-2.0, which
is the latest PDF standard.
pypdf use an extra dependency to do encryption or decryption for AES algorithms.
We recommend pyca/cryptography. Alternatively,
you can use pycryptodome.
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:
pypdf_test_setup("user/encryption-decryption", {
"example.pdf": "../resources/example.pdf",
"encrypted-file.pdf": "../resources/encrypted-file.pdf",
})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.
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:
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
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.
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.pypdf_test_setup("user/extract-images", {
"example.pdf": "../resources/example.pdf",
})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:
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:
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.
passUser Extract Text (`docs/user/extract-text.md`)
Extract Text from a PDF
You can extract text from a PDF:
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",
})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)):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 topRefer to {func}~pypdf._page.PageObject.extract_text for more details.
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.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.multfunction as follows:txt2user = mult(tm, cm).
The font size is the raw text size and affected by theuser_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, but ignores the header (y > 720) and footer (y < 50). In this file we also need to include new line characters (y == 0).
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 into
an SVG file.
Such an SVG export may help to understand what is going on in a page.
% We prefer not to execute doc examples for unmaintained third-party package "svgwrite"
: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:
- 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? - Page numbers: Should they be included in the extract?
- Headers and Footers: Similar to page numbers - should they be extracted?
- Outlines: Should outlines be extracted at all?
- Formatting: If the text is bold or italic, should it be included in the
output? - 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? - Captions: Should image and table captions be included?
- Ligatures: The Unicode symbol U+FB00
is a single symbol ff for two lowercase letters 'f'. Should that be parsed as
the Unicode symbol 'ff' or as two ASCII symbols 'ff'? - SVG images: Should the text parts be extracted?
- Mathematical Formulas: Should they be extracted? Formulas have indices
and nested fractions. - 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? - Footnotes: When the text of multiple pages is extracted, where should footnotes be shown?
- Hyperlinks and Metadata: Should it be extracted at all? Where should it
be placed in which format? - 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:
- 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. - 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
- Negative numbers in PDF content stream text object
- Mark Stephens: 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 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:
- Store the contents of the PDF as an image
- Use a scrambled font
However, text extraction cannot be completely prevented if people should still
be able to read the document. In the worst case, people can make a screenshot,
print it, scan it, and run OCR over it.
User File Size (`docs/user/file-size.md`)
Reduce PDF File Size
There are multiple ways to reduce the size of a given PDF file. The easiest
one is to remove content (e.g., images) or pages.
Removing duplication
Some PDF documents contain the same object multiple times. For example, if an
image appears three times in a PDF, it could be embedded three times. Or it can
be embedded once and referenced twice.
When adding data to a PdfWriter, the data is copied while respecting the original format.
For example, if two pages include the same image which is duplicated in the source document, the object will be duplicated in the PdfWriter object.
Additionally, when you delete objects in a document, pypdf cannot easily identify whether the objects are used elsewhere or not or if the user wants to keep them in. When writing the PDF file, these objects will be hidden within (part of the file, but not displayed).
To reduce the file size, use a compression call: writer.compress_identical_objects(remove_duplicates=True, remove_unreferenced=True)
remove_duplicatesenables/disables compression merging identical objects.remove_unreferencedenables/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
pypdf_test_setup("user/file-size", {
"example.pdf": "../resources/example.pdf",
})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.
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 :
from pypdf import PdfWriter
writer = PdfWriter(clone_from="example.pdf")
for page in writer.pages:
page.compress_content_streams() # This is CPU intensive!
writer.write("out-lossless.pdf")page.compress_content_streams uses zlib.compress
and supports the level parameter: level=0 means no compression,level=9 refers to the highest compression.
Using this method, we have seen a reduction by 70% (from 11.8 MB to 3.5 MB)
with a real PDF.
Removing Sources
When a page is removed from the page list, its content will still be present in
the PDF file. This means that the data may still be used elsewhere.
Simply removing a page from the page list will reduce the page count but not the
file size. To exclude the content completely, the pages should not be
added to the PDF using the PdfWriter.append() function. Instead, only the
desired pages should be selected for inclusion
(note: PR #1843 will add a page
deletion feature).
There can be issues with poor PDF formatting, such as when all pages are linked
to the same resource. In such cases, dropping references to specific pages
becomes useless because there is only one source for all pages.
Cropping is an ineffective method for reducing the file size because it only
adjusts the viewboxes and not the external parts of the source image. Therefore,
the content that is no longer visible will still be present in the PDF.
Going Further
The presentation Putting a Squeeze on Your PDF has other suggestions. One takeaway is that most of the significant size optimizations usually come from image and font modification. However, font optimization, such as replacing, merging, and subsetting, is not within the functionality of pypdf at the moment.
User Forms (`docs/user/forms.md`)
Interactions with PDF Forms
Reading form fields
pypdf_test_setup("user/forms", {
"form.pdf": "../resources/form.pdf",
})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
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
/AcroFormstructure exists.
Inside it, you could find (optional):- some global elements (Fonts, Resources,...)
- some global flags (like
/NeedAppearances(set/cleared withauto_regenerateparameter inupdate_page_form_field_values()) that indicates if the reading program should re-render the visual fields upon document launch) /XFAthat houses a form in XDP format (very specific XML that describes the form rendered by some viewers); the/XFAform overrides the page content/Fieldsthat houses an array of indirect references that reference the upper Field Objects (roots)
Within the page
/Annots, you will spot/Widgetannotations 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
/Parentwill have an IndirectObject providing Bottom-Up links and/Kidsis 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:
from pypdf import PdfReader
reader = PdfReader("form.pdf")
fields = reader.get_fields()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).
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.
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:
pypdf_test_setup("user/handle-attachments", {
"example.pdf": "../resources/example.pdf",
})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:
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:
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:
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:
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:
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.
pypdf_test_setup("user/handling-outlines", {
"crazyones.pdf":"../resources/crazyones.pdf",
})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.
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.
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"):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.
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}")Simple Outline (Top-Level Only):
--------------------------------
Introduction -> page 1Reading 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.
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)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:
pip install pypdfIf you are not a superuser (a system administrator / root), you can also just
install pypdf for your current user:
pip install --user pypdfOptional 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 The installation procedure
depends on our operating system. For Ubuntu, use the following, for example:
sudo apt-get install jbig2decPython Version Support
Since pypdf 4.0, every release, including point releases, should work with all
supported versions of Python. Thus,
every point release is designed to work with all existing Python versions,
excluding end-of-life versions.
Previous versions of pypdf support the following versions of Python:
| Python | 3.11 | 3.10 | 3.9 | 3.8 | 3.7 | 3.6 | 2.7 |
|---|---|---|---|---|---|---|---|
| pypdf 3.x | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| PyPDF2 >= 2.0 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| PyPDF2 1.20.0 - 1.28.4 | ❌ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| PyPDF2 1.15.0 - 1.20.0 | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
Anaconda
Anaconda users can install pypdf via conda-forge.
Development Version
In case you want to use the current version under development:
pip install git+https://github.com/py-pdf/pypdf.gitUser Merging Pdfs (`docs/user/merging-pdfs.md`)
Merging PDF files
Basic Example
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",
})from pypdf import PdfWriter
merger = PdfWriter()
for pdf in ["example.pdf", "hello-world.pdf", "jpeg.pdf"]:
merger.append(pdf)
merger.write("out-basic.pdf")For more details, see an excellent answer on
StackOverflow
by Paul Rooney.
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
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
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:
# 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 callingreader.get_form_text_fields(True) or reader.get_fields().
After that, you can append the input PDF completely or partially usingwriter.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:
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:
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:
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:
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:
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
pypdf_test_setup("user/metadata", {
"example.pdf": "../resources/example.pdf",
"commented-xmp.pdf": "../resources/commented-xmp.pdf",
})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
:hide:
PDF Example Document
None
None
None
Skia/PDF m103 Google Docs Renderer
...
...Writing metadata
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
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
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
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):hide:
{'x-default': 'PDF Example Document'}
{}
2025-10-30 09:29:55Creating XMP metadata
You can create XMP metadata easily using the XmpInformation.create() method:
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
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
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
xmp.pdf_keywords = "keyword1, keyword2, keyword3"
xmp.pdf_pdfversion = "1.4"
xmp.pdf_producer = "pypdf"XMP Media Management fields
xmp.xmpmm_document_id = "uuid:12345678-1234-1234-1234-123456789abc"
xmp.xmpmm_instance_id = "uuid:87654321-4321-4321-4321-cba987654321"PDF/A fields
xmp.pdfaid_part = "1"
xmp.pdfaid_conformance = "B"Clearing metadata fields
You can clear any field by assigning None:
xmp.dc_title = None
xmp.dc_creator = None
xmp.pdf_producer = NoneIncrementally 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:
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 = creatorsThis 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:
<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:
from pypdf import PdfWriter
writer = PdfWriter(clone_from="commented-xmp.pdf")
metadata = writer.xmp_metadata
assert metadata # Ensure that it is not `None`.
rdf_root = metadata.rdf_root
xmp_meta = rdf_root.parentNode
xmp_document = xmp_meta.parentNode
# Please note that without a text node, the corresponding elements might
# be omitted completely.
pdfuaid_description = xmp_document.createElement("rdf:Description")
pdfuaid_description.setAttribute("rdf:about", "")
pdfuaid_description.setAttribute("xmlns:pdfuaid", "http://www.aiim.org/pdfua/ns/id/")
pdfuaid_part = xmp_document.createElement("pdfuaid:part")
pdfuaid_part_text = xmp_document.createTextNode("1")
pdfuaid_part.appendChild(pdfuaid_part_text)
pdfuaid_description.appendChild(pdfuaid_part)
rdf_root.appendChild(pdfuaid_description)
metadata.stream.set_data(xmp_document.toxml().encode("utf-8"))
writer.write("out-xmp-update.pdf")For further details on modifying the structure, please refer to {py:mod}xml.dom.minidom.
User Pdf Version Support (`docs/user/pdf-version-support.md`)
PDF Version Support
PDF comes in the following versions:
- 1993: 1.0
- 1994: 1.1
- 1996: 1.2
- 1999: 1.3
- 2001: 1.4
- 2003: 1.5
- 2004: 1.6
- 2008: 1.7, ISO 32000-1:2008
- 2017: 2.0, ISO 32000-2:2017
The general format didn't change, but new features got added. It can be that
pypdf can do the operations you want on PDF 2.0 files without fully supporting
all features of PDF 2.0.
PDF Feature Support by pypdf
| Feature | PDF Version | pypdf Support |
|---|---|---|
| CMaps | 1.4 | ✅ |
| Transparent Graphics | 1.4 | ✅ |
| Content Stream Compression | 1.5 | ✅ |
| Cross-reference Streams | 1.5 | ✅ |
| Object Streams | 1.5 | ✅ |
| Optional Content Groups (OCGs) | 1.5 | ❓ |
| AES Encryption | 1.6 | ✅ |
This table is not complete - if in doubt, consider having a look at the API documentation or
inside the issues or try with a corresponding PDF file. In general, we are open to
add support for missing features. Please open a new issue if it does not exist yet, and
keep in mind that we rely on external contributors to support us with the implementation.
One commonly requested feature is proper support reading/handling incremental PDF files, see
issue #3304.
See History of PDF for more
features.
Some PDF features are not supported by pypdf, but other libraries can be used
for them:
- pyHanko: Cryptographically sign a PDF (#302)
- camelot-py: Table Extraction (#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
- Level B (Basic): Like PDF/A-1b, but support for PDF 1.7 features such
- PDF/A-3: Based on PDF 1.7 (ISO 32000-1), PDF/A-3 is similar to PDF/A-2 but
allows the embedding of non-PDF/A files as attachments, enabling the archiving
of source or supplementary data alongside the PDF/A document. This is
interesting for invoices which can add XML files. - PDF/A-4: Based on PDF 2.0 (ISO 32000-2), PDF/A-4 introduces new features
and improvements for better archiving and accessibility. The previous levels
are replaced by PDF/A-4f (ensuring visual preservation and allowing attachments)
and PDF/A-4e (Engineering, allows 3D content).
PDF/A-1b
In contrast to other PDF documents, PDF/A-1b documents must fulfill those
requirements:
- MarkInfo Object: The MarkInfo object is a dictionary object within a PDF/A
file that provides information about the logical structure and tagging of the
document. The MarkInfo object indicates whether the document is tagged,
contains optional content, or has a structure tree that describes the logical
arrangement of content such as headings, paragraphs, lists, and tables. By
including the MarkInfo object, PDF/A ensures that electronic documents are
accessible to users with disabilities, such as those using screen readers or
other assistive technologies. - Embedded fonts: All fonts used in the document must be embedded to ensure
consistent text rendering across different devices and systems. - Color Spaces: DeviceRGB is a device-dependent color space that relies on
the specific characteristics of the output device, which can lead to
inconsistent color rendering across various devices. To achieve accurate and
consistent color representation, PDF/A requires the use of device-independent
color spaces, such as ICC-based color profiles. - XMP (Extensible Metadata Platform) metadata: XMP metadata provides a
standardized and extensible way to store essential information about a
document and its properties. XMP metadata is an XML-based format embedded
directly within a PDF/A file. It contains various types of information, such
as document title, author, creation and modification dates, keywords, and
copyright information, as well as PDF/A-specific details like conformance
level and OutputIntent.
Validation
VeraPDF is the go-to PDF/A validator.
There are several online validators that allow you to simply upload the document:
- pdfen.com
- avepdf.com : Gives an error report
- pdfa.org
- visual-paradigm.com - can convert the PDF to a PDF/A
- pdf2go.com
- slub-dresden.de 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.
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
def replace_ligatures(text: str) -> str:
ligatures = {
"ff": "ff",
"fi": "fi",
"fl": "fl",
"ffi": "ffi",
"ffl": "ffl",
"ſt": "ft",
"st": "st",
# "Ꜳ": "AA",
# "Æ": "AE",
"ꜳ": "aa",
}
for search, replace in ligatures.items():
text = text.replace(search, replace)
return textDehyphenation
Hyphens are used to break words up so that the appearance of the page is nicer.
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 linesHeader/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.
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_textsOther ideas
- Whitespaces in units: Between a number and its unit should be a space.
(source).
That means: 42 ms, 42 GHz, 42 GB. - Percent: English style guides prescribe writing the percent sign following the number without any space between (e.g., 50%).
- Whitespaces before dots: Should typically be removed.
- Whitespaces after dots: Should typically be added.
User Reading Pdf Annotations (`docs/user/reading-pdf-annotations.md`)
Reading PDF Annotations
PDF 2.0 defines the following annotation types:
- Text
- Link
- FreeText
- Line
- Square
- Circle
- Polygon
- PolyLine
- Highlight
- Underline
- Squiggly
- StrikeOut
- Caret
- Stamp
- Ink
- Popup
- FileAttachment
- Sound
- Movie
- Screen
- Widget
- PrinterMark
- TrapNet
- Watermark
- 3D
- Redact
- Projection
- RichMedia
In general, annotations can be read like this:
pypdf_test_setup("user/reading-pdf-annotations", {
"example.pdf": "../resources/example.pdf",
})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"]}):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
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"]):hide:
Text commentHighlights
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 = coordsAttachments
from pypdf import PdfReader
reader = PdfReader("example.pdf")
attachments = {}
for page in reader.pages:
if "/Annots" in page:
for annotation in page["/Annots"]:
subtype = annotation.get_object()["/Subtype"]
if subtype == "/FileAttachment":
fileobj = annotation.get_object()["/FS"]
attachments[fileobj["/F"]] = fileobj["/EF"]["/F"].get_data()User Robustness (`docs/user/robustness.md`)
Robustness and strict=False
PDF is specified in various versions.
The specification of PDF 2.0 has 1003 pages. This length makes it hard to get
everything right. As a consequence, a lot of PDF files are not strictly following the
specification.
If a PDF file does not follow the specification, it is not always possible to
be certain what the intended effect would be. Think of the following broken
Python code as an example:
# Broken
function (foo, bar):
# Potentially intended:
def function(foo, bar):
...
# Also possible:
function = (foo, bar):hide:
Traceback (most recent call last):
...
SyntaxError: invalid syntaxWriting 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.
pypdf_test_setup("user/security", {
"example.pdf": "../resources/example.pdf",
})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.
passReading
pypdf currently employs the following reading limits on PdfReader instances:
root_object_recovery_limitlimits the number of objects to read before stopping with Root object recovery in
non-strict mode. It defaults to 10 000. Setting it toNonewill 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 likePdfWriter(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_limitlimits the number of objects to read during cloning. It defaults to
500 000. Setting it toNonewill fully disable this limit.incremental_clone_object_id_limitlimits the maximum object ID to read during cloning. It defaults to
1 000 000. Setting it toNonewill fully disable this limit.
Reporting possible vulnerabilities
Please refer to our security policy.
Invalid reports
Exceptions
Most exceptions raised by our code are considered bugs or robustness issues and can be reported publicly.
We consider it the task of the library user to catch exceptions which could cause their service to crash, although we try to
only raise a known set of exception types.
Cryptographic functions
We receive reports about possibly insecure cryptography from time to time. This includes the following aspects:
- Using the ARC4 cipher
- Using the AES cipher in ECB mode
- Using MD5 for hashing
These are requirements of the PDF standard, which we need to achieve the greatest compatibility with.
Although some of them might be deprecated in PDF 2.0, the PDF 2.0 adoption rate is very low and legacy documents need to be supported.
XML parsing
We use xml.minidom for parsing XMP information. Given recent Python versions built against recent Expat versions, the usual attacks
(exponential entity expansion and external entity expansion) should not be possible. We have corresponding tests in place to ensure
this for the platforms our tests run against.
For some details, see the official documentation and the
README for defusedxml.
Please note that automated scanners tend to still flag any direct imports of XML modules from the Python standard library as unsafe.
There have been discussions about this being outdated already, but they are still being flagged.
User Streaming Data (`docs/user/streaming-data.md`)
Streaming Data with pypdf
In some cases, you might want to avoid saving things explicitly as a file
to disk, e.g. when you want to store the PDF in a database or AWS S3.
pypdf supports streaming data to a file-like object:
pypdf_test_setup("user/streaming-data", {
"example.pdf": "../resources/example.pdf",
})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
: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
: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
: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 thestrict=Truemode, 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 thewarningsmodule - those are different from the log-level
"warning." - Log messages are informative messages that can be used for post-mortem
analysis. Most of the time, users can ignore them. They come in different
levels, such as info / warning / error indicating the severity.
Examples are non-standard compliant PDF files which pypdf can deal with or
a missing implementation that leads to a part of the text not being extracted.
Exceptions
Exceptions need to be caught if you want to handle them. For example, you could
want to read the text from a PDF as a part of a search function.
Most PDF files do not follow the specification. In this case, pypdf needs to
guess which kinds of mistakes were potentially done when the PDF file was created.
See the robustness page for the related issues.
As a user, you likely do not care about it. If it is readable in any way, you
want the text. You might use pdfminer.six as a fallback and do this:
% We prefer not to execute doc examples for third-party package "pdfminer.six" used in one code snippet only
:skipif: True
from pypdf import PdfReader
from pdfminer.high_level import extract_text as fallback_text_extraction
text = ""
try:
reader = PdfReader("example.pdf")
for page in reader.pages:
text += page.extract_text()
except Exception as exc:
text = fallback_text_extraction("example.pdf")You could also capture pypdf.errors.PyPdfError
if you prefer something more specific.
Warnings
The warnings module allows
you to ignore warnings:
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:
import logging
logger = logging.getLogger("pypdf")
logger.setLevel(logging.ERROR)The logging module
defines six log levels:
- CRITICAL
- ERROR
- WARNING
- INFO
- DEBUG
- NOTSET
User Viewer Preferences (`docs/user/viewer-preferences.md`)
Adding Viewer Preferences
It is possible to set viewer preferences of a PDF file.
§12.2 of the PDF 1.7 specification.
Note that the /ViewerPreferences dictionary does not exist by default.
If it is not already present, it must be created by calling the
{func}~pypdf.PdfWriter.create_viewer_preferences method.
If viewer preferences exist in a PDF file being read with {class}~pypdf.PdfReader,
you can access them as properties of {attr}~pypdf.PdfReader.viewer_preferences.
Otherwise, the {attr}~pypdf.PdfReader.viewer_preferences property will be set to None.
Example
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