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 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.
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 add_outline_item() API documentation.
The fit parameter determines how the page is displayed when the user clicks the bookmark. You can use the Fit helper to specify modes like fit(), fit_horizontally(), or 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")
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 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 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 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 1
Reading Nested Outlines
When dealing with hierarchical bookmarks, the 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)